# -*- coding: utf-8 -*- # ocitysmap, city map and street index generator from OpenStreetMap data # Copyright (C) 2010 David Decotigny # Copyright (C) 2010 Frédéric Lehobey # Copyright (C) 2010 Pierre Mauduit # Copyright (C) 2010 David Mentré # Copyright (C) 2010 Maxime Petazzoni # Copyright (C) 2010 Thomas Petazzoni # Copyright (C) 2010 Gaël Utard # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """OCitySMap 2. """ __author__ = 'The MapOSMatic developers' __version__ = '0.2' import cairo import ConfigParser import gzip import logging import os import psycopg2 import re import tempfile import coords import i18n import index import renderers l = logging.getLogger('ocitysmap') class RenderingConfiguration: """ The RenderingConfiguration class encapsulate all the information concerning a rendering request. This data is used by the layout renderer, in conjonction with its rendering mode (defined by its implementation), to produce the map. """ def __init__(self): self.title = None # str self.osmid = None # None / int (shading + city name) self.bounding_box = None # bbox (from osmid if None) self.language = None # str (locale) self.stylesheet = None # Obj Stylesheet self.paper_width_mm = None self.paper_height_mm = None class Stylesheet: """ A Stylesheet object defines how the map features will be rendered. It contains information pointing to the Mapnik stylesheet and other styling parameters. """ def __init__(self): self.name = None # str self.path = None # str self.description = '' # str self.zoom_level = 16 self.grid_line_color = 'black' self.grid_line_alpha = 0.5 self.grid_line_width = 3 self.shade_color = 'black' self.shade_alpha = 0.1 @staticmethod def create_from_config_section(parser, section_name): """Creates a Stylesheet object from the OCitySMap configuration. Args: parser (ConfigParser.ConfigParser): the configuration parser object. section_name (string): the stylesheet section name in the configuration. """ s = Stylesheet() def assign_if_present(key, cast_fn=str): if parser.has_option(section_name, key): setattr(s, key, cast_fn(parser.get(section_name, key))) s.name = parser.get(section_name, 'name') s.path = parser.get(section_name, 'path') assign_if_present('description') assign_if_present('zoom_level', int) assign_if_present('grid_line_color') assign_if_present('grid_line_alpha', float) assign_if_present('grid_line_width', int) assign_if_present('shade_color') assign_if_present('shade_alpha', float) return s @staticmethod def create_all_from_config(parser): styles = parser.get('rendering', 'available_stylesheets') if not styles: raise ValueError, \ 'OCitySMap configuration does not contain any stylesheet!' return [Stylesheet.create_from_config_section(parser, name) for name in styles.split(',')] class OCitySMap: DEFAULT_REQUEST_TIMEOUT_MIN = 15 DEFAULT_ZOOM_LEVEL = 16 DEFAULT_RESOLUTION_KM_IN_MM = 150 STYLESHEET_REGISTRY = [] def __init__(self, config_files=['/etc/ocitysmap.conf', '~/.ocitysmap.conf'], grid_table_prefix=None): """...""" config_files = map(os.path.expanduser, config_files) l.info('Reading OCitySMap configuration from %s...' % ', '.join(config_files)) self._parser = ConfigParser.RawConfigParser() if not self._parser.read(config_files): raise IOError, 'None of the configuration files could be read!' self._locale_path = os.path.join(os.path.dirname(__file__), '..', 'locale') self._grid_table_prefix = '%sgrid_squares' % (grid_table_prefix or '') self.__db = None # Read stylesheet configuration self.STYLESHEET_REGISTRY = Stylesheet.create_all_from_config(self._parser) def _get_db(self): if self.__db: return self.__db # Database connection datasource = dict(self._parser.items('datasource')) l.info('Connecting to database %s on %s as %s...' % (datasource['dbname'], datasource['host'], datasource['user'])) db = psycopg2.connect(user=datasource['user'], password=datasource['password'], host=datasource['host'], database=datasource['dbname']) # Force everything to be unicode-encoded, in case we run along Django # (which loads the unicode extensions for psycopg2) db.set_client_encoding('utf8') try: timeout = self._parser.get('datasource', 'request_timeout') except ConfigParser.NoOptionError: timeout = OCitySMap.DEFAULT_REQUEST_TIMEOUT_MIN self._set_request_timeout(db, timeout) self.__db = db return self.__db _db = property(_get_db) def _set_request_timeout(self, db, timeout_minutes=15): """Sets the PostgreSQL request timeout to avoid long-running queries on the database.""" cursor = db.cursor() cursor.execute('set session statement_timeout=%d;' % (timeout_minutes * 60 * 1000)) cursor.execute('show statement_timeout;') l.debug('Configured statement timeout: %s.' % cursor.fetchall()[0][0]) def _get_bounding_box(self, osmid): l.debug('Searching bounding box around OSM ID %d...' % osmid) cursor = self._db.cursor() cursor.execute("""select st_astext(st_transform(st_envelope(way), 4002)) from planet_osm_polygon where osm_id=%d;""" % osmid) records = cursor.fetchall() if not records: raise ValueError, 'OSM ID %d not found in the database!' % osmid bbox = coords.BoundingBox.parse_wkt(records[0][0]) l.debug('Found bounding box %s.' % bbox) return bbox def _cleanup_tempdir(self, tmpdir): l.debug('Cleaning up %s...' % tmpdir) for root, dirs, files in os.walk(tmpdir, topdown=False): for name in files: os.remove(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) os.rmdir(tmpdir) _regexp_polygon = re.compile('^POLYGON\(\(([^)]*)\)\)$') def _get_shade_wkt(self, bounding_box, osmid): l.info('Looking for contour around OSM ID %d...' % osmid) cursor = self._db.cursor() cursor.execute("""select st_astext(st_transform(st_buildarea(way), 4002)) as polygon from planet_osm_polygon where osm_id=%d;""" % osmid) data = cursor.fetchall() try: polygon = data[0][0].strip() except (KeyError, IndexError, AttributeError): l.error('Invalid database structure!') return None if not polygon: return None matches = self._regexp_polygon.match(polygon) if not matches: l.error('Administrative boundary looks invalid!') return None inside = matches.groups()[0] bounding_box = bounding_box.create_expanded(0.05, 0.05) xmax, ymin = bounding_box.get_top_left() xmin, ymax = bounding_box.get_bottom_right() poly = "MULTIPOLYGON(((%f %f, %f %f, %f %f, %f %f, %f %f)),((%s)))" % \ (ymin, xmin, ymin, xmax, ymax, xmax, ymax, xmin, ymin, xmin, inside) return poly def get_all_style_configurations(self): """Returns the list of all available stylesheet configurations (list of Stylesheet objects).""" return self.STYLESHEET_REGISTRY def get_stylesheet_by_name(self, name): """Returns a stylesheet by its key name.""" for style in self.STYLESHEET_REGISTRY: if style.name == name: return style raise LookupError, 'The requested stylesheet %s was not found!' % name def get_all_renderers(self): """Returns the list of all available layout renderers (list of Renderer objects).""" pass def render(self, config, renderer_name, output_formats, file_prefix): """Renders a job with the given rendering configuration, using the provided renderer, to the given output formats. Args: config (RenderingConfiguration): the rendering configuration object. renderer_name (string): the layout renderer to use for this rendering. output_formats (list): a list of output formats to render to, from the list of supported output formats (pdf, svgz, etc.). file_prefix (string): filename prefix for all output files. """ assert config.osmid or config.bounding_box, \ 'At least an OSM ID or a bounding box must be provided!' output_formats = map(lambda x: x.lower(), output_formats) self._i18n = i18n.install_translation(config.language, self._locale_path) config.rtl = self._i18n.isrtl() l.info('Rendering language: %s (rtl: %s).' % (self._i18n.language_code(), config.rtl)) # Make sure we have a bounding box config.bounding_box = (config.bounding_box or self._get_bounding_box(config.osmid)) # Create a temporary directory for all our shape files tmpdir = tempfile.mkdtemp(prefix='ocitysmap') l.debug('Rendering in temporary directory %s' % tmpdir) renderer_cls = renderers.get_renderer_class_by_name(renderer_name) renderer = renderer_cls(config, tmpdir) renderer.create_map_canvas() if config.osmid: shade_wkt = self._get_shade_wkt( renderer.canvas.get_actual_bounding_box(), config.osmid) renderer.render_shade(shade_wkt) renderer.canvas.render() street_index = index.StreetIndex(config.osmid, renderer.canvas.get_actual_bounding_box(), config.language, renderer.grid) try: for output_format in output_formats: output_filename = '%s.%s' % (file_prefix, output_format) self._render_one(renderer, street_index, output_filename, output_format) # TODO: street_index.as_csv() finally: self._cleanup_tempdir(tmpdir) def _render_one(self, renderer, street_index, filename, output_format): l.info('Rendering %s...' % filename) factory = None if output_format == 'png': factory = lambda w,h: cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) elif output_format == 'svg': factory = lambda w,h: cairo.SVGSurface(filename, w, h) elif output_format == 'svgz': factory = lambda w,h: cairo.SVGSurface( gzip.GzipFile(filename, 'wb'), w, h) elif output_format == 'pdf': factory = lambda w,h: cairo.PDFSurface(filename, w, h) elif output_format == 'ps': factory = lambda w,h: cairo.PSSurface(filename, w, h) elif output_format == 'csv': # We don't render maps into CSV. return else: raise ValueError, \ 'Unsupported output format: %s!' % output_format.upper() surface = factory(renderer.paper_width_pt, renderer.paper_height_pt) renderer.render(surface, street_index) surface.finish() if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) o = OCitySMap(['/home/sam/src/python/maposmatic/ocitysmap/ocitysmap.conf.mine']) c = RenderingConfiguration() c.title = 'Chevreuse' c.osmid = -943886 # -7444 (Paris) c.language = 'fr_FR' c.paper_width_mm = 210 c.paper_height_mm = 297 c.stylesheet = o.get_stylesheet_by_name('Default') o.render(c, 'plain', ['png', 'pdf'], '/tmp/mymap')