2
# -*- coding: utf-8 -*-
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.
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.
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/>.
18
# Author: Daniel Fett agtl@danielfett.de
19
# Jabber: fett.daniel@jaber.ccc.de
20
# Bugtracker and GIT Repository: http://github.com/webhamster/advancedcaching
27
logger = logging.getLogger('abstractmap')
36
RADIUS_EARTH = 6371000.0
39
CLICK_CHECK_RADIUS = 17
42
def set_config(Map, map_providers, map_path, placeholder_cantload, placeholder_loading):
44
Map.noimage_cantload = Map._load_tile(placeholder_cantload)
45
Map.noimage_loading = Map._load_tile(placeholder_loading)
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
54
Map.tile_loaders.append((name, tl))
56
def __init__(self, center, zoom, tile_loader = None):
57
self.active_tile_loaders = []
58
self.double_size = False
60
self.osd_message = None
62
if tile_loader == None:
63
self.tile_loader = self.tile_loaders[0][1]
65
self.tile_loader = tile_loader
67
self.drag_offset_x = 0
68
self.drag_offset_y = 0
70
self.total_map_width = 256 * 2 ** zoom
71
self.set_center(center, False)
74
##############################################
76
# Controlling the layers
78
##############################################
80
def add_layer(self, layer):
81
self.layers.append(layer)
85
def set_osd_message(self, message):
86
self.osd_message = message
88
##############################################
90
# Controlling the map view
92
##############################################
94
def set_center(self, coord, update = True):
97
self.map_center_x, self.map_center_y = self.deg2num(coord)
98
self.center_latlon = coord
104
def set_center_lazy(self, coord):
107
old_center_x, old_center_y = self.coord2point(self.center_latlon)
108
new_center_x, new_center_y = self.coord2point(coord)
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!')
117
logger.debug('Lazy!')
121
def get_center(self):
122
return self.center_latlon
124
def relative_zoom(self, direction=None, update=True):
125
if direction != None:
126
self.set_zoom(self.zoom + direction, update)
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])
135
def set_zoom(self, newzoom, update = True):
136
if newzoom < 0 or newzoom > self.tile_loader.MAX_ZOOM:
138
logger.debug('New zoom level: %d' % newzoom)
140
self.total_map_width = (256 * 2**self.zoom)
141
self.set_center(self.center_latlon, update)
146
def get_max_zoom(self):
147
return self.tile_loader.MAX_ZOOM
149
def get_min_zoom(self):
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)
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())
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)
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)
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))
176
center = geo.Coordinate((maxlat + minlat) / 2.0, (maxlon + minlon) / 2.0)
177
logger.debug("New Center: %s" % center)
179
self.set_center(center, False)
180
self.set_zoom(max(min(target_zoom, self.get_max_zoom()), self.get_min_zoom()))
183
##############################################
187
##############################################
189
def set_double_size(self, ds):
190
self.double_size = ds
192
def get_double_size(self):
193
return self.double_size
195
def set_tile_loader(self, loader):
196
self.tile_loader = loader
197
self.emit('tile-loader-changed', loader)
198
self.relative_zoom(0)
200
def set_placeholder_images(self, cantload, loading):
201
self.noimage_cantload = self._load_tile(cantload)
202
self.noimage_loading = self._load_tile(loading)
204
##############################################
206
# Coordinate Conversion and Checking
208
##############################################
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)
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)
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)
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 \
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))
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)
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:
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:
254
if l.clicked_coordinate(c, c1, c2) == False:
259
##############################################
261
# Tile Number calculations
263
##############################################
265
return self.tile_loader.TILE_SIZE
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
270
def deg2tilenum(self, lat_deg, lon_deg):
271
lat_rad = math.radians(lat_deg)
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)
277
def deg2num(self, coord):
278
lat_rad = math.radians(coord.lat)
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
284
def num2deg(self, xtile, ytile):
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)
291
def check_bounds(self, xtile, ytile):
301
class AbstractMapLayer():
308
def clicked_screen(self, screenpoint):
311
def clicked_coordinate(self, center, topleft, bottomright):
317
def attach(self, map):
324
logger = logging.getLogger('abstractmarkslayer')
326
class AbstractMarksLayer(AbstractMapLayer):
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)]
332
AbstractMapLayer.__init__(self)
333
self.current_target = None
334
self.gps_target_distance = None
335
self.gps_target_bearing = None
337
self.gps_last_good_fix = None
338
self.gps_has_fix = None
339
self.follow_position = False
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
348
def get_follow_position(self):
349
return self.follow_position
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
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:
364
if (self.follow_position and not self.map.set_center_lazy(self.gps_data.position)) or not self.follow_position:
368
def on_no_fix(self, caller, gps_data, status):
369
self.gps_data = gps_data
370
self.gps_has_fix = False
374
def _get_arrow_transformed(root_x, root_y, width, height, angle):
375
multiply = height / (2 * (2-AbstractMarksLayer.ARROW_OFFSET))
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
385
class AbstractGeocacheLayer(AbstractMapLayer):
389
CACHES_ZOOM_LOWER_BOUND = 9
392
MAX_NUM_RESULTS_SHOW = 100
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
404
def set_show_found(self, show_found):
406
self.select_found = None
408
self.select_found = False
410
def set_show_name(self, show_name):
411
self.show_name = show_name
413
def set_current_cache(self, cache):
414
self.current_cache = cache
416
def clicked_coordinate(self, center, topleft, bottomright):
417
mindistance = (center.lat - topleft.lat) ** 2 + (center.lon - topleft.lon) ** 2
419
for c in self.visualized_geocaches:
420
dist = (c.lat - center.lat) ** 2 + (c.lon - center.lon) ** 2
422
if dist < mindistance:
427
self.show_cache_callback(mincache)
432
def shorten_name(s, chars):
440
# Case 1: Return string if it is shorter (or equal to) than the limit
442
if length <= max_pos:
445
# Case 2: Return it to nearest period if possible
447
end = s.rindex('.', min_pos, max_pos)
449
# Case 3: Return string to nearest space
450
end = s.rfind(' ', min_pos, max_pos)
453
return s[0:end] + suffix