kopia lustrzana https://github.com/hholzgra/ocitysmap
				
				
				
			
		
			
				
	
	
		
			556 wiersze
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			556 wiersze
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| # ocitysmap, city map and street index generator from OpenStreetMap data
 | |
| # Copyright (C) 2010  David Decotigny
 | |
| # Copyright (C) 2010  Frédéric Lehobey
 | |
| # Copyright (C) 2010  Pierre Mauduit
 | |
| # Copyright (C) 2010  David Mentré
 | |
| # Copyright (C) 2010  Maxime Petazzoni
 | |
| # Copyright (C) 2010  Thomas Petazzoni
 | |
| # Copyright (C) 2010  Gaël Utard
 | |
| 
 | |
| # This program is free software: you can redistribute it and/or modify
 | |
| # it under the terms of the GNU Affero General Public License as
 | |
| # published by the Free Software Foundation, either version 3 of the
 | |
| # License, or any later version.
 | |
| 
 | |
| # This program is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU Affero General Public License for more details.
 | |
| 
 | |
| # You should have received a copy of the GNU Affero General Public License
 | |
| # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| """OCitySMap 2.
 | |
| 
 | |
| OCitySMap is a Mapnik-based map rendering engine from OpenStreetMap.org data.
 | |
| It is architectured around the concept of Renderers, in charge of rendering the
 | |
| map and all the visual features that go along with it (scale, grid, legend,
 | |
| index, etc.) on the given paper size using a provided Mapnik stylesheet,
 | |
| according to their implemented layout.
 | |
| 
 | |
| The PlainRenderer for example renders a full-page map with its grid, a title
 | |
| header and copyright notice, but without the index.
 | |
| 
 | |
| How to use OCitySMap?
 | |
| ---------------------
 | |
| 
 | |
| The API of OCitySMap is very simple. First, you need to instanciate the main
 | |
| OCitySMap class with the path to your OCitySMap configuration file (see
 | |
| ocitysmap.conf-template):
 | |
| 
 | |
| 
 | |
|     ocitysmap = ocitysmap2.OCitySMap('/path/to/your/config')
 | |
| 
 | |
| The next step is to create a RenderingConfiguration, the object that
 | |
| encapsulates all the information to parametize the rendering, including the
 | |
| Mapnik stylesheet. You can retrieve the list of supported stylesheets (directly
 | |
| as Stylesheet objects) with:
 | |
| 
 | |
|     styles = ocitysmap.get_all_style_configurations()
 | |
| 
 | |
| Fill in your RenderingConfiguration with the map title, the OSM ID or bounding
 | |
| box, the chosen map language, the Stylesheet object and the paper size (in
 | |
| millimeters) and simply pass it to OCitySMap's render method:
 | |
| 
 | |
|     ocitysmap.render(rendering_configuration, layout_name,
 | |
|                      output_formats, prefix)
 | |
| 
 | |
| The layout name is the renderer's key name. You can get the list of all
 | |
| supported renderers with ocitysmap.get_all_renderers(). The output_formats is a
 | |
| list of output formats. For now, the following formats are supported:
 | |
| 
 | |
|     * PNG at 72dpi
 | |
|     * PDF
 | |
|     * SVG
 | |
|     * SVGZ (gzipped-SVG)
 | |
|     * PS
 | |
| 
 | |
| The prefix is the filename prefix for all the rendered files. This is usually a
 | |
| path to the destination's directory, eventually followed by some unique, yet
 | |
| common prefix for the files rendered for a job.
 | |
| """
 | |
| 
 | |
| __author__ = 'The MapOSMatic developers'
 | |
| __version__ = '0.2'
 | |
| 
 | |
| import cairo
 | |
| import ConfigParser
 | |
| import gzip
 | |
| import logging
 | |
| import os
 | |
| import psycopg2
 | |
| import re
 | |
| import tempfile
 | |
| 
 | |
| import shapely
 | |
| import shapely.wkt
 | |
| import shapely.geometry
 | |
| 
 | |
| import coords
 | |
| import i18n
 | |
| 
 | |
| from indexlib.indexer import StreetIndex
 | |
| from indexlib.commons import IndexDoesNotFitError, IndexEmptyError
 | |
| 
 | |
| from layoutlib import PAPER_SIZES, renderers
 | |
| import layoutlib.commons
 | |
| 
 | |
| LOG = logging.getLogger('ocitysmap')
 | |
| 
 | |
| class RenderingConfiguration:
 | |
|     """
 | |
|     The RenderingConfiguration class encapsulate all the information concerning
 | |
|     a rendering request. This data is used by the layout renderer, in
 | |
|     conjonction with its rendering mode (defined by its implementation), to
 | |
|     produce the map.
 | |
|     """
 | |
| 
 | |
|     def __init__(self):
 | |
|         self.title           = None # str
 | |
|         self.osmid           = None # None / int (shading + city name)
 | |
|         self.bounding_box    = None # bbox (from osmid if None)
 | |
|         self.language        = None # str (locale)
 | |
| 
 | |
|         self.stylesheet      = None # Obj Stylesheet
 | |
| 
 | |
|         self.paper_width_mm  = None
 | |
|         self.paper_height_mm = None
 | |
| 
 | |
|         # Setup by OCitySMap::render() from osmid and bounding_box fields:
 | |
|         self.polygon_wkt     = None # str (WKT of interest)
 | |
| 
 | |
|         # Setup by OCitySMap::render() from language field:
 | |
|         self.i18n            = None # i18n object
 | |
| 
 | |
| 
 | |
| class Stylesheet:
 | |
|     """
 | |
|     A Stylesheet object defines how the map features will be rendered. It
 | |
|     contains information pointing to the Mapnik stylesheet and other styling
 | |
|     parameters.
 | |
|     """
 | |
|     DEFAULT_ZOOM_LEVEL = 16
 | |
| 
 | |
|     def __init__(self):
 | |
|         self.name        = None # str
 | |
|         self.path        = None # str
 | |
|         self.description = '' # str
 | |
| 
 | |
|         self.grid_line_color = 'black'
 | |
|         self.grid_line_alpha = 0.5
 | |
|         self.grid_line_width = 1
 | |
| 
 | |
|         self.shade_color = 'black'
 | |
|         self.shade_alpha = 0.1
 | |
| 
 | |
|         # shade color for town contour in multi-pages
 | |
|         self.shade_color_2 = 'white'
 | |
|         self.shade_alpha_2 = 0.4
 | |
| 
 | |
|     @staticmethod
 | |
|     def create_from_config_section(parser, section_name):
 | |
|         """Creates a Stylesheet object from the OCitySMap configuration.
 | |
| 
 | |
|         Args:
 | |
|             parser (ConfigParser.ConfigParser): the configuration parser
 | |
|                 object.
 | |
|             section_name (string): the stylesheet section name in the
 | |
|                 configuration.
 | |
|         """
 | |
|         s = Stylesheet()
 | |
| 
 | |
|         def assign_if_present(key, cast_fn=str):
 | |
|             if parser.has_option(section_name, key):
 | |
|                 setattr(s, key, cast_fn(parser.get(section_name, key)))
 | |
| 
 | |
|         s.name = parser.get(section_name, 'name')
 | |
|         s.path = parser.get(section_name, 'path')
 | |
|         assign_if_present('description')
 | |
| 
 | |
|         assign_if_present('grid_line_color')
 | |
|         assign_if_present('grid_line_alpha', float)
 | |
|         assign_if_present('grid_line_width', int)
 | |
| 
 | |
|         assign_if_present('shade_color')
 | |
|         assign_if_present('shade_alpha', float)
 | |
| 
 | |
|         assign_if_present('shade_color_2')
 | |
|         assign_if_present('shade_alpha_2', float)
 | |
|         return s
 | |
| 
 | |
|     @staticmethod
 | |
|     def create_all_from_config(parser):
 | |
|         styles = parser.get('rendering', 'available_stylesheets')
 | |
|         if not styles:
 | |
|             raise ValueError, \
 | |
|                     'OCitySMap configuration does not contain any stylesheet!'
 | |
| 
 | |
|         return [Stylesheet.create_from_config_section(parser, name.strip())
 | |
|                 for name in styles.split(',')]
 | |
| 
 | |
| class OCitySMap:
 | |
|     """
 | |
|     This is the main entry point of the OCitySMap map rendering engine. Read
 | |
|     this module's documentation for more details on its API.
 | |
|     """
 | |
| 
 | |
|     DEFAULT_REQUEST_TIMEOUT_MIN = 15
 | |
| 
 | |
|     DEFAULT_RENDERING_PNG_DPI = 72
 | |
| 
 | |
|     STYLESHEET_REGISTRY = []
 | |
| 
 | |
|     def __init__(self, config_files=None):
 | |
|         """Instanciate a new configured OCitySMap instance.
 | |
| 
 | |
|         Args:
 | |
|             config_file (string or list or None): path, or list of paths to
 | |
|                 the OCitySMap configuration file(s). If None, sensible defaults
 | |
|                 are tried.
 | |
|         """
 | |
| 
 | |
|         if config_files is None:
 | |
|             config_files = ['/etc/ocitysmap.conf', '~/.ocitysmap.conf']
 | |
|         elif not isinstance(config_files, list):
 | |
|             config_files = [config_files]
 | |
| 
 | |
|         config_files = map(os.path.expanduser, config_files)
 | |
|         LOG.info('Reading OCitySMap configuration from %s...' %
 | |
|                  ', '.join(config_files))
 | |
| 
 | |
|         self._parser = ConfigParser.RawConfigParser()
 | |
|         if not self._parser.read(config_files):
 | |
|             raise IOError, 'None of the configuration files could be read!'
 | |
| 
 | |
|         self._locale_path = os.path.join(os.path.dirname(__file__), '..', 'locale')
 | |
|         self.__db = None
 | |
| 
 | |
|         # Read stylesheet configuration
 | |
|         self.STYLESHEET_REGISTRY = Stylesheet.create_all_from_config(self._parser)
 | |
|         LOG.debug('Found %d Mapnik stylesheets.'
 | |
|                   % len(self.STYLESHEET_REGISTRY))
 | |
| 
 | |
|     @property
 | |
|     def _db(self):
 | |
|         if self.__db:
 | |
|             return self.__db
 | |
| 
 | |
|         # Database connection
 | |
|         datasource = dict(self._parser.items('datasource'))
 | |
|         # The port is not a mandatory configuration option, so make
 | |
|         # sure we define a default value.
 | |
|         if not datasource.has_key('port'):
 | |
|             datasource['port'] = 5432
 | |
|         LOG.info('Connecting to database %s on %s:%s as %s...' %
 | |
|                  (datasource['dbname'], datasource['host'], datasource['port'],
 | |
|                   datasource['user']))
 | |
| 
 | |
|         db = psycopg2.connect(user=datasource['user'],
 | |
|                               password=datasource['password'],
 | |
|                               host=datasource['host'],
 | |
|                               database=datasource['dbname'],
 | |
|                               port=datasource['port'])
 | |
| 
 | |
|         # Force everything to be unicode-encoded, in case we run along Django
 | |
|         # (which loads the unicode extensions for psycopg2)
 | |
|         db.set_client_encoding('utf8')
 | |
| 
 | |
|         # Make sure the DB is correctly installed
 | |
|         self._verify_db(db)
 | |
| 
 | |
|         try:
 | |
|             timeout = int(self._parser.get('datasource', 'request_timeout'))
 | |
|         except (ConfigParser.NoOptionError, ValueError):
 | |
|             timeout = OCitySMap.DEFAULT_REQUEST_TIMEOUT_MIN
 | |
|         self._set_request_timeout(db, timeout)
 | |
| 
 | |
|         self.__db = db
 | |
|         return self.__db
 | |
| 
 | |
|     def _verify_db(self, db):
 | |
|         """Make sure the PostGIS DB is compatible with us."""
 | |
|         cursor = db.cursor()
 | |
|         cursor.execute("""
 | |
| SELECT ST_AsText(ST_LongestLine(
 | |
|                     'POINT(100 100)'::geometry,
 | |
| 		    'LINESTRING(20 80, 98 190, 110 180, 50 75 )'::geometry)
 | |
| 	        ) As lline;
 | |
| """)
 | |
|         assert cursor.fetchall()[0][0] == "LINESTRING(100 100,98 190)", \
 | |
|             LOG.fatal("PostGIS >= 1.5 required for correct operation !")
 | |
| 
 | |
|     def _set_request_timeout(self, db, timeout_minutes=15):
 | |
|         """Sets the PostgreSQL request timeout to avoid long-running queries on
 | |
|         the database."""
 | |
|         cursor = db.cursor()
 | |
|         cursor.execute('set session statement_timeout=%d;' %
 | |
|                        (timeout_minutes * 60 * 1000))
 | |
|         cursor.execute('show statement_timeout;')
 | |
|         LOG.debug('Configured statement timeout: %s.' %
 | |
|                   cursor.fetchall()[0][0])
 | |
| 
 | |
|     def _cleanup_tempdir(self, tmpdir):
 | |
|         LOG.debug('Cleaning up %s...' % tmpdir)
 | |
|         for root, dirs, files in os.walk(tmpdir, topdown=False):
 | |
|             for name in files:
 | |
|                 os.remove(os.path.join(root, name))
 | |
|             for name in dirs:
 | |
|                 os.rmdir(os.path.join(root, name))
 | |
|         os.rmdir(tmpdir)
 | |
| 
 | |
|     def _get_geographic_info(self, osmid, table):
 | |
|         """Return the area for the given osm id in the given table, or raise
 | |
|         LookupError when not found
 | |
| 
 | |
|         Args:
 | |
|             osmid (integer): OSM ID
 | |
|             table (str): either 'polygon' or 'line'
 | |
| 
 | |
|         Return:
 | |
|             Geos geometry object
 | |
|         """
 | |
| 
 | |
|         # Ensure all OSM IDs are integers, bust cast them back to strings
 | |
|         # afterwards.
 | |
|         LOG.debug('Looking up bounding box and contour of OSM ID %d...'
 | |
|                   % osmid)
 | |
| 
 | |
|         cursor = self._db.cursor()
 | |
|         cursor.execute("""select
 | |
|                             st_astext(st_transform(st_buildarea(st_union(way)),
 | |
|                                                    4002))
 | |
|                           from planet_osm_%s where osm_id = %d
 | |
|                           group by osm_id;""" %
 | |
|                        (table, osmid))
 | |
|         records = cursor.fetchall()
 | |
|         try:
 | |
|             ((wkt,),) = records
 | |
|         except ValueError:
 | |
|             raise LookupError("OSM ID %d not found in table %s" %
 | |
|                               (osmid, table))
 | |
| 
 | |
|         return shapely.wkt.loads(wkt)
 | |
| 
 | |
|     def get_geographic_info(self, osmid):
 | |
|         """Return a tuple (WKT_envelope, WKT_buildarea) or raise
 | |
|         LookupError when not found
 | |
| 
 | |
|         Args:
 | |
|             osmid (integer): OSM ID
 | |
| 
 | |
|         Return:
 | |
|             tuple (WKT bbox, WKT area)
 | |
|         """
 | |
|         found = False
 | |
| 
 | |
|         # Scan polygon table:
 | |
|         try:
 | |
|             polygon_geom = self._get_geographic_info(osmid, 'polygon')
 | |
|             found = True
 | |
|         except LookupError:
 | |
|             polygon_geom = shapely.geometry.Polygon()
 | |
| 
 | |
|         # Scan line table:
 | |
|         try:
 | |
|             line_geom = self._get_geographic_info(osmid, 'line')
 | |
|             found = True
 | |
|         except LookupError:
 | |
|             line_geom = shapely.geometry.Polygon()
 | |
| 
 | |
|         # Merge results:
 | |
|         if not found:
 | |
|             raise LookupError("No such OSM id: %d" % osmid)
 | |
| 
 | |
|         result = polygon_geom.union(line_geom)
 | |
|         return (result.envelope.wkt, result.wkt)
 | |
| 
 | |
|     def get_osm_database_last_update(self):
 | |
|         cursor = self._db.cursor()
 | |
|         query = "select last_update from maposmatic_admin;"
 | |
|         try:
 | |
|             cursor.execute(query)
 | |
|         except psycopg2.ProgrammingError:
 | |
|             self._db.rollback()
 | |
|             return None
 | |
|         # Extract datetime object. It is located as the first element
 | |
|         # of a tuple, itself the first element of an array.
 | |
|         return cursor.fetchall()[0][0]
 | |
| 
 | |
|     def get_all_style_configurations(self):
 | |
|         """Returns the list of all available stylesheet configurations (list of
 | |
|         Stylesheet objects)."""
 | |
|         return self.STYLESHEET_REGISTRY
 | |
| 
 | |
|     def get_stylesheet_by_name(self, name):
 | |
|         """Returns a stylesheet by its key name."""
 | |
|         for style in self.STYLESHEET_REGISTRY:
 | |
|             if style.name == name:
 | |
|                 return style
 | |
|         raise LookupError, 'The requested stylesheet %s was not found!' % name
 | |
| 
 | |
|     def get_all_renderers(self):
 | |
|         """Returns the list of all available layout renderers (list of
 | |
|         Renderer classes)."""
 | |
|         return renderers.get_renderers()
 | |
| 
 | |
|     def get_all_paper_sizes(self):
 | |
|         return PAPER_SIZES
 | |
| 
 | |
|     def render(self, config, renderer_name, output_formats, file_prefix):
 | |
|         """Renders a job with the given rendering configuration, using the
 | |
|         provided renderer, to the given output formats.
 | |
| 
 | |
|         Args:
 | |
|             config (RenderingConfiguration): the rendering configuration
 | |
|                 object.
 | |
|             renderer_name (string): the layout renderer to use for this rendering.
 | |
|             output_formats (list): a list of output formats to render to, from
 | |
|                 the list of supported output formats (pdf, svgz, etc.).
 | |
|             file_prefix (string): filename prefix for all output files.
 | |
|         """
 | |
| 
 | |
|         assert config.osmid or config.bounding_box, \
 | |
|                 'At least an OSM ID or a bounding box must be provided!'
 | |
| 
 | |
|         output_formats = map(lambda x: x.lower(), output_formats)
 | |
|         config.i18n = i18n.install_translation(config.language,
 | |
|                                                self._locale_path)
 | |
| 
 | |
|         LOG.info('Rendering with renderer %s in language: %s (rtl: %s).' %
 | |
|                  (renderer_name, config.i18n.language_code(),
 | |
|                   config.i18n.isrtl()))
 | |
| 
 | |
|         # Determine bounding box and WKT of interest
 | |
|         if config.osmid:
 | |
|             osmid_bbox, osmid_area \
 | |
|                 = self.get_geographic_info(config.osmid)
 | |
| 
 | |
|             # Define the bbox if not already defined
 | |
|             if not config.bounding_box:
 | |
|                 config.bounding_box \
 | |
|                     = coords.BoundingBox.parse_wkt(osmid_bbox)
 | |
| 
 | |
|             # Update the polygon WKT of interest
 | |
|             config.polygon_wkt = osmid_area
 | |
|         else:
 | |
|             # No OSM ID provided => use specified bbox
 | |
|             config.polygon_wkt = config.bounding_box.as_wkt()
 | |
| 
 | |
|         # Make sure we have a bounding box
 | |
|         assert config.bounding_box is not None
 | |
|         assert config.polygon_wkt is not None
 | |
| 
 | |
|         osm_date = self.get_osm_database_last_update()
 | |
| 
 | |
|         # Create a temporary directory for all our shape files
 | |
|         tmpdir = tempfile.mkdtemp(prefix='ocitysmap')
 | |
|         try:
 | |
|             LOG.debug('Rendering in temporary directory %s' % tmpdir)
 | |
| 
 | |
|             # Prepare the generic renderer
 | |
|             renderer_cls = renderers.get_renderer_class_by_name(renderer_name)
 | |
| 
 | |
|             # Perform the actual rendering to the Cairo devices
 | |
|             for output_format in output_formats:
 | |
|                 output_filename = '%s.%s' % (file_prefix, output_format)
 | |
|                 try:
 | |
|                     self._render_one(config, tmpdir, renderer_cls,
 | |
|                                      output_format, output_filename, osm_date,
 | |
|                                      file_prefix)
 | |
|                 except IndexDoesNotFitError:
 | |
|                     LOG.exception("The actual font metrics probably don't "
 | |
|                                   "match those pre-computed by the renderer's"
 | |
|                                   "constructor. Backtrace follows...")
 | |
|         finally:
 | |
|             self._cleanup_tempdir(tmpdir)
 | |
| 
 | |
|     def _render_one(self, config, tmpdir, renderer_cls,
 | |
|                     output_format, output_filename, osm_date, file_prefix):
 | |
| 
 | |
|         LOG.info('Rendering to %s format...' % output_format.upper())
 | |
| 
 | |
|         factory = None
 | |
|         dpi = layoutlib.commons.PT_PER_INCH
 | |
| 
 | |
|         if output_format == 'png':
 | |
|             try:
 | |
|                 dpi = int(self._parser.get('rendering', 'png_dpi'))
 | |
|             except ConfigParser.NoOptionError:
 | |
|                 dpi = OCitySMap.DEFAULT_RENDERING_PNG_DPI
 | |
| 
 | |
|             # As strange as it may seem, we HAVE to use a vector
 | |
|             # device here and not a raster device such as
 | |
|             # ImageSurface. Because, for some reason, with
 | |
|             # ImageSurface, the font metrics would NOT match those
 | |
|             # pre-computed by renderer_cls.__init__() and used to
 | |
|             # layout the whole page
 | |
|             def factory(w,h):
 | |
|                 w_px = int(layoutlib.commons.convert_pt_to_dots(w, dpi))
 | |
|                 h_px = int(layoutlib.commons.convert_pt_to_dots(h, dpi))
 | |
|                 LOG.debug("Rendering PNG into %dpx x %dpx area..."
 | |
|                           % (w_px, h_px))
 | |
|                 return cairo.PDFSurface(None, w_px, h_px)
 | |
| 
 | |
|         elif output_format == 'svg':
 | |
|             factory = lambda w,h: cairo.SVGSurface(output_filename, w, h)
 | |
|         elif output_format == 'svgz':
 | |
|             factory = lambda w,h: cairo.SVGSurface(
 | |
|                     gzip.GzipFile(output_filename, 'wb'), w, h)
 | |
|         elif output_format == 'pdf':
 | |
|             factory = lambda w,h: cairo.PDFSurface(output_filename, w, h)
 | |
|         elif output_format == 'ps':
 | |
|             factory = lambda w,h: cairo.PSSurface(output_filename, w, h)
 | |
|         elif output_format == 'ps.gz':
 | |
|             factory = lambda w,h: cairo.PSSurface(
 | |
|                 gzip.GzipFile(output_filename, 'wb'), w, h)
 | |
|         elif output_format == 'csv':
 | |
|             # We don't render maps into CSV.
 | |
|             return
 | |
| 
 | |
|         else:
 | |
|             raise ValueError, \
 | |
|                 'Unsupported output format: %s!' % output_format.upper()
 | |
| 
 | |
|         renderer = renderer_cls(self._db, config, tmpdir, dpi, file_prefix)
 | |
| 
 | |
|         surface = factory(renderer.paper_width_pt, renderer.paper_height_pt)
 | |
| 
 | |
|         renderer.render(surface, dpi, osm_date)
 | |
| 
 | |
|         LOG.debug('Writing %s...' % output_filename)
 | |
|         if output_format == 'png':
 | |
|             surface.write_to_png(output_filename)
 | |
| 
 | |
|         surface.finish()
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     logging.basicConfig(level=logging.DEBUG)
 | |
| 
 | |
|     o = OCitySMap([os.path.join(os.path.dirname(__file__), '..',
 | |
|                                 'ocitysmap.conf.mine')])
 | |
| 
 | |
|     c = RenderingConfiguration()
 | |
|     c.title = 'Chevreuse, Yvelines, Île-de-France, France, Europe, Monde'
 | |
|     c.osmid = -943886 # Chevreuse
 | |
|     # c.osmid = -7444   # Paris
 | |
|     c.language = 'fr_FR.UTF-8'
 | |
|     c.paper_width_mm = 297
 | |
|     c.paper_height_mm = 420
 | |
|     c.stylesheet = o.get_stylesheet_by_name('Default')
 | |
| 
 | |
|     # c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
 | |
|     o.render(c, 'single_page_index_bottom',
 | |
|              ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
 | |
|              '/tmp/mymap_index_bottom')
 | |
| 
 | |
|     c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
 | |
|     o.render(c, 'single_page_index_side',
 | |
|              ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
 | |
|              '/tmp/mymap_index_side')
 | |
| 
 | |
|     o.render(c, 'plain',
 | |
|              ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
 | |
|              '/tmp/mymap_plain')
 |