"""Routing graph keys (docs/backcountry/PLAN.md B13.2): one name per graph across its build, tiles and register step. Each app lists its graphs in its own tools/geodata/-graph/graphs.json (`offroad-official`, `offroad-osm`; later `outdoor-official`, `outdoor-osm`), so a new graph adds a file instead of editing this one: kind official (rows from official sources, never an OSM edge) or osm (OpenStreetMap edges only, ODbL: its own archive, never joined with another graph, with the ODbL notice in its metadata) tiers an official graph's tiers (optional, default clean only: Offroad's RJ-18); Backcountry's outdoor-official takes clean and unverified edges, each tagged, for the trip's "Include unverified sources" (BJ-5) layer the geo.layers key, the archive's name in /data/tiles and the ODbL notice's dataset dataset_key the geo.dataset_imports key the register step records name the archive's name (tippecanoe -n) table the graph-builds table the register step writes, and table_graph the row's `graph` value there (the app's API reads it, so it keeps the app's own value) edge_props the edge properties the tiles carry (layer e), in order; poi_props those of layer p and poi_kinds the point kinds it takes (both or neither; an osm graph always has them) `aliases` maps an app's older short names (Offroad's `official`, `osm`) to keys. """ import glob import json import os HERE = os.path.dirname(os.path.abspath(__file__)) GEODATA = os.path.normpath(os.path.join(HERE, '..', '..')) KINDS = ('official', 'osm') FIELDS = ('app', 'kind', 'layer', 'dataset_key', 'name', 'table', 'table_graph', 'edge_props') class GraphKeyError(Exception): pass def load(root=GEODATA): """Every app's graphs, {key: spec}, and the aliases. A duplicate key or alias, or a spec missing a field, fails.""" graphs, aliases = {}, {} for path in sorted(glob.glob(os.path.join(root, '*-graph', 'graphs.json'))): with open(path, encoding='utf-8') as f: data = json.load(f) for key, spec in data.get('graphs', {}).items(): missing = [k for k in FIELDS if k not in spec] points = [k for k in ('poi_props', 'poi_kinds') if k in spec] # an osm graph's points (layer p), only its bad_tiers = not set(spec.get('tiers', ['clean'])) <= {'clean', 'unverified'} or (spec['kind'] == 'osm' and 'tiers' in spec) if key in graphs or missing or spec['kind'] not in KINDS or len(points) not in (0, 2) or bad_tiers or (spec['kind'] == 'osm' and not points): raise GraphKeyError(f'{path}: graph {key} is a duplicate or incomplete ({", ".join(missing) or spec.get("kind")})') graphs[key] = {**spec, 'key': key} for alias, key in data.get('aliases', {}).items(): if alias in aliases or alias in graphs: raise GraphKeyError(f'{path}: alias {alias} is taken') aliases[alias] = key unknown = sorted(a for a, k in aliases.items() if k not in graphs) if unknown: raise GraphKeyError(f'aliases to no graph: {", ".join(unknown)}') return graphs, aliases _CACHE = {} def spec(key, root=GEODATA): """The graph a key (or an alias) names.""" if root not in _CACHE: _CACHE[root] = load(root) graphs, aliases = _CACHE[root] key = aliases.get(key, key) if key not in graphs: raise GraphKeyError(f'no routing graph {key!r} (graph keys: {", ".join(sorted(graphs))})') return graphs[key] def props(s): """The properties the tiles keep (tippecanoe -y): the edges', the points' (osm), then the graph version `v`.""" return tuple(dict.fromkeys(s['edge_props'] + s.get('poi_props', []) + ['v']))