~ubuntu-branches/ubuntu/quantal/agtl/quantal

1.1.5 by Heiko Stuebner
Import upstream version 0.8.0.3
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3
4
#   Copyright (C) 2010 Daniel Fett
5
#   This program is free software: you can redistribute it and/or modify
6
#   it under the terms of the GNU General Public License as published by
7
#   the Free Software Foundation, either version 3 of the License, or
8
#   (at your option) any later version.
9
#
10
#   This program is distributed in the hope that it will be useful,
11
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
12
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
#   GNU General Public License for more details.
14
#
15
#   You should have received a copy of the GNU General Public License
16
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
#
18
#   Author: Daniel Fett agtl@danielfett.de
19
#   Jabber: fett.daniel@jaber.ccc.de
20
#   Bugtracker and GIT Repository: http://github.com/webhamster/advancedcaching
21
#
22
23
24
import openstreetmap
25
26
import logging
27
logger = logging.getLogger('abstractmap')
28
import geo
29
import math
30
31
32
33
34
class AbstractMap():
35
    MAP_FACTOR = 0
36
    RADIUS_EARTH = 6371000.0
37
38
    CLICK_MAX_RADIUS = 7
39
    CLICK_CHECK_RADIUS = 17
40
41
    @classmethod
42
    def set_config(Map, map_providers, map_path, placeholder_cantload, placeholder_loading):
43
44
        Map.noimage_cantload = Map._load_tile(placeholder_cantload)
45
        Map.noimage_loading = Map._load_tile(placeholder_loading)
46
        Map.tile_loaders = []
47
48
        for name, params in map_providers:
49
            tl = openstreetmap.get_tile_loader( ** dict([(str(a), b) for a, b in params.items()]))
50
            tl.noimage_loading = Map.noimage_loading
51
            tl.noimage_cantload = Map.noimage_cantload
52
            tl.base_dir = map_path
53
            #tl.gui = self
54
            Map.tile_loaders.append((name, tl))
55
56
    def __init__(self, center, zoom, tile_loader = None):
57
        self.active_tile_loaders = []
58
        self.double_size = False
59
        self.layers = []
60
        self.osd_message = None
61
62
        if tile_loader == None:
63
            self.tile_loader = self.tile_loaders[0][1]
64
        else:
65
            self.tile_loader = tile_loader
66
        self.dragging = False
67
        self.drag_offset_x = 0
68
        self.drag_offset_y = 0
69
        self.zoom = zoom
70
        self.total_map_width = 256 * 2 ** zoom
71
        self.set_center(center, False)
72
        #self.set_zoom(zoom)
73
74
        ##############################################
75
        #
76
        # Controlling the layers
77
        #
78
        ##############################################
79
80
    def add_layer(self, layer):
81
        self.layers.append(layer)
82
        layer.attach(self)
83
84
85
    def set_osd_message(self, message):
86
        self.osd_message = message
87
88
        ##############################################
89
        #
90
        # Controlling the map view
91
        #
92
        ##############################################
93
94
    def set_center(self, coord, update = True):
95
        if self.dragging:
96
            return
97
        self.map_center_x, self.map_center_y = self.deg2num(coord)
98
        self.center_latlon = coord
99
        self.draw_at_x = 0
100
        self.draw_at_y = 0
101
        if update:
102
            self._draw_map()
103
104
    def set_center_lazy(self, coord):
105
        if self.dragging:
106
            return
107
        old_center_x, old_center_y = self.coord2point(self.center_latlon)
108
        new_center_x, new_center_y = self.coord2point(coord)
109
110
        if abs(old_center_x - new_center_x) > \
111
            self.map_width * self.LAZY_SET_CENTER_DIFFERENCE or \
112
            abs(old_center_y - new_center_y) > \
113
            self.map_height * self.LAZY_SET_CENTER_DIFFERENCE:
114
            self.set_center(coord)
115
            logger.debug('Not lazy!')
116
            return True
117
        logger.debug('Lazy!')
118
        return False
119
120
121
    def get_center(self):
122
        return self.center_latlon
123
124
    def relative_zoom(self, direction=None, update=True):
125
        if direction != None:
126
            self.set_zoom(self.zoom + direction, update)
127
128
    def relative_zoom_preserve_center_at(self, screenpoint, direction):
129
        offs = screenpoint[0] - self.map_width/2.0, screenpoint[1] - self.map_height/2.0
130
        self.set_center(self.screenpoint2coord(screenpoint), False)
131
        self.relative_zoom(direction, False)
132
        self._move_map_relative(-offs[0], -offs[1])
133
134
135
    def set_zoom(self, newzoom, update = True):
136
        if newzoom < 0 or newzoom > self.tile_loader.MAX_ZOOM:
137
            return
138
        logger.debug('New zoom level: %d' % newzoom)
139
        self.zoom = newzoom
140
        self.total_map_width = (256 * 2**self.zoom)
141
        self.set_center(self.center_latlon, update)
142
143
    def get_zoom(self):
144
        return self.zoom
145
146
    def get_max_zoom(self):
147
        return self.tile_loader.MAX_ZOOM
148
149
    def get_min_zoom(self):
150
        return 0
151
152
    def _move_map_relative(self, offset_x, offset_y, update = True):
153
        self.map_center_x += (float(offset_x) / self.tile_loader.TILE_SIZE)
154
        self.map_center_y += (float(offset_y) / self.tile_loader.TILE_SIZE)
155
        self.map_center_x, self.map_center_y = self.check_bounds(self.map_center_x, self.map_center_y)
156
        self.center_latlon = self.num2deg(self.map_center_x, self.map_center_y)
157
        if update:
158
            self._draw_map()
159
160
161
    def fit_to_bounds(self, minlat, maxlat, minlon, maxlon):
162
        if minlat == maxlat and minlon == maxlon:
163
            self.set_center(geo.Coordinate(minlat, minlon))
164
            self.set_zoom(self.get_max_zoom())
165
            return
166
        logger.debug("Settings Bounds: lat(%f, %f) lon(%f, %f)" % (minlat, maxlat, minlon, maxlon))
167
        req_deg_per_pix_lat = (maxlat - minlat) / self.map_height
168
        prop_zoom_lat = math.log(((180.0/req_deg_per_pix_lat) / self.tile_loader.TILE_SIZE), 2)
169
170
        req_deg_per_pix_lon = (maxlon - minlon) / self.map_width
171
        prop_zoom_lon = math.log(((360.0/req_deg_per_pix_lon) / self.tile_loader.TILE_SIZE), 2)
172
173
        target_zoom = math.floor(min(prop_zoom_lat, prop_zoom_lon))
174
        logger.debug("Proposed zoom lat: %f, proposed zoom lon: %f, target: %f" %(prop_zoom_lat, prop_zoom_lon, target_zoom))
175
        
176
        center = geo.Coordinate((maxlat + minlat) / 2.0, (maxlon + minlon) / 2.0)
177
        logger.debug("New Center: %s" % center)
178
179
        self.set_center(center, False)
180
        self.set_zoom(max(min(target_zoom, self.get_max_zoom()), self.get_min_zoom()))
181
182
183
        ##############################################
184
        #
185
        # Configuration
186
        #
187
        ##############################################
188
189
    def set_double_size(self, ds):
190
        self.double_size = ds
191
192
    def get_double_size(self):
193
        return self.double_size
194
195
    def set_tile_loader(self, loader):
196
        self.tile_loader = loader
197
        self.emit('tile-loader-changed', loader)
198
        self.relative_zoom(0)
199
200
    def set_placeholder_images(self, cantload, loading):
201
        self.noimage_cantload = self._load_tile(cantload)
202
        self.noimage_loading = self._load_tile(loading)
203
204
        ##############################################
205
        #
206
        # Coordinate Conversion and Checking
207
        #
208
        ##############################################
209
210
    def point_in_screen(self, point):
211
        a = (point[0] >= 0 and point[1] >= 0 and point[0] < self.map_width and point[1] < self.map_height)
212
        return a
213
214
    def coord2point(self, coord):
215
        point = self.deg2num(coord)
216
        size = self.tile_loader.TILE_SIZE
217
        p_x = int(point[0] * size + self.map_width / 2) - self.map_center_x * size
218
        p_y = int(point[1] * size + self.map_height / 2) - self.map_center_y * size
219
        return (p_x % self.total_map_width , p_y)
220
221
    def coord2point_float(self, coord):
222
        point = self.deg2num(coord)
223
        size = self.tile_loader.TILE_SIZE
224
        p_x = point[0] * size + self.map_width / 2 - self.map_center_x * size
225
        p_y = point[1] * size + self.map_height / 2 - self.map_center_y * size
226
        return (p_x % self.total_map_width , p_y)
227
228
    def screenpoint2coord(self, point):
229
        size = self.tile_loader.TILE_SIZE
230
        coord = self.num2deg(\
231
                                ((point[0] - self.draw_at_x) + self.map_center_x * size - self.map_width / 2) / size, \
232
                                ((point[1] - self.draw_at_y) + self.map_center_y * size - self.map_height / 2) / size \
233
                                )
234
        return coord
235
236
    def get_visible_area(self):
237
        a = self.screenpoint2coord((0, 0))
238
        b = self.screenpoint2coord((self.map_width, self.map_height))
239
        return geo.Coordinate(min(a.lat, b.lat), min(a.lon, b.lon)), geo.Coordinate(max(a.lat, b.lat), max(a.lon, b.lon))
240
241
    @staticmethod
242
    def in_area(coord, area):
243
        return (coord.lat > area[0].lat and coord.lat < area[1].lat and coord.lon > area[0].lon and coord.lon < area[1].lon)
244
245
    def _check_click(self, offset_x, offset_y, ev_x, ev_y):
246
        if offset_x ** 2 + offset_y ** 2 < self.CLICK_MAX_RADIUS ** 2:
247
248
            c = self.screenpoint2coord((ev_x, ev_y))
249
            c1 = self.screenpoint2coord([ev_x - self.CLICK_CHECK_RADIUS, ev_y - self.CLICK_CHECK_RADIUS])
250
            c2 = self.screenpoint2coord([ev_x + self.CLICK_CHECK_RADIUS, ev_y + self.CLICK_CHECK_RADIUS])
251
            for l in reversed(self.layers):
252
                if l.clicked_screen((ev_x, ev_y)) == False:
253
                    break
254
                if l.clicked_coordinate(c, c1, c2) == False:
255
                    break
256
            return True
257
        return False
258
259
        ##############################################
260
        #
261
        # Tile Number calculations
262
        #
263
        ##############################################
264
    def tile_size(self):
265
        return self.tile_loader.TILE_SIZE
266
267
    def get_meters_per_pixel(self, lat):
268
        return math.cos(lat * math.pi / 180.0) * 2.0 * math.pi * self.RADIUS_EARTH / self.total_map_width
269
270
    def deg2tilenum(self, lat_deg, lon_deg):
271
        lat_rad = math.radians(lat_deg)
272
        n = 2 ** self.zoom
273
        xtile = int((lon_deg + 180) / 360 * n)
274
        ytile = int((1.0 - math.log(math.tan(lat_rad) + (1.0 / math.cos(lat_rad))) / math.pi) / 2.0 * n)
275
        return(xtile, ytile)
276
277
    def deg2num(self, coord):
278
        lat_rad = math.radians(coord.lat)
279
        n = 2 ** self.zoom
280
        xtile = (coord.lon + 180.0) / 360 * n
281
        ytile = (1.0 - math.log(math.tan(lat_rad) + (1.0 / math.cos(lat_rad))) / math.pi) / 2.0 * n
282
        return(xtile, ytile)
283
284
    def num2deg(self, xtile, ytile):
285
        n = 2 ** self.zoom
286
        lon_deg = xtile / n * 360.0 - 180.0
287
        lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
288
        lat_deg = lat_rad * 180.0 / math.pi
289
        return geo.Coordinate(lat_deg, lon_deg)
290
291
    def check_bounds(self, xtile, ytile):
292
        max_x = 2**self.zoom
293
        max_y = 2**self.zoom
294
        return (
295
            xtile % max_x,
296
            ytile % max_y
297
        )
298
299
300
301
class AbstractMapLayer():
302
    def __init__(self):
303
        self.result = None
304
305
    def draw(self):
306
        pass
307
308
    def clicked_screen(self, screenpoint):
309
        pass
310
311
    def clicked_coordinate(self, center, topleft, bottomright):
312
        pass
313
314
    def resize(self):
315
        pass
316
317
    def attach(self, map):
318
        self.map = map
319
        
320
    def refresh(self):
321
        self.draw()
322
        self.map.refresh()
323
324
logger = logging.getLogger('abstractmarkslayer')
325
326
class AbstractMarksLayer(AbstractMapLayer):
327
328
    ARROW_OFFSET = 1.0 / 3.0 # Offset to center of arrow, calculated as 2-x = sqrt(1^2+(x+1)^2)
329
    ARROW_SHAPE = [(0, -2 + ARROW_OFFSET), (1, + 1 + ARROW_OFFSET), (0, 0 + ARROW_OFFSET), (-1, 1 + ARROW_OFFSET), (0, -2 + ARROW_OFFSET)]
330
331
    def __init__(self):
332
        AbstractMapLayer.__init__(self)
333
        self.current_target = None
334
        self.gps_target_distance = None
335
        self.gps_target_bearing = None
336
        self.gps_data = None
337
        self.gps_last_good_fix = None
338
        self.gps_has_fix = None
339
        self.follow_position = False
340
341
342
    def set_follow_position(self, value):
343
        logger.info('Setting "Follow position" to :' + repr(value))
344
        if value and not self.follow_position and self.gps_last_good_fix != None:
345
            self.map.set_center(self.gps_last_good_fix.position)
346
        self.follow_position = value
347
348
    def get_follow_position(self):
349
        return self.follow_position
350
351
    def on_target_changed(self, caller, cache, distance, bearing):
352
        self.current_target = cache
353
        self.gps_target_distance = distance
354
        self.gps_target_bearing = bearing
355
356
    def on_good_fix(self, core, gps_data, distance, bearing):
357
        self.gps_data = gps_data
358
        self.gps_last_good_fix = gps_data
359
        self.gps_has_fix = True
360
        self.gps_target_distance = distance
361
        self.gps_target_bearing = bearing
362
        if self.map.dragging:
363
            return
364
        if (self.follow_position and not self.map.set_center_lazy(self.gps_data.position)) or not self.follow_position:
365
            self.draw()
366
            self.map.refresh()
367
368
    def on_no_fix(self, caller, gps_data, status):
369
        self.gps_data = gps_data
370
        self.gps_has_fix = False
371
372
373
    @staticmethod
374
    def _get_arrow_transformed(root_x, root_y, width, height, angle):
375
        multiply = height / (2 * (2-AbstractMarksLayer.ARROW_OFFSET))
376
        offset_x = width / 2
377
        offset_y = height / 2
378
        s = multiply * math.sin(math.radians(angle))
379
        c = multiply * math.cos(math.radians(angle))
380
        arrow_transformed = [(int(x * c + offset_x - y * s) + root_x,
381
                              int(y * c + offset_y + x * s) + root_y) for x, y in AbstractMarksLayer.ARROW_SHAPE]
382
        return arrow_transformed
383
                
384
385
class AbstractGeocacheLayer(AbstractMapLayer):
386
387
    CACHE_SIZE = 20
388
    TOO_MANY_POINTS = 30
389
    CACHES_ZOOM_LOWER_BOUND = 9
390
    CACHE_DRAW_SIZE = 10
391
392
    MAX_NUM_RESULTS_SHOW = 100
393
394
    def __init__(self, get_geocaches_callback, show_cache_callback):
395
        AbstractMapLayer.__init__(self)
396
        #self.show_found = False
397
        self.show_name = False
398
        self.get_geocaches_callback = get_geocaches_callback
399
        self.visualized_geocaches = []
400
        self.show_cache_callback = show_cache_callback
401
        self.current_cache = None
402
        self.select_found = None
403
    '''
404
    def set_show_found(self, show_found):
405
        if show_found:
406
            self.select_found = None
407
        else:
408
            self.select_found = False
409
    '''
410
    def set_show_name(self, show_name):
411
        self.show_name = show_name
412
413
    def set_current_cache(self, cache):
414
        self.current_cache = cache
415
416
    def clicked_coordinate(self, center, topleft, bottomright):
417
        mindistance = (center.lat - topleft.lat) ** 2 + (center.lon - topleft.lon) ** 2
418
        mincache = None
419
        for c in self.visualized_geocaches:
420
            dist = (c.lat - center.lat) ** 2 + (c.lon - center.lon) ** 2
421
422
            if dist < mindistance:
423
                mindistance = dist
424
                mincache = c
425
426
        if mincache != None:
427
            self.show_cache_callback(mincache)
428
        return False
429
430
431
    @staticmethod
432
    def shorten_name(s, chars):
433
        max_pos = chars
434
        min_pos = chars - 10
435
436
        NOT_FOUND = -1
437
438
        suffix = '…'
439
440
        # Case 1: Return string if it is shorter (or equal to) than the limit
441
        length = len(s)
442
        if length <= max_pos:
443
            return s
444
        else:
445
            # Case 2: Return it to nearest period if possible
446
            try:
447
                end = s.rindex('.', min_pos, max_pos)
448
            except ValueError:
449
                # Case 3: Return string to nearest space
450
                end = s.rfind(' ', min_pos, max_pos)
451
                if end == NOT_FOUND:
452
                    end = max_pos
453
            return s[0:end] + suffix