diff --git a/ocitysmap2/__init__.py b/ocitysmap2/__init__.py index 1587b3d..71bd20b 100644 --- a/ocitysmap2/__init__.py +++ b/ocitysmap2/__init__.py @@ -30,6 +30,7 @@ __version__ = '0.2' import cairo import ConfigParser +import gzip import logging import os import psycopg2 @@ -37,6 +38,7 @@ import tempfile import coords import i18n +import index import renderers l = logging.getLogger('ocitysmap') @@ -72,6 +74,8 @@ class Stylesheet: self.grid_line_alpha = 0.8 self.grid_line_width = 3 + self.zoom_level = 16 + class OCitySMap: @@ -114,7 +118,6 @@ class OCitySMap: timeout = OCitySMap.DEFAULT_REQUEST_TIMEOUT_MIN self._set_request_timeout(timeout) - def _set_request_timeout(self, timeout_minutes=15): """Sets the PostgreSQL request timeout to avoid long-running queries on the database.""" @@ -175,7 +178,9 @@ class OCitySMap: assert config.osmid or config.bbox, \ 'At least an OSM ID or a bounding box must be provided!' - self._i18n = i18n.install_translation(config.language, self._locale_path) + output_formats = map(lambda x: x.lower(), output_formats) + self._i18n = i18n.install_translation(config.language, + self._locale_path) l.info('Rendering language: %s.' % self._i18n.language_code()) # Make sure we have a bounding box @@ -186,13 +191,54 @@ class OCitySMap: tmpdir = tempfile.mkdtemp(prefix='ocitysmap') l.debug('Rendering in temporary directory %s' % tmpdir) + canvas, grid = renderer.create_map_canvas(config, tmpdir) + canvas.render() + + street_index = index.StreetIndex(config.osmid, + canvas.get_actual_bounding_box(), + config.language, grid) + try: - surface = cairo.PDFSurface('/tmp/plain.pdf', 2000, 2000) - renderer.render(config, surface, None, tmpdir) - surface.finish() + for output_format in output_formats: + output_filename = '%s.%s' % (file_prefix, output_format) + self._render_one(config, canvas, renderer, street_index, + output_filename, output_format) + + # TODO: street_index.as_csv() finally: self._cleanup_tempdir(tmpdir) + + def _render_one(self, config, canvas, renderer, street_index, + filename, output_format): + l.info('Rendering %s...' % filename) + + factory = None + + if output_format == 'png': + raise NotImplementedError + + 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.PDFSurface(filename, w, h) + + else: + raise ValueError, \ + 'Unsupported output format: %s!' % output_format.upper() + + surface = factory(renderers.Renderer.convert_mm_to_pt(config.paper_width_mm), + renderers.Renderer.convert_mm_to_pt(config.paper_height_mm)) + + renderer.render(config, canvas, surface, street_index) + surface.finish() + if __name__ == '__main__': s = Stylesheet() s.name = 'osm' @@ -210,4 +256,4 @@ if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) o = OCitySMap(['/home/sam/src/python/maposmatic/ocitysmap/ocitysmap.conf.mine']) - o.render(c, renderers.PlainRenderer(), None, None) + o.render(c, renderers.PlainRenderer(), ['pdf', 'svgz'], '/tmp/mymap') diff --git a/ocitysmap2/index.py b/ocitysmap2/index.py index 1f570e3..6e6521f 100644 --- a/ocitysmap2/index.py +++ b/ocitysmap2/index.py @@ -36,12 +36,15 @@ class StreetIndex: self._language = language self._grid = grid + # TODO: seed index data from bounding box or osmid + def render(self, surface, x, y, w, h): """Render the street and amenities index at the given (x,y) coordinates into the provided Cairo surface. The index must not be larger than the provided width and height (in pixels). Args: + surface (cairo.Surface): the cairo surface to render into. x (int): horizontal origin position, in pixels. y (int): vertical origin position, in pixels. w (int): maximum usable width for the index, in pixels. diff --git a/ocitysmap2/map_canvas.py b/ocitysmap2/map_canvas.py index 4eb9ee8..df26de7 100644 --- a/ocitysmap2/map_canvas.py +++ b/ocitysmap2/map_canvas.py @@ -42,13 +42,13 @@ class MapCanvas: their respective alpha levels. """ - def __init__(self, stylesheet, bounding_box, graphical_ratio, - zoom_level): + def __init__(self, stylesheet, bounding_box, graphical_ratio): """Initialize the map canvas for rendering. Args: stylesheet (Stylesheet): map stylesheet. bounding_box (coords.BoundingBox): geographic bounding box. + graphical_ratio (float): ratio of the map area (width/height). """ self._proj = mapnik.Projection(_MAIN_PROJECTION) @@ -65,7 +65,8 @@ class MapCanvas: off_x+width, off_y+height) self._geo_bbox = self._inverse_envelope(envelope) - g_width, g_height = self._geo_bbox.get_pixel_size_for_zoom_factor(zoom_level) + g_width, g_height = self._geo_bbox.get_pixel_size_for_zoom_factor( + stylesheet.zoom_level) l.debug('Corrected bounding box from %s to %s (ratio: %.2f).' % (bounding_box, self._geo_bbox, graphical_ratio)) @@ -121,6 +122,7 @@ class MapCanvas: for shape in self._shapes: self._render_shape_file(**shape) + def get_rendered_map(self): return self._map def get_actual_bounding_box(self): @@ -165,13 +167,14 @@ if __name__ == '__main__': class StylesheetMock: def __init__(self): self.path = '/home/sam/src/python/maposmatic/mapnik-osm/osm.xml' + self.zoom_level = 16 logging.basicConfig(level=logging.DEBUG) # Basic unit test bbox = coords.BoundingBox(48.7148, 2.0155, 48.6950, 2.0670) - canvas = MapCanvas(StylesheetMock(), bbox, 297.0/210, 16) + canvas = MapCanvas(StylesheetMock(), bbox, 297.0/210) new_bbox = canvas.get_actual_bounding_box() diff --git a/ocitysmap2/renderers.py b/ocitysmap2/renderers.py index 4f68fe9..b980cdf 100644 --- a/ocitysmap2/renderers.py +++ b/ocitysmap2/renderers.py @@ -22,6 +22,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import cairo import logging import mapnik import os @@ -57,51 +58,69 @@ class Renderer: ('40x40cm', 400, 400), ] - def render(self, rc, surface, street_index, zoom_level, tmpdir): + + def _create_map_canvas(self, rc, graphical_ratio, tmpdir): + canvas = map_canvas.MapCanvas(rc.stylesheet, rc.bounding_box, + graphical_ratio) + + _grid = grid.Grid(canvas.get_actual_bounding_box()) + grid_shape = _grid.generate_shape_file(os.path.join(tmpdir, 'grid.shp')) + canvas.add_shape_file(grid_shape, + rc.stylesheet.grid_line_color, + rc.stylesheet.grid_line_alpha, + rc.stylesheet.grid_line_width) + + return canvas, _grid + + def create_map_canvas(self, rc, tmpdir): + """Returns the map canvas object and the grid object that has been + overlayed on the created map. + + Args: + rc (RenderingConfiguration): the rendering configuration. + tmpdir (path): path to a directory for temporary shape files. + """ + raise NotImplementedError + + def render(self, rc, canvas, surface, street_index): raise NotImplementedError def get_compatible_paper_sizes(self, bounding_box, zoom_level, resolution_km_in_mm): raise NotImplementedError + @staticmethod + def convert_mm_to_pt(mm): + return ((mm/10.0) / 2.54) * 72 + class PlainRenderer(Renderer): def __init__(self): self.name = 'plain' self.description = 'A basic, full-page layout for the map.' - def render(self, rc, surface, street_index, zoom_level, tmpdir): + def create_map_canvas(self, rc, tmpdir): + return self._create_map_canvas(rc, (float(rc.paper_width_mm) / + rc.paper_height_mm), tmpdir) + + def render(self, rc, canvas, surface, street_index): """...""" l.info('PlainRenderer rendering on %dx%dmm paper.' % (rc.paper_width_mm, rc.paper_height_mm)) - canvas = map_canvas.MapCanvas(rc.stylesheet, rc.bounding_box, - (float(rc.paper_width_mm) / - rc.paper_height_mm), - zoom_level) + rendered_map = canvas.get_rendered_map() - grid_shape = (grid.Grid(canvas.get_actual_bounding_box()) - .generate_shape_file(os.path.join(tmpdir, 'grid.shp'))) - canvas.add_shape_file(grid_shape, - rc.stylesheet.grid_line_color, - rc.stylesheet.grid_line_alpha, - rc.stylesheet.grid_line_width) - - rendered_map = canvas.render() ctx = cairo.Context(surface) - - def mm_to_pt(mm): - return ((mm/10.0) / 2.54) * 72 - - ctx.scale(mm_to_pt(rc.paper_width_mm) / rendered_map.width, - mm_to_pt(rc.paper_height_mm) / rendered_map.height) - - mapnik.render(rendered_map, ctx) - surface.flush() + ctx.scale(Renderer.convert_mm_to_pt(rc.paper_width_mm) / + rendered_map.width, + Renderer.convert_mm_to_pt(rc.paper_height_mm) / + rendered_map.height) # TODO: scale # TODO: compass rose + mapnik.render(rendered_map, ctx) + surface.flush() return surface def get_compatible_paper_sizes(self, bounding_box, zoom_level, @@ -148,7 +167,8 @@ if __name__ == '__main__': plain = PlainRenderer() - papers = plain.get_compatible_paper_sizes(bbox, zoom, resolution_km_in_mm=150) + papers = plain.get_compatible_paper_sizes(bbox, zoom, + resolution_km_in_mm=110) print 'Compatible paper sizes:' for p in papers: print ' * %s (%.1fx%.1fcm)' % (p[0], p[1]/10.0, p[2]/10.0) @@ -160,6 +180,7 @@ if __name__ == '__main__': self.grid_line_color = 'black' self.grid_line_alpha = 0.9 self.grid_line_width = 2 + self.zoom_level = 16 class RenderingConfigurationMock: def __init__(self): @@ -171,13 +192,13 @@ if __name__ == '__main__': config = RenderingConfigurationMock() - def mm_to_pt(mm): - return ((mm/10.0) / 2.54) * 72 - surface = cairo.PDFSurface('/tmp/plain.pdf', - mm_to_pt(config.paper_width_mm), - mm_to_pt(config.paper_height_mm)) - plain.render(config, surface, None, zoom, '/tmp') + Renderer.convert_mm_to_pt(config.paper_width_mm), + Renderer.convert_mm_to_pt(config.paper_height_mm)) + + canvas, _ = plain.create_map_canvas(config, '/tmp') + canvas.render() + plain.render(config, canvas, surface, None) surface.finish()