[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