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

« back to all changes in this revision

Viewing changes to files/advancedcaching/abstractmap.py

  • Committer: Bazaar Package Importer
  • Author(s): Angel Abad
  • Date: 2011-01-23 13:14:08 UTC
  • mfrom: (4.1.4 sid)
  • Revision ID: james.westby@ubuntu.com-20110123131408-4wy5aefap3o6a4lw
Tags: 0.8.0.3-1ubuntu1
* Merge from debian unstable.  Remaining changes:
  - debian/control: Switch section from python to misc
  - Add category Geography to .desktop

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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