def __init__(self, db, rc, tmpdir, dpi, file_prefix, index_position="side"): """ Create the renderer. Args: rc (RenderingConfiguration): rendering parameters. tmpdir (os.path): Path to a temp dir that can hold temp files. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom). """ Renderer.__init__(self, db, rc, tmpdir, dpi) # Prepare the index self.street_index = StreetIndex(db, rc.polygon_wkt, rc.i18n) if not self.street_index.categories: LOG.warning("Designated area leads to an empty index") self.street_index = None self._grid_legend_margin_pt = min( Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt, Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt, ) self._title_margin_pt = 0.05 * self.paper_height_pt self._copyright_margin_pt = 0.02 * self.paper_height_pt 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 + self._title_margin_pt + self._copyright_margin_pt ) # Prepare the Index (may raise a IndexDoesNotFitError) if index_position and self.street_index and self.street_index.categories: self._index_renderer, self._index_area = self._create_index_rendering(index_position == "side") else: self._index_renderer, self._index_area = None, None # Prepare the layout of the whole page if not self._index_area: # No index displayed self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), self._usable_area_width_pt, self._usable_area_height_pt, ) elif index_position == "side": # Index present, displayed on the side if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT: # Index on the right -> map on the left self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), (self._usable_area_width_pt - self._index_area.w), self._usable_area_height_pt, ) else: # Index on the left -> map on the right self._map_coords = ( self._index_area.x + self._index_area.w, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), (self._usable_area_width_pt - self._index_area.w), self._usable_area_height_pt, ) elif index_position == "bottom": # Index present, displayed at the bottom -> map on top self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), self._usable_area_width_pt, (self._usable_area_height_pt - self._index_area.h), ) else: raise AssertionError("Invalid index position %s" % repr(index_position)) # Prepare the map self._map_canvas = self._create_map_canvas( float(self._map_coords[2]), float(self._map_coords[3]), dpi, rc.osmid is not None # W # H ) # Prepare map overlay if self.rc.overlay: self._overlay_canvas = MapCanvas( self.rc.overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, ) # Prepare map overlay if self.rc.overlay: self._overlay_canvas = MapCanvas( self.rc.overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, ) # Prepare map overlay if self.rc.overlay: self._overlay_canvas = MapCanvas( self.rc.overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, ) # Prepare the grid self.grid = self._create_grid(self._map_canvas, dpi) # Update the street_index to reflect the grid's actual position if self.grid and self.street_index: self.street_index.apply_grid(self.grid) # Dump the CSV street index if self.street_index: self.street_index.write_to_csv(rc.title, "%s.csv" % file_prefix) # Commit the internal rendering stack of the map self._map_canvas.render() if self.rc.overlay: self._overlay_canvas.render()
def __init__(self, db, rc, tmpdir, dpi, file_prefix, index_position = 'side'): """ Create the renderer. Args: rc (RenderingConfiguration): rendering parameters. tmpdir (os.path): Path to a temp dir that can hold temp files. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom). """ Renderer.__init__(self, db, rc, tmpdir, dpi) # Prepare the index self.street_index = StreetIndex(db, rc.polygon_wkt, rc.i18n) if not self.street_index.categories: LOG.warning("Designated area leads to an empty index") self.street_index = None self._grid_legend_margin_pt = \ min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt, Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt) self._title_margin_pt = 0.05 * self.paper_height_pt self._copyright_margin_pt = 0.02 * self.paper_height_pt 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 + self._title_margin_pt + self._copyright_margin_pt)) # Prepare the Index (may raise a IndexDoesNotFitError) if ( index_position and self.street_index and self.street_index.categories ): self._index_renderer, self._index_area \ = self._create_index_rendering(index_position == "side") else: self._index_renderer, self._index_area = None, None # Prepare the layout of the whole page if not self._index_area: # No index displayed self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, self._usable_area_height_pt ) elif index_position == 'side': # Index present, displayed on the side if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT: # Index on the right -> map on the left self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) else: # Index on the left -> map on the right self._map_coords = ( self._index_area.x + self._index_area.w, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) elif index_position == 'bottom': # Index present, displayed at the bottom -> map on top self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, ( self._usable_area_height_pt - self._index_area.h ) ) else: raise AssertionError("Invalid index position %s" % repr(index_position)) # Prepare the map self._map_canvas = self._create_map_canvas( float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi ) # Prepare the grid self.grid = self._create_grid(self._map_canvas) # Update the street_index to reflect the grid's actual position if self.grid and self.street_index: self.street_index.apply_grid(self.grid) # Dump the CSV street index if self.street_index: self.street_index.write_to_csv(rc.title, '%s.csv' % file_prefix) # Commit the internal rendering stack of the map self._map_canvas.render()
class SinglePageRenderer(Renderer): """ This Renderer creates a full-page map, with the overlayed features like the grid, grid labels, scale and compass rose and can draw an index. """ name = "generic_single_page" description = "A generic full-page layout with or without index." MAX_INDEX_OCCUPATION_RATIO = 1 / 3.0 def __init__(self, db, rc, tmpdir, dpi, file_prefix, index_position="side"): """ Create the renderer. Args: rc (RenderingConfiguration): rendering parameters. tmpdir (os.path): Path to a temp dir that can hold temp files. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom). """ Renderer.__init__(self, db, rc, tmpdir, dpi) # Prepare the index self.street_index = StreetIndex(db, rc.polygon_wkt, rc.i18n) if not self.street_index.categories: LOG.warning("Designated area leads to an empty index") self.street_index = None self._grid_legend_margin_pt = min( Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt, Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt, ) self._title_margin_pt = 0.05 * self.paper_height_pt self._copyright_margin_pt = 0.02 * self.paper_height_pt 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 + self._title_margin_pt + self._copyright_margin_pt ) # Prepare the Index (may raise a IndexDoesNotFitError) if index_position and self.street_index and self.street_index.categories: self._index_renderer, self._index_area = self._create_index_rendering(index_position == "side") else: self._index_renderer, self._index_area = None, None # Prepare the layout of the whole page if not self._index_area: # No index displayed self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), self._usable_area_width_pt, self._usable_area_height_pt, ) elif index_position == "side": # Index present, displayed on the side if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT: # Index on the right -> map on the left self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), (self._usable_area_width_pt - self._index_area.w), self._usable_area_height_pt, ) else: # Index on the left -> map on the right self._map_coords = ( self._index_area.x + self._index_area.w, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), (self._usable_area_width_pt - self._index_area.w), self._usable_area_height_pt, ) elif index_position == "bottom": # Index present, displayed at the bottom -> map on top self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), self._usable_area_width_pt, (self._usable_area_height_pt - self._index_area.h), ) else: raise AssertionError("Invalid index position %s" % repr(index_position)) # Prepare the map self._map_canvas = self._create_map_canvas( float(self._map_coords[2]), float(self._map_coords[3]), dpi, rc.osmid is not None # W # H ) # Prepare map overlay if self.rc.overlay: self._overlay_canvas = MapCanvas( self.rc.overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, ) # Prepare map overlay if self.rc.overlay: self._overlay_canvas = MapCanvas( self.rc.overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, ) # Prepare map overlay if self.rc.overlay: self._overlay_canvas = MapCanvas( self.rc.overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, ) # Prepare the grid self.grid = self._create_grid(self._map_canvas, dpi) # Update the street_index to reflect the grid's actual position if self.grid and self.street_index: self.street_index.apply_grid(self.grid) # Dump the CSV street index if self.street_index: self.street_index.write_to_csv(rc.title, "%s.csv" % file_prefix) # Commit the internal rendering stack of the map self._map_canvas.render() if self.rc.overlay: self._overlay_canvas.render() def _create_index_rendering(self, on_the_side): """ Prepare to render the Street index. Args: on_the_side (bool): True=index on the side, False=at bottom. Return a couple (StreetIndexRenderer, StreetIndexRenderingArea). """ # Now we determine the actual occupation of the index index_renderer = StreetIndexRenderer(self.rc.i18n, self.street_index.categories) # We use a fake vector device to determine the actual # rendering characteristics fake_surface = cairo.PDFSurface(None, self.paper_width_pt, self.paper_height_pt) if on_the_side: index_max_width_pt = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_width_pt if not self.rc.i18n.isrtl(): # non-RTL: Index is on the right index_area = index_renderer.precompute_occupation_area( fake_surface, (self.paper_width_pt - Renderer.PRINT_SAFE_MARGIN_PT - index_max_width_pt), (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), index_max_width_pt, self._usable_area_height_pt, "width", "right", ) else: # RTL: Index is on the left index_area = index_renderer.precompute_occupation_area( fake_surface, Renderer.PRINT_SAFE_MARGIN_PT, (Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt), index_max_width_pt, self._usable_area_height_pt, "width", "left", ) else: # Index at the bottom of the page index_max_height_pt = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_height_pt index_area = index_renderer.precompute_occupation_area( fake_surface, Renderer.PRINT_SAFE_MARGIN_PT, ( self.paper_height_pt - Renderer.PRINT_SAFE_MARGIN_PT - self._copyright_margin_pt - index_max_height_pt ), self._usable_area_width_pt, index_max_height_pt, "height", "bottom", ) return index_renderer, index_area def _draw_title(self, ctx, w_dots, h_dots, font_face): """ Draw the title at the current position inside a w_dots*h_dots rectangle. Args: ctx (cairo.Context): The Cairo context to use to draw. w_dots,h_dots (number): Rectangle dimension (ciaro units) font_face (str): Pango font specification. """ # Title background ctx.save() ctx.set_source_rgb(0.8, 0.9, 0.96) ctx.rectangle(0, 0, w_dots, h_dots) ctx.fill() ctx.restore() # Retrieve and paint the OSM logo ctx.save() grp, logo_width = self._get_osm_logo(ctx, 0.8 * h_dots) if grp: ctx.translate(w_dots - logo_width - 0.1 * h_dots, 0.1 * h_dots) ctx.set_source(grp) ctx.paint_with_alpha(0.5) else: LOG.warning("OSM Logo not available.") logo_width = 0 ctx.restore() # Prepare the title pc = pangocairo.CairoContext(ctx) layout = pc.create_layout() layout.set_width(int((w_dots - 0.1 * w_dots - logo_width) * pango.SCALE)) if not self.rc.i18n.isrtl(): layout.set_alignment(pango.ALIGN_LEFT) else: layout.set_alignment(pango.ALIGN_RIGHT) fd = pango.FontDescription(font_face) fd.set_size(pango.SCALE) layout.set_font_description(fd) layout.set_text(self.rc.title) draw_utils.adjust_font_size(layout, fd, layout.get_width(), 0.8 * h_dots) # Draw the title ctx.save() ctx.rectangle(0, 0, w_dots, h_dots) ctx.stroke() ctx.translate(0.1 * h_dots, (h_dots - (layout.get_size()[1] / pango.SCALE)) / 2.0) pc.show_layout(layout) ctx.restore() def _draw_copyright_notice(self, ctx, w_dots, h_dots, notice=None, osm_date=None): """ Draw a copyright notice at current location and within the given w_dots*h_dots rectangle. Args: ctx (cairo.Context): The Cairo context to use to draw. w_dots,h_dots (number): Rectangle dimension (ciaro units). font_face (str): Pango font specification. notice (str): Optional notice to replace the default. """ today = datetime.date.today() notice = notice or _( u"Copyright © %(year)d MapOSMatic/OCitySMap developers. " u"Map data © %(year)d OpenStreetMap.org " u"and contributors (cc-by-sa).\n" u"Map rendered on: %(date)s. OSM data updated on: %(osmdate)s. " u"The map may be incomplete or inaccurate. " u"You can contribute to improve this map. " 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) ctx.save() pc = pangocairo.CairoContext(ctx) fd = pango.FontDescription("DejaVu") fd.set_size(pango.SCALE) layout = pc.create_layout() layout.set_font_description(fd) layout.set_text(notice) draw_utils.adjust_font_size(layout, fd, w_dots, h_dots) pc.show_layout(layout) ctx.restore() def render(self, cairo_surface, dpi, osm_date): """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. """ LOG.info( "SinglePageRenderer rendering on %dx%dmm paper at %d dpi." % (self.rc.paper_width_mm, self.rc.paper_height_mm, dpi) ) # First determine some useful drawing parameters safe_margin_dots = commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT, dpi) usable_area_width_dots = commons.convert_pt_to_dots(self._usable_area_width_pt, dpi) usable_area_height_dots = commons.convert_pt_to_dots(self._usable_area_height_pt, dpi) title_margin_dots = commons.convert_pt_to_dots(self._title_margin_pt, dpi) copyright_margin_dots = commons.convert_pt_to_dots(self._copyright_margin_pt, dpi) map_coords_dots = map(lambda l: commons.convert_pt_to_dots(l, dpi), self._map_coords) ctx = cairo.Context(cairo_surface) # Set a white background ctx.save() ctx.set_source_rgb(1, 1, 1) ctx.rectangle( 0, 0, commons.convert_pt_to_dots(self.paper_width_pt, dpi), commons.convert_pt_to_dots(self.paper_height_pt, dpi), ) ctx.fill() ctx.restore() ## ## Draw the map, scaled to fit the designated area ## ctx.save() # prevent map background from filling the full canvas ctx.rectangle(map_coords_dots[0], map_coords_dots[1], map_coords_dots[2], map_coords_dots[3]) ctx.clip() # Prepare to draw the map at the right location ctx.translate(map_coords_dots[0], map_coords_dots[1]) # Draw the rescaled Map ctx.save() scale_factor = dpi / 72 rendered_map = self._map_canvas.get_rendered_map() LOG.debug("Map:") LOG.debug("Mapnik scale: 1/%f" % rendered_map.scale_denominator()) LOG.debug("Actual scale: 1/%f" % self._map_canvas.get_actual_scale()) mapnik.render(rendered_map, ctx, scale_factor, 0, 0) ctx.restore() # Draw the rescaled Overlay if self.rc.overlay: LOG.debug("Overlay:") ctx.save() scale_factor = dpi / 72 rendered_overlay = self._overlay_canvas.get_rendered_map() mapnik.render(rendered_overlay, ctx, scale_factor, 0, 0) ctx.restore() # Draw a rectangle around the map ctx.rectangle(0, 0, map_coords_dots[2], map_coords_dots[3]) ctx.stroke() # Place the vertical and horizontal square labels self._draw_labels( ctx, self.grid, map_coords_dots[2], map_coords_dots[3], commons.convert_pt_to_dots(self._grid_legend_margin_pt, dpi), ) ctx.restore() ## ## Draw the title ## ctx.save() ctx.translate(safe_margin_dots, safe_margin_dots) self._draw_title(ctx, usable_area_width_dots, title_margin_dots, "Georgia Bold") ctx.restore() ## ## Draw the index, when applicable ## if self._index_renderer and self._index_area: ctx.save() # NEVER use ctx.scale() here because otherwise pango will # choose different dont metrics which may be incompatible # with what has been computed by __init__(), which may # require more columns than expected ! Instead, we have # to trick pangocairo into believing it is rendering to a # device with the same default resolution, but with a # cairo resolution matching the 'dpi' specified # resolution. See # index::render::StreetIndexRenederer::render() and # comments within. self._index_renderer.render(ctx, self._index_area, dpi) ctx.restore() # Also draw a rectangle ctx.save() ctx.rectangle( commons.convert_pt_to_dots(self._index_area.x, dpi), commons.convert_pt_to_dots(self._index_area.y, dpi), commons.convert_pt_to_dots(self._index_area.w, dpi), commons.convert_pt_to_dots(self._index_area.h, dpi), ) ctx.stroke() ctx.restore() ## ## Draw the copyright notice ## ctx.save() # Move to the right position ctx.translate( safe_margin_dots, (safe_margin_dots + title_margin_dots + usable_area_height_dots + copyright_margin_dots / 4.0), ) # Draw the copyright notice self._draw_copyright_notice(ctx, usable_area_width_dots, copyright_margin_dots, osm_date=osm_date) ctx.restore() # Draw compass rose # TODO: proper positioning/scaling, move to abstract renderer ctx.save() ctx.translate(50, title_margin_dots + 50) ctx.scale(0.33, 0.33) compass_path = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "..", "images", "compass-rose.svg") ) svg = rsvg.Handle(compass_path) svg.render_cairo(ctx) ctx.restore() # TODO: map scale cairo_surface.flush() @staticmethod def _generic_get_compatible_paper_sizes(bounding_box, scale=Renderer.DEFAULT_SCALE, index_position=None): """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. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom). 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. """ # the mapnik scale depends on the latitude lat = bounding_box.get_top_left()[0] scale *= math.cos(math.radians(lat)) # by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi scale *= float(72) / 90 geo_height_m, geo_width_m = bounding_box.spheric_sizes() paper_width_mm = geo_width_m * 1000 / scale paper_height_mm = geo_height_m * 1000 / scale LOG.debug( "Map represents %dx%dm, needs at least %.1fx%.1fcm " "on paper." % (geo_width_m, geo_height_m, paper_width_mm / 10.0, paper_height_mm / 10.0) ) # Take index into account, when applicable if index_position == "side": paper_width_mm /= 1.0 - SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO elif index_position == "bottom": paper_height_mm /= 1.0 - SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO # Take margins into account paper_width_mm += 2 * commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT) paper_height_mm += 2 * commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT) # Take grid legend, title and copyright into account paper_width_mm /= 1 - Renderer.GRID_LEGEND_MARGIN_RATIO paper_height_mm /= 1 - (Renderer.GRID_LEGEND_MARGIN_RATIO + 0.05 + 0.02) # Transform the values into integers paper_width_mm = int(math.ceil(paper_width_mm)) paper_height_mm = int(math.ceil(paper_height_mm)) LOG.debug("Best fit is %.1fx%.1fcm." % (paper_width_mm / 10.0, paper_height_mm / 10.0)) # Test both portrait and landscape orientations when checking for paper # sizes. valid_sizes = [] for name, w, h in ocitysmap.layoutlib.PAPER_SIZES: portrait_ok = paper_width_mm <= w and paper_height_mm <= h landscape_ok = paper_width_mm <= h and paper_height_mm <= w if portrait_ok or landscape_ok: valid_sizes.append([name, w, h, portrait_ok, landscape_ok, False]) # Add a 'Custom' paper format to the list that perfectly matches the # bounding box. valid_sizes.append( [ "Best fit", min(paper_width_mm, paper_height_mm), max(paper_width_mm, paper_height_mm), paper_width_mm < paper_height_mm, paper_width_mm > paper_height_mm, False, ] ) # select the first one as default valid_sizes[0][5] = True return valid_sizes
class SinglePageRenderer(Renderer): """ This Renderer creates a full-page map, with the overlayed features like the grid, grid labels, scale and compass rose and can draw an index. """ name = 'generic_single_page' description = 'A generic full-page layout with or without index.' MAX_INDEX_OCCUPATION_RATIO = 1/3. def __init__(self, db, rc, tmpdir, dpi, file_prefix, index_position = 'side'): """ Create the renderer. Args: rc (RenderingConfiguration): rendering parameters. tmpdir (os.path): Path to a temp dir that can hold temp files. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom). """ Renderer.__init__(self, db, rc, tmpdir, dpi) # Prepare the index self.street_index = StreetIndex(db, rc.polygon_wkt, rc.i18n) if not self.street_index.categories: LOG.warning("Designated area leads to an empty index") self.street_index = None self._grid_legend_margin_pt = \ min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt, Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt) self._title_margin_pt = 0.05 * self.paper_height_pt self._copyright_margin_pt = 0.02 * self.paper_height_pt 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 + self._title_margin_pt + self._copyright_margin_pt)) # Prepare the Index (may raise a IndexDoesNotFitError) if ( index_position and self.street_index and self.street_index.categories ): self._index_renderer, self._index_area \ = self._create_index_rendering(index_position == "side") else: self._index_renderer, self._index_area = None, None # Prepare the layout of the whole page if not self._index_area: # No index displayed self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, self._usable_area_height_pt ) elif index_position == 'side': # Index present, displayed on the side if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT: # Index on the right -> map on the left self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) else: # Index on the left -> map on the right self._map_coords = ( self._index_area.x + self._index_area.w, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) elif index_position == 'bottom': # Index present, displayed at the bottom -> map on top self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, ( self._usable_area_height_pt - self._index_area.h ) ) else: raise AssertionError("Invalid index position %s" % repr(index_position)) # Prepare the map self._map_canvas = self._create_map_canvas( float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi ) # Prepare the grid self.grid = self._create_grid(self._map_canvas) # Update the street_index to reflect the grid's actual position if self.grid and self.street_index: self.street_index.apply_grid(self.grid) # Dump the CSV street index if self.street_index: self.street_index.write_to_csv(rc.title, '%s.csv' % file_prefix) # Commit the internal rendering stack of the map self._map_canvas.render() def _create_index_rendering(self, on_the_side): """ Prepare to render the Street index. Args: on_the_side (bool): True=index on the side, False=at bottom. Return a couple (StreetIndexRenderer, StreetIndexRenderingArea). """ # Now we determine the actual occupation of the index index_renderer = StreetIndexRenderer(self.rc.i18n, self.street_index.categories) # We use a fake vector device to determine the actual # rendering characteristics fake_surface = cairo.PDFSurface(None, self.paper_width_pt, self.paper_height_pt) if on_the_side: index_max_width_pt \ = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_width_pt if not self.rc.i18n.isrtl(): # non-RTL: Index is on the right index_area = index_renderer.precompute_occupation_area( fake_surface, ( self.paper_width_pt - Renderer.PRINT_SAFE_MARGIN_PT - index_max_width_pt ), ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), index_max_width_pt, self._usable_area_height_pt, 'width', 'right') else: # RTL: Index is on the left index_area = index_renderer.precompute_occupation_area( fake_surface, Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), index_max_width_pt, self._usable_area_height_pt, 'width', 'left') else: # Index at the bottom of the page index_max_height_pt \ = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_height_pt index_area = index_renderer.precompute_occupation_area( fake_surface, Renderer.PRINT_SAFE_MARGIN_PT, ( self.paper_height_pt - Renderer.PRINT_SAFE_MARGIN_PT - self._copyright_margin_pt - index_max_height_pt ), self._usable_area_width_pt, index_max_height_pt, 'height', 'bottom') return index_renderer, index_area def _draw_title(self, ctx, w_dots, h_dots, font_face): """ Draw the title at the current position inside a w_dots*h_dots rectangle. Args: ctx (cairo.Context): The Cairo context to use to draw. w_dots,h_dots (number): Rectangle dimension (ciaro units) font_face (str): Pango font specification. """ # Title background ctx.save() ctx.set_source_rgb(0.8, 0.9, 0.96) ctx.rectangle(0, 0, w_dots, h_dots) ctx.fill() ctx.restore() # Retrieve and paint the OSM logo ctx.save() grp, logo_width = self._get_osm_logo(ctx, 0.8*h_dots) if grp: ctx.translate(w_dots - logo_width - 0.1*h_dots, 0.1*h_dots) ctx.set_source(grp) ctx.paint_with_alpha(0.5) else: LOG.warning("OSM Logo not available.") logo_width = 0 ctx.restore() # Prepare the title pc = pangocairo.CairoContext(ctx) layout = pc.create_layout() layout.set_width(int((w_dots - 0.1*w_dots - logo_width) * pango.SCALE)) if not self.rc.i18n.isrtl(): layout.set_alignment(pango.ALIGN_LEFT) else: layout.set_alignment(pango.ALIGN_RIGHT) fd = pango.FontDescription(font_face) fd.set_size(pango.SCALE) layout.set_font_description(fd) layout.set_text(self.rc.title) draw_utils.adjust_font_size(layout, fd, layout.get_width(), 0.8*h_dots) # Draw the title ctx.save() ctx.rectangle(0, 0, w_dots, h_dots) ctx.stroke() ctx.translate(0.1*h_dots, (h_dots - (layout.get_size()[1] / pango.SCALE)) / 2.0) pc.show_layout(layout) ctx.restore() def _draw_copyright_notice(self, ctx, w_dots, h_dots, notice=None, osm_date=None): """ Draw a copyright notice at current location and within the given w_dots*h_dots rectangle. Args: ctx (cairo.Context): The Cairo context to use to draw. w_dots,h_dots (number): Rectangle dimension (ciaro units). font_face (str): Pango font specification. notice (str): Optional notice to replace the default. """ today = datetime.date.today() notice = notice or \ _(u'Copyright © %(year)d MapOSMatic/OCitySMap developers. ' u'Map data © %(year)d OpenStreetMap.org ' u'and contributors (cc-by-sa).\n' u'Map rendered on: %(date)s. OSM data updated on: %(osmdate)s. ' u'The map may be incomplete or inaccurate. ' u'You can contribute to improve this map. ' 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) ctx.save() pc = pangocairo.CairoContext(ctx) fd = pango.FontDescription('DejaVu') fd.set_size(pango.SCALE) layout = pc.create_layout() layout.set_font_description(fd) layout.set_text(notice) draw_utils.adjust_font_size(layout, fd, w_dots, h_dots) pc.show_layout(layout) ctx.restore() def render(self, cairo_surface, dpi, osm_date): """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. """ LOG.info('SinglePageRenderer rendering on %dx%dmm paper at %d dpi.' % (self.rc.paper_width_mm, self.rc.paper_height_mm, dpi)) # First determine some useful drawing parameters safe_margin_dots \ = commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT, dpi) usable_area_width_dots \ = commons.convert_pt_to_dots(self._usable_area_width_pt, dpi) usable_area_height_dots \ = commons.convert_pt_to_dots(self._usable_area_height_pt, dpi) title_margin_dots \ = commons.convert_pt_to_dots(self._title_margin_pt, dpi) copyright_margin_dots \ = commons.convert_pt_to_dots(self._copyright_margin_pt, dpi) map_coords_dots = map(lambda l: commons.convert_pt_to_dots(l, dpi), self._map_coords) ctx = cairo.Context(cairo_surface) # Set a white background ctx.save() ctx.set_source_rgb(1, 1, 1) ctx.rectangle(0, 0, commons.convert_pt_to_dots(self.paper_width_pt, dpi), commons.convert_pt_to_dots(self.paper_height_pt, dpi)) ctx.fill() ctx.restore() ## ## Draw the index, when applicable ## if self._index_renderer and self._index_area: ctx.save() # NEVER use ctx.scale() here because otherwise pango will # choose different dont metrics which may be incompatible # with what has been computed by __init__(), which may # require more columns than expected ! Instead, we have # to trick pangocairo into believing it is rendering to a # device with the same default resolution, but with a # cairo resolution matching the 'dpi' specified # resolution. See # index::render::StreetIndexRenederer::render() and # comments within. self._index_renderer.render(ctx, self._index_area, dpi) ctx.restore() # Also draw a rectangle ctx.save() ctx.rectangle(commons.convert_pt_to_dots(self._index_area.x, dpi), commons.convert_pt_to_dots(self._index_area.y, dpi), commons.convert_pt_to_dots(self._index_area.w, dpi), commons.convert_pt_to_dots(self._index_area.h, dpi)) ctx.stroke() ctx.restore() ## ## Draw the map, scaled to fit the designated area ## ctx.save() # Prepare to draw the map at the right location ctx.translate(map_coords_dots[0], map_coords_dots[1]) # Draw the rescaled Map ctx.save() rendered_map = self._map_canvas.get_rendered_map() LOG.debug('Mapnik scale: 1/%f' % rendered_map.scale_denominator()) LOG.debug('Actual scale: 1/%f' % self._map_canvas.get_actual_scale()) mapnik.render(rendered_map, ctx) ctx.restore() # Draw a rectangle around the map ctx.rectangle(0, 0, map_coords_dots[2], map_coords_dots[3]) ctx.stroke() # Place the vertical and horizontal square labels self._draw_labels(ctx, self.grid, map_coords_dots[2], map_coords_dots[3], commons.convert_pt_to_dots(self._grid_legend_margin_pt, dpi)) ctx.restore() ## ## Draw the title ## ctx.save() ctx.translate(safe_margin_dots, safe_margin_dots) self._draw_title(ctx, usable_area_width_dots, title_margin_dots, 'Georgia Bold') ctx.restore() ## ## Draw the copyright notice ## ctx.save() # Move to the right position ctx.translate(safe_margin_dots, ( safe_margin_dots + title_margin_dots + usable_area_height_dots + copyright_margin_dots/4. ) ) # Draw the copyright notice self._draw_copyright_notice(ctx, usable_area_width_dots, copyright_margin_dots, osm_date=osm_date) ctx.restore() # TODO: map scale # TODO: compass rose cairo_surface.flush() @staticmethod def _generic_get_compatible_paper_sizes(bounding_box, scale=Renderer.DEFAULT_SCALE, index_position = None): """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. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom). 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. """ # the mapnik scale depends on the latitude lat = bounding_box.get_top_left()[0] scale *= math.cos(math.radians(lat)) # by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi scale *= float(72) / 90 geo_height_m, geo_width_m = bounding_box.spheric_sizes() paper_width_mm = geo_width_m * 1000 / scale paper_height_mm = geo_height_m * 1000 / scale LOG.debug('Map represents %dx%dm, needs at least %.1fx%.1fcm ' 'on paper.' % (geo_width_m, geo_height_m, paper_width_mm/10., paper_height_mm/10.)) # Take index into account, when applicable if index_position == 'side': paper_width_mm /= (1. - SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO) elif index_position == 'bottom': paper_height_mm /= (1. - SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO) # Take margins into account paper_width_mm += 2 * commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT) paper_height_mm += 2 * commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT) # Take grid legend, title and copyright into account paper_width_mm /= 1 - Renderer.GRID_LEGEND_MARGIN_RATIO paper_height_mm /= 1 - (Renderer.GRID_LEGEND_MARGIN_RATIO + 0.05 + 0.02) # Transform the values into integers paper_width_mm = int(math.ceil(paper_width_mm)) paper_height_mm = int(math.ceil(paper_height_mm)) LOG.debug('Best fit is %.1fx%.1fcm.' % (paper_width_mm/10., paper_height_mm/10.)) # Test both portrait and landscape orientations when checking for paper # sizes. valid_sizes = [] for name, w, h in ocitysmap.layoutlib.PAPER_SIZES: portrait_ok = paper_width_mm <= w and paper_height_mm <= h landscape_ok = paper_width_mm <= h and paper_height_mm <= w if portrait_ok or landscape_ok: valid_sizes.append([name, w, h, portrait_ok, landscape_ok, False]) # Add a 'Custom' paper format to the list that perfectly matches the # bounding box. valid_sizes.append(['Best fit', min(paper_width_mm, paper_height_mm), max(paper_width_mm, paper_height_mm), paper_width_mm < paper_height_mm, paper_width_mm > paper_height_mm, False]) # select the first one as default valid_sizes[0][5] = True return valid_sizes
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 canvas for overlay on current page overla_canvas = None if self.rc.overlay: overlay_canvas = MapCanvas(self.rc.overlay, 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() if overlay_canvas: overlay_canvas.render() self.pages.append((map_canvas, map_grid, overlay_canvas)) # 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 __init__(self, db, rc, tmpdir, dpi, file_prefix, index_position = 'side'): """ Create the renderer. Args: rc (RenderingConfiguration): rendering parameters. tmpdir (os.path): Path to a temp dir that can hold temp files. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom), 'extra_page' (index on 2nd page for PDF). """ Renderer.__init__(self, db, rc, tmpdir, dpi) self.file_prefix = file_prefix # Prepare the index if rc.poi_file: self.street_index = PoiIndex(rc.poi_file) else: self.street_index = StreetIndex(db, rc.polygon_wkt, rc.i18n) if not self.street_index.categories: LOG.warning("Designated area leads to an empty index") self.street_index = None self.index_position = index_position # grid marker offset (originally used for solid grid frame, # now just for the letter/number overlay offset inside the map) self._grid_legend_margin_pt = \ min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt, Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt) # reserve space for the page title if given if self.rc.title: self._title_margin_pt = 0.05 * self.paper_height_pt else: self._title_margin_pt = 0 # reserve space for the page footer self._copyright_margin_pt = 0.03 * self.paper_height_pt # calculate remaining usable paper space after taking header # and footer into account 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 + self._title_margin_pt + self._copyright_margin_pt)) # Prepare the Index (may raise a IndexDoesNotFitError) if ( index_position and self.street_index and self.street_index.categories ): self._index_renderer, self._index_area \ = self._create_index_rendering(index_position) else: self._index_renderer, self._index_area = None, None # Prepare the layout of the whole page if not self._index_area or index_position == 'extra_page': # No index displayed self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, self._usable_area_height_pt ) elif index_position == 'side': # Index present, displayed on the side if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT: # Index on the right -> map on the left self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) else: # Index on the left -> map on the right self._map_coords = ( self._index_area.x + self._index_area.w, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) elif index_position == 'bottom': # Index present, displayed at the bottom -> map on top self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, ( self._usable_area_height_pt - self._index_area.h ) ) else: raise AssertionError("Invalid index position %s" % repr(index_position)) # Prepare the map self._map_canvas = self._create_map_canvas( float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, rc.osmid != None ) # Prepare overlay styles for uploaded files self._overlays = copy(self.rc.overlays) # generate style file for GPX file if self.rc.gpx_file: try: gpx_style = GpxStylesheet(self.rc.gpx_file, self.tmpdir) except Exception as e: LOG.warning("GPX stylesheet error: %s" % e) else: self._overlays.append(gpx_style) # denormalize UMAP json to geojson, then create style for it if self.rc.umap_file: try: umap_style = UmapStylesheet(self.rc.umap_file, self.tmpdir) except Exception as e: LOG.warning("UMAP stylesheet error: %s" % e) else: self._overlays.append(umap_style) # Prepare map overlays self._overlay_canvases = [] self._overlay_effects = {} for overlay in self._overlays: path = overlay.path.strip() if path.startswith('internal:'): plugin_name = path.lstrip('internal:') self._overlay_effects[plugin_name] = self.get_plugin(plugin_name) else: self._overlay_canvases.append(MapCanvas(overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi)) # add special POI marker overlay if a POI file is given # TODO: refactor this special case if self.rc.poi_file: self._overlay_effects['poi_markers'] = self.get_plugin('poi_markers') # Prepare the grid self.grid = self._create_grid(self._map_canvas, dpi) if index_position: # only show grid if an actual index refers to it self._apply_grid(self.grid, self._map_canvas) # Commit the internal rendering stack of the map self._map_canvas.render() for overlay_canvas in self._overlay_canvases: overlay_canvas.render()
class SinglePageRenderer(Renderer): """ This Renderer creates a full-page map, with the overlayed features like the grid, grid labels, scale and compass rose and can draw an index. """ name = 'generic_single_page' description = 'A generic full-page layout with or without index.' MAX_INDEX_OCCUPATION_RATIO = 1/3. def __init__(self, db, rc, tmpdir, dpi, file_prefix, index_position = 'side'): """ Create the renderer. Args: rc (RenderingConfiguration): rendering parameters. tmpdir (os.path): Path to a temp dir that can hold temp files. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom), 'extra_page' (index on 2nd page for PDF). """ Renderer.__init__(self, db, rc, tmpdir, dpi) self.file_prefix = file_prefix # Prepare the index if rc.poi_file: self.street_index = PoiIndex(rc.poi_file) else: self.street_index = StreetIndex(db, rc.polygon_wkt, rc.i18n) if not self.street_index.categories: LOG.warning("Designated area leads to an empty index") self.street_index = None self.index_position = index_position # grid marker offset (originally used for solid grid frame, # now just for the letter/number overlay offset inside the map) self._grid_legend_margin_pt = \ min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt, Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt) # reserve space for the page title if given if self.rc.title: self._title_margin_pt = 0.05 * self.paper_height_pt else: self._title_margin_pt = 0 # reserve space for the page footer self._copyright_margin_pt = 0.03 * self.paper_height_pt # calculate remaining usable paper space after taking header # and footer into account 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 + self._title_margin_pt + self._copyright_margin_pt)) # Prepare the Index (may raise a IndexDoesNotFitError) if ( index_position and self.street_index and self.street_index.categories ): self._index_renderer, self._index_area \ = self._create_index_rendering(index_position) else: self._index_renderer, self._index_area = None, None # Prepare the layout of the whole page if not self._index_area or index_position == 'extra_page': # No index displayed self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, self._usable_area_height_pt ) elif index_position == 'side': # Index present, displayed on the side if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT: # Index on the right -> map on the left self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) else: # Index on the left -> map on the right self._map_coords = ( self._index_area.x + self._index_area.w, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), ( self._usable_area_width_pt - self._index_area.w ), self._usable_area_height_pt ) elif index_position == 'bottom': # Index present, displayed at the bottom -> map on top self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), self._usable_area_width_pt, ( self._usable_area_height_pt - self._index_area.h ) ) else: raise AssertionError("Invalid index position %s" % repr(index_position)) # Prepare the map self._map_canvas = self._create_map_canvas( float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi, rc.osmid != None ) # Prepare overlay styles for uploaded files self._overlays = copy(self.rc.overlays) # generate style file for GPX file if self.rc.gpx_file: try: gpx_style = GpxStylesheet(self.rc.gpx_file, self.tmpdir) except Exception as e: LOG.warning("GPX stylesheet error: %s" % e) else: self._overlays.append(gpx_style) # denormalize UMAP json to geojson, then create style for it if self.rc.umap_file: try: umap_style = UmapStylesheet(self.rc.umap_file, self.tmpdir) except Exception as e: LOG.warning("UMAP stylesheet error: %s" % e) else: self._overlays.append(umap_style) # Prepare map overlays self._overlay_canvases = [] self._overlay_effects = {} for overlay in self._overlays: path = overlay.path.strip() if path.startswith('internal:'): plugin_name = path.lstrip('internal:') self._overlay_effects[plugin_name] = self.get_plugin(plugin_name) else: self._overlay_canvases.append(MapCanvas(overlay, self.rc.bounding_box, float(self._map_coords[2]), # W float(self._map_coords[3]), # H dpi)) # add special POI marker overlay if a POI file is given # TODO: refactor this special case if self.rc.poi_file: self._overlay_effects['poi_markers'] = self.get_plugin('poi_markers') # Prepare the grid self.grid = self._create_grid(self._map_canvas, dpi) if index_position: # only show grid if an actual index refers to it self._apply_grid(self.grid, self._map_canvas) # Commit the internal rendering stack of the map self._map_canvas.render() for overlay_canvas in self._overlay_canvases: overlay_canvas.render() def _create_index_rendering(self, index_position): """ Prepare to render the Street index. Args: index_position (string): None, side, bottom or extra_page Return a couple (StreetIndexRenderer, StreetIndexRenderingArea). """ index_area = None # Now we determine the actual occupation of the index if self.rc.poi_file: index_renderer = PoiIndexRenderer(self.rc.i18n, self.street_index.categories) else: index_renderer = StreetIndexRenderer(self.rc.i18n, self.street_index.categories) # We use a fake vector device to determine the actual # rendering characteristics fake_surface = cairo.PDFSurface(None, self.paper_width_pt, self.paper_height_pt) # calculate the area required for the index if index_position == 'side': index_max_width_pt \ = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_width_pt if not self.rc.i18n.isrtl(): # non-RTL: Index is on the right index_area = index_renderer.precompute_occupation_area( fake_surface, ( self.paper_width_pt - Renderer.PRINT_SAFE_MARGIN_PT - index_max_width_pt ), ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), index_max_width_pt, self._usable_area_height_pt, 'width', 'right') else: # RTL: Index is on the left index_area = index_renderer.precompute_occupation_area( fake_surface, Renderer.PRINT_SAFE_MARGIN_PT, ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ), index_max_width_pt, self._usable_area_height_pt, 'width', 'left') elif index_position == 'bottom': # Index at the bottom of the page index_max_height_pt \ = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_height_pt index_area = index_renderer.precompute_occupation_area( fake_surface, Renderer.PRINT_SAFE_MARGIN_PT, ( self.paper_height_pt - Renderer.PRINT_SAFE_MARGIN_PT - self._copyright_margin_pt - index_max_height_pt ), self._usable_area_width_pt, index_max_height_pt, 'height', 'bottom') return index_renderer, index_area def _draw_title(self, ctx, w_dots, h_dots, font_face): """ Draw the title at the current position inside a w_dots*h_dots rectangle. Args: ctx (cairo.Context): The Cairo context to use to draw. w_dots,h_dots (number): Rectangle dimension (ciaro units) font_face (str): Pango font specification. """ # Title background ctx.save() ctx.set_source_rgb(0.8, 0.9, 0.96) # TODO: make title bar color configurable? ctx.rectangle(0, 0, w_dots, h_dots) ctx.fill() ctx.restore() # Retrieve and paint the OSM logo ctx.save() grp, logo_width = self._get_osm_logo(ctx, 0.8*h_dots) if grp: ctx.translate(w_dots - logo_width - 0.1*h_dots, 0.1*h_dots) ctx.set_source(grp) ctx.paint_with_alpha(0.5) else: LOG.warning("OSM Logo not available.") logo_width = 0 ctx.restore() # Retrieve and paint the extra logo # TODO: logo_width2 = 0 if self.rc.poi_file: ctx.save() grp, logo_width2 = self._get_extra_logo(ctx, 0.8*h_dots) if grp: ctx.translate(0.4*h_dots, 0.1*h_dots) ctx.set_source(grp) ctx.paint_with_alpha(0.5) logo_width2 += 0.4*h_dots else: LOG.warning("Extra Logo not available.") logo_width2 = 0 ctx.restore() # Prepare the title pc = PangoCairo.create_context(ctx) layout = PangoCairo.create_layout(ctx) layout.set_width(int((w_dots - 0.1*w_dots - logo_width - logo_width2) * Pango.SCALE)) if not self.rc.i18n.isrtl(): layout.set_alignment(Pango.Alignment.LEFT) else: layout.set_alignment(Pango.Alignment.RIGHT) fd = Pango.FontDescription(font_face) fd.set_size(Pango.SCALE) layout.set_font_description(fd) layout.set_text(self.rc.title, -1) draw_utils.adjust_font_size(layout, fd, layout.get_width(), 0.8*h_dots) # Draw the title ctx.save() ctx.set_line_width(1) ctx.rectangle(0, 0, w_dots, h_dots) ctx.stroke() ctx.translate(0.4*h_dots + logo_width2, (h_dots - (layout.get_size()[1] / Pango.SCALE)) / 2.0) PangoCairo.update_layout(ctx, layout) PangoCairo.show_layout(ctx, layout) ctx.restore() def _draw_copyright_notice(self, ctx, w_dots, h_dots, notice=None, osm_date=None): """ Draw a copyright notice at current location and within the given w_dots*h_dots rectangle. Args: ctx (cairo.Context): The Cairo context to use to draw. w_dots,h_dots (number): Rectangle dimension (ciaro units). font_face (str): Pango font specification. notice (str): Optional notice to replace the default. """ today = datetime.date.today() if notice is None: notice = _(u'Copyright © %(year)d MapOSMatic/OCitySMap developers.') notice+= ' ' notice+= _(u'Map data © %(year)d OpenStreetMap contributors (see http://osm.org/copyright)') notice+= '\n' annotations = [] if self.rc.stylesheet.annotation != '': annotations.append(self.rc.stylesheet.annotation) for overlay in self._overlays: if overlay.annotation != '': annotations.append(overlay.annotation) if len(annotations) > 0: notice+= _(u'Map styles:') notice+= ' ' + '; '.join(annotations) + '\n' datasources = set() if self.rc.stylesheet.datasource != '': datasources.add(self.rc.stylesheet.datasource) for overlay in self._overlays: if overlay.datasource != '': datasources.add(overlay.datasource) if len(datasources) > 0: notice+= _(u'Additional data sources:') notice+= ' ' + '; '.join(list(datasources)) + '\n' notice+= _(u'Map rendered on: %(date)s. OSM data updated on: %(osmdate)s.') notice+= ' ' notice+= _(u'The map may be incomplete or inaccurate.') # We need the correct locale to be set for strftime(). prev_locale = locale.getlocale(locale.LC_TIME) try: locale.setlocale(locale.LC_TIME, self.rc.i18n.language_code()) except Exception: LOG.warning('error while setting LC_COLLATE to "%s"' % 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) ctx.save() pc = PangoCairo.create_context(ctx) fd = Pango.FontDescription('DejaVu') fd.set_size(Pango.SCALE) layout = PangoCairo.create_layout(ctx) layout.set_font_description(fd) layout.set_text(notice, -1) draw_utils.adjust_font_size(layout, fd, w_dots, h_dots) PangoCairo.update_layout(ctx, layout) PangoCairo.show_layout(ctx, layout) ctx.restore() def render(self, cairo_surface, dpi, osm_date): """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. """ LOG.info('SinglePageRenderer rendering -%s- on %dx%dmm paper at %d dpi.' % (self.rc.output_format, self.rc.paper_width_mm, self.rc.paper_height_mm, dpi)) # First determine some useful drawing parameters safe_margin_dots \ = commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT, dpi) usable_area_width_dots \ = commons.convert_pt_to_dots(self._usable_area_width_pt, dpi) usable_area_height_dots \ = commons.convert_pt_to_dots(self._usable_area_height_pt, dpi) title_margin_dots \ = commons.convert_pt_to_dots(self._title_margin_pt, dpi) copyright_margin_dots \ = commons.convert_pt_to_dots(self._copyright_margin_pt, dpi) map_coords_dots = list(map(lambda l: commons.convert_pt_to_dots(l, dpi), self._map_coords)) ctx = cairo.Context(cairo_surface) # Set a white background ctx.save() ctx.set_source_rgb(1, 1, 1) ctx.rectangle(0, 0, commons.convert_pt_to_dots(self.paper_width_pt, dpi), commons.convert_pt_to_dots(self.paper_height_pt, dpi)) ctx.fill() ctx.restore() ## ## Draw the map, scaled to fit the designated area ## ctx.save() # prevent map background from filling the full canvas ctx.rectangle(map_coords_dots[0], map_coords_dots[1], map_coords_dots[2], map_coords_dots[3]) ctx.clip() # Prepare to draw the map at the right location ctx.translate(map_coords_dots[0], map_coords_dots[1]) # Draw the rescaled Map ctx.save() scale_factor = int(dpi / 72) rendered_map = self._map_canvas.get_rendered_map() LOG.debug('Map:') LOG.debug('Mapnik scale: 1/%f' % rendered_map.scale_denominator()) LOG.debug('Actual scale: 1/%f' % self._map_canvas.get_actual_scale()) # exclude layers based on configuration setting "exclude_layers" for layer in rendered_map.layers: if layer.name in self.rc.stylesheet.exclude_layers: LOG.debug("Excluding layer: %s" % layer.name) layer.status = False # now perform the actual drawing mapnik.render(rendered_map, ctx, scale_factor, 0, 0) ctx.restore() # Draw the rescaled Overlays for overlay_canvas in self._overlay_canvases: ctx.save() rendered_overlay = overlay_canvas.get_rendered_map() LOG.debug('Overlay:') # TODO: overlay name mapnik.render(rendered_overlay, ctx, scale_factor, 0, 0) ctx.restore() # Place the vertical and horizontal square labels if self.grid and self.index_position: self._draw_labels(ctx, self.grid, map_coords_dots[2], map_coords_dots[3], commons.convert_pt_to_dots(self._grid_legend_margin_pt, dpi)) ctx.restore() # Draw a rectangle frame around the map ctx.save() ctx.set_line_width(1) ctx.rectangle(map_coords_dots[0], map_coords_dots[1], map_coords_dots[2], map_coords_dots[3]) ctx.stroke() ctx.restore() ## ## Draw the title ## if self.rc.title: ctx.save() ctx.translate(safe_margin_dots, safe_margin_dots) self._draw_title(ctx, usable_area_width_dots, title_margin_dots, 'Droid Sans Bold') ctx.restore() # make sure that plugins do not render outside the actual map area ctx.save() ctx.rectangle(map_coords_dots[0], map_coords_dots[1], map_coords_dots[2], map_coords_dots[3]) ctx.clip() # apply effect plugin overlays for plugin_name, effect in self._overlay_effects.items(): try: effect.render(self, ctx) except Exception as e: # TODO better logging LOG.warning("Error while rendering overlay: %s\n%s" % (plugin_name, e)) ctx.restore() ## ## Draw the index, when applicable ## # Update the street_index to reflect the grid's actual position if self.grid and self.street_index and self.index_position is not None: self.street_index.apply_grid(self.grid) # Dump the CSV street index self.street_index.write_to_csv(self.rc.title, '%s.csv' % self.file_prefix) if self._index_renderer and self._index_area: ctx.save() # NEVER use ctx.scale() here because otherwise pango will # choose different dont metrics which may be incompatible # with what has been computed by __init__(), which may # require more columns than expected ! Instead, we have # to trick pangocairo into believing it is rendering to a # device with the same default resolution, but with a # cairo resolution matching the 'dpi' specified # resolution. See # index::render::StreetIndexRenederer::render() and # comments within. self._index_renderer.render(ctx, self._index_area, dpi) ctx.restore() # Also draw a rectangle frame around the index ctx.save() ctx.set_line_width(1) ctx.rectangle(commons.convert_pt_to_dots(self._index_area.x, dpi), commons.convert_pt_to_dots(self._index_area.y, dpi), commons.convert_pt_to_dots(self._index_area.w, dpi), commons.convert_pt_to_dots(self._index_area.h, dpi)) ctx.stroke() ctx.restore() ## ## Draw the copyright notice ## ctx.save() # Move to the right position ctx.translate(safe_margin_dots, ( safe_margin_dots + title_margin_dots + usable_area_height_dots + copyright_margin_dots/4. ) ) # Draw the copyright notice self._draw_copyright_notice(ctx, usable_area_width_dots, copyright_margin_dots, osm_date=osm_date) ctx.restore() # render index on 2nd page if requested, and output format supports it if self.index_position == 'extra_page' and self._has_multipage_format() and self._index_renderer is not None: cairo_surface.show_page() # We use a fake vector device to determine the actual # rendering characteristics fake_surface = cairo.PDFSurface(None, self.paper_width_pt, self.paper_height_pt) usable_area_width_pt = (self.paper_width_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT) usable_area_height_pt = (self.paper_height_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT) index_area = self._index_renderer.precompute_occupation_area( fake_surface, Renderer.PRINT_SAFE_MARGIN_PT, ( self.paper_height_pt - Renderer.PRINT_SAFE_MARGIN_PT - usable_area_height_pt ), usable_area_width_pt, usable_area_height_pt, 'width', 'left') ctx.save() self._index_renderer.render(ctx, index_area, dpi) ctx.restore() cairo_surface.show_page() else: cairo_surface.flush() @staticmethod def _generic_get_compatible_paper_sizes(bounding_box, paper_sizes, scale=Renderer.DEFAULT_SCALE, index_position = None): """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. index_position (str): None or 'side' (index on side), 'bottom' (index at bottom), 'extra_page' (index on 2nd page for PDF). 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. """ # the mapnik scale depends on the latitude lat = bounding_box.get_top_left()[0] scale *= math.cos(math.radians(lat)) # by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi scale *= float(72) / 90 geo_height_m, geo_width_m = bounding_box.spheric_sizes() paper_width_mm = geo_width_m * 1000 / scale paper_height_mm = geo_height_m * 1000 / scale LOG.debug('Map represents %dx%dm, needs at least %.1fx%.1fcm ' 'on paper.' % (geo_width_m, geo_height_m, paper_width_mm/10., paper_height_mm/10.)) # Take index into account, when applicable if index_position == 'side': paper_width_mm /= (1. - SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO) elif index_position == 'bottom': paper_height_mm /= (1. - SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO) # Take margins into account paper_width_mm += 2 * commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT) paper_height_mm += 2 * commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT) # Take grid legend, title and copyright into account paper_width_mm /= 1 - Renderer.GRID_LEGEND_MARGIN_RATIO paper_height_mm /= 1 - (Renderer.GRID_LEGEND_MARGIN_RATIO + 0.05 + 0.02) # Transform the values into integers paper_width_mm = int(math.ceil(paper_width_mm)) paper_height_mm = int(math.ceil(paper_height_mm)) # TODO make min. width / height configurable if paper_width_mm < 100: paper_height_mm = paper_height_mm * 100 / paper_width_mm paper_width_mm = 100 if paper_height_mm < 100: paper_width_mm = paper_width_mm * 100 / paper_height_mm paper_height_mm = 100 LOG.info('Best fit is %.0fx%.0fmm.' % (paper_width_mm, paper_height_mm)) # Test both portrait and landscape orientations when checking for paper # sizes. is_default = True valid_sizes = [] for name, w, h in paper_sizes: LOG.debug("is %s compatible" % name) if w is None: continue portrait_ok = paper_width_mm <= w and paper_height_mm <= h landscape_ok = paper_width_mm <= h and paper_height_mm <= w if portrait_ok or landscape_ok: valid_sizes.append({ "name": name, "width": w, "height": h, "portrait_ok": portrait_ok, "landscape_ok": landscape_ok, "default": is_default, "landscape_preferred": paper_width_mm > paper_height_mm }) is_default = False # Add a 'Custom' paper format to the list that perfectly matches the # bounding box. valid_sizes.append({ "name": 'Best fit', "width": min(paper_width_mm, paper_height_mm), "height": max(paper_width_mm, paper_height_mm), "portrait_ok": paper_width_mm < paper_height_mm, "landscape_ok": paper_width_mm > paper_height_mm, "default": is_default, "landscape_preferred": paper_width_mm > paper_height_mm }) valid_sizes.append({ "name": 'Custom', "width": 0, "height": 0, "portrait_ok": True, "landscape_ok": False, "default": False, "landscape_preferred": False }) return valid_sizes
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)