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
David Decotigny 2010-10-10 15:54:55 +02:00
rodzic f595f4520d
commit 093b95bcf6
5 zmienionych plików z 85 dodań i 53 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()

Wyświetl plik

@ -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):

Wyświetl plik

@ -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(