\begin{filecontents*}[overwrite]{maptiles.texpy} """ The LaTeX package mercatormap - version 1.02 (2020/08/06) maptiles.texpy: Python script for map tile download ------------------------------------------------------------------------------------------- Copyright (c) 2020 by Prof. Dr. Dr. Thomas F. Sturm ------------------------------------------------------------------------------------------- This work may be distributed and/or modified under the conditions of the LaTeX Project Public License, either version 1.3 of this license or (at your option) any later version. The latest version of this license is in http://www.latex-project.org/lppl.txt and version 1.3 or later is part of all distributions of LaTeX version 2005/12/01 or later. This work has the LPPL maintenance status `author-maintained'. This work consists of all files listed in README """ import argparse import math import os import requests from pathlib import Path from PIL import Image packageversion = '1.02 (2020/08/06)' def gd(x): return math.atan(math.sinh(x)) def arggd(x): return math.asinh(math.tan(x)) def rad2planar(lon_rad, lat_rad): x = 0.5*lon_rad/math.pi + 0.5 y = 0.5*arggd(lat_rad)/math.pi + 0.5 return (x, y) def deg2planar(lon_deg, lat_deg): return rad2planar(math.radians(lon_deg), math.radians(lat_deg)) def planar2rad(x, y): lon_rad = math.pi*(2*x-1) lat_rad = gd(math.pi*(2*y-1)) return (lon_rad, lat_rad) def planar2deg(x, y): (lon_rad, lat_rad) = planar2rad(x, y) return (math.degrees(lon_rad), math.degrees(lat_rad)) def download(session, url, filename, overwrite=False): local_file = Path(filename) if local_file.is_file() and not overwrite: return print(url+' -> '+filename) r = session.get(url) r.raise_for_status() with open(filename, 'wb') as handler: handler.write(r.content) return def makeparentdirs(afile): directory = Path(os.path.abspath(afile)).parent if not directory.is_dir(): os.makedirs(directory) class MapDefinition(object): def __init__(self, args, west, east, north, south, zoom, x_1, y_1, x_2, y_2, width, height, west_offset, north_offset, south_offset): self.west = west self.east = east self.north = north self.south = south self.zoom = zoom self.x_1 = x_1 self.y_1 = y_1 self.x_2 = x_2 self.y_2 = y_2 self.width = width self.height= height self.west_offset = west_offset self.north_offset = north_offset self.south_offset = south_offset self.n = 2 ** self.zoom self.local_file_prefix = 'tiles/tile_' self.attribution = args.attribution self.attributionprint = args.attributionprint self.pixel = args.pixel self.pixel_width = round(self.width*self.pixel) self.pixel_height = round(self.height*self.pixel) self.tilesize = args.tilesize @classmethod def construct_from_boundaries_rad(cls, args, zoom, west_rad, east_rad, north_rad, south_rad): if zoom<0: zoom = 0 if west_rad>=east_rad: east_rad += 2*math.pi*math.ceil(0.5*(west_rad-east_rad)/math.pi+1e-10) max_lat = gd(math.pi)-1e-10 if north_rad max_lat: north_rad = max_lat if south_rad < -max_lat: south_rad = -max_lat P = 2**(zoom-1)/math.pi a1 = (west_rad+math.pi)*P b1 = (math.pi-arggd(north_rad))*P a2 = (east_rad+math.pi)*P b2 = (math.pi-arggd(south_rad))*P x1 = math.floor(a1) y1 = math.floor(b1) x2 = math.ceil(a2)-1 y2 = math.ceil(b2)-1 width = a2-a1 height = b2-b1 west_offset = a1-x1 north_offset = b1-y1 south_offset = y2-b2+1 return cls(args, west_rad, east_rad, north_rad, south_rad, zoom, x1, y1, x2, y2, width, height, west_offset, north_offset, south_offset ) @classmethod def construct_from_boundaries_deg(cls, args, zoom, west_deg, east_deg, north_deg, south_deg): return cls.construct_from_boundaries_rad(args, zoom,math.radians(west_deg),math.radians(east_deg), math.radians(north_deg),math.radians(south_deg)) @classmethod def construct_from_boundaries_args(cls, args): return cls.construct_from_boundaries_rad(args, args.zoom,math.radians(args.west),math.radians(args.east),math.radians(args.north),math.radians(args.south)) @classmethod def construct_from_reference_rad(cls, args, zoom, width, height, ref_lon_rad, ref_lat_rad, xalign, yalign): if zoom<0: zoom = 0 P = 2**(zoom-1)/math.pi ac = (ref_lon_rad+math.pi)*P bc = (math.pi-arggd(ref_lat_rad))*P a1 = ac - (1+xalign)/2*width a2 = a1 + width b1 = bc - (1+yalign)/2*height b2 = b1 + height west_rad = a1/P - math.pi north_rad = gd(math.pi-b1/P) east_rad = a2/P - math.pi south_rad = gd(math.pi-b2/P) x1 = math.floor(a1) y1 = math.floor(b1) x2 = math.ceil(a2)-1 y2 = math.ceil(b2)-1 west_offset = a1-x1 north_offset = b1-y1 south_offset = y2-b2+1 return cls(args, west_rad, east_rad, north_rad, south_rad, zoom, x1, y1, x2, y2, width, height, west_offset, north_offset, south_offset ) @classmethod def construct_from_reference_deg(cls, args, zoom, width, height, ref_lon_deg, ref_lat_deg, xalign, yalign): return cls.construct_from_center_rad(args, zoom, width, height, math.radians(ref_lon_deg), math.radians(ref_lat_deg), xalign, yalign) @classmethod def construct_from_reference_args(cls, args): return cls.construct_from_reference_rad(args, args.zoom, args.width, args.height, math.radians(args.longitude), math.radians(args.latitude), args.xalign, args.yalign) @classmethod def construct_to_fit_region_rad(cls, args, width, height, reg_west_rad, reg_east_rad, reg_north_rad, reg_south_rad, xalign, yalign): if reg_west_rad>=reg_east_rad: reg_east_rad += 2*math.pi*math.ceil(0.5*(reg_west_rad-reg_east_rad)/math.pi+1e-10) max_lat = gd(math.pi)-1e-10 if reg_north_rad max_lat: reg_north_rad = max_lat if reg_south_rad < -max_lat: reg_south_rad = -max_lat zoom = max( 0, 1 + math.floor( math.log2( math.pi * min( width/(reg_east_rad-reg_west_rad), height/(arggd(reg_north_rad)-arggd(reg_south_rad))) ))) P = 2**(zoom-1)/math.pi if xalign<0: a1 = (reg_west_rad+math.pi)*P a2 = a1 + width elif xalign>0: a2 = (reg_east_rad+math.pi)*P a1 = a2 - width else: a1 = ((reg_west_rad+reg_east_rad)/2+math.pi)*P - width/2 a2 = a1 + width if yalign<0: b1 = (math.pi-arggd(reg_north_rad))*P b2 = b1 + height elif yalign>0: b2 = (math.pi-arggd(reg_south_rad))*P b1 = b2 - height else: b1 = (math.pi-(arggd(reg_north_rad)+arggd(reg_south_rad))/2)*P - height/2 b2 = b1 + height west_rad = a1/P - math.pi north_rad = gd(math.pi-b1/P) east_rad = a2/P - math.pi south_rad = gd(math.pi-b2/P) x1 = math.floor(a1) y1 = math.floor(b1) x2 = math.ceil(a2)-1 y2 = math.ceil(b2)-1 west_offset = a1-x1 north_offset = b1-y1 south_offset = y2-b2+1 return cls(args, west_rad, east_rad, north_rad, south_rad, zoom, x1, y1, x2, y2, width, height, west_offset, north_offset, south_offset ) @classmethod def construct_to_fit_region_deg(cls, args, width, height, reg_west_deg, reg_east_deg, reg_north_deg, reg_south_deg, xalign, yalign): return cls.construct_to_fit_region_rad(args, width, height, math.radians(reg_west_deg),math.radians(reg_east_deg), math.radians(reg_north_deg),math.radians(reg_south_deg), xalign, yalign) @classmethod def construct_to_fit_region_args(cls, args): return cls.construct_to_fit_region_rad(args, args.width, args.height, math.radians(args.west),math.radians(args.east), math.radians(args.north),math.radians(args.south), args.xalign, args.yalign) def get_tile_quantity(self): return (self.x_2-self.x_1+1)*(self.y_2-self.y_1+1) def get_pixel_quantity(self): return self.pixel_width*self.pixel_height def download_tiles(self, url_format, local_file_prefix): self.local_file_prefix = local_file_prefix makeparentdirs(local_file_prefix) with requests.Session() as s: s.headers.update(dict(referer = 'mercatormap/'+packageversion)) try: for xx in range(self.x_1, self.x_2+1): x = xx % self.n for y in range(self.y_1, self.y_2+1): if (y>=0) and (y=0) and (y0: if args.url!=None: if target<3: if mapdef.get_tile_quantity()>400: if args.confirm: tiles_success = mapdef.download_tiles(args.url, args.fileprefix) else: print(f'Too many tiles ({mapdef.get_tile_quantity()}). Add confirm option to proceed anyway.'); else: tiles_success = mapdef.download_tiles(args.url, args.fileprefix) if tiles_success: print('** Download of tiles successful for', args.definition) if target>1: merge_success = mapdef.merge_map(args.definition, args.fileprefix) if merge_success: print('** Merging tiles successful for', args.definition) else: print('** WARNING: Merging tiles failed for', args.definition) else: print('** WARNING: Map tiles download failed for', args.definition) else: if mapdef.get_pixel_quantity()>100000000: if args.confirm: wms_success = mapdef.download_wms_map(args.url, args.definition) else: print(f'Too many pixels ({mapdef.get_pixel_quantity()}). Add confirm option to proceed anyway.'); else: wms_success = mapdef.download_wms_map(args.url, args.definition) else: print('No url for download given') mapdef.save_map(args.definition, tiles_success, merge_success, wms_success) \end{filecontents*}