import xml.etree.ElementTree
import itertools
import os
from pprint import pprint
from geos.geometry import GeographicBB
import re
import random
F_SEP = "/" # folder separator in mapsources (not necessarily == os.sep)
[docs]def load_maps(maps_dir):
"""
Load all xml map sources from a given directory.
Args:
maps_dir: path to directory to search for maps
Returns:
dict of MapSource:
"""
maps_dir = os.path.abspath(maps_dir)
maps = {}
for root, dirnames, filenames in os.walk(maps_dir):
for filename in filenames:
if filename.endswith(".xml"):
xml_file = os.path.join(root, filename)
map = MapSource.from_xml(xml_file, maps_dir)
if map.id in maps:
raise MapSourceException("duplicate map id: {} in file {}".format(map.id, xml_file))
else:
maps[map.id] = map
return maps
[docs]def walk_mapsources(mapsources, root=""):
"""
recursively walk through foldernames of mapsources.
Like os.walk, only for a list of mapsources.
Args:
mapsources (list of MapSource):
Yields:
(root, foldernames, maps)
>>> mapsources = load_maps("test/mapsources")
>>> pprint([x for x in walk_mapsources(mapsources.values())])
[('',
['asia', 'europe'],
[<MapSource: osm1 (root 1), n_layers: 1, min_zoom:5, max_zoom:18>,
<MapSource: osm10 (root 2), n_layers: 1, min_zoom:5, max_zoom:18>]),
('/asia',
[],
[<MapSource: osm6 (asia), n_layers: 1, min_zoom:5, max_zoom:18>]),
('/europe',
['france', 'germany', 'switzerland'],
[<MapSource: osm4 (eruope 1), n_layers: 1, min_zoom:1, max_zoom:18>]),
('/europe/france',
[],
[<MapSource: osm2 (europe/france 1), n_layers: 1, min_zoom:5, max_zoom:18>,
<MapSource: osm3 (europe/france 2), n_layers: 1, min_zoom:1, max_zoom:18>,
<MapSource: osm5 (europe/france 3), n_layers: 1, min_zoom:5, max_zoom:18>]),
('/europe/germany',
[],
[<MapSource: osm7 (europe/germany 1), n_layers: 1, min_zoom:5, max_zoom:18>,
<MapSource: osm8 (europe/germany 2), n_layers: 1, min_zoom:5, max_zoom:18>]),
('/europe/switzerland',
[],
[<MapSource: osm9 (europe/switzerland), n_layers: 1, min_zoom:5, max_zoom:18>])]
"""
def get_first_folder(path):
"""
Get the first folder in a path
> get_first_folder("europe/switzerland/bs")
europe
"""
path = path[len(root):]
path = path.lstrip(F_SEP)
return path.split(F_SEP)[0]
path_tuples = sorted(((get_first_folder(m.folder), m) for m in mapsources), key=lambda x: x[0])
groups = {k: [x for x in g] for k, g in itertools.groupby(path_tuples, lambda x: x[0])}
folders = sorted([x for x in groups.keys() if x != ""])
mapsources = sorted([t[1] for t in groups.get("", [])], key=lambda x: x.id)
yield (root, folders, mapsources)
for fd in folders:
yield from walk_mapsources([t[1] for t in groups[fd]], F_SEP.join([root, fd]))
[docs]class MapSourceException(Exception):
pass
[docs]class MapLayer(object):
"""
The layer object contained in a MapSource.
A MapSource can contain multiple layers.
A layer contains all information on how to access the tiles.
Args:
tile_url: URL to the tiles which {$z}, {$x} and {$y} as placeholders.
min_zoom (int): minimal zoom level at which the layer is active
max_zoom (int): maximal zoom level at which the layer is active
"""
def __init__(self, tile_url=None, min_zoom=1, max_zoom=17):
self.tile_url = tile_url
self.min_zoom = min_zoom
self.max_zoom = max_zoom
self.server_parts = []
[docs] def get_tile_url(self, zoom, x, y):
"""
Fill the placeholders of the tile url with zoom, x and y.
>>> ms = MapSource.from_xml("mapsources/osm.xml")
>>> ms.layers[0].get_tile_url(42, 43, 44)
'http://tile.openstreetmap.org/42/43/44.png'
"""
if(len(self.server_parts) == 0):
return self.tile_url.format(**{"$z": zoom, "$x": x, "$y": y})
else:
return self.tile_url.format(**{"$z": zoom, "$x": x, "$y": y, "$serverpart": random.choice(self.server_parts)})
@property
def get_tile_urls(self):
if(len(self.server_parts) == 0):
return [self.tile_url.replace("$", "")]
else:
cur_tile_url = []
for s in self.server_parts:
cur_tile_url.append(self.tile_url.replace("{$serverpart}", s).replace("$", ""))
return cur_tile_url
def __repr__(self):
return "<MapLayer: url:{}, min_zoom:{}, max_zoom:{}>".format(
self.tile_url, self.min_zoom, self.max_zoom)
[docs]class MapSource(object):
"""
A MapSource contains Meta-Information of the map.
Additionally it can hold one or more MapLayers which contain the information
on how to access the tiles.
"""
def __init__(self, id, name, folder="", bbox=None):
"""
Args:
id (str): unique identifier (e.g. filename)
name (str): display name
folder (str): folder to organize the map in (e.g. /europe/germany)
bbox (GeographicBB): bounding box, only load tiles within
>>> ms = MapSource.from_xml("mapsources/osm.xml")
>>> ms.name
'OSM Mapnik (default)'
>>> ms.min_zoom
0
>>> ms.max_zoom
18
>>> ms.layers
[<MapLayer: url:http://tile.openstreetmap.org/{$z}/{$x}/{$y}.png, min_zoom:0, max_zoom:18>]
"""
self.id = id
self.name = name
self.layers = []
self.folder = folder
self.bbox = bbox
@property
def min_zoom(self):
"""
Get the minimal zoom level of all layers.
Returns:
int: the minimum of all zoom levels of all layers
Raises:
ValueError: if no layers exist
"""
zoom_levels = [map_layer.min_zoom for map_layer in self.layers]
return min(zoom_levels)
@property
def max_zoom(self):
"""
Get the maximal zoom level of all layers.
Returns:
int: the maximum of all zoom levels of all layers
Raises:
ValueError: if no layers exist
"""
zoom_levels = [map_layer.max_zoom for map_layer in self.layers]
return max(zoom_levels)
[docs] @staticmethod
def parse_xml_boundary(xml_region):
"""
Get the geographic bounds from an XML element
Args:
xml_region (Element): The <region> tag as XML Element
Returns:
GeographicBB:
"""
try:
bounds = {}
for boundary in xml_region:
bounds[boundary.tag] = float(boundary.text)
bbox = GeographicBB(min_lon=bounds["west"], max_lon=bounds["east"],
min_lat=bounds["south"], max_lat=bounds["north"])
return bbox
except (KeyError, ValueError):
raise MapSourceException("region boundaries are invalid. ")
[docs] @staticmethod
def parse_xml_layers(xml_layers):
"""
Get the MapLayers from an XML element
Args:
xml_layers (Element): The <layers> tag as XML Element
Returns:
list of MapLayer:
"""
layers = []
for custom_map_source in xml_layers:
layers.append(MapSource.parse_xml_layer(custom_map_source))
return layers
[docs] @staticmethod
def parse_xml_layer(xml_custom_map_source):
"""
Get one MapLayer from an XML element
Args:
xml_custom_map_source (Element): The <customMapSource> element tag wrapped
in a <layers> tag as XML Element
Returns:
MapLayer:
"""
map_layer = MapLayer()
try:
for elem in xml_custom_map_source.getchildren():
if elem.tag == 'url':
map_layer.tile_url = elem.text
elif elem.tag == 'minZoom':
map_layer.min_zoom = int(elem.text)
elif elem.tag == 'maxZoom':
map_layer.max_zoom = int(elem.text)
elif elem.tag == 'serverParts':
map_layer.server_parts += str(elem.text).split()
except ValueError:
raise MapSourceException("minZoom/maxZoom must be an integer. ")
if map_layer.tile_url is None:
raise MapSourceException("Layer requires a tile_url parameter. ")
return map_layer
[docs] @staticmethod
def from_xml(xml_path, mapsource_prefix=""):
"""
Create a MapSource object from a MOBAC
mapsource xml.
Args:
xml_path: path to the MOBAC mapsource xml file.
mapsource_prefix: root path of the mapsource folder.
Used to determine relative path within the maps
directory.
Note:
The Meta-Information is read from the xml
<id>, <folder>, <name> tags. If <id> is not available it defaults
to the xml file basename. If <folder> is not available if defaults to
the folder of the xml file with the `mapsource_prefix` removed.
The function first tries <url>, <minZoom>, <maxZoom> from <customMapSource>
tags within the <layers> tag. If the <layers> tag is not available,
the function tries to find <url>, <minZoom> and <maxZoom> on the top level.
If none of thie information is found, a MapSourceException is raised.
Returns:
MapSource:
Raises:
MapSourceException: when the xml file could not be parsed properly.
"""
xmldoc = xml.etree.ElementTree.parse(xml_path).getroot()
map_id = os.path.splitext(os.path.basename(xml_path))[0]
map_name = map_id
map_folder = re.sub("^" + re.escape(mapsource_prefix), "", os.path.dirname(xml_path))
bbox = None
layers = None
for elem in xmldoc:
if elem.tag == 'id':
map_id = elem.text
elif elem.tag == 'name':
map_name = elem.text
elif elem.tag == 'folder':
map_folder = elem.text
elif elem.tag == 'region':
bbox = MapSource.parse_xml_boundary(elem)
elif elem.tag == 'layers':
layers = MapSource.parse_xml_layers(elem)
if map_folder is None:
map_folder = "" # fallback if bad specification in xml
if layers is None: # layers tag not found, expect url etc. in main xmldoc
layers = [MapSource.parse_xml_layer(xmldoc)]
ms = MapSource(map_id, map_name, map_folder, bbox=bbox)
ms.layers = layers
return ms
def __repr__(self):
return "<MapSource: {} ({}), n_layers: {}, min_zoom:{}, max_zoom:{}>".format(
self.id, self.name, len(self.layers), self.min_zoom, self.max_zoom)