#!/usr/bin/env python # Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo # Copyright (C) 2009-2024 German Aerospace Center (DLR) and others. # This program and the accompanying materials are made available under the # terms of the Eclipse Public License 2.0 which is available at # https://www.eclipse.org/legal/epl-2.0/ # This Source Code may also be made available under the following Secondary # Licenses when the conditions for such availability set forth in the Eclipse # Public License 2.0 are satisfied: GNU General Public License, version 2 # or later which is available at # https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later # @file tileGet.py # @author Michael Behrisch # @author Robert Hilbrich # @date 2019-12-11 from __future__ import absolute_import from __future__ import print_function from __future__ import division import math import os import sys from multiprocessing.pool import Pool import signal try: # python3 import urllib.request as urllib from urllib.error import HTTPError as urlerror except ImportError: import urllib from urllib2 import HTTPError as urlerror import sumolib # noqa MERCATOR_RANGE = 256 MAX_TILE_SIZE = 640 MAPQUEST_TYPES = {"roadmap": "map", "satellite": "sat", "hybrid": "hyb", "terrain": "sat"} def fromLatLonToPoint(lat, lon): # inspired by https://stackoverflow.com/questions/12507274/how-to-get-bounds-of-a-google-static-map x = lon * MERCATOR_RANGE / 360 siny = math.sin(math.radians(lat)) y = 0.5 * math.log((1 + siny) / (1 - siny)) * -MERCATOR_RANGE / (2 * math.pi) return x, y def fromLatLonToTile(lat, lon, zoom): # inspired by https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Python n = 2.0 ** zoom xtile = int((lon + 180.0) / 360.0 * n) ytile = int((1.0 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2.0 * n) return xtile, ytile def fromTileToLatLon(xtile, ytile, zoom): n = 2.0 ** zoom lon = xtile / n * 360.0 - 180.0 lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))) return lat, lon def getZoomWidthHeight(south, west, north, east, maxTileSize): center = ((north + south) / 2, (east + west) / 2) centerPx = fromLatLonToPoint(*center) nePx = fromLatLonToPoint(north, east) zoom = 20 width = (nePx[0] - centerPx[0]) * 2**zoom * 2 height = (centerPx[1] - nePx[1]) * 2**zoom * 2 while width > maxTileSize or height > maxTileSize: zoom -= 1 width /= 2 height /= 2 return center, zoom, width, height def worker(options, request, filename): # print(request) urllib.urlretrieve(request, filename) if os.stat(filename).st_size < options.min_file_size: raise ValueError("small file") def retrieveOpenStreetMapTiles(options, west, south, east, north, decals, net): zoom = 18 numTiles = options.tiles + 1 while numTiles > options.tiles: zoom -= 1 sx, sy = fromLatLonToTile(north, west, zoom) ex, ey = fromLatLonToTile(south, east, zoom) numTiles = (ex - sx + 1) * (ey - sy + 1) if options.user_agent: opener = urllib.build_opener() opener.addheaders = [('User-agent', options.user_agent)] urllib.install_opener(opener) for x in range(sx, ex + 1): for y in range(sy, ey + 1): request = "%s/%s/%s/%s.png" % (options.url, zoom, x, y) filename = os.path.join(options.output_dir, "%s%s_%s.png" % (options.prefix, x, y)) worker(options, request, filename) if net is not None: lat, lon = fromTileToLatLon(x, y, zoom) upperLeft = net.convertLonLat2XY(lon, lat) lat, lon = fromTileToLatLon(x + 0.5, y + 0.5, zoom) center = net.convertLonLat2XY(lon, lat) print(' ' % (os.path.basename(filename), center[0], center[1], 2 * (center[0] - upperLeft[0]), 2 * (upperLeft[1] - center[1]), options.layer), file=decals) def retrieveMapServerTiles(options, west, south, east, north, decals, net): zoom = 20 numTiles = options.tiles + 1 while numTiles > options.tiles: zoom -= 1 sx, sy = fromLatLonToTile(north, west, zoom) ex, ey = fromLatLonToTile(south, east, zoom) numTiles = (ex - sx + 1) * (ey - sy + 1) # opener = urllib.build_opener() # opener.addheaders = [('User-agent', 'Mozilla/5.0')] # urllib.install_opener(opener) if options.parallel_jobs != 0: original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) pool = Pool(options.parallel_jobs) signal.signal(signal.SIGINT, original_sigint_handler) futures = [] for x in range(sx, ex + 1): for y in range(sy, ey + 1): request = "%s/%s/%s/%s" % (options.url, zoom, y, x) filename = os.path.join(options.output_dir, "%s%s_%s.jpeg" % (options.prefix, x, y)) if options.parallel_jobs == 0: worker(options, request, filename) else: futures.append((x, y, pool.apply_async(worker, (options, request, filename)))) if net is not None: lat, lon = fromTileToLatLon(x, y, zoom) upperLeft = net.convertLonLat2XY(lon, lat) lat, lon = fromTileToLatLon(x + 0.5, y + 0.5, zoom) center = net.convertLonLat2XY(lon, lat) print(' ' % (os.path.basename(filename), center[0], center[1], 2 * (center[0] - upperLeft[0]), 2 * (upperLeft[1] - center[1]), options.layer), file=decals) for x, y, future in futures: future.get() def get_options(args=None): optParser = sumolib.options.ArgumentParser() optParser.add_option("-p", "--prefix", category="output", default="tile", help="for output file") optParser.add_option("-b", "--bbox", category="input", help="bounding box to retrieve in geo coordinates west,south,east,north") optParser.add_option("-t", "--tiles", type=int, default=1, help="maximum number of tiles the output gets split into") optParser.add_option("-d", "--output-dir", category="output", default=".", help="optional output directory (must already exist)") optParser.add_option("-s", "--decals-file", category="output", default="settings.xml", help="name of decals settings file") optParser.add_option("-l", "--layer", type=int, default=0, help="(int) layer at which the image will appear, default 0") optParser.add_option("-x", "--polygon", category="input", help="calculate bounding box from polygon data in file") optParser.add_option("-n", "--net", category="input", help="get bounding box from net file") optParser.add_option("-k", "--key", help="API key to use") optParser.add_option("-m", "--maptype", default="satellite", help="map type (roadmap, satellite, hybrid, terrain)") optParser.add_option("-u", "--url", default="arcgis", help="Download from the given tile server") optParser.add_option("-a", "--user-agent", help="user agent string to be used when downloading tiles") optParser.add_option("-f", "--min-file-size", type=int, default=3000, help="maximum number of tiles the output gets split into") optParser.add_option("-j", "--parallel-jobs", type=int, default=8, help="Number of parallel jobs to run when downloading tiles. 0 means no parallelism.") URL_SHORTCUTS = { "arcgis": "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile", "mapquest": "https://www.mapquestapi.com/staticmap/v5/map", "google": "https://maps.googleapis.com/maps/api/staticmap", "openstreetmap": "https://tile.openstreetmap.org" } options = optParser.parse_args(args=args) if not options.bbox and not options.net and not options.polygon: optParser.error("At least one of 'bbox' and 'net' and 'polygon' has to be set.") options.url = URL_SHORTCUTS.get(options.url.lower(), options.url) if not options.url.startswith("http"): options.url = "https://" + options.url if options.bbox: west, south, east, north = [float(v) for v in options.bbox.split(',')] if south > north or west > east: optParser.error("Invalid geocoordinates in bbox.") return options def get(args=None): options = get_options(args) if options.polygon: west = 1e400 south = 1e400 east = -1e400 north = -1e400 for area in sumolib.output.parse_fast(options.polygon, 'poly', ['shape']): coordList = [tuple(map(float, x.split(','))) for x in area.shape.split()] for point in coordList: west = min(point[0], west) south = min(point[1], south) east = max(point[0], east) north = max(point[1], north) if options.bbox: west, south, east, north = [float(v) for v in options.bbox.split(',')] net = None if options.net: net = sumolib.net.readNet(options.net) bboxNet = net.getBBoxXY() offset = (bboxNet[1][0] - bboxNet[0][0]) / options.tiles west, south = net.convertXY2LonLat(*bboxNet[0]) east, north = net.convertXY2LonLat(*bboxNet[1]) prefix = os.path.join(options.output_dir, options.prefix) mapQuest = "mapquest" in options.url with sumolib.openz(os.path.join(options.output_dir, options.decals_file), "w") as decals: sumolib.xml.writeHeader(decals, root="viewsettings") if "MapServer" in options.url: retrieveMapServerTiles(options, west, south, east, north, decals, net) elif "openstreetmap" in options.url or "geofabrik" in options.url: retrieveOpenStreetMapTiles(options, west, south, east, north, decals, net) else: b = west for i in range(options.tiles): e = b + (east - west) / options.tiles c, z, w, h = getZoomWidthHeight(south, b, north, e, 2560 if mapQuest else 640) if mapQuest: size = "size=%d,%d" % (w, h) maptype = 'imagetype=png&type=' + MAPQUEST_TYPES[options.maptype] else: size = "size=%dx%d" % (w, h) maptype = 'maptype=' + options.maptype request = ("%s?%s¢er=%.6f,%.6f&zoom=%s&%s&key=%s" % (options.url, size, c[0], c[1], z, maptype, options.key)) # print(request) filename = os.path.join(options.output_dir, "%s%s.png" % (prefix, i)) urllib.urlretrieve(request, filename) if os.stat(filename).st_size < options.min_file_size: raise ValueError("small file") if net is not None: print(' ' % (os.path.basename(filename), bboxNet[0][0] + (i + 0.5) * offset, (bboxNet[0][1] + bboxNet[1][1]) / 2, offset, bboxNet[1][1] - bboxNet[0][1], options.layer), file=decals) b = e print("", file=decals) if __name__ == "__main__": try: get() except urlerror as e: print("Error: Tile server returned %s." % e, file=sys.stderr) if e.code == 403: print(" Maybe an API key is required.", file=sys.stderr) except ValueError as e: print("Error: Tile server returned %s." % e, file=sys.stderr)