Changeset 206

Show
Ignore:
Timestamp:
03/26/07 17:37:35 (2 years ago)
Author:
migurski
Message:

Brought compose.py roughly up to speed with what MMaps AS2 can do

Files:

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 
     1import sys, math, PIL.Image, urllib, StringIO, optparse, thread, ModestMaps 
    32 
    4 def deg2rad(deg): 
    5     return deg * math.pi / 180 
     3def 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) 
    610 
    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 
     11def 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 
     17def 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 
     30def assertProvider(parser): 
     31    try: 
     32        parser.provider 
     33    except AttributeError: 
     34        raise optparse.OptionValueError('Provider must be provided, e.g.: --provider <name>') 
     35 
     36def 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 
     43def 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 
    1152     
    12 def locationCoord(lat, lon, zoom=0): 
    13     x, y = project(deg2rad(lat), deg2rad(lon)) 
     53class 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() 
    1481     
    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 
     86class 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 
    2096parser = optparse.OptionParser() 
     97 
     98parser.add_option('-v', '--verbose', dest='verbose', 
     99                  help='Make a bunch of noise', 
     100                  action='store_true') 
    21101 
    22102parser.add_option('-o', '--out', dest='outfile', 
    23103                  help='Write to output file') 
    24104 
    25 parser.add_option('-z', '--zoom', dest='zoomlevel', 
    26                   help='Zoom level', type='int') 
     105parser.add_option('-c', '--center', dest='centerzoom', nargs=3, 
     106                  help='Center (lat, lon) and zoom level', type='float', 
     107                  action='callback', callback=parseCenterZoom) 
    27108 
    28 parser.add_option('-b', '--bounds', dest='bounds', 
    29                   help='Lat/long bounding box', type='float', nargs=4) 
     109parser.add_option('-e', '--extent', dest='extent', nargs=4, 
     110                  help='Geographical extent (lat, lon pair)', type='float', 
     111                  action='callback', callback=parseExtent) 
    30112 
    31113parser.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 
     117parser.add_option('-d', '--dimensions', dest='dimensions', nargs=2, 
     118                  help='Pixel dimensions (width, height) of resulting image', type='int', 
     119                  action='callback', callback=parseWidthHeight) 
    33120 
    34121(options, args) = parser.parse_args() 
    35122 
    36123if __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() 
    37148 
    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)) 
    39158     
    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)) 
    46161     
    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 
     1from Core import Coordinate 
     2from Geo import LinearProjection, MercatorProjection, Transformation 
    23 
    34ids = ('MICROSOFT_ROAD', 'MICROSOFT_AERIAL', 'MICROSOFT_HYBRID', 
     
    78       'OPEN_STREET_MAP') 
    89 
    9 class Provider: 
    10     """ 
    11     """ 
     10class 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.") 
    1216 
    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) 
    2419 
    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
    2722 
    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  
     1import math 
     2 
    13import Tiles 
    24import Providers 
     5import Core 
     6import Geo 
     7import Microsoft 
     8 
     9TILE_WIDTH = 256 
     10TILE_HEIGHT = 256 
     11 
     12def 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 
     28def 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)