kopia lustrzana https://github.com/hholzgra/ocitysmap
283 wiersze
10 KiB
Python
283 wiersze
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# ocitysmap, city map and street index generator from OpenStreetMap data
|
|
# Copyright (C) 2012 David Decotigny
|
|
# Copyright (C) 2012 Frédéric Lehobey
|
|
# Copyright (C) 2012 Pierre Mauduit
|
|
# Copyright (C) 2012 David Mentré
|
|
# Copyright (C) 2012 Maxime Petazzoni
|
|
# 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 math
|
|
import os
|
|
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 pango
|
|
import re
|
|
|
|
from ocitysmap2.maplib.map_canvas import MapCanvas
|
|
from ocitysmap2.maplib.grid import Grid
|
|
import commons
|
|
from ocitysmap2 import maplib
|
|
from ocitysmap2 import draw_utils
|
|
import shapely.wkt
|
|
|
|
import logging
|
|
|
|
LOG = logging.getLogger('ocitysmap')
|
|
|
|
class Renderer:
|
|
"""
|
|
The job of an OCitySMap layout renderer is to lay out the resulting map and
|
|
render it from a given rendering configuration.
|
|
"""
|
|
name = 'abstract'
|
|
description = 'The abstract interface of a renderer'
|
|
|
|
# The PRINT_SAFE_MARGIN_PT is a small margin we leave on all page borders
|
|
# to ease printing as printers often eat up margins with misaligned paper,
|
|
# etc.
|
|
PRINT_SAFE_MARGIN_PT = 15
|
|
|
|
GRID_LEGEND_MARGIN_RATIO = .02
|
|
|
|
# The DEFAULT_KM_IN_MM represents the minimum acceptable mapnik scale
|
|
# 12000 ensures that the zoom level will be 16 or higher
|
|
# see entities.xml.inc file from osm style sheet
|
|
DEFAULT_SCALE = 12000
|
|
|
|
def __init__(self, db, rc, tmpdir, dpi):
|
|
"""
|
|
Create the renderer.
|
|
|
|
Args:
|
|
rc (RenderingConfiguration): rendering parameters.
|
|
tmpdir (os.path): Path to a temp dir that can hold temp files.
|
|
street_index (StreetIndex): None or the street index object.
|
|
"""
|
|
# Note: street_index may be None
|
|
self.db = db
|
|
self.rc = rc
|
|
self.tmpdir = tmpdir
|
|
self.grid = None # The implementation is in charge of it
|
|
|
|
self.paper_width_pt = \
|
|
commons.convert_mm_to_pt(self.rc.paper_width_mm)
|
|
self.paper_height_pt = \
|
|
commons.convert_mm_to_pt(self.rc.paper_height_mm)
|
|
|
|
@staticmethod
|
|
def _get_osm_logo(ctx, height):
|
|
"""
|
|
Read the OSM logo file and rescale it to fit within height.
|
|
|
|
Args:
|
|
ctx (cairo.Context): The cairo context to use to draw.
|
|
height (number): final height of the logo (cairo units).
|
|
|
|
Return a tuple (cairo group object for the logo, logo width in
|
|
cairo units).
|
|
"""
|
|
# TODO: read vector logo
|
|
logo_path = os.path.abspath(os.path.join(
|
|
os.path.dirname(__file__), '..', '..', 'images', 'osm-logo.png'))
|
|
if not os.path.exists(logo_path):
|
|
logo_path = os.path.join(
|
|
sys.exec_prefix, 'share', 'images', 'ocitysmap2',
|
|
'osm-logo.png')
|
|
|
|
try:
|
|
with open(logo_path, 'rb') as f:
|
|
png = cairo.ImageSurface.create_from_png(f)
|
|
LOG.debug('Using copyright logo: %s.' % logo_path)
|
|
except IOError:
|
|
LOG.warning('Cannot open logo from %s.' % logo_path)
|
|
return None, None
|
|
|
|
ctx.push_group()
|
|
ctx.save()
|
|
ctx.move_to(0, 0)
|
|
factor = height / png.get_height()
|
|
ctx.scale(factor, factor)
|
|
ctx.set_source_surface(png)
|
|
ctx.paint()
|
|
ctx.restore()
|
|
return ctx.pop_group(), png.get_width()*factor
|
|
|
|
@staticmethod
|
|
def _draw_labels(ctx, map_grid,
|
|
map_area_width_dots, map_area_height_dots,
|
|
grid_legend_margin_dots):
|
|
"""
|
|
Draw the Grid labels at current position.
|
|
|
|
Args:
|
|
ctx (cairo.Context): The cairo context to use to draw.
|
|
map_grid (Grid): the grid objects whose labels we want to draw.
|
|
map_area_width_dots/map_area_height_dots (numbers): size of the
|
|
map (cairo units).
|
|
grid_legend_margin_dots (number): margin between border of
|
|
map and grid labels (cairo units).
|
|
"""
|
|
ctx.save()
|
|
|
|
step_horiz = map_area_width_dots / map_grid.horiz_count
|
|
last_horiz_portion = math.modf(map_grid.horiz_count)[0]
|
|
|
|
step_vert = map_area_height_dots / map_grid.vert_count
|
|
last_vert_portion = math.modf(map_grid.vert_count)[0]
|
|
|
|
ctx.set_font_size(min(0.75 * grid_legend_margin_dots,
|
|
0.5 * step_horiz))
|
|
|
|
for i, label in enumerate(map_grid.horizontal_labels):
|
|
x = i * step_horiz
|
|
|
|
if i < len(map_grid.horizontal_labels) - 1:
|
|
x += step_horiz/2.0
|
|
elif last_horiz_portion >= 0.3:
|
|
x += step_horiz * last_horiz_portion/2.0
|
|
else:
|
|
continue
|
|
|
|
draw_utils.draw_simpletext_center(ctx, label,
|
|
x, grid_legend_margin_dots/2.0)
|
|
draw_utils.draw_simpletext_center(ctx, label,
|
|
x, map_area_height_dots -
|
|
grid_legend_margin_dots/2.0)
|
|
|
|
for i, label in enumerate(map_grid.vertical_labels):
|
|
y = i * step_vert
|
|
|
|
if i < len(map_grid.vertical_labels) - 1:
|
|
y += step_vert/2.0
|
|
elif last_vert_portion >= 0.3:
|
|
y += step_vert * last_vert_portion/2.0
|
|
else:
|
|
continue
|
|
|
|
draw_utils.draw_simpletext_center(ctx, label,
|
|
grid_legend_margin_dots/2.0, y)
|
|
draw_utils.draw_simpletext_center(ctx, label,
|
|
map_area_width_dots -
|
|
grid_legend_margin_dots/2.0, y)
|
|
|
|
ctx.restore()
|
|
|
|
def _create_map_canvas(self, width, height, dpi,
|
|
draw_contour_shade = True):
|
|
"""
|
|
Create a new MapCanvas object.
|
|
|
|
Args:
|
|
graphical_ratio (float): ratio W/H of the area to render into.
|
|
draw_contour_shade (bool): whether to draw a shade around
|
|
the area of interest or not.
|
|
|
|
Return the MapCanvas object or raise ValueError.
|
|
"""
|
|
|
|
# Prepare the map canvas
|
|
canvas = MapCanvas(self.rc.stylesheet,
|
|
self.rc.bounding_box,
|
|
width, height, dpi)
|
|
|
|
if draw_contour_shade:
|
|
# Area to keep visible
|
|
interior = shapely.wkt.loads(self.rc.polygon_wkt)
|
|
|
|
# Surroundings to gray-out
|
|
bounding_box \
|
|
= canvas.get_actual_bounding_box().create_expanded(0.05, 0.05)
|
|
exterior = shapely.wkt.loads(bounding_box.as_wkt())
|
|
|
|
# Determine the shade WKT
|
|
shade_wkt = exterior.difference(interior).wkt
|
|
|
|
# Prepare the shade SHP
|
|
shade_shape = maplib.shapes.PolyShapeFile(
|
|
canvas.get_actual_bounding_box(),
|
|
os.path.join(self.tmpdir, 'shade.shp'),
|
|
'shade')
|
|
shade_shape.add_shade_from_wkt(shade_wkt)
|
|
|
|
# Add the shade SHP to the map
|
|
canvas.add_shape_file(shade_shape,
|
|
self.rc.stylesheet.shade_color,
|
|
self.rc.stylesheet.shade_alpha,
|
|
self.rc.stylesheet.grid_line_width)
|
|
|
|
return canvas
|
|
|
|
def _create_grid(self, canvas):
|
|
"""
|
|
Create a new Grid object for the given MapCanvas.
|
|
|
|
Args:
|
|
canvas (MapCanvas): Map Canvas (see _create_map_canvas).
|
|
|
|
Return a new Grid object.
|
|
"""
|
|
# Prepare the grid SHP
|
|
map_grid = Grid(canvas.get_actual_bounding_box(), canvas.get_actual_scale(), self.rc.i18n.isrtl())
|
|
grid_shape = map_grid.generate_shape_file(
|
|
os.path.join(self.tmpdir, 'grid.shp'))
|
|
|
|
# Add the grid SHP to the 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)
|
|
|
|
return map_grid
|
|
|
|
# The next two methods are to be overloaded by the actual renderer.
|
|
def render(self, cairo_surface, dpi):
|
|
"""Renders the map, the index and all other visual map features on the
|
|
given Cairo surface.
|
|
|
|
Args:
|
|
cairo_surface (Cairo.Surface): the destination Cairo device.
|
|
dpi (int): dots per inch of the device.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
def get_compatible_output_formats():
|
|
return [ "png", "svgz", "pdf", "csv" ]
|
|
|
|
@staticmethod
|
|
def get_compatible_paper_sizes(bounding_box, scale):
|
|
"""Returns a list of the compatible paper sizes for the given bounding
|
|
box. The list is sorted, smaller papers first, and a "custom" paper
|
|
matching the dimensions of the bounding box is added at the end.
|
|
|
|
Args:
|
|
bounding_box (coords.BoundingBox): the map geographic bounding box.
|
|
scale (int): minimum mapnik scale of the map.
|
|
|
|
Returns a list of tuples (paper name, width in mm, height in
|
|
mm, portrait_ok, landscape_ok, is_default). Paper sizes are
|
|
represented in portrait mode.
|
|
"""
|
|
raise NotImplementedError
|