#!/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)