2010-08-03 22:10:06 +00:00
|
|
|
# -*- 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
"""OCitySMap 2.
|
2010-08-07 09:52:39 +00:00
|
|
|
|
2010-08-08 09:30:54 +00:00
|
|
|
OCitySMap is a Mapnik-based map rendering engine from OpenStreetMap.org data.
|
|
|
|
|
It is architectured around the concept of Renderers, in charge of rendering the
|
|
|
|
|
map and all the visual features that go along with it (scale, grid, legend,
|
|
|
|
|
index, etc.) on the given paper size using a provided Mapnik stylesheet,
|
|
|
|
|
according to their implemented layout.
|
|
|
|
|
|
|
|
|
|
The PlainRenderer for example renders a full-page map with its grid, a title
|
|
|
|
|
header and copyright notice, but without the index.
|
|
|
|
|
|
|
|
|
|
How to use OCitySMap?
|
|
|
|
|
---------------------
|
|
|
|
|
|
|
|
|
|
The API of OCitySMap is very simple. First, you need to instanciate the main
|
|
|
|
|
OCitySMap class with the path to your OCitySMap configuration file (see
|
|
|
|
|
ocitysmap.conf-template):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ocitysmap = ocitysmap2.OCitySMap('/path/to/your/config')
|
|
|
|
|
|
|
|
|
|
The next step is to create a RenderingConfiguration, the object that
|
|
|
|
|
encapsulates all the information to parametize the rendering, including the
|
|
|
|
|
Mapnik stylesheet. You can retrieve the list of supported stylesheets (directly
|
|
|
|
|
as Stylesheet objects) with:
|
|
|
|
|
|
|
|
|
|
styles = ocitysmap.get_all_style_configurations()
|
|
|
|
|
|
|
|
|
|
Fill in your RenderingConfiguration with the map title, the OSM ID or bounding
|
|
|
|
|
box, the chosen map language, the Stylesheet object and the paper size (in
|
|
|
|
|
millimeters) and simply pass it to OCitySMap's render method:
|
|
|
|
|
|
|
|
|
|
ocitysmap.render(rendering_configuration, layout_name,
|
|
|
|
|
output_formats, prefix)
|
|
|
|
|
|
|
|
|
|
The layout name is the renderer's key name. You can get the list of all
|
|
|
|
|
supported renderers with ocitysmap.get_all_renderers(). The output_formats is a
|
|
|
|
|
list of output formats. For now, the following formats are supported:
|
|
|
|
|
|
|
|
|
|
* PNG at 300dpi
|
|
|
|
|
* PDF
|
|
|
|
|
* SVG
|
|
|
|
|
* SVGZ (gzipped-SVG)
|
|
|
|
|
* PS
|
|
|
|
|
|
|
|
|
|
The prefix is the filename prefix for all the rendered files. This is usually a
|
|
|
|
|
path to the destination's directory, eventually followed by some unique, yet
|
|
|
|
|
common prefix for the files rendered for a job.
|
2010-08-03 22:10:06 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
__author__ = 'The MapOSMatic developers'
|
|
|
|
|
__version__ = '0.2'
|
|
|
|
|
|
2010-08-04 14:34:15 +00:00
|
|
|
import cairo
|
|
|
|
|
import ConfigParser
|
2010-08-04 21:21:37 +00:00
|
|
|
import gzip
|
2010-08-04 14:34:15 +00:00
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import psycopg2
|
2010-08-04 22:47:07 +00:00
|
|
|
import re
|
2010-08-04 14:34:15 +00:00
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
import coords
|
|
|
|
|
import i18n
|
2010-09-11 10:31:08 +00:00
|
|
|
|
|
|
|
|
from indexlib.indexer import StreetIndex
|
|
|
|
|
from indexlib.commons import IndexDoesNotFitError
|
|
|
|
|
|
|
|
|
|
from layoutlib import renderers
|
|
|
|
|
import layoutlib.commons
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG = logging.getLogger('ocitysmap')
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-08-03 22:10:06 +00:00
|
|
|
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)
|
2010-08-04 14:34:15 +00:00
|
|
|
self.bounding_box = None # bbox (from osmid if None)
|
2010-08-03 22:10:06 +00:00
|
|
|
self.language = None # str (locale)
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-08-03 22:10:06 +00:00
|
|
|
self.stylesheet = None # Obj Stylesheet
|
2010-08-04 14:34:15 +00:00
|
|
|
|
|
|
|
|
self.paper_width_mm = None
|
|
|
|
|
self.paper_height_mm = None
|
2010-08-03 22:10:06 +00:00
|
|
|
|
2010-09-04 17:48:51 +00:00
|
|
|
# Setup by OCitySMap::render() from osmid and bounding_box fields:
|
2010-09-02 19:20:39 +00:00
|
|
|
self.polygon_wkt = None # str (WKT of interest)
|
|
|
|
|
|
2010-09-04 17:48:51 +00:00
|
|
|
# Setup by OCitySMap::render() from language field:
|
|
|
|
|
self.i18n = None # i18n object
|
|
|
|
|
|
|
|
|
|
|
2010-08-03 22:10:06 +00:00
|
|
|
class Stylesheet:
|
2010-08-05 14:58:55 +00:00
|
|
|
"""
|
|
|
|
|
A Stylesheet object defines how the map features will be rendered. It
|
|
|
|
|
contains information pointing to the Mapnik stylesheet and other styling
|
|
|
|
|
parameters.
|
|
|
|
|
"""
|
2010-09-11 13:00:41 +00:00
|
|
|
DEFAULT_ZOOM_LEVEL = 16
|
2010-08-05 14:58:55 +00:00
|
|
|
|
2010-08-03 22:10:06 +00:00
|
|
|
def __init__(self):
|
|
|
|
|
self.name = None # str
|
|
|
|
|
self.path = None # str
|
2010-08-05 14:58:55 +00:00
|
|
|
self.description = '' # str
|
2010-09-11 13:00:41 +00:00
|
|
|
self.zoom_level = Stylesheet.DEFAULT_ZOOM_LEVEL
|
2010-08-03 22:10:06 +00:00
|
|
|
|
2010-08-04 14:34:15 +00:00
|
|
|
self.grid_line_color = 'black'
|
2010-08-04 22:47:07 +00:00
|
|
|
self.grid_line_alpha = 0.5
|
2010-08-04 14:34:15 +00:00
|
|
|
self.grid_line_width = 3
|
2010-08-03 22:10:06 +00:00
|
|
|
|
2010-08-04 22:47:07 +00:00
|
|
|
self.shade_color = 'black'
|
|
|
|
|
self.shade_alpha = 0.1
|
|
|
|
|
|
2010-08-05 14:58:55 +00:00
|
|
|
@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)
|
2010-08-04 21:21:37 +00:00
|
|
|
|
2010-08-05 14:58:55 +00:00
|
|
|
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!'
|
|
|
|
|
|
2010-09-11 13:00:41 +00:00
|
|
|
return [Stylesheet.create_from_config_section(parser, name.strip())
|
2010-08-05 14:58:55 +00:00
|
|
|
for name in styles.split(',')]
|
2010-08-03 22:10:06 +00:00
|
|
|
|
|
|
|
|
class OCitySMap:
|
2010-08-07 09:52:39 +00:00
|
|
|
"""
|
2010-08-08 09:30:54 +00:00
|
|
|
This is the main entry point of the OCitySMap map rendering engine. Read
|
|
|
|
|
this module's documentation for more details on its API.
|
2010-08-07 09:52:39 +00:00
|
|
|
"""
|
2010-08-04 14:34:15 +00:00
|
|
|
|
|
|
|
|
DEFAULT_REQUEST_TIMEOUT_MIN = 15
|
|
|
|
|
|
2010-08-04 17:36:45 +00:00
|
|
|
DEFAULT_RESOLUTION_KM_IN_MM = 150
|
2010-08-06 10:04:20 +00:00
|
|
|
DEFAULT_RENDERING_PNG_DPI = 300
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-08-05 14:58:55 +00:00
|
|
|
STYLESHEET_REGISTRY = []
|
|
|
|
|
|
2010-09-11 13:00:41 +00:00
|
|
|
def __init__(self, config_files=None):
|
2010-08-07 09:52:39 +00:00
|
|
|
"""Instanciate a new configured OCitySMap instance.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config_file (string or list or None): path, or list of paths to
|
|
|
|
|
the OCitySMap configuration file(s). If None, sensible defaults
|
|
|
|
|
are tried.
|
|
|
|
|
"""
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-08-06 13:36:56 +00:00
|
|
|
if config_files is None:
|
|
|
|
|
config_files = ['/etc/ocitysmap.conf', '~/.ocitysmap.conf']
|
|
|
|
|
elif not isinstance(config_files, list):
|
|
|
|
|
config_files = [config_files]
|
|
|
|
|
|
2010-08-04 14:34:15 +00:00
|
|
|
config_files = map(os.path.expanduser, config_files)
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.info('Reading OCitySMap configuration from %s...' %
|
2010-08-04 14:34:15 +00:00
|
|
|
', '.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')
|
2010-08-05 21:52:40 +00:00
|
|
|
self.__db = None
|
|
|
|
|
|
|
|
|
|
# Read stylesheet configuration
|
|
|
|
|
self.STYLESHEET_REGISTRY = Stylesheet.create_all_from_config(self._parser)
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.debug('Found %d Mapnik stylesheets.'
|
|
|
|
|
% len(self.STYLESHEET_REGISTRY))
|
2010-08-05 21:52:40 +00:00
|
|
|
|
2010-09-11 10:31:08 +00:00
|
|
|
@property
|
|
|
|
|
def _db(self):
|
2010-08-05 21:52:40 +00:00
|
|
|
if self.__db:
|
|
|
|
|
return self.__db
|
2010-08-04 14:34:15 +00:00
|
|
|
|
|
|
|
|
# Database connection
|
|
|
|
|
datasource = dict(self._parser.items('datasource'))
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.info('Connecting to database %s on %s as %s...' %
|
|
|
|
|
(datasource['dbname'], datasource['host'],
|
|
|
|
|
datasource['user']))
|
2010-08-05 21:52:40 +00:00
|
|
|
|
|
|
|
|
db = psycopg2.connect(user=datasource['user'],
|
|
|
|
|
password=datasource['password'],
|
|
|
|
|
host=datasource['host'],
|
|
|
|
|
database=datasource['dbname'])
|
2010-08-04 14:34:15 +00:00
|
|
|
|
|
|
|
|
# Force everything to be unicode-encoded, in case we run along Django
|
|
|
|
|
# (which loads the unicode extensions for psycopg2)
|
2010-08-05 21:52:40 +00:00
|
|
|
db.set_client_encoding('utf8')
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-09-11 13:27:38 +00:00
|
|
|
# Make sure the DB is correctly installed
|
|
|
|
|
self._verify_db(db)
|
|
|
|
|
|
2010-08-04 14:34:15 +00:00
|
|
|
try:
|
2010-08-06 10:04:20 +00:00
|
|
|
timeout = int(self._parser.get('datasource', 'request_timeout'))
|
|
|
|
|
except (ConfigParser.NoOptionError, ValueError):
|
2010-08-04 14:34:15 +00:00
|
|
|
timeout = OCitySMap.DEFAULT_REQUEST_TIMEOUT_MIN
|
2010-08-05 21:52:40 +00:00
|
|
|
self._set_request_timeout(db, timeout)
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-08-05 21:52:40 +00:00
|
|
|
self.__db = db
|
|
|
|
|
return self.__db
|
|
|
|
|
|
2010-09-11 13:27:38 +00:00
|
|
|
def _verify_db(self, db):
|
|
|
|
|
"""Make sure the PostGIS DB is compatible with us."""
|
|
|
|
|
cursor = db.cursor()
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
SELECT ST_AsText(ST_LongestLine(
|
|
|
|
|
'POINT(100 100)'::geometry,
|
|
|
|
|
'LINESTRING(20 80, 98 190, 110 180, 50 75 )'::geometry)
|
|
|
|
|
) As lline;
|
|
|
|
|
""")
|
|
|
|
|
assert cursor.fetchall()[0][0] == "LINESTRING(100 100,98 190)", \
|
|
|
|
|
LOG.fatal("PostGIS >= 1.5 required for correct operation !")
|
|
|
|
|
|
2010-08-05 21:52:40 +00:00
|
|
|
def _set_request_timeout(self, db, timeout_minutes=15):
|
2010-08-04 14:34:15 +00:00
|
|
|
"""Sets the PostgreSQL request timeout to avoid long-running queries on
|
|
|
|
|
the database."""
|
2010-08-05 21:52:40 +00:00
|
|
|
cursor = db.cursor()
|
2010-08-04 14:34:15 +00:00
|
|
|
cursor.execute('set session statement_timeout=%d;' %
|
|
|
|
|
(timeout_minutes * 60 * 1000))
|
|
|
|
|
cursor.execute('show statement_timeout;')
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.debug('Configured statement timeout: %s.' %
|
2010-08-04 14:34:15 +00:00
|
|
|
cursor.fetchall()[0][0])
|
|
|
|
|
|
2010-08-07 09:52:39 +00:00
|
|
|
def _cleanup_tempdir(self, tmpdir):
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.debug('Cleaning up %s...' % tmpdir)
|
2010-08-07 09:52:39 +00:00
|
|
|
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)
|
|
|
|
|
|
2010-08-08 10:07:56 +00:00
|
|
|
def get_geographic_info(self, osmids):
|
2010-09-02 19:20:39 +00:00
|
|
|
"""Return a list of tuples (one tuple for each specified ID in
|
|
|
|
|
osmids) where each tuple contains (osmid, WKT_envelope,
|
|
|
|
|
WKT_buildarea)"""
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-08-08 10:07:56 +00:00
|
|
|
# Ensure all OSM IDs are integers, bust cast them back to strings
|
|
|
|
|
# afterwards.
|
|
|
|
|
osmids = map(str, map(int, osmids))
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.debug('Looking up bounding box and contour of OSM IDs %s...'
|
|
|
|
|
% osmids)
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-08-04 22:47:07 +00:00
|
|
|
cursor = self._db.cursor()
|
2010-08-08 10:07:56 +00:00
|
|
|
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))
|
2010-08-07 09:52:39 +00:00
|
|
|
records = cursor.fetchall()
|
|
|
|
|
|
2010-08-04 22:47:07 +00:00
|
|
|
try:
|
2010-08-08 10:07:56 +00:00
|
|
|
return map(lambda x: (x[0], x[1].strip(), x[2].strip()), records)
|
2010-08-04 22:47:07 +00:00
|
|
|
except (KeyError, IndexError, AttributeError):
|
2010-08-08 10:07:56 +00:00
|
|
|
raise AssertionError, 'Invalid database structure!'
|
2010-08-04 22:47:07 +00:00
|
|
|
|
2010-08-07 07:49:12 +00:00
|
|
|
def _get_shade_wkt(self, bounding_box, polygon):
|
2010-08-07 09:52:39 +00:00
|
|
|
"""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)
|
2010-08-04 22:47:07 +00:00
|
|
|
if not matches:
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.error('Administrative boundary looks invalid!')
|
2010-08-04 22:47:07 +00:00
|
|
|
return None
|
|
|
|
|
inside = matches.groups()[0]
|
|
|
|
|
|
|
|
|
|
bounding_box = bounding_box.create_expanded(0.05, 0.05)
|
2010-08-07 18:22:02 +00:00
|
|
|
poly = "MULTIPOLYGON(((%s)),((%s)))" % \
|
|
|
|
|
(bounding_box.as_wkt(with_polygon_statement = False), inside)
|
2010-08-04 22:47:07 +00:00
|
|
|
return poly
|
|
|
|
|
|
2010-08-03 22:10:06 +00:00
|
|
|
def get_all_style_configurations(self):
|
|
|
|
|
"""Returns the list of all available stylesheet configurations (list of
|
|
|
|
|
Stylesheet objects)."""
|
2010-08-05 14:58:55 +00:00
|
|
|
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
|
2010-08-03 22:10:06 +00:00
|
|
|
|
|
|
|
|
def get_all_renderers(self):
|
2010-08-08 09:30:54 +00:00
|
|
|
"""Returns the list of all available layout renderers (list of
|
|
|
|
|
Renderer classes)."""
|
|
|
|
|
return renderers.get_renderers()
|
|
|
|
|
|
|
|
|
|
def get_all_paper_sizes(self):
|
|
|
|
|
return renderers.get_paper_sizes()
|
2010-08-03 22:10:06 +00:00
|
|
|
|
2010-08-05 11:51:32 +00:00
|
|
|
def render(self, config, renderer_name, output_formats, file_prefix):
|
2010-08-03 22:10:06 +00:00
|
|
|
"""Renders a job with the given rendering configuration, using the
|
|
|
|
|
provided renderer, to the given output formats.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config (RenderingConfiguration): the rendering configuration
|
|
|
|
|
object.
|
2010-08-05 11:51:32 +00:00
|
|
|
renderer_name (string): the layout renderer to use for this rendering.
|
2010-08-03 22:10:06 +00:00
|
|
|
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.
|
|
|
|
|
"""
|
|
|
|
|
|
2010-08-05 21:52:40 +00:00
|
|
|
assert config.osmid or config.bounding_box, \
|
2010-08-04 14:34:15 +00:00
|
|
|
'At least an OSM ID or a bounding box must be provided!'
|
|
|
|
|
|
2010-08-04 21:21:37 +00:00
|
|
|
output_formats = map(lambda x: x.lower(), output_formats)
|
2010-09-04 17:48:51 +00:00
|
|
|
config.i18n = i18n.install_translation(config.language,
|
|
|
|
|
self._locale_path)
|
2010-08-05 15:47:09 +00:00
|
|
|
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.info('Rendering with renderer %s in language: %s (rtl: %s).' %
|
|
|
|
|
(renderer_name, config.i18n.language_code(),
|
|
|
|
|
config.i18n.isrtl()))
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-09-02 19:20:39 +00:00
|
|
|
# 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!'
|
|
|
|
|
|
|
|
|
|
# Define the bbox if not already defined
|
|
|
|
|
if not config.bounding_box:
|
|
|
|
|
config.bounding_box \
|
|
|
|
|
= coords.BoundingBox.parse_wkt(osmid_geo_info[1])
|
|
|
|
|
|
|
|
|
|
# Update the polygon WKT of interest
|
|
|
|
|
config.polygon_wkt = osmid_geo_info[2]
|
|
|
|
|
else:
|
|
|
|
|
# No OSM ID provided => use specified bbox
|
|
|
|
|
config.polygon_wkt = config.bounding_box.as_wkt()
|
2010-08-08 10:07:56 +00:00
|
|
|
|
2010-08-04 14:34:15 +00:00
|
|
|
# Make sure we have a bounding box
|
2010-09-02 19:20:39 +00:00
|
|
|
assert config.bounding_box is not None
|
|
|
|
|
assert config.polygon_wkt is not None
|
|
|
|
|
|
|
|
|
|
# Prepare the index
|
2010-09-11 10:31:08 +00:00
|
|
|
street_index = StreetIndex(self._db,
|
|
|
|
|
config.polygon_wkt,
|
|
|
|
|
config.i18n)
|
2010-08-04 14:34:15 +00:00
|
|
|
|
|
|
|
|
# Create a temporary directory for all our shape files
|
|
|
|
|
tmpdir = tempfile.mkdtemp(prefix='ocitysmap')
|
2010-09-02 21:43:24 +00:00
|
|
|
try:
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.debug('Rendering in temporary directory %s' % tmpdir)
|
2010-08-04 22:47:07 +00:00
|
|
|
|
2010-09-02 21:43:24 +00:00
|
|
|
# Prepare the generic renderer
|
|
|
|
|
renderer_cls = renderers.get_renderer_class_by_name(renderer_name)
|
|
|
|
|
renderer = renderer_cls(config, tmpdir, street_index)
|
2010-08-07 07:49:12 +00:00
|
|
|
|
2010-09-02 21:43:24 +00:00
|
|
|
# Update the street_index to reflect the grid's actual position
|
|
|
|
|
if renderer.grid:
|
|
|
|
|
street_index.apply_grid(renderer.grid)
|
2010-08-04 21:21:37 +00:00
|
|
|
|
2010-09-02 21:43:24 +00:00
|
|
|
# Perform the actual rendering to the Cairo devices
|
2010-08-04 21:21:37 +00:00
|
|
|
for output_format in output_formats:
|
|
|
|
|
output_filename = '%s.%s' % (file_prefix, output_format)
|
Support for PNG output identical to PDF
This has not been so easy because:
- we need some kind of scaling because by default 1 pt in PDF = 1 px
in PNG... And we would prefer to have 1 pt in PDF = 300/72. px
(assuming PNG is 300dpi)
- we cannot call ctx.scale() before rendering the index because pango
takes the transformation matrix into account when it chooses the
font metrics. So with ctx.scale(), we could have the actual font
metrics different from those computed in
precompute_index_occupation() { which is called on a PDF surface by
SinglePageRenderer::render() }.
- we cannot render to a 72dpi vector surface (eg. PDF) and then
project it with a scaling factor onto a 300 DPI PNG surface,
because the result is ugly (pixelized)
- we cannot push_group()/post_group().set_matrix(xx=factor,
yy=factor=)/set_source() for the same reasons (pixelized)
So the solution we adopt is to trick pango into believing it is
rendering without any scale factor, which for us corresponds to a
72dpi resolution, and which for it corresponds to a 96 dpi cairo
resolution (see sources): this should always generate the same font
metrics as precompute_index_occupation() did. Then we tell it that the
cairo resolution, instead of being 96dpi as it assumes, is
96*desired_resolution/72. This is what this patch does.
One more note: we don't use an ImageSurface cairo surface for PNG
output because, for some other reason, it leads to different font
metrics. So, for PNG, we do use a PDF cairo surface !...
2010-09-05 17:06:37 +00:00
|
|
|
try:
|
|
|
|
|
self._render_one(renderer, output_format, output_filename)
|
2010-09-11 10:31:08 +00:00
|
|
|
except IndexDoesNotFitError:
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.exception("The actual font metrics probably don't "
|
|
|
|
|
"match those pre-computed by the renderer's"
|
|
|
|
|
"constructor. Backtrace follows...")
|
2010-09-02 21:43:24 +00:00
|
|
|
|
|
|
|
|
# Also dump the CSV street index
|
2010-08-15 14:37:21 +00:00
|
|
|
street_index.write_to_csv(config.title, '%s.csv' % file_prefix)
|
2010-08-04 14:34:15 +00:00
|
|
|
finally:
|
|
|
|
|
self._cleanup_tempdir(tmpdir)
|
|
|
|
|
|
2010-09-02 21:43:24 +00:00
|
|
|
def _render_one(self, renderer, output_format, output_filename):
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.info('Rendering to %s format...' % output_format.upper())
|
2010-08-04 21:21:37 +00:00
|
|
|
|
|
|
|
|
factory = None
|
2010-09-11 10:31:08 +00:00
|
|
|
dpi = layoutlib.commons.PT_PER_INCH
|
2010-08-04 21:21:37 +00:00
|
|
|
|
|
|
|
|
if output_format == 'png':
|
2010-08-06 10:04:20 +00:00
|
|
|
try:
|
|
|
|
|
dpi = int(self._parser.get('rendering', 'png_dpi'))
|
|
|
|
|
except ConfigParser.NoOptionError:
|
|
|
|
|
dpi = OCitySMap.DEFAULT_RENDERING_PNG_DPI
|
|
|
|
|
|
Support for PNG output identical to PDF
This has not been so easy because:
- we need some kind of scaling because by default 1 pt in PDF = 1 px
in PNG... And we would prefer to have 1 pt in PDF = 300/72. px
(assuming PNG is 300dpi)
- we cannot call ctx.scale() before rendering the index because pango
takes the transformation matrix into account when it chooses the
font metrics. So with ctx.scale(), we could have the actual font
metrics different from those computed in
precompute_index_occupation() { which is called on a PDF surface by
SinglePageRenderer::render() }.
- we cannot render to a 72dpi vector surface (eg. PDF) and then
project it with a scaling factor onto a 300 DPI PNG surface,
because the result is ugly (pixelized)
- we cannot push_group()/post_group().set_matrix(xx=factor,
yy=factor=)/set_source() for the same reasons (pixelized)
So the solution we adopt is to trick pango into believing it is
rendering without any scale factor, which for us corresponds to a
72dpi resolution, and which for it corresponds to a 96 dpi cairo
resolution (see sources): this should always generate the same font
metrics as precompute_index_occupation() did. Then we tell it that the
cairo resolution, instead of being 96dpi as it assumes, is
96*desired_resolution/72. This is what this patch does.
One more note: we don't use an ImageSurface cairo surface for PNG
output because, for some other reason, it leads to different font
metrics. So, for PNG, we do use a PDF cairo surface !...
2010-09-05 17:06:37 +00:00
|
|
|
# As strange as it may seem, we HAVE to use a vector
|
|
|
|
|
# device here and not a raster device such as
|
|
|
|
|
# ImageSurface. Because, for some reason, with
|
|
|
|
|
# ImageSurface, the font metrics would NOT match those
|
|
|
|
|
# pre-computed by renderer_cls.__init__() and used to
|
2010-09-05 18:28:28 +00:00
|
|
|
# layout the whole page
|
|
|
|
|
def factory(w,h):
|
2010-09-11 10:31:08 +00:00
|
|
|
w_px = int(layoutlib.commons.convert_pt_to_dots(w, dpi))
|
|
|
|
|
h_px = int(layoutlib.commons.convert_pt_to_dots(h, dpi))
|
2010-09-05 18:28:28 +00:00
|
|
|
LOG.debug("Rendering PNG into %dpx x %dpx area..."
|
|
|
|
|
% (w_px, h_px))
|
|
|
|
|
return cairo.PDFSurface(None, w_px, h_px)
|
|
|
|
|
|
2010-08-04 21:21:37 +00:00
|
|
|
elif output_format == 'svg':
|
2010-09-02 21:43:24 +00:00
|
|
|
factory = lambda w,h: cairo.SVGSurface(output_filename, w, h)
|
2010-08-04 21:21:37 +00:00
|
|
|
elif output_format == 'svgz':
|
|
|
|
|
factory = lambda w,h: cairo.SVGSurface(
|
2010-09-02 21:43:24 +00:00
|
|
|
gzip.GzipFile(output_filename, 'wb'), w, h)
|
2010-08-04 21:21:37 +00:00
|
|
|
elif output_format == 'pdf':
|
2010-09-02 21:43:24 +00:00
|
|
|
factory = lambda w,h: cairo.PDFSurface(output_filename, w, h)
|
2010-08-04 21:21:37 +00:00
|
|
|
elif output_format == 'ps':
|
2010-09-02 21:43:24 +00:00
|
|
|
factory = lambda w,h: cairo.PSSurface(output_filename, w, h)
|
2010-09-05 18:30:28 +00:00
|
|
|
elif output_format == 'ps.gz':
|
|
|
|
|
factory = lambda w,h: cairo.PSSurface(
|
|
|
|
|
gzip.GzipFile(output_filename, 'wb'), w, h)
|
2010-08-05 21:52:40 +00:00
|
|
|
elif output_format == 'csv':
|
|
|
|
|
# We don't render maps into CSV.
|
|
|
|
|
return
|
|
|
|
|
|
2010-08-04 21:21:37 +00:00
|
|
|
else:
|
|
|
|
|
raise ValueError, \
|
|
|
|
|
'Unsupported output format: %s!' % output_format.upper()
|
|
|
|
|
|
2010-08-05 11:51:32 +00:00
|
|
|
surface = factory(renderer.paper_width_pt, renderer.paper_height_pt)
|
2010-09-02 21:43:24 +00:00
|
|
|
renderer.render(surface, dpi)
|
2010-08-06 10:04:20 +00:00
|
|
|
|
2010-09-05 18:27:47 +00:00
|
|
|
LOG.debug('Writing %s...' % output_filename)
|
2010-08-06 10:04:20 +00:00
|
|
|
if output_format == 'png':
|
2010-09-02 21:43:24 +00:00
|
|
|
surface.write_to_png(output_filename)
|
2010-08-06 10:04:20 +00:00
|
|
|
|
2010-08-04 21:21:37 +00:00
|
|
|
surface.finish()
|
|
|
|
|
|
2010-08-04 14:34:15 +00:00
|
|
|
if __name__ == '__main__':
|
2010-08-05 14:58:55 +00:00
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
|
|
2010-08-07 18:28:40 +00:00
|
|
|
o = OCitySMap([os.path.join(os.path.dirname(__file__), '..',
|
|
|
|
|
'ocitysmap.conf.mine')])
|
2010-08-04 14:34:15 +00:00
|
|
|
|
|
|
|
|
c = RenderingConfiguration()
|
2010-08-07 18:45:20 +00:00
|
|
|
c.title = 'Chevreuse, Yvelines, Île-de-France, France, Europe, Monde'
|
Support for PNG output identical to PDF
This has not been so easy because:
- we need some kind of scaling because by default 1 pt in PDF = 1 px
in PNG... And we would prefer to have 1 pt in PDF = 300/72. px
(assuming PNG is 300dpi)
- we cannot call ctx.scale() before rendering the index because pango
takes the transformation matrix into account when it chooses the
font metrics. So with ctx.scale(), we could have the actual font
metrics different from those computed in
precompute_index_occupation() { which is called on a PDF surface by
SinglePageRenderer::render() }.
- we cannot render to a 72dpi vector surface (eg. PDF) and then
project it with a scaling factor onto a 300 DPI PNG surface,
because the result is ugly (pixelized)
- we cannot push_group()/post_group().set_matrix(xx=factor,
yy=factor=)/set_source() for the same reasons (pixelized)
So the solution we adopt is to trick pango into believing it is
rendering without any scale factor, which for us corresponds to a
72dpi resolution, and which for it corresponds to a 96 dpi cairo
resolution (see sources): this should always generate the same font
metrics as precompute_index_occupation() did. Then we tell it that the
cairo resolution, instead of being 96dpi as it assumes, is
96*desired_resolution/72. This is what this patch does.
One more note: we don't use an ImageSurface cairo surface for PNG
output because, for some other reason, it leads to different font
metrics. So, for PNG, we do use a PDF cairo surface !...
2010-09-05 17:06:37 +00:00
|
|
|
c.osmid = -943886 # Chevreuse
|
|
|
|
|
# c.osmid = -7444 # Paris
|
2010-08-07 18:28:40 +00:00
|
|
|
c.language = 'fr_FR.UTF-8'
|
2010-08-07 17:06:56 +00:00
|
|
|
c.paper_width_mm = 297
|
|
|
|
|
c.paper_height_mm = 420
|
2010-08-05 14:58:55 +00:00
|
|
|
c.stylesheet = o.get_stylesheet_by_name('Default')
|
2010-08-04 14:34:15 +00:00
|
|
|
|
2010-09-05 18:30:28 +00:00
|
|
|
# c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
|
Support for PNG output identical to PDF
This has not been so easy because:
- we need some kind of scaling because by default 1 pt in PDF = 1 px
in PNG... And we would prefer to have 1 pt in PDF = 300/72. px
(assuming PNG is 300dpi)
- we cannot call ctx.scale() before rendering the index because pango
takes the transformation matrix into account when it chooses the
font metrics. So with ctx.scale(), we could have the actual font
metrics different from those computed in
precompute_index_occupation() { which is called on a PDF surface by
SinglePageRenderer::render() }.
- we cannot render to a 72dpi vector surface (eg. PDF) and then
project it with a scaling factor onto a 300 DPI PNG surface,
because the result is ugly (pixelized)
- we cannot push_group()/post_group().set_matrix(xx=factor,
yy=factor=)/set_source() for the same reasons (pixelized)
So the solution we adopt is to trick pango into believing it is
rendering without any scale factor, which for us corresponds to a
72dpi resolution, and which for it corresponds to a 96 dpi cairo
resolution (see sources): this should always generate the same font
metrics as precompute_index_occupation() did. Then we tell it that the
cairo resolution, instead of being 96dpi as it assumes, is
96*desired_resolution/72. This is what this patch does.
One more note: we don't use an ImageSurface cairo surface for PNG
output because, for some other reason, it leads to different font
metrics. So, for PNG, we do use a PDF cairo surface !...
2010-09-05 17:06:37 +00:00
|
|
|
o.render(c, 'single_page_index_bottom',
|
2010-09-05 18:30:28 +00:00
|
|
|
['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
|
|
|
|
|
'/tmp/mymap_index_bottom')
|
2010-09-05 10:29:49 +00:00
|
|
|
|
|
|
|
|
c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
|
Support for PNG output identical to PDF
This has not been so easy because:
- we need some kind of scaling because by default 1 pt in PDF = 1 px
in PNG... And we would prefer to have 1 pt in PDF = 300/72. px
(assuming PNG is 300dpi)
- we cannot call ctx.scale() before rendering the index because pango
takes the transformation matrix into account when it chooses the
font metrics. So with ctx.scale(), we could have the actual font
metrics different from those computed in
precompute_index_occupation() { which is called on a PDF surface by
SinglePageRenderer::render() }.
- we cannot render to a 72dpi vector surface (eg. PDF) and then
project it with a scaling factor onto a 300 DPI PNG surface,
because the result is ugly (pixelized)
- we cannot push_group()/post_group().set_matrix(xx=factor,
yy=factor=)/set_source() for the same reasons (pixelized)
So the solution we adopt is to trick pango into believing it is
rendering without any scale factor, which for us corresponds to a
72dpi resolution, and which for it corresponds to a 96 dpi cairo
resolution (see sources): this should always generate the same font
metrics as precompute_index_occupation() did. Then we tell it that the
cairo resolution, instead of being 96dpi as it assumes, is
96*desired_resolution/72. This is what this patch does.
One more note: we don't use an ImageSurface cairo surface for PNG
output because, for some other reason, it leads to different font
metrics. So, for PNG, we do use a PDF cairo surface !...
2010-09-05 17:06:37 +00:00
|
|
|
o.render(c, 'single_page_index_side',
|
2010-09-05 18:30:28 +00:00
|
|
|
['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
|
|
|
|
|
'/tmp/mymap_index_side')
|
|
|
|
|
|
|
|
|
|
o.render(c, 'plain',
|
|
|
|
|
['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
|
|
|
|
|
'/tmp/mymap_plain')
|