Source code for geos.mapsource

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)