Source code for geos.print

"""
Module for printing maps / exporting them as pdf
"""

from geos.geometry import *
import urllib.request
from urllib.error import URLError
from PIL import Image, ImageDraw
from geos import app
from tempfile import NamedTemporaryFile
from multiprocessing import Pool
import pkg_resources

TILE_SIZE = 256  # px
# some tile servers block requests noch coming from a Browser
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, ' \
             'like Gecko) Chrome/35.0.1916.153 Safari/537.36 SE 2.X MetaSr 1.0'
N_DOWNLOAD_WORKERS = 16

# set default user agent
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', USER_AGENT)]
urllib.request.install_opener(opener)


[docs]class MapPrintError(Exception): pass
[docs]def dpi_to_dpmm(dpi): """ Convert dpi (dots per inch) to dpmm (dots per millimeter) Args: dpi (int): Returns: float: dots per millimeter """ return dpi / 25.4
[docs]def get_print_bbox(x, y, zoom, width, height, dpi): """ Calculate the tile bounding box based on position, map size and resolution. The function returns the next larger tile-box, that covers the specified page size in mm. Args: x (float): map center x-coordinate in Mercator projection (EPSG:4326) y (float): map center y-coordinate in Mercator projection (EPSG:4326) zoom (int): tile zoom level to use for printing width (float): page width in mm height (float): page height in mm dpi (int): resolution in dots per inch Returns: GridBB: Bounding box of the map in TileCoordinates. >>> str(get_print_bbox(4164462.1505763642, 985738.7965919945, 14, 297, 150, 120)) '<tile min: <zoom: 14, x: 9891, y: 7786>, max: <zoom: 14, x: 9897, y: 7790>>' """ tiles_h = width * dpi_to_dpmm(dpi) / TILE_SIZE tiles_v = height * dpi_to_dpmm(dpi) / TILE_SIZE mercator_coords = MercatorCoordinate(x, y) tile_coords = mercator_coords.to_tile(zoom) tile_bb = GridBB(zoom, min_x=tile_coords.x - math.ceil(tiles_h / 2), max_x=tile_coords.x + math.ceil(tiles_h / 2), min_y=tile_coords.y - math.ceil(tiles_v / 2), max_y=tile_coords.y + math.ceil(tiles_v / 2)) return tile_bb
[docs]def download_tile(map_layer, zoom, x, y): """ Download a given tile from the tile server. Args: map_layer (MapLayer): MapLayer object which provides the tile-url. zoom (int): zoom level x (int): Tile-x-coordinate y (int): Tile-y-coordinate Returns: file: temporary file containing the downloaded image. """ try: tile_url = map_layer.get_tile_url(zoom, x, y) tmp_file, headers = urllib.request.urlretrieve(tile_url) return (x, y), tmp_file except URLError as e: app.logger.info("Error downloading tile x={}, y={}, z={} for layer {}: {}".format( x, y, zoom, map_layer, e.reason)) return (x, y), pkg_resources.resource_filename("geos", "static/empty_tile.png")
def _download_tile_wrapper(args): """wrapper function for using the multiprocessing module with multiple arguments""" return download_tile(*args)
[docs]def get_tiles(map_layer, bbox, n_workers=N_DOWNLOAD_WORKERS): """ Download tiles. Args: map_source (MapSource): bbox (TileBB): Bounding box delimiting the map n_workers (int): number of threads to used for downloading. Returns: dict of file: Dictionary mapping coordinates to temporary files. Example:: { (x, y) : <FileHandle> } """ p = Pool(n_workers) tiles = {} for (x, y), tmp_file in p.imap_unordered(_download_tile_wrapper, zip(itertools.repeat(map_layer), itertools.repeat(bbox.zoom), *zip(*bboxiter(bbox)))): app.logger.info("Downloaded tile x={}, y={}, z={}".format(x, y, bbox.zoom)) tiles[(x, y)] = tmp_file return tiles
[docs]def stitch_map(tiles, width, height, bbox, dpi): """ Merge tiles together into one image. Args: tiles (list of dict of file): tiles for each layer width (float): page width in mm height (height): page height in mm dpi (dpi): resolution in dots per inch Returns: PIL.Image: merged map. """ size = (int(width * dpi_to_dpmm(dpi)), int(height * dpi_to_dpmm(dpi))) background = Image.new('RGBA', size, (255, 255, 255)) for layer in tiles: layer_img = Image.new("RGBA", size) for (x, y), tile_path in layer.items(): tile = Image.open(tile_path) layer_img.paste(tile, ((x - bbox.min.x) * TILE_SIZE, (y - bbox.min.y) * TILE_SIZE)) background = Image.alpha_composite(background, layer_img) add_scales_bar(background, bbox) return background.convert("RGB")
[docs]def add_scales_bar(img, bbox): """ Add a scales bar to the map. Calculates the resolution at the current latitude and inserts the corresponding scales bar on the map. Args: img (Image): Image object to which the scales bar will be added. bbox (TileBB): boundaries of the map """ tc = TileCoordinate(bbox.min.zoom, bbox.min.x, bbox.min.y) meters_per_pixel = tc.resolution() one_km_bar = int(1000 * (1 / meters_per_pixel)) col_black = (0, 0, 0) line_start = (100, img.size[1] - 100) # px line_end = (line_start[0] + one_km_bar, line_start[1]) whiskers_left = [line_start[0], line_start[1] - 15, line_start[0], line_start[1] + 15] whiskers_right = [line_end[0], line_end[1] - 15, line_end[0], line_end[1] + 15] draw = ImageDraw.Draw(img) draw.line([line_start, line_end], fill=col_black, width=5) draw.line(whiskers_left, fill=col_black, width=2) draw.line(whiskers_right, fill=col_black, width=2) draw.text((line_start[0] + 10, line_start[1] + 10), fill=col_black, text="1 km") del draw