maposmatic-dev
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Maposmatic-dev] [PATCH 01/22] Initial version of a multi-page renderer


From: Thomas Petazzoni
Subject: [Maposmatic-dev] [PATCH 01/22] Initial version of a multi-page renderer
Date: Fri, 30 Mar 2012 13:00:18 +0200

So far, it only outputs the map, split on several pages, with no grid,
no index, no page number. Only the raw maps.

Signed-off-by: Thomas Petazzoni <address@hidden>
---
 ocitysmap2/layoutlib/commons.py             |    3 +
 ocitysmap2/layoutlib/multi_page_renderer.py |  261 +++++++++++++++++++++++++++
 ocitysmap2/layoutlib/renderers.py           |    2 +
 ocitysmap2/maplib/map_canvas.py             |   21 ++-
 4 files changed, 280 insertions(+), 7 deletions(-)
 create mode 100644 ocitysmap2/layoutlib/multi_page_renderer.py

diff --git a/ocitysmap2/layoutlib/commons.py b/ocitysmap2/layoutlib/commons.py
index e2cceab..75959ad 100644
--- a/ocitysmap2/layoutlib/commons.py
+++ b/ocitysmap2/layoutlib/commons.py
@@ -30,3 +30,6 @@ def convert_pt_to_dots(pt, dpi = PT_PER_INCH):
 
 def convert_mm_to_pt(mm):
     return ((mm/10.0) / 2.54) * 72
+
+def convert_pt_to_mm(pt):
+    return (float(pt) * 10.0 * 2.54) / 72
diff --git a/ocitysmap2/layoutlib/multi_page_renderer.py 
b/ocitysmap2/layoutlib/multi_page_renderer.py
new file mode 100644
index 0000000..e1c1f24
--- /dev/null
+++ b/ocitysmap2/layoutlib/multi_page_renderer.py
@@ -0,0 +1,261 @@
+# -*- 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/>.
+
+import os
+import logging
+import tempfile
+import math
+import sys
+import cairo
+import mapnik
+import coords
+
+from abstract_renderer import Renderer
+
+from ocitysmap2.maplib.map_canvas import MapCanvas
+
+import ocitysmap2
+import commons
+
+LOG = logging.getLogger('ocitysmap')
+PAGE_STR = " - Page %(page_number)d"
+
+_MAPNIK_PROJECTION = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 " \
+                     "+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m   " \
+                     "address@hidden +no_defs +over"
+
+class MultiPageRenderer(Renderer):
+    """
+    This Renderer creates a multi-pages map, with all the classic overlayed
+    features and no index page.
+    """
+
+    name = 'multi_page'
+    description = 'A multi-page layout.'
+    multipages = True
+
+    def __init__(self, *args, **kwargs):
+        Renderer.__init__(self, *args, **kwargs)
+
+        print "One page width  (in mm) : %f" % self.rc.paper_width_mm
+        print "One page height (in mm) : %f" % self.rc.paper_height_mm
+
+        # Compute the usable area per page
+        self._usable_area_width_pt = (self.paper_width_pt -
+                                      (2 * Renderer.PRINT_SAFE_MARGIN_PT))
+        self._usable_area_height_pt = (self.paper_height_pt -
+                                       (2 * Renderer.PRINT_SAFE_MARGIN_PT))
+
+        scale_denom = 10000
+        OUTTER_MARGIN_MM = 20
+
+        print 'BoundingBox(%f,%f,%f,%f,"original", {color: "#00ff00"})' % \
+            (self.rc.bounding_box.get_top_left()[0],
+             self.rc.bounding_box.get_top_left()[1],
+             self.rc.bounding_box.get_bottom_right()[0],
+             self.rc.bounding_box.get_bottom_right()[1])
+
+        # Convert the original Bounding box into Mercator meters
+        self._proj = mapnik.Projection(_MAPNIK_PROJECTION)
+        orig_envelope = self._project_envelope(self.rc.bounding_box)
+
+        # Extend the bounding box to take into account the lost outter
+        # margin
+        off_x  = orig_envelope.minx - (OUTTER_MARGIN_MM * scale_denom) / 1000
+        off_y  = orig_envelope.miny - (OUTTER_MARGIN_MM * scale_denom) / 1000
+        width  = orig_envelope.width() + (2 * OUTTER_MARGIN_MM * scale_denom) 
/ 1000
+        height = orig_envelope.height() + (2 * OUTTER_MARGIN_MM * scale_denom) 
/ 1000
+
+        # Calculate the total width and height of paper needed to
+        # render the geographical area at the current scale.
+        total_width_pt   = commons.convert_mm_to_pt(float(width) * 1000 / 
scale_denom)
+        total_height_pt  = commons.convert_mm_to_pt(float(height) * 1000 / 
scale_denom)
+        outter_margin_pt = commons.convert_mm_to_pt(OUTTER_MARGIN_MM)
+
+        # Calculate the number of pages needed in both directions
+        if total_width_pt < self._usable_area_width_pt:
+            nb_pages_width = 1
+        else:
+            nb_pages_width = \
+                (float(total_width_pt - self._usable_area_width_pt) / \
+                     (self._usable_area_width_pt - outter_margin_pt)) + 1
+
+        if total_height_pt < self._usable_area_height_pt:
+            nb_pages_height = 1
+        else:
+            nb_pages_height = \
+                (float(total_height_pt - self._usable_area_height_pt) / \
+                     (self._usable_area_height_pt - outter_margin_pt)) + 1
+
+        # Round up the number of pages needed so that we have integer
+        # number of pages
+        nb_pages_width = int(math.ceil(nb_pages_width))
+        nb_pages_height = int(math.ceil(nb_pages_height))
+
+        # Calculate the entire paper area available
+        total_width_pt_after_extension = \
+            self._usable_area_width_pt + (self._usable_area_width_pt - 
outter_margin_pt) * (nb_pages_width - 1)
+        total_height_pt_after_extension = \
+            self._usable_area_height_pt + (self._usable_area_height_pt - 
outter_margin_pt) * (nb_pages_height - 1)
+
+        # Convert this paper area available in the number of Mercator
+        # meters that can we rendered on the map
+        total_width_merc = \
+            commons.convert_pt_to_mm(total_width_pt_after_extension) * 
scale_denom / 1000
+        total_height_merc = \
+            commons.convert_pt_to_mm(total_height_pt_after_extension) * 
scale_denom / 1000
+
+        # Extend the geographical boundaries so that we completely
+        # fill the available paper size. We are careful to extend the
+        # boundaries evenly on all directions (so the center of the
+        # previous boundaries remain the same as the new one)
+        off_x -= (total_width_merc - width) / 2
+        width = total_width_merc
+        off_y -= (total_height_merc - height) / 2
+        height = total_height_merc
+
+        # Calculate what is the final global bounding box that we will render
+        envelope = mapnik.Box2d(off_x, off_y, off_x + width, off_y + height)
+        self._geo_bbox = self._inverse_envelope(envelope)
+
+        print 'BoundingBox(%f,%f,%f,%f,"extended", {color: "#0f0f0f"})' % \
+            (self._geo_bbox.get_top_left()[0],
+             self._geo_bbox.get_top_left()[1],
+             self._geo_bbox.get_bottom_right()[0],
+             self._geo_bbox.get_bottom_right()[1])
+
+        # Convert the usable area on each sheet of paper into the
+        # amount of Mercator meters we can render in this area.
+        usable_area_merc_m_width  = 
commons.convert_pt_to_mm(self._usable_area_width_pt) * scale_denom / 1000
+        usable_area_merc_m_height = 
commons.convert_pt_to_mm(self._usable_area_height_pt) * scale_denom / 1000
+        outter_margin_merc_m      = (OUTTER_MARGIN_MM * scale_denom) / 1000
+
+        # Calculate all the bounding boxes that correspond to the
+        # geographical area that will be rendered on each sheet of
+        # paper.
+        bboxes = []
+        for j in reversed(range(0, nb_pages_height)):
+            for i in range(0, nb_pages_width):
+                cur_x = off_x + i * (usable_area_merc_m_width - 
outter_margin_merc_m)
+                cur_y = off_y + j * (usable_area_merc_m_height - 
outter_margin_merc_m)
+                envelope = mapnik.Box2d(cur_x, cur_y,
+                                        cur_x+usable_area_merc_m_width,
+                                        cur_y+usable_area_merc_m_height)
+                bboxes.append(self._inverse_envelope(envelope))
+
+        for i, bb in enumerate(bboxes):
+            print 'BoundingBox(%f,%f,%f,%f,"p%d")' % \
+                (bb.get_top_left()[0],
+                 bb.get_top_left()[1],
+                 bb.get_bottom_right()[0],
+                 bb.get_bottom_right()[1], i)
+
+        # Create the map canvas for each page
+        self.canvas = []
+        print "List of all bboxes"
+        for bb in bboxes:
+            print bb
+            # Create one canvas for each page
+            map_canvas = MapCanvas(self.rc.stylesheet,
+                                   bb, graphical_ratio=None)
+
+            map_canvas.render()
+
+            self.canvas.append(map_canvas)
+
+        self.map_coords = (Renderer.PRINT_SAFE_MARGIN_PT,
+                           Renderer.PRINT_SAFE_MARGIN_PT,
+                           self._usable_area_width_pt,
+                           self._usable_area_height_pt)
+
+        print self.canvas
+
+    def _project_envelope(self, bbox):
+        """Project the given bounding box into the rendering projection."""
+        envelope = mapnik.Box2d(bbox.get_top_left()[1],
+                                bbox.get_top_left()[0],
+                                bbox.get_bottom_right()[1],
+                                bbox.get_bottom_right()[0])
+        c0 = self._proj.forward(mapnik.Coord(envelope.minx, envelope.miny))
+        c1 = self._proj.forward(mapnik.Coord(envelope.maxx, envelope.maxy))
+        return mapnik.Box2d(c0.x, c0.y, c1.x, c1.y)
+
+    def _inverse_envelope(self, envelope):
+        """Inverse the given cartesian envelope (in 900913) back to a 4002
+        bounding box."""
+        c0 = self._proj.inverse(mapnik.Coord(envelope.minx, envelope.miny))
+        c1 = self._proj.inverse(mapnik.Coord(envelope.maxx, envelope.maxy))
+        return coords.BoundingBox(c0.y, c0.x, c1.y, c1.x)
+
+    def render(self, cairo_surface, dpi, osm_date):
+        ctx = cairo.Context(cairo_surface)
+        for c in self.canvas:
+            ctx.save()
+
+            # Prepare to draw the map at the right location
+            
ctx.translate(commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT),
+                          
commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT))
+
+            ctx.save()
+            rendered_map = c.get_rendered_map()
+            ctx.scale(commons.convert_pt_to_dots(self._usable_area_width_pt)
+                      / rendered_map.width,
+                      commons.convert_pt_to_dots(self._usable_area_height_pt)
+                      / rendered_map.height)
+            mapnik.render(rendered_map, ctx)
+            ctx.restore()
+
+            ctx.restore()
+
+            cairo_surface.show_page()
+        cairo_surface.flush()
+        print "I'm rendering"
+        pass
+
+    # Convert a length in geometric meters (in the real life) into a
+    # length in paper millimiters (as drawn on the map).
+    def _geo_m_to_paper_mm(self, geo_m):
+        return geo_m / 1000.0 * Renderer.DEFAULT_KM_IN_MM * 2
+
+    def _paper_mm_to_geo_m(self, paper_mm):
+        return paper_mm * 1000.0 / (Renderer.DEFAULT_KM_IN_MM * 2)
+
+    def _paper_pt_to_geo_m(self, paper_pt):
+        return self._paper_mm_to_geo_m(commons.convert_pt_to_mm(paper_pt))
+
+    # In multi-page mode, we only accept A4, A5 and US letter as paper
+    # sizes. The goal is to render booklets, not posters.
+    @staticmethod
+    def get_compatible_paper_sizes(bounding_box, zoom_level,
+                                   
resolution_km_in_mm=Renderer.DEFAULT_KM_IN_MM,
+                                   index_position=None, hsplit=1, vsplit=1):
+        valid_sizes = []
+        acceptable_formats = [ 'A5', 'A4', 'US letter' ]
+        for sz in ocitysmap2.layoutlib.PAPER_SIZES:
+            # Skip unsupported paper formats
+            if sz[0] not in acceptable_formats:
+                continue
+            valid_sizes.append((sz[0], sz[1], sz[2], True, True))
+        return valid_sizes
+
diff --git a/ocitysmap2/layoutlib/renderers.py 
b/ocitysmap2/layoutlib/renderers.py
index 40bbd85..7c739cd 100644
--- a/ocitysmap2/layoutlib/renderers.py
+++ b/ocitysmap2/layoutlib/renderers.py
@@ -1,12 +1,14 @@
 # -*- coding: utf-8 -*-
 
 import single_page_renderers
+import multi_page_renderer
 
 # The renderers registry
 _RENDERERS = [
     single_page_renderers.SinglePageRendererIndexBottom,
     single_page_renderers.SinglePageRendererIndexOnSide,
     single_page_renderers.SinglePageRendererNoIndex,
+    multi_page_renderer.MultiPageRenderer,
     ]
 
 def get_renderer_class_by_name(name):
diff --git a/ocitysmap2/maplib/map_canvas.py b/ocitysmap2/maplib/map_canvas.py
index 3ba0cd9..63bc099 100644
--- a/ocitysmap2/maplib/map_canvas.py
+++ b/ocitysmap2/maplib/map_canvas.py
@@ -47,13 +47,17 @@ class MapCanvas:
     their respective alpha levels.
     """
 
-    def __init__(self, stylesheet, bounding_box, _width, _height, dpi):
+    def __init__(self, stylesheet, bounding_box, _width, _height, dpi,
+                 extend_bbox_to_ratio=True):
         """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).
+            extend_bbox_to_ratio (boolean): allow MapCanvas to extend
+            the bounding box to make it match the ratio of the
+            provided rendering area. Needed by SinglePageRenderer.
         """
 
         self._proj = mapnik.Projection(_MAPNIK_PROJECTION)
@@ -65,20 +69,23 @@ class MapCanvas:
         orig_envelope = self._project_envelope(bounding_box)
         graphical_ratio = _width / _height
 
-        off_x, off_y, width, height = self._fix_bbox_ratio(
+        if extend_bbox_to_ratio:
+            off_x, off_y, width, height = self._fix_bbox_ratio(
                 orig_envelope.minx, orig_envelope.miny,
                 orig_envelope.width(), orig_envelope.height(),
                 graphical_ratio)
 
-        envelope = mapnik.Box2d(off_x, off_y, off_x+width, off_y+height)
+            envelope = mapnik.Box2d(off_x, off_y, off_x+width, off_y+height)
+            self._geo_bbox = self._inverse_envelope(envelope)
+            l.debug('Corrected bounding box from %s to %s, ratio: %.2f.' %
+                    (bounding_box, self._geo_bbox, graphical_ratio))
+        else:
+            envelope = orig_envelope
+            self._geo_bbox = bounding_box
 
-        self._geo_bbox = self._inverse_envelope(envelope)
         g_width  = int(convert_pt_to_dots(_width, dpi))
         g_height = int(convert_pt_to_dots(_height, dpi))
 
-        l.debug('Corrected bounding box from %s to %s, ratio: %.2f.' %
-                (bounding_box, self._geo_bbox, graphical_ratio))
-
         # Create the Mapnik map with the corrected width and height and zoom to
         # the corrected bounding box ('envelope' in the Mapnik jargon)
         self._map = mapnik.Map(g_width, g_height, _MAPNIK_PROJECTION)
-- 
1.7.4.1




reply via email to

[Prev in Thread] Current Thread [Next in Thread]