diff --git a/INSTALL.lucid b/INSTALL.lucid index e5f0ee6..de70f1f 100644 --- a/INSTALL.lucid +++ b/INSTALL.lucid @@ -226,7 +226,8 @@ are using. They have been tested on several x86_64 hosts. c. Install dependencies sudo aptitude install python-psycopg2 python-gdal \ - python-gtk2 python-cairo + python-gtk2 python-cairo \ + python-shapely Note that python-gtk2 is not needed for any graphical interface, but because it contains Pango and PangoCairo that we use to render diff --git a/ocitysmap2-render b/ocitysmap2-render index 674dad9..384c03a 100755 --- a/ocitysmap2-render +++ b/ocitysmap2-render @@ -65,7 +65,7 @@ def main(): default="My Map") parser.add_option('--polygon-osmid', dest='polygon_osmid', metavar='OSMID', help='OSM id representing the polygon of the city ' - 'to render.'), + 'to render.', type="int"), parser.add_option('-b', '--bounding-box', dest='bbox', nargs=2, metavar='LAT1,LON1 LAT2,LON2', help='Bounding box (EPSG: 4326).') @@ -131,10 +131,9 @@ def main(): osmid = None if options.polygon_osmid: try: - osmid = int(options.polygon_osmid) bbox = BoundingBox.parse_wkt( - mapper.get_geographic_info([osmid])[0][1]) - except ValueError: + mapper.get_geographic_info(options.polygon_osmid)[0]) + except LookupError: parser.error('Invalid polygon OSM id!\n') # Parse stylesheet (defaults to 1st one) diff --git a/ocitysmap2/__init__.py b/ocitysmap2/__init__.py index d8953cc..1fb2390 100644 --- a/ocitysmap2/__init__.py +++ b/ocitysmap2/__init__.py @@ -84,6 +84,9 @@ import psycopg2 import re import tempfile +import shapely +import shapely.wkt + import coords import i18n @@ -287,44 +290,71 @@ SELECT ST_AsText(ST_LongestLine( os.rmdir(os.path.join(root, name)) os.rmdir(tmpdir) - def get_geographic_info(self, osmids): - """Return a list of tuples (one tuple for each specified ID in - osmids) where each tuple contains (osmid, WKT_envelope, - WKT_buildarea)""" + def _get_geographic_info(self, osmid, table): + """Return the area for the given osm id in the given table, or raise + LookupError when not found + + Args: + osmid (integer): OSM ID + table (str): either 'polygon' or 'line' + + Return: + Geos geometry object + """ # Ensure all OSM IDs are integers, bust cast them back to strings # afterwards. - osmids = map(str, map(int, osmids)) - LOG.debug('Looking up bounding box and contour of OSM IDs %s...' - % osmids) + LOG.debug('Looking up bounding box and contour of OSM ID %d...' + % osmid) cursor = self._db.cursor() - cursor.execute("""select osm_id, - st_astext(st_transform(st_envelope(way), 4002)), - st_astext(st_transform(st_buildarea(way), 4002)) - from planet_osm_polygon where osm_id in (%s);""" % - ', '.join(osmids)) + cursor.execute("""select + st_astext(st_transform(st_buildarea(st_union(way)), + 4002)) + from planet_osm_%s where osm_id = %d + group by osm_id;""" % + (table, osmid)) records = cursor.fetchall() - try: - return map(lambda x: (x[0], x[1].strip(), x[2].strip()), records) - except (KeyError, IndexError, AttributeError): - raise AssertionError, 'Invalid database structure!' + ((wkt,),) = records + except ValueError: + raise LookupError("OSM ID %d not found in table %s" % + (osmid, table)) - def _get_shade_wkt(self, bounding_box, polygon): - """Creates a shade area for bounding_box with an inner hole for the - given polygon.""" - regexp_polygon = re.compile('^POLYGON\(\(([^)]*)\)\)$') - matches = regexp_polygon.match(polygon) - if not matches: - LOG.error('Administrative boundary looks invalid!') - return None - inside = matches.groups()[0] + return shapely.wkt.loads(wkt) - bounding_box = bounding_box.create_expanded(0.05, 0.05) - poly = "MULTIPOLYGON(((%s)),((%s)))" % \ - (bounding_box.as_wkt(with_polygon_statement = False), inside) - return poly + def get_geographic_info(self, osmid): + """Return a tuple (WKT_envelope, WKT_buildarea) or raise + LookupError when not found + + Args: + osmid (integer): OSM ID + + Return: + tuple (WKT bbox, WKT area) + """ + found = False + + # Scan polygon table: + try: + polygon_geom = self._get_geographic_info(osmid, 'polygon') + found = True + except LookupError: + polygon_geom = shapely.Polygon() + + # Scan line table: + try: + line_geom = self._get_geographic_info(osmid, 'line') + found = True + except LookupError: + line_geom = shapely.Polygon() + + # Merge results: + if not found: + raise LookupError("No such OSM id: %d" % osmid) + + result = polygon_geom.union(line_geom) + return (result.envelope.wkt, result.wkt) def get_all_style_configurations(self): """Returns the list of all available stylesheet configurations (list of @@ -372,18 +402,16 @@ SELECT ST_AsText(ST_LongestLine( # Determine bounding box and WKT of interest if config.osmid: - try: - osmid_geo_info = self.get_geographic_info([config.osmid])[0] - except IndexError: - raise AssertionError, 'OSM ID not found in the database!' + osmid_bbox, osmid_area \ + = self.get_geographic_info(config.osmid) # Define the bbox if not already defined if not config.bounding_box: config.bounding_box \ - = coords.BoundingBox.parse_wkt(osmid_geo_info[1]) + = coords.BoundingBox.parse_wkt(osmid_bbox) # Update the polygon WKT of interest - config.polygon_wkt = osmid_geo_info[2] + config.polygon_wkt = osmid_area else: # No OSM ID provided => use specified bbox config.polygon_wkt = config.bounding_box.as_wkt() diff --git a/ocitysmap2/coords.py b/ocitysmap2/coords.py index 7ee3e3d..1159956 100644 --- a/ocitysmap2/coords.py +++ b/ocitysmap2/coords.py @@ -24,6 +24,8 @@ import math +import shapely.wkt + EARTH_RADIUS = 6370986 # meters @@ -79,9 +81,12 @@ class BoundingBox: def parse_wkt(wkt): """Returns a BoundingBox object created from the coordinates of a polygon given in WKT format.""" - coords = [p.split(' ') for p in wkt[9:].split(',')] - return BoundingBox(coords[1][1], coords[1][0], - coords[3][1], coords[3][0]) + try: + geom_envelope = shapely.wkt.loads(wkt).bounds + except Exception, rx: + raise ValueError("Invalid input WKT: %s" % ex) + return BoundingBox(geom_envelope[1], geom_envelope[0], + geom_envelope[3], geom_envelope[2]) @staticmethod def parse_latlon_strtuple(points): diff --git a/ocitysmap2/layoutlib/abstract_renderer.py b/ocitysmap2/layoutlib/abstract_renderer.py index 25e2b6d..17f23b0 100644 --- a/ocitysmap2/layoutlib/abstract_renderer.py +++ b/ocitysmap2/layoutlib/abstract_renderer.py @@ -33,6 +33,7 @@ from ocitysmap2.maplib.map_canvas import MapCanvas from ocitysmap2.maplib.grid import Grid import commons from ocitysmap2 import maplib +import shapely.wkt import logging @@ -221,7 +222,7 @@ class Renderer: draw_contour_shade (bool): whether to draw a shade around the area of interest or not. - Return the MapCanvas object. + Return the MapCanvas object or raise ValueError. """ # Prepare the map canvas @@ -230,18 +231,16 @@ class Renderer: graphical_ratio) if draw_contour_shade: - # Determine the shade WKT - regexp_polygon = re.compile('^POLYGON\(\(([^)]*)\)\)$') - matches = regexp_polygon.match(self.rc.polygon_wkt) - if not matches: - LOG.error('Administrative boundary looks invalid!') - return None - inside = matches.groups()[0] + # Area to keep visible + interior = shapely.wkt.loads(self.rc.polygon_wkt) + # Surroundings to gray-out bounding_box \ = canvas.get_actual_bounding_box().create_expanded(0.05, 0.05) - shade_wkt = "MULTIPOLYGON(((%s)),((%s)))" % \ - (bounding_box.as_wkt(with_polygon_statement = False), inside) + exterior = shapely.wkt.loads(bounding_box.as_wkt()) + + # Determine the shade WKT + shade_wkt = exterior.difference(interior).wkt # Prepare the shade SHP shade_shape = maplib.shapes.PolyShapeFile(