[nik4] 01/10: Imported Upstream version 0.0~20150516-3415338
Sebastiaan Couwenberg
sebastic at moszumanska.debian.org
Sun May 29 17:35:02 UTC 2016
This is an automated email from the git hooks/post-receive script.
sebastic pushed a commit to branch master
in repository nik4.
commit c3f7f6e993040869060c99ec168accd5c88784d8
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date: Fri Feb 19 00:09:03 2016 +0100
Imported Upstream version 0.0~20150516-3415338
---
.gitignore | 6 +
CHANGELOG.md | 45 +++++
LICENSE.txt | 14 ++
MANIFEST.in | 3 +
README.md | 206 +++++++++++++++++++++
img/demo-zoom-levels.png | Bin 0 -> 141534 bytes
img/paper-options.png | Bin 0 -> 39848 bytes
img/svg-factor.png | Bin 0 -> 388268 bytes
nik4.py | 469 +++++++++++++++++++++++++++++++++++++++++++++++
setup.py | 37 ++++
10 files changed, 780 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..15bffec
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+dist/
+build/
+README.txt
+markdown_to_rst
+img-src
+MANIFEST
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ebbadbb
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,45 @@
+# Nik4 Change History
+
+## *master*
+
+* `--version` option.
+* Added `--fonts` option for registering additional fonts. [#16](https://github.com/Zverik/Nik4/issues/16)
+* Fixed `--center` with `--scale` error. [#18](https://github.com/Zverik/Nik4/issues/18)
+* Swapped sizes 4A0 and 2A0. [#17](https://github.com/Zverik/Nik4/issues/17)
+
+## 1.5, 7.12.2014
+
+* Removed debug output for `--url`.
+* Substitute variables with default values when `--vars` is empty.
+* `--just-tiles` option for keeping tiles instead of merging them, also creates ozi/wld files if needed. [#15](https://github.com/Zverik/Nik4/issues/15)
+
+## 1.4, 4.06.2014
+
+* **Breaking change:** width and height specified in `--size` and `--size-px` are now swapped if they fit bbox better that way. Use `--norotate` to preserve old behaviour (that is, to force `WIDTH HEIGHT` order).
+* You can specify 0 for one of the dimensions: first one is considered "long" side, the second is "short". E.g. for "portrait" bbox size "0 123" could become "123 200". `--norotate` option also applies to this. [#10](https://github.com/Zverik/Nik4/issues/10)
+* Added `--dpi`, a synonim for `--ppi`.
+* Now allowing underscores in variable names.
+* Streaming output to `-` (stdout) now works. [#9](https://github.com/Zverik/Nik4/issues/9)
+
+## 1.3, 22.05.2014
+
+* Fixed value order in world files.
+* Added more paper formats. [#7](https://github.com/Zverik/Nik4/issues/7)
+* Style XML can now be streamed from stdin.
+* Style XML can now contain variables `${name:default}`, set them with `--vars name=value`. [#6](https://github.com/Zverik/Nik4/issues/6)
+
+## 1.2, 19.05.2014
+
+* Fixed georeferencing of tiled maps. [#4](https://github.com/Zverik/Nik4/issues/4)
+* World files are now written in EPSG:3857 projection.
+* Added `--url` option for leaflet / openlayers map URLs. [#5](https://github.com/Zverik/Nik4/issues/5)
+
+## 1.1, 18.05.2014
+
+* True scale (like 1:10000) was calculated incorrectly. [#3](https://github.com/Zverik/Nik4/issues/3)
+* Mapnik image size limit (16384×16384) is now enforced.
+* Fixed breaking of large tiled maps.
+
+## 1.0, 16.05.2014
+
+Initial release
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..ee7d6a5
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,14 @@
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam at hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..f49c570
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include *.md
+include *.txt
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..aa30136
--- /dev/null
+++ b/README.md
@@ -0,0 +1,206 @@
+# Nik4
+
+This is a mapnik-to-image exporting script. It requires only `mapnik-python` bindings.
+Install it with `pip install nik4` or `easy_install nik4` and run with `-h` option
+to see available options and their descriptions.
+
+## Why is it better
+
+Nik4 takes great care to preserve values you feed it. If you say you need a 800×600 image,
+it won't take a pixel less or more. It won't shrink a bounding box or distort lines when
+specifying so called "scale factor". When you need a 300 dpi image, you tell it `--ppi 300`
+and can be sure you will get what you intended.
+
+For example, this is a sample rendering of an area in Tallinn on zoom 17, by Nik4, Nik2img
+and as seen on the default layer on osm.org:
+
+![nik4 - osm.org - nik2img](img/demo-zoom-levels.png)
+
+Also it can use real-world units, that is, millimeters (and prefers to). Specify dimensions
+for printing, choose bounding box and ppi scale — and the result won't disappoint. Options
+are intuitive and plenty, and you will be amazed how much tasks became simpler with Nik4.
+
+## How to use it
+
+Again, run `nik4.py -h` to see the list of all available options. Here are some examples.
+
+### Watch a mapping party area
+
+First, if you haven't already, install PostgreSQL+PostGIS and Mapnik, and use osm2pgsql
+to populate the database with a planet extract. For instructions see
+[here](http://switch2osm.org/loading-osm-data/) or [here](http://wiki.openstreetmap.org/wiki/User:Zverik/Tile_Server_on_Fedora_20).
+Get bounds by visiting [osm.org](http://openstreetmap.org): click "Export" and "Choose another region". Then:
+
+ nik4.py -b -0.009 51.47 0.013 51.484 -z 17 openstreetmap-carto/osm.xml party-before.png
+
+Here `osm.xml` is the compiled Mapnik style.
+Then you can [update](http://wiki.openstreetmap.org/wiki/Minutely_Mapnik) you database and generate
+snapshots of an area as it is being mapped. Alternatively, you can specify an area with its center
+and desired image size in pixels:
+
+ nik4.py -c 0 51.477 --size-px 800 600 -z 17 openstreetmap-carto/osm.xml party-before.png
+
+Even simpler, instead of `--center` and `--zoom` options, just grab an URL of a place:
+
+ nik4.py --url http://www.openstreetmap.org/#map=16/55.9865/37.2160 osm.xml screenshot.png
+
+### Make a georeferenced raster image
+
+Some people prefer planning routes with OziExplorer or similar programs. Or want to take a big
+raster map with them on the road. For that a very big image is needed. Usually they turn to
+downloading and stitching hundreds of tiles, but with Nik4 you can make Mapnik produce a better
+looking map, faster and without bothering tile server administrators.
+
+Since you are not bound to any tile provider, you should employ [TileMill](https://www.mapbox.com/tilemill/)
+for customizing your map style: for example, remove forest on low zooms, add contrast to
+road lines, render more villages, highlight useful POI and cycling routes.
+
+ nik4.py -b 25 61.6 30.6 63.3 -z 13 custom.xml kuopio.png --ozi kuopio.map
+
+This will render 16311×10709 image with a georeferencing file ready to open in OziExplorer.
+For a `.wld` file, which can be used in desktop GIS applications or for creating a GeoTIFF file,
+use `--wld` option. You can convert png+wld to geotiff with GDAL:
+
+ gdal_translate -of GTiff -a_srs epsg:4326 image.png image.tif
+
+### Make a BIG raster image
+
+You would likely encounter out of memory error while trying to generate 16311×10709 image from the last
+chapter. Despair not:
+
+ nik4.py -b 25 61.6 30.6 63.3 -z 13 custom.xml kuopio.png --ozi kuopio.map --tiles 4
+
+Voilà — now Mapnik has to generate 16 images of a manageable size 4078×2678. After that Nik4 will call
+`montage` from the Imagemagick package to stitch all tiles together.
+
+What if `montage` cannot fit images into memory? There is a way, but you would need quite a lot of disk
+space, several gigabytes:
+
+ for i in *_kuopio.png; do convert $i `basename $i .png`.mpc; done
+ montage -geometry +0+0 -tile 4x4 *_kuopio.mpc kuopio.png
+ rm *_kuopio.{png,mpc,cache}
+
+These lines will convert all images to Imagemagick's internal MPC format, from which `montage` reads directly.
+You would need more space for a similar MPC cache of the output file. Note that most software will have
+trouble opening an image surpassing 200 megapixels.
+
+### Get an image for printing
+
+![A4 options](img/paper-options.png)
+
+Let's say you need a 1:5000 image of a city center for printing on a A4 sheet with margins.
+
+ nik4.py -s 5000 --ppi 300 -a 4 -c 24.1094 56.9488 --margin 10 ~/osm/krym/carto/osm.xml 4print.png
+
+What you get is a raster image, which when printed on an A4 with 300 dpi resolution, would have 10 mm margins
+and scale of exactly 50 m in a cm. See the picture above for explanation of margins and other options.
+Formats can be `a0-a9`, `letter`, `card` and so on. The paper orientation depends on a bbox;
+to force landscape or portrait orientation prepend the format with `+` or `-` characters.
+Or don't bother and enter numbers by hand: `-d 150 100` will export a 15×10 postcard map.
+
+### Wait, what's that again, about dimensions?
+
+Dimensions you specify in `--size` (`-d`) and `--size-px` (`-x`) arguments are not exactly width and height
+in that order: they will be swapped if a bounding box would fit better. For example, when you export
+"landscape" bbox and specify `-d 200 400`, the image would be 40 cm wide and 20 cm tall. To prevent this
+behaviour, use `--norotate` option: with it, that image would be 20 cm wide, with the bounding box
+expanded vertically.
+
+When you don't want your bounding box altered, use `0` for one of dimension values. The first one in that
+case is considered a long side length, the second is for shorter side. With `--norotate` option, they
+are width and height respectively. For example, `-x 1024 0 --norotate` would make the resulting image
+1024 pixels wide regardless of bounding box proportions.
+
+### Print a route
+
+On the image above there is a route. Nik4 cannot parse GPX files or draw anything on top of exported
+images, but it can manage layers in Mapnik style file. And Mapnik (via OGR plugin) can draw
+[a lot of things](http://www.gdal.org/ogr/ogr_formats.html), including GPX, GeoJSON, CSV, KML.
+Just add your route to the style like this:
+
+```xml
+<Style name="route" filter-mode="first">
+ <Rule>
+ <LineSymbolizer stroke-width="5" stroke="#012d64" stroke-linejoin="round" stroke-linecap="round" />
+ </Rule>
+</Style>
+<Layer name="route" status="off" srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
+ <StyleName>route</StyleName>
+ <Datasource>
+ <Parameter name="type">ogr</Parameter>
+ <Parameter name="file">/home/user/route.gpx</Parameter>
+ <Parameter name="layer">tracks</Parameter>
+ </Datasource>
+ </Layer>
+```
+
+Note that you can add it in any place: for example, between road and label layers, so the route does not
+obscure any text. Also note `status="off"`: this layer won't be drawn by default. So if you want
+to export a clean map for the extent of your route (or any other) layer, use those options:
+
+ nik4.py --fit route --size-px 400 700 osm.xml route_area.png
+
+To enable drawing of the layer, use `--add-layers` option:
+
+ nik4.py --fit route --add-layers route,stops --ppi 150 -a 6 osm.xml route.png
+
+You can list many layers, separating them with commas. And you can hide some layers:
+`--hide-layers contours,shields`. Obviously you can fit several layers at once, as well
+as specify a bounding box to include on a map. All layer names are case-sensitive, so if
+something does not appear, check your style file for exact layer names.
+
+### Print a different route each time
+
+Nik4 supports variables in XML styles: `${name:default}` defines a variable with the given name
+and its default value (which can be omitted, along with `:`). To substitute variable
+definitions with values or defaults, use `--vars` parameter. For example, let's make
+stroke width in the last example configurable, and request GPX file name:
+
+```xml
+ <LineSymbolizer stroke-width="${width:5}" stroke="#012d64" stroke-linejoin="round" stroke-linecap="round" />
+ ...
+ <Parameter name="file">${route}</Parameter>
+```
+
+Now to make an image of a route, use this command:
+
+ nik4.py --fit route --ppi 150 -a 6 --vars width=8 route=~/routes/day2.gpx osm.xml route.png
+
+Note that path would likely to be resolved relative to the XML file location. If you omit `route` variable
+in this example, you'll get an error message.
+
+### Generate a vector drawing from a map
+
+It's as easy as adding an `.svg` extension to the output file name.
+
+ nik4.py --fit route -a -5 --factor 4 osm.xml map.svg
+
+Why did I use `--factor` (it's the same as using `--ppi 362.8`, which is 90.7 * 4)? Shouldn't
+vector images be independent of the resolution? Well, the problem is in label kerning:
+
+![SVG labels quality](img/svg-factor.png)
+
+Left image was exported with `--factor 1`. You can see in "ali", "sis", "Uus" that distance between
+letters is varying unpredictably, not like the font instructs. That's because Mapnik rounds letter widths
+to nearest integers, that is, to pixels. By increasing the resolution, you make that granularity finer,
+so rounding errors are much less prominent. Labels would become slightly longer, that's why they are
+different in the second image.
+
+You can export a map to PDF and be done with it, but often you'd want to do some postprocessing:
+move labels away from roads, highlight features, draw additional labels and arrows. For that
+I recommend processing the SVG file with [mapnik-group-text](https://github.com/Zverik/mapnik-group-text),
+which would allow for easier label movement.
+
+## See also
+
+* [mapnik/demo/python](https://github.com/mapnik/mapnik/tree/master/demo/python)
+* [generate\_image.py](http://svn.openstreetmap.org/applications/rendering/mapnik/generate_image.py)
+* [mapnik-render-image](https://github.com/plepe/mapnik-render-image)
+* [osm.org/export](https://trac.openstreetmap.org/browser/sites/tile.openstreetmap.org/cgi-bin/export)
+* [nik2img](http://code.google.com/p/mapnik-utils/wiki/Nik2Img)
+
+For generating tiles, see [polytiles.py](https://github.com/Zverik/polytiles).
+
+## Author and license
+
+The script was written by Ilya Zverev and published under WTFPL.
diff --git a/img/demo-zoom-levels.png b/img/demo-zoom-levels.png
new file mode 100644
index 0000000..ef2528e
Binary files /dev/null and b/img/demo-zoom-levels.png differ
diff --git a/img/paper-options.png b/img/paper-options.png
new file mode 100644
index 0000000..001fde4
Binary files /dev/null and b/img/paper-options.png differ
diff --git a/img/svg-factor.png b/img/svg-factor.png
new file mode 100644
index 0000000..bd122c4
Binary files /dev/null and b/img/svg-factor.png differ
diff --git a/nik4.py b/nik4.py
new file mode 100755
index 0000000..4b8326b
--- /dev/null
+++ b/nik4.py
@@ -0,0 +1,469 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Nik4: Export image from mapnik
+# Run it with -h to see the list of options
+# Written by Ilya Zverev, licensed WTFPL
+
+import mapnik
+import sys, os, re, argparse, math, tempfile
+
+try:
+ import cairo
+ HAS_CAIRO = True
+except ImportError:
+ HAS_CAIRO = False
+
+VERSION = '1.5.2'
+TILE_BUFFER = 128
+IM_MONTAGE = 'montage'
+
+p3857 = 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')
+p4326 = mapnik.Projection('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+transform = mapnik.ProjTransform(p4326, p3857)
+
+def layer_bbox(m, names, bbox=None):
+ """Calculate extent of given layers and bbox"""
+ for layer in (l for l in m.layers if l.name in names):
+ # it may as well be a GPX layer in WGS84
+ p = mapnik.Projection(layer.srs)
+ lbbox = layer.envelope().inverse(p).forward(p3857)
+ if bbox:
+ bbox.expand_to_include(lbbox)
+ else:
+ bbox = lbbox
+ return bbox
+
+def filter_layers(m, lst):
+ """Leave only layers in list active, disable others"""
+ for l in m.layers:
+ l.active = l.name in lst
+
+def select_layers(m, enable, disable):
+ """Enable and disable layers in corresponding lists"""
+ for l in m.layers:
+ if l.name in enable:
+ l.active = True
+ if l.name in disable:
+ l.active = False
+
+def prepare_ozi(mbbox, mwidth, mheight, name):
+ """Create georeferencing file for OziExplorer"""
+ def deg(value, is_lon):
+ degrees = math.floor(abs(value))
+ minutes = (abs(value) - degrees) * 60
+ return '{:4d},{:3.5F},{}'.format(int(round(degrees)), minutes, ('W' if is_lon else 'S') if value < 0 else ('E' if is_lon else 'N'))
+ bbox = transform.backward(mbbox)
+ points = "\n".join(['Point{:02d},xy, , ,in, deg, , ,N, , ,E, grid, , , ,N'.format(n) for n in range(3,31)])
+ return '''OziExplorer Map Data File Version 2.2
+Nik4
+{}
+1 ,Map Code,
+WGS 84,WGS 84, 0.0000, 0.0000,WGS 84
+Reserved 1
+Reserved 2
+Magnetic Variation,,,E
+Map Projection,Mercator,PolyCal,No,AutoCalOnly,No,BSBUseWPX,No
+Point01,xy, 0, 0,in, deg,{},{}, grid, , , ,N
+Point02,xy, {:4d}, {:4d},in, deg,{},{}, grid, , , ,N
+{}
+Projection Setup,,,,,,,,,,
+Map Feature = MF ; Map Comment = MC These follow if they exist
+Track File = TF These follow if they exist
+Moving Map Parameters = MM? These follow if they exist
+MM0,Yes
+MMPNUM,4
+MMPXY,1,0,0
+'''.format(name, deg(bbox.maxy, False), deg(bbox.minx, True), mwidth - 1, mheight - 1, deg(bbox.miny, False), deg(bbox.maxx, True), points) \
+ + "MMPXY,2,{},0\n".format(mwidth) \
+ + "MMPXY,3,{},{}\n".format(mwidth, mheight) \
+ + "MMPXY,4,0,{}\n".format(mheight) \
+ + 'MMPLL,1,{:4.6f},{:4.6f}\n'.format(bbox.minx, bbox.maxy) \
+ + 'MMPLL,2,{:4.6f},{:4.6f}\n'.format(bbox.maxx, bbox.maxy) \
+ + 'MMPLL,3,{:4.6f},{:4.6f}\n'.format(bbox.maxx, bbox.miny) \
+ + 'MMPLL,4,{:4.6f},{:4.6f}\n'.format(bbox.minx, bbox.miny) \
+ + "MM1B,{}\n".format((mbbox.maxx - mbbox.minx) / mwidth * math.cos(math.radians(bbox.center().y))) \
+ + "MOP,Map Open Position,0,0\n" \
+ + "IWH,Map Image Width/Height,{},{}\n".format(mwidth, mheight)
+
+def prepare_wld(bbox, mwidth, mheight):
+ """Create georeferencing world file"""
+ pixel_x_size = (bbox.maxx - bbox.minx) / mwidth
+ pixel_y_size = (bbox.maxy - bbox.miny) / mheight
+ left_pixel_center_x = bbox.minx + pixel_x_size * 0.5
+ top_pixel_center_y = bbox.maxy - pixel_y_size * 0.5
+ return ''.join(["{:.8f}\n".format(n) for n in [pixel_x_size, 0.0, 0.0, -pixel_y_size, left_pixel_center_x, top_pixel_center_y]])
+
+def parse_url(url, options):
+ """Parse map URL into options map"""
+ lat = None
+ lon = None
+ zoom = None
+ m = re.search(r'[#/=]([0-9]{1,2})/(-?[0-9]{1,2}\.[0-9]+)/(-?[0-9]{1,3}\.[0-9]+)', url)
+ if m:
+ zoom = int(m.group(1))
+ lat = float(m.group(2))
+ lon = float(m.group(3))
+ else:
+ m = re.search(r'lat=(-[0-9]{1,2}\.[0-9]+)', url, flags=re.IGNORECASE)
+ if m:
+ lat = float(m.group(1))
+ m = re.search(r'lon=(-[0-9]{1,3}\.[0-9]+)', url, flags=re.IGNORECASE)
+ if m:
+ lon = float(m.group(1))
+ m = re.search(r'zoom=([0-9]{1,2})', url, flags=re.IGNORECASE)
+ if m:
+ zoom = int(m.group(1))
+ if zoom and not options.zoom:
+ options.zoom = zoom
+ if lat and lon and not options.center:
+ options.center = [lon, lat]
+ if not options.size and not options.size_px and not options.paper and not options.fit and not options.bbox:
+ options.size_px = [1280, 1024]
+
+def get_paper_size(name):
+ """Returns paper size for name, [long, short] sides in mm"""
+ # ISO A*
+ m = re.match(r'^a?(\d)$', name)
+ if m:
+ return [math.floor(1000 / 2**((2*int(m.group(1)) - 1) / 4.0) + 0.2), math.floor(1000 / 2**((2*(int(m.group(1)) + 1) - 1) / 4.0) + 0.2)]
+ # ISO B*
+ m = re.match(r'^b(\d)$', name)
+ if m:
+ return [math.floor(1000 / 2**((int(m.group(1)) - 1) / 2.0) + 0.2), math.floor(1000 / 2**(int(m.group(1)) / 2.0) + 0.2)]
+ # German extensions
+ if name == '4a0':
+ return [2378, 1682]
+ if name == '2a0':
+ return [1682, 1189]
+ # US Legal
+ if re.match(r'^leg', name):
+ return [355.6, 215.9]
+ # US Letter
+ if re.match(r'^l', name):
+ return [279.4, 215.9]
+ # Cards
+ if re.match(r'^c(?:re|ar)d', name):
+ return [85.6, 54]
+ return None
+
+def xml_vars(style, variables):
+ """Replace ${name:default} from style with variables[name] or 'default'"""
+ # Convert variables to a dict
+ v = {}
+ for kv in variables:
+ keyvalue = kv.split('=', 1)
+ if len(keyvalue) > 1:
+ v[keyvalue[0]] = keyvalue[1].replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')
+ # Scan all variables in style
+ r = re.compile(r'\$\{([a-z0-9_]+)(?::([^}]*))?\}')
+ rstyle = ''
+ last = 0
+ for m in r.finditer(style):
+ if m.group(1) in v:
+ value = v[m.group(1)]
+ elif m.group(2) is not None:
+ value = m.group(2)
+ else:
+ raise Exception('Found required style parameter: ' + m.group(1))
+ rstyle = rstyle + style[last:m.start()] + value
+ last = m.end()
+ if last < len(style):
+ rstyle = rstyle + style[last:]
+ return rstyle
+
+def add_fonts(path):
+ if os.path.exists(path):
+ mapnik.register_fonts(path)
+ else:
+ raise Exception('The directory "{p}" does not exists'.format(p=path))
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='Nik4 {}: Tile-aware mapnik image renderer'.format(VERSION))
+ parser.add_argument('--version', action='version', version='Nik4 {}'.format(VERSION))
+ parser.add_argument('-z', '--zoom', type=float, help='Target zoom level')
+ parser.add_argument('-p', '--ppi', '--dpi', type=float, help='Pixels per inch (alternative to scale)')
+ parser.add_argument('--factor', type=float, help='Scale factor (affects ppi, default=1)', default=1)
+ parser.add_argument('-s', '--scale', type=float, help='Scale as in 1:100000 (specifying ppi is recommended)')
+ parser.add_argument('-b', '--bbox', nargs=4, type=float, metavar=('Xmin', 'Ymin', 'Xmax', 'Ymax'), help='Bounding box')
+ parser.add_argument('-a', '--paper', help='Paper format: -a +4 for landscape A4, -a -4 for portrait A4, -a letter for autorotated US Letter')
+ parser.add_argument('-d', '--size', nargs=2, metavar=('W', 'H'), type=int, help='Target dimensions in mm (one 0 allowed)')
+ parser.add_argument('-x', '--size-px', nargs=2, metavar=('W', 'H'), type=int, help='Target dimensions in pixels (one 0 allowed)')
+ parser.add_argument('--norotate', action='store_true', default=False, help='Do not swap width and height for bbox')
+ parser.add_argument('-m', '--margin', type=int, help='Amount in mm to reduce paper size')
+ parser.add_argument('-c', '--center', nargs=2, metavar=('X', 'Y'), type=float, help='Center of an image')
+
+ parser.add_argument('--fit', help='Fit layers in the map, comma-separated')
+ parser.add_argument('--padding', type=int, help='Margin for layers in --fit (default=5), mm', default=5)
+ parser.add_argument('--layers', help='Map layers to render, comma-separated')
+ parser.add_argument('--add-layers', help='Map layers to include, comma-separated')
+ parser.add_argument('--hide-layers', help='Map layers to hide, comma-separated')
+
+ parser.add_argument('--url', help='URL of a map to center on')
+ parser.add_argument('--ozi', type=argparse.FileType('w'), help='Generate ozi map file')
+ parser.add_argument('--wld', type=argparse.FileType('w'), help='Generate world file')
+ parser.add_argument('-t', '--tiles', type=int, choices=range(1, 13), default=1, help='Write N×N tiles, then join using imagemagick')
+ parser.add_argument('--just-tiles', action='store_true', default=False, help='Do not join tiles, instead write ozi/wld file for each')
+ parser.add_argument('-v', '--debug', action='store_true', default=False, help='Display calculated values')
+ parser.add_argument('-f', '--format', dest='fmt', help='Target file format (by default looks at extension)')
+ parser.add_argument('--base', help='Base path for style file, in case it\'s piped to stdin')
+ parser.add_argument('--vars', nargs='*', help='List of variables (name=value) to substitute in style file (use ${name:default})')
+ parser.add_argument('--fonts', nargs='*', help='List of full path to directories containing fonts')
+ parser.add_argument('style', help='Style file for mapnik')
+ parser.add_argument('output', help='Resulting image file')
+ options = parser.parse_args()
+
+ dim_mm = None
+ scale = None
+ size = None
+ bbox = None
+ rotate = not options.norotate
+
+ if options.url:
+ parse_url(options.url, options)
+
+ # format should not be empty
+ if options.fmt:
+ fmt = options.fmt.lower()
+ elif '.' in options.output:
+ fmt = options.output.split('.')[-1].lower()
+ else:
+ fmt = 'png256'
+
+ need_cairo = fmt in ['svg', 'pdf']
+
+ # get image size in millimeters
+ if options.paper:
+ portrait = False
+ if options.paper[0] == '-':
+ portrait = True
+ rotate = False
+ options.paper = options.paper[1:]
+ elif options.paper[0] == '+':
+ rotate = False
+ options.paper = options.paper[1:]
+ else:
+ rotate = True
+ dim_mm = get_paper_size(options.paper.lower())
+ if not dim_mm:
+ raise Exception('Incorrect paper format: ' + options.paper)
+ if portrait:
+ dim_mm = [dim_mm[1], dim_mm[0]]
+ elif options.size:
+ dim_mm = options.size
+ if dim_mm and options.margin:
+ dim_mm[0] = max(0, dim_mm[0] - options.margin * 2)
+ dim_mm[1] = max(0, dim_mm[1] - options.margin * 2)
+
+ # ppi and scale factor are the same thing
+ if options.ppi:
+ ppmm = options.ppi / 25.4
+ scale_factor = options.ppi / 90.7
+ else:
+ scale_factor = options.factor
+ ppmm = 90.7 / 25.4 * scale_factor
+
+ # svg / pdf can be scaled only in cairo mode
+ if scale_factor != 1 and need_cairo and not HAS_CAIRO:
+ sys.stderr.write('Warning: install pycairo for using --factor or --ppi')
+ scale_factor = 1
+ ppmm = 90.7 / 25.4
+
+ # convert physical size to pixels
+ if options.size_px:
+ size = options.size_px
+ elif dim_mm:
+ size = [int(round(dim_mm[0] * ppmm)), int(round(dim_mm[1] * ppmm))]
+
+ if size and size[0] + size[1] <= 0:
+ raise Exception('Both dimensions are less or equal to zero')
+
+ # scale can be specified with zoom or with 1:NNN scale
+ fix_scale = False
+ if options.zoom:
+ scale = 2 * 3.14159 * 6378137 / 2 ** (options.zoom + 8) / scale_factor
+ elif options.scale:
+ scale = options.scale * 0.00028 / scale_factor
+ # Now we have to divide by cos(lat), but we might not know latitude at this point
+ if options.center:
+ scale = scale / math.cos(math.radians(options.center[1]))
+ elif options.bbox:
+ scale = scale / math.cos(math.radians((options.bbox[3] + options.bbox[1]) / 2))
+ else:
+ fix_scale = True
+
+ if options.bbox:
+ bbox = options.bbox
+ # all calculations are in EPSG:3857 projection (it's easier)
+ if bbox:
+ bbox = transform.forward(mapnik.Box2d(*bbox))
+
+ # calculate bbox through center, zoom and target size
+ if not bbox and options.center and size and size[0] > 0 and size[1] > 0 and scale:
+ center = transform.forward(mapnik.Coord(*options.center))
+ w = size[0] * scale / 2
+ h = size[1] * scale / 2
+ bbox = mapnik.Box2d(center.x-w, center.y-h, center.x+w, center.y+h)
+
+ # reading style xml into memory for preprocessing
+ if options.style == '-':
+ style_xml = sys.stdin.read()
+ style_path = ''
+ else:
+ with open(options.style, 'r') as style_file:
+ style_xml = style_file.read()
+ style_path = os.path.dirname(options.style)
+ if options.base:
+ style_path = options.base
+ if 'vars' in options and options.vars is not None:
+ style_xml = xml_vars(style_xml, options.vars)
+
+ # for layer processing we need to create the Map object
+ m = mapnik.Map(100, 100) # temporary size, will be changed before output
+ mapnik.load_map_from_string(m, style_xml, False, style_path)
+ m.srs = p3857.params()
+
+ # register non-standard fonts
+ if options.fonts:
+ for f in options.fonts:
+ add_fonts(f)
+
+ # get bbox from layer extents
+ if options.fit:
+ bbox = layer_bbox(m, options.fit.split(','), bbox)
+ # here's where we can fix scale, no new bboxes below
+ if bbox and fix_scale:
+ scale = scale / math.cos(math.radians(transform.backward(bbox.center()).y))
+ # expand bbox with padding in mm
+ if bbox and options.padding and (scale or size):
+ if scale:
+ tscale = scale
+ else:
+ tscale = min((bbox.maxx - bbox.minx) / max(size[0], 0.01), (bbox.maxy - bbox.miny) / max(size[1], 0.01))
+ bbox.pad(options.padding * ppmm * tscale)
+
+ # bbox should be specified by this point
+ if not bbox:
+ raise Exception('Bounding box was not specified in any way')
+
+ # rotate image to fit bbox better
+ if rotate and size:
+ portrait = bbox.maxy - bbox.miny > bbox.maxx - bbox.minx
+ # take into consideration zero values, which mean they should be calculated from bbox
+ if (size[0] == 0 or size[0] > size[1]) and portrait:
+ size = [size[1], size[0]]
+
+ # calculate pixel size from bbox and scale
+ if not size:
+ if scale:
+ size = [int(round(abs(bbox.maxx - bbox.minx) / scale)), int(round(abs(bbox.maxy - bbox.miny) / scale))]
+ else:
+ raise Exception('Image dimensions or scale were not specified in any way')
+ elif size[0] == 0:
+ size[0] = int(round(size[1] * (bbox.maxx - bbox.minx) / (bbox.maxy - bbox.miny)))
+ elif size[1] == 0:
+ size[1] = int(round(size[0] / (bbox.maxx - bbox.minx) * (bbox.maxy - bbox.miny)))
+
+ if options.output == '-' or (need_cairo and options.tiles > 1):
+ options.tiles = 1
+ if max(size[0], size[1]) / options.tiles > 16384:
+ raise Exception('Image size exceeds mapnik limit ({} > {}), use --tiles'.format(max(size[0], size[1]) / options.tiles, 16384))
+
+ # add / remove some layers
+ if options.layers:
+ filter_layers(m, options.layers.split(','))
+ if options.add_layers or options.hide_layers:
+ select_layers(m, options.add_layers.split(',') if options.add_layers else [], options.hide_layers.split(',') if options.hide_layers else [])
+
+ if options.debug:
+ print 'scale={}'.format(scale)
+ print 'scale_factor={}'.format(scale_factor)
+ print 'size={},{}'.format(size[0], size[1])
+ print 'bbox={}'.format(bbox)
+ print 'bbox_wgs84={}'.format(transform.backward(bbox) if bbox else None)
+ print 'layers=' + ','.join([l.name for l in m.layers if l.active])
+
+ # generate metadata
+ if options.ozi:
+ options.ozi.write(prepare_ozi(bbox, size[0], size[1], options.output))
+ if options.wld:
+ options.wld.write(prepare_wld(bbox, size[0], size[1]))
+
+ # export image
+ m.aspect_fix_mode = mapnik.aspect_fix_mode.GROW_BBOX;
+ m.resize(size[0], size[1])
+ m.zoom_to_box(bbox)
+
+ outfile = options.output
+ if options.output == '-':
+ outfile = tempfile.TemporaryFile(mode='w+b')
+
+ if need_cairo:
+ if HAS_CAIRO:
+ surface = cairo.SVGSurface(outfile, size[0], size[1]) if fmt == 'svg' else cairo.PDFSurface(outfile, size[0], size[1])
+ mapnik.render(m, surface, scale_factor, 0, 0)
+ surface.finish()
+ else:
+ mapnik.render_to_file(m, outfile, fmt)
+ else:
+ if options.tiles == 1:
+ im = mapnik.Image(size[0], size[1])
+ mapnik.render(m, im, scale_factor)
+ im.save(outfile, fmt)
+ else:
+ # we cannot make mapnik calculate scale for us, so fixing aspect ratio outselves
+ rdiff = (bbox.maxx-bbox.minx) / (bbox.maxy-bbox.miny) - size[0] / size[1]
+ if rdiff > 0:
+ bbox.height((bbox.maxx - bbox.minx) * size[1] / size[0])
+ elif rdiff < 0:
+ bbox.width((bbox.maxy - bbox.miny) * size[0] / size[1])
+ scale = (bbox.maxx - bbox.minx) / size[0]
+ width = max(32, int(math.ceil(1.0 * size[0] / options.tiles)))
+ height = max(32, int(math.ceil(1.0 * size[1] / options.tiles)))
+ m.resize(width, height)
+ m.buffer_size = TILE_BUFFER
+ tile_cnt = [int(math.ceil(1.0 * size[0] / width)), int(math.ceil(1.0 * size[1] / height))]
+ if options.debug:
+ print 'tile_count={},{}'.format(tile_cnt[0], tile_cnt[1])
+ print 'tile_size={},{}'.format(width, height)
+ tmp_tile = '{:02d}_{:02d}_{}'
+ tile_files = []
+ for row in range(0, tile_cnt[1]):
+ for column in range(0, tile_cnt[0]):
+ if options.debug:
+ print 'tile={},{}'.format(row, column)
+ tile_bbox = mapnik.Box2d(bbox.minx + 1.0 * width * scale * column, bbox.maxy - 1.0 * height * scale * row, bbox.minx + 1.0 * width * scale * (column + 1), bbox.maxy - 1.0 * height * scale * (row + 1))
+ tile_size = [width if column < tile_cnt[0] - 1 else size[0] - width * (tile_cnt[0] - 1), height if row < tile_cnt[1] - 1 else size[1] - height * (tile_cnt[1] - 1)]
+ m.zoom_to_box(tile_bbox)
+ im = mapnik.Image(tile_size[0], tile_size[1])
+ mapnik.render(m, im, scale_factor)
+ tile_name = tmp_tile.format(row, column, options.output)
+ im.save(tile_name, fmt)
+ if options.just_tiles:
+ # write ozi/wld for a tile if needed
+ tile_basename = tile_name + '.' if not '.' in tile_name else tile_name[0:tile_name.rindex('.')+1]
+ if options.ozi:
+ with open(tile_basename + 'ozi', 'w') as f:
+ f.write(prepare_ozi(tile_bbox, tile_size[0], tile_size[1], tile_basename + '.ozi'))
+ if options.wld:
+ with open(tile_basename + 'wld', 'w') as f:
+ f.write(prepare_wld(tile_bbox, tile_size[0], tile_size[1]))
+ else:
+ tile_files.append(tile_name)
+ if not options.just_tiles:
+ # join tiles and remove them if joining succeeded
+ import subprocess
+ result = subprocess.call([IM_MONTAGE, '-geometry', '+0+0', '-tile', '{}x{}'.format(tile_cnt[0], tile_cnt[1])] + tile_files + [options.output])
+ if result == 0:
+ for tile in tile_files:
+ os.remove(tile)
+
+ if options.output == '-':
+ if sys.platform == "win32":
+ # fix binary output on windows
+ import msvcrt
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+
+ outfile.seek(0)
+ print outfile.read()
+ outfile.close()
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..55359cf
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,37 @@
+from distutils.core import setup
+
+setup(
+ name='Nik4',
+ version='1.5.2',
+ license='WTFPL',
+ description='Command-line interface to a Mapnik rendering toolkit',
+ long_description="""
+Nik4
+====
+
+This is a mapnik-to-image exporting script. It requires only ``mapnik-python`` bindings.
+Install it with ``pip install nik4`` or ``easy_install nik4`` and run with ``-h`` option
+to see available options and their descriptions.
+
+.. _See documentation here: https://github.com/Zverik/Nik4/blob/master/README.md
+""",
+ url='https://github.com/Zverik/Nik4',
+ author='Ilya Zverev',
+ author_email='zverik at textual.ru',
+ platforms=['any'],
+ requires=['Mapnik'],
+ keywords='Mapnik,GIS,OpenStreetMap,mapping,export',
+ scripts=['nik4.py'],
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Console',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: End Users/Desktop',
+ 'Intended Audience :: Science/Research',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Scientific/Engineering :: GIS',
+ 'Topic :: Printing',
+ 'Topic :: Utilities'
+ ]
+)
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/nik4.git
More information about the Pkg-grass-devel
mailing list