Changeset 206
- Timestamp:
- 03/26/07 17:37:35 (2 years ago)
- Files:
-
- trunk/py/compose.py (modified) (1 diff)
- trunk/py/ModestMaps/Core.py (added)
- trunk/py/ModestMaps/Geo.py (added)
- trunk/py/ModestMaps/Microsoft.py (added)
- trunk/py/ModestMaps/Providers.py (modified) (2 diffs)
- trunk/py/ModestMaps/__init__.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/py/compose.py
r153 r206 1 import sys, math, PIL.Image, urllib, StringIO, time, optparse 2 from ModestMaps import Providers 1 import sys, math, PIL.Image, urllib, StringIO, optparse, thread, ModestMaps 3 2 4 def deg2rad(deg): 5 return deg * math.pi / 180 3 def parseWidthHeight(option, opt, values, parser): 4 if values[0] > 0 and values[1] > 0: 5 parser.width = values[0] 6 parser.height = values[1] 7 8 else: 9 raise optparse.OptionValueError('Image dimensions must be positive (got: width %d, height %d)' % values) 6 10 7 def project(lat, lon): 8 x = lon 9 y = math.log((1 + math.sin(lat)) / (1 - math.sin(lat))) / 2 10 return x / math.pi, y / math.pi 11 def assertWidthHeight(parser): 12 try: 13 parser.width, parser.height 14 except AttributeError: 15 raise optparse.OptionValueError('Image dimensions must be provided, e.g.: --dimensions <width> <height>') 16 17 def parseProvider(option, opt, value, parser): 18 if value == 'MICROSOFT_ROAD': 19 parser.provider = ModestMaps.Microsoft.RoadProvider() 20 21 elif value == 'MICROSOFT_AERIAL': 22 parser.provider = ModestMaps.Microsoft.AerialProvider() 23 24 elif value == 'MICROSOFT_HYBRID': 25 parser.provider = ModestMaps.Microsoft.HybridProvider() 26 27 else: 28 raise optparse.OptionValueError('Provider must be in eligible list (got: "%s")' % value) 29 30 def assertProvider(parser): 31 try: 32 parser.provider 33 except AttributeError: 34 raise optparse.OptionValueError('Provider must be provided, e.g.: --provider <name>') 35 36 def parseCenterZoom(option, opt, values, parser): 37 assertProvider(parser) 38 39 coordinate = parser.provider.locationCoordinate(ModestMaps.Geo.Location(values[0], values[1])).zoomTo(values[2]) 40 coordPoint = ModestMaps.calculateMapCenter(coordinate) 41 parser.coord, parser.point = coordPoint 42 43 def parseExtent(option, opt, values, parser): 44 assertWidthHeight(parser) 45 assertProvider(parser) 46 47 coordPoint = ModestMaps.calculateMapExtent(parser.provider, 48 parser.width, parser.height, 49 ModestMaps.Geo.Location(values[0], values[1]), 50 ModestMaps.Geo.Location(values[2], values[3])) 51 parser.coord, parser.point = coordPoint 11 52 12 def locationCoord(lat, lon, zoom=0): 13 x, y = project(deg2rad(lat), deg2rad(lon)) 53 class RequestJob: 54 def __init__(self, provider, coord, point): 55 self.done = False 56 self.provider = provider 57 self.coord = coord 58 self.point = point 59 60 def do(self, lock, verbose): 61 if self.done: 62 # don't bother? 63 return 64 65 url = self.provider.getTileUrl(self.coord) 66 67 if verbose: 68 print 'Requesting', url, 'in thread', thread.get_ident() 69 70 # this is the time-consuming part 71 try: 72 img = PIL.Image.open(StringIO.StringIO(urllib.urlopen(url).read())) 73 74 except: 75 print 'Failed', url, 'in thread', thread.get_ident() 76 77 else: 78 if lock.acquire(): 79 if verbose: 80 print 'Received', url, 'in thread', thread.get_ident() 14 81 15 row = math.pow(2, zoom) * ((1 - y) / 2) 16 col = math.pow(2, zoom) * ((x + 1) / 2) 17 18 return row, col, zoom 19 82 self.img = img 83 self.done = True 84 lock.release() 85 86 class RequestQueue(list): 87 """ List of RequestJob objects, that's sensitive to when they're done. 88 """ 89 90 def done(self): 91 """ True if all contained jobs are done. 92 """ 93 unfinished = [job for job in self if not job.done] 94 return len(unfinished) == 0 95 20 96 parser = optparse.OptionParser() 97 98 parser.add_option('-v', '--verbose', dest='verbose', 99 help='Make a bunch of noise', 100 action='store_true') 21 101 22 102 parser.add_option('-o', '--out', dest='outfile', 23 103 help='Write to output file') 24 104 25 parser.add_option('-z', '--zoom', dest='zoomlevel', 26 help='Zoom level', type='int') 105 parser.add_option('-c', '--center', dest='centerzoom', nargs=3, 106 help='Center (lat, lon) and zoom level', type='float', 107 action='callback', callback=parseCenterZoom) 27 108 28 parser.add_option('-b', '--bounds', dest='bounds', 29 help='Lat/long bounding box', type='float', nargs=4) 109 parser.add_option('-e', '--extent', dest='extent', nargs=4, 110 help='Geographical extent (lat, lon pair)', type='float', 111 action='callback', callback=parseExtent) 30 112 31 113 parser.add_option('-p', '--provider', dest='provider', 32 help='Map Provider', choices=Providers.ids) 114 help='Map Provider', type='string', 115 action='callback', callback=parseProvider) 116 117 parser.add_option('-d', '--dimensions', dest='dimensions', nargs=2, 118 help='Pixel dimensions (width, height) of resulting image', type='int', 119 action='callback', callback=parseWidthHeight) 33 120 34 121 (options, args) = parser.parse_args() 35 122 36 123 if __name__ == '__main__': 124 if options.verbose: 125 print parser.coord, parser.point, '->', options.outfile, (parser.width, parser.height) 126 127 corner = ModestMaps.Core.Point(int(parser.point.x + parser.width / 2), int(parser.point.y + parser.height / 2)) 128 129 while corner.x > 0: 130 corner.x -= ModestMaps.TILE_WIDTH 131 parser.coord = parser.coord.left() 132 133 while corner.y > 0: 134 corner.y -= ModestMaps.TILE_HEIGHT 135 parser.coord = parser.coord.up() 136 137 jobs = RequestQueue() 138 139 rowCoord = parser.coord.copy() 140 for y in range(corner.y, parser.height, ModestMaps.TILE_HEIGHT): 141 tileCoord = rowCoord.copy() 142 for x in range(corner.x, parser.width, ModestMaps.TILE_WIDTH): 143 jobs.append(RequestJob(parser.provider, tileCoord, ModestMaps.Core.Point(x, y))) 144 tileCoord = tileCoord.right() 145 rowCoord = rowCoord.down() 146 147 lock = thread.allocate_lock() 37 148 38 provider = Providers.Provider(options.provider) 149 for job in jobs: 150 # request all needed images 151 thread.start_new_thread(job.do, (lock, options.verbose)) 152 153 while not jobs.done(): 154 # hang around until they are done... 155 pass 156 157 mapImg = PIL.Image.new('RGB', (parser.width, parser.height)) 39 158 40 #print locationCoord(85, -180, 0) 41 #print locationCoord(0, 0, 0) 42 #print locationCoord(-85, 180, 0) 43 #sys.exit() 44 45 zoom = options.zoomlevel 159 for job in jobs: 160 mapImg.paste(job.img, (job.point.x, job.point.y)) 46 161 47 #print math.pow(2, zoom) 48 49 minRow, minCol, minZoom = map(int, map(math.floor, locationCoord(options.bounds[0], options.bounds[1], zoom))) 50 maxRow, maxCol, maxZoom = map(int, map(math.ceil, locationCoord(options.bounds[2], options.bounds[3], zoom))) 51 52 rows = (maxRow - minRow) 53 cols = (maxCol - minCol) 54 55 print 'Allocating...', rows, 'x', cols 56 destImg = PIL.Image.new('RGB', (256 * cols, 256 * rows)) 57 58 for row in range(minRow, maxRow): 59 for col in range(minCol, maxCol): 60 destY = (row - minRow) * 256 61 destX = (col - minCol) * 256 62 63 url = provider.url(col, row, zoom) 64 print row, col, zoom, '->', url, '->', destX, destY 65 66 img = PIL.Image.open(StringIO.StringIO(urllib.urlopen(url).read())) 67 destImg.paste(img, (destX, destY)) 68 69 #time.sleep(10) 70 71 print 'Saving...' 72 destImg.save(options.outfile) 73 print options.outfile 162 mapImg.save(options.outfile) trunk/py/ModestMaps/Providers.py
r152 r206 1 import random, Tiles 1 from Core import Coordinate 2 from Geo import LinearProjection, MercatorProjection, Transformation 2 3 3 4 ids = ('MICROSOFT_ROAD', 'MICROSOFT_AERIAL', 'MICROSOFT_HYBRID', … … 7 8 'OPEN_STREET_MAP') 8 9 9 class Provider: 10 """ 11 """ 10 class IMapProvider: 11 def __init__(self): 12 raise NotImplementedError("Abstract method not implemented by subclass.") 13 14 def getTileUrl(self, coordinate): 15 raise NotImplementedError("Abstract method not implemented by subclass.") 12 16 13 def __init__(self, id): 14 if id not in ids: 15 raise KeyError("Unknown provider: '%s'" % id) 16 17 self.id = id 18 19 def url(self, column, row, zoom): 20 """ Retrieve a tile URL for a given column, row, zoom. 21 """ 22 if self.id == 'MICROSOFT_ROAD': 23 return 'http://r%d.ortho.tiles.virtualearth.net/tiles/r%s.png?g=45' % (random.randint(0,3), Tiles.toMicrosoft(column, row, zoom)) 17 def locationCoordinate(self, location): 18 return self.projection.locationCoordinate(location) 24 19 25 if self.id == 'MICROSOFT_HYBRID':26 return 'http://h%d.ortho.tiles.virtualearth.net/tiles/h%s.jpeg?g=45' % (random.randint(0,3), Tiles.toMicrosoft(column, row, zoom))20 def coordinateLocation(self, location): 21 return self.projection.coordinateLocation(location) 27 22 28 if self.id == 'MICROSOFT_AERIAL': 29 return 'http://a%d.ortho.tiles.virtualearth.net/tiles/a%s.jpeg?g=45' % (random.randint(0,3), Tiles.toMicrosoft(column, row, zoom)) 30 31 else: 32 raise NotImplementedError("Unimplemented provider: '%s'" % self.id) 23 def sourceCoordinate(self, coordinate): 24 raise NotImplementedError("Abstract method not implemented by subclass.") trunk/py/ModestMaps/__init__.py
r152 r206 1 import math 2 1 3 import Tiles 2 4 import Providers 5 import Core 6 import Geo 7 import Microsoft 8 9 TILE_WIDTH = 256 10 TILE_HEIGHT = 256 11 12 def calculateMapCenter(centerCoord): 13 """ Based on a center coordinate, returns the coordinate 14 of an initial tile and it's point placement, relative to 15 the map center. 16 """ 17 18 # initial tile coordinate 19 initTileCoord = Core.Coordinate(math.floor(centerCoord.row), math.floor(centerCoord.column), math.floor(centerCoord.zoom)) 20 21 # initial tile position, assuming centered tile well in grid 22 initX = (initTileCoord.column - centerCoord.column) * TILE_WIDTH 23 initY = (initTileCoord.row - centerCoord.row) * TILE_HEIGHT 24 initPoint = Core.Point(round(initX), round(initY)) 25 26 return initTileCoord, initPoint 27 28 def calculateMapExtent(provider, width, height, *args): 29 30 coordinates = map(provider.locationCoordinate, args) 31 32 TL = Core.Coordinate(min([c.row for c in coordinates]), 33 min([c.column for c in coordinates]), 34 min([c.zoom for c in coordinates])) 35 36 BR = Core.Coordinate(max([c.row for c in coordinates]), 37 max([c.column for c in coordinates]), 38 max([c.zoom for c in coordinates])) 39 40 # multiplication factor between horizontal span and map width 41 hFactor = (BR.column - TL.column) / (width / TILE_WIDTH) 42 43 # multiplication factor expressed as base-2 logarithm, for zoom difference 44 hZoomDiff = math.log(hFactor) / math.log(2) 45 46 # possible horizontal zoom to fit geographical extent in map width 47 hPossibleZoom = TL.zoom - math.ceil(hZoomDiff) 48 49 # multiplication factor between vertical span and map height 50 vFactor = (BR.row - TL.row) / (height / TILE_HEIGHT) 51 52 # multiplication factor expressed as base-2 logarithm, for zoom difference 53 vZoomDiff = math.log(vFactor) / math.log(2) 54 55 # possible vertical zoom to fit geographical extent in map height 56 vPossibleZoom = TL.zoom - math.ceil(vZoomDiff) 57 58 # initial zoom to fit extent vertically and horizontally 59 initZoom = min(hPossibleZoom, vPossibleZoom) 60 61 ## additionally, make sure it's not outside the boundaries set by provider limits 62 #initZoom = min(initZoom, provider.outerLimits()[1].zoom) 63 #initZoom = max(initZoom, provider.outerLimits()[0].zoom) 64 65 # coordinate of extent center 66 centerRow = (TL.row + BR.row) / 2 67 centerColumn = (TL.column + BR.column) / 2 68 centerZoom = (TL.zoom + BR.zoom) / 2 69 centerCoord = Core.Coordinate(centerRow, centerColumn, centerZoom).zoomTo(initZoom) 70 71 return calculateMapCenter(centerCoord)
