From c983f67c0320b3cab16b2bc9f2bc71b11e26e87b Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sat, 19 Oct 2019 21:25:05 +0200 Subject: [PATCH] Add installer --- README.md | 55 +---- hm-render-mapnik.py | 199 ------------------ ....config.xml => hm_render_mapnik.config.xml | 6 +- hm_render_mapnik/__init__.py | 2 + hm_render_mapnik/hm_render_mapnik.py | 189 +++++++++++++++++ requirements.txt | 1 + setup.py | 31 +++ 7 files changed, 231 insertions(+), 252 deletions(-) delete mode 100755 hm-render-mapnik.py rename render_mapnik.config.xml => hm_render_mapnik.config.xml (87%) create mode 100644 hm_render_mapnik/__init__.py create mode 100755 hm_render_mapnik/hm_render_mapnik.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/README.md b/README.md index 794867b..890a7c0 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,10 @@ # Mapnik rendering -The render.py script renders an area with given boundaries using mapnik. It is designed to work with hikingmap but it can be used standalone as well if desired. +This program renders an area with given boundaries using mapnik. It is designed to work with hikingmap but it can be used standalone as well if desired. -## Usage - -`render.py [OPTION]... gpxfiles...` - -Options: - -| Parameter | Description -| --------- | ----------- -| `-o` | Minimum longitude of the area boundaries -| `-O` | Maximum longitude of the area boundaries -| `-a` | Minimum latitude of the area boundaries -| `-A` | Maximum latitude of the area boundaries -| `-w` | Page width in cm -| `-h` | Page height in cm -| `-t` | Temp track file to render. This is used to draw the page boundaries of the overview map, hikingmap will save those as a temporary GPX file. -| `-y` | Temp waypoints file to render. This is used to render the distance each kilometer or mile, hikingmap will save those waypoints as a temporary GPX file. -| `-v, --verbose` | Display extra information while processing. -| `-h, --help` | Display help -| `gpxfiles` | The GPX track(s) to render. - -## Configuration - -Apart from these parameters there are some specific parameters for this renderer. They need to be configured in the file render_mapnik.config.xml. An example is included in this repository: - -``` - - - mapnik_style.xml - hikingmap_style.xml - pdf - 300 - 1.0 - - /usr/share/fonts/noto - /usr/share/fonts/noto-cjk - /usr/share/fonts/TTF - - +## Installation +Clone this repository and run the following command in the created directory. +```bash +python setup.py install ``` -Options: - -| Tag | Description -| --- | ----------- -| mapstyle | The filename of the mapnik stylesheet. This stylesheet contains the style to draw the actual map. -| hikingmapstyle | The filename of the hikingmap stylesheet. This stylesheet contains the styles to draw the GPX track and waypoints. -| outputformat | Output format. See the [mapnik documentation](http://mapnik.org/docs/v2.2.0/api/python/mapnik._mapnik-module.html#render_to_file) for possible values. -| dpi | Amount of detail to render in dots per inch. This value is unrelated to the setting on your printer, a higher value will simply result in smaller icons, thinner roads and unreadable text. -| scalefactor | The scale factor to use when rendering to image formats. -| fontdirs | Optional. Can contain one or more fontdir subtags with additional font directories to be used by mapnik. - diff --git a/hm-render-mapnik.py b/hm-render-mapnik.py deleted file mode 100755 index b59b9e8..0000000 --- a/hm-render-mapnik.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python - -# hikingmap -- render maps on paper using data from OpenStreetMap -# Copyright (C) 2019 Roel Derickx - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) 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 General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys, getopt, math, mapnik -from xml.dom import minidom - -# global constants -inch = 2.54 # cm - -class Parameters: - def __init__(self): - # default parameters - self.minlon = 0.0 - self.maxlon = 0.0 - self.minlat = 0.0 - self.maxlat = 0.0 - self.pagewidth = 20.0 - self.pageheight = 28.7 - self.dpi = 300 - self.basefilename = "" - self.temptrackfile = "" - self.tempwaypointfile = "" - self.verbose = False - self.mapstyle = "mapnik_style.xml" - self.hikingmapstyle = "hikingmap_style.xml" - self.output_format = "pdf" - self.scale_factor = 1.0 - self.gpxfiles = [ ] - - - def __usage(self): - print("Usage: " + sys.argv[0] + " [OPTION]... gpxfiles\n" - "Render map page using mapnik / postrgesql toolchain\n\n" - " -o Minimum longitude\n" - " -O Maximum longitude\n" - " -a Minimum latitude\n" - " -A Maximum latitude\n" - " -w Page width in cm\n" - " -h Page height in cm\n" - " -b Filename, base without extension\n" - " -t Temp track file to render\n" - " -y Temp waypoints file to render\n" - " -v, --verbose Verbose\n" - " --help Display help and exit\n") - - - # returns True if parameters could be parsed successfully - def parse_commandline(self): - try: - opts, args = getopt.getopt(sys.argv[1:], "o:O:a:A:w:h:d:b:t:y:v", [ - "verbose", - "help"]) - except getopt.GetoptError: - self.__usage() - return False - for opt, arg in opts: - if opt == "--help": - self.__usage() - return False - elif opt in ("-o"): - self.minlon = float(arg) - elif opt in ("-O"): - self.maxlon = float(arg) - elif opt in ("-a"): - self.minlat = float(arg) - elif opt in ("-A"): - self.maxlat = float(arg) - elif opt in ("-w"): - self.pagewidth = float(arg) - elif opt in ("-h"): - self.pageheight = float(arg) - elif opt in ("-b"): - self.basefilename = arg - elif opt in ("-t"): - self.temptrackfile = arg - elif opt in ("-y"): - self.tempwaypointfile = arg - elif opt in ("-v", "--verbose"): - self.verbose = True - - self.gpxfiles = args - - if self.verbose: - print("Parameters:") - print("minlon = " + str(self.minlon)) - print("maxlon = " + str(self.maxlon)) - print("minlat = " + str(self.minlat)) - print("maxlat = " + str(self.maxlat)) - print("pagewidth = " + str(self.pagewidth)) - print("pageheight = " + str(self.pageheight)) - print("dpi = " + str(self.dpi)) - print("basefilename = " + self.basefilename) - print("temptrackfile = " + self.temptrackfile) - print("tempwaypointfile = " + self.tempwaypointfile) - print("gpxfiles = " + ', '.join(self.gpxfiles)) - - return True - - - def __get_xml_subtag_value(self, xmlnode, sublabelname, defaultvalue): - elements = xmlnode.getElementsByTagName(sublabelname) - return str(elements[0].firstChild.nodeValue) \ - if elements and elements[0].childNodes \ - else defaultvalue - - - def parse_configfile(self): - xmldoc = minidom.parse("render_mapnik.config.xml") - xmlmapnik = xmldoc.getElementsByTagName('render_mapnik')[0] - - self.mapstyle = self.__get_xml_subtag_value(xmlmapnik, 'mapstyle', 'mapnik_style.xml') - self.hikingmapstyle = self.__get_xml_subtag_value(xmlmapnik, 'hikingmapstyle', \ - 'hikingmap_style.xml') - self.output_format = self.__get_xml_subtag_value(xmlmapnik, 'outputformat', 'pdf') - self.dpi = int(self.__get_xml_subtag_value(xmlmapnik, 'dpi', '300')) - self.scale_factor = float(self.__get_xml_subtag_value(xmlmapnik, 'scalefactor', '1.0')) - - xmlfontdirlist = xmlmapnik.getElementsByTagName('fontdirs') - - for xmlfontdir in xmlfontdirlist: - fontdir = self.__get_xml_subtag_value(xmlfontdir, 'fontdir', '') - if fontdir != '': - mapnik.FontEngine.register_fonts(fontdir, True) - - return True - - - -# MAIN - -if not hasattr(mapnik, 'mapnik_version') or mapnik.mapnik_version() < 600: - raise SystemExit('This script requires Mapnik >= 0.6.0)') - -parameters = Parameters() -if not parameters.parse_commandline(): - sys.exit() - -if not parameters.parse_configfile(): - sys.exit() - -if not parameters.verbose: - mapnik.logger.set_severity(getattr(mapnik.severity_type, 'None')) - -merc = mapnik.Projection('+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over') -longlat = mapnik.Projection('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs') - -imgwidth = math.trunc(parameters.pagewidth / inch * parameters.dpi) -imgheight = math.trunc(parameters.pageheight / inch * parameters.dpi) - -m = mapnik.Map(imgwidth, imgheight) -mapnik.load_map(m, parameters.mapstyle) -mapnik.load_map(m, parameters.hikingmapstyle) -m.srs = merc.params() - -if hasattr(mapnik, 'Box2d'): - bbox = mapnik.Box2d(parameters.minlon, parameters.minlat, parameters.maxlon, parameters.maxlat) -else: - bbox = mapnik.Envelope(parameters.minlon, parameters.minlat, parameters.maxlon, parameters.maxlat) - -transform = mapnik.ProjTransform(longlat, merc) -merc_bbox = transform.forward(bbox) -m.zoom_to_box(merc_bbox) - -for gpxfile in parameters.gpxfiles: - gpxlayer = mapnik.Layer('GPXLayer') - gpxlayer.datasource = mapnik.Ogr(file = gpxfile, layer = 'tracks') - gpxlayer.styles.append('GPXStyle') - m.layers.append(gpxlayer) - -if parameters.temptrackfile != "": - overviewlayer = mapnik.Layer('OverviewLayer') - overviewlayer.datasource = mapnik.Ogr(file = parameters.temptrackfile, layer = 'tracks') - overviewlayer.styles.append('GPXStyle') - m.layers.append(overviewlayer) -elif parameters.tempwaypointfile != "": - waypointlayer = mapnik.Layer('WaypointLayer') - waypointlayer.datasource = mapnik.Ogr(file = parameters.tempwaypointfile, layer = 'waypoints') - waypointlayer.styles.append('WaypointStyle') - m.layers.append(waypointlayer) - -mapnik.render_to_file(m, parameters.basefilename + "." + parameters.output_format, - parameters.output_format, - parameters.scale_factor) - diff --git a/render_mapnik.config.xml b/hm_render_mapnik.config.xml similarity index 87% rename from render_mapnik.config.xml rename to hm_render_mapnik.config.xml index d211940..147737e 100644 --- a/render_mapnik.config.xml +++ b/hm_render_mapnik.config.xml @@ -1,14 +1,14 @@ - + mapnik_style.xml hikingmap_style.xml png - 100 + 300 1.0 /usr/share/fonts/noto /usr/share/fonts/noto-cjk /usr/share/fonts/TTF - + diff --git a/hm_render_mapnik/__init__.py b/hm_render_mapnik/__init__.py new file mode 100644 index 0000000..810f143 --- /dev/null +++ b/hm_render_mapnik/__init__.py @@ -0,0 +1,2 @@ +name = "hm-render-mapnik" + diff --git a/hm_render_mapnik/hm_render_mapnik.py b/hm_render_mapnik/hm_render_mapnik.py new file mode 100755 index 0000000..6d33974 --- /dev/null +++ b/hm_render_mapnik/hm_render_mapnik.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 + +# hikingmap -- render maps on paper using data from OpenStreetMap +# Copyright (C) 2019 Roel Derickx + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) 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 General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse, math, mapnik +from xml.dom import minidom + +# global constants +earthCircumference = 40041.44 # km (average, equatorial 40075.017 km / meridional 40007.86 km) +cmToKmFactor = 100000.0 +inch = 2.54 # cm + +def parse_commandline(): + parser = argparse.ArgumentParser(description = "Render a map on paper using mapnik") + parser.add_argument('--pagewidth', dest = 'pagewidth', type = float, default = 20.0, \ + help = "page width in cm") + parser.add_argument('--pageheight', dest = 'pageheight', type = float, default = 28.7, \ + help = "page height in cm") + parser.add_argument('-b', '--basename', dest = 'basefilename', default = "detail", \ + help = "base filename without extension") + parser.add_argument('-t', dest = 'temptrackfile', \ + help = "temp track file to render") + parser.add_argument('-y', dest = 'tempwaypointfile', \ + help = "temp waypoints file to render") + parser.add_argument('-v', dest = 'verbose', action = 'store_true') + # hm-render-mapnik specific parameters + parser.add_argument('-d', '--dpi', type=int, default=300, \ + help = "amount of detail to render in dots per inch (default: %(default)s)") + parser.add_argument('-S', '--scale-factor', type=float, default=1.0, \ + help = "scale factor (default: %(default)s)") + parser.add_argument('-m', '--mapstyle', default='mapnik_style.xml', \ + help = "mapnik stylesheet file (default: %(default)s)") + parser.add_argument('--hikingmapstyle', default='hikingmap_style.xml', \ + help = "hikingmap stylesheet file, contains the CartoCSS for " + \ + "the tracks and the waypoints (default: %(default)s)") + parser.add_argument('-f', '--format', dest='output_format', default='png', \ + help = "output format, consult the mapnik documentation for " + \ + "possible values (default: %(default)s)") + # -- + parser.add_argument('gpxfiles', nargs = '*') + + subparsers = parser.add_subparsers(dest='mode', help='bounding box or center mode') + + # create the parser for the bbox command + parser_bbox = subparsers.add_parser('bbox', help='define bounding box') + parser_bbox.add_argument('-o', '--minlon', type=float, required = True, \ + help = "minimum longitude") + parser_bbox.add_argument('-O', '--maxlon', type=float, required = True, \ + help = "maximum longitude") + parser_bbox.add_argument('-a', '--minlat', type=float, required = True, \ + help = "minimum latitude") + parser_bbox.add_argument('-A', '--maxlat', type=float, required = True, \ + help = "maximum latitude") + + # create the parser for the atlas command + parser_atlas = subparsers.add_parser('center', help='define center mode') + parser_atlas.add_argument('--lon', type=float, required=True, \ + help='longitude of the center of map') + parser_atlas.add_argument('--lat', type=float, required=True, \ + help='latitude of the center of map') + parser_atlas.add_argument('--scale', type=int, default=50000, \ + help='scale denominator') + + return parser.parse_args() + + +def convert_cm_to_degrees_lon(lengthcm, scale, latitude): + lengthkm = lengthcm / cmToKmFactor * scale + return lengthkm / ((earthCircumference / 360.0) * math.cos(math.radians(latitude))) + + +def convert_cm_to_degrees_lat(lengthcm, scale): + lengthkm = lengthcm / cmToKmFactor * scale + return lengthkm / (earthCircumference / 360.0) + + +def assure_bbox_mode(parameters): + if parameters.mode == 'center': + pagesize_lon = convert_cm_to_degrees_lon(parameters.pagewidth, \ + parameters.scale, parameters.lat) + pagesize_lat = convert_cm_to_degrees_lat(parameters.pageheight, parameters.scale) + + parameters.minlon = parameters.lon - pagesize_lon / 2 + parameters.minlat = parameters.lat - pagesize_lat / 2 + parameters.maxlon = parameters.lon + pagesize_lon / 2 + parameters.maxlat = parameters.lat + pagesize_lat / 2 + +''' +def __get_xml_subtag_value(self, xmlnode, sublabelname, defaultvalue): + elements = xmlnode.getElementsByTagName(sublabelname) + return str(elements[0].firstChild.nodeValue) \ + if elements and elements[0].childNodes \ + else defaultvalue + + +def parse_configfile(self): + xmldoc = minidom.parse("render_mapnik.config.xml") + xmlmapnik = xmldoc.getElementsByTagName('render_mapnik')[0] + + self.mapstyle = self.__get_xml_subtag_value(xmlmapnik, 'mapstyle', 'mapnik_style.xml') + self.hikingmapstyle = self.__get_xml_subtag_value(xmlmapnik, 'hikingmapstyle', \ + 'hikingmap_style.xml') + self.output_format = self.__get_xml_subtag_value(xmlmapnik, 'outputformat', 'pdf') + self.dpi = int(self.__get_xml_subtag_value(xmlmapnik, 'dpi', '300')) + self.scale_factor = float(self.__get_xml_subtag_value(xmlmapnik, 'scalefactor', '1.0')) + + xmlfontdirlist = xmlmapnik.getElementsByTagName('fontdirs') + + for xmlfontdir in xmlfontdirlist: + fontdir = self.__get_xml_subtag_value(xmlfontdir, 'fontdir', '') + if fontdir != '': + mapnik.FontEngine.register_fonts(fontdir, True) + + return True +''' + +def render(parameters): + if not parameters.verbose: + mapnik.logger.set_severity(getattr(mapnik.severity_type, 'None')) + + merc = mapnik.Projection('+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over') + longlat = mapnik.Projection('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs') + + imgwidth = math.trunc(parameters.pagewidth / inch * parameters.dpi) + imgheight = math.trunc(parameters.pageheight / inch * parameters.dpi) + + m = mapnik.Map(imgwidth, imgheight) + mapnik.load_map(m, parameters.mapstyle) + mapnik.load_map(m, parameters.hikingmapstyle) + m.srs = merc.params() + + if hasattr(mapnik, 'Box2d'): + bbox = mapnik.Box2d(parameters.minlon, parameters.minlat, parameters.maxlon, parameters.maxlat) + else: + bbox = mapnik.Envelope(parameters.minlon, parameters.minlat, parameters.maxlon, parameters.maxlat) + + transform = mapnik.ProjTransform(longlat, merc) + merc_bbox = transform.forward(bbox) + m.zoom_to_box(merc_bbox) + + for gpxfile in parameters.gpxfiles: + gpxlayer = mapnik.Layer('GPXLayer') + gpxlayer.datasource = mapnik.Ogr(file = gpxfile, layer = 'tracks') + gpxlayer.styles.append('GPXStyle') + m.layers.append(gpxlayer) + + if parameters.temptrackfile: + overviewlayer = mapnik.Layer('OverviewLayer') + overviewlayer.datasource = mapnik.Ogr(file = parameters.temptrackfile, layer = 'tracks') + overviewlayer.styles.append('GPXStyle') + m.layers.append(overviewlayer) + + if parameters.tempwaypointfile: + waypointlayer = mapnik.Layer('WaypointLayer') + waypointlayer.datasource = mapnik.Ogr(file = parameters.tempwaypointfile, layer = 'waypoints') + waypointlayer.styles.append('WaypointStyle') + m.layers.append(waypointlayer) + + mapnik.render_to_file(m, parameters.basefilename + "." + parameters.output_format, + parameters.output_format, + parameters.scale_factor) + + +def main(): + parameters = parse_commandline() + assure_bbox_mode(parameters) + + #parse_configfile(parameters) + + render(parameters) + + +if __name__ == '__main__': + main() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8545c07 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +mapnik>=0.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fcf6c33 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +import setuptools + +with open('README.md', 'r', encoding='utf-8') as fh: + README = fh.read() + +with open('requirements.txt', 'r', encoding='utf-8') as f: + requirements = f.read().splitlines() + +setuptools.setup( + name="hm-render-mapnik", + version="0.0.1", + license='GNU General Public License (GNU GPL v3 or above)', + author="Roel Derickx", + author_email="roel.derickx AT gmail", + description="Render a map for a given area to paper using mapnik", + long_description=README, + long_description_content_type="text/markdown", + url="https://github.com/roelderickx/hm-render-mapnik", + packages=setuptools.find_packages(), + install_requires=requirements, + python_requires='>=3.5', + entry_points={ + 'console_scripts': ['hm-render-mapnik = hm_render_mapnik.hm_render_mapnik:main'] + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Operating System :: OS Independent", + ], +) +