kopia lustrzana https://github.com/hholzgra/ocitysmap
				
				
				
			Support for complex city envelopes
Some OSM Ids such as 365634 have several entries in the polygon/line tables. The original maposmatic code crashed because it assumed only one entry was in the DB, and the ocitysmap code only considered the 1st entry returned by postgis. This patch allows to work on the union of all the areas returned by the polygon and line tables. It adds another dependency: shapely. This allows to call libgeos functions from within python. This also calls for some code refactoring (=> TODO): the coords et al. API should be replaced by the plain shapelib API (http://gispython.org/shapely/docs/1.2/manual.html).stable
							rodzic
							
								
									f595f4520d
								
							
						
					
					
						commit
						093b95bcf6
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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( | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 David Decotigny
						David Decotigny