[Git][debian-gis-team/nik4][master] 8 commits: Update branch in gbp.conf & Vcs-Git URL.

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Sun Aug 15 11:36:49 BST 2021



Bas Couwenberg pushed to branch master at Debian GIS Project / nik4


Commits:
43c4f71b by Bas Couwenberg at 2021-03-28T07:12:16+02:00
Update branch in gbp.conf & Vcs-Git URL.

- - - - -
825ad6a7 by Bas Couwenberg at 2021-03-28T07:16:09+02:00
New upstream version 1.7
- - - - -
8e9dc01a by Bas Couwenberg at 2021-03-28T07:16:11+02:00
Update upstream source from tag 'upstream/1.7'

Update to upstream version '1.7'
with Debian dir 0b40716d65565ca809228cc2f05285aa80408f25
- - - - -
17c778b3 by Bas Couwenberg at 2021-03-28T07:17:58+02:00
New upstream release.

- - - - -
2e6c6330 by Bas Couwenberg at 2021-03-28T07:19:50+02:00
Refresh patches.

- - - - -
39453189 by Bas Couwenberg at 2021-03-28T07:20:06+02:00
Set distribution to experimental.

- - - - -
1009c263 by Bas Couwenberg at 2021-08-15T12:30:50+02:00
Revert "Update branch in gbp.conf & Vcs-Git URL."

This reverts commit 43c4f71bdc72aff3e3d38ce46c64cb8324bf0abe.

- - - - -
331dda6d by Bas Couwenberg at 2021-08-15T12:30:57+02:00
Move from experimental to unstable.

- - - - -


7 changed files:

- .gitignore
- CHANGELOG.md
- README.md
- debian/changelog
- debian/patches/nik4.patch
- nik4.py
- setup.py


Changes:

=====================================
.gitignore
=====================================
@@ -4,3 +4,8 @@ README.txt
 markdown_to_rst
 img-src
 MANIFEST
+*.pyc
+*.png
+*.wld
+*.svg
+*.xml


=====================================
CHANGELOG.md
=====================================
@@ -1,5 +1,14 @@
 # Nik4 Change History
 
+## 1.7, 27.03.2021
+
+* Choose any projection with `--projection`. [#29](https://github.com/Zverik/Nik4/pull/29) - thanks @Nakaner.
+* Removing `status="off"` for enabled layers. [#27](https://github.com/Zverik/Nik4/issues/27), [#28](https://github.com/Zverik/Nik4/issues/28)
+* Refactoring and PEP8 compliance.
+* Fixed mapnik bindings that require 8-bit strings. [#30](https://github.com/Zverik/Nik4/pull/30) - thanks @Nakaner.
+* Fixed scale calculation with `--fit` option present. [#33](https://github.com/Zverik/Nik4/pull/33) - thanks @knowname.
+* You can use `--tiles NxM` (e.g. 2x4) now. [#35](https://github.com/Zverik/Nik4/pull/35) - thanks @woodpeck.
+
 ## 1.6, 1.06.2016
 
 * `--version` option.


=====================================
README.md
=====================================
@@ -28,7 +28,7 @@ Again, run `nik4.py -h` to see the list of all available options. Here are some
 
 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).
+[here](https://osm2pgsql.org/doc/manual.html) 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
@@ -164,7 +164,7 @@ stroke width in the last example configurable, and request GPX file name:
 
 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
+    nik4.py --fit route --ppi 150 -a 6 osm.xml route.png --vars width=8 route=~/routes/day2.gpx
 
 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.


=====================================
debian/changelog
=====================================
@@ -1,9 +1,17 @@
-nik4 (1.6-8) UNRELEASED; urgency=medium
+nik4 (1.7-1) unstable; urgency=medium
 
+  * Move from experimental to unstable.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Sun, 15 Aug 2021 12:30:54 +0200
+
+nik4 (1.7-1~exp1) experimental; urgency=medium
+
+  * New upstream release.
   * Bump Standards-Version to 4.5.1, no changes.
   * Update watch file for GitHub URL changes.
+  * Refresh patches.
 
- -- Bas Couwenberg <sebastic at debian.org>  Sat, 28 Nov 2020 13:56:19 +0100
+ -- Bas Couwenberg <sebastic at debian.org>  Sun, 28 Mar 2021 07:19:55 +0200
 
 nik4 (1.6-7) unstable; urgency=medium
 


=====================================
debian/patches/nik4.patch
=====================================
@@ -14,7 +14,7 @@ Forwarded: not-needed
  ### Watch a mapping party area
  
 @@ -31,18 +31,18 @@ to populate the database with a planet e
- [here](http://switch2osm.org/loading-osm-data/) or [here](http://wiki.openstreetmap.org/wiki/User:Zverik/Tile_Server_on_Fedora_20).
+ [here](https://osm2pgsql.org/doc/manual.html) 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
@@ -80,8 +80,8 @@ Forwarded: not-needed
  
  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
-+    nik4 --fit route --ppi 150 -a 6 --vars width=8 route=~/routes/day2.gpx osm.xml route.png
+-    nik4.py --fit route --ppi 150 -a 6 osm.xml route.png --vars width=8 route=~/routes/day2.gpx
++    nik4 --fit route --ppi 150 -a 6 osm.xml route.png --vars width=8 route=~/routes/day2.gpx
  
  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.


=====================================
nik4.py
=====================================
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
 # Nik4: Export image from mapnik
@@ -6,67 +6,87 @@
 # Written by Ilya Zverev, licensed WTFPL
 
 import mapnik
-import sys, os, re, argparse, math, tempfile
+import sys
+import os
+import re
+import argparse
+import math
+import tempfile
+import logging
+import codecs
 
 try:
-	import cairo
-	HAS_CAIRO = True
+    import cairo
+    HAS_CAIRO = True
 except ImportError:
-	HAS_CAIRO = False
+    HAS_CAIRO = False
 
-VERSION = '1.6'
+VERSION = '1.7'
 TILE_BUFFER = 128
 IM_MONTAGE = 'montage'
+EPSG_4326 = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+EPSG_3857 = ('+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')
+
+proj_lonlat = mapnik.Projection(EPSG_4326)
+proj_web_merc = mapnik.Projection(EPSG_3857)
+transform_lonlat_webmerc = mapnik.ProjTransform(proj_lonlat, proj_web_merc)
+
+
+def layer_bbox(m, names, proj_target, 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
+        layer_proj = mapnik.Projection(layer.srs)
+        box_trans = mapnik.ProjTransform(layer_proj, proj_target)
+        lbbox = box_trans.forward(layer.envelope())
+        if bbox:
+            bbox.expand_to_include(lbbox)
+        else:
+            bbox = lbbox
+    return bbox
 
-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
+    """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
+    """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, transform):
+    """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'))
+
+    ozipoint = ('Point{:02d},xy,     ,     ,in, deg,    ,        ,N,    ,        ,E' +
+                ', grid,   ,           ,           ,N')
+    bbox = transform.backward(mbbox)
+    points = "\n".join([ozipoint.format(n) for n in range(3, 31)])
+    header = '''OziExplorer Map Data File Version 2.2
 Nik4
-{}
+{name}
 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
-{}
+Point01,xy,    0,    0,in, deg,{top},{left}, grid,   ,           ,           ,N
+Point02,xy, {width:4d}, {height:4d},in, deg,{bottom},{right}, grid,   ,           ,           ,N
+{points}
 Projection Setup,,,,,,,,,,
 Map Feature = MF ; Map Comment = MC     These follow if they exist
 Track File = TF      These follow if they exist
@@ -74,396 +94,583 @@ 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)
+'''.format(name=name,
+           top=deg(bbox.maxy, False),
+           left=deg(bbox.minx, True),
+           width=mwidth - 1,
+           height=mheight - 1,
+           bottom=deg(bbox.miny, False),
+           right=deg(bbox.maxx, True),
+           points=points)
+    return ''.join([
+        header,
+        "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]])
+    """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 write_metadata(bbox, mwidth, mheight, transform, img_output_file, wld_file=None, ozi_file=None):
+    """Write worldfile and/or OZI file if required.
+
+    Parameters
+    ----------
+    bbox: mapnik.Box2d
+        bounding box of the map
+    mwidth : int
+        width of the image
+    mheight : int
+        height of the image
+    transform : mapnik.ProjTransform
+        transformation from EPSG:4326 to the target projection
+    img_output_file : str
+        image output path (required for OZI file)
+    wld : file
+        file pointer to the world file to be written (or None if non has to be written)
+    ozi : file
+        file pointer to the OZI file to be written (or None if non has to be written)
+    """
+    if ozi_file:
+        ozi_file.write(prepare_ozi(bbox, mwidth, mheight, img_output_file, transform))
+    if wld_file:
+        wld_file.write(prepare_wld(bbox, mwidth, mheight))
+
 
 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]
+    """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
+    """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
+    """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("'", ''')
+    if not v:
+        return style
+    # 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 reenable_layers(style, layers):
+    """Remove status=off from layers we need."""
+    layer_select = '|'.join([l.replace('\\', '\\\\').replace('|', '\\|')
+                             .replace('.', '\\.').replace('+', '\\+')
+                             .replace('*', '\\*') for l in layers])
+    style = re.sub(
+        r'(<Layer[^>]+name=["\'](?:{})["\'][^>]+)status=["\']off["\']'.format(layer_select),
+        r'\1', style, flags=re.DOTALL)
+    style = re.sub(
+        r'(<Layer[^>]+)status=["\']off["\']([^>]+name=["\'](?:{})["\'])'.format(layer_select),
+        r'\1\2', style, flags=re.DOTALL)
+    return style
+
+
+def parse_layers_string(layers):
+    if not layers:
+        return []
+    return [l1 for l1 in (l.strip() for l in layers.split(',')) if l1]
+
 
 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 os.path.exists(path):
+        mapnik.register_fonts(path)
+    else:
+        raise Exception('The directory "{p}" does not exists'.format(p=path))
+
+
+def correct_scale(bbox, scale, bbox_web_merc, bbox_target):
+    # correct scale if output projection is not EPSG:3857
+    x_dist_merc = bbox_web_merc.maxx - bbox_web_merc.minx
+    x_dist_target = bbox.maxx - bbox.minx
+    return scale * (x_dist_target / x_dist_merc)
+
+
+def run(options):
+    dim_mm = None
+    scale = None
+    size = None
+    bbox = None
+    rotate = not options.norotate
+
+    if (options.ozi and options.projection.lower() != 'epsg:3857'
+            and options.projection != EPSG_3857):
+        raise Exception('Ozi map file output is only supported for Web Mercator (EPSG:3857). ' +
+                        'Please remove --projection.')
+
+    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']
+
+    # output projection
+    if options.projection.isdigit():
+        proj_target = mapnik.Projection('+init=epsg:{}'.format(options.projection))
+    else:
+        proj_target = mapnik.Projection(options.projection)
+    transform = mapnik.ProjTransform(proj_lonlat, proj_target)
+
+    # 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:
+        logging.error('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')
+
+    if options.bbox:
+        bbox = options.bbox
+
+    # 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
+        # TODO: division should only happen for EPSG:3857 or not at all
+        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
+
+    # all calculations are in EPSG:3857 projection (it's easier)
+    if bbox:
+        bbox = transform.forward(mapnik.Box2d(*bbox))
+        bbox_web_merc = transform_lonlat_webmerc.forward(mapnik.Box2d(*(options.bbox)))
+        if scale:
+            scale = correct_scale(bbox, scale, bbox_web_merc, 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:
+        # We don't know over which latitude range the bounding box spans, so we
+        # first do everything in Web Mercator.
+        center = transform_lonlat_webmerc.forward(mapnik.Coord(*options.center))
+        w = size[0] * scale / 2
+        h = size[1] * scale / 2
+        bbox_web_merc = mapnik.Box2d(center.x-w, center.y-h, center.x+w, center.y+h)
+        bbox = transform_lonlat_webmerc.backward(bbox_web_merc)
+        bbox = transform.forward(bbox)
+        # now correct the scale
+        scale = correct_scale(bbox, scale, bbox_web_merc, bbox)
+        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 codecs.open(options.style, 'r', 'utf-8') as style_file:
+            style_xml = style_file.read()
+        style_path = os.path.dirname(options.style)
+    if options.base:
+        style_path = options.base
+    if options.vars:
+        style_xml = xml_vars(style_xml, options.vars)
+    if options.layers or options.add_layers:
+        style_xml = reenable_layers(
+            style_xml, parse_layers_string(options.layers) +
+            parse_layers_string(options.add_layers))
+
+    # 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.encode("utf-8"), False, style_path)
+    m.srs = proj_target.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(','), proj_target, 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))
+        bbox_web_merc = transform_lonlat_webmerc.forward(transform.backward(bbox))
+        if scale:
+            scale = correct_scale(bbox, scale, bbox_web_merc, bbox)
+        # 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_x > 1 or options.tiles_y > 1)):
+        options.tiles_x = 1
+        options.tiles_y = 1
+    max_img_size = max(size[0] / options.tiles_x, size[1] / options.tiles_y)
+    if max_img_size > 16384:
+        larger_part = 'a larger value for ' if options.tiles_x > 1 or options.tiles_y > 1 else ''
+        raise Exception('Image size exceeds mapnik limit ({} > {}), use {}--tiles'.format(
+           max_img_size, 16384, larger_part))
+
+    # add / remove some layers
+    if options.layers:
+        filter_layers(m, parse_layers_string(options.layers))
+    if options.add_layers or options.hide_layers:
+        select_layers(m, parse_layers_string(options.add_layers),
+                      parse_layers_string(options.hide_layers))
+
+    logging.debug('scale=%s', scale)
+    logging.debug('scale_factor=%s', scale_factor)
+    logging.debug('size=%s,%s', size[0], size[1])
+    logging.debug('bbox=%s', bbox)
+    logging.debug('bbox_wgs84=%s', transform.backward(bbox) if bbox else None)
+    logging.debug('layers=%s', ','.join([l.name for l in m.layers if l.active]))
+
+    # export image
+    m.aspect_fix_mode = mapnik.aspect_fix_mode.GROW_BBOX
+    m.resize(size[0], size[1])
+    m.zoom_to_box(bbox)
+    logging.debug('m.envelope(): {}'.format(m.envelope()))
+
+    outfile = options.output
+    if options.output == '-':
+        outfile = tempfile.TemporaryFile(mode='w+b')
+
+    if need_cairo:
+        if HAS_CAIRO:
+            if fmt == 'svg':
+                surface = cairo.SVGSurface(outfile, size[0], size[1])
+            else:
+                surface = 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)
+        write_metadata(m.envelope(), size[0], size[1], transform, options.output,
+                       options.wld, options.ozi)
+    else:
+        if options.tiles_x == options.tiles_y == 1:
+            im = mapnik.Image(size[0], size[1])
+            mapnik.render(m, im, scale_factor)
+            im.save(outfile, fmt)
+            write_metadata(m.envelope(), size[0], size[1], transform, options.output,
+                           options.wld, options.ozi)
+        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_x)))
+            height = max(32, int(math.ceil(1.0 * size[1] / options.tiles_y)))
+            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))]
+            logging.debug('tile_count=%s %s', tile_cnt[0], tile_cnt[1])
+            logging.debug('tile_size=%s,%s', width, height)
+            tmp_tile = '{:02d}_{:02d}_{}'
+            tile_files = []
+            for row in range(0, tile_cnt[1]):
+                for column in range(0, tile_cnt[0]):
+                    logging.debug('tile=%s,%s', 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
+                        if '.' not in tile_name:
+                            tile_basename = tile_name + '.'
+                        else:
+                            tile_basename = 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', transform))
+                        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)
+                    write_metadata(bbox, size[0], size[1], transform, options.output,
+                                   options.wld, options.ozi)
+
+    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)
+        sys.stdout.write(outfile.read())
+        outfile.close()
+
 
 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()
+    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, default=1,
+                        help='Scale factor (affects ppi, 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, default=5,
+                        help='Margin for layers in --fit (default=5), mm')
+    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('-P', '--projection', default=EPSG_3857,
+                        help='EPSG code as 1234 (without prefix "EPSG:" or Proj4 string')
+
+    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', default='1',
+                        help='Write N×N (--tiles N) or N×M (--tiles NxM) 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()
+
+    options.tiles_x = 0
+    options.tiles_y = 0
+
+    if options.tiles:
+        if options.tiles.isdigit():
+            options.tiles_x = int(options.tiles)
+            options.tiles_y = options.tiles_x
+        else:
+            match = re.search(r'^(\d+)x(\d+)$', options.tiles)
+            if match:
+                options.tiles_x = int(match.group(1))
+                options.tiles_y = int(match.group(2))
+        if not 1 <= options.tiles_x * options.tiles_y <= 144:
+            raise Exception('--tiles needs positive integer argument, or two integers separated '
+                            'by x; max. number of tiles is 144')
+
+    if options.debug:
+        log_level = logging.DEBUG
+    else:
+        log_level = logging.INFO
+    logging.basicConfig(level=log_level, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')
+    run(options)


=====================================
setup.py
=====================================
@@ -1,11 +1,11 @@
 from distutils.core import setup
 
 setup(
-		name='Nik4',
-		version='1.6.0',
-		license='WTFPL',
-		description='Command-line interface to a Mapnik rendering toolkit',
-		long_description="""
+        name='Nik4',
+        version='1.7.0',
+        license='WTFPL',
+        description='Command-line interface to a Mapnik rendering toolkit',
+        long_description="""
 Nik4
 ====
 
@@ -15,23 +15,23 @@ 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'
-		]
+        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'
+        ]
 )



View it on GitLab: https://salsa.debian.org/debian-gis-team/nik4/-/compare/60a9a2fb360817a194cfc6af87b739311dbaa2e2...331dda6dd95142dc754437acc28cb7239380c351

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/nik4/-/compare/60a9a2fb360817a194cfc6af87b739311dbaa2e2...331dda6dd95142dc754437acc28cb7239380c351
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210815/49cd3a94/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list