ocitysmap/ocitysmap2/layoutlib/multi_page_renderer.py

788 wiersze
34 KiB
Python

# -*- coding: utf-8 -*-
# ocitysmap, city map and street index generator from OpenStreetMap data
# Copyright (C) 2012 David Mentré
# Copyright (C) 2012 Thomas Petazzoni
# Copyright (C) 2012 Gaël Utard
# Copyright (C) 2012 Étienne Loks
# 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
assert mapnik.mapnik_version >= 200100, \
"Mapnik module version %s is too old, see ocitysmap's INSTALL " \
"for more details." % mapnik.mapnik_version_string()
import coords
import locale
import pangocairo
import pango
import datetime
from itertools import groupby
from abstract_renderer import Renderer
from ocitysmap2.maplib.map_canvas import MapCanvas
from ocitysmap2.maplib.grid import Grid
from ocitysmap2.maplib.overview_grid import OverviewGrid
from indexlib.indexer import StreetIndex
from indexlib.multi_page_renderer import MultiPageStreetIndexRenderer
import ocitysmap2
import commons
import shapely.wkt
from ocitysmap2 import maplib
from ocitysmap2 import draw_utils
from indexlib.commons import IndexCategory
LOG = logging.getLogger('ocitysmap')
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, db, rc, tmpdir, dpi, file_prefix):
Renderer.__init__(self, db, rc, tmpdir, dpi)
self._grid_legend_margin_pt = \
min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt,
Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt)
# 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 = Renderer.DEFAULT_SCALE
# the mapnik scale depends on the latitude. However we are
# always using Mapnik conversion functions (lat,lon <->
# mercator_meters) so we don't need to take into account
# latitude in following computations
# by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi
scale_denom *= float(72) / 90
GRAYED_MARGIN_MM = 10
OVERLAP_MARGIN_MM = 20
# Debug: show original bounding box as JS code
# print self.rc.bounding_box.as_javascript("original", "#00ff00")
# Convert the original Bounding box into Mercator meters
self._proj = mapnik.Projection(coords._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 - (GRAYED_MARGIN_MM * scale_denom) / 1000
off_y = orig_envelope.miny - (GRAYED_MARGIN_MM * scale_denom) / 1000
width = orig_envelope.width() + (2 * GRAYED_MARGIN_MM * scale_denom) / 1000
height = orig_envelope.height() + (2 * GRAYED_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)
self.grayed_margin_pt = commons.convert_mm_to_pt(GRAYED_MARGIN_MM)
overlap_margin_pt = commons.convert_mm_to_pt(OVERLAP_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 - overlap_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 - overlap_margin_pt)) + 1
# Round up the number of pages needed so that we have integer
# number of pages
self.nb_pages_width = int(math.ceil(nb_pages_width))
self.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 - overlap_margin_pt) * (self.nb_pages_width - 1)
total_height_pt_after_extension = self._usable_area_height_pt + \
(self._usable_area_height_pt - overlap_margin_pt) * (self.nb_pages_height - 1)
# Convert this paper area available in the number of Mercator
# meters that can be 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)
# Debug: show transformed bounding box as JS code
# print self._geo_bbox.as_javascript("extended", "#0f0f0f")
# 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
grayed_margin_merc_m = (GRAYED_MARGIN_MM * scale_denom) / 1000
overlap_margin_merc_m = (OVERLAP_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.
area_polygon = shapely.wkt.loads(self.rc.polygon_wkt)
bboxes = []
self.page_disposition, map_number = {}, 0
for j in reversed(range(0, self.nb_pages_height)):
col = self.nb_pages_height - j - 1
self.page_disposition[col] = []
for i in range(0, self.nb_pages_width):
cur_x = off_x + i * (usable_area_merc_m_width - overlap_margin_merc_m)
cur_y = off_y + j * (usable_area_merc_m_height - overlap_margin_merc_m)
envelope = mapnik.Box2d(cur_x, cur_y,
cur_x+usable_area_merc_m_width,
cur_y+usable_area_merc_m_height)
envelope_inner = mapnik.Box2d(cur_x + grayed_margin_merc_m,
cur_y + grayed_margin_merc_m,
cur_x + usable_area_merc_m_width - grayed_margin_merc_m,
cur_y + usable_area_merc_m_height - grayed_margin_merc_m)
inner_bb = self._inverse_envelope(envelope_inner)
if not area_polygon.disjoint(shapely.wkt.loads(
inner_bb.as_wkt())):
self.page_disposition[col].append(map_number)
map_number += 1
bboxes.append((self._inverse_envelope(envelope),
inner_bb))
else:
self.page_disposition[col].append(None)
# Debug: show per-page bounding boxes as JS code
# for i, (bb, bb_inner) in enumerate(bboxes):
# print bb.as_javascript(name="p%d" % i)
self.pages = []
# Create an overview map
overview_bb = self._geo_bbox.create_expanded(0.001, 0.001)
# Create the overview grid
self.overview_grid = OverviewGrid(overview_bb,
[bb_inner for bb, bb_inner in bboxes], self.rc.i18n.isrtl())
grid_shape = self.overview_grid.generate_shape_file(
os.path.join(self.tmpdir, 'grid_overview.shp'))
# Create a canvas for the overview page
self.overview_canvas = MapCanvas(self.rc.stylesheet,
overview_bb, self._usable_area_width_pt,
self._usable_area_height_pt, dpi,
extend_bbox_to_ratio=True)
# Create the gray shape around the overview map
exterior = shapely.wkt.loads(self.overview_canvas.get_actual_bounding_box()\
.as_wkt())
interior = shapely.wkt.loads(self.rc.polygon_wkt)
shade_wkt = exterior.difference(interior).wkt
shade = maplib.shapes.PolyShapeFile(self.rc.bounding_box,
os.path.join(self.tmpdir, 'shape_overview.shp'),
'shade-overview')
shade.add_shade_from_wkt(shade_wkt)
self.overview_canvas.add_shape_file(shade)
self.overview_canvas.add_shape_file(grid_shape,
self.rc.stylesheet.grid_line_color, 1,
self.rc.stylesheet.grid_line_width)
self.overview_canvas.render()
# Create the map canvas for each page
indexes = []
for i, (bb, bb_inner) in enumerate(bboxes):
# Create the gray shape around the map
exterior = shapely.wkt.loads(bb.as_wkt())
interior = shapely.wkt.loads(bb_inner.as_wkt())
shade_wkt = exterior.difference(interior).wkt
shade = maplib.shapes.PolyShapeFile(
bb, os.path.join(self.tmpdir, 'shade%d.shp' % i),
'shade%d' % i)
shade.add_shade_from_wkt(shade_wkt)
# Create the contour shade
# Area to keep visible
interior_contour = shapely.wkt.loads(self.rc.polygon_wkt)
# Determine the shade WKT
shade_contour_wkt = interior.difference(interior_contour).wkt
# Prepare the shade SHP
shade_contour = maplib.shapes.PolyShapeFile(bb,
os.path.join(self.tmpdir, 'shade_contour%d.shp' % i),
'shade_contour%d' % i)
shade_contour.add_shade_from_wkt(shade_contour_wkt)
# Create one canvas for the current page
map_canvas = MapCanvas(self.rc.stylesheet,
bb, self._usable_area_width_pt,
self._usable_area_height_pt, dpi,
extend_bbox_to_ratio=False)
# Create the grid
map_grid = Grid(bb_inner, map_canvas.get_actual_scale(), self.rc.i18n.isrtl())
grid_shape = map_grid.generate_shape_file(
os.path.join(self.tmpdir, 'grid%d.shp' % i))
map_canvas.add_shape_file(shade)
map_canvas.add_shape_file(shade_contour,
self.rc.stylesheet.shade_color_2,
self.rc.stylesheet.shade_alpha_2)
map_canvas.add_shape_file(grid_shape,
self.rc.stylesheet.grid_line_color,
self.rc.stylesheet.grid_line_alpha,
self.rc.stylesheet.grid_line_width)
map_canvas.render()
self.pages.append((map_canvas, map_grid))
# Create the index for the current page
inside_contour_wkt = interior_contour.intersection(interior).wkt
index = StreetIndex(self.db,
inside_contour_wkt,
self.rc.i18n, page_number=(i + 4))
index.apply_grid(map_grid)
indexes.append(index)
# Merge all indexes
self.index_categories = self._merge_page_indexes(indexes)
# Prepare the small map for the front page
self._front_page_map = self._prepare_front_page_map(dpi)
def _merge_page_indexes(self, indexes):
# First, we split street categories and "other" categories,
# because we sort them and we don't want to have the "other"
# categories intermixed with the street categories. This
# sorting is required for the groupby Python operator to work
# properly.
all_categories_streets = []
all_categories_others = []
for page_number, idx in enumerate(indexes):
for cat in idx.categories:
# Split in two lists depending on the category type
# (street or other)
if cat.is_street:
all_categories_streets.append(cat)
else:
all_categories_others.append(cat)
all_categories_streets_merged = \
self._merge_index_same_categories(all_categories_streets, is_street=True)
all_categories_others_merged = \
self._merge_index_same_categories(all_categories_others, is_street=False)
all_categories_merged = \
all_categories_streets_merged + all_categories_others_merged
return all_categories_merged
def _merge_index_same_categories(self, categories, is_street=True):
# Sort by categories. Now we may have several consecutive
# categories with the same name (i.e category for letter 'A'
# from page 1, category for letter 'A' from page 3).
categories.sort(key=lambda s:s.name)
categories_merged = []
for category_name,grouped_categories in groupby(categories,
key=lambda s:s.name):
# Group the different IndexItem from categories having the
# same name. The groupby() function guarantees us that
# categories with the same name are grouped together in
# grouped_categories[].
grouped_items = []
for cat in grouped_categories:
grouped_items.extend(cat.items)
# Re-sort alphabetically all the IndexItem according to
# the street name.
prev_locale = locale.getlocale(locale.LC_COLLATE)
locale.setlocale(locale.LC_COLLATE, self.rc.i18n.language_code())
try:
grouped_items_sorted = \
sorted(grouped_items,
lambda x,y: locale.strcoll(x.label, y.label))
finally:
locale.setlocale(locale.LC_COLLATE, prev_locale)
self._blank_duplicated_names(grouped_items_sorted)
# Rebuild a IndexCategory object with the list of merged
# and sorted IndexItem
categories_merged.append(
IndexCategory(category_name, grouped_items_sorted, is_street))
return categories_merged
# We set the label to empty string in case of duplicated item. In
# multi-page renderer we won't draw the dots in that case
def _blank_duplicated_names(self, grouped_items_sorted):
prev_label = ''
for item in grouped_items_sorted:
if prev_label == item.label:
item.label = ''
else:
prev_label = item.label
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 _prepare_front_page_map(self, dpi):
front_page_map_w = \
self._usable_area_width_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
front_page_map_h = \
(self._usable_area_height_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT) / 2
# Create the nice small map
front_page_map = \
MapCanvas(self.rc.stylesheet,
self.rc.bounding_box,
front_page_map_w,
front_page_map_h,
dpi,
extend_bbox_to_ratio=True)
# Add the shape that greys out everything that is outside of
# the administrative boundary.
exterior = shapely.wkt.loads(front_page_map.get_actual_bounding_box().as_wkt())
interior = shapely.wkt.loads(self.rc.polygon_wkt)
shade_wkt = exterior.difference(interior).wkt
shade = maplib.shapes.PolyShapeFile(self.rc.bounding_box,
os.path.join(self.tmpdir, 'shape_overview_cover.shp'),
'shade-overview-cover')
shade.add_shade_from_wkt(shade_wkt)
front_page_map.add_shape_file(shade)
front_page_map.render()
return front_page_map
def _render_front_page_header(self, ctx, w, h):
# Draw a light blue block which will contain the name of the
# city being rendered.
blue_w = w
blue_h = 0.3 * h
ctx.set_source_rgb(.80,.80,.80)
ctx.rectangle(0, 0, blue_w, blue_h)
ctx.fill()
draw_utils.draw_text_adjusted(ctx, self.rc.title, blue_w/2, blue_h/2,
blue_w, blue_h)
def _render_front_page_map(self, ctx, dpi, w, h):
# We will render the map slightly below the title
ctx.save()
ctx.translate(0, 0.3 * h + Renderer.PRINT_SAFE_MARGIN_PT)
# Render the map !
mapnik.render(self._front_page_map.get_rendered_map(), ctx)
ctx.restore()
def _render_front_page_footer(self, ctx, w, h, osm_date):
ctx.save()
# Draw the footer
ctx.translate(0, 0.8 * h + 2 * Renderer.PRINT_SAFE_MARGIN_PT)
# Display a nice grey rectangle as the background of the
# footer
footer_w = w
footer_h = 0.2 * h - 2 * Renderer.PRINT_SAFE_MARGIN_PT
ctx.set_source_rgb(.80,.80,.80)
ctx.rectangle(0, 0, footer_w, footer_h)
ctx.fill()
# Draw the OpenStreetMap logo to the right of the footer
logo_height = footer_h / 2
grp, logo_width = self._get_osm_logo(ctx, logo_height)
if grp:
ctx.save()
ctx.translate(w - logo_width - Renderer.PRINT_SAFE_MARGIN_PT,
logo_height / 2)
ctx.set_source(grp)
ctx.paint_with_alpha(0.8)
ctx.restore()
# Prepare the text for the left of the footer
today = datetime.date.today()
notice = \
_(u'Copyright © %(year)d MapOSMatic/OCitySMap developers.\n'
u'http://www.maposmatic.org\n\n'
u'Map data © %(year)d OpenStreetMap.org '
u'and contributors (cc-by-sa).\n'
u'http://www.openstreetmap.org\n\n'
u'Map rendered on: %(date)s. OSM data updated on: %(osmdate)s.\n'
u'The map may be incomplete or inaccurate. '
u'You can contribute to improve this map.\n'
u'See http://wiki.openstreetmap.org')
# We need the correct locale to be set for strftime().
prev_locale = locale.getlocale(locale.LC_TIME)
locale.setlocale(locale.LC_TIME, self.rc.i18n.language_code())
try:
if osm_date is None:
osm_date_str = _(u'unknown')
else:
osm_date_str = osm_date.strftime("%d %B %Y %H:%M")
notice = notice % {'year': today.year,
'date': today.strftime("%d %B %Y"),
'osmdate': osm_date_str}
finally:
locale.setlocale(locale.LC_TIME, prev_locale)
draw_utils.draw_text_adjusted(ctx, notice,
Renderer.PRINT_SAFE_MARGIN_PT, footer_h/2, footer_w,
footer_h, align=pango.ALIGN_LEFT)
ctx.restore()
def _render_front_page(self, ctx, cairo_surface, dpi, osm_date):
# Draw a nice grey rectangle covering the whole page
ctx.save()
ctx.set_source_rgb(.95,.95,.95)
ctx.rectangle(Renderer.PRINT_SAFE_MARGIN_PT,
Renderer.PRINT_SAFE_MARGIN_PT,
self._usable_area_width_pt,
self._usable_area_height_pt)
ctx.fill()
ctx.restore()
# Translate into the working area, taking another
# PRINT_SAFE_MARGIN_PT inside the grey area.
ctx.save()
ctx.translate(2 * Renderer.PRINT_SAFE_MARGIN_PT,
2 * Renderer.PRINT_SAFE_MARGIN_PT)
w = self._usable_area_width_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
h = self._usable_area_height_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
self._render_front_page_header(ctx, w, h)
self._render_front_page_map(ctx, dpi, w, h)
self._render_front_page_footer(ctx, w, h, osm_date)
ctx.restore()
cairo_surface.show_page()
def _render_blank_page(self, ctx, cairo_surface, dpi):
"""
Render a blank page with a nice "intentionally blank" notice
"""
ctx.save()
ctx.translate(
commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT),
commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT))
# footer notice
w = self._usable_area_width_pt
h = self._usable_area_height_pt
ctx.set_source_rgb(.6,.6,.6)
draw_utils.draw_simpletext_center(ctx, _('This page is intentionally left '\
'blank.'), w/2.0, 0.95*h)
draw_utils.render_page_number(ctx, 2,
self._usable_area_width_pt,
self._usable_area_height_pt,
self.grayed_margin_pt,
transparent_background=False)
cairo_surface.show_page()
ctx.restore()
def _render_overview_page(self, ctx, cairo_surface, dpi):
rendered_map = self.overview_canvas.get_rendered_map()
mapnik.render(rendered_map, ctx)
# draw pages numbers
self._draw_overview_labels(ctx, self.overview_canvas, self.overview_grid,
commons.convert_pt_to_dots(self._usable_area_width_pt),
commons.convert_pt_to_dots(self._usable_area_height_pt))
# Render the page number
draw_utils.render_page_number(ctx, 3,
self._usable_area_width_pt,
self._usable_area_height_pt,
self.grayed_margin_pt,
transparent_background = True)
cairo_surface.show_page()
def _draw_arrow(self, ctx, cairo_surface, number, max_digit_number,
reverse_text=False):
arrow_edge = self.grayed_margin_pt*.6
ctx.save()
ctx.set_source_rgb(0, 0, 0)
ctx.translate(-arrow_edge/2, -arrow_edge*0.45)
ctx.line_to(0, 0)
ctx.line_to(0, arrow_edge)
ctx.line_to(arrow_edge, arrow_edge)
ctx.line_to(arrow_edge, 0)
ctx.line_to(arrow_edge/2, -arrow_edge*.25)
ctx.close_path()
ctx.fill()
ctx.restore()
ctx.save()
if reverse_text:
ctx.rotate(math.pi)
draw_utils.draw_text_adjusted(ctx, unicode(number), 0, 0, arrow_edge,
arrow_edge, max_char_number=max_digit_number,
text_color=(1, 1, 1, 1), width_adjust=0.85,
height_adjust=0.9)
ctx.restore()
def _render_neighbour_arrows(self, ctx, cairo_surface, map_number,
max_digit_number):
nb_previous_pages = 4
current_line, current_col = None, None
for line_nb in xrange(self.nb_pages_height):
if map_number in self.page_disposition[line_nb]:
current_line = line_nb
current_col = self.page_disposition[line_nb].index(
map_number)
break
if current_line == None:
# page not referenced
return
# north arrow
for line_nb in reversed(xrange(current_line)):
if self.page_disposition[line_nb][current_col] != None:
north_arrow = self.page_disposition[line_nb][current_col]
ctx.save()
ctx.translate(self._usable_area_width_pt/2,
commons.convert_pt_to_dots(self.grayed_margin_pt)/2)
self._draw_arrow(ctx, cairo_surface,
north_arrow + nb_previous_pages, max_digit_number)
ctx.restore()
break
# south arrow
for line_nb in xrange(current_line + 1, self.nb_pages_height):
if self.page_disposition[line_nb][current_col] != None:
south_arrow = self.page_disposition[line_nb][current_col]
ctx.save()
ctx.translate(self._usable_area_width_pt/2,
self._usable_area_height_pt \
- commons.convert_pt_to_dots(self.grayed_margin_pt)/2)
ctx.rotate(math.pi)
self._draw_arrow(ctx, cairo_surface,
south_arrow + nb_previous_pages, max_digit_number,
reverse_text=True)
ctx.restore()
break
# west arrow
for col_nb in reversed(xrange(0, current_col)):
if self.page_disposition[current_line][col_nb] != None:
west_arrow = self.page_disposition[current_line][col_nb]
ctx.save()
ctx.translate(
commons.convert_pt_to_dots(self.grayed_margin_pt)/2,
self._usable_area_height_pt/2)
ctx.rotate(-math.pi/2)
self._draw_arrow(ctx, cairo_surface,
west_arrow + nb_previous_pages, max_digit_number)
ctx.restore()
break
# east arrow
for col_nb in xrange(current_col + 1, self.nb_pages_width):
if self.page_disposition[current_line][col_nb] != None:
east_arrow = self.page_disposition[current_line][col_nb]
ctx.save()
ctx.translate(
self._usable_area_width_pt \
- commons.convert_pt_to_dots(self.grayed_margin_pt)/2,
self._usable_area_height_pt/2)
ctx.rotate(math.pi/2)
self._draw_arrow(ctx, cairo_surface,
east_arrow + nb_previous_pages, max_digit_number)
ctx.restore()
break
def render(self, cairo_surface, dpi, osm_date):
ctx = cairo.Context(cairo_surface)
self._render_front_page(ctx, cairo_surface, dpi, osm_date)
self._render_blank_page(ctx, cairo_surface, dpi)
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))
self._render_overview_page(ctx, cairo_surface, dpi)
for map_number, (canvas, grid) in enumerate(self.pages):
rendered_map = canvas.get_rendered_map()
LOG.debug('Mapnik scale: 1/%f' % rendered_map.scale_denominator())
LOG.debug('Actual scale: 1/%f' % canvas.get_actual_scale())
mapnik.render(rendered_map, ctx)
# Place the vertical and horizontal square labels
ctx.save()
ctx.translate(commons.convert_pt_to_dots(self.grayed_margin_pt),
commons.convert_pt_to_dots(self.grayed_margin_pt))
self._draw_labels(ctx, grid,
commons.convert_pt_to_dots(self._usable_area_width_pt) \
- 2 * commons.convert_pt_to_dots(self.grayed_margin_pt),
commons.convert_pt_to_dots(self._usable_area_height_pt) \
- 2 * commons.convert_pt_to_dots(self.grayed_margin_pt),
commons.convert_pt_to_dots(self._grid_legend_margin_pt))
ctx.restore()
# Render the page number
draw_utils.render_page_number(ctx, map_number+4,
self._usable_area_width_pt,
self._usable_area_height_pt,
self.grayed_margin_pt,
transparent_background = True)
self._render_neighbour_arrows(ctx, cairo_surface, map_number,
len(unicode(len(self.pages)+4)))
cairo_surface.show_page()
ctx.restore()
mpsir = MultiPageStreetIndexRenderer(self.rc.i18n,
ctx, cairo_surface,
self.index_categories,
(Renderer.PRINT_SAFE_MARGIN_PT,
Renderer.PRINT_SAFE_MARGIN_PT,
self._usable_area_width_pt,
self._usable_area_height_pt),
map_number+5)
mpsir.render()
cairo_surface.flush()
# In multi-page mode, we only render pdf format
@staticmethod
def get_compatible_output_formats():
return [ "pdf" ]
# In multi-page mode, we only accept A4, A5 and US letter as paper
# sizes. The goal is to render booklets, not posters.
# The default paper size is A4 portrait
@staticmethod
def get_compatible_paper_sizes(bounding_box,
scale=Renderer.DEFAULT_SCALE,
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, sz[0] == 'A4'))
return valid_sizes
@classmethod
def _draw_overview_labels(cls, ctx, map_canvas, overview_grid,
area_width_dots, area_height_dots):
"""
Draw the page numbers for the overview grid.
Args:
ctx (cairo.Context): The cairo context to use to draw.
overview_grid (OverViewGrid): the overview grid object
area_width_dots/area_height_dots (numbers): size of the
drawing area (cairo units).
"""
ctx.save()
ctx.set_font_size(14)
bbox = map_canvas.get_actual_bounding_box()
bottom_right, bottom_left, top_left, top_right = bbox.to_mercator()
bottom, left = bottom_right.y, top_left.x
coord_delta_y = top_left.y - bottom_right.y
coord_delta_x = bottom_right.x - top_left.x
w, h = None, None
for idx, page_bb in enumerate(overview_grid._pages_bbox):
p_bottom_right, p_bottom_left, p_top_left, p_top_right = \
page_bb.to_mercator()
center_x = p_top_left.x+(p_top_right.x-p_top_left.x)/2
center_y = p_bottom_left.y+(p_top_right.y-p_bottom_right.y)/2
y_percent = 100 - 100.0*(center_y - bottom)/coord_delta_y
y = int(area_height_dots*y_percent/100)
x_percent = 100.0*(center_x - left)/coord_delta_x
x = int(area_width_dots*x_percent/100)
if not w or not h:
w = area_width_dots*(p_bottom_right.x - p_bottom_left.x
)/coord_delta_x
h = area_height_dots*(p_top_right.y - p_bottom_right.y
)/coord_delta_y
draw_utils.draw_text_adjusted(ctx, unicode(idx+4), x, y, w, h,
max_char_number=len(unicode(len(overview_grid._pages_bbox)+3)),
text_color=(0, 0, 0, 0.6))
ctx.restore()