root/trunk/py/ModestMaps/__init__.py

Revision 702, 16.1 kB (checked in by migurski, 2 months ago)

Added a more informative User-Agent string for python requests

Line 
1 """
2 >>> m = Map(Microsoft.RoadProvider(), Core.Point(600, 600), Core.Coordinate(3165, 1313, 13), Core.Point(-144, -94))
3 >>> p = m.locationPoint(Geo.Location(37.804274, -122.262940))
4 >>> p
5 (370.724, 342.549)
6 >>> m.pointLocation(p)
7 (37.804, -122.263)
8
9 >>> c = Geo.Location(37.804274, -122.262940)
10 >>> z = 12
11 >>> d = Core.Point(800, 600)
12 >>> m = mapByCenterZoom(Microsoft.RoadProvider(), c, z, d)
13 >>> m.dimensions
14 (800.000, 600.000)
15 >>> m.coordinate
16 (1582.000, 656.000 @12.000)
17 >>> m.offset
18 (-235.000, -196.000)
19
20 >>> sw = Geo.Location(36.893326, -123.533554)
21 >>> ne = Geo.Location(38.864246, -121.208153)
22 >>> d = Core.Point(800, 600)
23 >>> m = mapByExtent(Microsoft.RoadProvider(), sw, ne, d)
24 >>> m.dimensions
25 (800.000, 600.000)
26 >>> m.coordinate
27 (98.000, 40.000 @8.000)
28 >>> m.offset
29 (-251.000, -218.000)
30
31 >>> se = Geo.Location(36.893326, -121.208153)
32 >>> nw = Geo.Location(38.864246, -123.533554)
33 >>> d = Core.Point(1600, 1200)
34 >>> m = mapByExtent(Microsoft.RoadProvider(), se, nw, d)
35 >>> m.dimensions
36 (1600.000, 1200.000)
37 >>> m.coordinate
38 (197.000, 81.000 @9.000)
39 >>> m.offset
40 (-246.000, -179.000)
41
42 >>> sw = Geo.Location(36.893326, -123.533554)
43 >>> ne = Geo.Location(38.864246, -121.208153)
44 >>> z = 10
45 >>> m = mapByExtentZoom(Microsoft.RoadProvider(), sw, ne, z)
46 >>> m.dimensions
47 (1693.000, 1818.000)
48 >>> m.coordinate
49 (395.000, 163.000 @10.000)
50 >>> m.offset
51 (-236.000, -102.000)
52
53 >>> se = Geo.Location(36.893326, -121.208153)
54 >>> nw = Geo.Location(38.864246, -123.533554)
55 >>> z = 9
56 >>> m = mapByExtentZoom(Microsoft.RoadProvider(), se, nw, z)
57 >>> m.dimensions
58 (846.000, 909.000)
59 >>> m.coordinate
60 (197.000, 81.000 @9.000)
61 >>> m.offset
62 (-246.000, -179.000)
63 """
64
65 import sys, PIL.Image, urllib, httplib, urlparse, StringIO, math, thread, time
66
67 import Tiles
68 import Providers
69 import Core
70 import Geo
71 import Google, Yahoo, Microsoft, BlueMarble, OpenStreetMap
72 import time
73
74 # a handy list of possible providers, which isn't
75 # to say that you can't go writing your own.
76 builtinProviders = {
77     'OPENSTREETMAP':    OpenStreetMap.Provider,
78     'OPEN_STREET_MAP':  OpenStreetMap.Provider,
79     'BLUE_MARBLE':      BlueMarble.Provider,
80     'MICROSOFT_ROAD':   Microsoft.RoadProvider,
81     'MICROSOFT_AERIAL': Microsoft.AerialProvider,
82     'MICROSOFT_HYBRID': Microsoft.HybridProvider,
83     'GOOGLE_ROAD':      Google.RoadProvider,
84     'GOOGLE_AERIAL':    Google.AerialProvider,
85     'GOOGLE_HYBRID':    Google.HybridProvider,
86     'GOOGLE_TERRAIN':   Google.TerrainProvider,
87     'YAHOO_ROAD':       Yahoo.RoadProvider,
88     'YAHOO_AERIAL':     Yahoo.AerialProvider,
89     'YAHOO_HYBRID':     Yahoo.HybridProvider
90     }
91
92 def mapByCenterZoom(provider, center, zoom, dimensions):
93     """ Return map instance given a provider, center location, zoom value, and dimensions point.
94     """
95     centerCoord = provider.locationCoordinate(center).zoomTo(zoom)
96     mapCoord, mapOffset = calculateMapCenter(provider, centerCoord)
97
98     return Map(provider, dimensions, mapCoord, mapOffset)
99
100 def mapByExtent(provider, locationA, locationB, dimensions):
101     """ Return map instance given a provider, two corner locations, and dimensions point.
102     """
103     mapCoord, mapOffset = calculateMapExtent(provider, dimensions.x, dimensions.y, locationA, locationB)
104
105     return Map(provider, dimensions, mapCoord, mapOffset)
106    
107 def mapByExtentZoom(provider, locationA, locationB, zoom):
108     """ Return map instance given a provider, two corner locations, and zoom value.
109     """
110     # a coordinate per corner
111     coordA = provider.locationCoordinate(locationA).zoomTo(zoom)
112     coordB = provider.locationCoordinate(locationB).zoomTo(zoom)
113
114     # precise width and height in pixels
115     width = abs(coordA.column - coordB.column) * provider.tileWidth()
116     height = abs(coordA.row - coordB.row) * provider.tileHeight()
117    
118     # nearest pixel actually
119     dimensions = Core.Point(int(width), int(height))
120    
121     # projected center of the map
122     centerCoord = Core.Coordinate((coordA.row + coordB.row) / 2,
123                                   (coordA.column + coordB.column) / 2,
124                                   zoom)
125    
126     mapCoord, mapOffset = calculateMapCenter(provider, centerCoord)
127
128     return Map(provider, dimensions, mapCoord, mapOffset)
129
130 def calculateMapCenter(provider, centerCoord):
131     """ Based on a provider and center coordinate, returns the coordinate
132         of an initial tile and its point placement, relative to the map center.
133     """
134     # initial tile coordinate
135     initTileCoord = centerCoord.container()
136
137     # initial tile position, assuming centered tile well in grid
138     initX = (initTileCoord.column - centerCoord.column) * provider.tileWidth()
139     initY = (initTileCoord.row - centerCoord.row) * provider.tileHeight()
140     initPoint = Core.Point(round(initX), round(initY))
141    
142     return initTileCoord, initPoint
143
144 def calculateMapExtent(provider, width, height, *args):
145     """ Based on a provider, width & height values, and a list of locations,
146         returns the coordinate of an initial tile and its point placement,
147         relative to the map center.
148     """
149     coordinates = map(provider.locationCoordinate, args)
150    
151     TL = Core.Coordinate(min([c.row for c in coordinates]),
152                          min([c.column for c in coordinates]),
153                          min([c.zoom for c in coordinates]))
154
155     BR = Core.Coordinate(max([c.row for c in coordinates]),
156                          max([c.column for c in coordinates]),
157                          max([c.zoom for c in coordinates]))
158                    
159     # multiplication factor between horizontal span and map width
160     hFactor = (BR.column - TL.column) / (float(width) / provider.tileWidth())
161
162     # multiplication factor expressed as base-2 logarithm, for zoom difference
163     hZoomDiff = math.log(hFactor) / math.log(2)
164        
165     # possible horizontal zoom to fit geographical extent in map width
166     hPossibleZoom = TL.zoom - math.ceil(hZoomDiff)
167        
168     # multiplication factor between vertical span and map height
169     vFactor = (BR.row - TL.row) / (float(height) / provider.tileHeight())
170        
171     # multiplication factor expressed as base-2 logarithm, for zoom difference
172     vZoomDiff = math.log(vFactor) / math.log(2)
173        
174     # possible vertical zoom to fit geographical extent in map height
175     vPossibleZoom = TL.zoom - math.ceil(vZoomDiff)
176        
177     # initial zoom to fit extent vertically and horizontally
178     initZoom = min(hPossibleZoom, vPossibleZoom)
179
180     ## additionally, make sure it's not outside the boundaries set by provider limits
181     #initZoom = min(initZoom, provider.outerLimits()[1].zoom)
182     #initZoom = max(initZoom, provider.outerLimits()[0].zoom)
183
184     # coordinate of extent center
185     centerRow = (TL.row + BR.row) / 2
186     centerColumn = (TL.column + BR.column) / 2
187     centerZoom = (TL.zoom + BR.zoom) / 2
188     centerCoord = Core.Coordinate(centerRow, centerColumn, centerZoom).zoomTo(initZoom)
189    
190     return calculateMapCenter(provider, centerCoord)
191    
192 class TileRequest:
193    
194     # how many times to retry a failing tile
195     MAX_ATTEMPTS = 5
196
197     def __init__(self, provider, coord, offset):
198         self.done = False
199         self.provider = provider
200         self.coord = coord
201         self.offset = offset
202        
203     def loaded(self):
204         return self.done
205    
206     def images(self):
207         return self.imgs
208    
209     def load(self, lock, verbose, attempt=1):
210         if self.done:
211             # don't bother?
212             return
213
214         urls = self.provider.getTileUrls(self.coord)
215        
216         if verbose:
217             print 'Requesting', urls, '- attempt no.', attempt, 'in thread', hex(thread.get_ident())
218
219         # this is the time-consuming part
220         try:
221             imgs = []
222        
223             for (scheme, netloc, path, params, query, fragment) in map(urlparse.urlparse, urls):
224                 conn = httplib.HTTPConnection(netloc)
225                 conn.request('GET', path + ('?' + query).rstrip('?'), headers={'User-Agent': 'Modest Maps python branch (http://modestmaps.com)'})
226                 response = conn.getresponse()
227                
228                 if str(response.status).startswith('2'):
229                     imgs.append(PIL.Image.open(StringIO.StringIO(response.read())).convert('RGBA'))
230
231         except:
232                
233             if verbose:
234                 print 'Failed', urls, '- attempt no.', attempt, 'in thread', hex(thread.get_ident())
235
236             if attempt < TileRequest.MAX_ATTEMPTS:
237                 time.sleep(1 * attempt)
238                 return self.load(lock, verbose, attempt+1)
239             else:
240                 imgs = [None for url in urls]
241
242         else:
243             if verbose:
244                 print 'Received', urls, '- attempt no.', attempt, 'in thread', hex(thread.get_ident())
245
246         if lock.acquire():
247             self.imgs = imgs
248             self.done = True
249             lock.release()
250
251 class TileQueue(list):
252     """ List of TileRequest objects, that's sensitive to when they're loaded.
253     """
254
255     def __getslice__(self, i, j):
256         """ Return a TileQueue when a list slice is called-for.
257        
258             Python docs say that __getslice__ is deprecated, but its
259             replacement __getitem__ doesn't seem to be doing anything.
260         """
261         other = TileQueue()
262        
263         for t in range(i, j):
264             if t < len(self):
265                 other.append(self[t])
266
267         return other
268
269     def pending(self):
270         """ True if any contained tile is still loading.
271         """
272         remaining = [tile for tile in self if not tile.loaded()]
273         return len(remaining) > 0
274
275 class Map:
276
277     def __init__(self, provider, dimensions, coordinate, offset):
278         """ Instance of a map intended for drawing to an image.
279        
280             provider
281                 Instance of IMapProvider
282                
283             dimensions
284                 Size of output image, instance of Point
285                
286             coordinate
287                 Base tile, instance of Coordinate
288                
289             offset
290                 Position of base tile relative to map center, instance of Point
291         """
292         self.provider = provider
293         self.dimensions = dimensions
294         self.coordinate = coordinate
295         self.offset = offset
296        
297     def __str__(self):
298         return 'Map(%(provider)s, %(dimensions)s, %(coordinate)s, %(offset)s)' % self.__dict__
299
300     def locationPoint(self, location):
301         """ Return an x, y point on the map image for a given geographical location.
302         """
303         point = Core.Point(self.offset.x, self.offset.y)
304         coord = self.provider.locationCoordinate(location).zoomTo(self.coordinate.zoom)
305        
306         # distance from the known coordinate offset
307         point.x += self.provider.tileWidth() * (coord.column - self.coordinate.column)
308         point.y += self.provider.tileHeight() * (coord.row - self.coordinate.row)
309        
310         # because of the center/corner business
311         point.x += self.dimensions.x/2
312         point.y += self.dimensions.y/2
313        
314         return point
315        
316     def pointLocation(self, point):
317         """ Return a geographical location on the map image for a given x, y point.
318         """
319         hizoomCoord = self.coordinate.zoomTo(Core.Coordinate.MAX_ZOOM)
320        
321         # because of the center/corner business
322         point = Core.Point(point.x - self.dimensions.x/2,
323                            point.y - self.dimensions.y/2)
324        
325         # distance in tile widths from reference tile to point
326         xTiles = (point.x - self.offset.x) / self.provider.tileWidth();
327         yTiles = (point.y - self.offset.y) / self.provider.tileHeight();
328        
329         # distance in rows & columns at maximum zoom
330         xDistance = xTiles * math.pow(2, (Core.Coordinate.MAX_ZOOM - self.coordinate.zoom));
331         yDistance = yTiles * math.pow(2, (Core.Coordinate.MAX_ZOOM - self.coordinate.zoom));
332        
333         # new point coordinate reflecting that distance
334         coord = Core.Coordinate(round(hizoomCoord.row + yDistance),
335                                 round(hizoomCoord.column + xDistance),
336                                 hizoomCoord.zoom)
337
338         coord = coord.zoomTo(self.coordinate.zoom)
339        
340         location = self.provider.coordinateLocation(coord)
341        
342         return location
343
344     #
345    
346     def draw_bbox(self, bbox, zoom=16, verbose=False) :
347
348         sw = Geo.Location(bbox[0], bbox[1])
349         ne = Geo.Location(bbox[2], bbox[3])
350         nw = Geo.Location(ne.lat, sw.lon)
351         se = Geo.Location(sw.lat, ne.lon)
352        
353         TL = self.provider.locationCoordinate(nw).zoomTo(zoom)
354
355         #
356
357         tiles = TileQueue()
358
359         cur_lon = sw.lon
360         cur_lat = ne.lat       
361         max_lon = ne.lon
362         max_lat = sw.lat
363        
364         x_off = 0
365         y_off = 0
366         tile_x = 0
367         tile_y = 0
368        
369         tileCoord = TL.copy()
370
371         while cur_lon < max_lon :
372
373             y_off = 0
374             tile_y = 0
375            
376             while cur_lat > max_lat :
377                
378                 tiles.append(TileRequest(self.provider, tileCoord, Core.Point(x_off, y_off)))
379                 y_off += self.provider.tileHeight()
380                
381                 tileCoord = tileCoord.down()
382                 loc = self.provider.coordinateLocation(tileCoord)
383                 cur_lat = loc.lat
384
385                 tile_y += 1
386                
387             x_off += self.provider.tileWidth()           
388             cur_lat = ne.lat
389            
390             tile_x += 1
391             tileCoord = TL.copy().right(tile_x)
392
393             loc = self.provider.coordinateLocation(tileCoord)
394             cur_lon = loc.lon
395
396         width = int(self.provider.tileWidth() * tile_x)
397         height = int(self.provider.tileHeight() * tile_y)
398
399         # Quick, look over there!
400
401         coord, offset = calculateMapExtent(self.provider,
402                                            width, height,
403                                            Geo.Location(bbox[0], bbox[1]),
404                                            Geo.Location(bbox[2], bbox[3]))
405
406         self.offset = offset
407         self.coordinates = coord
408         self.dimensions = Core.Point(width, height)
409
410         return self.draw()
411    
412     #
413    
414     def draw(self, verbose=False):
415         """ Draw map out to a PIL.Image and return it.
416         """
417         coord = self.coordinate.copy()
418         corner = Core.Point(int(self.offset.x + self.dimensions.x/2), int(self.offset.y + self.dimensions.y/2))
419
420         while corner.x > 0:
421             corner.x -= self.provider.tileWidth()
422             coord = coord.left()
423        
424         while corner.y > 0:
425             corner.y -= self.provider.tileHeight()
426             coord = coord.up()
427        
428         tiles = TileQueue()
429        
430         rowCoord = coord.copy()
431         for y in range(corner.y, self.dimensions.y, self.provider.tileHeight()):
432             tileCoord = rowCoord.copy()
433             for x in range(corner.x, self.dimensions.x, self.provider.tileWidth()):
434                 tiles.append(TileRequest(self.provider, tileCoord, Core.Point(x, y)))
435                 tileCoord = tileCoord.right()
436             rowCoord = rowCoord.down()
437
438         return self.render_tiles(tiles, self.dimensions.x, self.dimensions.y, verbose)
439
440     #
441    
442     def render_tiles(self, tiles, img_width, img_height, verbose=False):
443        
444         lock = thread.allocate_lock()
445         threads = 32
446        
447         for off in range(0, len(tiles), threads):
448             pool = tiles[off:(off + threads)]
449            
450             for tile in pool:
451                 # request all needed images
452                 thread.start_new_thread(tile.load, (lock, verbose))
453                
454             # if it takes any longer than 20 sec overhead + 10 sec per tile, give up
455             due = time.time() + 20 + len(pool) * 10
456            
457             while time.time() < due and pool.pending():
458                 # hang around until they are loaded or we run out of time...
459                 time.sleep(1)
460
461         mapImg = PIL.Image.new('RGB', (img_width, img_height))
462        
463         for tile in tiles:
464             try:
465                 for img in tile.images():
466                     mapImg.paste(img, (tile.offset.x, tile.offset.y), img)
467             except:
468                 # something failed to paste, so we ignore it
469                 pass
470
471         return mapImg
472
473 if __name__ == '__main__':
474     import doctest
475     doctest.testmod()
Note: See TracBrowser for help on using the browser.