Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
|
06fc8bc13c | ||
|
15d3fcc149 |
97 changed files with 3984 additions and 12565 deletions
47
.github/workflows/python-app.yml
vendored
47
.github/workflows/python-app.yml
vendored
|
@ -1,47 +0,0 @@
|
|||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python application
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Install dependencies for linters
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8==6.0.0 black==23.1.0
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
flake8
|
||||
- name: Check with black
|
||||
run: |
|
||||
black --check --line-length 79 .
|
||||
- name: Test subways with unittest
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
pip freeze | xargs pip uninstall -y
|
||||
pip install -r subways/requirements.txt
|
||||
python -m unittest discover subways
|
||||
- name: Test tools with unittest
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
pip freeze | xargs pip uninstall -y
|
||||
pip install -r tools/make_poly/requirements.txt
|
||||
python -m unittest discover tools/make_poly
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,7 +3,6 @@ tmp_html/
|
|||
html/
|
||||
.idea
|
||||
.DS_Store
|
||||
.venv
|
||||
*.log
|
||||
*.json
|
||||
*.geojson
|
||||
|
@ -12,6 +11,3 @@ html/
|
|||
*.yaml
|
||||
*.pyc
|
||||
*.txt
|
||||
*.zip
|
||||
*.osm.pbf
|
||||
*.o5m
|
||||
|
|
55
README.md
55
README.md
|
@ -1,12 +1,10 @@
|
|||
# Subway Preprocessor
|
||||
|
||||
Here you see a list of scripts that can be used for preprocessing all the metro
|
||||
systems in the world from OpenStreetMap. `subways` package produces
|
||||
systems in the world from OpenStreetMap. `subway_structure.py` produces
|
||||
a list of disjunct systems that can be used for routing and for displaying
|
||||
of metro maps.
|
||||
|
||||
[](https://github.com/psf/black)
|
||||
|
||||
|
||||
## How To Validate
|
||||
|
||||
|
@ -16,14 +14,14 @@ of metro maps.
|
|||
2. If you don't specify `--xml` or `--source` option to the `process_subways.py` script
|
||||
it tries to fetch data over [Overpass API](https://wiki.openstreetmap.org/wiki/Overpass_API).
|
||||
**Not suitable for the whole planet or large countries.**
|
||||
* Run `scripts/process_subways.py` with appropriate set of command line arguments
|
||||
* Run `process_subways.py` with appropriate set of command line arguments
|
||||
to build metro structures and receive a validation log.
|
||||
* Run `tools/v2h/validation_to_html.py` on that log to create readable HTML tables.
|
||||
* Run `validation_to_html.py` on that log to create readable HTML tables.
|
||||
|
||||
|
||||
## Validating of all metro networks
|
||||
|
||||
There is a `scripts/process_subways.sh` script that is suitable
|
||||
There is a `process_subways.sh` in the `scripts` directory that is suitable
|
||||
for validation of all or many metro networks. It relies on a bunch of
|
||||
environment variables and takes advantage of previous validation runs
|
||||
for effective recurring validations. See
|
||||
|
@ -34,7 +32,8 @@ for details. Here is an example of the script usage:
|
|||
|
||||
```bash
|
||||
export PLANET=https://ftp5.gwdg.de/pub/misc/openstreetmap/planet.openstreetmap.org/pbf/planet-latest.osm.pbf
|
||||
export PLANET_METRO="$HOME/metro/planet-metro.osm.pbf
|
||||
export PLANET_METRO="$HOME/metro/planet-metro.o5m
|
||||
export OSMCTOOLS="$HOME/osmctools"
|
||||
export TMPDIR="$HOME/metro/tmp"
|
||||
export HTML_DIR="$HOME/metro/tmp_html"
|
||||
export DUMP="$HTML_DIR"
|
||||
|
@ -50,24 +49,17 @@ a city's bbox has been extended.
|
|||
## Validating of a single city
|
||||
|
||||
A single city or a country with few metro networks can be validated much faster
|
||||
if you allow the `scripts/process_subway.py` to fetch data from Overpass API. Here are the steps:
|
||||
if you allow the `process_subway.py` to fetch data from Overpass API. Here are the steps:
|
||||
|
||||
1. Python3 interpreter required (3.11+)
|
||||
1. Python3 interpreter required (3.5+)
|
||||
2. Clone the repo
|
||||
```bash
|
||||
```
|
||||
git clone https://github.com/alexey-zakharenkov/subways.git subways_validator
|
||||
cd subways_validator
|
||||
```
|
||||
3. Configure python environment, e.g.
|
||||
3. Execute
|
||||
```bash
|
||||
python3 -m venv scripts/.venv
|
||||
source scripts/.venv/bin/activate
|
||||
pip install -r scripts/requirements.txt
|
||||
```
|
||||
(this is optional if you only process a single city though.)
|
||||
4. Execute
|
||||
```bash
|
||||
PYTHONPATH=. python3 scripts/process_subways.py -c "London" \
|
||||
python3 ./process_subways.py -c "London" \
|
||||
-l validation.log -d London.yaml
|
||||
```
|
||||
here
|
||||
|
@ -79,39 +71,28 @@ if you allow the `scripts/process_subway.py` to fetch data from Overpass API. He
|
|||
|
||||
`validation.log` would contain the list of errors and warnings.
|
||||
To convert it into pretty HTML format
|
||||
5. do
|
||||
4. do
|
||||
```bash
|
||||
mkdir html
|
||||
python3 tools/v2h/validation_to_html.py validation.log html
|
||||
python3 ./validation_to_html.py validation.log html
|
||||
```
|
||||
|
||||
## Publishing validation reports to the Web
|
||||
|
||||
Expose a directory with static contents via a web-server and put into it:
|
||||
- HTML files from the directory specified in the 2nd parameter of `scripts/v2h/validation_to_html.py`
|
||||
- To vitalize "Y" (YAML), "J" (GeoJSON) and "M" (Map) links beside each city name:
|
||||
- The contents of `render` directory from the repository
|
||||
- `cities.txt` file generated with `--dump-city-list` parameter of `scripts/process_subways.py`
|
||||
- YAML files created due to -d option of `scripts/process_subways.py`
|
||||
- GeoJSON files created due to -j option of `scripts/process_subways.py`
|
||||
|
||||
|
||||
## Related external resources
|
||||
|
||||
Summary information about all metro networks that are monitored is gathered in the
|
||||
[Google Spreadsheet](https://docs.google.com/spreadsheets/d/1SEW1-NiNOnA2qDwievcxYV1FOaQl1mb1fdeyqAxHu3k).
|
||||
|
||||
Regular updates of validation results are available at
|
||||
[this website](https://cdn.organicmaps.app/subway).
|
||||
Regular updates of validation results are available at [Organic Maps](https://cdn.organicmaps.app/subway/) and
|
||||
[mail.ru](https://maps.mail.ru/osm/tools/subways/latest/) servers.
|
||||
You can find more info about this validator instance in
|
||||
[OSM Wiki](https://wiki.openstreetmap.org/wiki/Quality_assurance#subway-preprocessor).
|
||||
|
||||
|
||||
## Adding Stop Areas To OSM
|
||||
|
||||
To quickly add `stop_area` relations for the entire city, use the `tools/stop_areas/make_stop_areas.py` script.
|
||||
Give it a bounding box or a `.json` file download from Overpass API.
|
||||
It would produce a JOSM XML file that you should manually check in JOSM. After that
|
||||
To quickly add `stop_area` relations for the entire city, use the `make_stop_areas.py` script
|
||||
from the `stop_area` directory. Give it a bounding box or a `.json` file download from Overpass API.
|
||||
It would produce an JOSM XML file that you should manually check in JOSM. After that
|
||||
just upload it.
|
||||
|
||||
## Author and License
|
||||
|
|
140
checkers/common.py
Normal file
140
checkers/common.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
import logging
|
||||
import math
|
||||
import functools
|
||||
|
||||
|
||||
"""A coordinate of a station precision of which we must take into account
|
||||
is calculated as an average of somewhat 10 elements.
|
||||
Taking machine epsilon 1e-15, averaging 10 numbers with close magnitudes
|
||||
ensures relative precision of 1e-14."""
|
||||
coord_isclose = functools.partial(math.isclose, rel_tol=1e-14)
|
||||
|
||||
|
||||
def coords_eq(lon1, lat1, lon2, lat2):
|
||||
return coord_isclose(lon1, lon2) and coord_isclose(lat1, lat2)
|
||||
|
||||
|
||||
def osm_id_comparator(el):
|
||||
"""This function is used as key for sorting lists of
|
||||
OSM-originated objects
|
||||
"""
|
||||
return (el['osm_type'], el['osm_id'])
|
||||
|
||||
|
||||
def itinerary_comparator(itinerary):
|
||||
"This function is used as key for sorting itineraries in a route"""
|
||||
return (itinerary['stops'], itinerary['interval'])
|
||||
|
||||
|
||||
def compare_stops(stop0, stop1):
|
||||
"""Compares json of two stops in route"""
|
||||
stop_keys = ('name', 'int_name', 'id', 'osm_id', 'osm_type')
|
||||
stop0_props = tuple(stop0[k] for k in stop_keys)
|
||||
stop1_props = tuple(stop1[k] for k in stop_keys)
|
||||
|
||||
if stop0_props != stop1_props:
|
||||
logging.debug("Different stops properties: %s, %s",
|
||||
stop0_props, stop1_props)
|
||||
return False
|
||||
|
||||
if not coords_eq(stop0['lon'], stop0['lat'],
|
||||
stop1['lon'], stop1['lat']):
|
||||
logging.debug("Different stops coordinates: %s (%f, %f), %s (%f, %f)",
|
||||
stop0_props, stop0['lon'], stop0['lat'],
|
||||
stop1_props, stop1['lon'], stop1['lat'])
|
||||
return False
|
||||
|
||||
entrances0 = sorted(stop0['entrances'], key=osm_id_comparator)
|
||||
entrances1 = sorted(stop1['entrances'], key=osm_id_comparator)
|
||||
if entrances0 != entrances1:
|
||||
logging.debug("Different stop entrances")
|
||||
return False
|
||||
|
||||
exits0 = sorted(stop0['exits'], key=osm_id_comparator)
|
||||
exits1 = sorted(stop1['exits'], key=osm_id_comparator)
|
||||
if exits0 != exits1:
|
||||
logging.debug("Different stop exits")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def compare_transfers(transfers0, transfers1):
|
||||
"""Compares two arrays of transfers of the form
|
||||
[(stop1_uid, stop2_uid, time), ...]
|
||||
"""
|
||||
if len(transfers0) != len(transfers1):
|
||||
logging.debug("Different len(transfers): %d != %d",
|
||||
len(transfers0), len(transfers1))
|
||||
return False
|
||||
|
||||
transfers0 = [tuple([t[0], t[1], t[2]])
|
||||
if t[0] < t[1] else
|
||||
tuple([t[1], t[0], t[2]])
|
||||
for t in transfers0]
|
||||
transfers1 = [tuple([t[0], t[1], t[2]])
|
||||
if t[0] < t[1] else
|
||||
tuple([t[1], t[0], t[2]])
|
||||
for t in transfers1]
|
||||
|
||||
transfers0.sort()
|
||||
transfers1.sort()
|
||||
|
||||
diff_cnt = 0
|
||||
for tr0, tr1 in zip(transfers0, transfers1):
|
||||
if tr0 != tr1:
|
||||
if diff_cnt == 0:
|
||||
logging.debug("First pair of different transfers: %s, %s",
|
||||
tr0, tr1)
|
||||
diff_cnt += 1
|
||||
if diff_cnt:
|
||||
logging.debug("Different transfers number = %d", diff_cnt)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def compare_networks(network0, network1):
|
||||
if network0['agency_id'] != network1['agency_id']:
|
||||
logging.debug("Different agency_id at route '%s'",
|
||||
network0['network'])
|
||||
return False
|
||||
|
||||
route_ids0 = sorted(x['route_id'] for x in network0['routes'])
|
||||
route_ids1 = sorted(x['route_id'] for x in network1['routes'])
|
||||
|
||||
if route_ids0 != route_ids1:
|
||||
logging.debug("Different route_ids: %s != %s",
|
||||
route_ids0, route_ids1)
|
||||
return False
|
||||
|
||||
routes0 = sorted(network0['routes'], key=lambda x: x['route_id'])
|
||||
routes1 = sorted(network1['routes'], key=lambda x: x['route_id'])
|
||||
|
||||
# Keys to compare routes. 'name' key is omitted since RouteMaster
|
||||
# can get its name from one of its Routes unpredictably.
|
||||
route_keys = ('type', 'ref', 'colour', 'route_id')
|
||||
|
||||
for route0, route1 in zip(routes0, routes1):
|
||||
route0_props = tuple(route0[k] for k in route_keys)
|
||||
route1_props = tuple(route1[k] for k in route_keys)
|
||||
if route0_props != route1_props:
|
||||
logging.debug("Route props of '%s' are different: %s, %s",
|
||||
route0['route_id'], route0_props, route1_props)
|
||||
return False
|
||||
|
||||
itineraries0 = sorted(route0['itineraries'], key=itinerary_comparator)
|
||||
itineraries1 = sorted(route1['itineraries'], key=itinerary_comparator)
|
||||
|
||||
for itin0, itin1 in zip(itineraries0, itineraries1):
|
||||
if itin0['interval'] != itin1['interval']:
|
||||
logging.debug("Different interval: %d != %d at route %s '%s'",
|
||||
itin0['interval'], itin1['interval'],
|
||||
route0['route_id'], route0['name'])
|
||||
return False
|
||||
if itin0['stops'] != itin1['stops']:
|
||||
logging.debug("Different stops at route %s '%s'",
|
||||
route0['route_id'], route0['name'])
|
||||
return False
|
||||
|
||||
return True
|
|
@ -10,11 +10,10 @@
|
|||
affect the process_subways.py output really doesn't change it.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from common import compare_networks, compare_stops, compare_transfers
|
||||
from common import compare_stops, compare_transfers, compare_networks
|
||||
|
||||
|
||||
def compare_jsons(cache0, cache1):
|
||||
|
@ -29,21 +28,21 @@ def compare_jsons(cache0, cache1):
|
|||
for name in city_names0:
|
||||
city0 = cache0[name]
|
||||
city1 = cache1[name]
|
||||
if not compare_networks(city0["network"], city1["network"]):
|
||||
if not compare_networks(city0['network'], city1['network']):
|
||||
return False
|
||||
|
||||
stop_ids0 = sorted(city0["stops"].keys())
|
||||
stop_ids1 = sorted(city1["stops"].keys())
|
||||
stop_ids0 = sorted(city0['stops'].keys())
|
||||
stop_ids1 = sorted(city1['stops'].keys())
|
||||
if stop_ids0 != stop_ids1:
|
||||
logging.debug("Different stop_ids")
|
||||
return False
|
||||
stops0 = [v for k, v in sorted(city0["stops"].items())]
|
||||
stops1 = [v for k, v in sorted(city1["stops"].items())]
|
||||
stops0 = [v for k, v in sorted(city0['stops'].items())]
|
||||
stops1 = [v for k, v in sorted(city1['stops'].items())]
|
||||
for stop0, stop1 in zip(stops0, stops1):
|
||||
if not compare_stops(stop0, stop1):
|
||||
return False
|
||||
|
||||
if not compare_transfers(city0["transfers"], city1["transfers"]):
|
||||
if not compare_transfers(city0['transfers'], city1['transfers']):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -58,8 +57,8 @@ if __name__ == "__main__":
|
|||
|
||||
path0, path1 = sys.argv[1:3]
|
||||
|
||||
j0 = json.load(open(path0, encoding="utf-8"))
|
||||
j1 = json.load(open(path1, encoding="utf-8"))
|
||||
j0 = json.load(open(path0, encoding='utf-8'))
|
||||
j1 = json.load(open(path1, encoding='utf-8'))
|
||||
|
||||
equal = compare_jsons(j0, j1)
|
||||
|
|
@ -10,39 +10,38 @@
|
|||
affect the process_subways.py output really doesn't change it.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from common import compare_networks, compare_stops, compare_transfers
|
||||
from common import compare_stops, compare_transfers, compare_networks
|
||||
|
||||
|
||||
def compare_jsons(result0, result1):
|
||||
"""Compares two objects which are results of subway generation"""
|
||||
|
||||
network_names0 = sorted([x["network"] for x in result0["networks"]])
|
||||
network_names1 = sorted([x["network"] for x in result1["networks"]])
|
||||
network_names0 = sorted([x['network'] for x in result0['networks']])
|
||||
network_names1 = sorted([x['network'] for x in result1['networks']])
|
||||
if network_names0 != network_names1:
|
||||
logging.debug("Different list of network names!")
|
||||
return False
|
||||
networks0 = sorted(result0["networks"], key=lambda x: x["network"])
|
||||
networks1 = sorted(result1["networks"], key=lambda x: x["network"])
|
||||
networks0 = sorted(result0['networks'], key=lambda x: x['network'])
|
||||
networks1 = sorted(result1['networks'], key=lambda x: x['network'])
|
||||
for network0, network1 in zip(networks0, networks1):
|
||||
if not compare_networks(network0, network1):
|
||||
return False
|
||||
|
||||
stop_ids0 = sorted(x["id"] for x in result0["stops"])
|
||||
stop_ids1 = sorted(x["id"] for x in result1["stops"])
|
||||
stop_ids0 = sorted(x['id'] for x in result0['stops'])
|
||||
stop_ids1 = sorted(x['id'] for x in result1['stops'])
|
||||
if stop_ids0 != stop_ids1:
|
||||
logging.debug("Different stop_ids")
|
||||
return False
|
||||
stops0 = sorted(result0["stops"], key=lambda x: x["id"])
|
||||
stops1 = sorted(result1["stops"], key=lambda x: x["id"])
|
||||
stops0 = sorted(result0['stops'], key=lambda x: x['id'])
|
||||
stops1 = sorted(result1['stops'], key=lambda x: x['id'])
|
||||
for stop0, stop1 in zip(stops0, stops1):
|
||||
if not compare_stops(stop0, stop1):
|
||||
return False
|
||||
|
||||
if not compare_transfers(result0["transfers"], result1["transfers"]):
|
||||
if not compare_transfers(result0['transfers'], result1['transfers']):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -57,8 +56,8 @@ if __name__ == "__main__":
|
|||
|
||||
path0, path1 = sys.argv[1:3]
|
||||
|
||||
j0 = json.load(open(path0, encoding="utf-8"))
|
||||
j1 = json.load(open(path1, encoding="utf-8"))
|
||||
j0 = json.load(open(path0, encoding='utf-8'))
|
||||
j1 = json.load(open(path1, encoding='utf-8'))
|
||||
|
||||
equal = compare_jsons(j0, j1)
|
||||
|
165
css_colours.py
Normal file
165
css_colours.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
import re
|
||||
|
||||
# Source: https://www.w3.org/TR/css3-color/#svg-color
|
||||
CSS_COLOURS = {
|
||||
'aliceblue': '#f0f8ff',
|
||||
'antiquewhite': '#faebd7',
|
||||
'aqua': '#00ffff',
|
||||
'aquamarine': '#7fffd4',
|
||||
'azure': '#f0ffff',
|
||||
'beige': '#f5f5dc',
|
||||
'bisque': '#ffe4c4',
|
||||
'black': '#000000',
|
||||
'blanchedalmond': '#ffebcd',
|
||||
'blue': '#0000ff',
|
||||
'blueviolet': '#8a2be2',
|
||||
'brown': '#a52a2a',
|
||||
'burlywood': '#deb887',
|
||||
'cadetblue': '#5f9ea0',
|
||||
'chartreuse': '#7fff00',
|
||||
'chocolate': '#d2691e',
|
||||
'coral': '#ff7f50',
|
||||
'cornflowerblue': '#6495ed',
|
||||
'cornsilk': '#fff8dc',
|
||||
'crimson': '#dc143c',
|
||||
'cyan': '#00ffff',
|
||||
'darkblue': '#00008b',
|
||||
'darkcyan': '#008b8b',
|
||||
'darkgoldenrod': '#b8860b',
|
||||
'darkgray': '#a9a9a9',
|
||||
'darkgreen': '#006400',
|
||||
'darkgrey': '#a9a9a9',
|
||||
'darkkhaki': '#bdb76b',
|
||||
'darkmagenta': '#8b008b',
|
||||
'darkolivegreen': '#556b2f',
|
||||
'darkorange': '#ff8c00',
|
||||
'darkorchid': '#9932cc',
|
||||
'darkred': '#8b0000',
|
||||
'darksalmon': '#e9967a',
|
||||
'darkseagreen': '#8fbc8f',
|
||||
'darkslateblue': '#483d8b',
|
||||
'darkslategray': '#2f4f4f',
|
||||
'darkslategrey': '#2f4f4f',
|
||||
'darkturquoise': '#00ced1',
|
||||
'darkviolet': '#9400d3',
|
||||
'deeppink': '#ff1493',
|
||||
'deepskyblue': '#00bfff',
|
||||
'dimgray': '#696969',
|
||||
'dimgrey': '#696969',
|
||||
'dodgerblue': '#1e90ff',
|
||||
'firebrick': '#b22222',
|
||||
'floralwhite': '#fffaf0',
|
||||
'forestgreen': '#228b22',
|
||||
'fuchsia': '#ff00ff',
|
||||
'gainsboro': '#dcdcdc',
|
||||
'ghostwhite': '#f8f8ff',
|
||||
'gold': '#ffd700',
|
||||
'goldenrod': '#daa520',
|
||||
'gray': '#808080',
|
||||
'green': '#008000',
|
||||
'greenyellow': '#adff2f',
|
||||
'grey': '#808080',
|
||||
'honeydew': '#f0fff0',
|
||||
'hotpink': '#ff69b4',
|
||||
'indianred': '#cd5c5c',
|
||||
'indigo': '#4b0082',
|
||||
'ivory': '#fffff0',
|
||||
'khaki': '#f0e68c',
|
||||
'lavender': '#e6e6fa',
|
||||
'lavenderblush': '#fff0f5',
|
||||
'lawngreen': '#7cfc00',
|
||||
'lemonchiffon': '#fffacd',
|
||||
'lightblue': '#add8e6',
|
||||
'lightcoral': '#f08080',
|
||||
'lightcyan': '#e0ffff',
|
||||
'lightgoldenrodyellow': '#fafad2',
|
||||
'lightgray': '#d3d3d3',
|
||||
'lightgreen': '#90ee90',
|
||||
'lightgrey': '#d3d3d3',
|
||||
'lightpink': '#ffb6c1',
|
||||
'lightsalmon': '#ffa07a',
|
||||
'lightseagreen': '#20b2aa',
|
||||
'lightskyblue': '#87cefa',
|
||||
'lightslategray': '#778899',
|
||||
'lightslategrey': '#778899',
|
||||
'lightsteelblue': '#b0c4de',
|
||||
'lightyellow': '#ffffe0',
|
||||
'lime': '#00ff00',
|
||||
'limegreen': '#32cd32',
|
||||
'linen': '#faf0e6',
|
||||
'magenta': '#ff00ff',
|
||||
'maroon': '#800000',
|
||||
'mediumaquamarine': '#66cdaa',
|
||||
'mediumblue': '#0000cd',
|
||||
'mediumorchid': '#ba55d3',
|
||||
'mediumpurple': '#9370db',
|
||||
'mediumseagreen': '#3cb371',
|
||||
'mediumslateblue': '#7b68ee',
|
||||
'mediumspringgreen': '#00fa9a',
|
||||
'mediumturquoise': '#48d1cc',
|
||||
'mediumvioletred': '#c71585',
|
||||
'midnightblue': '#191970',
|
||||
'mintcream': '#f5fffa',
|
||||
'mistyrose': '#ffe4e1',
|
||||
'moccasin': '#ffe4b5',
|
||||
'navajowhite': '#ffdead',
|
||||
'navy': '#000080',
|
||||
'oldlace': '#fdf5e6',
|
||||
'olive': '#808000',
|
||||
'olivedrab': '#6b8e23',
|
||||
'orange': '#ffa500',
|
||||
'orangered': '#ff4500',
|
||||
'orchid': '#da70d6',
|
||||
'palegoldenrod': '#eee8aa',
|
||||
'palegreen': '#98fb98',
|
||||
'paleturquoise': '#afeeee',
|
||||
'palevioletred': '#db7093',
|
||||
'papayawhip': '#ffefd5',
|
||||
'peachpuff': '#ffdab9',
|
||||
'peru': '#cd853f',
|
||||
'pink': '#ffc0cb',
|
||||
'plum': '#dda0dd',
|
||||
'powderblue': '#b0e0e6',
|
||||
'purple': '#800080',
|
||||
'red': '#ff0000',
|
||||
'rosybrown': '#bc8f8f',
|
||||
'royalblue': '#4169e1',
|
||||
'saddlebrown': '#8b4513',
|
||||
'salmon': '#fa8072',
|
||||
'sandybrown': '#f4a460',
|
||||
'seagreen': '#2e8b57',
|
||||
'seashell': '#fff5ee',
|
||||
'sienna': '#a0522d',
|
||||
'silver': '#c0c0c0',
|
||||
'skyblue': '#87ceeb',
|
||||
'slateblue': '#6a5acd',
|
||||
'slategray': '#708090',
|
||||
'slategrey': '#708090',
|
||||
'snow': '#fffafa',
|
||||
'springgreen': '#00ff7f',
|
||||
'steelblue': '#4682b4',
|
||||
'tan': '#d2b48c',
|
||||
'teal': '#008080',
|
||||
'thistle': '#d8bfd8',
|
||||
'tomato': '#ff6347',
|
||||
'turquoise': '#40e0d0',
|
||||
'violet': '#ee82ee',
|
||||
'wheat': '#f5deb3',
|
||||
'white': '#ffffff',
|
||||
'whitesmoke': '#f5f5f5',
|
||||
'yellow': '#ffff00',
|
||||
'yellowgreen': '#9acd32',
|
||||
}
|
||||
|
||||
|
||||
def normalize_colour(c):
|
||||
if not c:
|
||||
return None
|
||||
c = c.strip().lower()
|
||||
if c in CSS_COLOURS:
|
||||
return CSS_COLOURS[c]
|
||||
if re.match(r'^#?[0-9a-f]{3}([0-9a-f]{3})?$', c):
|
||||
if len(c) == 4:
|
||||
return c[0]+c[1]+c[1]+c[2]+c[2]+c[3]+c[3]
|
||||
return c
|
||||
raise ValueError('Unknown colour code: {}'.format(c))
|
35
make_all_metro_poly.py
Normal file
35
make_all_metro_poly.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import shapely.geometry
|
||||
import shapely.ops
|
||||
|
||||
from process_subways import download_cities
|
||||
|
||||
|
||||
def make_disjoint_metro_polygons():
|
||||
cities = download_cities()
|
||||
|
||||
polygons = []
|
||||
for c in cities:
|
||||
polygon = shapely.geometry.Polygon(
|
||||
[
|
||||
(c.bbox[1], c.bbox[0]),
|
||||
(c.bbox[1], c.bbox[2]),
|
||||
(c.bbox[3], c.bbox[2]),
|
||||
(c.bbox[3], c.bbox[0]),
|
||||
]
|
||||
)
|
||||
polygons.append(polygon)
|
||||
|
||||
union = shapely.ops.unary_union(polygons)
|
||||
|
||||
print("all metro")
|
||||
for i, polygon in enumerate(union, start=1):
|
||||
assert len(polygon.interiors) == 0
|
||||
print(i)
|
||||
for point in polygon.exterior.coords:
|
||||
print(" {lon} {lat}".format(lon=point[0], lat=point[1]))
|
||||
print("END")
|
||||
print("END")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
make_disjoint_metro_polygons()
|
49
mapsme_json_to_cities.py
Normal file
49
mapsme_json_to_cities.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
import argparse
|
||||
import json
|
||||
|
||||
from process_subways import download_cities
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arg_parser = argparse.ArgumentParser(
|
||||
description="""
|
||||
This script generates a list of good/all network names.
|
||||
It is used by subway render to generate the list of network at frontend.
|
||||
It uses two sources: a mapsme.json validator output with good networks, and
|
||||
a google spreadsheet with networks for the process_subways.download_cities()
|
||||
function.""",
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
'subway_json_file',
|
||||
type=argparse.FileType('r'),
|
||||
help="Validator output defined by -o option of process_subways.py script",
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
'--with-bad',
|
||||
action="store_true",
|
||||
help="Whether to include cities validation of which was failed",
|
||||
)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
with_bad = args.with_bad
|
||||
subway_json_file = args.subway_json_file
|
||||
subway_json = json.load(subway_json_file)
|
||||
|
||||
good_cities = set(
|
||||
n.get('network', n.get('title')) for n in subway_json['networks']
|
||||
)
|
||||
cities = download_cities()
|
||||
|
||||
lines = []
|
||||
for c in cities:
|
||||
if c.name in good_cities:
|
||||
lines.append(f"{c.name}, {c.country}")
|
||||
elif with_bad:
|
||||
lines.append(f"{c.name}, {c.country} (Bad)")
|
||||
|
||||
for line in sorted(lines):
|
||||
print(line)
|
413
process_subways.py
Executable file
413
process_subways.py
Executable file
|
@ -0,0 +1,413 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from processors import processor
|
||||
from subway_io import (
|
||||
dump_yaml,
|
||||
load_xml,
|
||||
make_geojson,
|
||||
read_recovery_data,
|
||||
write_recovery_data,
|
||||
)
|
||||
from subway_structure import (
|
||||
CriticalValidationError,
|
||||
download_cities,
|
||||
find_transfers,
|
||||
get_unused_entrances_geojson,
|
||||
MODES_OVERGROUND,
|
||||
MODES_RAPID,
|
||||
)
|
||||
|
||||
# Hack to always use IPv4.
|
||||
import socket
|
||||
|
||||
old_getaddrinfo = socket.getaddrinfo
|
||||
def new_getaddrinfo(*args, **kwargs):
|
||||
responses = old_getaddrinfo(*args, **kwargs)
|
||||
return [response
|
||||
for response in responses
|
||||
if response[0] == socket.AF_INET]
|
||||
socket.getaddrinfo = new_getaddrinfo
|
||||
|
||||
|
||||
def overpass_request(overground, overpass_api, bboxes):
|
||||
query = '[out:json][timeout:1000];('
|
||||
modes = MODES_OVERGROUND if overground else MODES_RAPID
|
||||
for bbox in bboxes:
|
||||
bbox_part = '({})'.format(','.join(str(coord) for coord in bbox))
|
||||
query += '('
|
||||
for mode in modes:
|
||||
query += 'rel[route="{}"]{};'.format(mode, bbox_part)
|
||||
query += ');'
|
||||
query += 'rel(br)[type=route_master];'
|
||||
if not overground:
|
||||
query += 'node[railway=subway_entrance]{};'.format(bbox_part)
|
||||
query += 'rel[public_transport=stop_area]{};'.format(bbox_part)
|
||||
query += (
|
||||
'rel(br)[type=public_transport][public_transport=stop_area_group];'
|
||||
)
|
||||
query += ');(._;>>;);out body center qt;'
|
||||
logging.debug('Query: %s', query)
|
||||
url = '{}?data={}'.format(overpass_api, urllib.parse.quote(query))
|
||||
response = urllib.request.urlopen(url, timeout=1000)
|
||||
if response.getcode() != 200:
|
||||
raise Exception(
|
||||
'Failed to query Overpass API: HTTP {}'.format(response.getcode())
|
||||
)
|
||||
return json.load(response)['elements']
|
||||
|
||||
|
||||
def multi_overpass(overground, overpass_api, bboxes):
|
||||
SLICE_SIZE = 10
|
||||
INTERREQUEST_WAIT = 5 # in seconds
|
||||
result = []
|
||||
for i in range(0, len(bboxes) + SLICE_SIZE - 1, SLICE_SIZE):
|
||||
if i > 0:
|
||||
time.sleep(INTERREQUEST_WAIT)
|
||||
result.extend(
|
||||
overpass_request(
|
||||
overground, overpass_api, bboxes[i : i + SLICE_SIZE]
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def slugify(name):
|
||||
return re.sub(r'[^a-z0-9_-]+', '', name.lower().replace(' ', '_'))
|
||||
|
||||
|
||||
def calculate_centers(elements):
|
||||
"""Adds 'center' key to each way/relation in elements,
|
||||
except for empty ways or relations.
|
||||
Relies on nodes-ways-relations order in the elements list.
|
||||
"""
|
||||
nodes = {} # id(int) => (lat, lon)
|
||||
ways = {} # id(int) => (lat, lon)
|
||||
relations = {} # id(int) => (lat, lon)
|
||||
empty_relations = set() # ids(int) of relations without members
|
||||
# or containing only empty relations
|
||||
|
||||
def calculate_way_center(el):
|
||||
# If element has been queried via overpass-api with 'out center;'
|
||||
# clause then ways already have 'center' attribute
|
||||
if 'center' in el:
|
||||
ways[el['id']] = (el['center']['lat'], el['center']['lon'])
|
||||
return
|
||||
center = [0, 0]
|
||||
count = 0
|
||||
for nd in el['nodes']:
|
||||
if nd in nodes:
|
||||
center[0] += nodes[nd][0]
|
||||
center[1] += nodes[nd][1]
|
||||
count += 1
|
||||
if count > 0:
|
||||
el['center'] = {'lat': center[0] / count, 'lon': center[1] / count}
|
||||
ways[el['id']] = (el['center']['lat'], el['center']['lon'])
|
||||
|
||||
def calculate_relation_center(el):
|
||||
# If element has been queried via overpass-api with 'out center;'
|
||||
# clause then some relations already have 'center' attribute
|
||||
if 'center' in el:
|
||||
relations[el['id']] = (el['center']['lat'], el['center']['lon'])
|
||||
return True
|
||||
center = [0, 0]
|
||||
count = 0
|
||||
for m in el.get('members', []):
|
||||
if m['type'] == 'relation' and m['ref'] not in relations:
|
||||
if m['ref'] in empty_relations:
|
||||
# Ignore empty child relations
|
||||
continue
|
||||
else:
|
||||
# Center of child relation is not known yet
|
||||
return False
|
||||
member_container = (
|
||||
nodes
|
||||
if m['type'] == 'node'
|
||||
else ways
|
||||
if m['type'] == 'way'
|
||||
else relations
|
||||
)
|
||||
if m['ref'] in member_container:
|
||||
center[0] += member_container[m['ref']][0]
|
||||
center[1] += member_container[m['ref']][1]
|
||||
count += 1
|
||||
if count == 0:
|
||||
empty_relations.add(el['id'])
|
||||
else:
|
||||
el['center'] = {'lat': center[0] / count, 'lon': center[1] / count}
|
||||
relations[el['id']] = (el['center']['lat'], el['center']['lon'])
|
||||
return True
|
||||
|
||||
relations_without_center = []
|
||||
|
||||
for el in elements:
|
||||
if el['type'] == 'node':
|
||||
nodes[el['id']] = (el['lat'], el['lon'])
|
||||
elif el['type'] == 'way':
|
||||
if 'nodes' in el:
|
||||
calculate_way_center(el)
|
||||
elif el['type'] == 'relation':
|
||||
if not calculate_relation_center(el):
|
||||
relations_without_center.append(el)
|
||||
|
||||
# Calculate centers for relations that have no one yet
|
||||
while relations_without_center:
|
||||
new_relations_without_center = []
|
||||
for rel in relations_without_center:
|
||||
if not calculate_relation_center(rel):
|
||||
new_relations_without_center.append(rel)
|
||||
if len(new_relations_without_center) == len(relations_without_center):
|
||||
break
|
||||
relations_without_center = new_relations_without_center
|
||||
|
||||
if relations_without_center:
|
||||
logging.error(
|
||||
"Cannot calculate center for the relations (%d in total): %s%s",
|
||||
len(relations_without_center),
|
||||
', '.join(str(rel['id']) for rel in relations_without_center[:20]),
|
||||
", ..." if len(relations_without_center) > 20 else "",
|
||||
)
|
||||
if empty_relations:
|
||||
logging.warning(
|
||||
"Empty relations (%d in total): %s%s",
|
||||
len(empty_relations),
|
||||
', '.join(str(x) for x in list(empty_relations)[:20]),
|
||||
", ..." if len(empty_relations) > 20 else "",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
'--source',
|
||||
help='File to write backup of OSM data, or to read data from',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-x', '--xml', help='OSM extract with routes, to read data from'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--overpass-api',
|
||||
default='http://overpass-api.de/api/interpreter',
|
||||
help="Overpass API URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-q',
|
||||
'--quiet',
|
||||
action='store_true',
|
||||
help='Show only warnings and errors',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--city', help='Validate only a single city or a country'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t',
|
||||
'--overground',
|
||||
action='store_true',
|
||||
help='Process overground transport instead of subways',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--entrances',
|
||||
type=argparse.FileType('w', encoding='utf-8'),
|
||||
help='Export unused subway entrances as GeoJSON here',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--log',
|
||||
type=argparse.FileType('w', encoding='utf-8'),
|
||||
help='Validation JSON file name',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
'--output',
|
||||
type=argparse.FileType('w', encoding='utf-8'),
|
||||
help='Processed metro systems output',
|
||||
)
|
||||
parser.add_argument('--cache', help='Cache file name for processed data')
|
||||
parser.add_argument(
|
||||
'-r', '--recovery-path', help='Cache file name for error recovery'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-d', '--dump', help='Make a YAML file for a city data'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-j', '--geojson', help='Make a GeoJSON file for a city data'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--crude',
|
||||
action='store_true',
|
||||
help='Do not use OSM railway geometry for GeoJSON',
|
||||
)
|
||||
options = parser.parse_args()
|
||||
|
||||
if options.quiet:
|
||||
log_level = logging.WARNING
|
||||
else:
|
||||
log_level = logging.INFO
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
datefmt='%H:%M:%S',
|
||||
format='%(asctime)s %(levelname)-7s %(message)s',
|
||||
)
|
||||
|
||||
# Downloading cities from Google Spreadsheets
|
||||
cities = download_cities(options.overground)
|
||||
if options.city:
|
||||
cities = [
|
||||
c
|
||||
for c in cities
|
||||
if c.name == options.city or c.country == options.city
|
||||
]
|
||||
if not cities:
|
||||
logging.error('No cities to process')
|
||||
sys.exit(2)
|
||||
|
||||
# Augment cities with recovery data
|
||||
recovery_data = None
|
||||
if options.recovery_path:
|
||||
recovery_data = read_recovery_data(options.recovery_path)
|
||||
for city in cities:
|
||||
city.recovery_data = recovery_data.get(city.name, None)
|
||||
|
||||
logging.info('Read %s metro networks', len(cities))
|
||||
|
||||
# Reading cached json, loading XML or querying Overpass API
|
||||
if options.source and os.path.exists(options.source):
|
||||
logging.info('Reading %s', options.source)
|
||||
with open(options.source, 'r') as f:
|
||||
osm = json.load(f)
|
||||
if 'elements' in osm:
|
||||
osm = osm['elements']
|
||||
calculate_centers(osm)
|
||||
elif options.xml:
|
||||
logging.info('Reading %s', options.xml)
|
||||
osm = load_xml(options.xml)
|
||||
calculate_centers(osm)
|
||||
if options.source:
|
||||
with open(options.source, 'w', encoding='utf-8') as f:
|
||||
json.dump(osm, f)
|
||||
else:
|
||||
if len(cities) > 10:
|
||||
logging.error(
|
||||
'Would not download that many cities from Overpass API, '
|
||||
'choose a smaller set'
|
||||
)
|
||||
sys.exit(3)
|
||||
bboxes = [c.bbox for c in cities]
|
||||
logging.info('Downloading data from Overpass API')
|
||||
osm = multi_overpass(options.overground, options.overpass_api, bboxes)
|
||||
calculate_centers(osm)
|
||||
if options.source:
|
||||
with open(options.source, 'w', encoding='utf-8') as f:
|
||||
json.dump(osm, f)
|
||||
logging.info('Downloaded %s elements, sorting by city', len(osm))
|
||||
|
||||
# Sorting elements by city and prepare a dict
|
||||
for el in osm:
|
||||
for c in cities:
|
||||
if c.contains(el):
|
||||
c.add(el)
|
||||
|
||||
logging.info('Building routes for each city')
|
||||
good_cities = []
|
||||
for c in cities:
|
||||
try:
|
||||
c.extract_routes()
|
||||
except CriticalValidationError as e:
|
||||
logging.error(
|
||||
"Critical validation error while processing %s: %s",
|
||||
c.name,
|
||||
str(e),
|
||||
)
|
||||
c.error(str(e))
|
||||
except AssertionError as e:
|
||||
logging.error(
|
||||
"Validation logic error while processing %s: %s",
|
||||
c.name,
|
||||
str(e),
|
||||
)
|
||||
c.error("Validation logic error: {}".format(str(e)))
|
||||
else:
|
||||
c.validate()
|
||||
if c.is_good():
|
||||
good_cities.append(c)
|
||||
|
||||
logging.info('Finding transfer stations')
|
||||
transfers = find_transfers(osm, cities)
|
||||
|
||||
good_city_names = set(c.name for c in good_cities)
|
||||
logging.info(
|
||||
'%s good cities: %s',
|
||||
len(good_city_names),
|
||||
', '.join(sorted(good_city_names)),
|
||||
)
|
||||
bad_city_names = set(c.name for c in cities) - good_city_names
|
||||
logging.info(
|
||||
'%s bad cities: %s',
|
||||
len(bad_city_names),
|
||||
', '.join(sorted(bad_city_names)),
|
||||
)
|
||||
|
||||
if options.recovery_path:
|
||||
write_recovery_data(options.recovery_path, recovery_data, cities)
|
||||
|
||||
if options.entrances:
|
||||
json.dump(get_unused_entrances_geojson(osm), options.entrances)
|
||||
|
||||
if options.dump:
|
||||
if os.path.isdir(options.dump):
|
||||
for c in cities:
|
||||
with open(
|
||||
os.path.join(options.dump, slugify(c.name) + '.yaml'),
|
||||
'w',
|
||||
encoding='utf-8',
|
||||
) as f:
|
||||
dump_yaml(c, f)
|
||||
elif len(cities) == 1:
|
||||
with open(options.dump, 'w', encoding='utf-8') as f:
|
||||
dump_yaml(cities[0], f)
|
||||
else:
|
||||
logging.error('Cannot dump %s cities at once', len(cities))
|
||||
|
||||
if options.geojson:
|
||||
if os.path.isdir(options.geojson):
|
||||
for c in cities:
|
||||
with open(
|
||||
os.path.join(
|
||||
options.geojson, slugify(c.name) + '.geojson'
|
||||
),
|
||||
'w',
|
||||
encoding='utf-8',
|
||||
) as f:
|
||||
json.dump(make_geojson(c, not options.crude), f)
|
||||
elif len(cities) == 1:
|
||||
with open(options.geojson, 'w', encoding='utf-8') as f:
|
||||
json.dump(make_geojson(cities[0], not options.crude), f)
|
||||
else:
|
||||
logging.error(
|
||||
'Cannot make a geojson of %s cities at once', len(cities)
|
||||
)
|
||||
|
||||
if options.log:
|
||||
res = []
|
||||
for c in cities:
|
||||
v = c.get_validation_result()
|
||||
v['slug'] = slugify(c.name)
|
||||
res.append(v)
|
||||
json.dump(res, options.log, indent=2, ensure_ascii=False)
|
||||
|
||||
if options.output:
|
||||
json.dump(
|
||||
processor.process(cities, transfers, options.cache),
|
||||
options.output,
|
||||
indent=1,
|
||||
ensure_ascii=False,
|
||||
)
|
2
processors/__init__.py
Normal file
2
processors/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Here you can change the processor
|
||||
from . import mapsme as processor
|
|
@ -1,71 +1,56 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
from subways.consts import DISPLACEMENT_TOLERANCE
|
||||
from subways.geom_utils import distance
|
||||
from subways.osm_element import el_center
|
||||
from subways.structure.station import Station
|
||||
from subways.types import IdT, LonLat, OsmElementT, TransfersT
|
||||
from ._common import (
|
||||
DEFAULT_AVE_VEHICLE_SPEED,
|
||||
DEFAULT_INTERVAL,
|
||||
format_colour,
|
||||
KMPH_TO_MPS,
|
||||
SPEED_ON_TRANSFER,
|
||||
TRANSFER_PENALTY,
|
||||
from subway_structure import (
|
||||
distance,
|
||||
el_center,
|
||||
Station,
|
||||
DISPLACEMENT_TOLERANCE,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
from subways.structure.stop_area import StopArea
|
||||
|
||||
|
||||
OSM_TYPES = {"n": (0, "node"), "w": (2, "way"), "r": (3, "relation")}
|
||||
OSM_TYPES = {'n': (0, 'node'), 'w': (2, 'way'), 'r': (3, 'relation')}
|
||||
ENTRANCE_PENALTY = 60 # seconds
|
||||
TRANSFER_PENALTY = 30 # seconds
|
||||
KMPH_TO_MPS = 1 / 3.6 # km/h to m/s conversion multiplier
|
||||
SPEED_TO_ENTRANCE = 5 * KMPH_TO_MPS # m/s
|
||||
|
||||
# (stoparea1_uid, stoparea2_uid) -> seconds; stoparea1_uid < stoparea2_uid
|
||||
TransferTimesT: TypeAlias = dict[tuple[int, int], int]
|
||||
SPEED_ON_TRANSFER = 3.5 * KMPH_TO_MPS # m/s
|
||||
SPEED_ON_LINE = 40 * KMPH_TO_MPS # m/s
|
||||
DEFAULT_INTERVAL = 2.5 # minutes
|
||||
|
||||
|
||||
def uid(elid: IdT, typ: str | None = None) -> int:
|
||||
def uid(elid, typ=None):
|
||||
t = elid[0]
|
||||
osm_id = int(elid[1:])
|
||||
if not typ:
|
||||
osm_id = (osm_id << 2) + OSM_TYPES[t][0]
|
||||
elif typ != t:
|
||||
raise Exception("Got {}, expected {}".format(elid, typ))
|
||||
raise Exception('Got {}, expected {}'.format(elid, typ))
|
||||
return osm_id << 1
|
||||
|
||||
|
||||
class DummyCache:
|
||||
"""This class may be used when you need to omit all cache processing"""
|
||||
|
||||
def __init__(self, cache_path: str, cities: list[City]) -> None:
|
||||
def __init__(self, cache_path, cities):
|
||||
pass
|
||||
|
||||
def __getattr__(self, name: str) -> Callable[..., None]:
|
||||
def __getattr__(self, name):
|
||||
"""This results in that a call to any method effectively does nothing
|
||||
and does not generate exceptions."""
|
||||
|
||||
def method(*args, **kwargs) -> None:
|
||||
def method(*args, **kwargs):
|
||||
return None
|
||||
|
||||
return method
|
||||
|
||||
|
||||
def if_object_is_used(method: Callable) -> Callable:
|
||||
def if_object_is_used(method):
|
||||
"""Decorator to skip method execution under certain condition.
|
||||
Relies on "is_used" object property."""
|
||||
|
||||
def inner(self, *args, **kwargs) -> Any:
|
||||
def inner(self, *args, **kwargs):
|
||||
if not self.is_used:
|
||||
return
|
||||
return method(self, *args, **kwargs)
|
||||
|
@ -74,10 +59,9 @@ def if_object_is_used(method: Callable) -> Callable:
|
|||
|
||||
|
||||
class MapsmeCache:
|
||||
def __init__(self, cache_path: str, cities: list[City]) -> None:
|
||||
def __init__(self, cache_path, cities):
|
||||
if not cache_path:
|
||||
# Cache is not used,
|
||||
# all actions with cache must be silently skipped
|
||||
# cache is not used, all actions with cache must be silently skipped
|
||||
self.is_used = False
|
||||
return
|
||||
self.cache_path = cache_path
|
||||
|
@ -85,7 +69,7 @@ class MapsmeCache:
|
|||
self.cache = {}
|
||||
if os.path.exists(cache_path):
|
||||
try:
|
||||
with open(cache_path, "r", encoding="utf-8") as f:
|
||||
with open(cache_path, 'r', encoding='utf-8') as f:
|
||||
self.cache = json.load(f)
|
||||
except json.decoder.JSONDecodeError:
|
||||
logging.warning(
|
||||
|
@ -97,16 +81,16 @@ class MapsmeCache:
|
|||
# One stoparea may participate in routes of different cities
|
||||
self.stop_cities = defaultdict(set) # stoparea id -> city names
|
||||
self.city_dict = {c.name: c for c in cities}
|
||||
self.good_city_names = {c.name for c in cities if c.is_good}
|
||||
self.good_city_names = {c.name for c in cities if c.is_good()}
|
||||
|
||||
def _is_cached_city_usable(self, city: City) -> bool:
|
||||
def _is_cached_city_usable(self, city):
|
||||
"""Check if cached stations still exist in osm data and
|
||||
not moved far away.
|
||||
"""
|
||||
city_cache_data = self.cache[city.name]
|
||||
for stoparea_id, cached_stoparea in city_cache_data["stops"].items():
|
||||
station_id = cached_stoparea["osm_type"][0] + str(
|
||||
cached_stoparea["osm_id"]
|
||||
for stoparea_id, cached_stoparea in city_cache_data['stops'].items():
|
||||
station_id = cached_stoparea['osm_type'][0] + str(
|
||||
cached_stoparea['osm_id']
|
||||
)
|
||||
city_station = city.elements.get(station_id)
|
||||
if not city_station or not Station.is_station(
|
||||
|
@ -114,9 +98,8 @@ class MapsmeCache:
|
|||
):
|
||||
return False
|
||||
station_coords = el_center(city_station)
|
||||
cached_station_coords = (
|
||||
cached_stoparea["lon"],
|
||||
cached_stoparea["lat"],
|
||||
cached_station_coords = tuple(
|
||||
cached_stoparea[coord] for coord in ('lon', 'lat')
|
||||
)
|
||||
displacement = distance(station_coords, cached_station_coords)
|
||||
if displacement > DISPLACEMENT_TOLERANCE:
|
||||
|
@ -125,100 +108,93 @@ class MapsmeCache:
|
|||
return True
|
||||
|
||||
@if_object_is_used
|
||||
def provide_stops_and_networks(
|
||||
self, stops: dict, networks: list[dict]
|
||||
) -> None:
|
||||
def provide_stops_and_networks(self, stops, networks):
|
||||
"""Put stops and networks for bad cities into containers
|
||||
passed as arguments."""
|
||||
for city in self.city_dict.values():
|
||||
if not city.is_good and city.name in self.cache:
|
||||
if not city.is_good() and city.name in self.cache:
|
||||
city_cached_data = self.cache[city.name]
|
||||
if self._is_cached_city_usable(city):
|
||||
stops.update(city_cached_data["stops"])
|
||||
networks.append(city_cached_data["network"])
|
||||
stops.update(city_cached_data['stops'])
|
||||
networks.append(city_cached_data['network'])
|
||||
logging.info("Taking %s from cache", city.name)
|
||||
self.recovered_city_names.add(city.name)
|
||||
|
||||
@if_object_is_used
|
||||
def provide_transfers(self, transfers: TransferTimesT) -> None:
|
||||
def provide_transfers(self, transfers):
|
||||
"""Add transfers from usable cached cities to 'transfers' dict
|
||||
passed as argument."""
|
||||
for city_name in self.recovered_city_names:
|
||||
city_cached_transfers = self.cache[city_name]["transfers"]
|
||||
city_cached_transfers = self.cache[city_name]['transfers']
|
||||
for stop1_uid, stop2_uid, transfer_time in city_cached_transfers:
|
||||
if (stop1_uid, stop2_uid) not in transfers:
|
||||
transfers[(stop1_uid, stop2_uid)] = transfer_time
|
||||
|
||||
@if_object_is_used
|
||||
def initialize_good_city(self, city_name: str, network: dict) -> None:
|
||||
def initialize_good_city(self, city_name, network):
|
||||
"""Create/replace one cache element with new data container.
|
||||
This should be done for each good city."""
|
||||
self.cache[city_name] = {
|
||||
"network": network,
|
||||
"stops": {}, # stoparea el_id -> jsonified stop data
|
||||
"transfers": [], # list of tuples
|
||||
# (stoparea1_uid, stoparea2_uid, time); uid1 < uid2
|
||||
'network': network,
|
||||
'stops': {}, # stoparea el_id -> jsonified stop data
|
||||
'transfers': [], # list of tuples (stoparea1_uid, stoparea2_uid, time); uid1 < uid2
|
||||
}
|
||||
|
||||
@if_object_is_used
|
||||
def link_stop_with_city(self, stoparea_id: IdT, city_name: str) -> None:
|
||||
def link_stop_with_city(self, stoparea_id, city_name):
|
||||
"""Remember that some stop_area is used in a city."""
|
||||
stoparea_uid = uid(stoparea_id)
|
||||
self.stop_cities[stoparea_uid].add(city_name)
|
||||
|
||||
@if_object_is_used
|
||||
def add_stop(self, stoparea_id: IdT, st: dict) -> None:
|
||||
def add_stop(self, stoparea_id, st):
|
||||
"""Add stoparea to the cache of each city the stoparea is in."""
|
||||
stoparea_uid = uid(stoparea_id)
|
||||
for city_name in self.stop_cities[stoparea_uid]:
|
||||
self.cache[city_name]["stops"][stoparea_id] = st
|
||||
self.cache[city_name]['stops'][stoparea_id] = st
|
||||
|
||||
@if_object_is_used
|
||||
def add_transfer(
|
||||
self, stoparea1_uid: int, stoparea2_uid: int, transfer_time: int
|
||||
) -> None:
|
||||
def add_transfer(self, stoparea1_uid, stoparea2_uid, transfer_time):
|
||||
"""If a transfer is inside a good city, add it to the city's cache."""
|
||||
for city_name in (
|
||||
self.good_city_names
|
||||
& self.stop_cities[stoparea1_uid]
|
||||
& self.stop_cities[stoparea2_uid]
|
||||
):
|
||||
self.cache[city_name]["transfers"].append(
|
||||
self.cache[city_name]['transfers'].append(
|
||||
(stoparea1_uid, stoparea2_uid, transfer_time)
|
||||
)
|
||||
|
||||
@if_object_is_used
|
||||
def save(self) -> None:
|
||||
def save(self):
|
||||
try:
|
||||
with open(self.cache_path, "w", encoding="utf-8") as f:
|
||||
with open(self.cache_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.cache, f, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logging.warning("Failed to save cache: %s", str(e))
|
||||
|
||||
|
||||
def transit_data_to_mapsme(
|
||||
cities: list[City], transfers: TransfersT, cache_path: str | None
|
||||
) -> dict:
|
||||
"""Generate all output and save to file.
|
||||
:param cities: List of City instances
|
||||
:param transfers: List of sets of StopArea.id
|
||||
:param cache_path: Path to json-file with good cities cache or None.
|
||||
def process(cities, transfers, cache_path):
|
||||
"""cities - list of City instances;
|
||||
transfers - list of sets of StopArea.id;
|
||||
cache_path - path to json-file with good cities cache or None.
|
||||
"""
|
||||
|
||||
def find_exits_for_platform(
|
||||
center: LonLat, nodes: list[OsmElementT]
|
||||
) -> list[OsmElementT]:
|
||||
def format_colour(c):
|
||||
return c[1:] if c else None
|
||||
|
||||
def find_exits_for_platform(center, nodes):
|
||||
exits = []
|
||||
min_distance = None
|
||||
for n in nodes:
|
||||
d = distance(center, (n["lon"], n["lat"]))
|
||||
d = distance(center, (n['lon'], n['lat']))
|
||||
if not min_distance:
|
||||
min_distance = d * 2 / 3
|
||||
elif d < min_distance:
|
||||
continue
|
||||
too_close = False
|
||||
for e in exits:
|
||||
d = distance((e["lon"], e["lat"]), (n["lon"], n["lat"]))
|
||||
d = distance((e['lon'], e['lat']), (n['lon'], n['lat']))
|
||||
if d < min_distance:
|
||||
too_close = True
|
||||
break
|
||||
|
@ -228,28 +204,28 @@ def transit_data_to_mapsme(
|
|||
|
||||
cache = MapsmeCache(cache_path, cities)
|
||||
|
||||
stop_areas: dict[IdT, StopArea] = {}
|
||||
stops: dict[IdT, dict] = {} # stoparea el_id -> stop jsonified data
|
||||
stop_areas = {} # stoparea el_id -> StopArea instance
|
||||
stops = {} # stoparea el_id -> stop jsonified data
|
||||
networks = []
|
||||
good_cities = [c for c in cities if c.is_good]
|
||||
good_cities = [c for c in cities if c.is_good()]
|
||||
platform_nodes = {}
|
||||
cache.provide_stops_and_networks(stops, networks)
|
||||
|
||||
for city in good_cities:
|
||||
network = {"network": city.name, "routes": [], "agency_id": city.id}
|
||||
network = {'network': city.name, 'routes': [], 'agency_id': city.id}
|
||||
cache.initialize_good_city(city.name, network)
|
||||
for route in city:
|
||||
routes = {
|
||||
"type": route.mode,
|
||||
"ref": route.ref,
|
||||
"name": route.name,
|
||||
"colour": format_colour(route.colour),
|
||||
"route_id": uid(route.id, "r"),
|
||||
"itineraries": [],
|
||||
'type': route.mode,
|
||||
'ref': route.ref,
|
||||
'name': route.name,
|
||||
'colour': format_colour(route.colour),
|
||||
'route_id': uid(route.id, 'r'),
|
||||
'itineraries': [],
|
||||
}
|
||||
if route.infill:
|
||||
routes["casing"] = routes["colour"]
|
||||
routes["colour"] = format_colour(route.infill)
|
||||
routes['casing'] = routes['colour']
|
||||
routes['colour'] = format_colour(route.infill)
|
||||
for i, variant in enumerate(route):
|
||||
itin = []
|
||||
for stop in variant:
|
||||
|
@ -258,45 +234,44 @@ def transit_data_to_mapsme(
|
|||
itin.append(
|
||||
[
|
||||
uid(stop.stoparea.id),
|
||||
round(stop.distance / DEFAULT_AVE_VEHICLE_SPEED),
|
||||
round(stop.distance / SPEED_ON_LINE),
|
||||
]
|
||||
)
|
||||
# Make exits from platform nodes,
|
||||
# if we don't have proper exits
|
||||
# Make exits from platform nodes, if we don't have proper exits
|
||||
if (
|
||||
len(stop.stoparea.entrances) + len(stop.stoparea.exits)
|
||||
== 0
|
||||
):
|
||||
for pl in stop.stoparea.platforms:
|
||||
pl_el = city.elements[pl]
|
||||
if pl_el["type"] == "node":
|
||||
if pl_el['type'] == 'node':
|
||||
pl_nodes = [pl_el]
|
||||
elif pl_el["type"] == "way":
|
||||
elif pl_el['type'] == 'way':
|
||||
pl_nodes = [
|
||||
city.elements.get("n{}".format(n))
|
||||
for n in pl_el["nodes"]
|
||||
city.elements.get('n{}'.format(n))
|
||||
for n in pl_el['nodes']
|
||||
]
|
||||
else:
|
||||
pl_nodes = []
|
||||
for m in pl_el["members"]:
|
||||
if m["type"] == "way":
|
||||
for m in pl_el['members']:
|
||||
if m['type'] == 'way':
|
||||
if (
|
||||
"{}{}".format(
|
||||
m["type"][0], m["ref"]
|
||||
'{}{}'.format(
|
||||
m['type'][0], m['ref']
|
||||
)
|
||||
in city.elements
|
||||
):
|
||||
pl_nodes.extend(
|
||||
[
|
||||
city.elements.get(
|
||||
"n{}".format(n)
|
||||
'n{}'.format(n)
|
||||
)
|
||||
for n in city.elements[
|
||||
"{}{}".format(
|
||||
m["type"][0],
|
||||
m["ref"],
|
||||
'{}{}'.format(
|
||||
m['type'][0],
|
||||
m['ref'],
|
||||
)
|
||||
]["nodes"]
|
||||
]['nodes']
|
||||
]
|
||||
)
|
||||
pl_nodes = [n for n in pl_nodes if n]
|
||||
|
@ -304,39 +279,39 @@ def transit_data_to_mapsme(
|
|||
stop.stoparea.centers[pl], pl_nodes
|
||||
)
|
||||
|
||||
routes["itineraries"].append(
|
||||
routes['itineraries'].append(
|
||||
{
|
||||
"stops": itin,
|
||||
"interval": round(
|
||||
variant.interval or DEFAULT_INTERVAL
|
||||
'stops': itin,
|
||||
'interval': round(
|
||||
(variant.interval or DEFAULT_INTERVAL) * 60
|
||||
),
|
||||
}
|
||||
)
|
||||
network["routes"].append(routes)
|
||||
network['routes'].append(routes)
|
||||
networks.append(network)
|
||||
|
||||
for stop_id, stop in stop_areas.items():
|
||||
st = {
|
||||
"name": stop.name,
|
||||
"int_name": stop.int_name,
|
||||
"lat": stop.center[1],
|
||||
"lon": stop.center[0],
|
||||
"osm_type": OSM_TYPES[stop.station.id[0]][1],
|
||||
"osm_id": int(stop.station.id[1:]),
|
||||
"id": uid(stop.id),
|
||||
"entrances": [],
|
||||
"exits": [],
|
||||
'name': stop.name,
|
||||
'int_name': stop.int_name,
|
||||
'lat': stop.center[1],
|
||||
'lon': stop.center[0],
|
||||
'osm_type': OSM_TYPES[stop.station.id[0]][1],
|
||||
'osm_id': int(stop.station.id[1:]),
|
||||
'id': uid(stop.id),
|
||||
'entrances': [],
|
||||
'exits': [],
|
||||
}
|
||||
for e_l, k in ((stop.entrances, "entrances"), (stop.exits, "exits")):
|
||||
for e_l, k in ((stop.entrances, 'entrances'), (stop.exits, 'exits')):
|
||||
for e in e_l:
|
||||
if e[0] == "n":
|
||||
if e[0] == 'n':
|
||||
st[k].append(
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": int(e[1:]),
|
||||
"lon": stop.centers[e][0],
|
||||
"lat": stop.centers[e][1],
|
||||
"distance": ENTRANCE_PENALTY
|
||||
'osm_type': 'node',
|
||||
'osm_id': int(e[1:]),
|
||||
'lon': stop.centers[e][0],
|
||||
'lat': stop.centers[e][1],
|
||||
'distance': ENTRANCE_PENALTY
|
||||
+ round(
|
||||
distance(stop.centers[e], stop.center)
|
||||
/ SPEED_TO_ENTRANCE
|
||||
|
@ -347,53 +322,52 @@ def transit_data_to_mapsme(
|
|||
if stop.platforms:
|
||||
for pl in stop.platforms:
|
||||
for n in platform_nodes[pl]:
|
||||
for k in ("entrances", "exits"):
|
||||
for k in ('entrances', 'exits'):
|
||||
st[k].append(
|
||||
{
|
||||
"osm_type": n["type"],
|
||||
"osm_id": n["id"],
|
||||
"lon": n["lon"],
|
||||
"lat": n["lat"],
|
||||
"distance": ENTRANCE_PENALTY
|
||||
'osm_type': n['type'],
|
||||
'osm_id': n['id'],
|
||||
'lon': n['lon'],
|
||||
'lat': n['lat'],
|
||||
'distance': ENTRANCE_PENALTY
|
||||
+ round(
|
||||
distance(
|
||||
(n["lon"], n["lat"]), stop.center
|
||||
(n['lon'], n['lat']), stop.center
|
||||
)
|
||||
/ SPEED_TO_ENTRANCE
|
||||
),
|
||||
}
|
||||
)
|
||||
else:
|
||||
for k in ("entrances", "exits"):
|
||||
for k in ('entrances', 'exits'):
|
||||
st[k].append(
|
||||
{
|
||||
"osm_type": OSM_TYPES[stop.station.id[0]][1],
|
||||
"osm_id": int(stop.station.id[1:]),
|
||||
"lon": stop.centers[stop.id][0],
|
||||
"lat": stop.centers[stop.id][1],
|
||||
"distance": 60,
|
||||
'osm_type': OSM_TYPES[stop.station.id[0]][1],
|
||||
'osm_id': int(stop.station.id[1:]),
|
||||
'lon': stop.centers[stop.id][0],
|
||||
'lat': stop.centers[stop.id][1],
|
||||
'distance': 60,
|
||||
}
|
||||
)
|
||||
|
||||
stops[stop_id] = st
|
||||
cache.add_stop(stop_id, st)
|
||||
|
||||
pairwise_transfers: TransferTimesT = {}
|
||||
for stoparea_id_set in transfers:
|
||||
stoparea_ids = list(stoparea_id_set)
|
||||
for i_first in range(len(stoparea_ids) - 1):
|
||||
for i_second in range(i_first + 1, len(stoparea_ids)):
|
||||
stoparea1_id = stoparea_ids[i_first]
|
||||
stoparea2_id = stoparea_ids[i_second]
|
||||
if stoparea1_id in stops and stoparea2_id in stops:
|
||||
uid1 = uid(stoparea1_id)
|
||||
uid2 = uid(stoparea2_id)
|
||||
pairwise_transfers = (
|
||||
{}
|
||||
) # (stoparea1_uid, stoparea2_uid) -> time; uid1 < uid2
|
||||
for t_set in transfers:
|
||||
t = list(t_set)
|
||||
for t_first in range(len(t) - 1):
|
||||
for t_second in range(t_first + 1, len(t)):
|
||||
stoparea1 = t[t_first]
|
||||
stoparea2 = t[t_second]
|
||||
if stoparea1.id in stops and stoparea2.id in stops:
|
||||
uid1 = uid(stoparea1.id)
|
||||
uid2 = uid(stoparea2.id)
|
||||
uid1, uid2 = sorted([uid1, uid2])
|
||||
transfer_time = TRANSFER_PENALTY + round(
|
||||
distance(
|
||||
stop_areas[stoparea1_id].center,
|
||||
stop_areas[stoparea2_id].center,
|
||||
)
|
||||
distance(stoparea1.center, stoparea2.center)
|
||||
/ SPEED_ON_TRANSFER
|
||||
)
|
||||
pairwise_transfers[(uid1, uid2)] = transfer_time
|
||||
|
@ -402,40 +376,14 @@ def transit_data_to_mapsme(
|
|||
cache.provide_transfers(pairwise_transfers)
|
||||
cache.save()
|
||||
|
||||
pairwise_transfers_list = [
|
||||
pairwise_transfers = [
|
||||
(stop1_uid, stop2_uid, transfer_time)
|
||||
for (stop1_uid, stop2_uid), transfer_time in pairwise_transfers.items()
|
||||
]
|
||||
|
||||
result = {
|
||||
"stops": list(stops.values()),
|
||||
"transfers": pairwise_transfers_list,
|
||||
"networks": networks,
|
||||
'stops': list(stops.values()),
|
||||
'transfers': pairwise_transfers,
|
||||
'networks': networks,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def process(
|
||||
cities: list[City],
|
||||
transfers: TransfersT,
|
||||
filename: str,
|
||||
cache_path: str | None,
|
||||
) -> None:
|
||||
"""Generate all output and save to file.
|
||||
:param cities: list of City instances
|
||||
:param transfers: all collected transfers in the world
|
||||
:param filename: Path to file to save the result
|
||||
:param cache_path: Path to json-file with good cities cache or None.
|
||||
"""
|
||||
if not filename.lower().endswith("json"):
|
||||
filename = f"{filename}.json"
|
||||
|
||||
mapsme_transit = transit_data_to_mapsme(cities, transfers, cache_path)
|
||||
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
mapsme_transit,
|
||||
f,
|
||||
indent=1,
|
||||
ensure_ascii=False,
|
||||
)
|
10
requirements.txt
Normal file
10
requirements.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
Flask==2.0.1
|
||||
kdtree==0.16
|
||||
lxml==4.6.3
|
||||
Shapely==1.7.1
|
||||
## The following requirements were added by pip freeze:
|
||||
click==8.0.1
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.1
|
||||
MarkupSafe==2.0.1
|
||||
Werkzeug==2.0.1
|
16
scripts/build_city.sh
Executable file
16
scripts/build_city.sh
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
set -e -u
|
||||
[ $# -lt 1 ] && echo "Usage: $0 <path_to_o5m> [<city_name> [<bbox>]]" && exit 1
|
||||
|
||||
export OSMCTOOLS="${OSMCTOOLS:-$HOME/osm/planet}"
|
||||
export DUMP=html
|
||||
export JSON=html
|
||||
if [ -n "${2-}" ]; then
|
||||
export CITY="$2"
|
||||
fi
|
||||
if [ -n "${3-}" ]; then
|
||||
export BBOX="$3"
|
||||
elif [ -n "${CITY-}" ]; then
|
||||
export BBOX="$(python3 -c 'import subway_structure; c = [x for x in subway_structure.download_cities() if x.name == "'"$CITY"'"]; print("{1},{0},{3},{2}".format(*c[0].bbox))')" || true
|
||||
fi
|
||||
"$(dirname "$0")/process_subways.sh" "$1"
|
16
scripts/build_trams.sh
Executable file
16
scripts/build_trams.sh
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
set -e -u
|
||||
[ $# -lt 1 ] && echo "Usage: $0 <path_to_o5m> [<city_name> [<bbox>]]" && exit 1
|
||||
|
||||
export OSMCTOOLS="${OSMCTOOLS:-$HOME/osm/planet}"
|
||||
export DUMP=html
|
||||
export JSON=html
|
||||
if [ -n "${2-}" ]; then
|
||||
export CITY="$2"
|
||||
fi
|
||||
if [ -n "${3-}" ]; then
|
||||
export BBOX="$3"
|
||||
elif [ -n "${CITY-}" ]; then
|
||||
export BBOX="$(python3 -c 'import subway_structure; c = [x for x in subway_structure.download_cities(True) if x.name == "'"$CITY"'"]; print("{1},{0},{3},{2}".format(*c[0].bbox))')" || true
|
||||
fi
|
||||
"$(dirname "$0")/process_trams.sh" "$1"
|
6
scripts/download_all_subways.sh
Executable file
6
scripts/download_all_subways.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
# Still times out, do not use unless you want to be blocked for some hours on Overpass API
|
||||
TIMEOUT=2000
|
||||
QUERY='[out:json][timeout:'$TIMEOUT'];(rel["route"="subway"];rel["route"="light_rail"];rel["public_transport"="stop_area"];rel["public_transport"="stop_area_group"];node["station"="subway"];node["station"="light_rail"];node["railway"="subway_entrance"];);(._;>;);out body center qt;'
|
||||
http http://overpass-api.de/api/interpreter "data==$QUERY" --timeout $TIMEOUT > subways-$(date +%y%m%d).json
|
||||
http https://overpass-api.de/api/status | grep available
|
6
scripts/filter_all_subways.sh
Executable file
6
scripts/filter_all_subways.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
[ $# -lt 1 ] && echo 'Usage: $0 <planet.o5m> [<path_to_osmfilter>] [<output_file>]' && exit 1
|
||||
OSMFILTER=${2-./osmfilter}
|
||||
QRELATIONS="route=subway =light_rail =monorail route_master=subway =light_rail =monorail public_transport=stop_area =stop_area_group"
|
||||
QNODES="station=subway =light_rail =monorail railway=subway_entrance subway=yes light_rail=yes monorail=yes"
|
||||
"$OSMFILTER" "$1" --keep= --keep-relations="$QRELATIONS" --keep-nodes="$QNODES" --drop-author -o="${3:-subways-$(date +%y%m%d).osm}"
|
|
@ -1,276 +0,0 @@
|
|||
import argparse
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from subways import processors
|
||||
from subways.overpass import multi_overpass
|
||||
from subways.subway_io import (
|
||||
dump_yaml,
|
||||
load_xml,
|
||||
make_geojson,
|
||||
read_recovery_data,
|
||||
write_recovery_data,
|
||||
)
|
||||
from subways.structure.city import (
|
||||
find_transfers,
|
||||
get_unused_subway_entrances_geojson,
|
||||
)
|
||||
from subways.validation import (
|
||||
add_osm_elements_to_cities,
|
||||
BAD_MARK,
|
||||
calculate_centers,
|
||||
DEFAULT_CITIES_INFO_URL,
|
||||
prepare_cities,
|
||||
validate_cities,
|
||||
)
|
||||
|
||||
|
||||
def slugify(name: str) -> str:
|
||||
return re.sub(r"[^a-z0-9_-]+", "", name.lower().replace(" ", "_"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--cities-info-url",
|
||||
default=DEFAULT_CITIES_INFO_URL,
|
||||
help=(
|
||||
"URL of CSV file with reference information about rapid transit "
|
||||
"networks. file:// protocol is also supported."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--source",
|
||||
help="File to write backup of OSM data, or to read data from",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-x", "--xml", help="OSM extract with routes, to read data from"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overpass-api",
|
||||
default="http://overpass-api.de/api/interpreter",
|
||||
help="Overpass API URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Show only warnings and errors",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--city", help="Validate only a single city or a country"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--overground",
|
||||
action="store_true",
|
||||
help="Process overground transport instead of subways",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--entrances",
|
||||
type=argparse.FileType("w", encoding="utf-8"),
|
||||
help="Export unused subway entrances as GeoJSON here",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--log",
|
||||
type=argparse.FileType("w", encoding="utf-8"),
|
||||
help="Validation JSON file name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dump-city-list",
|
||||
type=argparse.FileType("w", encoding="utf-8"),
|
||||
help=(
|
||||
"Dump sorted list of all city names, possibly with "
|
||||
f"{BAD_MARK} mark"
|
||||
),
|
||||
)
|
||||
|
||||
for processor_name, processor in inspect.getmembers(
|
||||
processors, inspect.ismodule
|
||||
):
|
||||
if not processor_name.startswith("_"):
|
||||
parser.add_argument(
|
||||
f"--output-{processor_name}",
|
||||
help=(
|
||||
"Processed metro systems output filename "
|
||||
f"in {processor_name.upper()} format"
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument("--cache", help="Cache file name for processed data")
|
||||
parser.add_argument(
|
||||
"-r", "--recovery-path", help="Cache file name for error recovery"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--dump", help="Make a YAML file for a city data"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j", "--geojson", help="Make a GeoJSON file for a city data"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--crude",
|
||||
action="store_true",
|
||||
help="Do not use OSM railway geometry for GeoJSON",
|
||||
)
|
||||
options = parser.parse_args()
|
||||
|
||||
if options.quiet:
|
||||
log_level = logging.WARNING
|
||||
else:
|
||||
log_level = logging.INFO
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
datefmt="%H:%M:%S",
|
||||
format="%(asctime)s %(levelname)-7s %(message)s",
|
||||
)
|
||||
|
||||
cities = prepare_cities(options.cities_info_url, options.overground)
|
||||
if options.city:
|
||||
cities = [
|
||||
c
|
||||
for c in cities
|
||||
if c.name == options.city or c.country == options.city
|
||||
]
|
||||
if not cities:
|
||||
logging.error("No cities to process")
|
||||
sys.exit(2)
|
||||
|
||||
# Augment cities with recovery data
|
||||
recovery_data = None
|
||||
if options.recovery_path:
|
||||
recovery_data = read_recovery_data(options.recovery_path)
|
||||
for city in cities:
|
||||
city.recovery_data = recovery_data.get(city.name, None)
|
||||
|
||||
logging.info("Read %s metro networks", len(cities))
|
||||
|
||||
# Reading cached json, loading XML or querying Overpass API
|
||||
if options.source and os.path.exists(options.source):
|
||||
logging.info("Reading %s", options.source)
|
||||
with open(options.source, "r") as f:
|
||||
osm = json.load(f)
|
||||
if "elements" in osm:
|
||||
osm = osm["elements"]
|
||||
calculate_centers(osm)
|
||||
elif options.xml:
|
||||
logging.info("Reading %s", options.xml)
|
||||
osm = load_xml(options.xml)
|
||||
calculate_centers(osm)
|
||||
if options.source:
|
||||
with open(options.source, "w", encoding="utf-8") as f:
|
||||
json.dump(osm, f)
|
||||
else:
|
||||
if len(cities) > 10:
|
||||
logging.error(
|
||||
"Would not download that many cities from Overpass API, "
|
||||
"choose a smaller set"
|
||||
)
|
||||
sys.exit(3)
|
||||
bboxes = [c.bbox for c in cities]
|
||||
logging.info("Downloading data from Overpass API")
|
||||
osm = multi_overpass(options.overground, options.overpass_api, bboxes)
|
||||
calculate_centers(osm)
|
||||
if options.source:
|
||||
with open(options.source, "w", encoding="utf-8") as f:
|
||||
json.dump(osm, f)
|
||||
logging.info("Downloaded %s elements", len(osm))
|
||||
|
||||
logging.info("Sorting elements by city")
|
||||
add_osm_elements_to_cities(osm, cities)
|
||||
|
||||
logging.info("Building routes for each city")
|
||||
good_cities = validate_cities(cities)
|
||||
|
||||
logging.info("Finding transfer stations")
|
||||
transfers = find_transfers(osm, good_cities)
|
||||
|
||||
good_city_names = set(c.name for c in good_cities)
|
||||
logging.info(
|
||||
"%s good cities: %s",
|
||||
len(good_city_names),
|
||||
", ".join(sorted(good_city_names)),
|
||||
)
|
||||
bad_city_names = set(c.name for c in cities) - good_city_names
|
||||
logging.info(
|
||||
"%s bad cities: %s",
|
||||
len(bad_city_names),
|
||||
", ".join(sorted(bad_city_names)),
|
||||
)
|
||||
|
||||
if options.dump_city_list:
|
||||
lines = sorted(
|
||||
f"{city.name}, {city.country}"
|
||||
f"{' ' + BAD_MARK if city.name in bad_city_names else ''}\n"
|
||||
for city in cities
|
||||
)
|
||||
options.dump_city_list.writelines(lines)
|
||||
|
||||
if options.recovery_path:
|
||||
write_recovery_data(options.recovery_path, recovery_data, cities)
|
||||
|
||||
if options.entrances:
|
||||
json.dump(get_unused_subway_entrances_geojson(osm), options.entrances)
|
||||
|
||||
if options.dump:
|
||||
if os.path.isdir(options.dump):
|
||||
for c in cities:
|
||||
with open(
|
||||
os.path.join(options.dump, slugify(c.name) + ".yaml"),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
dump_yaml(c, f)
|
||||
elif len(cities) == 1:
|
||||
with open(options.dump, "w", encoding="utf-8") as f:
|
||||
dump_yaml(cities[0], f)
|
||||
else:
|
||||
logging.error("Cannot dump %s cities at once", len(cities))
|
||||
|
||||
if options.geojson:
|
||||
if os.path.isdir(options.geojson):
|
||||
for c in cities:
|
||||
with open(
|
||||
os.path.join(
|
||||
options.geojson, slugify(c.name) + ".geojson"
|
||||
),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
json.dump(make_geojson(c, not options.crude), f)
|
||||
elif len(cities) == 1:
|
||||
with open(options.geojson, "w", encoding="utf-8") as f:
|
||||
json.dump(make_geojson(cities[0], not options.crude), f)
|
||||
else:
|
||||
logging.error(
|
||||
"Cannot make a geojson of %s cities at once", len(cities)
|
||||
)
|
||||
|
||||
if options.log:
|
||||
res = []
|
||||
for c in cities:
|
||||
v = c.get_validation_result()
|
||||
v["slug"] = slugify(c.name)
|
||||
res.append(v)
|
||||
json.dump(res, options.log, indent=2, ensure_ascii=False)
|
||||
|
||||
for processor_name, processor in inspect.getmembers(
|
||||
processors, inspect.ismodule
|
||||
):
|
||||
option_name = f"output_{processor_name}"
|
||||
|
||||
if not getattr(options, option_name, None):
|
||||
continue
|
||||
|
||||
filename = getattr(options, option_name)
|
||||
processor.process(cities, transfers, filename, options.cache)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -6,13 +6,13 @@ if [ $# -gt 0 -a \( "${1-}" = "-h" -o "${1-}" = '--help' \) ]; then
|
|||
This script updates a planet or an extract, processes metro networks in it
|
||||
and produces a set of HTML files with validation results.
|
||||
|
||||
Usage: $0 [<local/planet.osm.pbf | http://mirror.osm.ru/planet.osm.pbf>]
|
||||
Usage: $0 [<local/planet.{pbf,o5m} | http://mirror.osm.ru/planet.{pbf,o5m}>]
|
||||
|
||||
In more detail, the script does the following:
|
||||
- If \$PLANET is a remote file, downloads it.
|
||||
- If \$BBOX variable is set, proceeds with this setting for the planet clipping. Otherwise uses \$POLY:
|
||||
unless \$POLY variable is set and the file exists, generates a *.poly file with union of bboxes of all cities having metro.
|
||||
- Makes an extract of the \$PLANET using the *.poly file.
|
||||
- Makes a *.o5m extract of the \$PLANET using the *.poly file.
|
||||
- Updates the extract.
|
||||
- Filters railway infrastructure from the extract.
|
||||
- Uses filtered file for validation and generates a bunch of output files.
|
||||
|
@ -28,60 +28,46 @@ variable is not defined or is null, otherwise they are kept.
|
|||
The \$PLANET file from remote URL is saved to a tempfile and is removed at the end.
|
||||
|
||||
Environment variable reference:
|
||||
- PLANET: path to a local or remote pbf source file (the entire planet or an extract)
|
||||
- PLANET_METRO: path to a local pbf file with extract of cities having metro
|
||||
- PLANET: path to a local or remote o5m or pbf source file (the entire planet or an extract)
|
||||
- PLANET_METRO: path to a local o5m file with extract of cities having metro
|
||||
It's used instead of \$PLANET if exists otherwise it's created first
|
||||
- PLANET_UPDATE_SERVER: server to get replication data from. Defaults to https://planet.openstreetmap.org/replication/
|
||||
- CITIES_INFO_URL: http(s) or "file://" URL to a CSV file with reference information about rapid transit systems. A default value is hammered into python code.
|
||||
- CITY: name of a city/country to process
|
||||
- BBOX: bounding box of an extract; x1,y1,x2,y2. Has precedence over \$POLY
|
||||
- POLY: *.poly file with [multi]polygon comprising cities with metro
|
||||
If neither \$BBOX nor \$POLY is set, then \$POLY is generated
|
||||
- SKIP_PLANET_UPDATE: skip \$PLANET_METRO file update. Any non-empty string is True
|
||||
- SKIP_PLANET_UPDATE: skip \$PLANET file update. Any non-empty string is True
|
||||
- SKIP_FILTERING: skip filtering railway data. Any non-empty string is True
|
||||
- FILTERED_DATA: path to filtered data. Defaults to \$TMPDIR/subways.osm
|
||||
- MAPSME: file name for maps.me json output
|
||||
- GTFS: file name for GTFS output
|
||||
- DUMP: directory/file name to dump YAML city data. Do not set to omit dump
|
||||
- GEOJSON: directory/file name to dump GeoJSON data. Do not set to omit dump
|
||||
- ELEMENTS_CACHE: file name to elements cache. Allows OSM xml processing phase
|
||||
- CITY_CACHE: json file with good cities obtained on previous validation runs
|
||||
- RECOVERY_PATH: file with some data collected at previous validation runs that
|
||||
may help to recover some simple validation errors
|
||||
- OSMCTOOLS: path to osmconvert and osmupdate binaries
|
||||
- PYTHON: python 3 executable
|
||||
- GIT_PULL: set to 1 to update the scripts
|
||||
- TMPDIR: path to temporary files
|
||||
- HTML_DIR: target path for generated HTML files
|
||||
- DUMP_CITY_LIST: file name to save sorted list of cities, with [bad] mark for bad cities
|
||||
- SERVER: server name and path to upload HTML files (e.g. ilya@osmz.ru:/var/www/)
|
||||
- SERVER_KEY: rsa key to supply for uploading the files
|
||||
- REMOVE_HTML: set to 1 to remove \$HTML_DIR after uploading
|
||||
- QUIET: set to any non-empty value to use WARNING log level in process_subways.py. Default is INFO.
|
||||
EOF
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
function activate_venv_at_path() {
|
||||
path=$1
|
||||
|
||||
if [ ! -d "$path/".venv ]; then
|
||||
"${PYTHON:-python3}" -m venv "$path"/.venv
|
||||
fi
|
||||
|
||||
source "$path"/.venv/bin/activate
|
||||
|
||||
if [ -f "$path"/requirements.txt ]; then
|
||||
pip install --upgrade pip
|
||||
pip install -r "$path"/requirements.txt
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function check_osmium() {
|
||||
if ! which osmium > /dev/null; then
|
||||
echo "Please install osmium-tool"
|
||||
exit 1
|
||||
function check_osmctools() {
|
||||
OSMCTOOLS="${OSMCTOOLS:-$HOME/osmctools}"
|
||||
if [ ! -f "$OSMCTOOLS/osmupdate" ]; then
|
||||
if which osmupdate > /dev/null; then
|
||||
OSMCTOOLS="$(dirname "$(which osmupdate)")"
|
||||
else
|
||||
echo "Please compile osmctools to $OSMCTOOLS"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -101,39 +87,36 @@ function check_poly() {
|
|||
|
||||
if [ -z "${POLY-}" -o ! -f "${POLY-}" ]; then
|
||||
POLY=${POLY:-$(mktemp "$TMPDIR/all-metro.XXXXXXXX.poly")}
|
||||
activate_venv_at_path "$SUBWAYS_REPO_PATH/tools/make_poly"
|
||||
python "$SUBWAYS_REPO_PATH"/tools/make_poly/make_all_metro_poly.py \
|
||||
${CITIES_INFO_URL:+--cities-info-url "$CITIES_INFO_URL"} > "$POLY"
|
||||
deactivate
|
||||
if [ -n "$("$PYTHON" -c "import shapely" 2>&1)" ]; then
|
||||
"$PYTHON" -m pip install shapely
|
||||
fi
|
||||
"$PYTHON" "$SUBWAYS_PATH"/make_all_metro_poly.py > "$POLY"
|
||||
fi
|
||||
fi
|
||||
POLY_CHECKED=1
|
||||
fi
|
||||
}
|
||||
|
||||
# "readlink -f" echoes canonicalized absolute path to a file/directory
|
||||
SUBWAYS_REPO_PATH="$(readlink -f $(dirname "$0")/..)"
|
||||
|
||||
if [ ! -f "$SUBWAYS_REPO_PATH/scripts/process_subways.py" ]; then
|
||||
echo "Please clone the subways repo to $SUBWAYS_REPO_PATH"
|
||||
PYTHON=${PYTHON:-python3}
|
||||
# This will fail if there is no python
|
||||
"$PYTHON" --version > /dev/null
|
||||
|
||||
SUBWAYS_PATH="$(dirname "$0")/.."
|
||||
if [ ! -f "$SUBWAYS_PATH/process_subways.py" ]; then
|
||||
echo "Please clone the subways repo to $SUBWAYS_PATH"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Contains 'subways' dir and is required by the main validator python script
|
||||
# as well as by some tools
|
||||
export PYTHONPATH="$SUBWAYS_REPO_PATH"
|
||||
TMPDIR="${TMPDIR:-$SUBWAYS_PATH}"
|
||||
mkdir -p "$TMPDIR"
|
||||
|
||||
# Downloading the latest version of the subways script
|
||||
if [ -n "${GIT_PULL-}" ]; then (
|
||||
pushd "$SUBWAYS_REPO_PATH"
|
||||
cd "$SUBWAYS_PATH"
|
||||
git pull origin master
|
||||
popd
|
||||
) fi
|
||||
|
||||
|
||||
TMPDIR="${TMPDIR:-"$SUBWAYS_REPO_PATH"}"
|
||||
mkdir -p "$TMPDIR"
|
||||
|
||||
if [ -z "${FILTERED_DATA-}" ]; then
|
||||
FILTERED_DATA="$TMPDIR/subways.osm"
|
||||
NEED_TO_REMOVE_FILTERED_DATA=1
|
||||
|
@ -157,7 +140,7 @@ if [ -n "${NEED_FILTER-}" ]; then
|
|||
fi
|
||||
|
||||
if [ ! -f "${PLANET_METRO-}" ]; then
|
||||
check_osmium
|
||||
check_osmctools
|
||||
check_poly
|
||||
|
||||
PLANET="${PLANET:-${1-}}"
|
||||
|
@ -180,7 +163,7 @@ if [ -n "${NEED_FILTER-}" ]; then
|
|||
fi
|
||||
|
||||
if [ -z "${PLANET_METRO-}" ]; then
|
||||
PLANET_METRO=$(mktemp "$TMPDIR/planet-metro.XXXXXXXX.osm.pbf")
|
||||
PLANET_METRO=$(mktemp "$TMPDIR/planet-metro.XXXXXXXX.o5m")
|
||||
NEED_TO_REMOVE_PLANET_METRO=1
|
||||
fi
|
||||
|
||||
|
@ -189,8 +172,10 @@ if [ -n "${NEED_FILTER-}" ]; then
|
|||
exit 6
|
||||
fi
|
||||
|
||||
osmium extract "$PLANET" \
|
||||
${BBOX:+"--bbox=$BBOX"} ${POLY:+"--polygon=$POLY"} -O -o "$PLANET_METRO"
|
||||
mkdir -p $TMPDIR/osmconvert_temp/
|
||||
"$OSMCTOOLS"/osmconvert "$PLANET" \
|
||||
-t=$TMPDIR/osmconvert_temp/temp \
|
||||
${BBOX:+"-b=$BBOX"} ${POLY:+"-B=$POLY"} -o="$PLANET_METRO"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
@ -202,38 +187,40 @@ fi
|
|||
|
||||
# If there's no need to filter, then update is also unnecessary
|
||||
if [ -z "${SKIP_PLANET_UPDATE-}" -a -n "${NEED_FILTER-}" ]; then
|
||||
check_osmium
|
||||
check_osmctools
|
||||
check_poly
|
||||
PLANET_UPDATE_SERVER=${PLANET_UPDATE_SERVER:-https://planet.openstreetmap.org/replication/hour/}
|
||||
PLANET_UPDATE_SERVER=${PLANET_UPDATE_SERVER:-https://planet.openstreetmap.org/replication/}
|
||||
PLANET_METRO_ABS="$(cd "$(dirname "$PLANET_METRO")"; pwd)/$(basename "$PLANET_METRO")"
|
||||
PLANET_METRO_ABS_NEW="$PLANET_METRO_ABS.new.osm.pbf"
|
||||
mkdir -p $TMPDIR/osmupdate_temp/
|
||||
|
||||
activate_venv_at_path "$SUBWAYS_REPO_PATH/scripts"
|
||||
OSMUPDATE_ERRORS=$(pyosmium-up-to-date \
|
||||
"$PLANET_METRO_ABS" \
|
||||
--server $PLANET_UPDATE_SERVER \
|
||||
--tmpdir $TMPDIR/osmupdate_temp/temp \
|
||||
-o "$PLANET_METRO_ABS_NEW" 2>&1 || :)
|
||||
deactivate
|
||||
pushd $TMPDIR/osmupdate_temp/
|
||||
export PATH="$PATH:$OSMCTOOLS"
|
||||
OSMUPDATE_ERRORS=$(osmupdate --drop-author --out-o5m ${BBOX:+"-b=$BBOX"} \
|
||||
${POLY:+"-B=$POLY"} "$PLANET_METRO_ABS" \
|
||||
--base-url=$PLANET_UPDATE_SERVER \
|
||||
--tempfiles=$TMPDIR/osmupdate_temp/temp \
|
||||
"$PLANET_METRO_ABS.new.o5m" 2>&1 || :)
|
||||
if [ -n "$OSMUPDATE_ERRORS" ]; then
|
||||
echo "osmupdate failed: $OSMUPDATE_ERRORS"
|
||||
exit 7
|
||||
fi
|
||||
|
||||
# Since updating adds things outside the area, trim those again.
|
||||
osmium extract "$PLANET_METRO_ABS_NEW" \
|
||||
${BBOX:+"--bbox=$BBOX"} ${POLY:+"--polygon=$POLY"} -O -o "$PLANET_METRO_ABS"
|
||||
rm -f "$PLANET_METRO_ABS_NEW"
|
||||
popd
|
||||
mv "$PLANET_METRO_ABS.new.o5m" "$PLANET_METRO_ABS"
|
||||
fi
|
||||
|
||||
# Filtering planet-metro
|
||||
|
||||
if [ -n "${NEED_FILTER-}" ]; then
|
||||
check_osmium
|
||||
QRELATIONS="r/route,route_master=subway,light_rail,monorail,train r/public_transport=stop_area,stop_area_group"
|
||||
QNODES="n/railway=station,subway_entrance,train_station_entrance n/station=subway,light_rail,monorail n/subway=yes n/light_rail=yes n/monorail=yes n/train=yes"
|
||||
osmium tags-filter "$PLANET_METRO" $QRELATIONS $QNODES -o "$FILTERED_DATA" -O
|
||||
check_osmctools
|
||||
mkdir -p $TMPDIR/osmfilter_temp/
|
||||
QRELATIONS="route=subway =light_rail =monorail =train route_master=subway =light_rail =monorail =train public_transport=stop_area =stop_area_group"
|
||||
QNODES="railway=station station=subway =light_rail =monorail railway=subway_entrance subway=yes light_rail=yes monorail=yes train=yes"
|
||||
"$OSMCTOOLS/osmfilter" "$PLANET_METRO" \
|
||||
--keep= \
|
||||
--keep-relations="$QRELATIONS" \
|
||||
--keep-nodes="$QNODES" \
|
||||
--drop-author \
|
||||
-t=$TMPDIR/osmfilter_temp/temp \
|
||||
-o="$FILTERED_DATA"
|
||||
fi
|
||||
|
||||
if [ -n "${NEED_TO_REMOVE_PLANET_METRO-}" ]; then
|
||||
|
@ -248,28 +235,14 @@ if [ -n "${DUMP-}" ]; then
|
|||
mkdir -p "$DUMP"
|
||||
fi
|
||||
|
||||
if [ -n "${DUMP-}" ]; then
|
||||
mkdir -p "$DUMP"
|
||||
fi
|
||||
|
||||
VALIDATION="$TMPDIR/validation.json"
|
||||
|
||||
activate_venv_at_path "$SUBWAYS_REPO_PATH/scripts"
|
||||
python "$SUBWAYS_REPO_PATH/scripts/process_subways.py" ${QUIET:+-q} \
|
||||
"$PYTHON" "$SUBWAYS_PATH/process_subways.py" -q \
|
||||
-x "$FILTERED_DATA" -l "$VALIDATION" \
|
||||
${CITIES_INFO_URL:+--cities-info-url "$CITIES_INFO_URL"} \
|
||||
${MAPSME:+--output-mapsme "$MAPSME"} \
|
||||
${FMK:+--output-fmk "$FMK"} \
|
||||
${GTFS:+--output-gtfs "$GTFS"} \
|
||||
${CITY:+-c "$CITY"} \
|
||||
${DUMP:+-d "$DUMP"} \
|
||||
${GEOJSON:+-j "$GEOJSON"} \
|
||||
${DUMP_CITY_LIST:+--dump-city-list "$DUMP_CITY_LIST"} \
|
||||
${MAPSME:+-o "$MAPSME"} \
|
||||
${CITY:+-c "$CITY"} ${DUMP:+-d "$DUMP"} ${GEOJSON:+-j "$GEOJSON"} \
|
||||
${ELEMENTS_CACHE:+-i "$ELEMENTS_CACHE"} \
|
||||
${CITY_CACHE:+--cache "$CITY_CACHE"} \
|
||||
${RECOVERY_PATH:+-r "$RECOVERY_PATH"}
|
||||
deactivate
|
||||
|
||||
|
||||
if [ -n "${NEED_TO_REMOVE_FILTERED_DATA-}" ]; then
|
||||
rm "$FILTERED_DATA"
|
||||
|
@ -278,18 +251,13 @@ fi
|
|||
# Preparing HTML files
|
||||
|
||||
if [ -z "${HTML_DIR-}" ]; then
|
||||
HTML_DIR="$SUBWAYS_REPO_PATH/html"
|
||||
HTML_DIR="$SUBWAYS_PATH/html"
|
||||
REMOVE_HTML=1
|
||||
fi
|
||||
|
||||
mkdir -p $HTML_DIR
|
||||
rm -f "$HTML_DIR"/*.html
|
||||
|
||||
activate_venv_at_path "$SUBWAYS_REPO_PATH/tools/v2h"
|
||||
python "$SUBWAYS_REPO_PATH/tools/v2h/validation_to_html.py" \
|
||||
${CITIES_INFO_URL:+--cities-info-url "$CITIES_INFO_URL"} \
|
||||
"$VALIDATION" "$HTML_DIR"
|
||||
deactivate
|
||||
"$PYTHON" "$SUBWAYS_PATH/validation_to_html.py" "$VALIDATION" "$HTML_DIR"
|
||||
|
||||
# Uploading files to the server
|
||||
|
||||
|
|
94
scripts/process_trams.sh
Executable file
94
scripts/process_trams.sh
Executable file
|
@ -0,0 +1,94 @@
|
|||
#!/bin/bash
|
||||
set -e -u
|
||||
|
||||
if [ $# -lt 1 -a -z "${PLANET-}" ]; then
|
||||
echo "This script updates a planet or an extract, processes tram networks in it"
|
||||
echo "and produses a set of HTML files with validation results."
|
||||
echo
|
||||
echo "Usage: $0 <planet.o5m>"
|
||||
echo
|
||||
echo "Variable reference:"
|
||||
echo "- PLANET: path for the source o5m file (the entire planet or an extract)"
|
||||
echo "- CITY: name of a city to process"
|
||||
echo "- BBOX: bounding box of an extract; x1,y1,x2,y2"
|
||||
echo "- DUMP: file name to dump city data"
|
||||
echo "- MAPSME: file name for maps.me json output"
|
||||
echo "- OSMCTOOLS: path to osmconvert and osmupdate binaries"
|
||||
echo "- PYTHON: python 3 executable"
|
||||
echo "- GIT_PULL: set to 1 to update the scripts"
|
||||
echo "- TMPDIR: path to temporary files"
|
||||
echo "- HTML_DIR: target path for generated HTML files"
|
||||
echo "- SERVER: server name and path to upload HTML files (e.g. ilya@osmz.ru:/var/www/)"
|
||||
echo "- SERVER_KEY: rsa key to supply for uploading the files"
|
||||
echo "- REMOVE_HTML: set to 1 to remove HTML_DIR after uploading"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -n "${WHAT-}" ] && echo WHAT
|
||||
|
||||
PLANET="${PLANET:-${1-}}"
|
||||
[ ! -f "$PLANET" ] && echo "Cannot find planet file $PLANET" && exit 2
|
||||
OSMCTOOLS="${OSMCTOOLS:-$HOME/osmctools}"
|
||||
if [ ! -f "$OSMCTOOLS/osmupdate" ]; then
|
||||
if which osmupdate > /dev/null; then
|
||||
OSMCTOOLS="$(dirname "$(which osmupdate)")"
|
||||
else
|
||||
echo "Please compile osmctools to $OSMCTOOLS"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
PYTHON=${PYTHON:-python3}
|
||||
# This will fail if there is no python
|
||||
"$PYTHON" --version > /dev/null
|
||||
SUBWAYS_PATH="$(dirname "$0")/.."
|
||||
[ ! -f "$SUBWAYS_PATH/process_subways.py" ] && echo "Please clone the subways repo to $SUBWAYS_PATH" && exit 2
|
||||
TMPDIR="${TMPDIR:-$SUBWAYS_PATH}"
|
||||
|
||||
# Downloading the latest version of the subways script
|
||||
|
||||
|
||||
if [ -n "${GIT_PULL-}" ]; then (
|
||||
cd "$SUBWAYS_PATH"
|
||||
git pull origin master
|
||||
) fi
|
||||
|
||||
|
||||
# Updating the planet file
|
||||
|
||||
PLANET_ABS="$(cd "$(dirname "$PLANET")"; pwd)/$(basename "$PLANET")"
|
||||
(
|
||||
cd "$OSMCTOOLS" # osmupdate requires osmconvert in a current directory
|
||||
./osmupdate --drop-author --out-o5m "$PLANET_ABS" ${BBOX+"-b=$BBOX"} "$PLANET_ABS.new.o5m" && mv "$PLANET_ABS.new.o5m" "$PLANET_ABS" || true
|
||||
)
|
||||
|
||||
# Filtering it
|
||||
|
||||
FILTERED_DATA="$TMPDIR/subways.osm"
|
||||
QRELATIONS="route=tram route_master=tram public_transport=stop_area =stop_area_group"
|
||||
QNODES="railway=tram_stop railway=subway_entrance tram=yes"
|
||||
"$OSMCTOOLS/osmfilter" "$PLANET" --keep= --keep-relations="$QRELATIONS" --keep-nodes="$QNODES" --drop-author "-o=$FILTERED_DATA"
|
||||
|
||||
# Running the validation
|
||||
|
||||
VALIDATION="$TMPDIR/validation.json"
|
||||
"$PYTHON" "$SUBWAYS_PATH/process_subways.py" -t -q -x "$FILTERED_DATA" -l "$VALIDATION" ${MAPSME+-o "$MAPSME"} ${CITY+-c "$CITY"} ${DUMP+-d "$DUMP"} ${JSON+-j "$JSON"}
|
||||
rm "$FILTERED_DATA"
|
||||
|
||||
# Preparing HTML files
|
||||
|
||||
if [ -z "${HTML_DIR-}" ]; then
|
||||
HTML_DIR="$SUBWAYS_PATH/html"
|
||||
REMOVE_HTML=1
|
||||
fi
|
||||
|
||||
mkdir -p $HTML_DIR
|
||||
rm -f "$HTML_DIR"/*.html
|
||||
"$PYTHON" "$SUBWAYS_PATH/validation_to_html.py" "$VALIDATION" "$HTML_DIR"
|
||||
rm "$VALIDATION"
|
||||
|
||||
# Uploading files to the server
|
||||
|
||||
if [ -n "${SERVER-}" ]; then
|
||||
scp -q ${SERVER_KEY+-i "$SERVER_KEY"} "$HTML_DIR"/* "$SERVER"
|
||||
[ -n "${REMOVE_HTML-}" ] && rm -r "$HTML_DIR"
|
||||
fi
|
|
@ -1,2 +0,0 @@
|
|||
osmium
|
||||
-r ../subways/requirements.txt
|
241
stop_areas/make_stop_areas.py
Executable file
241
stop_areas/make_stop_areas.py
Executable file
|
@ -0,0 +1,241 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import codecs
|
||||
from lxml import etree
|
||||
import sys
|
||||
import kdtree
|
||||
import math
|
||||
import re
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
QUERY = """
|
||||
[out:json][timeout:250][bbox:{{bbox}}];
|
||||
(
|
||||
relation["route"="subway"];<<;
|
||||
relation["route"="light_rail"];<<;
|
||||
relation["public_transport"="stop_area"];<<;
|
||||
way["station"="subway"];
|
||||
way["station"="light_rail"];
|
||||
node["railway"="station"]["subway"="yes"];
|
||||
node["railway"="station"]["light_rail"="yes"];
|
||||
node["station"="subway"];
|
||||
node["station"="light_rail"];
|
||||
node["railway"="subway_entrance"];
|
||||
node["public_transport"]["subway"="yes"];
|
||||
node["public_transport"]["light_rail"="yes"];
|
||||
);
|
||||
(._;>;);
|
||||
out meta center qt;
|
||||
"""
|
||||
|
||||
|
||||
def el_id(el):
|
||||
return el['type'][0] + str(el.get('id', el.get('ref', '')))
|
||||
|
||||
|
||||
class StationWrapper:
|
||||
def __init__(self, st):
|
||||
if 'center' in st:
|
||||
self.coords = (st['center']['lon'], st['center']['lat'])
|
||||
elif 'lon' in st:
|
||||
self.coords = (st['lon'], st['lat'])
|
||||
else:
|
||||
raise Exception('Coordinates not found for station {}'.format(st))
|
||||
self.station = st
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.coords[i]
|
||||
|
||||
def distance(self, other):
|
||||
"""Calculate distance in meters."""
|
||||
dx = math.radians(self[0] - other['lon']) * math.cos(
|
||||
0.5 * math.radians(self[1] + other['lat'])
|
||||
)
|
||||
dy = math.radians(self[1] - other['lat'])
|
||||
return 6378137 * math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
|
||||
def overpass_request(bbox):
|
||||
url = 'http://overpass-api.de/api/interpreter?data={}'.format(
|
||||
urllib.parse.quote(QUERY.replace('{{bbox}}', bbox))
|
||||
)
|
||||
response = urllib.request.urlopen(url, timeout=1000)
|
||||
if response.getcode() != 200:
|
||||
raise Exception(
|
||||
'Failed to query Overpass API: HTTP {}'.format(response.getcode())
|
||||
)
|
||||
reader = codecs.getreader('utf-8')
|
||||
return json.load(reader(response))['elements']
|
||||
|
||||
|
||||
def add_stop_areas(src):
|
||||
if not src:
|
||||
raise Exception('Empty dataset provided to add_stop_areas')
|
||||
|
||||
# Add station=* tags to stations in subway and light_rail routes
|
||||
stations = {}
|
||||
for el in src:
|
||||
if 'tags' in el and el['tags'].get('railway', None) == 'station':
|
||||
stations[el_id(el)] = el
|
||||
|
||||
for el in src:
|
||||
if (
|
||||
el['type'] == 'relation'
|
||||
and 'tags' in el
|
||||
and el['tags'].get('route', None) in ('subway', 'light_rail')
|
||||
):
|
||||
for m in el['members']:
|
||||
st = stations.get(el_id(m), None)
|
||||
if st and 'station' not in st['tags']:
|
||||
st['tags']['station'] = el['tags']['route']
|
||||
st['modified'] = True
|
||||
|
||||
# Create a kd-tree out of subway stations
|
||||
stations = kdtree.create(dimensions=2)
|
||||
for el in src:
|
||||
if 'tags' in el and el['tags'].get('station', None) in (
|
||||
'subway',
|
||||
'light_rail',
|
||||
):
|
||||
stations.add(StationWrapper(el))
|
||||
|
||||
if stations.is_leaf:
|
||||
raise Exception('No stations found')
|
||||
|
||||
# Populate a list of nearby subway exits and platforms for each station
|
||||
MAX_DISTANCE = 300 # meters
|
||||
stop_areas = {}
|
||||
for el in src:
|
||||
if 'tags' not in el:
|
||||
continue
|
||||
if 'station' in el['tags']:
|
||||
continue
|
||||
if el['tags'].get('railway', None) not in (
|
||||
'subway_entrance',
|
||||
'platform',
|
||||
) and el['tags'].get('public_transport', None) not in (
|
||||
'platform',
|
||||
'stop_position',
|
||||
):
|
||||
continue
|
||||
coords = el.get('center', el)
|
||||
station = stations.search_nn((coords['lon'], coords['lat']))[0].data
|
||||
if station.distance(coords) < MAX_DISTANCE:
|
||||
k = (
|
||||
station.station['id'],
|
||||
station.station['tags'].get('name', 'station_with_no_name'),
|
||||
)
|
||||
# Disregard exits and platforms that are differently named
|
||||
if el['tags'].get('name', k[1]) == k[1]:
|
||||
if k not in stop_areas:
|
||||
stop_areas[k] = {el_id(station.station): station.station}
|
||||
stop_areas[k][el_id(el)] = el
|
||||
|
||||
# Find existing stop_area relations for stations and remove these stations
|
||||
for el in src:
|
||||
if (
|
||||
el['type'] == 'relation'
|
||||
and el['tags'].get('public_transport', None) == 'stop_area'
|
||||
):
|
||||
found = False
|
||||
for m in el['members']:
|
||||
if found:
|
||||
break
|
||||
for st in stop_areas:
|
||||
if el_id(m) in stop_areas[st]:
|
||||
del stop_areas[st]
|
||||
found = True
|
||||
break
|
||||
|
||||
# Create OSM XML for new stop_area relations
|
||||
root = etree.Element('osm', version='0.6')
|
||||
rid = -1
|
||||
for st, members in stop_areas.items():
|
||||
rel = etree.SubElement(root, 'relation', id=str(rid))
|
||||
rid -= 1
|
||||
etree.SubElement(rel, 'tag', k='type', v='public_transport')
|
||||
etree.SubElement(rel, 'tag', k='public_transport', v='stop_area')
|
||||
etree.SubElement(rel, 'tag', k='name', v=st[1])
|
||||
for m in members.values():
|
||||
if (
|
||||
m['tags'].get(
|
||||
'railway', m['tags'].get('public_transport', None)
|
||||
)
|
||||
== 'platform'
|
||||
):
|
||||
role = 'platform'
|
||||
elif m['tags'].get('public_transport', None) == 'stop_position':
|
||||
role = 'stop'
|
||||
else:
|
||||
role = ''
|
||||
etree.SubElement(
|
||||
rel, 'member', ref=str(m['id']), type=m['type'], role=role
|
||||
)
|
||||
|
||||
# Add all downloaded elements
|
||||
for el in src:
|
||||
obj = etree.SubElement(root, el['type'])
|
||||
for a in (
|
||||
'id',
|
||||
'type',
|
||||
'user',
|
||||
'uid',
|
||||
'version',
|
||||
'changeset',
|
||||
'timestamp',
|
||||
'lat',
|
||||
'lon',
|
||||
):
|
||||
if a in el:
|
||||
obj.set(a, str(el[a]))
|
||||
if 'modified' in el:
|
||||
obj.set('action', 'modify')
|
||||
if 'tags' in el:
|
||||
for k, v in el['tags'].items():
|
||||
etree.SubElement(obj, 'tag', k=k, v=v)
|
||||
if 'members' in el:
|
||||
for m in el['members']:
|
||||
etree.SubElement(
|
||||
obj,
|
||||
'member',
|
||||
ref=str(m['ref']),
|
||||
type=m['type'],
|
||||
role=m.get('role', ''),
|
||||
)
|
||||
if 'nodes' in el:
|
||||
for n in el['nodes']:
|
||||
etree.SubElement(obj, 'nd', ref=str(n))
|
||||
|
||||
return etree.tostring(root, pretty_print=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
'Read a JSON from Overpass and output JOSM OSM XML with added stop_area relations'
|
||||
)
|
||||
print(
|
||||
'Usage: {} {{<export.json>|<bbox>}} [output.osm]'.format(
|
||||
sys.argv[0]
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if re.match(r'^[-0-9.,]+$', sys.argv[1]):
|
||||
src = overpass_request(sys.argv[1])
|
||||
else:
|
||||
with open(sys.argv[1], 'r') as f:
|
||||
src = json.load(f)['elements']
|
||||
|
||||
result = add_stop_areas(src)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print(result.decode('utf-8'))
|
||||
else:
|
||||
with open(sys.argv[2], 'wb') as f:
|
||||
f.write(result)
|
219
stop_areas/make_tram_areas.py
Executable file
219
stop_areas/make_tram_areas.py
Executable file
|
@ -0,0 +1,219 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import codecs
|
||||
from lxml import etree
|
||||
import sys
|
||||
import kdtree
|
||||
import math
|
||||
import re
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
QUERY = """
|
||||
[out:json][timeout:250][bbox:{{bbox}}];
|
||||
(
|
||||
relation["route"="tram"];
|
||||
relation["public_transport"="stop_area"];
|
||||
nwr["railway"="tram_stop"];
|
||||
);
|
||||
(._;>>;);
|
||||
out meta center qt;
|
||||
"""
|
||||
|
||||
|
||||
def el_id(el):
|
||||
return el['type'][0] + str(el.get('id', el.get('ref', '')))
|
||||
|
||||
|
||||
class StationWrapper:
|
||||
def __init__(self, st):
|
||||
if 'center' in st:
|
||||
self.coords = (st['center']['lon'], st['center']['lat'])
|
||||
elif 'lon' in st:
|
||||
self.coords = (st['lon'], st['lat'])
|
||||
else:
|
||||
raise Exception('Coordinates not found for station {}'.format(st))
|
||||
self.station = st
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.coords[i]
|
||||
|
||||
def distance(self, other):
|
||||
"""Calculate distance in meters."""
|
||||
dx = math.radians(self[0] - other['lon']) * math.cos(
|
||||
0.5 * math.radians(self[1] + other['lat'])
|
||||
)
|
||||
dy = math.radians(self[1] - other['lat'])
|
||||
return 6378137 * math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
|
||||
def overpass_request(bbox):
|
||||
url = 'http://overpass-api.de/api/interpreter?data={}'.format(
|
||||
urllib.parse.quote(QUERY.replace('{{bbox}}', bbox))
|
||||
)
|
||||
response = urllib.request.urlopen(url, timeout=1000)
|
||||
if response.getcode() != 200:
|
||||
raise Exception(
|
||||
'Failed to query Overpass API: HTTP {}'.format(response.getcode())
|
||||
)
|
||||
reader = codecs.getreader('utf-8')
|
||||
return json.load(reader(response))['elements']
|
||||
|
||||
|
||||
def is_part_of_stop(tags):
|
||||
if tags.get('public_transport') in ('platform', 'stop_position'):
|
||||
return True
|
||||
if tags.get('railway') == 'platform':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_stop_areas(src):
|
||||
if not src:
|
||||
raise Exception('Empty dataset provided to add_stop_areas')
|
||||
|
||||
# Create a kd-tree out of tram stations
|
||||
stations = kdtree.create(dimensions=2)
|
||||
for el in src:
|
||||
if 'tags' in el and el['tags'].get('railway') == 'tram_stop':
|
||||
stations.add(StationWrapper(el))
|
||||
|
||||
if stations.is_leaf:
|
||||
raise Exception('No stations found')
|
||||
|
||||
elements = {}
|
||||
for el in src:
|
||||
if el.get('tags'):
|
||||
elements[el_id(el)] = el
|
||||
|
||||
# Populate a list of nearby subway exits and platforms for each station
|
||||
MAX_DISTANCE = 50 # meters
|
||||
stop_areas = {}
|
||||
for el in src:
|
||||
# Only tram routes
|
||||
if (
|
||||
'tags' not in el
|
||||
or el['type'] != 'relation'
|
||||
or el['tags'].get('route') != 'tram'
|
||||
):
|
||||
continue
|
||||
for m in el['members']:
|
||||
if el_id(m) not in elements:
|
||||
continue
|
||||
pel = elements[el_id(m)]
|
||||
if not is_part_of_stop(pel['tags']):
|
||||
continue
|
||||
if pel['tags'].get('railway') == 'tram_stop':
|
||||
continue
|
||||
coords = pel.get('center', pel)
|
||||
station = stations.search_nn(
|
||||
(coords['lon'], coords['lat'])
|
||||
)[0].data
|
||||
if station.distance(coords) < MAX_DISTANCE:
|
||||
k = (
|
||||
station.station['id'],
|
||||
station.station['tags'].get('name', None),
|
||||
)
|
||||
if k not in stop_areas:
|
||||
stop_areas[k] = {el_id(station.station): station.station}
|
||||
stop_areas[k][el_id(m)] = pel
|
||||
|
||||
# Find existing stop_area relations for stations and remove these stations
|
||||
for el in src:
|
||||
if (
|
||||
el['type'] == 'relation'
|
||||
and el['tags'].get('public_transport', None) == 'stop_area'
|
||||
):
|
||||
found = False
|
||||
for m in el['members']:
|
||||
if found:
|
||||
break
|
||||
for st in stop_areas:
|
||||
if el_id(m) in stop_areas[st]:
|
||||
del stop_areas[st]
|
||||
found = True
|
||||
break
|
||||
|
||||
# Create OSM XML for new stop_area relations
|
||||
root = etree.Element('osm', version='0.6')
|
||||
rid = -1
|
||||
for st, members in stop_areas.items():
|
||||
rel = etree.SubElement(root, 'relation', id=str(rid))
|
||||
rid -= 1
|
||||
etree.SubElement(rel, 'tag', k='type', v='public_transport')
|
||||
etree.SubElement(rel, 'tag', k='public_transport', v='stop_area')
|
||||
if st[1]:
|
||||
etree.SubElement(rel, 'tag', k='name', v=st[1])
|
||||
for m in members.values():
|
||||
etree.SubElement(
|
||||
rel, 'member', ref=str(m['id']), type=m['type'], role=''
|
||||
)
|
||||
|
||||
# Add all downloaded elements
|
||||
for el in src:
|
||||
obj = etree.SubElement(root, el['type'])
|
||||
for a in (
|
||||
'id',
|
||||
'type',
|
||||
'user',
|
||||
'uid',
|
||||
'version',
|
||||
'changeset',
|
||||
'timestamp',
|
||||
'lat',
|
||||
'lon',
|
||||
):
|
||||
if a in el:
|
||||
obj.set(a, str(el[a]))
|
||||
if 'modified' in el:
|
||||
obj.set('action', 'modify')
|
||||
if 'tags' in el:
|
||||
for k, v in el['tags'].items():
|
||||
etree.SubElement(obj, 'tag', k=k, v=v)
|
||||
if 'members' in el:
|
||||
for m in el['members']:
|
||||
etree.SubElement(
|
||||
obj,
|
||||
'member',
|
||||
ref=str(m['ref']),
|
||||
type=m['type'],
|
||||
role=m.get('role', ''),
|
||||
)
|
||||
if 'nodes' in el:
|
||||
for n in el['nodes']:
|
||||
etree.SubElement(obj, 'nd', ref=str(n))
|
||||
|
||||
return etree.tostring(root, pretty_print=True, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
'Read a JSON from Overpass and output JOSM OSM XML '
|
||||
'with added stop_area relations'
|
||||
)
|
||||
print(
|
||||
'Usage: {} {{<export.json>|<bbox>}} [output.osm]'.format(
|
||||
sys.argv[0]
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if re.match(r'^[-0-9.,]+$', sys.argv[1]):
|
||||
bbox = sys.argv[1].split(',')
|
||||
src = overpass_request(','.join([bbox[i] for i in (1, 0, 3, 2)]))
|
||||
else:
|
||||
with open(sys.argv[1], 'r') as f:
|
||||
src = json.load(f)['elements']
|
||||
|
||||
result = add_stop_areas(src)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print(result.decode('utf-8'))
|
||||
else:
|
||||
with open(sys.argv[2], 'wb') as f:
|
||||
f.write(result)
|
28
stop_areas/serve.py
Executable file
28
stop_areas/serve.py
Executable file
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env python3
|
||||
from flask import Flask, request, make_response, render_template
|
||||
from make_stop_areas import add_stop_areas, overpass_request
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def form():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/process', methods=['GET'])
|
||||
def convert():
|
||||
src = overpass_request(request.args.get('bbox'))
|
||||
if not src:
|
||||
return 'No data from overpass, sorry.'
|
||||
result = add_stop_areas(src)
|
||||
response = make_response(result)
|
||||
response.headers['Content-Disposition'] = (
|
||||
'attachment; filename="stop_areas.osm"'
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
|
@ -1,55 +1,44 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
from io import BufferedIOBase
|
||||
from typing import Any, TextIO
|
||||
|
||||
from subways.types import OsmElementT
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
from subways.structure.stop_area import StopArea
|
||||
|
||||
|
||||
def load_xml(f: BufferedIOBase | str) -> list[OsmElementT]:
|
||||
def load_xml(f):
|
||||
try:
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
elements: list[OsmElementT] = []
|
||||
elements = []
|
||||
|
||||
for event, element in etree.iterparse(f):
|
||||
if element.tag in ("node", "way", "relation"):
|
||||
el = {"type": element.tag, "id": int(element.get("id"))}
|
||||
if element.tag == "node":
|
||||
for n in ("lat", "lon"):
|
||||
if element.tag in ('node', 'way', 'relation'):
|
||||
el = {'type': element.tag, 'id': int(element.get('id'))}
|
||||
if element.tag == 'node':
|
||||
for n in ('lat', 'lon'):
|
||||
el[n] = float(element.get(n))
|
||||
tags = {}
|
||||
nd = []
|
||||
members = []
|
||||
for sub in element:
|
||||
if sub.tag == "tag":
|
||||
tags[sub.get("k")] = sub.get("v")
|
||||
elif sub.tag == "nd":
|
||||
nd.append(int(sub.get("ref")))
|
||||
elif sub.tag == "member":
|
||||
if sub.tag == 'tag':
|
||||
tags[sub.get('k')] = sub.get('v')
|
||||
elif sub.tag == 'nd':
|
||||
nd.append(int(sub.get('ref')))
|
||||
elif sub.tag == 'member':
|
||||
members.append(
|
||||
{
|
||||
"type": sub.get("type"),
|
||||
"ref": int(sub.get("ref")),
|
||||
"role": sub.get("role", ""),
|
||||
'type': sub.get('type'),
|
||||
'ref': int(sub.get('ref')),
|
||||
'role': sub.get('role', ''),
|
||||
}
|
||||
)
|
||||
if tags:
|
||||
el["tags"] = tags
|
||||
el['tags'] = tags
|
||||
if nd:
|
||||
el["nodes"] = nd
|
||||
el['nodes'] = nd
|
||||
if members:
|
||||
el["members"] = members
|
||||
el['members'] = members
|
||||
elements.append(el)
|
||||
element.clear()
|
||||
|
||||
|
@ -60,56 +49,56 @@ _YAML_SPECIAL_CHARACTERS = "!&*{}[],#|>@`'\""
|
|||
_YAML_SPECIAL_SEQUENCES = ("- ", ": ", "? ")
|
||||
|
||||
|
||||
def _get_yaml_compatible_string(scalar: Any) -> str:
|
||||
def _get_yaml_compatible_string(scalar):
|
||||
"""Enclose string in single quotes in some cases"""
|
||||
string = str(scalar)
|
||||
if string and (
|
||||
string[0] in _YAML_SPECIAL_CHARACTERS
|
||||
or any(seq in string for seq in _YAML_SPECIAL_SEQUENCES)
|
||||
or string.endswith(":")
|
||||
or string.endswith(':')
|
||||
):
|
||||
string = string.replace("'", "''")
|
||||
string = "'{}'".format(string)
|
||||
return string
|
||||
|
||||
|
||||
def dump_yaml(city: City, f: TextIO) -> None:
|
||||
def write_yaml(data: dict, f: TextIO, indent: str = "") -> None:
|
||||
def dump_yaml(city, f):
|
||||
def write_yaml(data, f, indent=''):
|
||||
if isinstance(data, (set, list)):
|
||||
f.write("\n")
|
||||
f.write('\n')
|
||||
for i in data:
|
||||
f.write(indent)
|
||||
f.write("- ")
|
||||
write_yaml(i, f, indent + " ")
|
||||
f.write('- ')
|
||||
write_yaml(i, f, indent + ' ')
|
||||
elif isinstance(data, dict):
|
||||
f.write("\n")
|
||||
f.write('\n')
|
||||
for k, v in data.items():
|
||||
if v is None:
|
||||
continue
|
||||
f.write(indent + _get_yaml_compatible_string(k) + ": ")
|
||||
write_yaml(v, f, indent + " ")
|
||||
f.write(indent + _get_yaml_compatible_string(k) + ': ')
|
||||
write_yaml(v, f, indent + ' ')
|
||||
if isinstance(v, (list, set, dict)):
|
||||
f.write("\n")
|
||||
f.write('\n')
|
||||
else:
|
||||
f.write(_get_yaml_compatible_string(data))
|
||||
f.write("\n")
|
||||
f.write('\n')
|
||||
|
||||
INCLUDE_STOP_AREAS = False
|
||||
stops = set()
|
||||
routes = []
|
||||
for route in city:
|
||||
stations = OrderedDict(
|
||||
[(sa.transfer or sa.id, sa.name) for sa in route.stopareas()]
|
||||
[(sa.transfer or sa.id, sa.name) for sa in route.stop_areas()]
|
||||
)
|
||||
rte = {
|
||||
"type": route.mode,
|
||||
"ref": route.ref,
|
||||
"name": route.name,
|
||||
"colour": route.colour,
|
||||
"infill": route.infill,
|
||||
"station_count": len(stations),
|
||||
"stations": list(stations.values()),
|
||||
"itineraries": {},
|
||||
'type': route.mode,
|
||||
'ref': route.ref,
|
||||
'name': route.name,
|
||||
'colour': route.colour,
|
||||
'infill': route.infill,
|
||||
'station_count': len(stations),
|
||||
'stations': list(stations.values()),
|
||||
'itineraries': {},
|
||||
}
|
||||
for variant in route:
|
||||
if INCLUDE_STOP_AREAS:
|
||||
|
@ -118,65 +107,76 @@ def dump_yaml(city: City, f: TextIO) -> None:
|
|||
s = st.stoparea
|
||||
if s.id == s.station.id:
|
||||
v_stops.append(
|
||||
"{} ({})".format(s.station.name, s.station.id)
|
||||
'{} ({})'.format(s.station.name, s.station.id)
|
||||
)
|
||||
else:
|
||||
v_stops.append(
|
||||
"{} ({}) in {} ({})".format(
|
||||
'{} ({}) in {} ({})'.format(
|
||||
s.station.name, s.station.id, s.name, s.id
|
||||
)
|
||||
)
|
||||
else:
|
||||
v_stops = [
|
||||
"{} ({})".format(
|
||||
'{} ({})'.format(
|
||||
s.stoparea.station.name, s.stoparea.station.id
|
||||
)
|
||||
for s in variant
|
||||
]
|
||||
rte["itineraries"][variant.id] = v_stops
|
||||
rte['itineraries'][variant.id] = v_stops
|
||||
stops.update(v_stops)
|
||||
routes.append(rte)
|
||||
transfers = []
|
||||
for t in city.transfers:
|
||||
v_stops = ["{} ({})".format(s.name, s.id) for s in t]
|
||||
v_stops = ['{} ({})'.format(s.name, s.id) for s in t]
|
||||
transfers.append(sorted(v_stops))
|
||||
|
||||
result = {
|
||||
"stations": sorted(stops),
|
||||
"transfers": sorted(transfers, key=lambda t: t[0]),
|
||||
"routes": sorted(routes, key=lambda r: r["ref"]),
|
||||
'stations': sorted(stops),
|
||||
'transfers': sorted(transfers, key=lambda t: t[0]),
|
||||
'routes': sorted(routes, key=lambda r: r['ref']),
|
||||
}
|
||||
write_yaml(result, f)
|
||||
|
||||
|
||||
def make_geojson(city: City, include_tracks_geometry: bool = True) -> dict:
|
||||
stopareas_in_transfers: set[StopArea] = set()
|
||||
def make_geojson(city, tracks=True):
|
||||
transfers = set()
|
||||
for t in city.transfers:
|
||||
stopareas_in_transfers.update(t)
|
||||
transfers.update(t)
|
||||
features = []
|
||||
stopareas = set()
|
||||
stops = set()
|
||||
for rmaster in city:
|
||||
for variant in rmaster:
|
||||
tracks = (
|
||||
variant.get_extended_tracks()
|
||||
if include_tracks_geometry
|
||||
else [s.stop for s in variant]
|
||||
)
|
||||
features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": tracks,
|
||||
},
|
||||
"properties": {
|
||||
"ref": variant.ref,
|
||||
"name": variant.name,
|
||||
"stroke": variant.colour,
|
||||
},
|
||||
}
|
||||
)
|
||||
if not tracks:
|
||||
features.append(
|
||||
{
|
||||
'type': 'Feature',
|
||||
'geometry': {
|
||||
'type': 'LineString',
|
||||
'coordinates': [s.stop for s in variant],
|
||||
},
|
||||
'properties': {
|
||||
'ref': variant.ref,
|
||||
'name': variant.name,
|
||||
'stroke': variant.colour,
|
||||
},
|
||||
}
|
||||
)
|
||||
elif variant.tracks:
|
||||
features.append(
|
||||
{
|
||||
'type': 'Feature',
|
||||
'geometry': {
|
||||
'type': 'LineString',
|
||||
'coordinates': variant.tracks,
|
||||
},
|
||||
'properties': {
|
||||
'ref': variant.ref,
|
||||
'name': variant.name,
|
||||
'stroke': variant.colour,
|
||||
},
|
||||
}
|
||||
)
|
||||
for st in variant:
|
||||
stops.add(st.stop)
|
||||
stopareas.add(st.stoparea)
|
||||
|
@ -184,58 +184,58 @@ def make_geojson(city: City, include_tracks_geometry: bool = True) -> dict:
|
|||
for stop in stops:
|
||||
features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": stop,
|
||||
'type': 'Feature',
|
||||
'geometry': {
|
||||
'type': 'Point',
|
||||
'coordinates': stop,
|
||||
},
|
||||
"properties": {
|
||||
"marker-size": "small",
|
||||
"marker-symbol": "circle",
|
||||
'properties': {
|
||||
'marker-size': 'small',
|
||||
'marker-symbol': 'circle',
|
||||
},
|
||||
}
|
||||
)
|
||||
for stoparea in stopareas:
|
||||
features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": stoparea.center,
|
||||
'type': 'Feature',
|
||||
'geometry': {
|
||||
'type': 'Point',
|
||||
'coordinates': stoparea.center,
|
||||
},
|
||||
"properties": {
|
||||
"name": stoparea.name,
|
||||
"marker-size": "small",
|
||||
"marker-color": "#ff2600"
|
||||
if stoparea in stopareas_in_transfers
|
||||
else "#797979",
|
||||
'properties': {
|
||||
'name': stoparea.name,
|
||||
'marker-size': 'small',
|
||||
'marker-color': '#ff2600'
|
||||
if stoparea in transfers
|
||||
else '#797979',
|
||||
},
|
||||
}
|
||||
)
|
||||
return {"type": "FeatureCollection", "features": features}
|
||||
return {'type': 'FeatureCollection', 'features': features}
|
||||
|
||||
|
||||
def _dumps_route_id(route_id: tuple[str | None, str | None]) -> str:
|
||||
"""Argument is a route_id that depends on route colour and ref. Name can
|
||||
be taken from route_master or can be route's own, we don't take it into
|
||||
consideration. Some of route attributes can be None. The function makes
|
||||
def _dumps_route_id(route_id):
|
||||
"""Argument is a route_id that depends on route colour and ref. Name
|
||||
can be taken from route_master or can be route's own, we don't take it
|
||||
into consideration. Some of route attributes can be None. The function makes
|
||||
route_id json-compatible - dumps it to a string."""
|
||||
return json.dumps(route_id, ensure_ascii=False)
|
||||
|
||||
|
||||
def _loads_route_id(route_id_dump: str) -> tuple[str | None, str | None]:
|
||||
def _loads_route_id(route_id_dump):
|
||||
"""Argument is a json-encoded identifier of a route.
|
||||
Return a tuple (colour, ref)."""
|
||||
return tuple(json.loads(route_id_dump))
|
||||
|
||||
|
||||
def read_recovery_data(path: str) -> dict:
|
||||
def read_recovery_data(path):
|
||||
"""Recovery data is a json with data from previous transport builds.
|
||||
It helps to recover cities from some errors, e.g. by resorting
|
||||
shuffled stations in routes."""
|
||||
data = None
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
with open(path, 'r') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
|
@ -257,15 +257,11 @@ def read_recovery_data(path: str) -> dict:
|
|||
return data
|
||||
|
||||
|
||||
def write_recovery_data(
|
||||
path: str, current_data: dict, cities: list[City]
|
||||
) -> None:
|
||||
def write_recovery_data(path, current_data, cities):
|
||||
"""Updates recovery data with good cities data and writes to file."""
|
||||
|
||||
def make_city_recovery_data(
|
||||
city: City,
|
||||
) -> dict[tuple[str | None, str | None], list[dict]]:
|
||||
routes: dict[tuple[str | None, str | None], list[dict]] = {}
|
||||
def make_city_recovery_data(city):
|
||||
routes = {}
|
||||
for route in city:
|
||||
# Recovery is based primarily on route/station names/refs.
|
||||
# If route's ref/colour changes, the route won't be used.
|
||||
|
@ -273,21 +269,21 @@ def write_recovery_data(
|
|||
itineraries = []
|
||||
for variant in route:
|
||||
itin = {
|
||||
"stations": [],
|
||||
"name": variant.name,
|
||||
"from": variant.element["tags"].get("from"),
|
||||
"to": variant.element["tags"].get("to"),
|
||||
'stations': [],
|
||||
'name': variant.name,
|
||||
'from': variant.element['tags'].get('from'),
|
||||
'to': variant.element['tags'].get('to'),
|
||||
}
|
||||
for stop in variant:
|
||||
station = stop.stoparea.station
|
||||
station_name = station.name
|
||||
if station_name == "?" and station.int_name:
|
||||
if station_name == '?' and station.int_name:
|
||||
station_name = station.int_name
|
||||
itin["stations"].append(
|
||||
itin['stations'].append(
|
||||
{
|
||||
"oms_id": station.id,
|
||||
"name": station_name,
|
||||
"center": station.center,
|
||||
'oms_id': station.id,
|
||||
'name': station_name,
|
||||
'center': station.center,
|
||||
}
|
||||
)
|
||||
if itin is not None:
|
||||
|
@ -297,7 +293,7 @@ def write_recovery_data(
|
|||
|
||||
data = current_data
|
||||
for city in cities:
|
||||
if city.is_good:
|
||||
if city.is_good():
|
||||
data[city.name] = make_city_recovery_data(city)
|
||||
|
||||
try:
|
||||
|
@ -308,7 +304,7 @@ def write_recovery_data(
|
|||
}
|
||||
for city_name, routes in data.items()
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
logging.warning("Cannot write recovery data to '%s': %s", path, str(e))
|
1916
subway_structure.py
Normal file
1916
subway_structure.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,92 +0,0 @@
|
|||
from .consts import (
|
||||
ALL_MODES,
|
||||
CONSTRUCTION_KEYS,
|
||||
DEFAULT_MODES_RAPID,
|
||||
DEFAULT_MODES_OVERGROUND,
|
||||
DISPLACEMENT_TOLERANCE,
|
||||
MAX_DISTANCE_STOP_TO_LINE,
|
||||
MODES_OVERGROUND,
|
||||
MODES_RAPID,
|
||||
RAILWAY_TYPES,
|
||||
)
|
||||
from .css_colours import normalize_colour
|
||||
from .geom_utils import (
|
||||
angle_between,
|
||||
distance,
|
||||
distance_on_line,
|
||||
find_segment,
|
||||
is_near,
|
||||
project_on_line,
|
||||
)
|
||||
from .osm_element import el_center, el_id
|
||||
from .overpass import multi_overpass, overpass_request
|
||||
from .subway_io import (
|
||||
dump_yaml,
|
||||
load_xml,
|
||||
make_geojson,
|
||||
read_recovery_data,
|
||||
write_recovery_data,
|
||||
)
|
||||
from .types import (
|
||||
CriticalValidationError,
|
||||
IdT,
|
||||
LonLat,
|
||||
OsmElementT,
|
||||
RailT,
|
||||
TransferT,
|
||||
TransfersT,
|
||||
)
|
||||
from .validation import (
|
||||
add_osm_elements_to_cities,
|
||||
BAD_MARK,
|
||||
calculate_centers,
|
||||
DEFAULT_CITIES_INFO_URL,
|
||||
DEFAULT_SPREADSHEET_ID,
|
||||
get_cities_info,
|
||||
prepare_cities,
|
||||
validate_cities,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ALL_MODES",
|
||||
"CONSTRUCTION_KEYS",
|
||||
"DEFAULT_MODES_RAPID",
|
||||
"DEFAULT_MODES_OVERGROUND",
|
||||
"DISPLACEMENT_TOLERANCE",
|
||||
"MAX_DISTANCE_STOP_TO_LINE",
|
||||
"MODES_OVERGROUND",
|
||||
"MODES_RAPID",
|
||||
"RAILWAY_TYPES",
|
||||
"angle_between",
|
||||
"distance",
|
||||
"distance_on_line",
|
||||
"find_segment",
|
||||
"is_near",
|
||||
"project_on_line",
|
||||
"normalize_colour",
|
||||
"el_center",
|
||||
"el_id",
|
||||
"overpass_request",
|
||||
"multi_overpass",
|
||||
"dump_yaml",
|
||||
"load_xml",
|
||||
"make_geojson",
|
||||
"read_recovery_data",
|
||||
"write_recovery_data",
|
||||
"CriticalValidationError",
|
||||
"IdT",
|
||||
"LonLat",
|
||||
"OsmElementT",
|
||||
"RailT",
|
||||
"TransferT",
|
||||
"TransfersT",
|
||||
"add_osm_elements_to_cities",
|
||||
"BAD_MARK",
|
||||
"calculate_centers",
|
||||
"DEFAULT_CITIES_INFO_URL",
|
||||
"DEFAULT_SPREADSHEET_ID",
|
||||
"get_cities_info",
|
||||
"prepare_cities",
|
||||
"validate_cities",
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
MAX_DISTANCE_STOP_TO_LINE = 50 # in meters
|
||||
|
||||
# If an object was moved not too far compared to previous validator run,
|
||||
# it is likely the same object
|
||||
DISPLACEMENT_TOLERANCE = 300 # in meters
|
||||
|
||||
MODES_RAPID = {"subway", "light_rail", "monorail", "train"}
|
||||
MODES_OVERGROUND = {"tram", "bus", "trolleybus", "aerialway", "ferry"}
|
||||
DEFAULT_MODES_RAPID = {"subway", "light_rail"}
|
||||
DEFAULT_MODES_OVERGROUND = {"tram"} # TODO: bus and trolleybus?
|
||||
ALL_MODES = MODES_RAPID | MODES_OVERGROUND
|
||||
RAILWAY_TYPES = {
|
||||
"rail",
|
||||
"light_rail",
|
||||
"subway",
|
||||
"narrow_gauge",
|
||||
"funicular",
|
||||
"monorail",
|
||||
"tram",
|
||||
}
|
||||
CONSTRUCTION_KEYS = (
|
||||
"construction",
|
||||
"proposed",
|
||||
"construction:railway",
|
||||
"proposed:railway",
|
||||
)
|
|
@ -1,165 +0,0 @@
|
|||
import re
|
||||
|
||||
# Source: https://www.w3.org/TR/css3-color/#svg-color
|
||||
CSS_COLOURS = {
|
||||
"aliceblue": "#f0f8ff",
|
||||
"antiquewhite": "#faebd7",
|
||||
"aqua": "#00ffff",
|
||||
"aquamarine": "#7fffd4",
|
||||
"azure": "#f0ffff",
|
||||
"beige": "#f5f5dc",
|
||||
"bisque": "#ffe4c4",
|
||||
"black": "#000000",
|
||||
"blanchedalmond": "#ffebcd",
|
||||
"blue": "#0000ff",
|
||||
"blueviolet": "#8a2be2",
|
||||
"brown": "#a52a2a",
|
||||
"burlywood": "#deb887",
|
||||
"cadetblue": "#5f9ea0",
|
||||
"chartreuse": "#7fff00",
|
||||
"chocolate": "#d2691e",
|
||||
"coral": "#ff7f50",
|
||||
"cornflowerblue": "#6495ed",
|
||||
"cornsilk": "#fff8dc",
|
||||
"crimson": "#dc143c",
|
||||
"cyan": "#00ffff",
|
||||
"darkblue": "#00008b",
|
||||
"darkcyan": "#008b8b",
|
||||
"darkgoldenrod": "#b8860b",
|
||||
"darkgray": "#a9a9a9",
|
||||
"darkgreen": "#006400",
|
||||
"darkgrey": "#a9a9a9",
|
||||
"darkkhaki": "#bdb76b",
|
||||
"darkmagenta": "#8b008b",
|
||||
"darkolivegreen": "#556b2f",
|
||||
"darkorange": "#ff8c00",
|
||||
"darkorchid": "#9932cc",
|
||||
"darkred": "#8b0000",
|
||||
"darksalmon": "#e9967a",
|
||||
"darkseagreen": "#8fbc8f",
|
||||
"darkslateblue": "#483d8b",
|
||||
"darkslategray": "#2f4f4f",
|
||||
"darkslategrey": "#2f4f4f",
|
||||
"darkturquoise": "#00ced1",
|
||||
"darkviolet": "#9400d3",
|
||||
"deeppink": "#ff1493",
|
||||
"deepskyblue": "#00bfff",
|
||||
"dimgray": "#696969",
|
||||
"dimgrey": "#696969",
|
||||
"dodgerblue": "#1e90ff",
|
||||
"firebrick": "#b22222",
|
||||
"floralwhite": "#fffaf0",
|
||||
"forestgreen": "#228b22",
|
||||
"fuchsia": "#ff00ff",
|
||||
"gainsboro": "#dcdcdc",
|
||||
"ghostwhite": "#f8f8ff",
|
||||
"gold": "#ffd700",
|
||||
"goldenrod": "#daa520",
|
||||
"gray": "#808080",
|
||||
"green": "#008000",
|
||||
"greenyellow": "#adff2f",
|
||||
"grey": "#808080",
|
||||
"honeydew": "#f0fff0",
|
||||
"hotpink": "#ff69b4",
|
||||
"indianred": "#cd5c5c",
|
||||
"indigo": "#4b0082",
|
||||
"ivory": "#fffff0",
|
||||
"khaki": "#f0e68c",
|
||||
"lavender": "#e6e6fa",
|
||||
"lavenderblush": "#fff0f5",
|
||||
"lawngreen": "#7cfc00",
|
||||
"lemonchiffon": "#fffacd",
|
||||
"lightblue": "#add8e6",
|
||||
"lightcoral": "#f08080",
|
||||
"lightcyan": "#e0ffff",
|
||||
"lightgoldenrodyellow": "#fafad2",
|
||||
"lightgray": "#d3d3d3",
|
||||
"lightgreen": "#90ee90",
|
||||
"lightgrey": "#d3d3d3",
|
||||
"lightpink": "#ffb6c1",
|
||||
"lightsalmon": "#ffa07a",
|
||||
"lightseagreen": "#20b2aa",
|
||||
"lightskyblue": "#87cefa",
|
||||
"lightslategray": "#778899",
|
||||
"lightslategrey": "#778899",
|
||||
"lightsteelblue": "#b0c4de",
|
||||
"lightyellow": "#ffffe0",
|
||||
"lime": "#00ff00",
|
||||
"limegreen": "#32cd32",
|
||||
"linen": "#faf0e6",
|
||||
"magenta": "#ff00ff",
|
||||
"maroon": "#800000",
|
||||
"mediumaquamarine": "#66cdaa",
|
||||
"mediumblue": "#0000cd",
|
||||
"mediumorchid": "#ba55d3",
|
||||
"mediumpurple": "#9370db",
|
||||
"mediumseagreen": "#3cb371",
|
||||
"mediumslateblue": "#7b68ee",
|
||||
"mediumspringgreen": "#00fa9a",
|
||||
"mediumturquoise": "#48d1cc",
|
||||
"mediumvioletred": "#c71585",
|
||||
"midnightblue": "#191970",
|
||||
"mintcream": "#f5fffa",
|
||||
"mistyrose": "#ffe4e1",
|
||||
"moccasin": "#ffe4b5",
|
||||
"navajowhite": "#ffdead",
|
||||
"navy": "#000080",
|
||||
"oldlace": "#fdf5e6",
|
||||
"olive": "#808000",
|
||||
"olivedrab": "#6b8e23",
|
||||
"orange": "#ffa500",
|
||||
"orangered": "#ff4500",
|
||||
"orchid": "#da70d6",
|
||||
"palegoldenrod": "#eee8aa",
|
||||
"palegreen": "#98fb98",
|
||||
"paleturquoise": "#afeeee",
|
||||
"palevioletred": "#db7093",
|
||||
"papayawhip": "#ffefd5",
|
||||
"peachpuff": "#ffdab9",
|
||||
"peru": "#cd853f",
|
||||
"pink": "#ffc0cb",
|
||||
"plum": "#dda0dd",
|
||||
"powderblue": "#b0e0e6",
|
||||
"purple": "#800080",
|
||||
"red": "#ff0000",
|
||||
"rosybrown": "#bc8f8f",
|
||||
"royalblue": "#4169e1",
|
||||
"saddlebrown": "#8b4513",
|
||||
"salmon": "#fa8072",
|
||||
"sandybrown": "#f4a460",
|
||||
"seagreen": "#2e8b57",
|
||||
"seashell": "#fff5ee",
|
||||
"sienna": "#a0522d",
|
||||
"silver": "#c0c0c0",
|
||||
"skyblue": "#87ceeb",
|
||||
"slateblue": "#6a5acd",
|
||||
"slategray": "#708090",
|
||||
"slategrey": "#708090",
|
||||
"snow": "#fffafa",
|
||||
"springgreen": "#00ff7f",
|
||||
"steelblue": "#4682b4",
|
||||
"tan": "#d2b48c",
|
||||
"teal": "#008080",
|
||||
"thistle": "#d8bfd8",
|
||||
"tomato": "#ff6347",
|
||||
"turquoise": "#40e0d0",
|
||||
"violet": "#ee82ee",
|
||||
"wheat": "#f5deb3",
|
||||
"white": "#ffffff",
|
||||
"whitesmoke": "#f5f5f5",
|
||||
"yellow": "#ffff00",
|
||||
"yellowgreen": "#9acd32",
|
||||
}
|
||||
|
||||
|
||||
def normalize_colour(c: str | None) -> str | None:
|
||||
if not c:
|
||||
return None
|
||||
c = c.strip().lower()
|
||||
if c in CSS_COLOURS:
|
||||
return CSS_COLOURS[c]
|
||||
if re.match(r"^#?[0-9a-f]{3}([0-9a-f]{3})?$", c):
|
||||
if len(c) == 4:
|
||||
return c[0] + c[1] + c[1] + c[2] + c[2] + c[3] + c[3]
|
||||
return c
|
||||
raise ValueError("Unknown colour code: {}".format(c))
|
|
@ -1,175 +0,0 @@
|
|||
import math
|
||||
|
||||
from subways.consts import MAX_DISTANCE_STOP_TO_LINE
|
||||
from subways.types import LonLat, RailT
|
||||
|
||||
|
||||
def distance(p1: LonLat, p2: LonLat) -> float:
|
||||
if p1 is None or p2 is None:
|
||||
raise Exception(
|
||||
"One of arguments to distance({}, {}) is None".format(p1, p2)
|
||||
)
|
||||
dx = math.radians(p1[0] - p2[0]) * math.cos(
|
||||
0.5 * math.radians(p1[1] + p2[1])
|
||||
)
|
||||
dy = math.radians(p1[1] - p2[1])
|
||||
return 6378137 * math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
|
||||
def is_near(p1: LonLat, p2: LonLat) -> bool:
|
||||
return (
|
||||
p1[0] - 1e-8 <= p2[0] <= p1[0] + 1e-8
|
||||
and p1[1] - 1e-8 <= p2[1] <= p1[1] + 1e-8
|
||||
)
|
||||
|
||||
|
||||
def project_on_segment(p: LonLat, p1: LonLat, p2: LonLat) -> float | None:
|
||||
"""Given three points, return u - the position of projection of
|
||||
point p onto segment p1p2 regarding point p1 and (p2-p1) direction vector
|
||||
"""
|
||||
dp = (p2[0] - p1[0], p2[1] - p1[1])
|
||||
d2 = dp[0] * dp[0] + dp[1] * dp[1]
|
||||
if d2 < 1e-14:
|
||||
return None
|
||||
u = ((p[0] - p1[0]) * dp[0] + (p[1] - p1[1]) * dp[1]) / d2
|
||||
if not 0 <= u <= 1:
|
||||
return None
|
||||
return u
|
||||
|
||||
|
||||
def project_on_line(p: LonLat, line: RailT) -> dict:
|
||||
result = {
|
||||
# In the first approximation, position on rails is the index of the
|
||||
# closest vertex of line to the point p. Fractional value means that
|
||||
# the projected point lies on a segment between two vertices.
|
||||
# More than one value can occur if a route follows the same tracks
|
||||
# more than once.
|
||||
"positions_on_line": None,
|
||||
"projected_point": None, # (lon, lat)
|
||||
}
|
||||
|
||||
if len(line) < 2:
|
||||
return result
|
||||
d_min = MAX_DISTANCE_STOP_TO_LINE * 5
|
||||
closest_to_vertex = False
|
||||
# First, check vertices in the line
|
||||
for i, vertex in enumerate(line):
|
||||
d = distance(p, vertex)
|
||||
if d < d_min:
|
||||
result["positions_on_line"] = [i]
|
||||
result["projected_point"] = vertex
|
||||
d_min = d
|
||||
closest_to_vertex = True
|
||||
elif vertex == result["projected_point"]:
|
||||
# Repeated occurrence of the track vertex in line, like Oslo Line 5
|
||||
result["positions_on_line"].append(i)
|
||||
# And then calculate distances to each segment
|
||||
for seg in range(len(line) - 1):
|
||||
# Check bbox for speed
|
||||
if not (
|
||||
(
|
||||
min(line[seg][0], line[seg + 1][0]) - MAX_DISTANCE_STOP_TO_LINE
|
||||
<= p[0]
|
||||
<= max(line[seg][0], line[seg + 1][0])
|
||||
+ MAX_DISTANCE_STOP_TO_LINE
|
||||
)
|
||||
and (
|
||||
min(line[seg][1], line[seg + 1][1]) - MAX_DISTANCE_STOP_TO_LINE
|
||||
<= p[1]
|
||||
<= max(line[seg][1], line[seg + 1][1])
|
||||
+ MAX_DISTANCE_STOP_TO_LINE
|
||||
)
|
||||
):
|
||||
continue
|
||||
u = project_on_segment(p, line[seg], line[seg + 1])
|
||||
if u:
|
||||
projected_point = (
|
||||
line[seg][0] + u * (line[seg + 1][0] - line[seg][0]),
|
||||
line[seg][1] + u * (line[seg + 1][1] - line[seg][1]),
|
||||
)
|
||||
d = distance(p, projected_point)
|
||||
if d < d_min:
|
||||
result["positions_on_line"] = [seg + u]
|
||||
result["projected_point"] = projected_point
|
||||
d_min = d
|
||||
closest_to_vertex = False
|
||||
elif projected_point == result["projected_point"]:
|
||||
# Repeated occurrence of the track segment in line,
|
||||
# like Oslo Line 5
|
||||
if not closest_to_vertex:
|
||||
result["positions_on_line"].append(seg + u)
|
||||
return result
|
||||
|
||||
|
||||
def find_segment(
|
||||
p: LonLat, line: RailT, start_vertex: int = 0
|
||||
) -> tuple[int, float] | tuple[None, None]:
|
||||
"""Returns index of a segment and a position inside it."""
|
||||
EPS = 1e-9
|
||||
for seg in range(start_vertex, len(line) - 1):
|
||||
if is_near(p, line[seg]):
|
||||
return seg, 0.0
|
||||
if line[seg][0] == line[seg + 1][0]:
|
||||
if not (p[0] - EPS <= line[seg][0] <= p[0] + EPS):
|
||||
continue
|
||||
px = None
|
||||
else:
|
||||
px = (p[0] - line[seg][0]) / (line[seg + 1][0] - line[seg][0])
|
||||
if px is None or (0 <= px <= 1):
|
||||
if line[seg][1] == line[seg + 1][1]:
|
||||
if not (p[1] - EPS <= line[seg][1] <= p[1] + EPS):
|
||||
continue
|
||||
py = None
|
||||
else:
|
||||
py = (p[1] - line[seg][1]) / (line[seg + 1][1] - line[seg][1])
|
||||
if py is None or (0 <= py <= 1):
|
||||
if py is None or px is None or (px - EPS <= py <= px + EPS):
|
||||
return seg, px or py
|
||||
return None, None
|
||||
|
||||
|
||||
def distance_on_line(
|
||||
p1: LonLat, p2: LonLat, line: RailT, start_vertex: int = 0
|
||||
) -> tuple[float, int] | None:
|
||||
"""Calculates distance via line between projections
|
||||
of points p1 and p2. Returns a TUPLE of (d, vertex):
|
||||
d is the distance and vertex is the number of the second
|
||||
vertex, to continue calculations for the next point."""
|
||||
line_len = len(line)
|
||||
seg1, pos1 = find_segment(p1, line, start_vertex)
|
||||
if seg1 is None:
|
||||
# logging.warn('p1 %s is not projected, st=%s', p1, start_vertex)
|
||||
return None
|
||||
seg2, pos2 = find_segment(p2, line, seg1)
|
||||
if seg2 is None:
|
||||
if line[0] == line[-1]:
|
||||
line = line + line[1:]
|
||||
seg2, pos2 = find_segment(p2, line, seg1)
|
||||
if seg2 is None:
|
||||
# logging.warn('p2 %s is not projected, st=%s', p2, start_vertex)
|
||||
return None
|
||||
if seg1 == seg2:
|
||||
return distance(line[seg1], line[seg1 + 1]) * abs(pos2 - pos1), seg1
|
||||
if seg2 < seg1:
|
||||
# Should not happen
|
||||
raise Exception("Pos1 %s is after pos2 %s", seg1, seg2)
|
||||
d = 0
|
||||
if pos1 < 1:
|
||||
d += distance(line[seg1], line[seg1 + 1]) * (1 - pos1)
|
||||
for i in range(seg1 + 1, seg2):
|
||||
d += distance(line[i], line[i + 1])
|
||||
if pos2 > 0:
|
||||
d += distance(line[seg2], line[seg2 + 1]) * pos2
|
||||
return d, seg2 % line_len
|
||||
|
||||
|
||||
def angle_between(p1: LonLat, c: LonLat, p2: LonLat) -> float:
|
||||
a = round(
|
||||
abs(
|
||||
math.degrees(
|
||||
math.atan2(p1[1] - c[1], p1[0] - c[0])
|
||||
- math.atan2(p2[1] - c[1], p2[0] - c[0])
|
||||
)
|
||||
)
|
||||
)
|
||||
return a if a <= 180 else 360 - a
|
|
@ -1,26 +0,0 @@
|
|||
from subways.types import IdT, LonLat, OsmElementT
|
||||
|
||||
|
||||
def el_id(el: OsmElementT) -> IdT | None:
|
||||
if not el:
|
||||
return None
|
||||
if "type" not in el:
|
||||
raise Exception("What is this element? {}".format(el))
|
||||
return el["type"][0] + str(el.get("id", el.get("ref", "")))
|
||||
|
||||
|
||||
def el_center(el: OsmElementT) -> LonLat | None:
|
||||
if not el:
|
||||
return None
|
||||
if "lat" in el:
|
||||
return el["lon"], el["lat"]
|
||||
elif "center" in el:
|
||||
return el["center"]["lon"], el["center"]["lat"]
|
||||
return None
|
||||
|
||||
|
||||
def get_network(relation: OsmElementT) -> str | None:
|
||||
for k in ("network:metro", "network", "operator"):
|
||||
if k in relation["tags"]:
|
||||
return relation["tags"][k]
|
||||
return None
|
|
@ -1,60 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
from subways.consts import MODES_OVERGROUND, MODES_RAPID
|
||||
from subways.types import OsmElementT
|
||||
|
||||
|
||||
def compose_overpass_request(
|
||||
overground: bool, bboxes: list[list[float]]
|
||||
) -> str:
|
||||
if not bboxes:
|
||||
raise RuntimeError("No bboxes given for overpass request")
|
||||
|
||||
query = "[out:json][timeout:1000];("
|
||||
modes = MODES_OVERGROUND if overground else MODES_RAPID
|
||||
for bbox in bboxes:
|
||||
bbox_part = f"({','.join(str(coord) for coord in bbox)})"
|
||||
query += "("
|
||||
for mode in sorted(modes):
|
||||
query += f'rel[route="{mode}"]{bbox_part};'
|
||||
query += ");"
|
||||
query += "rel(br)[type=route_master];"
|
||||
if not overground:
|
||||
query += f"node[railway=subway_entrance]{bbox_part};"
|
||||
query += f"node[railway=train_station_entrance]{bbox_part};"
|
||||
query += f"rel[public_transport=stop_area]{bbox_part};"
|
||||
query += (
|
||||
"rel(br)[type=public_transport][public_transport=stop_area_group];"
|
||||
)
|
||||
query += ");(._;>>;);out body center qt;"
|
||||
logging.debug("Query: %s", query)
|
||||
return query
|
||||
|
||||
|
||||
def overpass_request(
|
||||
overground: bool, overpass_api: str, bboxes: list[list[float]]
|
||||
) -> list[OsmElementT]:
|
||||
query = compose_overpass_request(overground, bboxes)
|
||||
url = f"{overpass_api}?data={urllib.parse.quote(query)}"
|
||||
response = urllib.request.urlopen(url, timeout=1000)
|
||||
if (r_code := response.getcode()) != 200:
|
||||
raise Exception(f"Failed to query Overpass API: HTTP {r_code}")
|
||||
return json.load(response)["elements"]
|
||||
|
||||
|
||||
def multi_overpass(
|
||||
overground: bool, overpass_api: str, bboxes: list[list[float]]
|
||||
) -> list[OsmElementT]:
|
||||
SLICE_SIZE = 10
|
||||
INTERREQUEST_WAIT = 5 # in seconds
|
||||
result = []
|
||||
for i in range(0, len(bboxes), SLICE_SIZE):
|
||||
if i > 0:
|
||||
time.sleep(INTERREQUEST_WAIT)
|
||||
bboxes_i = bboxes[i : i + SLICE_SIZE] # noqa E203
|
||||
result.extend(overpass_request(overground, overpass_api, bboxes_i))
|
||||
return result
|
|
@ -1,8 +0,0 @@
|
|||
# Import only those processors (modules) you want to use.
|
||||
# Ignore F401 "module imported but unused" violation since these modules
|
||||
# are addressed via introspection.
|
||||
from . import gtfs, mapsme, fmk # noqa F401
|
||||
from ._common import transit_to_dict
|
||||
|
||||
|
||||
__all__ = ["gtfs", "mapsme", "fmk", "transit_to_dict"]
|
|
@ -1,113 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from subways.osm_element import el_center
|
||||
from subways.types import TransfersT
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
|
||||
DEFAULT_INTERVAL = 2.5 * 60 # seconds
|
||||
KMPH_TO_MPS = 1 / 3.6 # km/h to m/s conversion multiplier
|
||||
DEFAULT_AVE_VEHICLE_SPEED = 40 * KMPH_TO_MPS # m/s
|
||||
SPEED_ON_TRANSFER = 3.5 * KMPH_TO_MPS # m/s
|
||||
TRANSFER_PENALTY = 30 # seconds
|
||||
|
||||
|
||||
def format_colour(colour: str | None) -> str | None:
|
||||
"""Truncate leading # sign."""
|
||||
return colour[1:] if colour else None
|
||||
|
||||
|
||||
def transit_to_dict(cities: list[City], transfers: TransfersT) -> dict:
|
||||
"""Get data for good cities as a dictionary."""
|
||||
data = {
|
||||
"stopareas": {}, # stoparea id => stoparea data
|
||||
"networks": {}, # city name => city data
|
||||
"transfers": {}, # set(tuple(stoparea_id1, stoparea_id2)), id1<id2
|
||||
}
|
||||
|
||||
for city in (c for c in cities if c.is_good):
|
||||
network = {
|
||||
"id": city.id,
|
||||
"name": city.name,
|
||||
"routes": [],
|
||||
}
|
||||
|
||||
for route_master in city:
|
||||
route_data = {
|
||||
"id": route_master.id,
|
||||
"mode": route_master.mode,
|
||||
"ref": route_master.ref,
|
||||
"name": route_master.name,
|
||||
"colour": route_master.colour,
|
||||
"infill": route_master.infill,
|
||||
"itineraries": [],
|
||||
}
|
||||
|
||||
for route in route_master:
|
||||
variant_data = {
|
||||
"id": route.id,
|
||||
"tracks": route.get_tracks_geometry(),
|
||||
"start_time": route.start_time,
|
||||
"end_time": route.end_time,
|
||||
"interval": route.interval,
|
||||
"duration": route.duration,
|
||||
"stops": [
|
||||
{
|
||||
"stoparea_id": route_stop.stoparea.id,
|
||||
"distance": route_stop.distance,
|
||||
}
|
||||
for route_stop in route.stops
|
||||
],
|
||||
}
|
||||
|
||||
# Store stopareas participating in the route
|
||||
# and that have not been stored yet
|
||||
for route_stop in route.stops:
|
||||
stoparea = route_stop.stoparea
|
||||
if stoparea.id in data["stopareas"]:
|
||||
continue
|
||||
stoparea_data = {
|
||||
"id": stoparea.id,
|
||||
"center": stoparea.center,
|
||||
"name": stoparea.station.name,
|
||||
"entrances": [
|
||||
{
|
||||
"id": egress_id,
|
||||
"name": egress["tags"].get("name"),
|
||||
"ref": egress["tags"].get("ref"),
|
||||
"center": el_center(egress),
|
||||
}
|
||||
for (egress_id, egress) in (
|
||||
(egress_id, city.elements[egress_id])
|
||||
for egress_id in stoparea.entrances
|
||||
| stoparea.exits
|
||||
)
|
||||
],
|
||||
}
|
||||
data["stopareas"][stoparea.id] = stoparea_data
|
||||
|
||||
route_data["itineraries"].append(variant_data)
|
||||
|
||||
network["routes"].append(route_data)
|
||||
|
||||
data["networks"][city.name] = network
|
||||
|
||||
# transfers
|
||||
pairwise_transfers = set()
|
||||
for stoparea_id_set in transfers:
|
||||
stoparea_ids = sorted(stoparea_id_set)
|
||||
for first_i in range(len(stoparea_ids) - 1):
|
||||
for second_i in range(first_i + 1, len(stoparea_ids)):
|
||||
stoparea1_id = stoparea_ids[first_i]
|
||||
stoparea2_id = stoparea_ids[second_i]
|
||||
if all(
|
||||
st_id in data["stopareas"]
|
||||
for st_id in (stoparea1_id, stoparea2_id)
|
||||
):
|
||||
pairwise_transfers.add((stoparea1_id, stoparea2_id))
|
||||
|
||||
data["transfers"] = pairwise_transfers
|
||||
return data
|
|
@ -1,235 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
from subways.consts import DISPLACEMENT_TOLERANCE
|
||||
from subways.geom_utils import distance
|
||||
from subways.osm_element import el_center
|
||||
from subways.structure.station import Station
|
||||
from subways.types import IdT, LonLat, OsmElementT, TransfersT
|
||||
from ._common import (
|
||||
DEFAULT_AVE_VEHICLE_SPEED,
|
||||
DEFAULT_INTERVAL,
|
||||
format_colour,
|
||||
KMPH_TO_MPS,
|
||||
SPEED_ON_TRANSFER,
|
||||
TRANSFER_PENALTY,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
from subways.structure.stop_area import StopArea
|
||||
|
||||
|
||||
OSM_TYPES = {"n": (0, "node"), "w": (2, "way"), "r": (3, "relation")}
|
||||
ENTRANCE_PENALTY = 60 # seconds
|
||||
SPEED_TO_ENTRANCE = 5 * KMPH_TO_MPS # m/s
|
||||
|
||||
# (stoparea1_uid, stoparea2_uid) -> seconds; stoparea1_uid < stoparea2_uid
|
||||
TransferTimesT: TypeAlias = dict[tuple[int, int], int]
|
||||
|
||||
|
||||
def uid(elid: IdT, typ: str | None = None) -> int:
|
||||
t = elid[0]
|
||||
osm_id = int(elid[1:])
|
||||
if not typ:
|
||||
osm_id = (osm_id << 2) + OSM_TYPES[t][0]
|
||||
elif typ != t:
|
||||
raise Exception("Got {}, expected {}".format(elid, typ))
|
||||
return osm_id << 1
|
||||
|
||||
|
||||
def transit_data_to_fmk(cities: list[City], transfers: TransfersT) -> dict:
|
||||
"""Generate all output and save to file.
|
||||
:param cities: List of City instances
|
||||
:param transfers: List of sets of StopArea.id
|
||||
:param cache_path: Path to json-file with good cities cache or None.
|
||||
"""
|
||||
|
||||
def find_exits_for_platform(
|
||||
center: LonLat, nodes: list[OsmElementT]
|
||||
) -> list[OsmElementT]:
|
||||
exits: list[OsmElementT] = []
|
||||
min_distance = None
|
||||
for n in nodes:
|
||||
d = distance(center, (n["lon"], n["lat"]))
|
||||
if not min_distance:
|
||||
min_distance = d * 2 / 3
|
||||
elif d < min_distance:
|
||||
continue
|
||||
too_close = False
|
||||
for e in exits:
|
||||
d = distance((e["lon"], e["lat"]), (n["lon"], n["lat"]))
|
||||
if d < min_distance:
|
||||
too_close = True
|
||||
break
|
||||
if not too_close:
|
||||
exits.append(n)
|
||||
return exits
|
||||
|
||||
stop_areas: dict[IdT, StopArea] = {}
|
||||
stops: dict[IdT, dict] = {} # stoparea el_id -> stop jsonified data
|
||||
networks = []
|
||||
good_cities = [c for c in cities if c.is_good]
|
||||
platform_nodes = {}
|
||||
|
||||
for city in good_cities:
|
||||
network = {"network": city.name, "routes": [], "agency_id": city.id}
|
||||
for route in city:
|
||||
routes = {
|
||||
"type": route.mode,
|
||||
"ref": route.ref,
|
||||
"name": route.name,
|
||||
"colour": format_colour(route.colour),
|
||||
"route_id": uid(route.id, "r"),
|
||||
"itineraries": [],
|
||||
}
|
||||
if route.infill:
|
||||
routes["casing"] = routes["colour"]
|
||||
routes["colour"] = format_colour(route.infill)
|
||||
for i, variant in enumerate(route):
|
||||
itin = []
|
||||
for stop in variant:
|
||||
stop_areas[stop.stoparea.id] = stop.stoparea
|
||||
itin.append(uid(stop.stoparea.id))
|
||||
# Make exits from platform nodes,
|
||||
# if we don't have proper exits
|
||||
if (
|
||||
len(stop.stoparea.entrances) + len(stop.stoparea.exits)
|
||||
== 0
|
||||
):
|
||||
for pl in stop.stoparea.platforms:
|
||||
pl_el = city.elements[pl]
|
||||
if pl_el["type"] == "node":
|
||||
pl_nodes = [pl_el]
|
||||
elif pl_el["type"] == "way":
|
||||
pl_nodes = [
|
||||
city.elements.get("n{}".format(n))
|
||||
for n in pl_el["nodes"]
|
||||
]
|
||||
else:
|
||||
pl_nodes = []
|
||||
for m in pl_el["members"]:
|
||||
if m["type"] == "way":
|
||||
if (
|
||||
"{}{}".format(
|
||||
m["type"][0], m["ref"]
|
||||
)
|
||||
in city.elements
|
||||
):
|
||||
pl_nodes.extend(
|
||||
[
|
||||
city.elements.get(
|
||||
"n{}".format(n)
|
||||
)
|
||||
for n in city.elements[
|
||||
"{}{}".format(
|
||||
m["type"][0],
|
||||
m["ref"],
|
||||
)
|
||||
]["nodes"]
|
||||
]
|
||||
)
|
||||
pl_nodes = [n for n in pl_nodes if n]
|
||||
platform_nodes[pl] = find_exits_for_platform(
|
||||
stop.stoparea.centers[pl], pl_nodes
|
||||
)
|
||||
|
||||
routes["itineraries"].append(itin)
|
||||
network["routes"].append(routes)
|
||||
networks.append(network)
|
||||
|
||||
for stop_id, stop in stop_areas.items():
|
||||
st = {
|
||||
"name": stop.name,
|
||||
"int_name": stop.int_name,
|
||||
"lat": stop.center[1],
|
||||
"lon": stop.center[0],
|
||||
"osm_type": OSM_TYPES[stop.station.id[0]][1],
|
||||
"osm_id": int(stop.station.id[1:]),
|
||||
"id": uid(stop.id),
|
||||
"entrances": [],
|
||||
"exits": [],
|
||||
}
|
||||
for e_l, k in ((stop.entrances, "entrances"), (stop.exits, "exits")):
|
||||
for e in e_l:
|
||||
if e[0] == "n":
|
||||
st[k].append(
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": int(e[1:]),
|
||||
"lon": stop.centers[e][0],
|
||||
"lat": stop.centers[e][1],
|
||||
}
|
||||
)
|
||||
if len(stop.entrances) + len(stop.exits) == 0:
|
||||
if stop.platforms:
|
||||
for pl in stop.platforms:
|
||||
for n in platform_nodes[pl]:
|
||||
for k in ("entrances", "exits"):
|
||||
st[k].append(
|
||||
{
|
||||
"osm_type": n["type"],
|
||||
"osm_id": n["id"],
|
||||
"lon": n["lon"],
|
||||
"lat": n["lat"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
for k in ("entrances", "exits"):
|
||||
st[k].append(
|
||||
{
|
||||
"osm_type": OSM_TYPES[stop.station.id[0]][1],
|
||||
"osm_id": int(stop.station.id[1:]),
|
||||
"lon": stop.centers[stop.id][0],
|
||||
"lat": stop.centers[stop.id][1],
|
||||
}
|
||||
)
|
||||
|
||||
stops[stop_id] = st
|
||||
|
||||
pairwise_transfers: list[list[int]] = []
|
||||
for stoparea_id_set in transfers:
|
||||
tr = list(sorted([uid(sa_id) for sa_id in stoparea_id_set
|
||||
if sa_id in stops]))
|
||||
if len(tr) > 1:
|
||||
pairwise_transfers.append(tr)
|
||||
|
||||
result = {
|
||||
"stops": list(stops.values()),
|
||||
"transfers": pairwise_transfers,
|
||||
"networks": networks,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def process(
|
||||
cities: list[City],
|
||||
transfers: TransfersT,
|
||||
filename: str,
|
||||
cache_path: str | None,
|
||||
) -> None:
|
||||
"""Generate all output and save to file.
|
||||
:param cities: list of City instances
|
||||
:param transfers: all collected transfers in the world
|
||||
:param filename: Path to file to save the result
|
||||
:param cache_path: Path to json-file with good cities cache or None.
|
||||
"""
|
||||
if not filename.lower().endswith("json"):
|
||||
filename = f"{filename}.json"
|
||||
|
||||
fmk_transit = transit_data_to_fmk(cities, transfers)
|
||||
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
fmk_transit,
|
||||
f,
|
||||
indent=1,
|
||||
ensure_ascii=False,
|
||||
)
|
|
@ -1,409 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import typing
|
||||
from functools import partial
|
||||
from io import BytesIO, StringIO
|
||||
from itertools import permutations
|
||||
from tarfile import TarFile, TarInfo
|
||||
from zipfile import ZipFile
|
||||
|
||||
from ._common import (
|
||||
DEFAULT_AVE_VEHICLE_SPEED,
|
||||
DEFAULT_INTERVAL,
|
||||
format_colour,
|
||||
KMPH_TO_MPS,
|
||||
SPEED_ON_TRANSFER,
|
||||
TRANSFER_PENALTY,
|
||||
transit_to_dict,
|
||||
)
|
||||
from subways.types import TransfersT
|
||||
from subways.geom_utils import distance
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
|
||||
|
||||
DEFAULT_TRIP_START_TIME = (5, 0) # 05:00
|
||||
DEFAULT_TRIP_END_TIME = (1, 0) # 01:00
|
||||
COORDINATE_PRECISION = 7 # fractional digits. It's OSM precision, ~ 5 cm
|
||||
|
||||
GTFS_COLUMNS = {
|
||||
"agency": [
|
||||
"agency_id",
|
||||
"agency_name",
|
||||
"agency_url",
|
||||
"agency_timezone",
|
||||
"agency_lang",
|
||||
"agency_phone",
|
||||
],
|
||||
"routes": [
|
||||
"route_id",
|
||||
"agency_id",
|
||||
"route_short_name",
|
||||
"route_long_name",
|
||||
"route_desc",
|
||||
"route_type",
|
||||
"route_url",
|
||||
"route_color",
|
||||
"route_text_color",
|
||||
"route_sort_order",
|
||||
"route_fare_class",
|
||||
"line_id",
|
||||
"listed_route",
|
||||
],
|
||||
"trips": [
|
||||
"route_id",
|
||||
"service_id",
|
||||
"trip_id",
|
||||
"trip_headsign",
|
||||
"trip_short_name",
|
||||
"direction_id",
|
||||
"block_id",
|
||||
"shape_id",
|
||||
"wheelchair_accessible",
|
||||
"trip_route_type",
|
||||
"route_pattern_id",
|
||||
"bikes_allowed",
|
||||
"average_speed", # extension field (km/h)
|
||||
],
|
||||
"stops": [
|
||||
"stop_id",
|
||||
"stop_code",
|
||||
"stop_name",
|
||||
"stop_desc",
|
||||
"platform_code",
|
||||
"platform_name",
|
||||
"stop_lat",
|
||||
"stop_lon",
|
||||
"zone_id",
|
||||
"stop_address",
|
||||
"stop_url",
|
||||
"level_id",
|
||||
"location_type",
|
||||
"parent_station",
|
||||
"wheelchair_boarding",
|
||||
"municipality",
|
||||
"on_street",
|
||||
"at_street",
|
||||
"vehicle_type",
|
||||
],
|
||||
"calendar": [
|
||||
"service_id",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
"start_date",
|
||||
"end_date",
|
||||
],
|
||||
"stop_times": [
|
||||
"trip_id",
|
||||
"arrival_time",
|
||||
"departure_time",
|
||||
"stop_id",
|
||||
"stop_sequence",
|
||||
"stop_headsign",
|
||||
"pickup_type",
|
||||
"drop_off_type",
|
||||
"shape_dist_traveled",
|
||||
"timepoint",
|
||||
"checkpoint_id",
|
||||
"continuous_pickup",
|
||||
"continuous_drop_off",
|
||||
],
|
||||
"frequencies": [
|
||||
"trip_id",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"headway_secs",
|
||||
"exact_times",
|
||||
],
|
||||
"shapes": [
|
||||
"shape_id",
|
||||
"shape_pt_lat",
|
||||
"shape_pt_lon",
|
||||
"shape_pt_sequence",
|
||||
"shape_dist_traveled",
|
||||
],
|
||||
"transfers": [
|
||||
"from_stop_id",
|
||||
"to_stop_id",
|
||||
"transfer_type",
|
||||
"min_transfer_time",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def round_coords(coords_tuple: tuple) -> tuple:
|
||||
return tuple(
|
||||
map(lambda coord: round(coord, COORDINATE_PRECISION), coords_tuple)
|
||||
)
|
||||
|
||||
|
||||
def transit_data_to_gtfs(data: dict) -> dict:
|
||||
# Keys correspond GTFS file names
|
||||
gtfs_data = {key: [] for key in GTFS_COLUMNS.keys()}
|
||||
|
||||
# GTFS calendar
|
||||
gtfs_data["calendar"].append(
|
||||
{
|
||||
"service_id": "always",
|
||||
"monday": 1,
|
||||
"tuesday": 1,
|
||||
"wednesday": 1,
|
||||
"thursday": 1,
|
||||
"friday": 1,
|
||||
"saturday": 1,
|
||||
"sunday": 1,
|
||||
"start_date": "19700101",
|
||||
"end_date": "30000101",
|
||||
}
|
||||
)
|
||||
|
||||
# GTFS stops
|
||||
for stoparea_id, stoparea_data in data["stopareas"].items():
|
||||
station_id = f"{stoparea_id}_st"
|
||||
station_name = stoparea_data["name"]
|
||||
station_center = round_coords(stoparea_data["center"])
|
||||
station_gtfs = {
|
||||
"stop_id": station_id,
|
||||
"stop_code": station_id,
|
||||
"stop_name": station_name,
|
||||
"stop_lat": station_center[1],
|
||||
"stop_lon": station_center[0],
|
||||
"location_type": 1, # station in GTFS terms
|
||||
}
|
||||
gtfs_data["stops"].append(station_gtfs)
|
||||
|
||||
platform_id = f"{stoparea_id}_plt"
|
||||
platform_gtfs = {
|
||||
"stop_id": platform_id,
|
||||
"stop_code": platform_id,
|
||||
"stop_name": station_name,
|
||||
"stop_lat": station_center[1],
|
||||
"stop_lon": station_center[0],
|
||||
"location_type": 0, # stop/platform in GTFS terms
|
||||
"parent_station": station_id,
|
||||
}
|
||||
gtfs_data["stops"].append(platform_gtfs)
|
||||
|
||||
if not stoparea_data["entrances"]:
|
||||
entrance_id = f"{stoparea_id}_egress"
|
||||
entrance_gtfs = {
|
||||
"stop_id": entrance_id,
|
||||
"stop_code": entrance_id,
|
||||
"stop_name": station_name,
|
||||
"stop_lat": station_center[1],
|
||||
"stop_lon": station_center[0],
|
||||
"location_type": 2,
|
||||
"parent_station": station_id,
|
||||
}
|
||||
gtfs_data["stops"].append(entrance_gtfs)
|
||||
else:
|
||||
for entrance in stoparea_data["entrances"]:
|
||||
entrance_id = f"{entrance['id']}_{stoparea_id}"
|
||||
entrance_name = entrance["name"]
|
||||
if not entrance["name"]:
|
||||
entrance_name = station_name
|
||||
ref = entrance["ref"]
|
||||
if ref:
|
||||
entrance_name += f" {ref}"
|
||||
center = round_coords(entrance["center"])
|
||||
entrance_gtfs = {
|
||||
"stop_id": entrance_id,
|
||||
"stop_code": entrance_id,
|
||||
"stop_name": entrance_name,
|
||||
"stop_lat": center[1],
|
||||
"stop_lon": center[0],
|
||||
"location_type": 2,
|
||||
"parent_station": station_id,
|
||||
}
|
||||
gtfs_data["stops"].append(entrance_gtfs)
|
||||
|
||||
# agency, routes, trips, stop_times, frequencies, shapes
|
||||
for network in data["networks"].values():
|
||||
agency = {
|
||||
"agency_id": network["id"],
|
||||
"agency_name": network["name"],
|
||||
}
|
||||
gtfs_data["agency"].append(agency)
|
||||
|
||||
for route_master in network["routes"]:
|
||||
route = {
|
||||
"route_id": route_master["id"],
|
||||
"agency_id": network["id"],
|
||||
"route_type": 12 if route_master["mode"] == "monorail" else 1,
|
||||
"route_short_name": route_master["ref"],
|
||||
"route_long_name": route_master["name"],
|
||||
"route_color": format_colour(route_master["colour"]),
|
||||
}
|
||||
gtfs_data["routes"].append(route)
|
||||
|
||||
for itinerary in route_master["itineraries"]:
|
||||
shape_id = itinerary["id"][1:] # truncate leading 'r'
|
||||
average_speed = round(
|
||||
(
|
||||
DEFAULT_AVE_VEHICLE_SPEED
|
||||
if not itinerary["duration"]
|
||||
else itinerary["stops"][-1]["distance"]
|
||||
/ itinerary["duration"]
|
||||
)
|
||||
/ KMPH_TO_MPS,
|
||||
1,
|
||||
) # km/h
|
||||
trip = {
|
||||
"trip_id": itinerary["id"],
|
||||
"route_id": route_master["id"],
|
||||
"service_id": "always",
|
||||
"shape_id": shape_id,
|
||||
"average_speed": average_speed,
|
||||
}
|
||||
gtfs_data["trips"].append(trip)
|
||||
|
||||
for i, (lon, lat) in enumerate(itinerary["tracks"]):
|
||||
lon, lat = round_coords((lon, lat))
|
||||
gtfs_data["shapes"].append(
|
||||
{
|
||||
"shape_id": shape_id,
|
||||
"trip_id": itinerary["id"],
|
||||
"shape_pt_lat": lat,
|
||||
"shape_pt_lon": lon,
|
||||
"shape_pt_sequence": i,
|
||||
}
|
||||
)
|
||||
|
||||
start_time = itinerary["start_time"] or DEFAULT_TRIP_START_TIME
|
||||
end_time = itinerary["end_time"] or DEFAULT_TRIP_END_TIME
|
||||
if end_time <= start_time:
|
||||
end_time = (end_time[0] + 24, end_time[1])
|
||||
start_time = f"{start_time[0]:02d}:{start_time[1]:02d}:00"
|
||||
end_time = f"{end_time[0]:02d}:{end_time[1]:02d}:00"
|
||||
|
||||
gtfs_data["frequencies"].append(
|
||||
{
|
||||
"trip_id": itinerary["id"],
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"headway_secs": itinerary["interval"]
|
||||
or DEFAULT_INTERVAL,
|
||||
}
|
||||
)
|
||||
|
||||
for i, route_stop in enumerate(itinerary["stops"]):
|
||||
platform_id = f"{route_stop['stoparea_id']}_plt"
|
||||
|
||||
gtfs_data["stop_times"].append(
|
||||
{
|
||||
"trip_id": itinerary["id"],
|
||||
"stop_sequence": i,
|
||||
"shape_dist_traveled": route_stop["distance"],
|
||||
"stop_id": platform_id,
|
||||
}
|
||||
)
|
||||
|
||||
# transfers
|
||||
for stoparea1_id, stoparea2_id in data["transfers"]:
|
||||
stoparea1 = data["stopareas"][stoparea1_id]
|
||||
stoparea2 = data["stopareas"][stoparea2_id]
|
||||
transfer_time = TRANSFER_PENALTY + round(
|
||||
distance(stoparea1["center"], stoparea2["center"])
|
||||
/ SPEED_ON_TRANSFER
|
||||
)
|
||||
gtfs_sa_id1 = f"{stoparea1['id']}_st"
|
||||
gtfs_sa_id2 = f"{stoparea2['id']}_st"
|
||||
for id1, id2 in permutations((gtfs_sa_id1, gtfs_sa_id2)):
|
||||
gtfs_data["transfers"].append(
|
||||
{
|
||||
"from_stop_id": id1,
|
||||
"to_stop_id": id2,
|
||||
"transfer_type": 0,
|
||||
"min_transfer_time": transfer_time,
|
||||
}
|
||||
)
|
||||
|
||||
return gtfs_data
|
||||
|
||||
|
||||
def process(
|
||||
cities: list[City],
|
||||
transfers: TransfersT,
|
||||
filename: str,
|
||||
cache_path: str | None,
|
||||
) -> None:
|
||||
"""Generate all output and save to file.
|
||||
:param cities: list of City instances
|
||||
:param transfers: all collected transfers in the world
|
||||
:param filename: Path to file to save the result
|
||||
:param cache_path: Path to json-file with good cities cache or None.
|
||||
"""
|
||||
|
||||
transit_data = transit_to_dict(cities, transfers)
|
||||
gtfs_data = transit_data_to_gtfs(transit_data)
|
||||
|
||||
# TODO: make universal cache for all processors,
|
||||
# and apply the cache to GTFS
|
||||
|
||||
make_gtfs(filename, gtfs_data)
|
||||
|
||||
|
||||
def dict_to_row(dict_data: dict, record_type: str) -> list:
|
||||
"""Given object stored in a dict and an array of columns,
|
||||
return a row to use in CSV.
|
||||
"""
|
||||
return [
|
||||
"" if (v := dict_data.get(column)) is None else v
|
||||
for column in GTFS_COLUMNS[record_type]
|
||||
]
|
||||
|
||||
|
||||
def make_gtfs(filename: str, gtfs_data: dict, fmt: str | None = None) -> None:
|
||||
if not fmt:
|
||||
fmt = "tar" if filename.endswith(".tar") else "zip"
|
||||
|
||||
if fmt == "zip":
|
||||
make_gtfs_zip(filename, gtfs_data)
|
||||
else:
|
||||
make_gtfs_tar(filename, gtfs_data)
|
||||
|
||||
|
||||
def make_gtfs_zip(filename: str, gtfs_data: dict) -> None:
|
||||
if not filename.lower().endswith(".zip"):
|
||||
filename = f"{filename}.zip"
|
||||
|
||||
with ZipFile(filename, "w") as zf:
|
||||
for gtfs_feature, columns in GTFS_COLUMNS.items():
|
||||
with StringIO(newline="") as string_io:
|
||||
writer = csv.writer(string_io, delimiter=",")
|
||||
writer.writerow(columns)
|
||||
writer.writerows(
|
||||
map(
|
||||
partial(dict_to_row, record_type=gtfs_feature),
|
||||
gtfs_data[gtfs_feature],
|
||||
)
|
||||
)
|
||||
zf.writestr(f"{gtfs_feature}.txt", string_io.getvalue())
|
||||
|
||||
|
||||
def make_gtfs_tar(filename: str, gtfs_data: dict) -> None:
|
||||
if not filename.lower().endswith(".tar"):
|
||||
filename = f"{filename}.tar"
|
||||
|
||||
with TarFile(filename, "w") as tf:
|
||||
for gtfs_feature, columns in GTFS_COLUMNS.items():
|
||||
with StringIO(newline="") as string_io:
|
||||
writer = csv.writer(string_io, delimiter=",")
|
||||
writer.writerow(columns)
|
||||
writer.writerows(
|
||||
map(
|
||||
partial(dict_to_row, record_type=gtfs_feature),
|
||||
gtfs_data[gtfs_feature],
|
||||
)
|
||||
)
|
||||
tarinfo = TarInfo(f"{gtfs_feature}.txt")
|
||||
data = string_io.getvalue().encode()
|
||||
tarinfo.size = len(data)
|
||||
tf.addfile(tarinfo, BytesIO(data))
|
|
@ -1 +0,0 @@
|
|||
lxml
|
|
@ -1,17 +0,0 @@
|
|||
from .city import City, get_unused_subway_entrances_geojson
|
||||
from .route import Route
|
||||
from .route_master import RouteMaster
|
||||
from .route_stop import RouteStop
|
||||
from .station import Station
|
||||
from .stop_area import StopArea
|
||||
|
||||
|
||||
__all__ = [
|
||||
"City",
|
||||
"get_unused_subway_entrances_geojson",
|
||||
"Route",
|
||||
"RouteMaster",
|
||||
"RouteStop",
|
||||
"Station",
|
||||
"StopArea",
|
||||
]
|
|
@ -1,622 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Collection, Iterator
|
||||
from itertools import chain
|
||||
|
||||
from subways.consts import (
|
||||
DEFAULT_MODES_OVERGROUND,
|
||||
DEFAULT_MODES_RAPID,
|
||||
)
|
||||
from subways.osm_element import el_center, el_id, get_network
|
||||
from subways.structure.route import Route
|
||||
from subways.structure.route_master import RouteMaster
|
||||
from subways.structure.station import Station
|
||||
from subways.structure.stop_area import StopArea
|
||||
from subways.types import (
|
||||
IdT,
|
||||
OsmElementT,
|
||||
TransfersT,
|
||||
TransferT,
|
||||
)
|
||||
|
||||
ALLOWED_STATIONS_MISMATCH = 0.02 # part of total station count
|
||||
ALLOWED_TRANSFERS_MISMATCH = 0.07 # part of total interchanges count
|
||||
|
||||
used_entrances = set()
|
||||
|
||||
|
||||
def format_elid_list(ids: Collection[IdT]) -> str:
|
||||
msg = ", ".join(sorted(ids)[:20])
|
||||
if len(ids) > 20:
|
||||
msg += ", ..."
|
||||
return msg
|
||||
|
||||
|
||||
class City:
|
||||
route_class = Route
|
||||
|
||||
def __init__(self, city_data: dict, overground: bool = False) -> None:
|
||||
self.validate_called = False
|
||||
self.errors: list[str] = []
|
||||
self.warnings: list[str] = []
|
||||
self.notices: list[str] = []
|
||||
self.id = None
|
||||
self.try_fill_int_attribute(city_data, "id")
|
||||
self.name = city_data["name"]
|
||||
self.country = city_data["country"]
|
||||
self.continent = city_data["continent"]
|
||||
self.overground = overground
|
||||
if not overground:
|
||||
self.try_fill_int_attribute(city_data, "num_stations")
|
||||
self.try_fill_int_attribute(city_data, "num_lines", "0")
|
||||
self.try_fill_int_attribute(city_data, "num_light_lines", "0")
|
||||
self.try_fill_int_attribute(city_data, "num_interchanges", "0")
|
||||
else:
|
||||
self.try_fill_int_attribute(city_data, "num_tram_lines", "0")
|
||||
self.try_fill_int_attribute(city_data, "num_trolleybus_lines", "0")
|
||||
self.try_fill_int_attribute(city_data, "num_bus_lines", "0")
|
||||
self.try_fill_int_attribute(city_data, "num_other_lines", "0")
|
||||
|
||||
# Acquiring list of networks and modes
|
||||
networks = (
|
||||
None
|
||||
if not city_data["networks"]
|
||||
else city_data["networks"].split(":")
|
||||
)
|
||||
if not networks or len(networks[-1]) == 0:
|
||||
self.networks = []
|
||||
else:
|
||||
self.networks = set(
|
||||
filter(None, [x.strip() for x in networks[-1].split(";")])
|
||||
)
|
||||
if not networks or len(networks) < 2 or len(networks[0]) == 0:
|
||||
if self.overground:
|
||||
self.modes = DEFAULT_MODES_OVERGROUND
|
||||
else:
|
||||
self.modes = DEFAULT_MODES_RAPID
|
||||
else:
|
||||
self.modes = {x.strip() for x in networks[0].split(",")}
|
||||
|
||||
# Reversing bbox so it is (xmin, ymin, xmax, ymax)
|
||||
bbox = city_data["bbox"].split(",")
|
||||
if len(bbox) == 4:
|
||||
self.bbox = [float(bbox[i]) for i in (1, 0, 3, 2)]
|
||||
else:
|
||||
self.bbox = None
|
||||
|
||||
self.elements: dict[IdT, OsmElementT] = {}
|
||||
self.stations: dict[IdT, list[StopArea]] = defaultdict(list)
|
||||
self.routes: dict[str, RouteMaster] = {} # keys are route_master refs
|
||||
self.masters: dict[IdT, OsmElementT] = {} # Route id → master element
|
||||
self.stop_areas: [IdT, list[OsmElementT]] = defaultdict(list)
|
||||
self.transfers: list[set[StopArea]] = []
|
||||
self.station_ids: set[IdT] = set()
|
||||
self.stops_and_platforms: set[IdT] = set()
|
||||
self.recovery_data = None
|
||||
|
||||
def try_fill_int_attribute(
|
||||
self, city_data: dict, attr: str, default: str | None = None
|
||||
) -> None:
|
||||
"""Try to convert string value to int. Conversion is considered
|
||||
to fail if one of the following is true:
|
||||
* attr is not empty and data type casting fails;
|
||||
* attr is empty and no default value is given.
|
||||
In such cases the city is marked as bad by adding an error
|
||||
to the city validation log.
|
||||
"""
|
||||
attr_value = city_data[attr]
|
||||
if not attr_value and default is not None:
|
||||
attr_value = default
|
||||
|
||||
try:
|
||||
attr_int = int(attr_value)
|
||||
except ValueError:
|
||||
print_value = (
|
||||
f"{city_data[attr]}" if city_data[attr] else "<empty>"
|
||||
)
|
||||
self.error(
|
||||
f"Configuration error: wrong value for {attr}: {print_value}"
|
||||
)
|
||||
setattr(self, attr, 0)
|
||||
else:
|
||||
setattr(self, attr, attr_int)
|
||||
|
||||
@staticmethod
|
||||
def log_message(message: str, el: OsmElementT) -> str:
|
||||
if el:
|
||||
tags = el.get("tags", {})
|
||||
message += ' ({} {}, "{}")'.format(
|
||||
el["type"],
|
||||
el.get("id", el.get("ref")),
|
||||
tags.get("name", tags.get("ref", "")),
|
||||
)
|
||||
return message
|
||||
|
||||
def notice(self, message: str, el: OsmElementT | None = None) -> None:
|
||||
"""This type of message may point to a potential problem."""
|
||||
msg = City.log_message(message, el)
|
||||
self.notices.append(msg)
|
||||
|
||||
def warn(self, message: str, el: OsmElementT | None = None) -> None:
|
||||
"""A warning is definitely a problem but is doesn't prevent
|
||||
from building a routing file and doesn't invalidate the city.
|
||||
"""
|
||||
msg = City.log_message(message, el)
|
||||
self.warnings.append(msg)
|
||||
|
||||
def error(self, message: str, el: OsmElementT | None = None) -> None:
|
||||
"""Error is a critical problem that invalidates the city."""
|
||||
msg = City.log_message(message, el)
|
||||
self.errors.append(msg)
|
||||
|
||||
def contains(self, el: OsmElementT) -> bool:
|
||||
center = el_center(el)
|
||||
if center:
|
||||
return (
|
||||
self.bbox[0] <= center[1] <= self.bbox[2]
|
||||
and self.bbox[1] <= center[0] <= self.bbox[3]
|
||||
)
|
||||
return False
|
||||
|
||||
def add(self, el: OsmElementT) -> None:
|
||||
if el["type"] == "relation" and "members" not in el:
|
||||
return
|
||||
|
||||
self.elements[el_id(el)] = el
|
||||
if not (el["type"] == "relation" and "tags" in el):
|
||||
return
|
||||
|
||||
relation_type = el["tags"].get("type")
|
||||
if relation_type == "route_master":
|
||||
for m in el["members"]:
|
||||
if m["type"] != "relation":
|
||||
continue
|
||||
|
||||
if el_id(m) in self.masters:
|
||||
self.error("Route in two route_masters", m)
|
||||
self.masters[el_id(m)] = el
|
||||
|
||||
elif el["tags"].get("public_transport") == "stop_area":
|
||||
if relation_type != "public_transport":
|
||||
self.warn(
|
||||
"stop_area relation with "
|
||||
f"type={relation_type}, needed type=public_transport",
|
||||
el,
|
||||
)
|
||||
return
|
||||
|
||||
warned_about_duplicates = False
|
||||
for m in el["members"]:
|
||||
stop_areas = self.stop_areas[el_id(m)]
|
||||
if el in stop_areas and not warned_about_duplicates:
|
||||
self.warn("Duplicate element in a stop area", el)
|
||||
warned_about_duplicates = True
|
||||
else:
|
||||
stop_areas.append(el)
|
||||
|
||||
def make_transfer(self, stoparea_group: OsmElementT) -> None:
|
||||
transfer: set[StopArea] = set()
|
||||
for m in stoparea_group["members"]:
|
||||
k = el_id(m)
|
||||
el = self.elements.get(k)
|
||||
if not el:
|
||||
# A stoparea_group member may validly not belong to the city
|
||||
# while the stoparea_group does - near the city bbox boundary
|
||||
continue
|
||||
if "tags" not in el:
|
||||
self.warn(
|
||||
"An untagged object {} in a stop_area_group".format(k),
|
||||
stoparea_group,
|
||||
)
|
||||
continue
|
||||
if (
|
||||
el["type"] != "relation"
|
||||
or el["tags"].get("type") != "public_transport"
|
||||
or el["tags"].get("public_transport") != "stop_area"
|
||||
):
|
||||
continue
|
||||
if k in self.stations:
|
||||
stoparea = self.stations[k][0]
|
||||
transfer.add(stoparea)
|
||||
if stoparea.transfer:
|
||||
# TODO: properly process such cases.
|
||||
# Counterexample 1: Paris,
|
||||
# Châtelet subway station <->
|
||||
# "Châtelet - Les Halles" railway station <->
|
||||
# Les Halles subway station
|
||||
# Counterexample 2: Saint-Petersburg, transfers
|
||||
# Витебский вокзал <->
|
||||
# Пушкинская <->
|
||||
# Звенигородская
|
||||
self.warn(
|
||||
"Stop area {} belongs to multiple interchanges".format(
|
||||
k
|
||||
)
|
||||
)
|
||||
stoparea.transfer = el_id(stoparea_group)
|
||||
if len(transfer) > 1:
|
||||
self.transfers.append(transfer)
|
||||
|
||||
def extract_routes(self) -> None:
|
||||
# Extract stations
|
||||
processed_stop_areas = set()
|
||||
for el in self.elements.values():
|
||||
if Station.is_station(el, self.modes):
|
||||
# See PR https://github.com/mapsme/subways/pull/98
|
||||
if (
|
||||
el["type"] == "relation"
|
||||
and el["tags"].get("type") != "multipolygon"
|
||||
):
|
||||
rel_type = el["tags"].get("type")
|
||||
self.warn(
|
||||
"A railway station cannot be a relation of type "
|
||||
f"{rel_type}",
|
||||
el,
|
||||
)
|
||||
continue
|
||||
st = Station(el, self)
|
||||
self.station_ids.add(st.id)
|
||||
if st.id in self.stop_areas:
|
||||
stations = []
|
||||
for sa in self.stop_areas[st.id]:
|
||||
stations.append(StopArea(st, self, sa))
|
||||
else:
|
||||
stations = [StopArea(st, self)]
|
||||
|
||||
for station in stations:
|
||||
if station.id not in processed_stop_areas:
|
||||
processed_stop_areas.add(station.id)
|
||||
for st_el in station.get_elements():
|
||||
self.stations[st_el].append(station)
|
||||
|
||||
# Check that stops and platforms belong to
|
||||
# a single stop_area
|
||||
for sp in chain(station.stops, station.platforms):
|
||||
if sp in self.stops_and_platforms:
|
||||
self.notice(
|
||||
f"A stop or a platform {sp} belongs to "
|
||||
"multiple stop areas, might be correct"
|
||||
)
|
||||
else:
|
||||
self.stops_and_platforms.add(sp)
|
||||
|
||||
# Extract routes
|
||||
for el in self.elements.values():
|
||||
if Route.is_route(el, self.modes):
|
||||
if el["tags"].get("access") in ("no", "private"):
|
||||
continue
|
||||
route_id = el_id(el)
|
||||
master_element = self.masters.get(route_id, None)
|
||||
if self.networks:
|
||||
network = get_network(el)
|
||||
if master_element:
|
||||
master_network = get_network(master_element)
|
||||
else:
|
||||
master_network = None
|
||||
if (
|
||||
network not in self.networks
|
||||
and master_network not in self.networks
|
||||
):
|
||||
continue
|
||||
|
||||
route = self.route_class(el, self, master_element)
|
||||
if not route.stops:
|
||||
self.warn("Route has no stops", el)
|
||||
continue
|
||||
elif len(route.stops) == 1:
|
||||
self.warn("Route has only one stop", el)
|
||||
continue
|
||||
|
||||
master_id = el_id(master_element) or route.ref
|
||||
route_master = self.routes.setdefault(
|
||||
master_id, RouteMaster(self, master_element)
|
||||
)
|
||||
route_master.add(route)
|
||||
|
||||
# And while we're iterating over relations, find interchanges
|
||||
if (
|
||||
el["type"] == "relation"
|
||||
and el.get("tags", {}).get("public_transport", None)
|
||||
== "stop_area_group"
|
||||
):
|
||||
self.make_transfer(el)
|
||||
|
||||
# Filter transfers, leaving only stations that belong to routes
|
||||
own_stopareas = set(self.stopareas())
|
||||
|
||||
self.transfers = [
|
||||
inner_transfer
|
||||
for inner_transfer in (
|
||||
own_stopareas.intersection(transfer)
|
||||
for transfer in self.transfers
|
||||
)
|
||||
if len(inner_transfer) > 1
|
||||
]
|
||||
|
||||
def __iter__(self) -> Iterator[RouteMaster]:
|
||||
return iter(self.routes.values())
|
||||
|
||||
def stopareas(self) -> Iterator[StopArea]:
|
||||
yielded_stopareas = set()
|
||||
for route_master in self:
|
||||
for stoparea in route_master.stopareas():
|
||||
if stoparea not in yielded_stopareas:
|
||||
yield stoparea
|
||||
yielded_stopareas.add(stoparea)
|
||||
|
||||
@property
|
||||
def is_good(self) -> bool:
|
||||
if not (self.errors or self.validate_called):
|
||||
raise RuntimeError(
|
||||
"You mustn't refer to City.is_good property before calling "
|
||||
"the City.validate() method unless an error already occurred."
|
||||
)
|
||||
return len(self.errors) == 0
|
||||
|
||||
def get_validation_result(self) -> dict:
|
||||
result = {
|
||||
"name": self.name,
|
||||
"country": self.country,
|
||||
"continent": self.continent,
|
||||
"stations_found": getattr(self, "found_stations", 0),
|
||||
"transfers_found": getattr(self, "found_interchanges", 0),
|
||||
"unused_entrances": getattr(self, "unused_entrances", 0),
|
||||
"networks": getattr(self, "found_networks", 0),
|
||||
}
|
||||
if not self.overground:
|
||||
result.update(
|
||||
{
|
||||
"subwayl_expected": getattr(self, "num_lines", 0),
|
||||
"lightrl_expected": getattr(self, "num_light_lines", 0),
|
||||
"subwayl_found": getattr(self, "found_lines", 0),
|
||||
"lightrl_found": getattr(self, "found_light_lines", 0),
|
||||
"stations_expected": getattr(self, "num_stations", 0),
|
||||
"transfers_expected": getattr(self, "num_interchanges", 0),
|
||||
}
|
||||
)
|
||||
else:
|
||||
result.update(
|
||||
{
|
||||
"stations_expected": 0,
|
||||
"transfers_expected": 0,
|
||||
"busl_expected": getattr(self, "num_bus_lines", 0),
|
||||
"trolleybusl_expected": getattr(
|
||||
self, "num_trolleybus_lines", 0
|
||||
),
|
||||
"traml_expected": getattr(self, "num_tram_lines", 0),
|
||||
"otherl_expected": getattr(self, "num_other_lines", 0),
|
||||
"busl_found": getattr(self, "found_bus_lines", 0),
|
||||
"trolleybusl_found": getattr(
|
||||
self, "found_trolleybus_lines", 0
|
||||
),
|
||||
"traml_found": getattr(self, "found_tram_lines", 0),
|
||||
"otherl_found": getattr(self, "found_other_lines", 0),
|
||||
}
|
||||
)
|
||||
result["warnings"] = self.warnings
|
||||
result["errors"] = self.errors
|
||||
result["notices"] = self.notices
|
||||
return result
|
||||
|
||||
def count_unused_entrances(self) -> None:
|
||||
global used_entrances
|
||||
stop_areas = set()
|
||||
for el in self.elements.values():
|
||||
if (
|
||||
el["type"] == "relation"
|
||||
and "tags" in el
|
||||
and el["tags"].get("public_transport") == "stop_area"
|
||||
and "members" in el
|
||||
):
|
||||
stop_areas.update([el_id(m) for m in el["members"]])
|
||||
unused = []
|
||||
not_in_sa = []
|
||||
for el in self.elements.values():
|
||||
if (
|
||||
el["type"] == "node"
|
||||
and "tags" in el
|
||||
and el["tags"].get("railway") == "subway_entrance"
|
||||
):
|
||||
i = el_id(el)
|
||||
if i in self.stations:
|
||||
used_entrances.add(i)
|
||||
if i not in stop_areas:
|
||||
not_in_sa.append(i)
|
||||
if i not in self.stations:
|
||||
unused.append(i)
|
||||
self.unused_entrances = len(unused)
|
||||
self.entrances_not_in_stop_areas = len(not_in_sa)
|
||||
if unused:
|
||||
self.notice(
|
||||
f"{len(unused)} subway entrances are not connected to a "
|
||||
f"station: {format_elid_list(unused)}"
|
||||
)
|
||||
if not_in_sa:
|
||||
self.notice(
|
||||
f"{len(not_in_sa)} subway entrances are not in stop_area "
|
||||
f"relations: {format_elid_list(not_in_sa)}"
|
||||
)
|
||||
|
||||
def validate_lines(self) -> None:
|
||||
self.found_light_lines = len(
|
||||
[x for x in self.routes.values() if x.mode != "subway"]
|
||||
)
|
||||
self.found_lines = len(self.routes) - self.found_light_lines
|
||||
if self.found_lines != self.num_lines:
|
||||
self.error(
|
||||
"Found {} subway lines, expected {}".format(
|
||||
self.found_lines, self.num_lines
|
||||
)
|
||||
)
|
||||
if self.found_light_lines != self.num_light_lines:
|
||||
self.error(
|
||||
"Found {} light rail lines, expected {}".format(
|
||||
self.found_light_lines, self.num_light_lines
|
||||
)
|
||||
)
|
||||
|
||||
def validate_overground_lines(self) -> None:
|
||||
self.found_tram_lines = len(
|
||||
[x for x in self.routes.values() if x.mode == "tram"]
|
||||
)
|
||||
self.found_bus_lines = len(
|
||||
[x for x in self.routes.values() if x.mode == "bus"]
|
||||
)
|
||||
self.found_trolleybus_lines = len(
|
||||
[x for x in self.routes.values() if x.mode == "trolleybus"]
|
||||
)
|
||||
self.found_other_lines = len(
|
||||
[
|
||||
x
|
||||
for x in self.routes.values()
|
||||
if x.mode not in ("bus", "trolleybus", "tram")
|
||||
]
|
||||
)
|
||||
if self.found_tram_lines != self.num_tram_lines:
|
||||
log_function = (
|
||||
self.error if self.found_tram_lines == 0 else self.notice
|
||||
)
|
||||
log_function(
|
||||
"Found {} tram lines, expected {}".format(
|
||||
self.found_tram_lines, self.num_tram_lines
|
||||
),
|
||||
)
|
||||
|
||||
def validate(self) -> None:
|
||||
networks = Counter()
|
||||
self.found_stations = 0
|
||||
unused_stations = set(self.station_ids)
|
||||
for rmaster in self.routes.values():
|
||||
networks[str(rmaster.network)] += 1
|
||||
if not self.overground:
|
||||
rmaster.check_return_routes()
|
||||
route_stations = set()
|
||||
for sa in rmaster.stopareas():
|
||||
route_stations.add(sa.transfer or sa.id)
|
||||
unused_stations.discard(sa.station.id)
|
||||
self.found_stations += len(route_stations)
|
||||
if unused_stations:
|
||||
self.unused_stations = len(unused_stations)
|
||||
self.notice(
|
||||
"{} unused stations: {}".format(
|
||||
self.unused_stations, format_elid_list(unused_stations)
|
||||
)
|
||||
)
|
||||
self.count_unused_entrances()
|
||||
self.found_interchanges = len(self.transfers)
|
||||
|
||||
if self.overground:
|
||||
self.validate_overground_lines()
|
||||
else:
|
||||
self.validate_lines()
|
||||
|
||||
if self.found_stations != self.num_stations:
|
||||
msg = "Found {} stations in routes, expected {}".format(
|
||||
self.found_stations, self.num_stations
|
||||
)
|
||||
log_function = (
|
||||
self.error
|
||||
if self.num_stations > 0
|
||||
and not (
|
||||
0
|
||||
<= (self.num_stations - self.found_stations)
|
||||
/ self.num_stations
|
||||
<= ALLOWED_STATIONS_MISMATCH
|
||||
)
|
||||
else self.warn
|
||||
)
|
||||
log_function(msg)
|
||||
|
||||
if self.found_interchanges != self.num_interchanges:
|
||||
msg = "Found {} interchanges, expected {}".format(
|
||||
self.found_interchanges, self.num_interchanges
|
||||
)
|
||||
log_function = (
|
||||
self.error
|
||||
if self.num_interchanges != 0
|
||||
and not (
|
||||
(self.num_interchanges - self.found_interchanges)
|
||||
/ self.num_interchanges
|
||||
<= ALLOWED_TRANSFERS_MISMATCH
|
||||
)
|
||||
else self.warn
|
||||
)
|
||||
log_function(msg)
|
||||
|
||||
self.found_networks = len(networks)
|
||||
if len(networks) > max(1, len(self.networks)):
|
||||
n_str = "; ".join(
|
||||
["{} ({})".format(k, v) for k, v in networks.items()]
|
||||
)
|
||||
self.notice("More than one network: {}".format(n_str))
|
||||
|
||||
self.validate_called = True
|
||||
|
||||
def calculate_distances(self) -> None:
|
||||
for route_master in self:
|
||||
for route in route_master:
|
||||
route.calculate_distances()
|
||||
|
||||
|
||||
def find_transfers(
|
||||
elements: list[OsmElementT], cities: Collection[City]
|
||||
) -> TransfersT:
|
||||
"""As for now, two Cities may contain the same stoparea, but those
|
||||
StopArea instances would have different python id. So we don't store
|
||||
references to StopAreas, but only their ids. This is important at
|
||||
inter-city interchanges.
|
||||
"""
|
||||
stop_area_groups = [
|
||||
el
|
||||
for el in elements
|
||||
if el["type"] == "relation"
|
||||
and "members" in el
|
||||
and el.get("tags", {}).get("public_transport") == "stop_area_group"
|
||||
]
|
||||
|
||||
stopareas_in_cities_ids = set(
|
||||
stoparea.id
|
||||
for city in cities
|
||||
if city.is_good
|
||||
for stoparea in city.stopareas()
|
||||
)
|
||||
|
||||
transfers = []
|
||||
for stop_area_group in stop_area_groups:
|
||||
transfer: TransferT = set(
|
||||
member_id
|
||||
for member_id in (
|
||||
el_id(member) for member in stop_area_group["members"]
|
||||
)
|
||||
if member_id in stopareas_in_cities_ids
|
||||
)
|
||||
if len(transfer) > 1:
|
||||
transfers.append(transfer)
|
||||
return transfers
|
||||
|
||||
|
||||
def get_unused_subway_entrances_geojson(elements: list[OsmElementT]) -> dict:
|
||||
global used_entrances
|
||||
features = []
|
||||
for el in elements:
|
||||
if (
|
||||
el["type"] == "node"
|
||||
and "tags" in el
|
||||
and el["tags"].get("railway") == "subway_entrance"
|
||||
):
|
||||
if el_id(el) not in used_entrances:
|
||||
geometry = {"type": "Point", "coordinates": el_center(el)}
|
||||
properties = {
|
||||
k: v
|
||||
for k, v in el["tags"].items()
|
||||
if k not in ("railway", "entrance")
|
||||
}
|
||||
features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": geometry,
|
||||
"properties": properties,
|
||||
}
|
||||
)
|
||||
return {"type": "FeatureCollection", "features": features}
|
|
@ -1,938 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import typing
|
||||
from collections.abc import Callable, Collection, Iterator
|
||||
from itertools import islice
|
||||
|
||||
from subways.consts import (
|
||||
CONSTRUCTION_KEYS,
|
||||
DISPLACEMENT_TOLERANCE,
|
||||
MAX_DISTANCE_STOP_TO_LINE,
|
||||
)
|
||||
from subways.css_colours import normalize_colour
|
||||
from subways.geom_utils import (
|
||||
angle_between,
|
||||
distance,
|
||||
distance_on_line,
|
||||
find_segment,
|
||||
project_on_line,
|
||||
)
|
||||
from subways.osm_element import el_id, el_center, get_network
|
||||
from subways.structure.route_stop import RouteStop
|
||||
from subways.structure.station import Station
|
||||
from subways.structure.stop_area import StopArea
|
||||
from subways.types import CriticalValidationError, IdT, OsmElementT, RailT
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
|
||||
START_END_TIMES_RE = re.compile(r".*?(\d{2}):(\d{2})-(\d{2}):(\d{2}).*")
|
||||
|
||||
ALLOWED_ANGLE_BETWEEN_STOPS = 45 # in degrees
|
||||
DISALLOWED_ANGLE_BETWEEN_STOPS = 20 # in degrees
|
||||
|
||||
|
||||
def parse_time_range(
|
||||
opening_hours: str,
|
||||
) -> tuple[tuple[int, int], tuple[int, int]] | None:
|
||||
"""Very simplified method to parse OSM opening_hours tag.
|
||||
We simply take the first HH:MM-HH:MM substring which is the most probable
|
||||
opening hours interval for the most of the weekdays.
|
||||
"""
|
||||
if opening_hours == "24/7":
|
||||
return (0, 0), (24, 0)
|
||||
|
||||
m = START_END_TIMES_RE.match(opening_hours)
|
||||
if not m:
|
||||
return None
|
||||
ints = tuple(map(int, m.groups()))
|
||||
if ints[1] > 59 or ints[3] > 59:
|
||||
return None
|
||||
start_time = (ints[0], ints[1])
|
||||
end_time = (ints[2], ints[3])
|
||||
return start_time, end_time
|
||||
|
||||
|
||||
def osm_interval_to_seconds(interval_str: str) -> int | None:
|
||||
"""Convert to int an OSM value for 'interval'/'headway'/'duration' tag
|
||||
which may be in these formats:
|
||||
HH:MM:SS,
|
||||
HH:MM,
|
||||
MM,
|
||||
M
|
||||
(https://wiki.openstreetmap.org/wiki/Key:interval#Format)
|
||||
"""
|
||||
hours, minutes, seconds = 0, 0, 0
|
||||
semicolon_count = interval_str.count(":")
|
||||
try:
|
||||
if semicolon_count == 0:
|
||||
minutes = int(interval_str)
|
||||
elif semicolon_count == 1:
|
||||
hours, minutes = map(int, interval_str.split(":"))
|
||||
elif semicolon_count == 2:
|
||||
hours, minutes, seconds = map(int, interval_str.split(":"))
|
||||
else:
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if seconds < 0 or minutes < 0 or hours < 0:
|
||||
return None
|
||||
if semicolon_count > 0 and (seconds >= 60 or minutes >= 60):
|
||||
return None
|
||||
|
||||
interval = seconds + 60 * minutes + 60 * 60 * hours
|
||||
if interval == 0:
|
||||
return None
|
||||
return interval
|
||||
|
||||
|
||||
def get_interval_in_seconds_from_tags(
|
||||
tags: dict, keys: str | Collection[str]
|
||||
) -> int | None:
|
||||
"""Extract time interval value from tags for keys among "keys".
|
||||
E.g., "interval" and "headway" means the same in OSM.
|
||||
Examples:
|
||||
interval=5 => 300
|
||||
headway:peak=00:01:30 => 90
|
||||
"""
|
||||
if isinstance(keys, str):
|
||||
keys = (keys,)
|
||||
|
||||
value = None
|
||||
for key in keys:
|
||||
if key in tags:
|
||||
value = tags[key]
|
||||
break
|
||||
if value is None:
|
||||
for key in keys:
|
||||
if value:
|
||||
break
|
||||
for tag_name in tags:
|
||||
if tag_name.startswith(key + ":"):
|
||||
value = tags[tag_name]
|
||||
break
|
||||
if not value:
|
||||
return None
|
||||
return osm_interval_to_seconds(value)
|
||||
|
||||
|
||||
def get_route_interval(tags: dict) -> int | None:
|
||||
return get_interval_in_seconds_from_tags(tags, ("interval", "headway"))
|
||||
|
||||
|
||||
def get_route_duration(tags: dict) -> int | None:
|
||||
return get_interval_in_seconds_from_tags(tags, "duration")
|
||||
|
||||
|
||||
class Route:
|
||||
"""The longest route for a city with a unique ref."""
|
||||
|
||||
@staticmethod
|
||||
def is_route(el: OsmElementT, modes: set[str]) -> bool:
|
||||
if (
|
||||
el["type"] != "relation"
|
||||
or el.get("tags", {}).get("type") != "route"
|
||||
):
|
||||
return False
|
||||
if "members" not in el:
|
||||
return False
|
||||
if el["tags"].get("route") not in modes:
|
||||
return False
|
||||
for k in CONSTRUCTION_KEYS:
|
||||
if k in el["tags"]:
|
||||
return False
|
||||
if "ref" not in el["tags"] and "name" not in el["tags"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def stopareas(self) -> Iterator[StopArea]:
|
||||
yielded_stopareas = set()
|
||||
for route_stop in self:
|
||||
stoparea = route_stop.stoparea
|
||||
if stoparea not in yielded_stopareas:
|
||||
yield stoparea
|
||||
yielded_stopareas.add(stoparea)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
relation: OsmElementT,
|
||||
city: City,
|
||||
master: OsmElementT | None = None,
|
||||
) -> None:
|
||||
assert Route.is_route(
|
||||
relation, city.modes
|
||||
), f"The relation does not seem to be a route: {relation}"
|
||||
self.city = city
|
||||
self.element: OsmElementT = relation
|
||||
self.id: IdT = el_id(relation)
|
||||
|
||||
self.ref = None
|
||||
self.name = None
|
||||
self.mode = None
|
||||
self.colour = None
|
||||
self.infill = None
|
||||
self.network = None
|
||||
self.interval = None
|
||||
self.duration = None
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.is_circular = False
|
||||
self.stops: list[RouteStop] = []
|
||||
# Would be a list of (lon, lat) for the longest stretch. Can be empty.
|
||||
self.tracks = None
|
||||
# Index of the first stop that is located on/near the self.tracks
|
||||
self.first_stop_on_rails_index = None
|
||||
# Index of the last stop that is located on/near the self.tracks
|
||||
self.last_stop_on_rails_index = None
|
||||
|
||||
self.process_tags(master)
|
||||
stop_position_elements = self.process_stop_members()
|
||||
self.process_tracks(stop_position_elements)
|
||||
|
||||
def build_longest_line(self) -> tuple[list[IdT], set[IdT]]:
|
||||
line_nodes: set[IdT] = set()
|
||||
last_track: list[IdT] = []
|
||||
track: list[IdT] = []
|
||||
warned_about_holes = False
|
||||
for m in self.element["members"]:
|
||||
el = self.city.elements.get(el_id(m), None)
|
||||
if not el or not StopArea.is_track(el):
|
||||
continue
|
||||
if "nodes" not in el or len(el["nodes"]) < 2:
|
||||
self.city.error("Cannot find nodes in a railway", el)
|
||||
continue
|
||||
nodes: list[IdT] = ["n{}".format(n) for n in el["nodes"]]
|
||||
if m["role"] == "backward":
|
||||
nodes.reverse()
|
||||
line_nodes.update(nodes)
|
||||
if not track:
|
||||
is_first = True
|
||||
track.extend(nodes)
|
||||
else:
|
||||
new_segment = list(nodes) # copying
|
||||
if new_segment[0] == track[-1]:
|
||||
track.extend(new_segment[1:])
|
||||
elif new_segment[-1] == track[-1]:
|
||||
track.extend(reversed(new_segment[:-1]))
|
||||
elif is_first and track[0] in (
|
||||
new_segment[0],
|
||||
new_segment[-1],
|
||||
):
|
||||
# We can reverse the track and try again
|
||||
track.reverse()
|
||||
if new_segment[0] == track[-1]:
|
||||
track.extend(new_segment[1:])
|
||||
else:
|
||||
track.extend(reversed(new_segment[:-1]))
|
||||
else:
|
||||
# Store the track if it is long and clean it
|
||||
if not warned_about_holes:
|
||||
self.city.warn(
|
||||
"Hole in route rails near node {}".format(
|
||||
track[-1]
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
warned_about_holes = True
|
||||
if len(track) > len(last_track):
|
||||
last_track = track
|
||||
track = []
|
||||
is_first = False
|
||||
if len(track) > len(last_track):
|
||||
last_track = track
|
||||
# Remove duplicate points
|
||||
last_track = [
|
||||
last_track[i]
|
||||
for i in range(0, len(last_track))
|
||||
if i == 0 or last_track[i - 1] != last_track[i]
|
||||
]
|
||||
return last_track, line_nodes
|
||||
|
||||
def get_stop_projections(self) -> tuple[list[dict], Callable[[int], bool]]:
|
||||
projected = [project_on_line(x.stop, self.tracks) for x in self.stops]
|
||||
|
||||
def stop_near_tracks_criterion(stop_index: int) -> bool:
|
||||
return (
|
||||
projected[stop_index]["projected_point"] is not None
|
||||
and distance(
|
||||
self.stops[stop_index].stop,
|
||||
projected[stop_index]["projected_point"],
|
||||
)
|
||||
<= MAX_DISTANCE_STOP_TO_LINE
|
||||
)
|
||||
|
||||
return projected, stop_near_tracks_criterion
|
||||
|
||||
def project_stops_on_line(self) -> dict:
|
||||
projected, stop_near_tracks_criterion = self.get_stop_projections()
|
||||
|
||||
projected_stops_data = {
|
||||
"first_stop_on_rails_index": None,
|
||||
"last_stop_on_rails_index": None,
|
||||
"stops_on_longest_line": [], # list [{'route_stop': RouteStop,
|
||||
# 'coords': LonLat,
|
||||
# 'positions_on_rails': [] }
|
||||
}
|
||||
first_index = 0
|
||||
while first_index < len(self.stops) and not stop_near_tracks_criterion(
|
||||
first_index
|
||||
):
|
||||
first_index += 1
|
||||
projected_stops_data["first_stop_on_rails_index"] = first_index
|
||||
|
||||
last_index = len(self.stops) - 1
|
||||
while last_index > projected_stops_data[
|
||||
"first_stop_on_rails_index"
|
||||
] and not stop_near_tracks_criterion(last_index):
|
||||
last_index -= 1
|
||||
projected_stops_data["last_stop_on_rails_index"] = last_index
|
||||
|
||||
for i, route_stop in enumerate(self.stops):
|
||||
if not first_index <= i <= last_index:
|
||||
continue
|
||||
|
||||
if projected[i]["projected_point"] is None:
|
||||
self.city.error(
|
||||
'Stop "{}" {} is nowhere near the tracks'.format(
|
||||
route_stop.stoparea.name, route_stop.stop
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
else:
|
||||
stop_data = {
|
||||
"route_stop": route_stop,
|
||||
"coords": None,
|
||||
"positions_on_rails": None,
|
||||
}
|
||||
projected_point = projected[i]["projected_point"]
|
||||
# We've got two separate stations with a good stretch of
|
||||
# railway tracks between them. Put these on tracks.
|
||||
d = round(distance(route_stop.stop, projected_point))
|
||||
if d > MAX_DISTANCE_STOP_TO_LINE:
|
||||
self.city.notice(
|
||||
'Stop "{}" {} is {} meters from the tracks'.format(
|
||||
route_stop.stoparea.name, route_stop.stop, d
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
else:
|
||||
stop_data["coords"] = projected_point
|
||||
stop_data["positions_on_rails"] = projected[i][
|
||||
"positions_on_line"
|
||||
]
|
||||
projected_stops_data["stops_on_longest_line"].append(stop_data)
|
||||
return projected_stops_data
|
||||
|
||||
def calculate_distances(self) -> None:
|
||||
dist = 0
|
||||
vertex = 0
|
||||
for i, stop in enumerate(self.stops):
|
||||
if i > 0:
|
||||
direct = distance(stop.stop, self.stops[i - 1].stop)
|
||||
d_line = None
|
||||
if (
|
||||
self.first_stop_on_rails_index
|
||||
<= i
|
||||
<= self.last_stop_on_rails_index
|
||||
):
|
||||
d_line = distance_on_line(
|
||||
self.stops[i - 1].stop, stop.stop, self.tracks, vertex
|
||||
)
|
||||
if d_line and direct - 10 <= d_line[0] <= direct * 2:
|
||||
vertex = d_line[1]
|
||||
dist += round(d_line[0])
|
||||
else:
|
||||
dist += round(direct)
|
||||
stop.distance = dist
|
||||
|
||||
def process_tags(self, master: OsmElementT) -> None:
|
||||
relation = self.element
|
||||
tags = relation["tags"]
|
||||
master_tags = {} if not master else master["tags"]
|
||||
if "ref" not in tags and "ref" not in master_tags:
|
||||
self.city.notice("Missing ref on a route", relation)
|
||||
self.ref = tags.get(
|
||||
"ref", master_tags.get("ref", tags.get("name", None))
|
||||
)
|
||||
self.name = tags.get("name", None)
|
||||
self.mode = tags["route"]
|
||||
if (
|
||||
"colour" not in tags
|
||||
and "colour" not in master_tags
|
||||
and self.mode != "tram"
|
||||
):
|
||||
self.city.notice("Missing colour on a route", relation)
|
||||
try:
|
||||
self.colour = normalize_colour(
|
||||
tags.get("colour", master_tags.get("colour", None))
|
||||
)
|
||||
except ValueError as e:
|
||||
self.colour = None
|
||||
self.city.warn(str(e), relation)
|
||||
try:
|
||||
self.infill = normalize_colour(
|
||||
tags.get(
|
||||
"colour:infill", master_tags.get("colour:infill", None)
|
||||
)
|
||||
)
|
||||
except ValueError as e:
|
||||
self.infill = None
|
||||
self.city.warn(str(e), relation)
|
||||
self.network = get_network(relation)
|
||||
self.interval = get_route_interval(tags) or get_route_interval(
|
||||
master_tags
|
||||
)
|
||||
self.duration = get_route_duration(tags) or get_route_duration(
|
||||
master_tags
|
||||
)
|
||||
parsed_time_range = parse_time_range(
|
||||
tags.get("opening_hours", master_tags.get("opening_hours", ""))
|
||||
)
|
||||
if parsed_time_range:
|
||||
self.start_time, self.end_time = parsed_time_range
|
||||
|
||||
if tags.get("public_transport:version") == "1":
|
||||
self.city.warn(
|
||||
"Public transport version is 1, which means the route "
|
||||
"is an unsorted pile of objects",
|
||||
relation,
|
||||
)
|
||||
|
||||
def process_stop_members(self) -> list[OsmElementT]:
|
||||
stations: set[StopArea] = set() # temporary for recording stations
|
||||
seen_stops = False
|
||||
seen_platforms = False
|
||||
repeat_pos = None
|
||||
stop_position_elements: list[OsmElementT] = []
|
||||
for m in self.element["members"]:
|
||||
if "inactive" in m["role"]:
|
||||
continue
|
||||
k = el_id(m)
|
||||
if k in self.city.stations:
|
||||
st_list = self.city.stations[k]
|
||||
st = st_list[0]
|
||||
if len(st_list) > 1:
|
||||
self.city.error(
|
||||
f"Ambiguous station {st.name} in route. Please "
|
||||
"use stop_position or split interchange stations",
|
||||
self.element,
|
||||
)
|
||||
el = self.city.elements[k]
|
||||
actual_role = RouteStop.get_actual_role(
|
||||
el, m["role"], self.city.modes
|
||||
)
|
||||
if actual_role:
|
||||
if m["role"] and actual_role not in m["role"]:
|
||||
self.city.warn(
|
||||
"Wrong role '{}' for {} {}".format(
|
||||
m["role"], actual_role, k
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
if repeat_pos is None:
|
||||
if not self.stops or st not in stations:
|
||||
stop = RouteStop(st)
|
||||
self.stops.append(stop)
|
||||
stations.add(st)
|
||||
elif self.stops[-1].stoparea.id == st.id:
|
||||
stop = self.stops[-1]
|
||||
else:
|
||||
# We've got a repeat
|
||||
if (
|
||||
(seen_stops and seen_platforms)
|
||||
or (
|
||||
actual_role == "stop"
|
||||
and not seen_platforms
|
||||
)
|
||||
or (
|
||||
actual_role == "platform"
|
||||
and not seen_stops
|
||||
)
|
||||
):
|
||||
# Circular route!
|
||||
stop = RouteStop(st)
|
||||
self.stops.append(stop)
|
||||
stations.add(st)
|
||||
else:
|
||||
repeat_pos = 0
|
||||
if repeat_pos is not None:
|
||||
if repeat_pos >= len(self.stops):
|
||||
continue
|
||||
# Check that the type matches
|
||||
if (actual_role == "stop" and seen_stops) or (
|
||||
actual_role == "platform" and seen_platforms
|
||||
):
|
||||
self.city.error(
|
||||
'Found an out-of-place {}: "{}" ({})'.format(
|
||||
actual_role, el["tags"].get("name", ""), k
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
continue
|
||||
# Find the matching stop starting with index repeat_pos
|
||||
while (
|
||||
repeat_pos < len(self.stops)
|
||||
and self.stops[repeat_pos].stoparea.id != st.id
|
||||
):
|
||||
repeat_pos += 1
|
||||
if repeat_pos >= len(self.stops):
|
||||
self.city.error(
|
||||
"Incorrect order of {}s at {}".format(
|
||||
actual_role, k
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
continue
|
||||
stop = self.stops[repeat_pos]
|
||||
|
||||
stop.add(m, self.element, self.city)
|
||||
if repeat_pos is None:
|
||||
seen_stops |= stop.seen_stop or stop.seen_station
|
||||
seen_platforms |= stop.seen_platform
|
||||
|
||||
if StopArea.is_stop(el):
|
||||
stop_position_elements.append(el)
|
||||
|
||||
continue
|
||||
|
||||
if k not in self.city.elements:
|
||||
if "stop" in m["role"] or "platform" in m["role"]:
|
||||
raise CriticalValidationError(
|
||||
f"{m['role']} {m['type']} {m['ref']} for route "
|
||||
f"relation {self.element['id']} is not in the dataset"
|
||||
)
|
||||
continue
|
||||
el = self.city.elements[k]
|
||||
if "tags" not in el:
|
||||
self.city.error(
|
||||
f"Untagged object {k} in a route", self.element
|
||||
)
|
||||
continue
|
||||
|
||||
is_under_construction = False
|
||||
for ck in CONSTRUCTION_KEYS:
|
||||
if ck in el["tags"]:
|
||||
self.city.warn(
|
||||
f"Under construction {m['role'] or 'feature'} {k} "
|
||||
"in route. Consider setting 'inactive' role or "
|
||||
"removing construction attributes",
|
||||
self.element,
|
||||
)
|
||||
is_under_construction = True
|
||||
break
|
||||
if is_under_construction:
|
||||
continue
|
||||
|
||||
if Station.is_station(el, self.city.modes):
|
||||
# A station may be not included in this route due to previous
|
||||
# 'stop area has multiple stations' error. No other error
|
||||
# message is needed.
|
||||
pass
|
||||
elif el["tags"].get("railway") in ("station", "halt"):
|
||||
self.city.error(
|
||||
"Missing station={} on a {}".format(self.mode, m["role"]),
|
||||
el,
|
||||
)
|
||||
else:
|
||||
actual_role = RouteStop.get_actual_role(
|
||||
el, m["role"], self.city.modes
|
||||
)
|
||||
if actual_role:
|
||||
self.city.error(
|
||||
f"{actual_role} {m['type']} {m['ref']} is not "
|
||||
"connected to a station in route",
|
||||
self.element,
|
||||
)
|
||||
elif not StopArea.is_track(el):
|
||||
self.city.warn(
|
||||
"Unknown member type for {} {} in route".format(
|
||||
m["type"], m["ref"]
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
return stop_position_elements
|
||||
|
||||
def process_tracks(
|
||||
self, stop_position_elements: list[OsmElementT]
|
||||
) -> None:
|
||||
tracks, line_nodes = self.build_longest_line()
|
||||
|
||||
for stop_el in stop_position_elements:
|
||||
stop_id = el_id(stop_el)
|
||||
if stop_id not in line_nodes:
|
||||
self.city.warn(
|
||||
'Stop position "{}" ({}) is not on tracks'.format(
|
||||
stop_el["tags"].get("name", ""), stop_id
|
||||
),
|
||||
self.element,
|
||||
)
|
||||
|
||||
# self.tracks would be a list of (lon, lat) for the longest stretch.
|
||||
# Can be empty.
|
||||
self.tracks = [el_center(self.city.elements.get(k)) for k in tracks]
|
||||
if (
|
||||
None in self.tracks
|
||||
): # usually, extending BBOX for the city is needed
|
||||
self.tracks = []
|
||||
for n in filter(lambda x: x not in self.city.elements, tracks):
|
||||
self.city.warn(
|
||||
f"The dataset is missing the railway tracks node {n}",
|
||||
self.element,
|
||||
)
|
||||
break
|
||||
|
||||
if len(self.stops) > 1:
|
||||
self.is_circular = (
|
||||
self.stops[0].stoparea == self.stops[-1].stoparea
|
||||
)
|
||||
if (
|
||||
self.is_circular
|
||||
and self.tracks
|
||||
and self.tracks[0] != self.tracks[-1]
|
||||
):
|
||||
self.city.warn(
|
||||
"Non-closed rail sequence in a circular route",
|
||||
self.element,
|
||||
)
|
||||
|
||||
projected_stops_data = self.project_stops_on_line()
|
||||
self.check_and_recover_stops_order(projected_stops_data)
|
||||
self.apply_projected_stops_data(projected_stops_data)
|
||||
|
||||
def apply_projected_stops_data(self, projected_stops_data: dict) -> None:
|
||||
"""Store better stop coordinates and indexes of first/last stops
|
||||
that lie on a continuous track line, to the instance attributes.
|
||||
"""
|
||||
for attr in ("first_stop_on_rails_index", "last_stop_on_rails_index"):
|
||||
setattr(self, attr, projected_stops_data[attr])
|
||||
|
||||
for stop_data in projected_stops_data["stops_on_longest_line"]:
|
||||
route_stop = stop_data["route_stop"]
|
||||
route_stop.positions_on_rails = stop_data["positions_on_rails"]
|
||||
if stop_coords := stop_data["coords"]:
|
||||
route_stop.stop = stop_coords
|
||||
|
||||
def get_extended_tracks(self) -> RailT:
|
||||
"""Amend tracks with points of leading/trailing self.stops
|
||||
that were not projected onto the longest tracks line.
|
||||
Return a new array.
|
||||
"""
|
||||
if self.first_stop_on_rails_index >= len(self.stops):
|
||||
tracks = [route_stop.stop for route_stop in self.stops]
|
||||
else:
|
||||
tracks = (
|
||||
[
|
||||
route_stop.stop
|
||||
for i, route_stop in enumerate(self.stops)
|
||||
if i < self.first_stop_on_rails_index
|
||||
]
|
||||
+ self.tracks
|
||||
+ [
|
||||
route_stop.stop
|
||||
for i, route_stop in enumerate(self.stops)
|
||||
if i > self.last_stop_on_rails_index
|
||||
]
|
||||
)
|
||||
return tracks
|
||||
|
||||
def get_truncated_tracks(self, tracks: RailT) -> RailT:
|
||||
"""Truncate leading/trailing segments of `tracks` param
|
||||
that are beyond the first and last stop locations.
|
||||
Return a new array.
|
||||
"""
|
||||
if self.is_circular:
|
||||
return tracks.copy()
|
||||
|
||||
first_stop_location = find_segment(self.stops[0].stop, tracks, 0)
|
||||
last_stop_location = find_segment(self.stops[-1].stop, tracks, 0)
|
||||
|
||||
if last_stop_location != (None, None):
|
||||
seg2, u2 = last_stop_location
|
||||
if u2 == 0.0:
|
||||
# Make seg2 the segment the last_stop_location is
|
||||
# at the middle or end of
|
||||
seg2 -= 1
|
||||
# u2 = 1.0
|
||||
if seg2 + 2 < len(tracks):
|
||||
tracks = tracks[0 : seg2 + 2] # noqa E203
|
||||
tracks[-1] = self.stops[-1].stop
|
||||
|
||||
if first_stop_location != (None, None):
|
||||
seg1, u1 = first_stop_location
|
||||
if u1 == 1.0:
|
||||
# Make seg1 the segment the first_stop_location is
|
||||
# at the beginning or middle of
|
||||
seg1 += 1
|
||||
# u1 = 0.0
|
||||
if seg1 > 0:
|
||||
tracks = tracks[seg1:]
|
||||
tracks[0] = self.stops[0].stop
|
||||
|
||||
return tracks
|
||||
|
||||
def are_tracks_complete(self) -> bool:
|
||||
return (
|
||||
self.first_stop_on_rails_index == 0
|
||||
and self.last_stop_on_rails_index == len(self) - 1
|
||||
)
|
||||
|
||||
def get_tracks_geometry(self) -> RailT:
|
||||
tracks = self.get_extended_tracks()
|
||||
tracks = self.get_truncated_tracks(tracks)
|
||||
return tracks
|
||||
|
||||
def check_stops_order_by_angle(self) -> tuple[list[str], list[str]]:
|
||||
disorder_warnings = []
|
||||
disorder_errors = []
|
||||
for i, route_stop in enumerate(
|
||||
islice(self.stops, 1, len(self.stops) - 1), start=1
|
||||
):
|
||||
angle = angle_between(
|
||||
self.stops[i - 1].stop,
|
||||
route_stop.stop,
|
||||
self.stops[i + 1].stop,
|
||||
)
|
||||
if angle < ALLOWED_ANGLE_BETWEEN_STOPS:
|
||||
msg = (
|
||||
"Angle between stops around "
|
||||
f'"{route_stop.stoparea.name}" {route_stop.stop} '
|
||||
f"is too narrow, {angle} degrees"
|
||||
)
|
||||
if angle < DISALLOWED_ANGLE_BETWEEN_STOPS:
|
||||
disorder_errors.append(msg)
|
||||
else:
|
||||
disorder_warnings.append(msg)
|
||||
return disorder_warnings, disorder_errors
|
||||
|
||||
def check_stops_order_on_tracks_direct(
|
||||
self, stop_sequence: Iterator[dict]
|
||||
) -> str | None:
|
||||
"""Checks stops order on tracks, following stop_sequence
|
||||
in direct order only.
|
||||
:param stop_sequence: list of dict{'route_stop', 'positions_on_rails',
|
||||
'coords'} for RouteStops that belong to the longest contiguous
|
||||
sequence of tracks in a route.
|
||||
:return: error message on the first order violation or None.
|
||||
"""
|
||||
allowed_order_violations = 1 if self.is_circular else 0
|
||||
max_position_on_rails = -1
|
||||
for stop_data in stop_sequence:
|
||||
positions_on_rails = stop_data["positions_on_rails"]
|
||||
suitable_occurrence = 0
|
||||
while (
|
||||
suitable_occurrence < len(positions_on_rails)
|
||||
and positions_on_rails[suitable_occurrence]
|
||||
< max_position_on_rails
|
||||
):
|
||||
suitable_occurrence += 1
|
||||
if suitable_occurrence == len(positions_on_rails):
|
||||
if allowed_order_violations > 0:
|
||||
suitable_occurrence -= 1
|
||||
allowed_order_violations -= 1
|
||||
else:
|
||||
route_stop = stop_data["route_stop"]
|
||||
return (
|
||||
"Stops on tracks are unordered near "
|
||||
f'"{route_stop.stoparea.name}" {route_stop.stop}'
|
||||
)
|
||||
max_position_on_rails = positions_on_rails[suitable_occurrence]
|
||||
|
||||
def check_stops_order_on_tracks(
|
||||
self, projected_stops_data: dict
|
||||
) -> str | None:
|
||||
"""Checks stops order on tracks, trying direct and reversed
|
||||
order of stops in the stop_sequence.
|
||||
:param projected_stops_data: info about RouteStops that belong to the
|
||||
longest contiguous sequence of tracks in a route. May be changed
|
||||
if tracks reversing is performed.
|
||||
:return: error message on the first order violation or None.
|
||||
"""
|
||||
error_message = self.check_stops_order_on_tracks_direct(
|
||||
projected_stops_data["stops_on_longest_line"]
|
||||
)
|
||||
if error_message:
|
||||
error_message_reversed = self.check_stops_order_on_tracks_direct(
|
||||
reversed(projected_stops_data["stops_on_longest_line"])
|
||||
)
|
||||
if error_message_reversed is None:
|
||||
error_message = None
|
||||
self.city.warn(
|
||||
"Tracks seem to go in the opposite direction to stops",
|
||||
self.element,
|
||||
)
|
||||
self.tracks.reverse()
|
||||
new_projected_stops_data = self.project_stops_on_line()
|
||||
projected_stops_data.update(new_projected_stops_data)
|
||||
|
||||
return error_message
|
||||
|
||||
def check_stops_order(
|
||||
self, projected_stops_data: dict
|
||||
) -> tuple[list[str], list[str]]:
|
||||
(
|
||||
angle_disorder_warnings,
|
||||
angle_disorder_errors,
|
||||
) = self.check_stops_order_by_angle()
|
||||
disorder_on_tracks_error = self.check_stops_order_on_tracks(
|
||||
projected_stops_data
|
||||
)
|
||||
disorder_warnings = angle_disorder_warnings
|
||||
disorder_errors = angle_disorder_errors
|
||||
if disorder_on_tracks_error:
|
||||
disorder_errors.append(disorder_on_tracks_error)
|
||||
return disorder_warnings, disorder_errors
|
||||
|
||||
def check_and_recover_stops_order(
|
||||
self, projected_stops_data: dict
|
||||
) -> None:
|
||||
"""
|
||||
:param projected_stops_data: may change if we need to reverse tracks
|
||||
"""
|
||||
disorder_warnings, disorder_errors = self.check_stops_order(
|
||||
projected_stops_data
|
||||
)
|
||||
if disorder_warnings or disorder_errors:
|
||||
resort_success = False
|
||||
if self.city.recovery_data:
|
||||
resort_success = self.try_resort_stops()
|
||||
if resort_success:
|
||||
for msg in disorder_warnings:
|
||||
self.city.notice(msg, self.element)
|
||||
for msg in disorder_errors:
|
||||
self.city.warn(
|
||||
"Fixed with recovery data: " + msg, self.element
|
||||
)
|
||||
|
||||
if not resort_success:
|
||||
for msg in disorder_warnings:
|
||||
self.city.notice(msg, self.element)
|
||||
for msg in disorder_errors:
|
||||
self.city.error(msg, self.element)
|
||||
|
||||
def try_resort_stops(self) -> bool:
|
||||
"""Precondition: self.city.recovery_data is not None.
|
||||
Return success of station order recovering."""
|
||||
self_stops = {} # station name => RouteStop
|
||||
for stop in self.stops:
|
||||
station = stop.stoparea.station
|
||||
stop_name = station.name
|
||||
if stop_name == "?" and station.int_name:
|
||||
stop_name = station.int_name
|
||||
# We won't programmatically recover routes with repeating stations:
|
||||
# such cases are rare and deserves manual verification
|
||||
if stop_name in self_stops:
|
||||
return False
|
||||
self_stops[stop_name] = stop
|
||||
|
||||
route_id = (self.colour, self.ref)
|
||||
if route_id not in self.city.recovery_data:
|
||||
return False
|
||||
|
||||
stop_names = list(self_stops.keys())
|
||||
suitable_itineraries = []
|
||||
for itinerary in self.city.recovery_data[route_id]:
|
||||
itinerary_stop_names = [
|
||||
stop["name"] for stop in itinerary["stations"]
|
||||
]
|
||||
if not (
|
||||
len(stop_names) == len(itinerary_stop_names)
|
||||
and sorted(stop_names) == sorted(itinerary_stop_names)
|
||||
):
|
||||
continue
|
||||
big_station_displacement = False
|
||||
for it_stop in itinerary["stations"]:
|
||||
name = it_stop["name"]
|
||||
it_stop_center = it_stop["center"]
|
||||
self_stop_center = self_stops[name].stoparea.station.center
|
||||
if (
|
||||
distance(it_stop_center, self_stop_center)
|
||||
> DISPLACEMENT_TOLERANCE
|
||||
):
|
||||
big_station_displacement = True
|
||||
break
|
||||
if not big_station_displacement:
|
||||
suitable_itineraries.append(itinerary)
|
||||
|
||||
if len(suitable_itineraries) == 0:
|
||||
return False
|
||||
elif len(suitable_itineraries) == 1:
|
||||
matching_itinerary = suitable_itineraries[0]
|
||||
else:
|
||||
from_tag = self.element["tags"].get("from")
|
||||
to_tag = self.element["tags"].get("to")
|
||||
if not from_tag and not to_tag:
|
||||
return False
|
||||
matching_itineraries = [
|
||||
itin
|
||||
for itin in suitable_itineraries
|
||||
if from_tag
|
||||
and itin["from"] == from_tag
|
||||
or to_tag
|
||||
and itin["to"] == to_tag
|
||||
]
|
||||
if len(matching_itineraries) != 1:
|
||||
return False
|
||||
matching_itinerary = matching_itineraries[0]
|
||||
self.stops = [
|
||||
self_stops[stop["name"]] for stop in matching_itinerary["stations"]
|
||||
]
|
||||
return True
|
||||
|
||||
def get_end_transfers(self) -> tuple[IdT, IdT]:
|
||||
"""Using transfer ids because a train can arrive at different
|
||||
stations within a transfer. But disregard transfer that may give
|
||||
an impression of a circular route (for example,
|
||||
Simonis / Elisabeth station and route 2 in Brussels).
|
||||
"""
|
||||
return (
|
||||
(self[0].stoparea.id, self[-1].stoparea.id)
|
||||
if (
|
||||
self[0].stoparea.transfer is not None
|
||||
and self[0].stoparea.transfer == self[-1].stoparea.transfer
|
||||
)
|
||||
else (
|
||||
self[0].stoparea.transfer or self[0].stoparea.id,
|
||||
self[-1].stoparea.transfer or self[-1].stoparea.id,
|
||||
)
|
||||
)
|
||||
|
||||
def get_transfers_sequence(self) -> list[IdT]:
|
||||
"""Return a list of stoparea or transfer (if not None) ids."""
|
||||
transfer_seq = [
|
||||
stop.stoparea.transfer or stop.stoparea.id for stop in self
|
||||
]
|
||||
if (
|
||||
self[0].stoparea.transfer is not None
|
||||
and self[0].stoparea.transfer == self[-1].stoparea.transfer
|
||||
):
|
||||
transfer_seq[0], transfer_seq[-1] = self.get_end_transfers()
|
||||
return transfer_seq
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.stops)
|
||||
|
||||
def __getitem__(self, i) -> RouteStop:
|
||||
return self.stops[i]
|
||||
|
||||
def __iter__(self) -> Iterator[RouteStop]:
|
||||
return iter(self.stops)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"Route(id={}, mode={}, ref={}, name={}, network={}, interval={}, "
|
||||
"circular={}, num_stops={}, line_length={} m, from={}, to={}"
|
||||
).format(
|
||||
self.id,
|
||||
self.mode,
|
||||
self.ref,
|
||||
self.name,
|
||||
self.network,
|
||||
self.interval,
|
||||
self.is_circular,
|
||||
len(self.stops),
|
||||
self.stops[-1].distance,
|
||||
self.stops[0],
|
||||
self.stops[-1],
|
||||
)
|
|
@ -1,473 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from collections.abc import Iterator
|
||||
from typing import TypeVar
|
||||
|
||||
from subways.consts import MAX_DISTANCE_STOP_TO_LINE
|
||||
from subways.css_colours import normalize_colour
|
||||
from subways.geom_utils import distance, project_on_line
|
||||
from subways.osm_element import el_id, get_network
|
||||
from subways.structure.route import get_route_duration, get_route_interval
|
||||
from subways.structure.stop_area import StopArea
|
||||
from subways.types import IdT, OsmElementT
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
from subways.structure.route_stop import RouteStop
|
||||
|
||||
|
||||
SUGGEST_TRANSFER_MIN_DISTANCE = 100 # in meters
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class RouteMaster:
|
||||
def __init__(self, city: City, master: OsmElementT = None) -> None:
|
||||
self.city = city
|
||||
self.routes = []
|
||||
self.best: Route = None # noqa: F821
|
||||
self.id: IdT = el_id(master)
|
||||
self.has_master = master is not None
|
||||
self.interval_from_master = False
|
||||
if master:
|
||||
self.ref = master["tags"].get(
|
||||
"ref", master["tags"].get("name", None)
|
||||
)
|
||||
try:
|
||||
self.colour = normalize_colour(
|
||||
master["tags"].get("colour", None)
|
||||
)
|
||||
except ValueError:
|
||||
self.colour = None
|
||||
try:
|
||||
self.infill = normalize_colour(
|
||||
master["tags"].get("colour:infill", None)
|
||||
)
|
||||
except ValueError:
|
||||
self.infill = None
|
||||
self.network = get_network(master)
|
||||
self.mode = master["tags"].get(
|
||||
"route_master", None
|
||||
) # This tag is required, but okay
|
||||
self.name = master["tags"].get("name", None)
|
||||
self.interval = get_route_interval(master["tags"])
|
||||
self.interval_from_master = self.interval is not None
|
||||
self.duration = get_route_duration(master["tags"])
|
||||
else:
|
||||
self.ref = None
|
||||
self.colour = None
|
||||
self.infill = None
|
||||
self.network = None
|
||||
self.mode = None
|
||||
self.name = None
|
||||
self.interval = None
|
||||
self.duration = None
|
||||
|
||||
def stopareas(self) -> Iterator[StopArea]:
|
||||
yielded_stopareas = set()
|
||||
for route in self:
|
||||
for stoparea in route.stopareas():
|
||||
if stoparea not in yielded_stopareas:
|
||||
yield stoparea
|
||||
yielded_stopareas.add(stoparea)
|
||||
|
||||
def add(self, route: Route) -> None: # noqa: F821
|
||||
if not self.network:
|
||||
self.network = route.network
|
||||
elif route.network and route.network != self.network:
|
||||
self.city.error(
|
||||
'Route has different network ("{}") from master "{}"'.format(
|
||||
route.network, self.network
|
||||
),
|
||||
route.element,
|
||||
)
|
||||
|
||||
if not self.colour:
|
||||
self.colour = route.colour
|
||||
elif route.colour and route.colour != self.colour:
|
||||
self.city.notice(
|
||||
'Route "{}" has different colour from master "{}"'.format(
|
||||
route.colour, self.colour
|
||||
),
|
||||
route.element,
|
||||
)
|
||||
|
||||
if not self.infill:
|
||||
self.infill = route.infill
|
||||
elif route.infill and route.infill != self.infill:
|
||||
self.city.notice(
|
||||
(
|
||||
f'Route "{route.infill}" has different infill colour '
|
||||
f'from master "{self.infill}"'
|
||||
),
|
||||
route.element,
|
||||
)
|
||||
|
||||
if not self.ref:
|
||||
self.ref = route.ref
|
||||
elif route.ref != self.ref:
|
||||
self.city.notice(
|
||||
'Route "{}" has different ref from master "{}"'.format(
|
||||
route.ref, self.ref
|
||||
),
|
||||
route.element,
|
||||
)
|
||||
|
||||
if not self.name:
|
||||
self.name = route.name
|
||||
|
||||
if not self.mode:
|
||||
self.mode = route.mode
|
||||
elif route.mode != self.mode:
|
||||
self.city.error(
|
||||
"Incompatible PT mode: master has {} and route has {}".format(
|
||||
self.mode, route.mode
|
||||
),
|
||||
route.element,
|
||||
)
|
||||
return
|
||||
|
||||
if not self.interval_from_master and route.interval:
|
||||
if not self.interval:
|
||||
self.interval = route.interval
|
||||
else:
|
||||
self.interval = min(self.interval, route.interval)
|
||||
|
||||
# Choose minimal id for determinancy
|
||||
if not self.has_master and (not self.id or self.id > route.id):
|
||||
self.id = route.id
|
||||
|
||||
self.routes.append(route)
|
||||
if (
|
||||
not self.best
|
||||
or len(route.stops) > len(self.best.stops)
|
||||
or (
|
||||
# Choose route with minimal id for determinancy
|
||||
len(route.stops) == len(self.best.stops)
|
||||
and route.element["id"] < self.best.element["id"]
|
||||
)
|
||||
):
|
||||
self.best = route
|
||||
|
||||
def get_meaningful_routes(self) -> list[Route]: # noqa: F821
|
||||
return [route for route in self if len(route) >= 2]
|
||||
|
||||
def find_twin_routes(self) -> dict[Route, Route]: # noqa: F821
|
||||
"""Two non-circular routes are twins if they have the same end
|
||||
stations and opposite directions, and the number of stations is
|
||||
the same or almost the same. We'll then find stops that are present
|
||||
in one direction and is missing in another direction - to warn.
|
||||
"""
|
||||
|
||||
twin_routes = {} # route => "twin" route
|
||||
|
||||
for route in self.get_meaningful_routes():
|
||||
if route.is_circular:
|
||||
continue # Difficult to calculate. TODO(?) in the future
|
||||
if route in twin_routes:
|
||||
continue
|
||||
|
||||
route_transfer_ids = set(route.get_transfers_sequence())
|
||||
ends = route.get_end_transfers()
|
||||
ends_reversed = ends[::-1]
|
||||
|
||||
twin_candidates = [
|
||||
r
|
||||
for r in self
|
||||
if not r.is_circular
|
||||
and r not in twin_routes
|
||||
and r.get_end_transfers() == ends_reversed
|
||||
# If absolute or relative difference in station count is large,
|
||||
# possibly it's an express version of a route - skip it.
|
||||
and (
|
||||
abs(len(r) - len(route)) <= 2
|
||||
or abs(len(r) - len(route)) / max(len(r), len(route))
|
||||
<= 0.2
|
||||
)
|
||||
]
|
||||
|
||||
if not twin_candidates:
|
||||
continue
|
||||
|
||||
twin_route = min(
|
||||
twin_candidates,
|
||||
key=lambda r: len(
|
||||
route_transfer_ids ^ set(r.get_transfers_sequence())
|
||||
),
|
||||
)
|
||||
twin_routes[route] = twin_route
|
||||
twin_routes[twin_route] = route
|
||||
|
||||
return twin_routes
|
||||
|
||||
def check_return_routes(self) -> None:
|
||||
"""Check if a route has return direction, and if twin routes
|
||||
miss stations.
|
||||
"""
|
||||
meaningful_routes = self.get_meaningful_routes()
|
||||
|
||||
if len(meaningful_routes) == 0:
|
||||
self.city.error(
|
||||
f"An empty route master {self.id}. "
|
||||
"Please set construction:route if it is under construction"
|
||||
)
|
||||
elif len(meaningful_routes) == 1:
|
||||
log_function = (
|
||||
self.city.error
|
||||
if not self.best.is_circular
|
||||
else self.city.notice
|
||||
)
|
||||
log_function(
|
||||
"Only one route in route_master. "
|
||||
"Please check if it needs a return route",
|
||||
self.best.element,
|
||||
)
|
||||
else:
|
||||
self.check_return_circular_routes()
|
||||
self.check_return_noncircular_routes()
|
||||
|
||||
def check_return_noncircular_routes(self) -> None:
|
||||
routes = [
|
||||
route
|
||||
for route in self.get_meaningful_routes()
|
||||
if not route.is_circular
|
||||
]
|
||||
all_ends = {route.get_end_transfers(): route for route in routes}
|
||||
for route in routes:
|
||||
ends = route.get_end_transfers()
|
||||
if ends[::-1] not in all_ends:
|
||||
self.city.notice(
|
||||
"Route does not have a return direction", route.element
|
||||
)
|
||||
|
||||
twin_routes = self.find_twin_routes()
|
||||
for route1, route2 in twin_routes.items():
|
||||
if route1.id > route2.id:
|
||||
continue # to process a pair of routes only once
|
||||
# and to ensure the order of routes in the pair
|
||||
self.alert_twin_routes_differ(route1, route2)
|
||||
|
||||
def check_return_circular_routes(self) -> None:
|
||||
routes = {
|
||||
route
|
||||
for route in self.get_meaningful_routes()
|
||||
if route.is_circular
|
||||
}
|
||||
routes_having_backward = set()
|
||||
|
||||
for route in routes:
|
||||
if route in routes_having_backward:
|
||||
continue
|
||||
transfer_sequence1 = [
|
||||
stop.stoparea.transfer or stop.stoparea.id for stop in route
|
||||
]
|
||||
transfer_sequence1.pop()
|
||||
for potential_backward_route in routes - {route}:
|
||||
transfer_sequence2 = [
|
||||
stop.stoparea.transfer or stop.stoparea.id
|
||||
for stop in potential_backward_route
|
||||
][
|
||||
-2::-1
|
||||
] # truncate repeated first stop and reverse
|
||||
common_subsequence = self.find_common_circular_subsequence(
|
||||
transfer_sequence1, transfer_sequence2
|
||||
)
|
||||
if len(common_subsequence) >= 0.8 * min(
|
||||
len(transfer_sequence1), len(transfer_sequence2)
|
||||
):
|
||||
routes_having_backward.add(route)
|
||||
routes_having_backward.add(potential_backward_route)
|
||||
break
|
||||
|
||||
for route in routes - routes_having_backward:
|
||||
self.city.notice(
|
||||
"Route does not have a return direction", route.element
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_common_circular_subsequence(
|
||||
seq1: list[T], seq2: list[T]
|
||||
) -> list[T]:
|
||||
"""seq1 and seq2 are supposed to be stops of some circular routes.
|
||||
Prerequisites to rely on the result:
|
||||
- elements of each sequence are not repeated
|
||||
- the order of stations is not violated.
|
||||
Under these conditions we don't need LCS algorithm. Linear scan is
|
||||
sufficient.
|
||||
"""
|
||||
i1, i2 = -1, -1
|
||||
for i1, x in enumerate(seq1):
|
||||
try:
|
||||
i2 = seq2.index(x)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
# x is found both in seq1 and seq2
|
||||
break
|
||||
|
||||
if i2 == -1:
|
||||
return []
|
||||
|
||||
# Shift cyclically so that the common element takes the first position
|
||||
# both in seq1 and seq2
|
||||
seq1 = seq1[i1:] + seq1[:i1]
|
||||
seq2 = seq2[i2:] + seq2[:i2]
|
||||
|
||||
common_subsequence = []
|
||||
i2 = 0
|
||||
for x in seq1:
|
||||
try:
|
||||
i2 = seq2.index(x, i2)
|
||||
except ValueError:
|
||||
continue
|
||||
common_subsequence.append(x)
|
||||
i2 += 1
|
||||
if i2 >= len(seq2):
|
||||
break
|
||||
return common_subsequence
|
||||
|
||||
def alert_twin_routes_differ(
|
||||
self,
|
||||
route1: Route, # noqa: F821
|
||||
route2: Route, # noqa: F821
|
||||
) -> None:
|
||||
"""Arguments are that route1.id < route2.id"""
|
||||
(
|
||||
stops_missing_from_route1,
|
||||
stops_missing_from_route2,
|
||||
stops_that_dont_match,
|
||||
) = self.calculate_twin_routes_diff(route1, route2)
|
||||
|
||||
for st in stops_missing_from_route1:
|
||||
if (
|
||||
not route1.are_tracks_complete()
|
||||
or (
|
||||
projected_point := project_on_line(
|
||||
st.stoparea.center, route1.tracks
|
||||
)["projected_point"]
|
||||
)
|
||||
is not None
|
||||
and distance(st.stoparea.center, projected_point)
|
||||
<= MAX_DISTANCE_STOP_TO_LINE
|
||||
):
|
||||
self.city.notice(
|
||||
f"Stop {st.stoparea.station.name} {st.stop} is included "
|
||||
f"in the {route2.id} but not included in {route1.id}",
|
||||
route1.element,
|
||||
)
|
||||
|
||||
for st in stops_missing_from_route2:
|
||||
if (
|
||||
not route2.are_tracks_complete()
|
||||
or (
|
||||
projected_point := project_on_line(
|
||||
st.stoparea.center, route2.tracks
|
||||
)["projected_point"]
|
||||
)
|
||||
is not None
|
||||
and distance(st.stoparea.center, projected_point)
|
||||
<= MAX_DISTANCE_STOP_TO_LINE
|
||||
):
|
||||
self.city.notice(
|
||||
f"Stop {st.stoparea.station.name} {st.stop} is included "
|
||||
f"in the {route1.id} but not included in {route2.id}",
|
||||
route2.element,
|
||||
)
|
||||
|
||||
for st1, st2 in stops_that_dont_match:
|
||||
if (
|
||||
st1.stoparea.station == st2.stoparea.station
|
||||
or distance(st1.stop, st2.stop) < SUGGEST_TRANSFER_MIN_DISTANCE
|
||||
):
|
||||
self.city.notice(
|
||||
"Should there be one stoparea or a transfer between "
|
||||
f"{st1.stoparea.station.name} {st1.stop} and "
|
||||
f"{st2.stoparea.station.name} {st2.stop}?",
|
||||
route1.element,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def calculate_twin_routes_diff(
|
||||
route1: Route, # noqa: F821
|
||||
route2: Route, # noqa: F821
|
||||
) -> tuple:
|
||||
"""Wagner–Fischer algorithm for stops diff in two twin routes."""
|
||||
|
||||
stops1 = route1.stops
|
||||
stops2 = route2.stops[::-1]
|
||||
|
||||
def stops_match(stop1: RouteStop, stop2: RouteStop) -> bool:
|
||||
return (
|
||||
stop1.stoparea == stop2.stoparea
|
||||
or stop1.stoparea.transfer is not None
|
||||
and stop1.stoparea.transfer == stop2.stoparea.transfer
|
||||
)
|
||||
|
||||
d = [[0] * (len(stops2) + 1) for _ in range(len(stops1) + 1)]
|
||||
d[0] = list(range(len(stops2) + 1))
|
||||
for i in range(len(stops1) + 1):
|
||||
d[i][0] = i
|
||||
|
||||
for i in range(1, len(stops1) + 1):
|
||||
for j in range(1, len(stops2) + 1):
|
||||
d[i][j] = (
|
||||
d[i - 1][j - 1]
|
||||
if stops_match(stops1[i - 1], stops2[j - 1])
|
||||
else min((d[i - 1][j], d[i][j - 1], d[i - 1][j - 1])) + 1
|
||||
)
|
||||
|
||||
stops_missing_from_route1: list[RouteStop] = []
|
||||
stops_missing_from_route2: list[RouteStop] = []
|
||||
stops_that_dont_match: list[tuple[RouteStop, RouteStop]] = []
|
||||
|
||||
i = len(stops1)
|
||||
j = len(stops2)
|
||||
while not (i == 0 and j == 0):
|
||||
action = None
|
||||
if i > 0 and j > 0:
|
||||
match = stops_match(stops1[i - 1], stops2[j - 1])
|
||||
if match and d[i - 1][j - 1] == d[i][j]:
|
||||
action = "no"
|
||||
elif not match and d[i - 1][j - 1] + 1 == d[i][j]:
|
||||
action = "change"
|
||||
if not action and i > 0 and d[i - 1][j] + 1 == d[i][j]:
|
||||
action = "add_2"
|
||||
if not action and j > 0 and d[i][j - 1] + 1 == d[i][j]:
|
||||
action = "add_1"
|
||||
|
||||
match action:
|
||||
case "add_1":
|
||||
stops_missing_from_route1.append(stops2[j - 1])
|
||||
j -= 1
|
||||
case "add_2":
|
||||
stops_missing_from_route2.append(stops1[i - 1])
|
||||
i -= 1
|
||||
case _:
|
||||
if action == "change":
|
||||
stops_that_dont_match.append(
|
||||
(stops1[i - 1], stops2[j - 1])
|
||||
)
|
||||
i -= 1
|
||||
j -= 1
|
||||
return (
|
||||
stops_missing_from_route1,
|
||||
stops_missing_from_route2,
|
||||
stops_that_dont_match,
|
||||
)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.routes)
|
||||
|
||||
def __getitem__(self, i) -> Route: # noqa: F821
|
||||
return self.routes[i]
|
||||
|
||||
def __iter__(self) -> Iterator[Route]: # noqa: F821
|
||||
return iter(self.routes)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"RouteMaster(id={self.id}, mode={self.mode}, ref={self.ref}, "
|
||||
f"name={self.name}, network={self.network}, "
|
||||
f"num_variants={len(self.routes)}"
|
||||
)
|
|
@ -1,122 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from subways.osm_element import el_center, el_id
|
||||
from subways.structure.station import Station
|
||||
from subways.structure.stop_area import StopArea
|
||||
from subways.types import LonLat, OsmElementT
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
|
||||
|
||||
class RouteStop:
|
||||
def __init__(self, stoparea: StopArea) -> None:
|
||||
self.stoparea: StopArea = stoparea
|
||||
self.stop: LonLat = None # Stop position, possibly projected
|
||||
self.distance = 0 # In meters from the start of the route
|
||||
self.platform_entry = None # Platform el_id
|
||||
self.platform_exit = None # Platform el_id
|
||||
self.can_enter = False
|
||||
self.can_exit = False
|
||||
self.seen_stop = False
|
||||
self.seen_platform_entry = False
|
||||
self.seen_platform_exit = False
|
||||
self.seen_station = False
|
||||
|
||||
@property
|
||||
def seen_platform(self) -> bool:
|
||||
return self.seen_platform_entry or self.seen_platform_exit
|
||||
|
||||
@staticmethod
|
||||
def get_actual_role(
|
||||
el: OsmElementT, role: str, modes: set[str]
|
||||
) -> str | None:
|
||||
if StopArea.is_stop(el):
|
||||
return "stop"
|
||||
elif StopArea.is_platform(el):
|
||||
return "platform"
|
||||
elif Station.is_station(el, modes):
|
||||
if "platform" in role:
|
||||
return "platform"
|
||||
else:
|
||||
return "stop"
|
||||
return None
|
||||
|
||||
def add(self, member: dict, relation: OsmElementT, city: City) -> None:
|
||||
el = city.elements[el_id(member)]
|
||||
role = member["role"]
|
||||
|
||||
if StopArea.is_stop(el):
|
||||
if "platform" in role:
|
||||
city.warn("Stop position in a platform role in a route", el)
|
||||
if el["type"] != "node":
|
||||
city.error("Stop position is not a node", el)
|
||||
self.stop = el_center(el)
|
||||
if "entry_only" not in role:
|
||||
self.can_exit = True
|
||||
if "exit_only" not in role:
|
||||
self.can_enter = True
|
||||
|
||||
elif Station.is_station(el, city.modes):
|
||||
if el["type"] != "node":
|
||||
city.notice("Station in route is not a node", el)
|
||||
|
||||
if not self.seen_stop and not self.seen_platform:
|
||||
self.stop = el_center(el)
|
||||
self.can_enter = True
|
||||
self.can_exit = True
|
||||
|
||||
elif StopArea.is_platform(el):
|
||||
if "stop" in role:
|
||||
city.warn("Platform in a stop role in a route", el)
|
||||
if "exit_only" not in role:
|
||||
self.platform_entry = el_id(el)
|
||||
self.can_enter = True
|
||||
if "entry_only" not in role:
|
||||
self.platform_exit = el_id(el)
|
||||
self.can_exit = True
|
||||
if not self.seen_stop:
|
||||
self.stop = el_center(el)
|
||||
|
||||
multiple_check = False
|
||||
actual_role = RouteStop.get_actual_role(el, role, city.modes)
|
||||
if actual_role == "platform":
|
||||
if role == "platform_entry_only":
|
||||
multiple_check = self.seen_platform_entry
|
||||
self.seen_platform_entry = True
|
||||
elif role == "platform_exit_only":
|
||||
multiple_check = self.seen_platform_exit
|
||||
self.seen_platform_exit = True
|
||||
else:
|
||||
if role != "platform" and "stop" not in role:
|
||||
city.warn(
|
||||
f'Platform "{el["tags"].get("name", "")}" '
|
||||
f'({el_id(el)}) with invalid role "{role}" in route',
|
||||
relation,
|
||||
)
|
||||
multiple_check = self.seen_platform
|
||||
self.seen_platform_entry = True
|
||||
self.seen_platform_exit = True
|
||||
elif actual_role == "stop":
|
||||
multiple_check = self.seen_stop
|
||||
self.seen_stop = True
|
||||
if multiple_check:
|
||||
log_function = city.error if actual_role == "stop" else city.notice
|
||||
log_function(
|
||||
f'Multiple {actual_role}s for a station "'
|
||||
f'{el["tags"].get("name", "")} '
|
||||
f"({el_id(el)}) in a route relation",
|
||||
relation,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"RouteStop(stop={}, pl_entry={}, pl_exit={}, stoparea={})".format(
|
||||
self.stop,
|
||||
self.platform_entry,
|
||||
self.platform_exit,
|
||||
self.stoparea,
|
||||
)
|
||||
)
|
|
@ -1,62 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from subways.consts import ALL_MODES, CONSTRUCTION_KEYS
|
||||
from subways.css_colours import normalize_colour
|
||||
from subways.osm_element import el_center, el_id
|
||||
from subways.types import IdT, OsmElementT
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
|
||||
|
||||
class Station:
|
||||
def __init__(self, el: OsmElementT, city: City) -> None:
|
||||
"""Call this with a railway=station OSM feature."""
|
||||
self.id: IdT = el_id(el)
|
||||
self.element: OsmElementT = el
|
||||
self.modes = Station.get_modes(el)
|
||||
self.name = el["tags"].get("name", "?")
|
||||
self.int_name = el["tags"].get(
|
||||
"int_name", el["tags"].get("name:en", None)
|
||||
)
|
||||
try:
|
||||
self.colour = normalize_colour(el["tags"].get("colour", None))
|
||||
except ValueError as e:
|
||||
self.colour = None
|
||||
city.warn(str(e), el)
|
||||
self.center = el_center(el)
|
||||
if self.center is None:
|
||||
raise Exception("Could not find center of {}".format(el))
|
||||
|
||||
@staticmethod
|
||||
def get_modes(el: OsmElementT) -> set[str]:
|
||||
modes = {m for m in ALL_MODES if el["tags"].get(m) == "yes"}
|
||||
if mode := el["tags"].get("station"):
|
||||
modes.add(mode)
|
||||
return modes
|
||||
|
||||
@staticmethod
|
||||
def is_station(el: OsmElementT, modes: set[str]) -> bool:
|
||||
# public_transport=station is too ambiguous and unspecific to use,
|
||||
# so we expect for it to be backed by railway=station.
|
||||
if (
|
||||
"tram" in modes
|
||||
and el.get("tags", {}).get("railway") == "tram_stop"
|
||||
):
|
||||
return True
|
||||
if el.get("tags", {}).get("railway") not in ("station", "halt"):
|
||||
return False
|
||||
for k in CONSTRUCTION_KEYS:
|
||||
if k in el["tags"]:
|
||||
return False
|
||||
# Not checking for station=train, obviously
|
||||
if "train" not in modes and Station.get_modes(el).isdisjoint(modes):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Station(id={}, modes={}, name={}, center={})".format(
|
||||
self.id, ",".join(self.modes), self.name, self.center
|
||||
)
|
|
@ -1,191 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from itertools import chain
|
||||
|
||||
from subways.consts import RAILWAY_TYPES
|
||||
from subways.css_colours import normalize_colour
|
||||
from subways.geom_utils import distance
|
||||
from subways.osm_element import el_id, el_center
|
||||
from subways.structure.station import Station
|
||||
from subways.types import IdT, OsmElementT
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from subways.structure.city import City
|
||||
|
||||
MAX_DISTANCE_TO_ENTRANCES = 300 # in meters
|
||||
|
||||
|
||||
class StopArea:
|
||||
@staticmethod
|
||||
def is_stop(el: OsmElementT) -> bool:
|
||||
if "tags" not in el:
|
||||
return False
|
||||
if el["tags"].get("railway") == "stop":
|
||||
return True
|
||||
if el["tags"].get("public_transport") == "stop_position":
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_platform(el: OsmElementT) -> bool:
|
||||
if "tags" not in el:
|
||||
return False
|
||||
if el["tags"].get("railway") in ("platform", "platform_edge"):
|
||||
return True
|
||||
if el["tags"].get("public_transport") == "platform":
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_track(el: OsmElementT) -> bool:
|
||||
if el["type"] != "way" or "tags" not in el:
|
||||
return False
|
||||
return el["tags"].get("railway") in RAILWAY_TYPES
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
station: Station,
|
||||
city: City,
|
||||
stop_area: OsmElementT | None = None,
|
||||
) -> None:
|
||||
"""Call this with a Station object."""
|
||||
|
||||
self.element: OsmElementT = stop_area or station.element
|
||||
self.id: IdT = el_id(self.element)
|
||||
self.station: Station = station
|
||||
self.stops = set() # set of el_ids of stop_positions
|
||||
self.platforms = set() # set of el_ids of platforms
|
||||
self.exits = set() # el_id of subway_entrance/train_station_entrance
|
||||
# for leaving the platform
|
||||
self.entrances = set() # el_id of subway/train_station entrance
|
||||
# for entering the platform
|
||||
self.center = None # lon, lat of the station centre point
|
||||
self.centers = {} # el_id -> (lon, lat) for all elements
|
||||
self.transfer = None # el_id of a transfer relation
|
||||
|
||||
self.modes = station.modes
|
||||
self.name = station.name
|
||||
self.int_name = station.int_name
|
||||
self.colour = station.colour
|
||||
|
||||
if stop_area:
|
||||
self.name = stop_area["tags"].get("name", self.name)
|
||||
self.int_name = stop_area["tags"].get(
|
||||
"int_name", stop_area["tags"].get("name:en", self.int_name)
|
||||
)
|
||||
try:
|
||||
self.colour = (
|
||||
normalize_colour(stop_area["tags"].get("colour"))
|
||||
or self.colour
|
||||
)
|
||||
except ValueError as e:
|
||||
city.warn(str(e), stop_area)
|
||||
|
||||
self._process_members(station, city, stop_area)
|
||||
else:
|
||||
self._add_nearby_entrances(station, city)
|
||||
|
||||
if self.exits and not self.entrances:
|
||||
city.warn(
|
||||
"Only exits for a station, no entrances",
|
||||
stop_area or station.element,
|
||||
)
|
||||
if self.entrances and not self.exits:
|
||||
city.warn("No exits for a station", stop_area or station.element)
|
||||
|
||||
for el in self.get_elements():
|
||||
self.centers[el] = el_center(city.elements[el])
|
||||
|
||||
"""Calculate the center point of the station. This algorithm
|
||||
cannot rely on a station node, since many stop_areas can share one.
|
||||
Basically it averages center points of all platforms
|
||||
and stop positions."""
|
||||
if len(self.stops) + len(self.platforms) == 0:
|
||||
self.center = station.center
|
||||
else:
|
||||
self.center = [0, 0]
|
||||
for sp in chain(self.stops, self.platforms):
|
||||
spc = self.centers[sp]
|
||||
for i in range(2):
|
||||
self.center[i] += spc[i]
|
||||
for i in range(2):
|
||||
self.center[i] /= len(self.stops) + len(self.platforms)
|
||||
|
||||
def _process_members(
|
||||
self, station: Station, city: City, stop_area: OsmElementT
|
||||
) -> None:
|
||||
# If we have a stop area, add all elements from it
|
||||
tracks_detected = False
|
||||
for m in stop_area["members"]:
|
||||
k = el_id(m)
|
||||
m_el = city.elements.get(k)
|
||||
if not m_el or "tags" not in m_el:
|
||||
continue
|
||||
if Station.is_station(m_el, city.modes):
|
||||
if k != station.id:
|
||||
city.error("Stop area has multiple stations", stop_area)
|
||||
elif StopArea.is_stop(m_el):
|
||||
self.stops.add(k)
|
||||
elif StopArea.is_platform(m_el):
|
||||
self.platforms.add(k)
|
||||
elif (entrance_type := m_el["tags"].get("railway")) in (
|
||||
"subway_entrance",
|
||||
"train_station_entrance",
|
||||
):
|
||||
if m_el["type"] != "node":
|
||||
city.warn(f"{entrance_type} is not a node", m_el)
|
||||
if (
|
||||
m_el["tags"].get("entrance") != "exit"
|
||||
and m["role"] != "exit_only"
|
||||
):
|
||||
self.entrances.add(k)
|
||||
if (
|
||||
m_el["tags"].get("entrance") != "entrance"
|
||||
and m["role"] != "entry_only"
|
||||
):
|
||||
self.exits.add(k)
|
||||
elif StopArea.is_track(m_el):
|
||||
tracks_detected = True
|
||||
|
||||
if tracks_detected:
|
||||
city.warn("Tracks in a stop_area relation", stop_area)
|
||||
|
||||
def _add_nearby_entrances(self, station: Station, city: City) -> None:
|
||||
center = station.center
|
||||
for entrance_el in (
|
||||
el
|
||||
for el in city.elements.values()
|
||||
if "tags" in el
|
||||
and (entrance_type := el["tags"].get("railway"))
|
||||
in ("subway_entrance", "train_station_entrance")
|
||||
):
|
||||
entrance_id = el_id(entrance_el)
|
||||
if entrance_id in city.stop_areas:
|
||||
continue # This entrance belongs to some stop_area
|
||||
c_center = el_center(entrance_el)
|
||||
if (
|
||||
c_center
|
||||
and distance(center, c_center) <= MAX_DISTANCE_TO_ENTRANCES
|
||||
):
|
||||
if entrance_el["type"] != "node":
|
||||
city.warn(f"{entrance_type} is not a node", entrance_el)
|
||||
etag = entrance_el["tags"].get("entrance")
|
||||
if etag != "exit":
|
||||
self.entrances.add(entrance_id)
|
||||
if etag != "entrance":
|
||||
self.exits.add(entrance_id)
|
||||
|
||||
def get_elements(self) -> set[IdT]:
|
||||
result = {self.id, self.station.id}
|
||||
result.update(self.entrances)
|
||||
result.update(self.exits)
|
||||
result.update(self.stops)
|
||||
result.update(self.platforms)
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"StopArea(id={self.id}, name={self.name}, station={self.station},"
|
||||
f" transfer={self.transfer}, center={self.center})"
|
||||
)
|
|
@ -1,10 +0,0 @@
|
|||
To perform tests, run this command from the top directory
|
||||
of the repository:
|
||||
|
||||
```bash
|
||||
export PYTHONPATH=$(pwd)
|
||||
[ -d "subways/tests/.venv" ] || python3 -m venv subways/tests/.venv
|
||||
source subways/tests/.venv/bin/activate
|
||||
pip install -r subways/requirements.txt
|
||||
python -m unittest discover subways
|
||||
```
|
|
@ -1,12 +0,0 @@
|
|||
#,City,Country,Region,Stations,Subway Lines,Light Rail +Monorail,Interchanges,"BBox (lon, lat)",Networks (opt.),Comment,Source,
|
||||
291,Moscow,Russia,Europe,335,14,3,66,,"subway,train:Московский метрополитен;МЦК;МЦД","No bbox - skip the city",
|
||||
,Moscow - Aeroexpress,Russia,Europe,22,0,3,0,"37.170,55.385,38.028,56.022",train:Аэроэкспресс,"No id - skip the city",https://aeroexpress.ru
|
||||
292,Nizhny Novgorod,Russia,Europe,16,2,0,,"43.759918,56.1662,44.13208,56.410862",,"No configuration errors",,,
|
||||
NBS,Novosibirsk,Russia,Europe,13,2,0,1,"82.774773,54.926747,83.059044,55.127864",,"Non-numeric ID",,,
|
||||
294,Saint Petersburg,Russia,Europe,72,,,,"30.0648,59.7509,30.5976,60.1292",,"Empty line counts - no problem at CSV parsing stage",,
|
||||
,,,,,,,,,,,,
|
||||
295,Samara,Russia,Europe,10x,1,0,0,"50.011826,53.094024,50.411453,53.384147",,"Non-numeric station count",,
|
||||
296,Volgograd,Russia,Europe,40,zero,2zero,0,"44.366704,48.636024,44.62302,48.81765",,"Non-numbers in subway and light_rail line counts",,
|
||||
297,Yekaterinburg,Russia,Europe,,1,0,0,"60.460854,56.730505,60.727272,56.920997",,"Empty station count",,
|
||||
,,,,,,,,,,,,
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
@ -1,558 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' visible='true' version='1' lat='0.5' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' visible='true' version='1' lat='0.5' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' visible='true' version='1' lat='0.5' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='5' visible='true' version='1' lat='2.0' lon='1.0'>
|
||||
<tag k='name' v='Station 5' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='8' visible='true' version='1' lat='-2.0' lon='1.5'>
|
||||
<tag k='name' v='Station 8' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='21' visible='true' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='22' visible='true' version='1' lat='0.0' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='23' visible='true' version='1' lat='0.0' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='24' visible='true' version='1' lat='0.0' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='31' visible='true' version='1' lat='0.0003' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='32' visible='true' version='1' lat='0.0003' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='33' visible='true' version='1' lat='0.0003' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='34' visible='true' version='1' lat='0.0003' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='101' visible='true' version='1' lat='0.00015' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='102' visible='true' version='1' lat='0.00015' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='103' visible='true' version='1' lat='0.00015' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='104' visible='true' version='1' lat='0.00015' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='10001' visible='true' version='1' lat='0.0' lon='3.0003' />
|
||||
<node id='10002' visible='true' version='1' lat='0.0' lon='-0.0003' />
|
||||
<way id='1' visible='true' version='1'>
|
||||
<nd ref='21' />
|
||||
<nd ref='22' />
|
||||
<nd ref='23' />
|
||||
<nd ref='24' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='2' visible='true' version='1'>
|
||||
<nd ref='34' />
|
||||
<nd ref='33' />
|
||||
<nd ref='32' />
|
||||
<nd ref='31' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='3' visible='true' version='1'>
|
||||
<nd ref='24' />
|
||||
<nd ref='8' />
|
||||
<nd ref='21' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='4' visible='true' version='1'>
|
||||
<nd ref='34' />
|
||||
<nd ref='10001' />
|
||||
<nd ref='8' />
|
||||
<nd ref='10002' />
|
||||
<nd ref='31' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<relation id='102' visible='true' version='1'>
|
||||
<member type='node' ref='102' role='' />
|
||||
<member type='node' ref='32' role='stop' />
|
||||
<member type='node' ref='22' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='103' visible='true' version='1'>
|
||||
<member type='node' ref='103' role='' />
|
||||
<member type='node' ref='23' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='104' visible='true' version='1'>
|
||||
<member type='node' ref='104' role='' />
|
||||
<member type='node' ref='24' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='107' visible='true' version='1'>
|
||||
<member type='node' ref='101' role='' />
|
||||
<member type='node' ref='21' role='stop' />
|
||||
<member type='node' ref='31' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='113' visible='true' version='1'>
|
||||
<member type='node' ref='103' role='' />
|
||||
<member type='node' ref='33' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='114' visible='true' version='1'>
|
||||
<member type='node' ref='104' role='' />
|
||||
<member type='node' ref='34' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='151' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='01: 1-2-3' />
|
||||
<tag k='ref' v='01' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='153' visible='true' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='01: 3-2-1' />
|
||||
<tag k='ref' v='01' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='155' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='02: 1-2-3' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='158' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='02: 1-3 (2)' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='159' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='C: 1-3-5-1' />
|
||||
<tag k='note' v='Circular route without backward and without master' />
|
||||
<tag k='ref' v='C' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='160' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='C2: 1-5-3-1' />
|
||||
<tag k='note' v='Circular route with backward' />
|
||||
<tag k='ref' v='C2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='161' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='C2: 1-3-5-1' />
|
||||
<tag k='note' v='Circular route with backward' />
|
||||
<tag k='ref' v='C2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='162' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='03: 1-2-3' />
|
||||
<tag k='note' v='Route without backward variants and without route_master' />
|
||||
<tag k='ref' v='03' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='163' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='04: 1-2-3' />
|
||||
<tag k='ref' v='04' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='164' visible='true' version='1'>
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='04: 2-1' />
|
||||
<tag k='ref' v='04' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='165' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='05: 1-2-3' />
|
||||
<tag k='note' v='No route_master. Will find twin by ref.' />
|
||||
<tag k='ref' v='05' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='166' visible='true' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='05: 3-2-1' />
|
||||
<tag k='note' v='No route_master. Will find twin by ref.' />
|
||||
<tag k='ref' v='05' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='168' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='name' v='C5: 1-3-5-1' />
|
||||
<tag k='ref' v='C5' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='169' visible='true' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='name' v='C5: 3-5-1-3' />
|
||||
<tag k='ref' v='C5' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='201' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='1: 1-2-3' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='202' visible='true' version='1'>
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='1: 3-2-1' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='203' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='2: 1-2-3' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='204' visible='true' version='1'>
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='2: 3-1' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='205' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='3: 1-2-3' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='206' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='3: 1-2-3' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='207' visible='true' version='1'>
|
||||
<member type='node' ref='34' role='' />
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='4: 4-3-2-1' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='208' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='24' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='4: 1-2-3-4' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='209' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='5: 1-2-3' />
|
||||
<tag k='ref' v='5' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='210' visible='true' version='1'>
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='5: 2-1' />
|
||||
<tag k='ref' v='5' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='211' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='C4: 1-2-3-8-1' />
|
||||
<tag k='ref' v='C4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='212' visible='true' version='1'>
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<member type='way' ref='4' role='' />
|
||||
<tag k='name' v='C4: 3-2-1-8-3' />
|
||||
<tag k='ref' v='C4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='213' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='C3: 1-2-3-8-1' />
|
||||
<tag k='note' v='Circular route without backward' />
|
||||
<tag k='ref' v='C3' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='1003' visible='true' version='1'>
|
||||
<member type='relation' ref='103' role='' />
|
||||
<member type='relation' ref='113' role='' />
|
||||
<tag k='public_transport' v='stop_area_group' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='10001' visible='true' version='1'>
|
||||
<member type='relation' ref='201' role='' />
|
||||
<member type='relation' ref='202' role='' />
|
||||
<tag k='note' v='Ideal twins. One end is one stop_area, another - two stop_areas combined into a transfer' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10002' visible='true' version='1'>
|
||||
<member type='relation' ref='203' role='' />
|
||||
<member type='relation' ref='204' role='' />
|
||||
<tag k='note' v='Routes with matching ends that differ in station count' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10003' visible='true' version='1'>
|
||||
<member type='relation' ref='205' role='' />
|
||||
<member type='relation' ref='206' role='' />
|
||||
<tag k='note' v='Two routes with the same ends AND with the same direction' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10004' visible='true' version='1'>
|
||||
<member type='relation' ref='208' role='' />
|
||||
<member type='relation' ref='207' role='' />
|
||||
<tag k='note' v='Two routes one end of which belongs to different stop_areas without transfer' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10005' visible='true' version='1'>
|
||||
<member type='relation' ref='209' role='' />
|
||||
<member type='relation' ref='210' role='' />
|
||||
<tag k='note' v='Almost twins, but end stations are different' />
|
||||
<tag k='ref' v='5' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10006' visible='true' version='1'>
|
||||
<member type='relation' ref='211' role='' />
|
||||
<member type='relation' ref='212' role='' />
|
||||
<tag k='note' v='Circular route with forward and backward directions' />
|
||||
<tag k='ref' v='C4' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10007' visible='true' version='1'>
|
||||
<member type='relation' ref='163' role='' />
|
||||
<member type='relation' ref='164' role='' />
|
||||
<tag k='note' v='Bad: end stations are different' />
|
||||
<tag k='ref' v='04' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10021' visible='true' version='1'>
|
||||
<member type='relation' ref='151' role='' />
|
||||
<member type='relation' ref='153' role='' />
|
||||
<tag k='note' v='Good route master with full twin routes' />
|
||||
<tag k='ref' v='01' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10022' visible='true' version='1'>
|
||||
<member type='relation' ref='155' role='' />
|
||||
<member type='relation' ref='158' role='' />
|
||||
<tag k='note' v='Good master, but backward route omits inner stations' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10023' visible='true' version='1'>
|
||||
<member type='relation' ref='161' role='' />
|
||||
<member type='relation' ref='160' role='' />
|
||||
<tag k='note' v='Circular route with both directions' />
|
||||
<tag k='ref' v='C2' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10024' visible='true' version='1'>
|
||||
<member type='node' ref='2' role='' />
|
||||
<tag k='note' v='Empty route_master, but center can be calculated due to a spurious member' />
|
||||
<tag k='ref' v='07' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10025' visible='true' version='1'>
|
||||
<member type='relation' ref='168' role='' />
|
||||
<member type='relation' ref='169' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Two cirlucar routes with the same direction. Shloud generate notice 'no return direction'' />
|
||||
<tag k='ref' v='C5' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10026' visible='true' version='1'>
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Empty route_master, so that it cannot be assigned to any city' />
|
||||
<tag k='ref' v='06' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
|
@ -1,245 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' visible='true' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' visible='true' version='1' lat='0.00466930266' lon='0.00473815872'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' visible='true' version='1' lat='0.0097589171' lon='0.01012040581'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='4' visible='true' version='1' lat='0.01' lon='0.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='5' visible='true' version='1' lat='0.00514739839' lon='0.0047718624'>
|
||||
<tag k='name' v='Station 5' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='6' visible='true' version='1' lat='0.0' lon='0.01'>
|
||||
<tag k='name' v='Station 6' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='7' visible='true' version='1' lat='0.01028895507' lon='0.0097109364'>
|
||||
<tag k='name' v='Station 7' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='light_rail' />
|
||||
</node>
|
||||
<node id='8' visible='true' version='1' lat='0.01433266979' lon='0.01236209024'>
|
||||
<tag k='name' v='Station 8' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='light_rail' />
|
||||
</node>
|
||||
<node id='101' visible='true' version='1' lat='0.0047037307' lon='0.00470373068'>
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='102' visible='true' version='1' lat='0.01025306758' lon='0.00976752835'>
|
||||
<tag k='railway' v='stop' />
|
||||
</node>
|
||||
<node id='103' visible='true' version='1' lat='0.01434446439' lon='0.01245616794'>
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
</node>
|
||||
<node id='104' visible='true' version='1' lat='0.01031966791' lon='0.00966618028'>
|
||||
<tag k='railway' v='stop' />
|
||||
</node>
|
||||
<node id='105' visible='true' version='1' lat='0.01441106473' lon='0.01235481987'>
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
</node>
|
||||
<node id='201' visible='true' version='1' lat='0.00967473055' lon='0.01007169217'>
|
||||
<tag k='railway' v='subway_entrance' />
|
||||
<tag k='ref' v='3-1' />
|
||||
</node>
|
||||
<node id='202' visible='true' version='1' lat='0.00966936613' lon='0.01018702716'>
|
||||
<tag k='railway' v='train_station_entrance' />
|
||||
<tag k='ref' v='3-2' />
|
||||
</node>
|
||||
<node id='203' visible='true' version='1' lat='0.01042574907' lon='0.00959962338'>
|
||||
<tag k='railway' v='train_station_entrance' />
|
||||
<tag k='ref' v='7-2' />
|
||||
</node>
|
||||
<node id='204' visible='true' version='1' lat='0.01034796501' lon='0.00952183932'>
|
||||
<tag k='railway' v='subway_entrance' />
|
||||
<tag k='ref' v='7-1' />
|
||||
</node>
|
||||
<node id='205' visible='true' version='1' lat='0.01015484596' lon='0.000201163'>
|
||||
<tag k='note' v='Though the entrance is not in the stop_area, the preprocessor should add the entrance to it' />
|
||||
<tag k='railway' v='subway_entrance' />
|
||||
<tag k='ref' v='4-1' />
|
||||
</node>
|
||||
<node id='1001' visible='true' version='1' lat='0.01' lon='0.01' />
|
||||
<way id='1' visible='true' version='1'>
|
||||
<nd ref='1' />
|
||||
<nd ref='101' />
|
||||
<nd ref='1001' />
|
||||
<tag k='layer' v='-2' />
|
||||
<tag k='railway' v='subway' />
|
||||
<tag k='tunnel' v='yes' />
|
||||
</way>
|
||||
<way id='2' visible='true' version='1'>
|
||||
<nd ref='4' />
|
||||
<nd ref='6' />
|
||||
<tag k='layer' v='-3' />
|
||||
<tag k='railway' v='subway' />
|
||||
<tag k='tunnel' v='yes' />
|
||||
</way>
|
||||
<way id='3' visible='true' version='1'>
|
||||
<nd ref='102' />
|
||||
<nd ref='103' />
|
||||
<tag k='railway' v='light_rail' />
|
||||
</way>
|
||||
<way id='4' visible='true' version='1'>
|
||||
<nd ref='104' />
|
||||
<nd ref='105' />
|
||||
<tag k='railway' v='light_rail' />
|
||||
</way>
|
||||
<relation id='1' visible='true' version='1'>
|
||||
<member type='node' ref='101' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='2' visible='true' version='1'>
|
||||
<member type='node' ref='5' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='3' visible='true' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='201' role='' />
|
||||
<member type='node' ref='202' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='4' visible='true' version='1'>
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='102' role='' />
|
||||
<member type='node' ref='104' role='' />
|
||||
<member type='node' ref='203' role='' />
|
||||
<member type='node' ref='204' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='5' visible='true' version='1'>
|
||||
<member type='relation' ref='2' role='' />
|
||||
<member type='relation' ref='1' role='' />
|
||||
<tag k='public_transport' v='stop_area_group' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='6' visible='true' version='1'>
|
||||
<member type='relation' ref='4' role='' />
|
||||
<member type='relation' ref='3' role='' />
|
||||
<tag k='public_transport' v='stop_area_group' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='7' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='101' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='colour' v='#0000FF' />
|
||||
<tag k='name' v='1 forward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='8' visible='true' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='101' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='colour' v='#0000FF' />
|
||||
<tag k='name' v='1 backward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='9' visible='true' version='1'>
|
||||
<member type='node' ref='102' role='' />
|
||||
<member type='node' ref='103' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='LR forward' />
|
||||
<tag k='ref' v='LR' />
|
||||
<tag k='route' v='light_rail' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='10' visible='true' version='1'>
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='104' role='' />
|
||||
<member type='way' ref='4' role='' />
|
||||
<tag k='name' v='LR backward' />
|
||||
<tag k='ref' v='LR' />
|
||||
<tag k='route' v='light_rail' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='11' visible='true' version='1'>
|
||||
<member type='relation' ref='10' role='' />
|
||||
<member type='relation' ref='9' role='' />
|
||||
<tag k='colour' v='brown' />
|
||||
<tag k='colour:infill' v='white' />
|
||||
<tag k='duration' v='5' />
|
||||
<tag k='name' v='LR Line' />
|
||||
<tag k='network' v='network-2' />
|
||||
<tag k='ref' v='LR' />
|
||||
<tag k='route_master' v='light_rail' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='12' visible='true' version='1'>
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='duration' v='10' />
|
||||
<tag k='name' v='2 forward' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='13' visible='true' version='1'>
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='duration:peak' v='8' />
|
||||
<tag k='name' v='2 backward' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='14' visible='true' version='1'>
|
||||
<member type='relation' ref='13' role='' />
|
||||
<member type='relation' ref='12' role='' />
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='name' v='Red Line' />
|
||||
<tag k='network' v='network-1' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='15' visible='true' version='1'>
|
||||
<member type='relation' ref='8' role='' />
|
||||
<member type='relation' ref='7' role='' />
|
||||
<tag k='name' v='Blue Line' />
|
||||
<tag k='network' v='network-1' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='16' visible='true' version='1'>
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='103' role='stop' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
</osm>
|
|
@ -1,3 +0,0 @@
|
|||
agency_id,agency_name,agency_url,agency_timezone,agency_lang,agency_phone
|
||||
1,Intersecting 2 metro lines,,,,
|
||||
2,One light rail line,,,,
|
|
@ -1,2 +0,0 @@
|
|||
service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date
|
||||
always,1,1,1,1,1,1,1,19700101,30000101
|
|
@ -1,7 +0,0 @@
|
|||
trip_id,start_time,end_time,headway_secs,exact_times
|
||||
r7,05:00:00,25:00:00,150.0,
|
||||
r8,05:00:00,25:00:00,150.0,
|
||||
r12,05:00:00,25:00:00,150.0,
|
||||
r13,05:00:00,25:00:00,150.0,
|
||||
r9,05:00:00,25:00:00,150.0,
|
||||
r10,05:00:00,25:00:00,150.0,
|
|
@ -1,4 +0,0 @@
|
|||
route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color,route_sort_order,route_fare_class,line_id,listed_route
|
||||
r15,1,1,Blue Line,,1,,0000ff,,,,,
|
||||
r14,1,2,Red Line,,1,,ff0000,,,,,
|
||||
r11,2,LR,LR Line,,1,,a52a2a,,,,,
|
|
@ -1,15 +0,0 @@
|
|||
shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled
|
||||
7,0.0,0.0,0,
|
||||
7,0.0047037,0.0047037,1,
|
||||
7,0.0099397,0.0099397,2,
|
||||
8,0.0099397,0.0099397,0,
|
||||
8,0.0047037,0.0047037,1,
|
||||
8,0.0,0.0,2,
|
||||
12,0.01,0.0,0,
|
||||
12,0.0,0.01,1,
|
||||
13,0.0,0.01,0,
|
||||
13,0.01,0.0,1,
|
||||
9,0.0102531,0.0097675,0,
|
||||
9,0.0143445,0.0124562,1,
|
||||
10,0.0143597,0.012321,0,
|
||||
10,0.0103197,0.0096662,1,
|
|
@ -1,17 +0,0 @@
|
|||
trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_headsign,pickup_type,drop_off_type,shape_dist_traveled,timepoint,checkpoint_id,continuous_pickup,continuous_drop_off
|
||||
r7,,,n1_plt,0,,,,0,,,,
|
||||
r7,,,r1_plt,1,,,,741,,,,
|
||||
r7,,,r3_plt,2,,,,1565,,,,
|
||||
r8,,,r3_plt,0,,,,0,,,,
|
||||
r8,,,r1_plt,1,,,,824,,,,
|
||||
r8,,,n1_plt,2,,,,1565,,,,
|
||||
r12,,,n4_plt,0,,,,0,,,,
|
||||
r12,,,r2_plt,1,,,,758,,,,
|
||||
r12,,,n6_plt,2,,,,1575,,,,
|
||||
r13,,,n6_plt,0,,,,0,,,,
|
||||
r13,,,r2_plt,1,,,,817,,,,
|
||||
r13,,,n4_plt,2,,,,1575,,,,
|
||||
r9,,,r4_plt,0,,,,0,,,,
|
||||
r9,,,r16_plt,1,,,,545,,,,
|
||||
r10,,,r16_plt,0,,,,0,,,,
|
||||
r10,,,r4_plt,1,,,,538,,,,
|
|
@ -1,27 +0,0 @@
|
|||
stop_id,stop_code,stop_name,stop_desc,platform_code,platform_name,stop_lat,stop_lon,zone_id,stop_address,stop_url,level_id,location_type,parent_station,wheelchair_boarding,municipality,on_street,at_street,vehicle_type
|
||||
n1_egress,n1_egress,Station 1,,,,0.0,0.0,,,,,2,n1_st,,,,,
|
||||
n1_plt,n1_plt,Station 1,,,,0.0,0.0,,,,,0,n1_st,,,,,
|
||||
n1_st,n1_st,Station 1,,,,0.0,0.0,,,,,1,,,,,,
|
||||
n201_r3,n201_r3,Station 3 3-1,,,,0.0096747,0.0100717,,,,,2,r3_st,,,,,
|
||||
n202_r3,n202_r3,Station 3 3-2,,,,0.0096694,0.010187,,,,,2,r3_st,,,,,
|
||||
n203_r4,n203_r4,Station 7 7-2,,,,0.0104257,0.0095996,,,,,2,r4_st,,,,,
|
||||
n204_r4,n204_r4,Station 7 7-1,,,,0.010348,0.0095218,,,,,2,r4_st,,,,,
|
||||
n205_n4,n205_n4,Station 4 4-1,,,,0.0101548,0.0002012,,,,,2,n4_st,,,,,
|
||||
n4_plt,n4_plt,Station 4,,,,0.01,0.0,,,,,0,n4_st,,,,,
|
||||
n4_st,n4_st,Station 4,,,,0.01,0.0,,,,,1,,,,,,
|
||||
n6_egress,n6_egress,Station 6,,,,0.0,0.01,,,,,2,n6_st,,,,,
|
||||
n6_plt,n6_plt,Station 6,,,,0.0,0.01,,,,,0,n6_st,,,,,
|
||||
n6_st,n6_st,Station 6,,,,0.0,0.01,,,,,1,,,,,,
|
||||
r16_egress,r16_egress,Station 8,,,,0.0143778,0.0124055,,,,,2,r16_st,,,,,
|
||||
r16_plt,r16_plt,Station 8,,,,0.0143778,0.0124055,,,,,0,r16_st,,,,,
|
||||
r16_st,r16_st,Station 8,,,,0.0143778,0.0124055,,,,,1,,,,,,
|
||||
r1_egress,r1_egress,Station 2,,,,0.0047037,0.0047037,,,,,2,r1_st,,,,,
|
||||
r1_plt,r1_plt,Station 2,,,,0.0047037,0.0047037,,,,,0,r1_st,,,,,
|
||||
r1_st,r1_st,Station 2,,,,0.0047037,0.0047037,,,,,1,,,,,,
|
||||
r2_egress,r2_egress,Station 5,,,,0.0051474,0.0047719,,,,,2,r2_st,,,,,
|
||||
r2_plt,r2_plt,Station 5,,,,0.0051474,0.0047719,,,,,0,r2_st,,,,,
|
||||
r2_st,r2_st,Station 5,,,,0.0051474,0.0047719,,,,,1,,,,,,
|
||||
r3_plt,r3_plt,Station 3,,,,0.0097589,0.0101204,,,,,0,r3_st,,,,,
|
||||
r3_st,r3_st,Station 3,,,,0.0097589,0.0101204,,,,,1,,,,,,
|
||||
r4_plt,r4_plt,Station 7,,,,0.0102864,0.0097169,,,,,0,r4_st,,,,,
|
||||
r4_st,r4_st,Station 7,,,,0.0102864,0.0097169,,,,,1,,,,,,
|
|
@ -1,5 +0,0 @@
|
|||
from_stop_id,to_stop_id,transfer_type,min_transfer_time
|
||||
r3_st,r4_st,0,106
|
||||
r4_st,r3_st,0,106
|
||||
r1_st,r2_st,0,81
|
||||
r2_st,r1_st,0,81
|
|
@ -1,7 +0,0 @@
|
|||
route_id,service_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,shape_id,wheelchair_accessible,trip_route_type,route_pattern_id,bikes_allowed,average_speed
|
||||
r15,always,r7,,,,,7,,,,,40.0
|
||||
r15,always,r8,,,,,8,,,,,40.0
|
||||
r14,always,r12,,,,,12,,,,,9.4
|
||||
r14,always,r13,,,,,13,,,,,11.8
|
||||
r11,always,r9,,,,,9,,,,,6.5
|
||||
r11,always,r10,,,,,10,,,,,6.5
|
|
@ -1,578 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' visible='true' version='1' lat='0.5' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' visible='true' version='1' lat='0.5' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' visible='true' version='1' lat='0.5' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='4' visible='true' version='1' lat='0.5' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='5' visible='true' version='1' lat='2.0' lon='1.5'>
|
||||
<tag k='name' v='Station 5' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='8' visible='true' version='1' lat='-3.5' lon='3.0'>
|
||||
<tag k='name' v='Station 8' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='21' visible='true' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='22' visible='true' version='1' lat='0.0' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='23' visible='true' version='1' lat='0.0' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='24' visible='true' version='1' lat='0.0' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='25' visible='true' version='1' lat='0.0' lon='4.0' />
|
||||
<node id='26' visible='true' version='1' lat='0.0' lon='5.0'>
|
||||
<tag k='name' v='Station 6' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='27' visible='true' version='1' lat='0.0' lon='6.0' />
|
||||
<node id='31' visible='true' version='1' lat='0.0003' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='32' visible='true' version='1' lat='0.0003' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='33' visible='true' version='1' lat='0.0003' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='34' visible='true' version='1' lat='0.0003' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='35' visible='true' version='1' lat='0.0003' lon='4.0' />
|
||||
<node id='36' visible='true' version='1' lat='0.0003' lon='5.0'>
|
||||
<tag k='name' v='Station 6' />
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='37' visible='true' version='1' lat='0.0003' lon='6.0' />
|
||||
<node id='101' visible='true' version='1' lat='0.00015' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='102' visible='true' version='1' lat='0.00015' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='103' visible='true' version='1' lat='0.00015' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='104' visible='true' version='1' lat='0.00015' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='105' visible='true' version='1' lat='0.00015' lon='4.0'>
|
||||
<tag k='name' v='Station 5' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='106' visible='true' version='1' lat='0.00015' lon='5.0'>
|
||||
<tag k='name' v='Station 6' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='107' visible='true' version='1' lat='0.00015' lon='6.0'>
|
||||
<tag k='name' v='Station 7' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='10001' visible='true' version='1' lat='0.0' lon='6.0003' />
|
||||
<node id='10002' visible='true' version='1' lat='0.0' lon='-0.0003' />
|
||||
<way id='1' visible='true' version='1'>
|
||||
<nd ref='21' />
|
||||
<nd ref='22' />
|
||||
<nd ref='23' />
|
||||
<nd ref='24' />
|
||||
<nd ref='25' />
|
||||
<nd ref='26' />
|
||||
<nd ref='27' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='2' visible='true' version='1'>
|
||||
<nd ref='37' />
|
||||
<nd ref='36' />
|
||||
<nd ref='35' />
|
||||
<nd ref='34' />
|
||||
<nd ref='33' />
|
||||
<nd ref='32' />
|
||||
<nd ref='31' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='3' visible='true' version='1'>
|
||||
<nd ref='27' />
|
||||
<nd ref='8' />
|
||||
<nd ref='21' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='4' visible='true' version='1'>
|
||||
<nd ref='37' />
|
||||
<nd ref='10001' />
|
||||
<nd ref='8' />
|
||||
<nd ref='10002' />
|
||||
<nd ref='31' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<relation id='102' visible='true' version='1'>
|
||||
<member type='node' ref='102' role='' />
|
||||
<member type='node' ref='32' role='stop' />
|
||||
<member type='node' ref='22' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='103' visible='true' version='1'>
|
||||
<member type='node' ref='103' role='' />
|
||||
<member type='node' ref='23' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='104' visible='true' version='1'>
|
||||
<member type='node' ref='104' role='' />
|
||||
<member type='node' ref='24' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='106' visible='true' version='1'>
|
||||
<member type='node' ref='36' role='stop' />
|
||||
<member type='node' ref='106' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='107' visible='true' version='1'>
|
||||
<member type='node' ref='101' role='' />
|
||||
<member type='node' ref='21' role='stop' />
|
||||
<member type='node' ref='31' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='113' visible='true' version='1'>
|
||||
<member type='node' ref='103' role='' />
|
||||
<member type='node' ref='33' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='114' visible='true' version='1'>
|
||||
<member type='node' ref='104' role='' />
|
||||
<member type='node' ref='34' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='116' visible='true' version='1'>
|
||||
<member type='node' ref='106' role='' />
|
||||
<member type='node' ref='26' role='stop' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='151' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='01: 1-2-3' />
|
||||
<tag k='ref' v='01' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='152' visible='true' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='01: 3-1' />
|
||||
<tag k='ref' v='01' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='153' visible='true' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='01: 3-2-1' />
|
||||
<tag k='ref' v='01' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='154' visible='true' version='1'>
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='02: 4-3' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='155' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='02: 1-3' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='156' visible='true' version='1'>
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<tag k='name' v='02: 2-4' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='157' visible='true' version='1'>
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='02: 4-1' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='158' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='02: 1-3 (2)' />
|
||||
<tag k='note' v='This should not be recognized as twin for route "2: 1-3"' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='159' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='name' v='C: 1-2-3-4-5-1' />
|
||||
<tag k='note' v='Circular route without backward' />
|
||||
<tag k='ref' v='C' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='160' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='C2: 1-5-4-3-2-1' />
|
||||
<tag k='note' v='Circular route with backward' />
|
||||
<tag k='ref' v='C2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='161' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='C2: 1-2-3-4-5-1' />
|
||||
<tag k='note' v='Circular route with backward' />
|
||||
<tag k='ref' v='C2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='201' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='1: 1-2-3' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='202' visible='true' version='1'>
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='1: 3-2-1' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='203' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='26' role='' />
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='2: 1-2-3-5-6-7' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='204' visible='true' version='1'>
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='node' ref='36' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='2: 7-6-1' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='205' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='24' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='26' role='' />
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='3: 1-2-3-4-5-6-7' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='206' visible='true' version='1'>
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='node' ref='36' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='3: 7-6-5-3-2-1' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='207' visible='true' version='1'>
|
||||
<member type='node' ref='34' role='' />
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='4: 4-3-2-1' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='208' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='24' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='4: 1-2-3-4' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='209' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='26' role='' />
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='5: 1-2-3-5-6-7' />
|
||||
<tag k='ref' v='5' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='210' visible='true' version='1'>
|
||||
<member type='node' ref='36' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='5: 6-5-3-2-1' />
|
||||
<tag k='ref' v='5' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='211' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='26' role='' />
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='C4: 1-2-3-5-6-7-8-1' />
|
||||
<tag k='ref' v='C4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='212' visible='true' version='1'>
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='node' ref='36' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='33' role='' />
|
||||
<member type='node' ref='32' role='' />
|
||||
<member type='node' ref='31' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<member type='way' ref='4' role='' />
|
||||
<tag k='name' v='C4: 7-6-5-3-2-1-8-7' />
|
||||
<tag k='ref' v='C4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='213' visible='true' version='1'>
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='105' role='' />
|
||||
<member type='node' ref='26' role='' />
|
||||
<member type='node' ref='107' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='name' v='C3: 1-2-3-5-6-7-8-1' />
|
||||
<tag k='note' v='Circular route without backward' />
|
||||
<tag k='ref' v='C3' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='1003' visible='true' version='1'>
|
||||
<member type='relation' ref='103' role='' />
|
||||
<member type='relation' ref='113' role='' />
|
||||
<tag k='public_transport' v='stop_area_group' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='1006' visible='true' version='1'>
|
||||
<member type='relation' ref='116' role='' />
|
||||
<member type='relation' ref='106' role='' />
|
||||
<tag k='public_transport' v='stop_area_group' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='10001' visible='true' version='1'>
|
||||
<member type='relation' ref='201' role='' />
|
||||
<member type='relation' ref='202' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Ideal twins. One end is one stop_area, another - two stop_areas combined into a transfer' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10002' visible='true' version='1'>
|
||||
<member type='relation' ref='203' role='' />
|
||||
<member type='relation' ref='204' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Twin candidates that differ by 50% in station count' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10003' visible='true' version='1'>
|
||||
<member type='relation' ref='205' role='' />
|
||||
<member type='relation' ref='206' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Twins that differ by 15% in station count' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10004' visible='true' version='1'>
|
||||
<member type='relation' ref='207' role='' />
|
||||
<member type='relation' ref='208' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Two routes one end of which belongs to different stop_areas without transfer' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10005' visible='true' version='1'>
|
||||
<member type='relation' ref='209' role='' />
|
||||
<member type='relation' ref='210' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Almost twins, but end stations are different' />
|
||||
<tag k='ref' v='5' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10006' visible='true' version='1'>
|
||||
<member type='relation' ref='211' role='' />
|
||||
<member type='relation' ref='212' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='ref' v='C4' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10021' visible='true' version='1'>
|
||||
<member type='relation' ref='151' role='' />
|
||||
<member type='relation' ref='152' role='' />
|
||||
<member type='relation' ref='153' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='There is a Forward 1 route (1-2-3) and two backward (3-1 and 3-2-1). Twin for forward should be 3-station backward variant.' />
|
||||
<tag k='ref' v='01' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10022' visible='true' version='1'>
|
||||
<member type='relation' ref='154' role='' />
|
||||
<member type='relation' ref='155' role='' />
|
||||
<member type='relation' ref='156' role='' />
|
||||
<member type='relation' ref='157' role='' />
|
||||
<member type='relation' ref='158' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='At least two routes in each direction, but no twins. Duplicated route is not a twin.' />
|
||||
<tag k='ref' v='02' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='10023' visible='true' version='1'>
|
||||
<member type='relation' ref='161' role='' />
|
||||
<member type='relation' ref='160' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Circular route with both directions' />
|
||||
<tag k='ref' v='C2' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
|
@ -1,680 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' visible='true' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' visible='true' version='1' lat='0.0' lon='0.01'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' visible='true' version='1' lat='0.0' lon='0.02'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='4' visible='true' version='1' lat='0.0' lon='0.03'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='5' visible='true' version='1' lat='0.0' lon='0.04'>
|
||||
<tag k='name' v='Station 5' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='6' visible='true' version='1' lat='0.0' lon='0.05'>
|
||||
<tag k='name' v='Station 6' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='7' visible='true' version='1' lat='0.0' lon='0.06'>
|
||||
<tag k='name' v='Station 7' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='8' visible='true' version='1' lat='0.0' lon='0.07'>
|
||||
<tag k='name' v='Station 8' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='9' visible='true' version='1' lat='0.0' lon='0.08'>
|
||||
<tag k='name' v='Station 9' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='10' visible='true' version='1' lat='0.0' lon='0.09'>
|
||||
<tag k='name' v='Station 10' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='11' visible='true' version='1' lat='0.0' lon='0.1'>
|
||||
<tag k='name' v='Station 11' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='12' visible='true' version='1' lat='0.0' lon='0.11'>
|
||||
<tag k='name' v='Station 12' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='13' visible='true' version='1' lat='0.0' lon='0.12'>
|
||||
<tag k='name' v='Station 13' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='14' visible='true' version='1' lat='0.0' lon='0.13'>
|
||||
<tag k='name' v='Station 14' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='15' visible='true' version='1' lat='0.0' lon='0.14'>
|
||||
<tag k='name' v='Station 15' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='16' visible='true' version='1' lat='0.0' lon='0.15'>
|
||||
<tag k='name' v='Station 16' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='17' visible='true' version='1' lat='0.0' lon='0.16'>
|
||||
<tag k='name' v='Station 17' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='18' visible='true' version='1' lat='0.0' lon='0.17'>
|
||||
<tag k='name' v='Station 18' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='19' visible='true' version='1' lat='0.0' lon='0.18'>
|
||||
<tag k='name' v='Station 19' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='20' visible='true' version='1' lat='0.0' lon='0.19'>
|
||||
<tag k='name' v='Station 20' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='21' visible='true' version='1' lat='0.0003' lon='0.09'>
|
||||
<tag k='name' v='Station 10(1)' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='22' visible='true' version='1' lat='0.0003' lon='0.1'>
|
||||
<tag k='name' v='Station 11(1)' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='23' visible='true' version='1' lat='0.004' lon='0.09'>
|
||||
<tag k='name' v='Station 10(2)' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='24' visible='true' version='1' lat='0.004' lon='0.1'>
|
||||
<tag k='name' v='Station 11(2)' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<way id='1' visible='true' version='1'>
|
||||
<nd ref='1' />
|
||||
<nd ref='2' />
|
||||
<nd ref='3' />
|
||||
<nd ref='4' />
|
||||
<nd ref='5' />
|
||||
<nd ref='6' />
|
||||
<nd ref='7' />
|
||||
<nd ref='8' />
|
||||
<nd ref='9' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='2' visible='true' version='1'>
|
||||
<nd ref='9' />
|
||||
<nd ref='10' />
|
||||
<nd ref='11' />
|
||||
<nd ref='12' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='3' visible='true' version='1'>
|
||||
<nd ref='12' />
|
||||
<nd ref='13' />
|
||||
<nd ref='14' />
|
||||
<nd ref='15' />
|
||||
<nd ref='16' />
|
||||
<nd ref='17' />
|
||||
<nd ref='18' />
|
||||
<nd ref='19' />
|
||||
<nd ref='20' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='4' visible='true' version='1'>
|
||||
<nd ref='9' />
|
||||
<nd ref='21' />
|
||||
<nd ref='22' />
|
||||
<nd ref='12' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='5' visible='true' version='1'>
|
||||
<nd ref='9' />
|
||||
<nd ref='23' />
|
||||
<nd ref='24' />
|
||||
<nd ref='12' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<relation id='101' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<tag k='name' v='1: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='102' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='1: 20-...12-11(1)-10(1)-9-...-1' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='103' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<tag k='name' v='2: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='104' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='24' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='2: 20-...12-11(2)-10(2)-9-...-1' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='105' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<tag k='name' v='3: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='106' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='3: 20-...-12-11(1)-9-...-1' />
|
||||
<tag k='note' v='Miss Station 10(1) compared to the twin route' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='107' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<tag k='name' v='4: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='108' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='24' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='4: 20-...12-11(2)-9-...-1' />
|
||||
<tag k='note' v='Miss Station 10(2) compared to the twin route' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='201' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='11: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='11' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='202' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='21' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<member type='way' ref='4' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='11: 20-...-12-11(1)-10(1)-9-...-1' />
|
||||
<tag k='ref' v='11' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='203' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='12: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='12' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='204' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='24' role='' />
|
||||
<member type='node' ref='23' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<member type='way' ref='5' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='12: 20-...12-11(2)-10(2)-9-...-1' />
|
||||
<tag k='ref' v='12' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='205' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='13: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='13' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='206' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='22' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<member type='way' ref='4' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='13: 20-...-12-11(1)-9-...-1' />
|
||||
<tag k='note' v='Miss Station 10(1) compared to the twin route' />
|
||||
<tag k='ref' v='13' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='207' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='10' role='' />
|
||||
<member type='node' ref='11' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<tag k='name' v='14: 1-...-9-10-11-...-20' />
|
||||
<tag k='ref' v='14' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='208' visible='true' version='1'>
|
||||
<member type='node' ref='20' role='' />
|
||||
<member type='node' ref='19' role='' />
|
||||
<member type='node' ref='18' role='' />
|
||||
<member type='node' ref='17' role='' />
|
||||
<member type='node' ref='16' role='' />
|
||||
<member type='node' ref='15' role='' />
|
||||
<member type='node' ref='14' role='' />
|
||||
<member type='node' ref='13' role='' />
|
||||
<member type='node' ref='12' role='' />
|
||||
<member type='node' ref='24' role='' />
|
||||
<member type='node' ref='9' role='' />
|
||||
<member type='node' ref='8' role='' />
|
||||
<member type='node' ref='7' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='3' role='' />
|
||||
<member type='way' ref='5' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='14: 20-...12-11(2)-9-...-1' />
|
||||
<tag k='note' v='Miss Station 10(2) compared to the twin route' />
|
||||
<tag k='ref' v='14' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='1101' visible='true' version='1'>
|
||||
<member type='relation' ref='101' role='' />
|
||||
<member type='relation' ref='102' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='No rails, no omitted stations, close paths' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='1102' visible='true' version='1'>
|
||||
<member type='relation' ref='103' role='' />
|
||||
<member type='relation' ref='104' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='No rails, no omitted stations, distant paths' />
|
||||
<tag k='ref' v='2' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='1103' visible='true' version='1'>
|
||||
<member type='relation' ref='105' role='' />
|
||||
<member type='relation' ref='106' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='No rails, close paths, one route is missing a station inside a divergent rail span' />
|
||||
<tag k='ref' v='3' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='1104' visible='true' version='1'>
|
||||
<member type='relation' ref='107' role='' />
|
||||
<member type='relation' ref='108' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='No rails, distant paths, one route is missing a station inside a divergent rail span' />
|
||||
<tag k='ref' v='4' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='1201' visible='true' version='1'>
|
||||
<member type='relation' ref='201' role='' />
|
||||
<member type='relation' ref='202' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Rails, no omitted stations, close paths' />
|
||||
<tag k='ref' v='11' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='1202' visible='true' version='1'>
|
||||
<member type='relation' ref='203' role='' />
|
||||
<member type='relation' ref='204' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Rails, no omitted stations, distant paths' />
|
||||
<tag k='ref' v='12' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='1203' visible='true' version='1'>
|
||||
<member type='relation' ref='205' role='' />
|
||||
<member type='relation' ref='206' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Rails, close paths, one route is missing a station inside a divergent rail span' />
|
||||
<tag k='ref' v='13' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='1204' visible='true' version='1'>
|
||||
<member type='relation' ref='207' role='' />
|
||||
<member type='relation' ref='208' role='' />
|
||||
<tag k='colour' v='gray' />
|
||||
<tag k='note' v='Rails, distant paths, one route is missing a station inside a divergent rail span' />
|
||||
<tag k='ref' v='14' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
File diff suppressed because it is too large
Load diff
|
@ -1,98 +0,0 @@
|
|||
metro_samples = [
|
||||
{
|
||||
"name": "Transfer at Kuntsevskaya",
|
||||
"xml": """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='244036218' visible='true' version='27' lat='55.7306986' lon='37.4460134'>
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='244038961' visible='true' version='26' lat='55.730801' lon='37.4464724'>
|
||||
<tag k='railway' v='subway_entrance' />
|
||||
</node>
|
||||
<node id='461075776' visible='true' version='5' lat='55.7304682' lon='37.4447392' />
|
||||
<node id='461075811' visible='true' version='7' lat='55.7308273' lon='37.4473927'>
|
||||
<tag k='barrier' v='gate' />
|
||||
</node>
|
||||
<node id='1191237441' visible='true' version='18' lat='55.7308185' lon='37.4459574'>
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='4821481210' visible='true' version='3' lat='55.7305372' lon='37.4447339' />
|
||||
<node id='4821481211' visible='true' version='3' lat='55.7306293' lon='37.4446944' />
|
||||
<node id='4821481212' visible='true' version='3' lat='55.7308921' lon='37.4473467' />
|
||||
<node id='4821481213' visible='true' version='3' lat='55.7309843' lon='37.4473072' />
|
||||
<node id='5176248500' visible='true' version='2' lat='55.7306626' lon='37.4460524'>
|
||||
<tag k='public_transport' v='stop_position' />
|
||||
<tag k='subway' v='yes' />
|
||||
</node>
|
||||
<node id='5176248502' visible='true' version='8' lat='55.7306808' lon='37.4460281'>
|
||||
<tag k='name' v='Кунцевская' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<way id='38836456' version='4' visible='true'>
|
||||
<nd ref='461075776' />
|
||||
<nd ref='461075811' />
|
||||
<tag k='railway' v='platform' />
|
||||
</way>
|
||||
<way id='489951237' visible='true' version='6'>
|
||||
<nd ref='4821481210' />
|
||||
<nd ref='4821481211' />
|
||||
<nd ref='4821481213' />
|
||||
<nd ref='4821481212' />
|
||||
<nd ref='4821481210' />
|
||||
</way>
|
||||
<relation id='7588527' visible='true' version='7'>
|
||||
<member type='node' ref='5176248502' role='' />
|
||||
<member type='node' ref='5176248500' role='stop' />
|
||||
<member type='way' ref='38836456' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='7588528' visible='true' version='6'>
|
||||
<member type='node' ref='5176248502' role='' />
|
||||
<member type='node' ref='244036218' role='stop' />
|
||||
<member type='node' ref='1191237441' role='stop' />
|
||||
<member type='relation' ref='13426423' role='platform' />
|
||||
<member type='node' ref='244038961' role='' />
|
||||
<member type='relation' ref='7588561' role='' /> <!-- cyclic ref -->
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='7588561' visible='true' version='5'>
|
||||
<member type='relation' ref='7588528' role='' />
|
||||
<member type='relation' ref='7588527' role='' />
|
||||
<member type='node' ref='1' role='' /> <!-- incomplete ref -->
|
||||
<member type='way' ref='1' role='' /> <!-- incomplete ref -->
|
||||
<member type='relation' ref='1' role='' /> <!-- incomplete ref -->
|
||||
<tag k='name' v='Кунцевская' />
|
||||
<tag k='public_transport' v='stop_area_group' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
<relation id='13426423' visible='true' version='4'>
|
||||
<member type='way' ref='489951237' role='outer' />
|
||||
<tag k='public_transport' v='platform' />
|
||||
<tag k='type' v='multipolygon' />
|
||||
</relation>
|
||||
<relation id='100' visible='true' version='1'>
|
||||
<tag k='description' v='emtpy relation' />
|
||||
</relation>
|
||||
<relation id='101' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' /> <!-- incomplete ref -->
|
||||
<tag k='description' v='only incomplete members' />
|
||||
</relation>
|
||||
</osm>
|
||||
""", # noqa: E501
|
||||
"expected_centers": {
|
||||
"w38836456": {"lat": 55.73064775, "lon": 37.446065950000005},
|
||||
"w489951237": {"lat": 55.730760724999996, "lon": 37.44602055},
|
||||
"r7588527": {"lat": 55.73066371666667, "lon": 37.44604881666667},
|
||||
"r7588528": {"lat": 55.73075192499999, "lon": 37.44609837},
|
||||
"r7588561": {"lat": 55.73070782083333, "lon": 37.44607359333334},
|
||||
"r13426423": {"lat": 55.730760724999996, "lon": 37.44602055},
|
||||
"r100": None,
|
||||
"r101": None,
|
||||
},
|
||||
},
|
||||
]
|
|
@ -1,450 +0,0 @@
|
|||
metro_samples = [
|
||||
{
|
||||
"name": "No errors",
|
||||
"xml": """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' version='1' lat='0.0' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<relation id='1' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<tag k='name' v='Forward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='2' version='1'>
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='Backward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='3' version='1'>
|
||||
<member type='relation' ref='1' role='' />
|
||||
<member type='relation' ref='2' role='' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
||||
""",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": 2,
|
||||
},
|
||||
],
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"notices": [],
|
||||
},
|
||||
{
|
||||
"name": "Station colour tag present/absent, correct/incorrect, on bear station / with stop_area", # noqa E501
|
||||
"xml": """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' visible='true' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='note' v='no 'colour' tag' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' visible='true' version='1' lat='0.0' lon='0.01'>
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='note' v='correct colour name' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' visible='true' version='1' lat='0.0' lon='0.02'>
|
||||
<tag k='colour' v='#C1e' />
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='note' v='correct colour 3-digit hex code' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='4' visible='true' version='1' lat='0.0' lon='0.03'>
|
||||
<tag k='colour' v='incorrect' />
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='note' v='incorrect 'colour' tag' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='5' visible='true' version='1' lat='0.0' lon='0.04'>
|
||||
<tag k='colour' v='#CD853F' />
|
||||
<tag k='name' v='Station 5' />
|
||||
<tag k='note' v='correct colour 6-digit hex code' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='6' visible='true' version='1' lat='0.0' lon='0.05'>
|
||||
<tag k='colour' v='incorrect' />
|
||||
<tag k='name' v='Station 6' />
|
||||
<tag k='note' v='incorrect colour; station in a stop_area' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<relation id='1' visible='true' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='6' role='' />
|
||||
<tag k='name' v='Forward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='2' visible='true' version='1'>
|
||||
<member type='node' ref='6' role='' />
|
||||
<member type='node' ref='5' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='Backward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='3' visible='true' version='1'>
|
||||
<member type='relation' ref='1' role='' />
|
||||
<member type='relation' ref='2' role='' />
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
<relation id='600' visible='true' version='1'>
|
||||
<member type='node' ref='6' role='' />
|
||||
<tag k='public_transport' v='stop_area' />
|
||||
<tag k='type' v='public_transport' />
|
||||
</relation>
|
||||
</osm>
|
||||
""",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": 6,
|
||||
},
|
||||
],
|
||||
"errors": [],
|
||||
"warnings": [
|
||||
'Unknown colour code: incorrect (node 4, "Station 4")',
|
||||
'Unknown colour code: incorrect (node 6, "Station 6")',
|
||||
],
|
||||
"notices": [],
|
||||
},
|
||||
{
|
||||
"name": "Bad station order",
|
||||
"xml": """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' version='1' lat='0.0' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' version='1' lat='0.0' lon='2.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='4' version='1' lat='0.0' lon='3.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<relation id='1' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<tag k='name' v='Forward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='2' version='1'>
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<tag k='name' v='Backward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='3' version='1'>
|
||||
<member type='relation' ref='1' role='' />
|
||||
<member type='relation' ref='2' role='' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
||||
""",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": 4,
|
||||
},
|
||||
],
|
||||
"errors": [
|
||||
'Angle between stops around "Station 3" (2.0, 0.0) is too narrow, 0 degrees (relation 1, "Forward")', # noqa: E501
|
||||
'Angle between stops around "Station 2" (1.0, 0.0) is too narrow, 0 degrees (relation 1, "Forward")', # noqa: E501
|
||||
],
|
||||
"warnings": [],
|
||||
"notices": [],
|
||||
},
|
||||
{
|
||||
"name": "Angle < 20 degrees",
|
||||
"xml": """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' version='1' lat='0.0' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' version='1' lat='0.2' lon='0.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<relation id='1' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='Forward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='2' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='Backward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='3' version='1'>
|
||||
<member type='relation' ref='1' role='' />
|
||||
<member type='relation' ref='2' role='' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
||||
""",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": 3,
|
||||
},
|
||||
],
|
||||
"errors": [
|
||||
'Angle between stops around "Station 2" (1.0, 0.0) is too narrow, 11 degrees (relation 1, "Forward")', # noqa: E501
|
||||
'Angle between stops around "Station 2" (1.0, 0.0) is too narrow, 11 degrees (relation 2, "Backward")', # noqa: E501
|
||||
],
|
||||
"warnings": [],
|
||||
"notices": [],
|
||||
},
|
||||
{
|
||||
"name": "Angle between 20 and 45 degrees",
|
||||
"xml": """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' version='1' lat='0.0' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' version='1' lat='0.5' lon='0.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<relation id='1' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<tag k='name' v='Forward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='2' version='1'>
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='Backward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='3' version='1'>
|
||||
<member type='relation' ref='1' role='' />
|
||||
<member type='relation' ref='2' role='' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
||||
""",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": 3,
|
||||
},
|
||||
],
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"notices": [
|
||||
'Angle between stops around "Station 2" (1.0, 0.0) is too narrow, 27 degrees (relation 1, "Forward")', # noqa: E501
|
||||
'Angle between stops around "Station 2" (1.0, 0.0) is too narrow, 27 degrees (relation 2, "Backward")', # noqa: E501
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Unordered stops provided each angle > 45 degrees",
|
||||
"xml": """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version='0.6' generator='JOSM'>
|
||||
<node id='1' version='1' lat='0.0' lon='0.0'>
|
||||
<tag k='name' v='Station 1' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='2' version='1' lat='0.0' lon='1.0'>
|
||||
<tag k='name' v='Station 2' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='3' version='1' lat='0.5' lon='0.0'>
|
||||
<tag k='name' v='Station 3' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<node id='4' version='1' lat='1.0' lon='1.0'>
|
||||
<tag k='name' v='Station 4' />
|
||||
<tag k='railway' v='station' />
|
||||
<tag k='station' v='subway' />
|
||||
</node>
|
||||
<way id='1' version='1'>
|
||||
<nd ref='1' />
|
||||
<nd ref='2' />
|
||||
<nd ref='3' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<way id='2' version='1'>
|
||||
<nd ref='3' />
|
||||
<nd ref='4' />
|
||||
<tag k='railway' v='subway' />
|
||||
</way>
|
||||
<relation id='1' version='1'>
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<tag k='name' v='Forward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='2' version='1'>
|
||||
<member type='node' ref='4' role='' />
|
||||
<member type='node' ref='2' role='' />
|
||||
<member type='node' ref='3' role='' />
|
||||
<member type='node' ref='1' role='' />
|
||||
<member type='way' ref='2' role='' />
|
||||
<member type='way' ref='1' role='' />
|
||||
<tag k='name' v='Backward' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='route' v='subway' />
|
||||
<tag k='type' v='route' />
|
||||
</relation>
|
||||
<relation id='3' version='1'>
|
||||
<member type='relation' ref='1' role='' />
|
||||
<member type='relation' ref='2' role='' />
|
||||
<tag k='ref' v='1' />
|
||||
<tag k='colour' v='red' />
|
||||
<tag k='route_master' v='subway' />
|
||||
<tag k='type' v='route_master' />
|
||||
</relation>
|
||||
</osm>
|
||||
""",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": 4,
|
||||
},
|
||||
],
|
||||
"errors": [
|
||||
'Stops on tracks are unordered near "Station 2" (1.0, 0.0) (relation 1, "Forward")', # noqa: E501
|
||||
'Stops on tracks are unordered near "Station 3" (0.0, 0.5) (relation 2, "Backward")', # noqa: E501
|
||||
],
|
||||
"warnings": [],
|
||||
"notices": [],
|
||||
},
|
||||
{
|
||||
"name": (
|
||||
"Many different route masters, both on naked stations and "
|
||||
"stop_positions/stop_areas/transfers, both linear and circular"
|
||||
),
|
||||
"xml_file": "assets/route_masters.osm",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": (3 + 3 + 3 + 5 + 3 + 3 + 4 + 3)
|
||||
+ (3 + 3 + 3 + 3 + 3 + 3 + 4),
|
||||
"num_lines": 8 + 7,
|
||||
"num_interchanges": 0 + 1,
|
||||
},
|
||||
],
|
||||
"errors": [
|
||||
'Only one route in route_master. Please check if it needs a return route (relation 162, "03: 1-2-3")' # noqa: E501
|
||||
],
|
||||
"warnings": [],
|
||||
"notices": [
|
||||
'Route does not have a return direction (relation 155, "02: 1-2-3")', # noqa: E501
|
||||
'Route does not have a return direction (relation 158, "02: 1-3 (2)")', # noqa: E501
|
||||
'Only one route in route_master. Please check if it needs a return route (relation 159, "C: 1-3-5-1")', # noqa: E501
|
||||
'Route does not have a return direction (relation 163, "04: 1-2-3")', # noqa: E501
|
||||
'Route does not have a return direction (relation 164, "04: 2-1")', # noqa: E501
|
||||
'Stop Station 2 (1.0, 0.0) is included in the r203 but not included in r204 (relation 204, "2: 3-1")', # noqa: E501
|
||||
'Route does not have a return direction (relation 205, "3: 1-2-3")', # noqa: E501
|
||||
'Route does not have a return direction (relation 206, "3: 1-2-3")', # noqa: E501
|
||||
'Route does not have a return direction (relation 207, "4: 4-3-2-1")', # noqa: E501
|
||||
'Route does not have a return direction (relation 208, "4: 1-2-3-4")', # noqa: E501
|
||||
'Route does not have a return direction (relation 209, "5: 1-2-3")', # noqa: E501
|
||||
'Route does not have a return direction (relation 210, "5: 2-1")', # noqa: E501
|
||||
'Only one route in route_master. Please check if it needs a return route (relation 213, "C3: 1-2-3-8-1")', # noqa: E501
|
||||
'Route does not have a return direction (relation 168, "C5: 1-3-5-1")', # noqa: E501
|
||||
'Route does not have a return direction (relation 169, "C5: 3-5-1-3")', # noqa: E501
|
||||
],
|
||||
},
|
||||
]
|
|
@ -1,692 +0,0 @@
|
|||
metro_samples = [
|
||||
{
|
||||
"name": "tiny_world",
|
||||
"xml_file": """assets/tiny_world.osm""",
|
||||
"cities_info": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Intersecting 2 metro lines",
|
||||
"num_stations": 4 + 2,
|
||||
"num_lines": 2,
|
||||
"num_interchanges": 1,
|
||||
"networks": "network-1",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "One light rail line",
|
||||
"num_stations": 2,
|
||||
"num_lines": 0,
|
||||
"num_light_lines": 1,
|
||||
"networks": "network-2",
|
||||
},
|
||||
],
|
||||
"gtfs_dir": "assets/tiny_world_gtfs",
|
||||
"transfers": [{"r1", "r2"}, {"r3", "r4"}],
|
||||
"json_dump": """
|
||||
{
|
||||
"stopareas": {
|
||||
"n1": {
|
||||
"id": "n1",
|
||||
"center": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"name": "Station 1",
|
||||
"entrances": []
|
||||
},
|
||||
"r1": {
|
||||
"id": "r1",
|
||||
"center": [
|
||||
0.00470373068,
|
||||
0.0047037307
|
||||
],
|
||||
"name": "Station 2",
|
||||
"entrances": []
|
||||
},
|
||||
"r3": {
|
||||
"id": "r3",
|
||||
"center": [
|
||||
0.01012040581,
|
||||
0.0097589171
|
||||
],
|
||||
"name": "Station 3",
|
||||
"entrances": [
|
||||
{
|
||||
"id": "n201",
|
||||
"name": null,
|
||||
"ref": "3-1",
|
||||
"center": [0.01007169217, 0.00967473055]
|
||||
},
|
||||
{
|
||||
"id": "n202",
|
||||
"name": null,
|
||||
"ref": "3-2",
|
||||
"center": [0.01018702716, 0.00966936613]
|
||||
}
|
||||
]
|
||||
},
|
||||
"n4": {
|
||||
"id": "n4",
|
||||
"center": [
|
||||
0,
|
||||
0.01
|
||||
],
|
||||
"name": "Station 4",
|
||||
"entrances": [
|
||||
{
|
||||
"id": "n205",
|
||||
"name": null,
|
||||
"ref": "4-1",
|
||||
"center": [0.000201163, 0.01015484596]
|
||||
}
|
||||
]
|
||||
},
|
||||
"r2": {
|
||||
"id": "r2",
|
||||
"center": [
|
||||
0.0047718624,
|
||||
0.00514739839
|
||||
],
|
||||
"name": "Station 5",
|
||||
"entrances": []
|
||||
},
|
||||
"n6": {
|
||||
"id": "n6",
|
||||
"center": [
|
||||
0.01,
|
||||
0
|
||||
],
|
||||
"name": "Station 6",
|
||||
"entrances": []
|
||||
},
|
||||
"r4": {
|
||||
"id": "r4",
|
||||
"center": [
|
||||
0.009716854315,
|
||||
0.010286367745
|
||||
],
|
||||
"name": "Station 7",
|
||||
"entrances": [
|
||||
{
|
||||
"id": "n204",
|
||||
"name": null,
|
||||
"ref": "7-1",
|
||||
"center": [0.00952183932, 0.01034796501]
|
||||
},
|
||||
{
|
||||
"id": "n203",
|
||||
"name": null,
|
||||
"ref": "7-2",
|
||||
"center": [0.00959962338, 0.01042574907]
|
||||
}
|
||||
]
|
||||
},
|
||||
"r16": {
|
||||
"id": "r16",
|
||||
"center": [
|
||||
0.012405493905,
|
||||
0.014377764559999999
|
||||
],
|
||||
"name": "Station 8",
|
||||
"entrances": []
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"Intersecting 2 metro lines": {
|
||||
"id": 1,
|
||||
"name": "Intersecting 2 metro lines",
|
||||
"routes": [
|
||||
{
|
||||
"id": "r15",
|
||||
"mode": "subway",
|
||||
"ref": "1",
|
||||
"name": "Blue Line",
|
||||
"colour": "#0000ff",
|
||||
"infill": null,
|
||||
"itineraries": [
|
||||
{
|
||||
"id": "r7",
|
||||
"tracks": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0.00470373068,
|
||||
0.0047037307
|
||||
],
|
||||
[
|
||||
0.009939661455227341,
|
||||
0.009939661455455193
|
||||
]
|
||||
],
|
||||
"start_time": null,
|
||||
"end_time": null,
|
||||
"interval": null,
|
||||
"duration": null,
|
||||
"stops": [
|
||||
{
|
||||
"stoparea_id": "n1",
|
||||
"distance": 0
|
||||
},
|
||||
{
|
||||
"stoparea_id": "r1",
|
||||
"distance": 741
|
||||
},
|
||||
{
|
||||
"stoparea_id": "r3",
|
||||
"distance": 1565
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "r8",
|
||||
"tracks": [
|
||||
[
|
||||
0.009939661455227341,
|
||||
0.009939661455455193
|
||||
],
|
||||
[
|
||||
0.00470373068,
|
||||
0.0047037307
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"start_time": null,
|
||||
"end_time": null,
|
||||
"interval": null,
|
||||
"duration": null,
|
||||
"stops": [
|
||||
{
|
||||
"stoparea_id": "r3",
|
||||
"distance": 0
|
||||
},
|
||||
{
|
||||
"stoparea_id": "r1",
|
||||
"distance": 824
|
||||
},
|
||||
{
|
||||
"stoparea_id": "n1",
|
||||
"distance": 1565
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "r14",
|
||||
"mode": "subway",
|
||||
"ref": "2",
|
||||
"name": "Red Line",
|
||||
"colour": "#ff0000",
|
||||
"infill": null,
|
||||
"itineraries": [
|
||||
{
|
||||
"id": "r12",
|
||||
"tracks": [
|
||||
[
|
||||
0,
|
||||
0.01
|
||||
],
|
||||
[
|
||||
0.01,
|
||||
0
|
||||
]
|
||||
],
|
||||
"start_time": null,
|
||||
"end_time": null,
|
||||
"interval": null,
|
||||
"duration": 600,
|
||||
"stops": [
|
||||
{
|
||||
"stoparea_id": "n4",
|
||||
"distance": 0
|
||||
},
|
||||
{
|
||||
"stoparea_id": "r2",
|
||||
"distance": 758
|
||||
},
|
||||
{
|
||||
"stoparea_id": "n6",
|
||||
"distance": 1575
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "r13",
|
||||
"tracks": [
|
||||
[
|
||||
0.01,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0.01
|
||||
]
|
||||
],
|
||||
"start_time": null,
|
||||
"end_time": null,
|
||||
"interval": null,
|
||||
"duration": 480,
|
||||
"stops": [
|
||||
{
|
||||
"stoparea_id": "n6",
|
||||
"distance": 0
|
||||
},
|
||||
{
|
||||
"stoparea_id": "r2",
|
||||
"distance": 817
|
||||
},
|
||||
{
|
||||
"stoparea_id": "n4",
|
||||
"distance": 1575
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"One light rail line": {
|
||||
"id": 2,
|
||||
"name": "One light rail line",
|
||||
"routes": [
|
||||
{
|
||||
"id": "r11",
|
||||
"mode": "light_rail",
|
||||
"ref": "LR",
|
||||
"name": "LR Line",
|
||||
"colour": "#a52a2a",
|
||||
"infill": "#ffffff",
|
||||
"itineraries": [
|
||||
{
|
||||
"id": "r9",
|
||||
"tracks": [
|
||||
[
|
||||
0.00976752835,
|
||||
0.01025306758
|
||||
],
|
||||
[
|
||||
0.01245616794,
|
||||
0.01434446439
|
||||
]
|
||||
],
|
||||
"start_time": null,
|
||||
"end_time": null,
|
||||
"interval": null,
|
||||
"duration": 300,
|
||||
"stops": [
|
||||
{
|
||||
"stoparea_id": "r4",
|
||||
"distance": 0
|
||||
},
|
||||
{
|
||||
"stoparea_id": "r16",
|
||||
"distance": 545
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "r10",
|
||||
"tracks": [
|
||||
[
|
||||
0.012321033122529725,
|
||||
0.014359650255679167
|
||||
],
|
||||
[
|
||||
0.00966618028,
|
||||
0.01031966791
|
||||
]
|
||||
],
|
||||
"start_time": null,
|
||||
"end_time": null,
|
||||
"interval": null,
|
||||
"duration": 300,
|
||||
"stops": [
|
||||
{
|
||||
"stoparea_id": "r16",
|
||||
"distance": 0
|
||||
},
|
||||
{
|
||||
"stoparea_id": "r4",
|
||||
"distance": 538
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"transfers": [
|
||||
[
|
||||
"r1",
|
||||
"r2"
|
||||
],
|
||||
[
|
||||
"r3",
|
||||
"r4"
|
||||
]
|
||||
]
|
||||
}
|
||||
""",
|
||||
"mapsme_output": {
|
||||
"stops": [
|
||||
{
|
||||
"name": "Station 1",
|
||||
"int_name": None,
|
||||
"lat": 0.0,
|
||||
"lon": 0.0,
|
||||
"osm_type": "node",
|
||||
"osm_id": 1,
|
||||
"id": 8,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 1,
|
||||
"lon": 0.0,
|
||||
"lat": 0.0,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 1,
|
||||
"lon": 0.0,
|
||||
"lat": 0.0,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Station 2",
|
||||
"int_name": None,
|
||||
"lat": 0.0047037307,
|
||||
"lon": 0.00470373068,
|
||||
"osm_type": "node",
|
||||
"osm_id": 2,
|
||||
"id": 14,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 2,
|
||||
"lon": 0.0047209447,
|
||||
"lat": 0.004686516680000001,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 2,
|
||||
"lon": 0.0047209447,
|
||||
"lat": 0.004686516680000001,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Station 3",
|
||||
"int_name": None,
|
||||
"lat": 0.0097589171,
|
||||
"lon": 0.01012040581,
|
||||
"osm_type": "node",
|
||||
"osm_id": 3,
|
||||
"id": 30,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 201,
|
||||
"lon": 0.01007169217,
|
||||
"lat": 0.00967473055,
|
||||
"distance": 68,
|
||||
},
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 202,
|
||||
"lon": 0.01018702716,
|
||||
"lat": 0.00966936613,
|
||||
"distance": 69,
|
||||
},
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 201,
|
||||
"lon": 0.01007169217,
|
||||
"lat": 0.00967473055,
|
||||
"distance": 68,
|
||||
},
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 202,
|
||||
"lon": 0.01018702716,
|
||||
"lat": 0.00966936613,
|
||||
"distance": 69,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Station 4",
|
||||
"int_name": None,
|
||||
"lat": 0.01,
|
||||
"lon": 0.0,
|
||||
"osm_type": "node",
|
||||
"osm_id": 4,
|
||||
"id": 32,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 205,
|
||||
"lon": 0.000201163,
|
||||
"lat": 0.01015484596,
|
||||
"distance": 80,
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 205,
|
||||
"lon": 0.000201163,
|
||||
"lat": 0.01015484596,
|
||||
"distance": 80,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Station 5",
|
||||
"int_name": None,
|
||||
"lat": 0.00514739839,
|
||||
"lon": 0.0047718624,
|
||||
"osm_type": "node",
|
||||
"osm_id": 5,
|
||||
"id": 22,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 5,
|
||||
"lon": 0.0047718624,
|
||||
"lat": 0.00514739839,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 5,
|
||||
"lon": 0.0047718624,
|
||||
"lat": 0.00514739839,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Station 6",
|
||||
"int_name": None,
|
||||
"lat": 0.0,
|
||||
"lon": 0.01,
|
||||
"osm_type": "node",
|
||||
"osm_id": 6,
|
||||
"id": 48,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 6,
|
||||
"lon": 0.01,
|
||||
"lat": 0.0,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 6,
|
||||
"lon": 0.01,
|
||||
"lat": 0.0,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Station 7",
|
||||
"int_name": None,
|
||||
"lat": 0.010286367745,
|
||||
"lon": 0.009716854315,
|
||||
"osm_type": "node",
|
||||
"osm_id": 7,
|
||||
"id": 38,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 203,
|
||||
"lon": 0.00959962338,
|
||||
"lat": 0.01042574907,
|
||||
"distance": 75,
|
||||
},
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 204,
|
||||
"lon": 0.00952183932,
|
||||
"lat": 0.01034796501,
|
||||
"distance": 76,
|
||||
},
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 203,
|
||||
"lon": 0.00959962338,
|
||||
"lat": 0.01042574907,
|
||||
"distance": 75,
|
||||
},
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 204,
|
||||
"lon": 0.00952183932,
|
||||
"lat": 0.01034796501,
|
||||
"distance": 76,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Station 8",
|
||||
"int_name": None,
|
||||
"lat": 0.014377764559999999,
|
||||
"lon": 0.012405493905,
|
||||
"osm_type": "node",
|
||||
"osm_id": 8,
|
||||
"id": 134,
|
||||
"entrances": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 8,
|
||||
"lon": 0.012391026016666667,
|
||||
"lat": 0.01436273297,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
{
|
||||
"osm_type": "node",
|
||||
"osm_id": 8,
|
||||
"lon": 0.012391026016666667,
|
||||
"lat": 0.01436273297,
|
||||
"distance": 60,
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
"transfers": [(14, 22, 81), (30, 38, 106)],
|
||||
"networks": [
|
||||
{
|
||||
"network": "Intersecting 2 metro lines",
|
||||
"routes": [
|
||||
{
|
||||
"type": "subway",
|
||||
"ref": "1",
|
||||
"name": "Blue Line",
|
||||
"colour": "0000ff",
|
||||
"route_id": 30,
|
||||
"itineraries": [
|
||||
{
|
||||
"stops": [[8, 0], [14, 67], [30, 141]],
|
||||
"interval": 150,
|
||||
},
|
||||
{
|
||||
"stops": [[30, 0], [14, 74], [8, 141]],
|
||||
"interval": 150,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "subway",
|
||||
"ref": "2",
|
||||
"name": "Red Line",
|
||||
"colour": "ff0000",
|
||||
"route_id": 28,
|
||||
"itineraries": [
|
||||
{
|
||||
"stops": [[32, 0], [22, 68], [48, 142]],
|
||||
"interval": 150,
|
||||
},
|
||||
{
|
||||
"stops": [[48, 0], [22, 74], [32, 142]],
|
||||
"interval": 150,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"agency_id": 1,
|
||||
},
|
||||
{
|
||||
"network": "One light rail line",
|
||||
"routes": [
|
||||
{
|
||||
"type": "light_rail",
|
||||
"ref": "LR",
|
||||
"name": "LR Line",
|
||||
"colour": "ffffff",
|
||||
"route_id": 22,
|
||||
"itineraries": [
|
||||
{
|
||||
"stops": [[38, 0], [134, 49]],
|
||||
"interval": 150,
|
||||
},
|
||||
{
|
||||
"stops": [[134, 0], [38, 48]],
|
||||
"interval": 150,
|
||||
},
|
||||
],
|
||||
"casing": "a52a2a",
|
||||
}
|
||||
],
|
||||
"agency_id": 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
|
@ -1,78 +0,0 @@
|
|||
metro_samples = [
|
||||
{
|
||||
"name": (
|
||||
"Many different routes, both on naked stations and stop_positions/stop_areas/transfers, both linear and circular" # noqa: E501
|
||||
),
|
||||
"xml_file": "assets/twin_routes.osm",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": (3 + 4 + 5 + 5) + (3 + 6 + 7 + 5 + 6 + 7 + 7),
|
||||
"num_lines": 4 + 7,
|
||||
"num_interchanges": 0 + 2,
|
||||
},
|
||||
],
|
||||
"twin_routes": { # route master => twin routes
|
||||
"r10021": {"r151": "r153", "r153": "r151"},
|
||||
"r10022": {},
|
||||
"r10023": {},
|
||||
"C": {},
|
||||
"r10001": {"r201": "r202", "r202": "r201"},
|
||||
"r10002": {},
|
||||
"r10003": {"r205": "r206", "r206": "r205"},
|
||||
"r10004": {},
|
||||
"r10005": {},
|
||||
"r10006": {},
|
||||
"C3": {},
|
||||
},
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"notices": [
|
||||
'Route does not have a return direction (relation 154, "02: 4-3")',
|
||||
'Route does not have a return direction (relation 155, "02: 1-3")',
|
||||
'Route does not have a return direction (relation 156, "02: 2-4")',
|
||||
'Route does not have a return direction (relation 157, "02: 4-1")',
|
||||
'Route does not have a return direction (relation 158, "02: 1-3 (2)")', # noqa: E501
|
||||
'Only one route in route_master. Please check if it needs a return route (relation 159, "C: 1-2-3-4-5-1")', # noqa: E501
|
||||
'Stop Station 4 (3.0, 0.0) is included in the r205 but not included in r206 (relation 206, "3: 7-6-5-3-2-1")', # noqa: E501
|
||||
'Route does not have a return direction (relation 207, "4: 4-3-2-1")', # noqa: E501
|
||||
'Route does not have a return direction (relation 208, "4: 1-2-3-4")', # noqa: E501
|
||||
'Route does not have a return direction (relation 209, "5: 1-2-3-5-6-7")', # noqa: E501
|
||||
'Route does not have a return direction (relation 210, "5: 6-5-3-2-1")', # noqa: E501
|
||||
'Only one route in route_master. Please check if it needs a return route (relation 213, "C3: 1-2-3-5-6-7-8-1")', # noqa: E501
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Twin routes diverging for some extent",
|
||||
"xml_file": "assets/twin_routes_with_divergence.osm",
|
||||
"cities_info": [
|
||||
{
|
||||
"num_stations": (22 + 22 + 21 + 21) * 2,
|
||||
"num_lines": 4 * 2,
|
||||
"num_interchanges": 0,
|
||||
},
|
||||
],
|
||||
"twin_routes": { # route master => twin routes
|
||||
"r1101": {"r101": "r102", "r102": "r101"},
|
||||
"r1102": {"r103": "r104", "r104": "r103"},
|
||||
"r1103": {"r105": "r106", "r106": "r105"},
|
||||
"r1104": {"r107": "r108", "r108": "r107"},
|
||||
"r1201": {"r201": "r202", "r202": "r201"},
|
||||
"r1202": {"r203": "r204", "r204": "r203"},
|
||||
"r1203": {"r205": "r206", "r206": "r205"},
|
||||
"r1204": {"r207": "r208", "r208": "r207"},
|
||||
},
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"notices": [
|
||||
'Should there be one stoparea or a transfer between Station 11 (0.1, 0.0) and Station 11(1) (0.1, 0.0003)? (relation 101, "1: 1-...-9-10-11-...-20")', # noqa: E501
|
||||
'Should there be one stoparea or a transfer between Station 10 (0.09, 0.0) and Station 10(1) (0.09, 0.0003)? (relation 101, "1: 1-...-9-10-11-...-20")', # noqa: E501
|
||||
'Stop Station 10 (0.09, 0.0) is included in the r105 but not included in r106 (relation 106, "3: 20-...-12-11(1)-9-...-1")', # noqa: E501
|
||||
'Should there be one stoparea or a transfer between Station 11 (0.1, 0.0) and Station 11(1) (0.1, 0.0003)? (relation 105, "3: 1-...-9-10-11-...-20")', # noqa: E501
|
||||
'Stop Station 10 (0.09, 0.0) is included in the r107 but not included in r108 (relation 108, "4: 20-...12-11(2)-9-...-1")', # noqa: E501
|
||||
'Should there be one stoparea or a transfer between Station 11 (0.1, 0.0) and Station 11(1) (0.1, 0.0003)? (relation 201, "11: 1-...-9-10-11-...-20")', # noqa: E501
|
||||
'Should there be one stoparea or a transfer between Station 10 (0.09, 0.0) and Station 10(1) (0.09, 0.0003)? (relation 201, "11: 1-...-9-10-11-...-20")', # noqa: E501
|
||||
'Stop Station 10 (0.09, 0.0) is included in the r205 but not included in r206 (relation 206, "13: 20-...-12-11(1)-9-...-1")', # noqa: E501
|
||||
'Should there be one stoparea or a transfer between Station 11 (0.1, 0.0) and Station 11(1) (0.1, 0.0003)? (relation 205, "13: 1-...-9-10-11-...-20")', # noqa: E501
|
||||
],
|
||||
},
|
||||
]
|
|
@ -1,112 +0,0 @@
|
|||
from subways.tests.sample_data_for_build_tracks import metro_samples
|
||||
from subways.tests.util import JsonLikeComparisonMixin, TestCase
|
||||
|
||||
|
||||
class TestOneRouteTracks(JsonLikeComparisonMixin, TestCase):
|
||||
"""Test tracks extending and truncating on one-route networks"""
|
||||
|
||||
def prepare_city_routes(self, metro_sample: dict) -> tuple:
|
||||
cities, transfers = self.prepare_cities(metro_sample)
|
||||
city = cities[0]
|
||||
|
||||
self.assertTrue(city.is_good)
|
||||
|
||||
route_master = list(city.routes.values())[0]
|
||||
variants = route_master.routes
|
||||
|
||||
fwd_route = [v for v in variants if v.name == "Forward"][0]
|
||||
bwd_route = [v for v in variants if v.name == "Backward"][0]
|
||||
|
||||
return fwd_route, bwd_route
|
||||
|
||||
def _test_tracks_extending_for_network(self, metro_sample: dict) -> None:
|
||||
fwd_route, bwd_route = self.prepare_city_routes(metro_sample)
|
||||
|
||||
self.assertEqual(
|
||||
fwd_route.tracks,
|
||||
metro_sample["tracks"],
|
||||
"Wrong tracks",
|
||||
)
|
||||
extended_tracks = fwd_route.get_extended_tracks()
|
||||
self.assertEqual(
|
||||
extended_tracks,
|
||||
metro_sample["extended_tracks"],
|
||||
"Wrong tracks after extending",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
bwd_route.tracks,
|
||||
metro_sample["tracks"][::-1],
|
||||
"Wrong backward tracks",
|
||||
)
|
||||
extended_tracks = bwd_route.get_extended_tracks()
|
||||
self.assertEqual(
|
||||
extended_tracks,
|
||||
metro_sample["extended_tracks"][::-1],
|
||||
"Wrong backward tracks after extending",
|
||||
)
|
||||
|
||||
def _test_tracks_truncating_for_network(self, metro_sample: dict) -> None:
|
||||
fwd_route, bwd_route = self.prepare_city_routes(metro_sample)
|
||||
|
||||
truncated_tracks = fwd_route.get_truncated_tracks(fwd_route.tracks)
|
||||
self.assertEqual(
|
||||
truncated_tracks,
|
||||
metro_sample["truncated_tracks"],
|
||||
"Wrong tracks after truncating",
|
||||
)
|
||||
truncated_tracks = bwd_route.get_truncated_tracks(bwd_route.tracks)
|
||||
self.assertEqual(
|
||||
truncated_tracks,
|
||||
metro_sample["truncated_tracks"][::-1],
|
||||
"Wrong backward tracks after truncating",
|
||||
)
|
||||
|
||||
def _test_stop_positions_on_rails_for_network(self, sample: dict) -> None:
|
||||
fwd_route, bwd_route = self.prepare_city_routes(sample)
|
||||
|
||||
for route, route_label in zip(
|
||||
(fwd_route, bwd_route), ("forward", "backward")
|
||||
):
|
||||
route_data = sample[route_label]
|
||||
|
||||
for attr in (
|
||||
"first_stop_on_rails_index",
|
||||
"last_stop_on_rails_index",
|
||||
):
|
||||
self.assertEqual(
|
||||
getattr(route, attr),
|
||||
route_data[attr],
|
||||
f"Wrong {attr} for {route_label} route",
|
||||
)
|
||||
|
||||
first_ind = route_data["first_stop_on_rails_index"]
|
||||
last_ind = route_data["last_stop_on_rails_index"]
|
||||
positions_on_rails = [
|
||||
rs.positions_on_rails
|
||||
for rs in route.stops[first_ind : last_ind + 1] # noqa E203
|
||||
]
|
||||
self.assertSequenceAlmostEqual(
|
||||
positions_on_rails, route_data["positions_on_rails"]
|
||||
)
|
||||
|
||||
def test_tracks_extending(self) -> None:
|
||||
for sample in metro_samples:
|
||||
sample_name = sample["name"]
|
||||
sample["cities_info"][0]["name"] = sample_name
|
||||
with self.subTest(msg=sample_name):
|
||||
self._test_tracks_extending_for_network(sample)
|
||||
|
||||
def test_tracks_truncating(self) -> None:
|
||||
for sample in metro_samples:
|
||||
sample_name = sample["name"]
|
||||
sample["cities_info"][0]["name"] = sample_name
|
||||
with self.subTest(msg=sample_name):
|
||||
self._test_tracks_truncating_for_network(sample)
|
||||
|
||||
def test_stop_position_on_rails(self) -> None:
|
||||
for sample in metro_samples:
|
||||
sample_name = sample["name"]
|
||||
sample["cities_info"][0]["name"] = sample_name
|
||||
with self.subTest(msg=sample_name):
|
||||
self._test_stop_positions_on_rails_for_network(sample)
|
|
@ -1,54 +0,0 @@
|
|||
import io
|
||||
from unittest import TestCase
|
||||
|
||||
from subways.validation import calculate_centers
|
||||
from subways.subway_io import load_xml
|
||||
from subways.tests.sample_data_for_center_calculation import metro_samples
|
||||
|
||||
|
||||
class TestCenterCalculation(TestCase):
|
||||
"""Test center calculation. Test data [should] contain among others
|
||||
the following edge cases:
|
||||
- an empty relation. Its element should not obtain "center" key.
|
||||
- relation as member of another relation, the child relation following
|
||||
the parent in the OSM XML.
|
||||
- relation with incomplete members (broken references).
|
||||
- relations with cyclic references.
|
||||
"""
|
||||
|
||||
def test_calculate_centers(self) -> None:
|
||||
for sample in metro_samples:
|
||||
with self.subTest(msg=sample["name"]):
|
||||
self._test_calculate_centers_for_sample(sample)
|
||||
|
||||
def _test_calculate_centers_for_sample(self, metro_sample: dict) -> None:
|
||||
elements = load_xml(io.BytesIO(metro_sample["xml"].encode()))
|
||||
calculate_centers(elements)
|
||||
|
||||
elements_dict = {
|
||||
f"{'w' if el['type'] == 'way' else 'r'}{el['id']}": el
|
||||
for el in elements
|
||||
}
|
||||
|
||||
calculated_centers = {
|
||||
k: el["center"]
|
||||
for k, el in elements_dict.items()
|
||||
if "center" in el
|
||||
}
|
||||
|
||||
expected_centers = metro_sample["expected_centers"]
|
||||
|
||||
self.assertTrue(set(calculated_centers).issubset(expected_centers))
|
||||
|
||||
for k, correct_center in expected_centers.items():
|
||||
if correct_center is None:
|
||||
self.assertNotIn("center", elements_dict[k])
|
||||
else:
|
||||
self.assertIn(k, calculated_centers)
|
||||
calculated_center = calculated_centers[k]
|
||||
self.assertAlmostEqual(
|
||||
calculated_center["lat"], correct_center["lat"], places=10
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
calculated_center["lon"], correct_center["lon"], places=10
|
||||
)
|
|
@ -1,36 +0,0 @@
|
|||
import itertools
|
||||
|
||||
from subways.tests.sample_data_for_error_messages import (
|
||||
metro_samples as metro_samples_error,
|
||||
)
|
||||
from subways.tests.sample_data_for_twin_routes import (
|
||||
metro_samples as metro_samples_route_masters,
|
||||
)
|
||||
from subways.tests.util import TestCase
|
||||
|
||||
|
||||
class TestValidationMessages(TestCase):
|
||||
"""Test that the validator provides expected validation messages
|
||||
on different types of errors in input OSM data.
|
||||
"""
|
||||
|
||||
def _test_validation_messages_for_network(
|
||||
self, metro_sample: dict
|
||||
) -> None:
|
||||
cities, transfers = self.prepare_cities(metro_sample)
|
||||
city = cities[0]
|
||||
|
||||
for err_level in ("errors", "warnings", "notices"):
|
||||
self.assertListEqual(
|
||||
sorted(getattr(city, err_level)),
|
||||
sorted(metro_sample[err_level]),
|
||||
)
|
||||
|
||||
def test_validation_messages(self) -> None:
|
||||
for sample in itertools.chain(
|
||||
metro_samples_error, metro_samples_route_masters
|
||||
):
|
||||
if "errors" not in sample:
|
||||
continue
|
||||
with self.subTest(msg=sample["name"]):
|
||||
self._test_validation_messages_for_network(sample)
|
|
@ -1,30 +0,0 @@
|
|||
from copy import deepcopy
|
||||
|
||||
from subways.tests.sample_data_for_outputs import metro_samples
|
||||
from subways.tests.util import TestCase, JsonLikeComparisonMixin
|
||||
|
||||
|
||||
class TestTransfers(JsonLikeComparisonMixin, TestCase):
|
||||
"""Test that the validator provides expected set of transfers."""
|
||||
|
||||
def _test__find_transfers__for_sample(self, metro_sample: dict) -> None:
|
||||
cities, transfers = self.prepare_cities(metro_sample)
|
||||
expected_transfers = metro_sample["transfers"]
|
||||
|
||||
self.assertSequenceAlmostEqualIgnoreOrder(
|
||||
expected_transfers,
|
||||
transfers,
|
||||
cmp=lambda transfer_as_set: sorted(transfer_as_set),
|
||||
)
|
||||
|
||||
def test__find_transfers(self) -> None:
|
||||
sample1 = metro_samples[0]
|
||||
|
||||
sample2 = deepcopy(metro_samples[0])
|
||||
# Make the second city invalid and thus exclude the inter-city transfer
|
||||
sample2["cities_info"][1]["num_stations"] += 1
|
||||
sample2["transfers"] = [{"r1", "r2"}]
|
||||
|
||||
for sample in sample1, sample2:
|
||||
with self.subTest(msg=sample["name"]):
|
||||
self._test__find_transfers__for_sample(sample)
|
|
@ -1,160 +0,0 @@
|
|||
import csv
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
from subways.processors._common import transit_to_dict
|
||||
from subways.processors.gtfs import (
|
||||
dict_to_row,
|
||||
GTFS_COLUMNS,
|
||||
transit_data_to_gtfs,
|
||||
)
|
||||
from subways.tests.sample_data_for_outputs import metro_samples
|
||||
from subways.tests.util import TestCase
|
||||
|
||||
|
||||
class TestGTFS(TestCase):
|
||||
"""Test processors/gtfs.py"""
|
||||
|
||||
def test__dict_to_row__Nones_and_absent_keys(self) -> None:
|
||||
"""Test that absent or None values in a GTFS feature item
|
||||
are converted by dict_to_row() function to empty strings
|
||||
in right amount.
|
||||
"""
|
||||
|
||||
if GTFS_COLUMNS["trips"][:3] != ["route_id", "service_id", "trip_id"]:
|
||||
raise RuntimeError("GTFS column names/order inconsistency")
|
||||
|
||||
test_trips = [
|
||||
{
|
||||
"description": "Absent keys",
|
||||
"trip_data": {
|
||||
"route_id": 1,
|
||||
"service_id": "a",
|
||||
"trip_id": "tr_123",
|
||||
},
|
||||
},
|
||||
{
|
||||
"description": "None or absent keys",
|
||||
"trip_data": {
|
||||
"route_id": 1,
|
||||
"service_id": "a",
|
||||
"trip_id": "tr_123",
|
||||
"trip_headsign": None,
|
||||
"trip_short_name": None,
|
||||
"route_pattern_id": None,
|
||||
},
|
||||
},
|
||||
{
|
||||
"description": "None, empty-string or absent keys",
|
||||
"trip_data": {
|
||||
"route_id": 1,
|
||||
"service_id": "a",
|
||||
"trip_id": "tr_123",
|
||||
"trip_headsign": "",
|
||||
"trip_short_name": "",
|
||||
"route_pattern_id": None,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
answer = [1, "a", "tr_123"] + [""] * (len(GTFS_COLUMNS["trips"]) - 3)
|
||||
|
||||
for test_trip in test_trips:
|
||||
with self.subTest(msg=test_trip["description"]):
|
||||
self.assertListEqual(
|
||||
dict_to_row(test_trip["trip_data"], "trips"), answer
|
||||
)
|
||||
|
||||
def test__dict_to_row__numeric_values(self) -> None:
|
||||
"""Test that zero numeric values remain zeros in dict_to_row()
|
||||
function, and not empty strings or None.
|
||||
"""
|
||||
|
||||
shapes = [
|
||||
{
|
||||
"description": "Numeric non-zeroes",
|
||||
"shape_data": {
|
||||
"shape_id": 1,
|
||||
"shape_pt_lat": 55.3242425,
|
||||
"shape_pt_lon": -179.23242,
|
||||
"shape_pt_sequence": 133,
|
||||
"shape_dist_traveled": 1.2345,
|
||||
},
|
||||
"answer": [1, 55.3242425, -179.23242, 133, 1.2345],
|
||||
},
|
||||
{
|
||||
"description": "Numeric zeroes and None keys",
|
||||
"shape_data": {
|
||||
"shape_id": 0,
|
||||
"shape_pt_lat": 0.0,
|
||||
"shape_pt_lon": 0,
|
||||
"shape_pt_sequence": 0,
|
||||
"shape_dist_traveled": None,
|
||||
},
|
||||
"answer": [0, 0.0, 0, 0, ""],
|
||||
},
|
||||
]
|
||||
|
||||
for shape in shapes:
|
||||
with self.subTest(shape["description"]):
|
||||
self.assertListEqual(
|
||||
dict_to_row(shape["shape_data"], "shapes"), shape["answer"]
|
||||
)
|
||||
|
||||
def test__transit_data_to_gtfs(self) -> None:
|
||||
for metro_sample in metro_samples:
|
||||
cities, transfers = self.prepare_cities(metro_sample)
|
||||
calculated_transit_data = transit_to_dict(cities, transfers)
|
||||
calculated_gtfs_data = transit_data_to_gtfs(
|
||||
calculated_transit_data
|
||||
)
|
||||
|
||||
control_gtfs_data = self._readGtfs(
|
||||
Path(__file__).resolve().parent / metro_sample["gtfs_dir"]
|
||||
)
|
||||
self._compareGtfs(calculated_gtfs_data, control_gtfs_data)
|
||||
|
||||
@staticmethod
|
||||
def _readGtfs(gtfs_dir: Path) -> dict:
|
||||
gtfs_data = dict()
|
||||
for gtfs_feature in GTFS_COLUMNS:
|
||||
with open(gtfs_dir / f"{gtfs_feature}.txt") as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # read header
|
||||
rows = list(reader)
|
||||
gtfs_data[gtfs_feature] = rows
|
||||
return gtfs_data
|
||||
|
||||
def _compareGtfs(
|
||||
self, calculated_gtfs_data: dict, control_gtfs_data: dict
|
||||
) -> None:
|
||||
for gtfs_feature in GTFS_COLUMNS:
|
||||
calculated_rows = sorted(
|
||||
map(
|
||||
partial(dict_to_row, record_type=gtfs_feature),
|
||||
calculated_gtfs_data[gtfs_feature],
|
||||
)
|
||||
)
|
||||
control_rows = sorted(control_gtfs_data[gtfs_feature])
|
||||
|
||||
self.assertEqual(len(calculated_rows), len(control_rows))
|
||||
|
||||
for i, (calculated_row, control_row) in enumerate(
|
||||
zip(calculated_rows, control_rows)
|
||||
):
|
||||
self.assertEqual(
|
||||
len(calculated_row),
|
||||
len(control_row),
|
||||
f"Different length of {i}-th row of {gtfs_feature}",
|
||||
)
|
||||
for calculated_value, control_value in zip(
|
||||
calculated_row, control_row
|
||||
):
|
||||
if calculated_value is None:
|
||||
self.assertEqual(control_value, "", f"in {i}-th row")
|
||||
else: # convert str to float/int/str
|
||||
self.assertAlmostEqual(
|
||||
calculated_value,
|
||||
type(calculated_value)(control_value),
|
||||
places=10,
|
||||
)
|
|
@ -1,53 +0,0 @@
|
|||
from operator import itemgetter
|
||||
|
||||
from subways.processors.mapsme import transit_data_to_mapsme
|
||||
from subways.tests.sample_data_for_outputs import metro_samples
|
||||
from subways.tests.util import JsonLikeComparisonMixin, TestCase
|
||||
|
||||
|
||||
class TestMapsme(JsonLikeComparisonMixin, TestCase):
|
||||
"""Test processors/mapsme.py"""
|
||||
|
||||
def test__transit_data_to_mapsme(self) -> None:
|
||||
for sample in metro_samples:
|
||||
with self.subTest(msg=sample["name"]):
|
||||
self._test__transit_data_to_mapsme__for_sample(sample)
|
||||
|
||||
def _test__transit_data_to_mapsme__for_sample(
|
||||
self, metro_sample: dict
|
||||
) -> None:
|
||||
cities, transfers = self.prepare_cities(metro_sample)
|
||||
calculated_mapsme_data = transit_data_to_mapsme(
|
||||
cities, transfers, cache_path=None
|
||||
)
|
||||
control_mapsme_data = metro_sample["mapsme_output"]
|
||||
|
||||
self.assertSetEqual(
|
||||
set(control_mapsme_data.keys()),
|
||||
set(calculated_mapsme_data.keys()),
|
||||
)
|
||||
|
||||
self.assertSequenceAlmostEqualIgnoreOrder(
|
||||
control_mapsme_data["stops"],
|
||||
calculated_mapsme_data["stops"],
|
||||
cmp=itemgetter("id"),
|
||||
unordered_lists={
|
||||
"entrances": lambda e: (e["osm_type"], e["osm_id"]),
|
||||
"exits": lambda e: (e["osm_type"], e["osm_id"]),
|
||||
},
|
||||
)
|
||||
|
||||
self.assertSequenceAlmostEqualIgnoreOrder(
|
||||
control_mapsme_data["transfers"],
|
||||
calculated_mapsme_data["transfers"],
|
||||
)
|
||||
|
||||
self.assertSequenceAlmostEqualIgnoreOrder(
|
||||
control_mapsme_data["networks"],
|
||||
calculated_mapsme_data["networks"],
|
||||
cmp=itemgetter("network"),
|
||||
unordered_lists={
|
||||
"routes": itemgetter("route_id"),
|
||||
"itineraries": lambda it: (it["stops"], it["interval"]),
|
||||
},
|
||||
)
|
|
@ -1,163 +0,0 @@
|
|||
from unittest import TestCase, mock
|
||||
|
||||
from subways.overpass import compose_overpass_request, overpass_request
|
||||
|
||||
|
||||
class TestOverpassQuery(TestCase):
|
||||
def test__compose_overpass_request__no_bboxes(self) -> None:
|
||||
bboxes = []
|
||||
for overground in (True, False):
|
||||
with self.subTest(msg=f"{overground=}"):
|
||||
with self.assertRaises(RuntimeError):
|
||||
compose_overpass_request(overground, bboxes)
|
||||
|
||||
def test__compose_overpass_request__one_bbox(self) -> None:
|
||||
bboxes = [[1, 2, 3, 4]]
|
||||
|
||||
expected = {
|
||||
False: (
|
||||
"[out:json][timeout:1000];"
|
||||
"("
|
||||
"("
|
||||
'rel[route="light_rail"](1,2,3,4);'
|
||||
'rel[route="monorail"](1,2,3,4);'
|
||||
'rel[route="subway"](1,2,3,4);'
|
||||
'rel[route="train"](1,2,3,4);'
|
||||
");"
|
||||
"rel(br)[type=route_master];"
|
||||
"node[railway=subway_entrance](1,2,3,4);"
|
||||
"node[railway=train_station_entrance](1,2,3,4);"
|
||||
"rel[public_transport=stop_area](1,2,3,4);"
|
||||
"rel(br)[type=public_transport]"
|
||||
"[public_transport=stop_area_group];"
|
||||
");"
|
||||
"(._;>>;);"
|
||||
"out body center qt;"
|
||||
),
|
||||
True: (
|
||||
"[out:json][timeout:1000];"
|
||||
"("
|
||||
"("
|
||||
'rel[route="aerialway"](1,2,3,4);'
|
||||
'rel[route="bus"](1,2,3,4);'
|
||||
'rel[route="ferry"](1,2,3,4);'
|
||||
'rel[route="tram"](1,2,3,4);'
|
||||
'rel[route="trolleybus"](1,2,3,4);'
|
||||
");"
|
||||
"rel(br)[type=route_master];"
|
||||
"rel[public_transport=stop_area](1,2,3,4);"
|
||||
"rel(br)[type=public_transport]"
|
||||
"[public_transport=stop_area_group];"
|
||||
");"
|
||||
"(._;>>;);"
|
||||
"out body center qt;"
|
||||
),
|
||||
}
|
||||
|
||||
for overground, expected_answer in expected.items():
|
||||
with self.subTest(msg=f"{overground=}"):
|
||||
self.assertEqual(
|
||||
expected_answer,
|
||||
compose_overpass_request(overground, bboxes),
|
||||
)
|
||||
|
||||
def test__compose_overpass_request__several_bboxes(self) -> None:
|
||||
bboxes = [[1, 2, 3, 4], [5, 6, 7, 8]]
|
||||
|
||||
expected = {
|
||||
False: (
|
||||
"[out:json][timeout:1000];"
|
||||
"("
|
||||
"("
|
||||
'rel[route="light_rail"](1,2,3,4);'
|
||||
'rel[route="monorail"](1,2,3,4);'
|
||||
'rel[route="subway"](1,2,3,4);'
|
||||
'rel[route="train"](1,2,3,4);'
|
||||
");"
|
||||
"rel(br)[type=route_master];"
|
||||
"node[railway=subway_entrance](1,2,3,4);"
|
||||
"node[railway=train_station_entrance](1,2,3,4);"
|
||||
"rel[public_transport=stop_area](1,2,3,4);"
|
||||
"rel(br)[type=public_transport][public_transport=stop_area_group];" # noqa E501
|
||||
"("
|
||||
'rel[route="light_rail"](5,6,7,8);'
|
||||
'rel[route="monorail"](5,6,7,8);'
|
||||
'rel[route="subway"](5,6,7,8);'
|
||||
'rel[route="train"](5,6,7,8);'
|
||||
");"
|
||||
"rel(br)[type=route_master];"
|
||||
"node[railway=subway_entrance](5,6,7,8);"
|
||||
"node[railway=train_station_entrance](5,6,7,8);"
|
||||
"rel[public_transport=stop_area](5,6,7,8);"
|
||||
"rel(br)[type=public_transport][public_transport=stop_area_group];" # noqa E501
|
||||
");"
|
||||
"(._;>>;);"
|
||||
"out body center qt;"
|
||||
),
|
||||
True: (
|
||||
"[out:json][timeout:1000];"
|
||||
"("
|
||||
"("
|
||||
'rel[route="aerialway"](1,2,3,4);'
|
||||
'rel[route="bus"](1,2,3,4);'
|
||||
'rel[route="ferry"](1,2,3,4);'
|
||||
'rel[route="tram"](1,2,3,4);'
|
||||
'rel[route="trolleybus"](1,2,3,4);'
|
||||
");"
|
||||
"rel(br)[type=route_master];"
|
||||
"rel[public_transport=stop_area](1,2,3,4);"
|
||||
"rel(br)[type=public_transport][public_transport=stop_area_group];" # noqa E501
|
||||
"("
|
||||
'rel[route="aerialway"](5,6,7,8);'
|
||||
'rel[route="bus"](5,6,7,8);'
|
||||
'rel[route="ferry"](5,6,7,8);'
|
||||
'rel[route="tram"](5,6,7,8);'
|
||||
'rel[route="trolleybus"](5,6,7,8);'
|
||||
");"
|
||||
"rel(br)[type=route_master];"
|
||||
"rel[public_transport=stop_area](5,6,7,8);"
|
||||
"rel(br)[type=public_transport][public_transport=stop_area_group];" # noqa E501
|
||||
");"
|
||||
"(._;>>;);"
|
||||
"out body center qt;"
|
||||
),
|
||||
}
|
||||
|
||||
for overground, expected_answer in expected.items():
|
||||
with self.subTest(msg=f"{overground=}"):
|
||||
self.assertEqual(
|
||||
expected_answer,
|
||||
compose_overpass_request(overground, bboxes),
|
||||
)
|
||||
|
||||
def test__overpass_request(self) -> None:
|
||||
overpass_api = "http://overpass.example/"
|
||||
overground = False
|
||||
bboxes = [[1, 2, 3, 4]]
|
||||
expected_url = (
|
||||
"http://overpass.example/?data="
|
||||
"%5Bout%3Ajson%5D%5Btimeout%3A1000%5D%3B%28%28"
|
||||
"rel%5Broute%3D%22light_rail%22%5D%281%2C2%2C3%2C4"
|
||||
"%29%3Brel%5Broute%3D%22monorail%22%5D%281%2C2%2C3%2C4%29%3B"
|
||||
"rel%5Broute%3D%22subway%22%5D%281%2C2%2C3%2C4%29%3B"
|
||||
"rel%5Broute%3D%22train%22%5D%281%2C2%2C3%2C4%29%3B%29%3B"
|
||||
"rel%28br%29%5Btype%3Droute_master%5D%3B"
|
||||
"node%5Brailway%3Dsubway_entrance%5D%281%2C2%2C3%2C4%29%3B"
|
||||
"node%5Brailway%3Dtrain_station_entrance%5D%281%2C2%2C3%2C4%29%3B"
|
||||
"rel%5Bpublic_transport%3Dstop_area%5D%281%2C2%2C3%2C4%29%3B"
|
||||
"rel%28br%29%5Btype%3Dpublic_transport%5D%5Bpublic_transport%3D"
|
||||
"stop_area_group%5D%3B%29%3B"
|
||||
"%28._%3B%3E%3E%3B%29%3Bout%20body%20center%20qt%3B"
|
||||
)
|
||||
|
||||
with mock.patch("subways.overpass.json.load") as load_mock:
|
||||
load_mock.return_value = {"elements": []}
|
||||
|
||||
with mock.patch(
|
||||
"subways.overpass.urllib.request.urlopen"
|
||||
) as urlopen_mock:
|
||||
urlopen_mock.return_value.getcode.return_value = 200
|
||||
|
||||
overpass_request(overground, overpass_api, bboxes)
|
||||
|
||||
urlopen_mock.assert_called_once_with(expected_url, timeout=1000)
|
|
@ -1,36 +0,0 @@
|
|||
import inspect
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
from subways.validation import prepare_cities
|
||||
|
||||
|
||||
class TestPrepareCities(TestCase):
|
||||
def test_prepare_cities(self) -> None:
|
||||
csv_path = (
|
||||
Path(inspect.getfile(self.__class__)).parent
|
||||
/ "assets"
|
||||
/ "cities_info_with_bad_values.csv"
|
||||
)
|
||||
|
||||
cities = prepare_cities(cities_info_url=f"file://{csv_path}")
|
||||
|
||||
city_errors = {city.name: sorted(city.errors) for city in cities}
|
||||
|
||||
expected_errors = {
|
||||
"Nizhny Novgorod": [],
|
||||
"Novosibirsk": ["Configuration error: wrong value for id: NBS"],
|
||||
"Saint Petersburg": [],
|
||||
"Samara": [
|
||||
"Configuration error: wrong value for num_stations: 10x"
|
||||
],
|
||||
"Volgograd": [
|
||||
"Configuration error: wrong value for num_light_lines: 2zero",
|
||||
"Configuration error: wrong value for num_lines: zero",
|
||||
],
|
||||
"Yekaterinburg": [
|
||||
"Configuration error: wrong value for num_stations: <empty>"
|
||||
],
|
||||
}
|
||||
|
||||
self.assertDictEqual(city_errors, expected_errors)
|
|
@ -1,166 +0,0 @@
|
|||
import collections
|
||||
import itertools
|
||||
import unittest
|
||||
|
||||
from subways.geom_utils import project_on_segment
|
||||
from subways.types import LonLat
|
||||
|
||||
|
||||
class TestProjection(unittest.TestCase):
|
||||
"""Test subways.geom_utils.project_on_segment function"""
|
||||
|
||||
PRECISION = 10 # decimal places in assertAlmostEqual
|
||||
|
||||
SHIFT = 1e-6 # Small distance between projected point and segment endpoint
|
||||
|
||||
def _test_projection_in_bulk(
|
||||
self,
|
||||
points: list[LonLat],
|
||||
segments: list[tuple[LonLat, LonLat]],
|
||||
answers: list[float | None],
|
||||
) -> None:
|
||||
"""Test 'project_on_segment' function for array of points and array
|
||||
of parallel segments projections on which are equal.
|
||||
"""
|
||||
for point, ans in zip(points, answers):
|
||||
for seg in segments:
|
||||
for segment, answer in zip(
|
||||
(seg, seg[::-1]), # What if invert the segment?
|
||||
(ans, None if ans is None else 1 - ans),
|
||||
):
|
||||
u = project_on_segment(point, segment[0], segment[1])
|
||||
|
||||
if answer is None:
|
||||
self.assertIsNone(
|
||||
u,
|
||||
f"Project of point {point} onto segment {segment} "
|
||||
f"should be None, but {u} returned",
|
||||
)
|
||||
else:
|
||||
self.assertAlmostEqual(
|
||||
u,
|
||||
answer,
|
||||
self.PRECISION,
|
||||
f"Wrong projection of point {point} onto segment "
|
||||
f"{segment}: {u} returned, {answer} expected",
|
||||
)
|
||||
|
||||
def test_projection_on_horizontal_segments(self) -> None:
|
||||
points = [
|
||||
(-2, 0),
|
||||
(-1 - self.SHIFT, 0),
|
||||
(-1, 0),
|
||||
(-1 + self.SHIFT, 0),
|
||||
(-0.5, 0),
|
||||
(0, 0),
|
||||
(0.5, 0),
|
||||
(1 - self.SHIFT, 0),
|
||||
(1, 0),
|
||||
(1 + self.SHIFT, 0),
|
||||
(2, 0),
|
||||
]
|
||||
horizontal_segments = [
|
||||
((-1, -1), (1, -1)),
|
||||
((-1, 0), (1, 0)),
|
||||
((-1, 1), (1, 1)),
|
||||
]
|
||||
answers = [
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
self.SHIFT / 2,
|
||||
0.25,
|
||||
0.5,
|
||||
0.75,
|
||||
1 - self.SHIFT / 2,
|
||||
1,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
self._test_projection_in_bulk(points, horizontal_segments, answers)
|
||||
|
||||
def test_projection_on_vertical_segments(self) -> None:
|
||||
points = [
|
||||
(0, -2),
|
||||
(0, -1 - self.SHIFT),
|
||||
(0, -1),
|
||||
(0, -1 + self.SHIFT),
|
||||
(0, -0.5),
|
||||
(0, 0),
|
||||
(0, 0.5),
|
||||
(0, 1 - self.SHIFT),
|
||||
(0, 1),
|
||||
(0, 1 + self.SHIFT),
|
||||
(0, 2),
|
||||
]
|
||||
vertical_segments = [
|
||||
((-1, -1), (-1, 1)),
|
||||
((0, -1), (0, 1)),
|
||||
((1, -1), (1, 1)),
|
||||
]
|
||||
answers = [
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
self.SHIFT / 2,
|
||||
0.25,
|
||||
0.5,
|
||||
0.75,
|
||||
1 - self.SHIFT / 2,
|
||||
1,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
self._test_projection_in_bulk(points, vertical_segments, answers)
|
||||
|
||||
def test_projection_on_inclined_segment(self) -> None:
|
||||
points = [
|
||||
(-2, -2),
|
||||
(-1, -1),
|
||||
(-0.5, -0.5),
|
||||
(0, 0),
|
||||
(0.5, 0.5),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
]
|
||||
segments = [
|
||||
((-2, 0), (0, 2)),
|
||||
((-1, -1), (1, 1)),
|
||||
((0, -2), (2, 0)),
|
||||
]
|
||||
answers = [None, 0, 0.25, 0.5, 0.75, 1, None]
|
||||
|
||||
self._test_projection_in_bulk(points, segments, answers)
|
||||
|
||||
def test_projection_with_different_collections(self) -> None:
|
||||
"""The tested function should accept points as any consecutive
|
||||
container with index operator.
|
||||
"""
|
||||
types = (
|
||||
tuple,
|
||||
list,
|
||||
collections.deque,
|
||||
)
|
||||
|
||||
point = (0, 0.5)
|
||||
segment_end1 = (0, 0)
|
||||
segment_end2 = (1, 0)
|
||||
|
||||
for p_type, s1_type, s2_type in itertools.product(types, types, types):
|
||||
p = p_type(point)
|
||||
s1 = s1_type(segment_end1)
|
||||
s2 = s2_type(segment_end2)
|
||||
project_on_segment(p, s1, s2)
|
||||
|
||||
def test_projection_on_degenerate_segment(self) -> None:
|
||||
coords = [-1, 0, 1]
|
||||
points = [(x, y) for x, y in itertools.product(coords, coords)]
|
||||
segments = [
|
||||
((0, 0), (0, 0)),
|
||||
((0, 0), (0, 1e-8)),
|
||||
]
|
||||
answers = [None] * len(points)
|
||||
|
||||
self._test_projection_in_bulk(points, segments, answers)
|
|
@ -1,141 +0,0 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from subways.structure.route import (
|
||||
get_interval_in_seconds_from_tags,
|
||||
osm_interval_to_seconds,
|
||||
parse_time_range,
|
||||
)
|
||||
|
||||
|
||||
class TestTimeIntervalsParsing(TestCase):
|
||||
def test__osm_interval_to_seconds__invalid_value(self) -> None:
|
||||
intervals = (
|
||||
["", "abc", "x30", "30x", "3x0"]
|
||||
+ ["5:", ":5", "01:05:", ":01:05", "01:01:00:", ":01:01:00"]
|
||||
+ ["01x:05", "01:x5", "x5:01:00", "01:0x:00", "01:01:x"]
|
||||
+ ["-5", "01:-05", "-01:05", "-01:00:00", "01:-01:00", "01:01:-01"]
|
||||
+ ["0", "00:00", "00:00:00"]
|
||||
+ ["00:60", "01:00:60", "01:60:00"]
|
||||
+ ["01:60:61", "01:61:60", "01:61:61"]
|
||||
)
|
||||
for interval in intervals:
|
||||
with self.subTest(msg=f"value='{interval}'"):
|
||||
self.assertIsNone(osm_interval_to_seconds(interval))
|
||||
|
||||
def test__osm_interval_to_seconds__valid_value(self) -> None:
|
||||
intervals = {
|
||||
"5": 300,
|
||||
"65": 3900,
|
||||
"10:55": 39300,
|
||||
"02:02:02": 7322,
|
||||
"2:2:2": 7322,
|
||||
"00:59": 3540,
|
||||
"01:00": 3600,
|
||||
"00:00:50": 50,
|
||||
"00:10:00": 600,
|
||||
"01:00:00": 3600,
|
||||
}
|
||||
|
||||
for interval_str, interval_sec in intervals.items():
|
||||
with self.subTest(msg=f"value='{interval_str}'"):
|
||||
self.assertEqual(
|
||||
interval_sec, osm_interval_to_seconds(interval_str)
|
||||
)
|
||||
|
||||
def test__parse_time_range__invalid_values(self) -> None:
|
||||
ranges = (
|
||||
["", "a", "ab:cd-ab:cd", "1", "1-2", "01-02"]
|
||||
+ ["24/8", "24/7/365"]
|
||||
+ ["1:00-02:00", "01:0-02:00", "01:00-2:00", "01:00-02:0"]
|
||||
+ ["1x:00-02:00", "01:0x-02:00", "01:00-1x:00", "01:00-02:ab"]
|
||||
+ ["-1:00-02:00", "01:-1-02:00", "01:00--2:00", "01:00-02:-1"]
|
||||
+ ["01;00-02:00", "01:00-02;00", "01:00=02:00"]
|
||||
+ ["01:00-#02:00", "01:00 - 02:00"]
|
||||
+ ["01:60-02:05", "01:00-01:61"]
|
||||
)
|
||||
for r in ranges:
|
||||
with self.subTest(msg=f"value='{r}'"):
|
||||
self.assertIsNone(parse_time_range(r))
|
||||
|
||||
def test__parse_time_range__valid_values(self) -> None:
|
||||
ranges = (
|
||||
["24/7"]
|
||||
+ ["00:00-00:00", "00:01-00:02"]
|
||||
+ ["01:00-02:00", "02:01-01:02"]
|
||||
+ ["02:00-26:59", "12:01-13:59"]
|
||||
+ ["Mo-Fr 06:00-21:30", "06:00-21:30 (weekdays)"]
|
||||
+ ["Mo-Fr 06:00-21:00; Sa-Su 07:00-20:00"]
|
||||
)
|
||||
answers = [
|
||||
((0, 0), (24, 0)),
|
||||
((0, 0), (0, 0)),
|
||||
((0, 1), (0, 2)),
|
||||
((1, 0), (2, 0)),
|
||||
((2, 1), (1, 2)),
|
||||
((2, 0), (26, 59)),
|
||||
((12, 1), (13, 59)),
|
||||
((6, 0), (21, 30)),
|
||||
((6, 0), (21, 30)),
|
||||
((6, 0), (21, 0)),
|
||||
]
|
||||
|
||||
for r, answer in zip(ranges, answers):
|
||||
with self.subTest(msg=f"value='{r}'"):
|
||||
self.assertTupleEqual(answer, parse_time_range(r))
|
||||
|
||||
|
||||
class TestRouteIntervals(TestCase):
|
||||
def test__get_interval_in_seconds_from_tags__one_key(self) -> None:
|
||||
cases = [
|
||||
{"tags": {}, "answer": None},
|
||||
{"tags": {"a": "1"}, "answer": None},
|
||||
{"tags": {"duration": "1"}, "answer": 60},
|
||||
{"tags": {"durationxxx"}, "answer": None},
|
||||
{"tags": {"xxxduration"}, "answer": None},
|
||||
# prefixes not considered
|
||||
{"tags": {"ru:duration"}, "answer": None},
|
||||
# suffixes considered
|
||||
{"tags": {"duration:peak": "1"}, "answer": 60},
|
||||
# bare tag has precedence over suffixed version
|
||||
{"tags": {"duration:peak": "1", "duration": "2"}, "answer": 120},
|
||||
# first suffixed version apply
|
||||
{"tags": {"duration:y": "1", "duration:x": "2"}, "answer": 60},
|
||||
# other tags present
|
||||
{"tags": {"a": "x", "duration": "1", "b": "y"}, "answer": 60},
|
||||
]
|
||||
|
||||
for case in cases:
|
||||
with self.subTest(msg=f"{case['tags']}"):
|
||||
self.assertEqual(
|
||||
case["answer"],
|
||||
get_interval_in_seconds_from_tags(
|
||||
case["tags"], "duration"
|
||||
),
|
||||
)
|
||||
|
||||
def test__get_interval_in_seconds_from_tags__several_keys(self) -> None:
|
||||
keys = ("interval", "headway")
|
||||
cases = [
|
||||
{"tags": {}, "answer": None},
|
||||
# prefixes not considered
|
||||
{"tags": {"ru:interval"}, "answer": None},
|
||||
{"tags": {"interval": "1"}, "answer": 60},
|
||||
{"tags": {"headway": "1"}, "answer": 60},
|
||||
{"tags": {"interval": "1", "headway": "2"}, "answer": 60},
|
||||
# interval has precedence due to its position in 'keys'
|
||||
{"tags": {"headway": "2", "interval": "1"}, "answer": 60},
|
||||
# non-suffixed keys has precedence
|
||||
{"tags": {"interval:peak": "1", "headway": "2"}, "answer": 120},
|
||||
# among suffixed versions, first key in 'keys' is used first
|
||||
{
|
||||
"tags": {"headway:peak": "2", "interval:peak": "1"},
|
||||
"answer": 60,
|
||||
},
|
||||
]
|
||||
|
||||
for case in cases:
|
||||
with self.subTest(msg=f"{case['tags']}"):
|
||||
self.assertEqual(
|
||||
case["answer"],
|
||||
get_interval_in_seconds_from_tags(case["tags"], keys),
|
||||
)
|
|
@ -1,114 +0,0 @@
|
|||
from subways.structure.route_master import RouteMaster
|
||||
from subways.tests.sample_data_for_twin_routes import metro_samples
|
||||
from subways.tests.util import TestCase
|
||||
|
||||
|
||||
class TestRouteMaster(TestCase):
|
||||
def test__find_common_circular_subsequence(self) -> None:
|
||||
cases = [
|
||||
{ # the 1st sequence is empty
|
||||
"sequence1": [],
|
||||
"sequence2": [1, 2, 3, 4],
|
||||
"answer": [],
|
||||
},
|
||||
{ # the 2nd sequence is empty
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [],
|
||||
"answer": [],
|
||||
},
|
||||
{ # equal sequences
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [1, 2, 3, 4],
|
||||
"answer": [1, 2, 3, 4],
|
||||
},
|
||||
{ # one sequence is a cyclic shift of the other
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [4, 1, 2, 3],
|
||||
"answer": [1, 2, 3, 4],
|
||||
},
|
||||
{ # the 2nd sequence is a subsequence of the 1st; equal ends
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [1, 2, 4],
|
||||
"answer": [1, 2, 4],
|
||||
},
|
||||
{ # the 1st sequence is a subsequence of the 2nd; equal ends
|
||||
"sequence1": [1, 2, 4],
|
||||
"sequence2": [1, 2, 3, 4],
|
||||
"answer": [1, 2, 4],
|
||||
},
|
||||
{ # the 2nd sequence is an innter subsequence of the 1st
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [2, 3],
|
||||
"answer": [2, 3],
|
||||
},
|
||||
{ # the 1st sequence is an inner subsequence of the 2nd
|
||||
"sequence1": [2, 3],
|
||||
"sequence2": [1, 2, 3, 4],
|
||||
"answer": [2, 3],
|
||||
},
|
||||
{ # the 2nd sequence is a continuation of the 1st
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [4, 5, 6],
|
||||
"answer": [4],
|
||||
},
|
||||
{ # the 1st sequence is a continuation of the 2nd
|
||||
"sequence1": [4, 5, 6],
|
||||
"sequence2": [1, 2, 3, 4],
|
||||
"answer": [4],
|
||||
},
|
||||
{ # no common elements
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [5, 6, 7],
|
||||
"answer": [],
|
||||
},
|
||||
{ # one sequence is the reversed other
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [4, 3, 2, 1],
|
||||
"answer": [1, 2],
|
||||
},
|
||||
{ # the 2nd is a subsequence of shifted 1st
|
||||
"sequence1": [1, 2, 3, 4],
|
||||
"sequence2": [2, 4, 1],
|
||||
"answer": [1, 2, 4],
|
||||
},
|
||||
{ # the 1st is a subsequence of shifted 2nd
|
||||
"sequence1": [2, 4, 1],
|
||||
"sequence2": [1, 2, 3, 4],
|
||||
"answer": [2, 4, 1],
|
||||
},
|
||||
{ # mixed case: few common elements
|
||||
"sequence1": [1, 2, 4],
|
||||
"sequence2": [2, 3, 4],
|
||||
"answer": [2, 4],
|
||||
},
|
||||
]
|
||||
|
||||
for i, case in enumerate(cases):
|
||||
with self.subTest(f"case#{i}"):
|
||||
self.assertListEqual(
|
||||
case["answer"],
|
||||
RouteMaster.find_common_circular_subsequence(
|
||||
case["sequence1"], case["sequence2"]
|
||||
),
|
||||
)
|
||||
|
||||
def _test_find_twin_routes_for_network(self, metro_sample: dict) -> None:
|
||||
cities, transfers = self.prepare_cities(metro_sample)
|
||||
city = cities[0]
|
||||
|
||||
self.assertTrue(city.is_good)
|
||||
|
||||
for route_master_id, expected_twin_ids in metro_sample[
|
||||
"twin_routes"
|
||||
].items():
|
||||
route_master = city.routes[route_master_id]
|
||||
calculated_twins = route_master.find_twin_routes()
|
||||
calculated_twin_ids = {
|
||||
r1.id: r2.id for r1, r2 in calculated_twins.items()
|
||||
}
|
||||
self.assertDictEqual(expected_twin_ids, calculated_twin_ids)
|
||||
|
||||
def test_find_twin_routes(self) -> None:
|
||||
for sample in metro_samples:
|
||||
with self.subTest(msg=sample["name"]):
|
||||
self._test_find_twin_routes_for_network(sample)
|
|
@ -1,46 +0,0 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from subways.structure.station import Station
|
||||
|
||||
|
||||
class TestStation(TestCase):
|
||||
def test__get_modes(self) -> None:
|
||||
cases = [
|
||||
{"element": {"tags": {"railway": "station"}}, "modes": set()},
|
||||
{
|
||||
"element": {
|
||||
"tags": {"railway": "station", "station": "train"}
|
||||
},
|
||||
"modes": {"train"},
|
||||
},
|
||||
{
|
||||
"element": {"tags": {"railway": "station", "train": "yes"}},
|
||||
"modes": {"train"},
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"tags": {
|
||||
"railway": "station",
|
||||
"station": "subway",
|
||||
"train": "yes",
|
||||
}
|
||||
},
|
||||
"modes": {"subway", "train"},
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"tags": {
|
||||
"railway": "station",
|
||||
"subway": "yes",
|
||||
"train": "yes",
|
||||
"light_rail": "yes",
|
||||
"monorail": "yes",
|
||||
}
|
||||
},
|
||||
"modes": {"subway", "train", "light_rail", "monorail"},
|
||||
},
|
||||
]
|
||||
for case in cases:
|
||||
element = case["element"]
|
||||
expected_modes = case["modes"]
|
||||
self.assertSetEqual(expected_modes, Station.get_modes(element))
|
|
@ -1,42 +0,0 @@
|
|||
import json
|
||||
from operator import itemgetter
|
||||
|
||||
from subways.processors._common import transit_to_dict
|
||||
from subways.tests.sample_data_for_outputs import metro_samples
|
||||
from subways.tests.util import JsonLikeComparisonMixin, TestCase
|
||||
|
||||
|
||||
class TestStorage(JsonLikeComparisonMixin, TestCase):
|
||||
def test_storage(self) -> None:
|
||||
for sample in metro_samples:
|
||||
with self.subTest(msg=sample["name"]):
|
||||
self._test_storage_for_sample(sample)
|
||||
|
||||
def _test_storage_for_sample(self, metro_sample: dict) -> None:
|
||||
cities, transfers = self.prepare_cities(metro_sample)
|
||||
|
||||
calculated_transit_data = transit_to_dict(cities, transfers)
|
||||
|
||||
control_transit_data = json.loads(metro_sample["json_dump"])
|
||||
control_transit_data["transfers"] = set(
|
||||
map(tuple, control_transit_data["transfers"])
|
||||
)
|
||||
|
||||
self._compare_transit_data(
|
||||
calculated_transit_data, control_transit_data
|
||||
)
|
||||
|
||||
def _compare_transit_data(
|
||||
self, transit_data1: dict, transit_data2: dict
|
||||
) -> None:
|
||||
id_cmp = itemgetter("id")
|
||||
|
||||
self.assertMappingAlmostEqual(
|
||||
transit_data1,
|
||||
transit_data2,
|
||||
unordered_lists={
|
||||
"routes": id_cmp,
|
||||
"itineraries": id_cmp,
|
||||
"entrances": id_cmp,
|
||||
},
|
||||
)
|
|
@ -1,309 +0,0 @@
|
|||
import io
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeAlias, Self
|
||||
from unittest import TestCase as unittestTestCase
|
||||
|
||||
from subways.structure.city import City, find_transfers
|
||||
from subways.subway_io import load_xml
|
||||
from subways.validation import (
|
||||
add_osm_elements_to_cities,
|
||||
validate_cities,
|
||||
calculate_centers,
|
||||
)
|
||||
|
||||
TestCaseMixin: TypeAlias = Self | unittestTestCase
|
||||
|
||||
|
||||
class TestCase(unittestTestCase):
|
||||
"""TestCase class for testing the Subway Validator"""
|
||||
|
||||
CITY_TEMPLATE = {
|
||||
"name": "Null Island",
|
||||
"country": "World",
|
||||
"continent": "Africa",
|
||||
"bbox": "-179, -89, 179, 89",
|
||||
"networks": "",
|
||||
"num_stations": None,
|
||||
"num_lines": 1,
|
||||
"num_light_lines": 0,
|
||||
"num_interchanges": 0,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.city_class = City
|
||||
|
||||
def prepare_cities(self, metro_sample: dict) -> tuple:
|
||||
"""Load cities from file/string, validate them and return cities
|
||||
and transfers.
|
||||
"""
|
||||
|
||||
def assign_unique_id(city_info: dict, cities_info: list[dict]) -> None:
|
||||
"""city_info - newly added city, cities_info - already added
|
||||
cities. Check city id uniqueness / assign unique id to the city.
|
||||
"""
|
||||
occupied_ids = set(c["id"] for c in cities_info)
|
||||
if "id" in city_info:
|
||||
if city_info["id"] in occupied_ids:
|
||||
raise RuntimeError("Not unique city ids in test data")
|
||||
else:
|
||||
city_info["id"] = max(occupied_ids, default=1) + 1
|
||||
|
||||
cities_given_info = metro_sample["cities_info"]
|
||||
cities_info = list()
|
||||
for city_given_info in cities_given_info:
|
||||
city_info = self.CITY_TEMPLATE.copy()
|
||||
for attr in city_given_info.keys():
|
||||
city_info[attr] = city_given_info[attr]
|
||||
assign_unique_id(city_info, cities_info)
|
||||
cities_info.append(city_info)
|
||||
|
||||
if len(set(ci["name"] for ci in cities_info)) < len(cities_info):
|
||||
raise RuntimeError("Not unique city names in test data")
|
||||
|
||||
cities = list(map(self.city_class, cities_info))
|
||||
if "xml" in metro_sample:
|
||||
xml_file = io.BytesIO(metro_sample["xml"].encode())
|
||||
else:
|
||||
xml_file = (
|
||||
Path(__file__).resolve().parent / metro_sample["xml_file"]
|
||||
)
|
||||
elements = load_xml(xml_file)
|
||||
calculate_centers(elements)
|
||||
add_osm_elements_to_cities(elements, cities)
|
||||
validate_cities(cities)
|
||||
transfers = find_transfers(elements, cities)
|
||||
return cities, transfers
|
||||
|
||||
|
||||
class JsonLikeComparisonMixin:
|
||||
"""Contains auxiliary methods for the TestCase class that allow
|
||||
to compare json-like structures where some lists do not imply order
|
||||
and actually represent sets.
|
||||
Also, all collections compare floats with given precision to any nesting
|
||||
depth.
|
||||
"""
|
||||
|
||||
def _assertAnyAlmostEqual(
|
||||
self: TestCaseMixin,
|
||||
first: Any,
|
||||
second: Any,
|
||||
places: int = 10,
|
||||
*,
|
||||
unordered_lists: dict[str, Callable] | None = None,
|
||||
ignore_keys: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Dispatcher method to other "...AlmostEqual" methods
|
||||
depending on argument types.
|
||||
|
||||
Compare dictionaries/lists recursively, numeric values being compared
|
||||
approximately.
|
||||
|
||||
:param: first a value of arbitrary type, including collections
|
||||
:param: second a value of arbitrary type, including collections
|
||||
:param: places number of fractional digits. Is passed to
|
||||
the self.assertAlmostEqual() method.
|
||||
:param: unordered_lists a dict whose keys are names of lists
|
||||
to be compared without order, values - comparators for
|
||||
the lists to sort them in an unambiguous order. If a comparator
|
||||
is None, then the lists are compared as sets.
|
||||
:param: ignore_keys a set of strs with keys that should be ignored
|
||||
during recursive comparison of dictionaries. May be used to
|
||||
elaborate a custom comparison mechanism for some substructures.
|
||||
:return: None
|
||||
"""
|
||||
if all(isinstance(x, Mapping) for x in (first, second)):
|
||||
self.assertMappingAlmostEqual(
|
||||
first,
|
||||
second,
|
||||
places,
|
||||
unordered_lists=unordered_lists,
|
||||
ignore_keys=ignore_keys,
|
||||
)
|
||||
elif all(
|
||||
isinstance(x, Sequence) and not isinstance(x, (str, bytes))
|
||||
for x in (first, second)
|
||||
):
|
||||
self.assertSequenceAlmostEqual(
|
||||
first,
|
||||
second,
|
||||
places,
|
||||
unordered_lists=unordered_lists,
|
||||
ignore_keys=ignore_keys,
|
||||
)
|
||||
elif isinstance(first, float) and isinstance(second, float):
|
||||
self.assertAlmostEqual(first, second, places)
|
||||
else:
|
||||
self.assertEqual(first, second)
|
||||
|
||||
def assertSequenceAlmostEqual(
|
||||
self: TestCaseMixin,
|
||||
seq1: Sequence,
|
||||
seq2: Sequence,
|
||||
places: int = 10,
|
||||
*,
|
||||
unordered_lists: dict[str, Callable] | None = None,
|
||||
ignore_keys: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Compare two sequences, items of numeric types being compared
|
||||
approximately, containers being approx-compared recursively.
|
||||
|
||||
:param: places see _assertAnyAlmostEqual() method
|
||||
:param: unordered_lists see _assertAnyAlmostEqual() method
|
||||
:param: ignore_keys see _assertAnyAlmostEqual() method
|
||||
:return: None
|
||||
"""
|
||||
if not (isinstance(seq1, Sequence) and isinstance(seq2, Sequence)):
|
||||
raise RuntimeError(
|
||||
f"Not a sequence passed to the '{self.__class__.__name__}."
|
||||
"assertSequenceAlmostEqual' method"
|
||||
)
|
||||
self.assertEqual(len(seq1), len(seq2))
|
||||
for a, b in zip(seq1, seq2):
|
||||
self._assertAnyAlmostEqual(
|
||||
a,
|
||||
b,
|
||||
places,
|
||||
unordered_lists=unordered_lists,
|
||||
ignore_keys=ignore_keys,
|
||||
)
|
||||
|
||||
def assertSequenceAlmostEqualIgnoreOrder(
|
||||
self: TestCaseMixin,
|
||||
seq1: Sequence,
|
||||
seq2: Sequence,
|
||||
cmp: Callable | None = None,
|
||||
places: int = 10,
|
||||
*,
|
||||
unordered_lists: dict[str, Callable] | None = None,
|
||||
ignore_keys: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Compares two sequences as sets, i.e. ignoring order. Nested
|
||||
lists determined with unordered_lists parameter are also compared
|
||||
without order.
|
||||
|
||||
:param: cmp if None then compare sequences as sets. If elements are
|
||||
not hashable then this method is inapplicable and the
|
||||
sorted (with the comparator) sequences are compared.
|
||||
:param: places see _assertAnyAlmostEqual() method
|
||||
:param: unordered_lists see _assertAnyAlmostEqual() method
|
||||
:param: ignore_keys see _assertAnyAlmostEqual() method
|
||||
:return: None
|
||||
"""
|
||||
if cmp is not None:
|
||||
v1 = sorted(seq1, key=cmp)
|
||||
v2 = sorted(seq2, key=cmp)
|
||||
self.assertSequenceAlmostEqual(
|
||||
v1,
|
||||
v2,
|
||||
places,
|
||||
unordered_lists=unordered_lists,
|
||||
ignore_keys=ignore_keys,
|
||||
)
|
||||
else:
|
||||
self.assertEqual(len(seq1), len(seq2))
|
||||
v1 = set(seq1)
|
||||
v2 = set(seq2)
|
||||
self.assertSetEqual(v1, v2)
|
||||
|
||||
def assertMappingAlmostEqual(
|
||||
self: TestCaseMixin,
|
||||
d1: Mapping,
|
||||
d2: Mapping,
|
||||
places: int = 10,
|
||||
*,
|
||||
unordered_lists: dict[str, Callable] | None = None,
|
||||
ignore_keys: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Compare dictionaries recursively, numeric values being compared
|
||||
approximately, some lists being compared without order.
|
||||
|
||||
:param: places see _assertAnyAlmostEqual() method
|
||||
:param: unordered_lists see _assertAnyAlmostEqual() method
|
||||
Example 1:
|
||||
d1 = {
|
||||
"name_from_unordered_list": [a1, b1, c1],
|
||||
"some_other_name": [e1, f1, g1],
|
||||
}
|
||||
d2 = {
|
||||
"name_from_unordered_list": [a2, b2, c2],
|
||||
"some_other_name": [e2, f2, g2],
|
||||
}
|
||||
Lists [a1, b1, c1] and [a2, b2, c2] will be compared
|
||||
without order, lists [e1, f1, g1] and [e2, f2, g2] -
|
||||
considering the order.
|
||||
|
||||
Example 2:
|
||||
d1 = {
|
||||
"name_from_unordered_list": {
|
||||
"key1": [a1, b1, c1],
|
||||
"key2": [e1, f1, g1],
|
||||
},
|
||||
"some_other_name": [h1, i1, k1],
|
||||
}
|
||||
d2 = {
|
||||
"name_from_unordered_list": {
|
||||
"key1": [a2, b2, c2],
|
||||
"key2": [e2, f2, g2],
|
||||
},
|
||||
"some_other_name": [h2, i2, k2],
|
||||
}
|
||||
Lists [a1, b1, c1] and [a2, b2, c2] will be compared
|
||||
without order, as well as [e1, f1, g1] and
|
||||
[e2, f2, g2]; lists [h1, i1, k1] and [h2, i2, k2] -
|
||||
considering the order.
|
||||
:param: ignore_keys see _assertAnyAlmostEqual() method
|
||||
:return: None
|
||||
"""
|
||||
if not (isinstance(d1, Mapping) and isinstance(d2, Mapping)):
|
||||
raise RuntimeError(
|
||||
f"Not a dictionary passed to the '{self.__class__.__name__}."
|
||||
"assertMappingAlmostEqual' method"
|
||||
)
|
||||
|
||||
d1_keys = set(d1.keys())
|
||||
d2_keys = set(d2.keys())
|
||||
if ignore_keys:
|
||||
d1_keys -= ignore_keys
|
||||
d2_keys -= ignore_keys
|
||||
self.assertSetEqual(d1_keys, d2_keys)
|
||||
|
||||
if unordered_lists is None:
|
||||
unordered_lists = {}
|
||||
|
||||
for k in d1_keys:
|
||||
v1 = d1[k]
|
||||
v2 = d2[k]
|
||||
if (cmp := unordered_lists.get(k, "")) == "" or not isinstance(
|
||||
v1, (Sequence, Mapping)
|
||||
):
|
||||
self._assertAnyAlmostEqual(
|
||||
v1,
|
||||
v2,
|
||||
places,
|
||||
unordered_lists=unordered_lists,
|
||||
ignore_keys=ignore_keys,
|
||||
)
|
||||
elif isinstance(v1, Sequence):
|
||||
self.assertSequenceAlmostEqualIgnoreOrder(
|
||||
v1,
|
||||
v2,
|
||||
cmp,
|
||||
places,
|
||||
unordered_lists=unordered_lists,
|
||||
ignore_keys=ignore_keys,
|
||||
)
|
||||
else:
|
||||
self.assertSetEqual(set(v1.keys()), set(v2.keys()))
|
||||
for ik in v1.keys():
|
||||
iv1 = v1[ik]
|
||||
iv2 = v2[ik]
|
||||
self.assertSequenceAlmostEqualIgnoreOrder(
|
||||
iv1,
|
||||
iv2,
|
||||
cmp,
|
||||
places,
|
||||
unordered_lists=unordered_lists,
|
||||
ignore_keys=ignore_keys,
|
||||
)
|
|
@ -1,14 +0,0 @@
|
|||
from typing import TypeAlias
|
||||
|
||||
|
||||
OsmElementT: TypeAlias = dict
|
||||
IdT: TypeAlias = str # Type of feature ids
|
||||
TransferT: TypeAlias = set[IdT] # A transfer is a set of StopArea IDs
|
||||
TransfersT: TypeAlias = list[TransferT]
|
||||
LonLat: TypeAlias = tuple[float, float]
|
||||
RailT: TypeAlias = list[LonLat]
|
||||
|
||||
|
||||
class CriticalValidationError(Exception):
|
||||
"""Is thrown if an error occurs
|
||||
that prevents further validation of a city."""
|
|
@ -1,253 +0,0 @@
|
|||
import csv
|
||||
import logging
|
||||
import urllib.request
|
||||
from functools import partial
|
||||
|
||||
from subways.structure.city import City
|
||||
from subways.types import CriticalValidationError, LonLat, OsmElementT
|
||||
|
||||
DEFAULT_SPREADSHEET_ID = "1SEW1-NiNOnA2qDwievcxYV1FOaQl1mb1fdeyqAxHu3k"
|
||||
DEFAULT_CITIES_INFO_URL = (
|
||||
"https://docs.google.com/spreadsheets/d/"
|
||||
f"{DEFAULT_SPREADSHEET_ID}/export?format=csv"
|
||||
)
|
||||
BAD_MARK = "[bad]"
|
||||
|
||||
|
||||
def get_way_center(
|
||||
element: OsmElementT, node_centers: dict[int, LonLat]
|
||||
) -> LonLat | None:
|
||||
"""
|
||||
:param element: dict describing OSM element
|
||||
:param node_centers: osm_id => LonLat
|
||||
:return: tuple with center coordinates, or None
|
||||
"""
|
||||
|
||||
# If elements have been queried via overpass-api with
|
||||
# 'out center;' clause then ways already have 'center' attribute
|
||||
if "center" in element:
|
||||
return element["center"]["lon"], element["center"]["lat"]
|
||||
|
||||
if "nodes" not in element:
|
||||
return None
|
||||
|
||||
center = [0, 0]
|
||||
count = 0
|
||||
way_nodes = element["nodes"]
|
||||
way_nodes_len = len(element["nodes"])
|
||||
for i, nd in enumerate(way_nodes):
|
||||
if nd not in node_centers:
|
||||
continue
|
||||
# Don't count the first node of a closed way twice
|
||||
if (
|
||||
i == way_nodes_len - 1
|
||||
and way_nodes_len > 1
|
||||
and way_nodes[0] == way_nodes[-1]
|
||||
):
|
||||
break
|
||||
center[0] += node_centers[nd][0]
|
||||
center[1] += node_centers[nd][1]
|
||||
count += 1
|
||||
if count == 0:
|
||||
return None
|
||||
element["center"] = {"lat": center[1] / count, "lon": center[0] / count}
|
||||
return element["center"]["lon"], element["center"]["lat"]
|
||||
|
||||
|
||||
def get_relation_center(
|
||||
element: OsmElementT,
|
||||
node_centers: dict[int, LonLat],
|
||||
way_centers: dict[int, LonLat],
|
||||
relation_centers: dict[int, LonLat],
|
||||
ignore_unlocalized_child_relations: bool = False,
|
||||
) -> LonLat | None:
|
||||
"""
|
||||
:param element: dict describing OSM element
|
||||
:param node_centers: osm_id => LonLat
|
||||
:param way_centers: osm_id => LonLat
|
||||
:param relation_centers: osm_id => LonLat
|
||||
:param ignore_unlocalized_child_relations: if a member that is a relation
|
||||
has no center, skip it and calculate center based on member nodes,
|
||||
ways and other, "localized" (with known centers), relations
|
||||
:return: tuple with center coordinates, or None
|
||||
"""
|
||||
|
||||
# If elements have been queried via overpass-api with
|
||||
# 'out center;' clause then some relations already have 'center'
|
||||
# attribute. But this is not the case for relations composed only
|
||||
# of other relations (e.g., route_master, stop_area_group or
|
||||
# stop_area with only members that are multipolygons)
|
||||
if "center" in element:
|
||||
return element["center"]["lon"], element["center"]["lat"]
|
||||
|
||||
center = [0, 0]
|
||||
count = 0
|
||||
for m in element.get("members", list()):
|
||||
m_id = m["ref"]
|
||||
m_type = m["type"]
|
||||
if m_type == "relation" and m_id not in relation_centers:
|
||||
if ignore_unlocalized_child_relations:
|
||||
continue
|
||||
else:
|
||||
# Cannot calculate fair center because the center
|
||||
# of a child relation is not known yet
|
||||
return None
|
||||
member_container = (
|
||||
node_centers
|
||||
if m_type == "node"
|
||||
else way_centers
|
||||
if m_type == "way"
|
||||
else relation_centers
|
||||
)
|
||||
if m_id in member_container:
|
||||
center[0] += member_container[m_id][0]
|
||||
center[1] += member_container[m_id][1]
|
||||
count += 1
|
||||
if count == 0:
|
||||
return None
|
||||
element["center"] = {"lat": center[1] / count, "lon": center[0] / count}
|
||||
return element["center"]["lon"], element["center"]["lat"]
|
||||
|
||||
|
||||
def calculate_centers(elements: list[OsmElementT]) -> None:
|
||||
"""Adds 'center' key to each way/relation in elements,
|
||||
except for empty ways or relations.
|
||||
Relies on nodes-ways-relations order in the elements list.
|
||||
"""
|
||||
nodes: dict[int, LonLat] = {} # id => LonLat
|
||||
ways: dict[int, LonLat] = {} # id => approx center LonLat
|
||||
relations: dict[int, LonLat] = {} # id => approx center LonLat
|
||||
|
||||
unlocalized_relations: list[OsmElementT] = [] # 'unlocalized' means
|
||||
# the center of the relation has not been calculated yet
|
||||
|
||||
for el in elements:
|
||||
if el["type"] == "node":
|
||||
nodes[el["id"]] = (el["lon"], el["lat"])
|
||||
elif el["type"] == "way":
|
||||
if center := get_way_center(el, nodes):
|
||||
ways[el["id"]] = center
|
||||
elif el["type"] == "relation":
|
||||
if center := get_relation_center(el, nodes, ways, relations):
|
||||
relations[el["id"]] = center
|
||||
else:
|
||||
unlocalized_relations.append(el)
|
||||
|
||||
def iterate_relation_centers_calculation(
|
||||
ignore_unlocalized_child_relations: bool,
|
||||
) -> list[OsmElementT]:
|
||||
unlocalized_relations_upd = []
|
||||
for rel in unlocalized_relations:
|
||||
if center := get_relation_center(
|
||||
rel, nodes, ways, relations, ignore_unlocalized_child_relations
|
||||
):
|
||||
relations[rel["id"]] = center
|
||||
else:
|
||||
unlocalized_relations_upd.append(rel)
|
||||
return unlocalized_relations_upd
|
||||
|
||||
# Calculate centers for relations that have no one yet
|
||||
while unlocalized_relations:
|
||||
unlocalized_relations_upd = iterate_relation_centers_calculation(False)
|
||||
progress = len(unlocalized_relations_upd) < len(unlocalized_relations)
|
||||
if not progress:
|
||||
unlocalized_relations_upd = iterate_relation_centers_calculation(
|
||||
True
|
||||
)
|
||||
progress = len(unlocalized_relations_upd) < len(
|
||||
unlocalized_relations
|
||||
)
|
||||
if not progress:
|
||||
break
|
||||
unlocalized_relations = unlocalized_relations_upd
|
||||
|
||||
|
||||
def add_osm_elements_to_cities(
|
||||
osm_elements: list[OsmElementT], cities: list[City]
|
||||
) -> None:
|
||||
for el in osm_elements:
|
||||
for c in cities:
|
||||
if c.contains(el):
|
||||
c.add(el)
|
||||
|
||||
|
||||
def validate_cities(cities: list[City]) -> list[City]:
|
||||
"""Validate cities. Return list of good cities."""
|
||||
good_cities = []
|
||||
for c in cities:
|
||||
try:
|
||||
c.extract_routes()
|
||||
except CriticalValidationError as e:
|
||||
logging.error(
|
||||
"Critical validation error while processing %s: %s",
|
||||
c.name,
|
||||
e,
|
||||
)
|
||||
c.error(str(e))
|
||||
except AssertionError as e:
|
||||
logging.error(
|
||||
"Validation logic error while processing %s: %s",
|
||||
c.name,
|
||||
e,
|
||||
)
|
||||
c.error(f"Validation logic error: {e}")
|
||||
else:
|
||||
c.validate()
|
||||
if c.is_good:
|
||||
c.calculate_distances()
|
||||
good_cities.append(c)
|
||||
|
||||
return good_cities
|
||||
|
||||
|
||||
def get_cities_info(
|
||||
cities_info_url: str = DEFAULT_CITIES_INFO_URL,
|
||||
) -> list[dict]:
|
||||
response = urllib.request.urlopen(cities_info_url)
|
||||
if (
|
||||
not cities_info_url.startswith("file://")
|
||||
and (r_code := response.getcode()) != 200
|
||||
):
|
||||
raise Exception(
|
||||
f"Failed to download cities spreadsheet: HTTP {r_code}"
|
||||
)
|
||||
data = response.read().decode("utf-8")
|
||||
reader = csv.DictReader(
|
||||
data.splitlines(),
|
||||
fieldnames=(
|
||||
"id",
|
||||
"name",
|
||||
"country",
|
||||
"continent",
|
||||
"num_stations",
|
||||
"num_lines",
|
||||
"num_light_lines",
|
||||
"num_interchanges",
|
||||
"bbox",
|
||||
"networks",
|
||||
),
|
||||
)
|
||||
|
||||
cities_info = list()
|
||||
names = set()
|
||||
next(reader) # skipping the header
|
||||
for city_info in reader:
|
||||
if city_info["id"] and city_info["bbox"]:
|
||||
cities_info.append(city_info)
|
||||
name = city_info["name"].strip()
|
||||
if name in names:
|
||||
logging.warning(
|
||||
"Duplicate city name in city list: %s",
|
||||
city_info,
|
||||
)
|
||||
names.add(name)
|
||||
return cities_info
|
||||
|
||||
|
||||
def prepare_cities(
|
||||
cities_info_url: str = DEFAULT_CITIES_INFO_URL, overground: bool = False
|
||||
) -> list[City]:
|
||||
if overground:
|
||||
raise NotImplementedError("Overground transit not implemented yet")
|
||||
cities_info = get_cities_info(cities_info_url)
|
||||
return list(map(partial(City, overground=overground), cities_info))
|
|
@ -1,159 +0,0 @@
|
|||
import functools
|
||||
import logging
|
||||
import math
|
||||
|
||||
|
||||
"""A coordinate of a station precision of which we must take into account
|
||||
is calculated as an average of somewhat 10 elements.
|
||||
Taking machine epsilon 1e-15, averaging 10 numbers with close magnitudes
|
||||
ensures relative precision of 1e-14."""
|
||||
coord_isclose = functools.partial(math.isclose, rel_tol=1e-14)
|
||||
|
||||
|
||||
def coords_eq(lon1, lat1, lon2, lat2):
|
||||
return coord_isclose(lon1, lon2) and coord_isclose(lat1, lat2)
|
||||
|
||||
|
||||
def osm_id_comparator(el):
|
||||
"""This function is used as key for sorting lists of
|
||||
OSM-originated objects
|
||||
"""
|
||||
return (el["osm_type"], el["osm_id"])
|
||||
|
||||
|
||||
def itinerary_comparator(itinerary):
|
||||
"""This function is used as key for sorting itineraries in a route"""
|
||||
return (itinerary["stops"], itinerary["interval"])
|
||||
|
||||
|
||||
def compare_stops(stop0, stop1):
|
||||
"""Compares json of two stops in route"""
|
||||
stop_keys = ("name", "int_name", "id", "osm_id", "osm_type")
|
||||
stop0_props = tuple(stop0[k] for k in stop_keys)
|
||||
stop1_props = tuple(stop1[k] for k in stop_keys)
|
||||
|
||||
if stop0_props != stop1_props:
|
||||
logging.debug(
|
||||
"Different stops properties: %s, %s", stop0_props, stop1_props
|
||||
)
|
||||
return False
|
||||
|
||||
if not coords_eq(stop0["lon"], stop0["lat"], stop1["lon"], stop1["lat"]):
|
||||
logging.debug(
|
||||
"Different stops coordinates: %s (%f, %f), %s (%f, %f)",
|
||||
stop0_props,
|
||||
stop0["lon"],
|
||||
stop0["lat"],
|
||||
stop1_props,
|
||||
stop1["lon"],
|
||||
stop1["lat"],
|
||||
)
|
||||
return False
|
||||
|
||||
entrances0 = sorted(stop0["entrances"], key=osm_id_comparator)
|
||||
entrances1 = sorted(stop1["entrances"], key=osm_id_comparator)
|
||||
if entrances0 != entrances1:
|
||||
logging.debug("Different stop entrances")
|
||||
return False
|
||||
|
||||
exits0 = sorted(stop0["exits"], key=osm_id_comparator)
|
||||
exits1 = sorted(stop1["exits"], key=osm_id_comparator)
|
||||
if exits0 != exits1:
|
||||
logging.debug("Different stop exits")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def compare_transfers(transfers0, transfers1):
|
||||
"""Compares two arrays of transfers of the form
|
||||
[(stop1_uid, stop2_uid, time), ...]
|
||||
"""
|
||||
if len(transfers0) != len(transfers1):
|
||||
logging.debug(
|
||||
"Different len(transfers): %d != %d",
|
||||
len(transfers0),
|
||||
len(transfers1),
|
||||
)
|
||||
return False
|
||||
|
||||
transfers0 = [
|
||||
tuple([t[0], t[1], t[2]]) if t[0] < t[1] else tuple([t[1], t[0], t[2]])
|
||||
for t in transfers0
|
||||
]
|
||||
transfers1 = [
|
||||
tuple([t[0], t[1], t[2]]) if t[0] < t[1] else tuple([t[1], t[0], t[2]])
|
||||
for t in transfers1
|
||||
]
|
||||
|
||||
transfers0.sort()
|
||||
transfers1.sort()
|
||||
|
||||
diff_cnt = 0
|
||||
for tr0, tr1 in zip(transfers0, transfers1):
|
||||
if tr0 != tr1:
|
||||
if diff_cnt == 0:
|
||||
logging.debug(
|
||||
"First pair of different transfers: %s, %s", tr0, tr1
|
||||
)
|
||||
diff_cnt += 1
|
||||
if diff_cnt:
|
||||
logging.debug("Different transfers number = %d", diff_cnt)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def compare_networks(network0, network1):
|
||||
if network0["agency_id"] != network1["agency_id"]:
|
||||
logging.debug("Different agency_id at route '%s'", network0["network"])
|
||||
return False
|
||||
|
||||
route_ids0 = sorted(x["route_id"] for x in network0["routes"])
|
||||
route_ids1 = sorted(x["route_id"] for x in network1["routes"])
|
||||
|
||||
if route_ids0 != route_ids1:
|
||||
logging.debug("Different route_ids: %s != %s", route_ids0, route_ids1)
|
||||
return False
|
||||
|
||||
routes0 = sorted(network0["routes"], key=lambda x: x["route_id"])
|
||||
routes1 = sorted(network1["routes"], key=lambda x: x["route_id"])
|
||||
|
||||
# Keys to compare routes. 'name' key is omitted since RouteMaster
|
||||
# can get its name from one of its Routes unpredictably.
|
||||
route_keys = ("type", "ref", "colour", "route_id")
|
||||
|
||||
for route0, route1 in zip(routes0, routes1):
|
||||
route0_props = tuple(route0[k] for k in route_keys)
|
||||
route1_props = tuple(route1[k] for k in route_keys)
|
||||
if route0_props != route1_props:
|
||||
logging.debug(
|
||||
"Route props of '%s' are different: %s, %s",
|
||||
route0["route_id"],
|
||||
route0_props,
|
||||
route1_props,
|
||||
)
|
||||
return False
|
||||
|
||||
itineraries0 = sorted(route0["itineraries"], key=itinerary_comparator)
|
||||
itineraries1 = sorted(route1["itineraries"], key=itinerary_comparator)
|
||||
|
||||
for itin0, itin1 in zip(itineraries0, itineraries1):
|
||||
if itin0["interval"] != itin1["interval"]:
|
||||
logging.debug(
|
||||
"Different interval: %d != %d at route %s '%s'",
|
||||
itin0["interval"],
|
||||
itin1["interval"],
|
||||
route0["route_id"],
|
||||
route0["name"],
|
||||
)
|
||||
return False
|
||||
if itin0["stops"] != itin1["stops"]:
|
||||
logging.debug(
|
||||
"Different stops at route %s '%s'",
|
||||
route0["route_id"],
|
||||
route0["name"],
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
|
@ -1,74 +0,0 @@
|
|||
"""
|
||||
Generate sorted list of all cities, with [bad] mark for bad cities.
|
||||
|
||||
!!! Deprecated for use in validation cycle.
|
||||
Use "scripts/process_subways.py --dump-city-list <filename>" instead.
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from subways.validation import (
|
||||
BAD_MARK,
|
||||
DEFAULT_CITIES_INFO_URL,
|
||||
get_cities_info,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arg_parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"""This script generates a list of good/all network names. It is
|
||||
used by subway render to generate the list of network at frontend.
|
||||
It uses two sources: a mapsme.json validator output with good
|
||||
networks, and a google spreadsheet with networks for the
|
||||
subways.validation.get_cities_info() function."""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
"subway_json_file",
|
||||
type=argparse.FileType("r"),
|
||||
help=(
|
||||
"Validator output defined by -o option "
|
||||
"of process_subways.py script",
|
||||
),
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
"--cities-info-url",
|
||||
default=DEFAULT_CITIES_INFO_URL,
|
||||
help=(
|
||||
"URL of CSV file with reference information about rapid transit "
|
||||
"networks. file:// protocol is also supported."
|
||||
),
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
"--with-bad",
|
||||
action="store_true",
|
||||
help="Whether to include cities validation of which was failed",
|
||||
)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
with_bad = args.with_bad
|
||||
subway_json_file = args.subway_json_file
|
||||
subway_json = json.load(subway_json_file)
|
||||
|
||||
good_cities = set(
|
||||
n.get("network", n.get("title")) for n in subway_json["networks"]
|
||||
)
|
||||
cities_info = get_cities_info(args.cities_info_url)
|
||||
|
||||
lines = []
|
||||
for ci in cities_info:
|
||||
if ci["name"] in good_cities:
|
||||
lines.append(f"{ci['name']}, {ci['country']}")
|
||||
elif with_bad:
|
||||
lines.append(f"{ci['name']}, {ci['country']} {BAD_MARK}")
|
||||
|
||||
for line in sorted(lines):
|
||||
print(line)
|
|
@ -1,58 +0,0 @@
|
|||
import argparse
|
||||
|
||||
from shapely import unary_union
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
|
||||
from subways.validation import DEFAULT_CITIES_INFO_URL, get_cities_info
|
||||
|
||||
|
||||
def make_disjoint_metro_polygons(cities_info_url: str) -> None:
|
||||
"""Make disjoint polygon from cities bboxes and write them
|
||||
in *.poly format to stdout.
|
||||
"""
|
||||
cities_info = get_cities_info(cities_info_url)
|
||||
|
||||
polygons = []
|
||||
for ci in cities_info:
|
||||
bbox = tuple(map(float, ci["bbox"].split(",")))
|
||||
polygon = Polygon(
|
||||
[
|
||||
(bbox[0], bbox[1]),
|
||||
(bbox[0], bbox[3]),
|
||||
(bbox[2], bbox[3]),
|
||||
(bbox[2], bbox[1]),
|
||||
]
|
||||
)
|
||||
polygons.append(polygon)
|
||||
|
||||
union = unary_union(polygons)
|
||||
|
||||
if union.geom_type == "Polygon":
|
||||
union = MultiPolygon([union])
|
||||
|
||||
print("all metro")
|
||||
for i, polygon in enumerate(union.geoms, start=1):
|
||||
assert len(polygon.interiors) == 0
|
||||
print(i)
|
||||
for lon, lat in polygon.exterior.coords:
|
||||
print(f" {lon} {lat}")
|
||||
print("END")
|
||||
print("END")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--cities-info-url",
|
||||
default=DEFAULT_CITIES_INFO_URL,
|
||||
help=(
|
||||
"URL of CSV file with reference information about rapid transit "
|
||||
"networks. file:// protocol is also supported."
|
||||
),
|
||||
)
|
||||
options = parser.parse_args()
|
||||
make_disjoint_metro_polygons(options.cities_info_url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,4 +0,0 @@
|
|||
shapely==2.0.4
|
||||
|
||||
# Fixate versions of indirect requirements
|
||||
NumPy==2.0.0
|
|
@ -1,2 +0,0 @@
|
|||
#,City,Country,Region,Stations,Subway Lines,Light Rail +Monorail,Interchanges,"BBox (lon, lat)",Networks (opt.),Approved,Comment,Source
|
||||
291,Moscow,Russia,Europe,351,14,3,68,"37.1667,55.3869,38.2626,56.0136","subway,train:Московский метрополитен;МЦК;МЦД"
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
@ -1,3 +0,0 @@
|
|||
#,City,Country,Region,Stations,Subway Lines,Light Rail +Monorail,Interchanges,"BBox (lon, lat)",Networks (opt.),Approved,Comment,Source
|
||||
313,London,UK,Europe,750,11,23,54,"-0.9747,51.1186,0.3315,51.8459","subway,train,light_rail:London Underground;London Overground;Docklands Light Railway;London Trams;Crossrail",,,https://tfl.gov.uk/maps/track/tube
|
||||
291,Moscow,Russia,Europe,351,14,3,68,"37.1667,55.3869,38.2626,56.0136","subway,train:Московский метрополитен;МЦК;МЦД"
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
@ -1,107 +0,0 @@
|
|||
import contextlib
|
||||
import io
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
from make_all_metro_poly import make_disjoint_metro_polygons
|
||||
|
||||
|
||||
cases = [
|
||||
{
|
||||
"csv_file": "cities_info_1city.csv",
|
||||
"expected_stdout": """all metro
|
||||
1
|
||||
37.1667 55.3869
|
||||
37.1667 56.0136
|
||||
38.2626 56.0136
|
||||
38.2626 55.3869
|
||||
37.1667 55.3869
|
||||
END
|
||||
END
|
||||
""",
|
||||
"shape_line_ranges": [
|
||||
{
|
||||
"start": 2,
|
||||
"end": 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"csv_file": "cities_info_2cities.csv",
|
||||
"expected_stdout": """all metro
|
||||
1
|
||||
-0.9747 51.8459
|
||||
0.3315 51.8459
|
||||
0.3315 51.1186
|
||||
-0.9747 51.1186
|
||||
-0.9747 51.8459
|
||||
END
|
||||
2
|
||||
37.1667 56.0136
|
||||
38.2626 56.0136
|
||||
38.2626 55.3869
|
||||
37.1667 55.3869
|
||||
37.1667 56.0136
|
||||
END
|
||||
END
|
||||
""",
|
||||
"shape_line_ranges": [
|
||||
{
|
||||
"start": 2,
|
||||
"end": 6,
|
||||
},
|
||||
{
|
||||
"start": 9,
|
||||
"end": 13,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestMakeAllMetroPoly(TestCase):
|
||||
def test_make_disjoint_metro_polygons(self) -> None:
|
||||
for case in cases:
|
||||
with self.subTest(msg=case["csv_file"]):
|
||||
assets_dir = Path(__file__).resolve().parent / "assets"
|
||||
file_url = f"file://{assets_dir}/{case['csv_file']}"
|
||||
stream = io.StringIO()
|
||||
with contextlib.redirect_stdout(stream):
|
||||
make_disjoint_metro_polygons(file_url)
|
||||
generated_poly = stream.getvalue()
|
||||
expected_poly = case["expected_stdout"]
|
||||
|
||||
# Since shapely may produce multipolygon with different order
|
||||
# of polygons in it and different vertex order in a polygon,
|
||||
# we should compare polygons/vertexes as sets.
|
||||
|
||||
generated_poly_lines = generated_poly.split("\n")
|
||||
expected_poly_lines = expected_poly.split("\n")
|
||||
self.assertSetEqual(
|
||||
set(expected_poly_lines), set(generated_poly_lines)
|
||||
)
|
||||
|
||||
line_ranges = case["shape_line_ranges"]
|
||||
|
||||
# Check that polygons are closed
|
||||
for line_range in line_ranges:
|
||||
self.assertEqual(
|
||||
generated_poly_lines[line_range["start"]],
|
||||
generated_poly_lines[line_range["end"]],
|
||||
)
|
||||
|
||||
generated_points = [
|
||||
sorted(
|
||||
generated_poly_lines[r["start"] : r["end"]] # noqa 203
|
||||
)
|
||||
for r in line_ranges
|
||||
]
|
||||
expected_points = [
|
||||
sorted(
|
||||
expected_poly_lines[r["start"] : r["end"]] # noqa 203
|
||||
)
|
||||
for r in line_ranges
|
||||
]
|
||||
expected_points.sort()
|
||||
generated_points.sort()
|
||||
self.assertListEqual(expected_points, generated_points)
|
|
@ -1,243 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import codecs
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import kdtree
|
||||
from lxml import etree
|
||||
|
||||
|
||||
QUERY = """
|
||||
[out:json][timeout:250][bbox:{{bbox}}];
|
||||
(
|
||||
relation["route"="subway"];<<;
|
||||
relation["route"="light_rail"];<<;
|
||||
relation["public_transport"="stop_area"];<<;
|
||||
way["station"="subway"];
|
||||
way["station"="light_rail"];
|
||||
node["railway"="station"]["subway"="yes"];
|
||||
node["railway"="station"]["light_rail"="yes"];
|
||||
node["station"="subway"];
|
||||
node["station"="light_rail"];
|
||||
node["railway"="subway_entrance"];
|
||||
node["public_transport"]["subway"="yes"];
|
||||
node["public_transport"]["light_rail"="yes"];
|
||||
);
|
||||
(._;>;);
|
||||
out meta center qt;
|
||||
"""
|
||||
|
||||
|
||||
def el_id(el):
|
||||
return el["type"][0] + str(el.get("id", el.get("ref", "")))
|
||||
|
||||
|
||||
class StationWrapper:
|
||||
def __init__(self, st):
|
||||
if "center" in st:
|
||||
self.coords = (st["center"]["lon"], st["center"]["lat"])
|
||||
elif "lon" in st:
|
||||
self.coords = (st["lon"], st["lat"])
|
||||
else:
|
||||
raise Exception("Coordinates not found for station {}".format(st))
|
||||
self.station = st
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.coords[i]
|
||||
|
||||
def distance(self, other):
|
||||
"""Calculate distance in meters."""
|
||||
dx = math.radians(self[0] - other["lon"]) * math.cos(
|
||||
0.5 * math.radians(self[1] + other["lat"])
|
||||
)
|
||||
dy = math.radians(self[1] - other["lat"])
|
||||
return 6378137 * math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
|
||||
def overpass_request(bbox):
|
||||
url = "http://overpass-api.de/api/interpreter?data={}".format(
|
||||
urllib.parse.quote(QUERY.replace("{{bbox}}", bbox))
|
||||
)
|
||||
response = urllib.request.urlopen(url, timeout=1000)
|
||||
if response.getcode() != 200:
|
||||
raise Exception(
|
||||
"Failed to query Overpass API: HTTP {}".format(response.getcode())
|
||||
)
|
||||
reader = codecs.getreader("utf-8")
|
||||
return json.load(reader(response))["elements"]
|
||||
|
||||
|
||||
def add_stop_areas(src):
|
||||
if not src:
|
||||
raise Exception("Empty dataset provided to add_stop_areas")
|
||||
|
||||
# Add station=* tags to stations in subway and light_rail routes
|
||||
stations = {}
|
||||
for el in src:
|
||||
if "tags" in el and el["tags"].get("railway", None) == "station":
|
||||
stations[el_id(el)] = el
|
||||
|
||||
for el in src:
|
||||
if (
|
||||
el["type"] == "relation"
|
||||
and "tags" in el
|
||||
and el["tags"].get("route", None) in ("subway", "light_rail")
|
||||
):
|
||||
for m in el["members"]:
|
||||
st = stations.get(el_id(m), None)
|
||||
if st and "station" not in st["tags"]:
|
||||
st["tags"]["station"] = el["tags"]["route"]
|
||||
st["modified"] = True
|
||||
|
||||
# Create a kd-tree out of subway stations
|
||||
stations = kdtree.create(dimensions=2)
|
||||
for el in src:
|
||||
if "tags" in el and el["tags"].get("station", None) in (
|
||||
"subway",
|
||||
"light_rail",
|
||||
):
|
||||
stations.add(StationWrapper(el))
|
||||
|
||||
if stations.is_leaf:
|
||||
raise Exception("No stations found")
|
||||
|
||||
# Populate a list of nearby subway exits and platforms for each station
|
||||
MAX_DISTANCE = 300 # meters
|
||||
stop_areas = {}
|
||||
for el in src:
|
||||
if "tags" not in el:
|
||||
continue
|
||||
if "station" in el["tags"]:
|
||||
continue
|
||||
if el["tags"].get("railway", None) not in (
|
||||
"subway_entrance",
|
||||
"platform",
|
||||
) and el["tags"].get("public_transport", None) not in (
|
||||
"platform",
|
||||
"stop_position",
|
||||
):
|
||||
continue
|
||||
coords = el.get("center", el)
|
||||
station = stations.search_nn((coords["lon"], coords["lat"]))[0].data
|
||||
if station.distance(coords) < MAX_DISTANCE:
|
||||
k = (
|
||||
station.station["id"],
|
||||
station.station["tags"].get("name", "station_with_no_name"),
|
||||
)
|
||||
# Disregard exits and platforms that are differently named
|
||||
if el["tags"].get("name", k[1]) == k[1]:
|
||||
if k not in stop_areas:
|
||||
stop_areas[k] = {el_id(station.station): station.station}
|
||||
stop_areas[k][el_id(el)] = el
|
||||
|
||||
# Find existing stop_area relations for stations and remove these stations
|
||||
for el in src:
|
||||
if (
|
||||
el["type"] == "relation"
|
||||
and el["tags"].get("public_transport", None) == "stop_area"
|
||||
):
|
||||
found = False
|
||||
for m in el["members"]:
|
||||
if found:
|
||||
break
|
||||
for st in stop_areas:
|
||||
if el_id(m) in stop_areas[st]:
|
||||
del stop_areas[st]
|
||||
found = True
|
||||
break
|
||||
|
||||
# Create OSM XML for new stop_area relations
|
||||
root = etree.Element("osm", version="0.6")
|
||||
rid = -1
|
||||
for st, members in stop_areas.items():
|
||||
rel = etree.SubElement(root, "relation", id=str(rid))
|
||||
rid -= 1
|
||||
etree.SubElement(rel, "tag", k="type", v="public_transport")
|
||||
etree.SubElement(rel, "tag", k="public_transport", v="stop_area")
|
||||
etree.SubElement(rel, "tag", k="name", v=st[1])
|
||||
for m in members.values():
|
||||
if (
|
||||
m["tags"].get(
|
||||
"railway", m["tags"].get("public_transport", None)
|
||||
)
|
||||
== "platform"
|
||||
):
|
||||
role = "platform"
|
||||
elif m["tags"].get("public_transport", None) == "stop_position":
|
||||
role = "stop"
|
||||
else:
|
||||
role = ""
|
||||
etree.SubElement(
|
||||
rel, "member", ref=str(m["id"]), type=m["type"], role=role
|
||||
)
|
||||
|
||||
# Add all downloaded elements
|
||||
for el in src:
|
||||
obj = etree.SubElement(root, el["type"])
|
||||
for a in (
|
||||
"id",
|
||||
"type",
|
||||
"user",
|
||||
"uid",
|
||||
"version",
|
||||
"changeset",
|
||||
"timestamp",
|
||||
"lat",
|
||||
"lon",
|
||||
):
|
||||
if a in el:
|
||||
obj.set(a, str(el[a]))
|
||||
if "modified" in el:
|
||||
obj.set("action", "modify")
|
||||
if "tags" in el:
|
||||
for k, v in el["tags"].items():
|
||||
etree.SubElement(obj, "tag", k=k, v=v)
|
||||
if "members" in el:
|
||||
for m in el["members"]:
|
||||
etree.SubElement(
|
||||
obj,
|
||||
"member",
|
||||
ref=str(m["ref"]),
|
||||
type=m["type"],
|
||||
role=m.get("role", ""),
|
||||
)
|
||||
if "nodes" in el:
|
||||
for n in el["nodes"]:
|
||||
etree.SubElement(obj, "nd", ref=str(n))
|
||||
|
||||
return etree.tostring(root, pretty_print=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
"Read a JSON from Overpass and output JOSM OSM XML with added "
|
||||
"stop_area relations"
|
||||
)
|
||||
print(
|
||||
"Usage: {} {{<export.json>|<bbox>}} [output.osm]".format(
|
||||
sys.argv[0]
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if re.match(r"^[-0-9.,]+$", sys.argv[1]):
|
||||
src = overpass_request(sys.argv[1])
|
||||
else:
|
||||
with open(sys.argv[1], "r") as f:
|
||||
src = json.load(f)["elements"]
|
||||
|
||||
result = add_stop_areas(src)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print(result.decode("utf-8"))
|
||||
else:
|
||||
with open(sys.argv[2], "wb") as f:
|
||||
f.write(result)
|
|
@ -1,220 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import codecs
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import kdtree
|
||||
from lxml import etree
|
||||
|
||||
|
||||
QUERY = """
|
||||
[out:json][timeout:250][bbox:{{bbox}}];
|
||||
(
|
||||
relation["route"="tram"];
|
||||
relation["public_transport"="stop_area"];
|
||||
nwr["railway"="tram_stop"];
|
||||
);
|
||||
(._;>>;);
|
||||
out meta center qt;
|
||||
"""
|
||||
|
||||
|
||||
def el_id(el):
|
||||
return el["type"][0] + str(el.get("id", el.get("ref", "")))
|
||||
|
||||
|
||||
class StationWrapper:
|
||||
def __init__(self, st):
|
||||
if "center" in st:
|
||||
self.coords = (st["center"]["lon"], st["center"]["lat"])
|
||||
elif "lon" in st:
|
||||
self.coords = (st["lon"], st["lat"])
|
||||
else:
|
||||
raise Exception("Coordinates not found for station {}".format(st))
|
||||
self.station = st
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.coords[i]
|
||||
|
||||
def distance(self, other):
|
||||
"""Calculate distance in meters."""
|
||||
dx = math.radians(self[0] - other["lon"]) * math.cos(
|
||||
0.5 * math.radians(self[1] + other["lat"])
|
||||
)
|
||||
dy = math.radians(self[1] - other["lat"])
|
||||
return 6378137 * math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
|
||||
def overpass_request(bbox):
|
||||
url = "http://overpass-api.de/api/interpreter?data={}".format(
|
||||
urllib.parse.quote(QUERY.replace("{{bbox}}", bbox))
|
||||
)
|
||||
response = urllib.request.urlopen(url, timeout=1000)
|
||||
if response.getcode() != 200:
|
||||
raise Exception(
|
||||
"Failed to query Overpass API: HTTP {}".format(response.getcode())
|
||||
)
|
||||
reader = codecs.getreader("utf-8")
|
||||
return json.load(reader(response))["elements"]
|
||||
|
||||
|
||||
def is_part_of_stop(tags):
|
||||
if tags.get("public_transport") in ("platform", "stop_position"):
|
||||
return True
|
||||
if tags.get("railway") == "platform":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_stop_areas(src):
|
||||
if not src:
|
||||
raise Exception("Empty dataset provided to add_stop_areas")
|
||||
|
||||
# Create a kd-tree out of tram stations
|
||||
stations = kdtree.create(dimensions=2)
|
||||
for el in src:
|
||||
if "tags" in el and el["tags"].get("railway") == "tram_stop":
|
||||
stations.add(StationWrapper(el))
|
||||
|
||||
if stations.is_leaf:
|
||||
raise Exception("No stations found")
|
||||
|
||||
elements = {}
|
||||
for el in src:
|
||||
if el.get("tags"):
|
||||
elements[el_id(el)] = el
|
||||
|
||||
# Populate a list of nearby subway exits and platforms for each station
|
||||
MAX_DISTANCE = 50 # meters
|
||||
stop_areas = {}
|
||||
for el in src:
|
||||
# Only tram routes
|
||||
if (
|
||||
"tags" not in el
|
||||
or el["type"] != "relation"
|
||||
or el["tags"].get("route") != "tram"
|
||||
):
|
||||
continue
|
||||
for m in el["members"]:
|
||||
if el_id(m) not in elements:
|
||||
continue
|
||||
pel = elements[el_id(m)]
|
||||
if not is_part_of_stop(pel["tags"]):
|
||||
continue
|
||||
if pel["tags"].get("railway") == "tram_stop":
|
||||
continue
|
||||
coords = pel.get("center", pel)
|
||||
station = stations.search_nn((coords["lon"], coords["lat"]))[
|
||||
0
|
||||
].data
|
||||
if station.distance(coords) < MAX_DISTANCE:
|
||||
k = (
|
||||
station.station["id"],
|
||||
station.station["tags"].get("name", None),
|
||||
)
|
||||
if k not in stop_areas:
|
||||
stop_areas[k] = {el_id(station.station): station.station}
|
||||
stop_areas[k][el_id(m)] = pel
|
||||
|
||||
# Find existing stop_area relations for stations and remove these stations
|
||||
for el in src:
|
||||
if (
|
||||
el["type"] == "relation"
|
||||
and el["tags"].get("public_transport", None) == "stop_area"
|
||||
):
|
||||
found = False
|
||||
for m in el["members"]:
|
||||
if found:
|
||||
break
|
||||
for st in stop_areas:
|
||||
if el_id(m) in stop_areas[st]:
|
||||
del stop_areas[st]
|
||||
found = True
|
||||
break
|
||||
|
||||
# Create OSM XML for new stop_area relations
|
||||
root = etree.Element("osm", version="0.6")
|
||||
rid = -1
|
||||
for st, members in stop_areas.items():
|
||||
rel = etree.SubElement(root, "relation", id=str(rid))
|
||||
rid -= 1
|
||||
etree.SubElement(rel, "tag", k="type", v="public_transport")
|
||||
etree.SubElement(rel, "tag", k="public_transport", v="stop_area")
|
||||
if st[1]:
|
||||
etree.SubElement(rel, "tag", k="name", v=st[1])
|
||||
for m in members.values():
|
||||
etree.SubElement(
|
||||
rel, "member", ref=str(m["id"]), type=m["type"], role=""
|
||||
)
|
||||
|
||||
# Add all downloaded elements
|
||||
for el in src:
|
||||
obj = etree.SubElement(root, el["type"])
|
||||
for a in (
|
||||
"id",
|
||||
"type",
|
||||
"user",
|
||||
"uid",
|
||||
"version",
|
||||
"changeset",
|
||||
"timestamp",
|
||||
"lat",
|
||||
"lon",
|
||||
):
|
||||
if a in el:
|
||||
obj.set(a, str(el[a]))
|
||||
if "modified" in el:
|
||||
obj.set("action", "modify")
|
||||
if "tags" in el:
|
||||
for k, v in el["tags"].items():
|
||||
etree.SubElement(obj, "tag", k=k, v=v)
|
||||
if "members" in el:
|
||||
for m in el["members"]:
|
||||
etree.SubElement(
|
||||
obj,
|
||||
"member",
|
||||
ref=str(m["ref"]),
|
||||
type=m["type"],
|
||||
role=m.get("role", ""),
|
||||
)
|
||||
if "nodes" in el:
|
||||
for n in el["nodes"]:
|
||||
etree.SubElement(obj, "nd", ref=str(n))
|
||||
|
||||
return etree.tostring(root, pretty_print=True, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
"Read a JSON from Overpass and output JOSM OSM XML "
|
||||
"with added stop_area relations"
|
||||
)
|
||||
print(
|
||||
"Usage: {} {{<export.json>|<bbox>}} [output.osm]".format(
|
||||
sys.argv[0]
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if re.match(r"^[-0-9.,]+$", sys.argv[1]):
|
||||
bbox = sys.argv[1].split(",")
|
||||
src = overpass_request(",".join([bbox[i] for i in (1, 0, 3, 2)]))
|
||||
else:
|
||||
with open(sys.argv[1], "r") as f:
|
||||
src = json.load(f)["elements"]
|
||||
|
||||
result = add_stop_areas(src)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print(result.decode("utf-8"))
|
||||
else:
|
||||
with open(sys.argv[2], "wb") as f:
|
||||
f.write(result)
|
|
@ -1,12 +0,0 @@
|
|||
Flask==2.2.3
|
||||
kdtree==0.16
|
||||
lxml==4.9.2
|
||||
|
||||
## The following requirements were added by pip freeze:
|
||||
click==8.1.3
|
||||
importlib-metadata==6.0.0
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.2
|
||||
Werkzeug==2.2.3
|
||||
zipp==3.13.0
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from flask import Flask, make_response, render_template, request
|
||||
|
||||
from make_stop_areas import add_stop_areas, overpass_request
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def form():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/process", methods=["GET"])
|
||||
def convert():
|
||||
src = overpass_request(request.args.get("bbox"))
|
||||
if not src:
|
||||
return "No data from overpass, sorry."
|
||||
result = add_stop_areas(src)
|
||||
response = make_response(result)
|
||||
response.headers[
|
||||
"Content-Disposition"
|
||||
] = 'attachment; filename="stop_areas.osm"'
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
|
@ -1,265 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
|
||||
from subways.validation import DEFAULT_SPREADSHEET_ID
|
||||
from v2h_templates import (
|
||||
COUNTRY_CITY,
|
||||
COUNTRY_FOOTER,
|
||||
COUNTRY_HEADER,
|
||||
INDEX_CONTINENT,
|
||||
INDEX_COUNTRY,
|
||||
INDEX_FOOTER,
|
||||
INDEX_HEADER,
|
||||
)
|
||||
|
||||
|
||||
class CityData:
|
||||
def __init__(self, city: dict | None = None) -> None:
|
||||
self.data = {
|
||||
"good_cities": 0,
|
||||
"total_cities": 1 if city else 0,
|
||||
"num_errors": 0,
|
||||
"num_warnings": 0,
|
||||
"num_notices": 0,
|
||||
}
|
||||
self.slug = None
|
||||
if city:
|
||||
self.slug = city["slug"]
|
||||
self.country = city["country"]
|
||||
self.continent = city["continent"]
|
||||
self.errors = city["errors"]
|
||||
self.warnings = city["warnings"]
|
||||
self.notices = city["notices"]
|
||||
if not self.errors:
|
||||
self.data["good_cities"] = 1
|
||||
self.data["num_errors"] = len(self.errors)
|
||||
self.data["num_warnings"] = len(self.warnings)
|
||||
self.data["num_notices"] = len(self.notices)
|
||||
for k, v in city.items():
|
||||
if "found" in k or "expected" in k or "unused" in k:
|
||||
self.data[k] = v
|
||||
|
||||
def __add__(self, other: CityData) -> CityData:
|
||||
d = CityData()
|
||||
for k in set(self.data.keys()) | set(other.data.keys()):
|
||||
d.data[k] = self.data.get(k, 0) + other.data.get(k, 0)
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def test_eq(v1: Any, v2: Any) -> str:
|
||||
return "1" if v1 == v2 else "0"
|
||||
|
||||
def format(self, s: str) -> str:
|
||||
for k in self.data:
|
||||
s = s.replace("{" + k + "}", str(self.data[k]))
|
||||
s = s.replace("{slug}", self.slug or "")
|
||||
for k in (
|
||||
"subwayl",
|
||||
"lightrl",
|
||||
"stations",
|
||||
"transfers",
|
||||
"busl",
|
||||
"trolleybusl",
|
||||
"traml",
|
||||
"otherl",
|
||||
):
|
||||
if k + "_expected" in self.data:
|
||||
s = s.replace(
|
||||
"{=" + k + "}",
|
||||
self.test_eq(
|
||||
self.data[k + "_found"], self.data[k + "_expected"]
|
||||
),
|
||||
)
|
||||
s = s.replace(
|
||||
"{=cities}",
|
||||
self.test_eq(self.data["good_cities"], self.data["total_cities"]),
|
||||
)
|
||||
s = s.replace(
|
||||
"{=entrances}", self.test_eq(self.data["unused_entrances"], 0)
|
||||
)
|
||||
for k in ("errors", "warnings", "notices"):
|
||||
s = s.replace(
|
||||
"{=" + k + "}", self.test_eq(self.data["num_" + k], 0)
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
def tmpl(s: str, data: CityData | None = None, **kwargs) -> str:
|
||||
if data:
|
||||
s = data.format(s)
|
||||
if kwargs:
|
||||
for k, v in kwargs.items():
|
||||
if v is not None:
|
||||
s = s.replace("{" + k + "}", str(v))
|
||||
s = re.sub(
|
||||
r"\{\?" + k + r"\}(.+?)\{end\}",
|
||||
r"\1" if v else "",
|
||||
s,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
EXPAND_OSM_TYPE = {"n": "node", "w": "way", "r": "relation"}
|
||||
RE_SHORT = re.compile(r"\b([nwr])(\d+)\b")
|
||||
RE_FULL = re.compile(r"\b(node|way|relation) (\d+)\b")
|
||||
RE_COORDS = re.compile(r"\((-?\d+\.\d+), (-?\d+\.\d+)\)")
|
||||
|
||||
|
||||
def osm_links(s: str) -> str:
|
||||
"""Converts object mentions to HTML links."""
|
||||
|
||||
def link(m: re.Match) -> str:
|
||||
osm_type = EXPAND_OSM_TYPE[m.group(1)[0]]
|
||||
osm_id = m.group(2)
|
||||
return (
|
||||
'<a href="https://www.openstreetmap.org/'
|
||||
f'{osm_type}/{osm_id}">{m.group(0)}</a>'
|
||||
)
|
||||
|
||||
s = RE_SHORT.sub(link, s)
|
||||
s = RE_FULL.sub(link, s)
|
||||
s = RE_COORDS.sub(
|
||||
r'(<a href="https://www.openstreetmap.org/search?'
|
||||
r'query=\2%2C\1#map=18/\2/\1">pos</a>)',
|
||||
s,
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
def esc(s: str) -> str:
|
||||
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def br_osm_links(elems: list) -> str:
|
||||
return "<br>".join(osm_links(esc(elem)) for elem in elems)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Reads a log from subway validator and prepares HTML files."
|
||||
)
|
||||
)
|
||||
parser.add_argument("validation_log")
|
||||
parser.add_argument("target_directory", nargs="?", default=".")
|
||||
parser.add_argument(
|
||||
"--cities-info-url",
|
||||
default=(
|
||||
"https://docs.google.com/spreadsheets/d/"
|
||||
f"{DEFAULT_SPREADSHEET_ID}/edit?usp=sharing"
|
||||
),
|
||||
)
|
||||
options = parser.parse_args()
|
||||
target_dir = options.target_directory
|
||||
cities_info_url = options.cities_info_url
|
||||
|
||||
with open(options.validation_log, encoding="utf-8") as f:
|
||||
data = {c["name"]: CityData(c) for c in json.load(f)}
|
||||
|
||||
countries = {}
|
||||
continents = {}
|
||||
c_by_c = defaultdict(set) # continent → set of countries
|
||||
for c in data.values():
|
||||
countries[c.country] = c + countries.get(c.country, CityData())
|
||||
continents[c.continent] = c + continents.get(c.continent, CityData())
|
||||
c_by_c[c.continent].add(c.country)
|
||||
world = sum(continents.values(), CityData())
|
||||
|
||||
overground = "traml_expected" in next(iter(data.values())).data
|
||||
date = datetime.datetime.utcnow().strftime("%d.%m.%Y %H:%M UTC")
|
||||
index = open(os.path.join(target_dir, "index.html"), "w", encoding="utf-8")
|
||||
index.write(tmpl(INDEX_HEADER, world))
|
||||
|
||||
for continent in sorted(continents.keys()):
|
||||
content = ""
|
||||
for country in sorted(c_by_c[continent]):
|
||||
country_file_name = country.lower().replace(" ", "-") + ".html"
|
||||
content += tmpl(
|
||||
INDEX_COUNTRY,
|
||||
countries[country],
|
||||
file=country_file_name,
|
||||
country=country,
|
||||
continent=continent,
|
||||
)
|
||||
country_file = open(
|
||||
os.path.join(target_dir, country_file_name),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
)
|
||||
country_file.write(
|
||||
tmpl(
|
||||
COUNTRY_HEADER,
|
||||
country=country,
|
||||
continent=continent,
|
||||
overground=overground,
|
||||
subways=not overground,
|
||||
)
|
||||
)
|
||||
for name, city in sorted(
|
||||
(name, city)
|
||||
for name, city in data.items()
|
||||
if city.country == country
|
||||
):
|
||||
file_base = os.path.join(target_dir, city.slug)
|
||||
yaml_file = (
|
||||
city.slug + ".yaml"
|
||||
if os.path.exists(file_base + ".yaml")
|
||||
else None
|
||||
)
|
||||
json_file = (
|
||||
city.slug + ".geojson"
|
||||
if os.path.exists(file_base + ".geojson")
|
||||
else None
|
||||
)
|
||||
errors = br_osm_links(city.errors)
|
||||
warnings = br_osm_links(city.warnings)
|
||||
notices = br_osm_links(city.notices)
|
||||
country_file.write(
|
||||
tmpl(
|
||||
COUNTRY_CITY,
|
||||
city,
|
||||
city=name,
|
||||
country=country,
|
||||
continent=continent,
|
||||
yaml=yaml_file,
|
||||
json=json_file,
|
||||
subways=not overground,
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
notices=notices,
|
||||
overground=overground,
|
||||
)
|
||||
)
|
||||
country_file.write(
|
||||
tmpl(
|
||||
COUNTRY_FOOTER,
|
||||
country=country,
|
||||
continent=continent,
|
||||
date=date,
|
||||
)
|
||||
)
|
||||
country_file.close()
|
||||
index.write(
|
||||
tmpl(
|
||||
INDEX_CONTINENT,
|
||||
continents[continent],
|
||||
content=content,
|
||||
continent=continent,
|
||||
)
|
||||
)
|
||||
|
||||
index.write(tmpl(INDEX_FOOTER, date=date, cities_info_url=cities_info_url))
|
||||
index.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,19 +1,7 @@
|
|||
validator_osm_wiki_url = (
|
||||
"https://wiki.openstreetmap.org/wiki/Quality_assurance#subway-preprocessor"
|
||||
)
|
||||
github_url = "https://github.com/alexey-zakharenkov/subways"
|
||||
produced_by = f"""Produced by
|
||||
<a href="{github_url}">Subway Preprocessor</a> on {{date}}"""
|
||||
metro_mapping_osm_article = "https://wiki.openstreetmap.org/wiki/Metro_Mapping"
|
||||
list_of_metro_systems_url = (
|
||||
"https://en.wikipedia.org/wiki/List_of_metro_systems#List"
|
||||
)
|
||||
|
||||
|
||||
# These are templates for validation_to_html.py
|
||||
# Variables should be in curly braces
|
||||
|
||||
STYLE = """
|
||||
STYLE = '''
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
|
@ -120,31 +108,30 @@ footer {
|
|||
position: sticky;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
'''
|
||||
|
||||
INDEX_HEADER = f"""
|
||||
INDEX_HEADER = '''
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Subway Validator</title>
|
||||
<meta charset="utf-8">
|
||||
{STYLE}
|
||||
(s)
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Subway Validation Results</h1>
|
||||
<p><b>{{good_cities}}</b> of <b>{{total_cities}}</b> networks validated without
|
||||
errors. To make a network validate successfully please follow the
|
||||
<a href="{metro_mapping_osm_article}">metro mapping
|
||||
instructions</a>. Commit your changes to the OSM and then check back to the
|
||||
updated validation results after the next validation cycle, please.
|
||||
See <a href="{validator_osm_wiki_url}">the validator instance(s)
|
||||
description</a> for the schedule and capabilities.</p>
|
||||
<p><b>{good_cities}</b> of <b>{total_cities}</b> networks validated without errors.
|
||||
To make a network validate successfully please follow the
|
||||
<a href="https://wiki.openstreetmap.org/wiki/Metro_Mapping">metro mapping instructions</a>.
|
||||
Commit your changes to the OSM and then check back to the updated validation results after the next validation cycle, please.
|
||||
See <a href="https://wiki.openstreetmap.org/wiki/Quality_assurance#subway-preprocessor">the validator instance(s) description</a>
|
||||
for the schedule and capabilities.</p>
|
||||
<p><a href="render.html">View networks on a map</a></p>
|
||||
<table cellspacing="3" cellpadding="2" style="margin-bottom: 1em;">
|
||||
"""
|
||||
'''.replace('(s)', STYLE)
|
||||
|
||||
INDEX_CONTINENT = """
|
||||
INDEX_CONTINENT = '''
|
||||
<tr><td colspan="9"> </td></tr>
|
||||
<tr>
|
||||
<th>Continent</th>
|
||||
|
@ -170,9 +157,9 @@ INDEX_CONTINENT = """
|
|||
<td class="color{=notices}">{num_notices}</td>
|
||||
</tr>
|
||||
{content}
|
||||
"""
|
||||
'''
|
||||
|
||||
INDEX_COUNTRY = """
|
||||
INDEX_COUNTRY = '''
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="bold color{=cities}"><a href="{file}">{country}</a></td>
|
||||
|
@ -185,57 +172,56 @@ INDEX_COUNTRY = """
|
|||
<td class="color{=warnings}">{num_warnings}</td>
|
||||
<td class="color{=notices}">{num_notices}</td>
|
||||
</tr>
|
||||
"""
|
||||
'''
|
||||
|
||||
INDEX_FOOTER = f"""
|
||||
INDEX_FOOTER = '''
|
||||
</table>
|
||||
</main>
|
||||
<footer>{produced_by}
|
||||
from <a href="{{cities_info_url}}">this reference metro statistics</a>. See
|
||||
<a href="{list_of_metro_systems_url}">
|
||||
this wiki page</a> for a list of all metro systems.</footer>
|
||||
<footer>Produced by <a href="https://github.com/alexey-zakharenkov/subways">Subway Preprocessor</a> on {date}.
|
||||
See <a href="{google}">this spreadsheet</a> for the reference metro statistics and
|
||||
<a href="https://en.wikipedia.org/wiki/List_of_metro_systems#List">this wiki page</a> for a list
|
||||
of all metro systems.</footer>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
'''
|
||||
|
||||
COUNTRY_HEADER = f"""
|
||||
COUNTRY_HEADER = '''
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Subway Validator: {{country}}</title>
|
||||
<title>Subway Validator: {country}</title>
|
||||
<meta charset="utf-8">
|
||||
{STYLE}
|
||||
(s)
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Subway Validation Results for {{country}}</h1>
|
||||
<h1>Subway Validation Results for {country}</h1>
|
||||
<p><a href="index.html">Return to the countries list</a>.</p>
|
||||
<table cellspacing="3" cellpadding="2">
|
||||
<tr>
|
||||
<th>City</th>
|
||||
{{?subways}}
|
||||
{?subways}
|
||||
<th>Subway Lines</th>
|
||||
<th>Light Rail Lines</th>
|
||||
{{end}}{{?overground}}
|
||||
{end}{?overground}
|
||||
<th>Tram Lines</th>
|
||||
<th>Bus Lines</th>
|
||||
<th>T-Bus Lines</th>
|
||||
<th>Other Lines</th>
|
||||
{{end}}
|
||||
{end}
|
||||
<th>Stations</th>
|
||||
<th>Interchanges</th>
|
||||
<th>Unused Entrances</th>
|
||||
</tr>
|
||||
"""
|
||||
'''.replace('(s)', STYLE)
|
||||
|
||||
COUNTRY_CITY = """
|
||||
COUNTRY_CITY = '''
|
||||
<tr id="{slug}">
|
||||
<td class="bold color{good_cities}">
|
||||
{city}
|
||||
{?yaml}<a href="{yaml}" class="hlink" title="Download YAML">Y</a>{end}
|
||||
{?json}<a href="{json}" class="hlink" title="Download GeoJSON">J</a>{end}
|
||||
{?json}<a href="render.html#{slug}" class="hlink" title="View map"
|
||||
target="_blank">M</a>{end}
|
||||
{?json}<a href="render.html#{slug}" class="hlink" title="View map" target="_blank">M</a>{end}
|
||||
</td>
|
||||
{?subways}
|
||||
<td class="color{=subwayl}">sub: {subwayl_found} / {subwayl_expected}</td>
|
||||
|
@ -243,55 +229,36 @@ COUNTRY_CITY = """
|
|||
{end}{?overground}
|
||||
<td class="color{=traml}">t: {traml_found} / {traml_expected}</td>
|
||||
<td class="color{=busl}">b: {busl_found} / {busl_expected}</td>
|
||||
<td class="color{=trolleybusl}">
|
||||
tb: {trolleybusl_found} / {trolleybusl_expected}
|
||||
</td>
|
||||
<td class="color{=trolleybusl}">tb: {trolleybusl_found} / {trolleybusl_expected}</td>
|
||||
<td class="color{=otherl}">o: {otherl_found} / {otherl_expected}</td>
|
||||
{end}
|
||||
<td class="color{=stations}">st: {stations_found} / {stations_expected}</td>
|
||||
<td class="color{=transfers}">
|
||||
int: {transfers_found} / {transfers_expected}
|
||||
</td>
|
||||
<td class="color{=transfers}">int: {transfers_found} / {transfers_expected}</td>
|
||||
<td class="color{=entrances}">ent: {unused_entrances}</td>
|
||||
</tr>
|
||||
<tr><td colspan="{?subways}6{end}{?overground}8{end}">
|
||||
{?errors}
|
||||
<div class="errors">
|
||||
<div
|
||||
data-text="Network is invalid and not suitable for routing."
|
||||
class="tooltip">
|
||||
🛑 Errors
|
||||
</div>
|
||||
<div class="errors"><div data-text="Network is invalid and not suitable for routing." class="tooltip">🛑 Errors</div>
|
||||
{errors}
|
||||
</div>
|
||||
{end}
|
||||
{?warnings}
|
||||
<div class="warnings">
|
||||
<div
|
||||
data-text="Problematic data but it's still possible to build routes."
|
||||
class="tooltip">
|
||||
⚠️ Warnings
|
||||
</div>
|
||||
<div class="warnings"><div data-text="Problematic data but it's still possible to build routes." class="tooltip">⚠️ Warnings</div>
|
||||
{warnings}
|
||||
</div>
|
||||
{end}
|
||||
{?notices}
|
||||
<div class="notices">
|
||||
<div
|
||||
data-text="Suspicious condition but not necessarily an error."
|
||||
class="tooltip">
|
||||
ℹ️ Notices
|
||||
</div>
|
||||
<div class="notices"><div data-text="Suspicious condition but not necessarily an error." class="tooltip">ℹ️ Notices</div>
|
||||
{notices}
|
||||
{end}
|
||||
</div>
|
||||
</td></tr>
|
||||
"""
|
||||
'''
|
||||
|
||||
COUNTRY_FOOTER = f"""
|
||||
COUNTRY_FOOTER = '''
|
||||
</table>
|
||||
</main>
|
||||
<footer>{produced_by}.</footer>
|
||||
<footer>Produced by <a href="https://github.com/alexey-zakharenkov/subways">Subway Preprocessor</a> on {date}.</footer>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
'''
|
232
validation_to_html.py
Executable file
232
validation_to_html.py
Executable file
|
@ -0,0 +1,232 @@
|
|||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from subway_structure import SPREADSHEET_ID
|
||||
from v2h_templates import *
|
||||
|
||||
|
||||
class CityData:
|
||||
def __init__(self, city=None):
|
||||
self.city = city is not None
|
||||
self.data = {
|
||||
'good_cities': 0,
|
||||
'total_cities': 1 if city else 0,
|
||||
'num_errors': 0,
|
||||
'num_warnings': 0,
|
||||
'num_notices': 0
|
||||
}
|
||||
self.slug = None
|
||||
if city:
|
||||
self.slug = city['slug']
|
||||
self.country = city['country']
|
||||
self.continent = city['continent']
|
||||
self.errors = city['errors']
|
||||
self.warnings = city['warnings']
|
||||
self.notices = city['notices']
|
||||
if not self.errors:
|
||||
self.data['good_cities'] = 1
|
||||
self.data['num_errors'] = len(self.errors)
|
||||
self.data['num_warnings'] = len(self.warnings)
|
||||
self.data['num_notices'] = len(self.notices)
|
||||
for k, v in city.items():
|
||||
if 'found' in k or 'expected' in k or 'unused' in k:
|
||||
self.data[k] = v
|
||||
|
||||
def not__get__(self, i):
|
||||
return self.data.get(i)
|
||||
|
||||
def not__set__(self, i, value):
|
||||
self.data[i] = value
|
||||
|
||||
def __add__(self, other):
|
||||
d = CityData()
|
||||
for k in set(self.data.keys()) | set(other.data.keys()):
|
||||
d.data[k] = self.data.get(k, 0) + other.data.get(k, 0)
|
||||
return d
|
||||
|
||||
def format(self, s):
|
||||
def test_eq(v1, v2):
|
||||
return '1' if v1 == v2 else '0'
|
||||
|
||||
for k in self.data:
|
||||
s = s.replace('{' + k + '}', str(self.data[k]))
|
||||
s = s.replace('{slug}', self.slug or '')
|
||||
for k in (
|
||||
'subwayl',
|
||||
'lightrl',
|
||||
'stations',
|
||||
'transfers',
|
||||
'busl',
|
||||
'trolleybusl',
|
||||
'traml',
|
||||
'otherl',
|
||||
):
|
||||
if k + '_expected' in self.data:
|
||||
s = s.replace(
|
||||
'{=' + k + '}',
|
||||
test_eq(
|
||||
self.data[k + '_found'], self.data[k + '_expected']
|
||||
),
|
||||
)
|
||||
s = s.replace(
|
||||
'{=cities}',
|
||||
test_eq(self.data['good_cities'], self.data['total_cities']),
|
||||
)
|
||||
s = s.replace(
|
||||
'{=entrances}', test_eq(self.data['unused_entrances'], 0)
|
||||
)
|
||||
for k in ('errors', 'warnings', 'notices'):
|
||||
s = s.replace('{=' + k + '}', test_eq(self.data['num_' + k], 0))
|
||||
return s
|
||||
|
||||
|
||||
def tmpl(s, data=None, **kwargs):
|
||||
if data:
|
||||
s = data.format(s)
|
||||
if kwargs:
|
||||
for k, v in kwargs.items():
|
||||
if v is not None:
|
||||
s = s.replace('{' + k + '}', str(v))
|
||||
s = re.sub(
|
||||
r'\{\?' + k + r'\}(.+?)\{end\}',
|
||||
r'\1' if v else '',
|
||||
s,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
s = s.replace('{date}', date)
|
||||
google_url = (
|
||||
'https://docs.google.com/spreadsheets/d/{}/edit?usp=sharing'.format(
|
||||
SPREADSHEET_ID
|
||||
)
|
||||
)
|
||||
s = s.replace('{google}', google_url)
|
||||
return s
|
||||
|
||||
|
||||
EXPAND_OSM_TYPE = {'n': 'node', 'w': 'way', 'r': 'relation'}
|
||||
RE_SHORT = re.compile(r'\b([nwr])(\d+)\b')
|
||||
RE_FULL = re.compile(r'\b(node|way|relation) (\d+)\b')
|
||||
RE_COORDS = re.compile(r'\((-?\d+\.\d+), (-?\d+\.\d+)\)')
|
||||
|
||||
|
||||
def osm_links(s):
|
||||
"""Converts object mentions to HTML links."""
|
||||
|
||||
def link(m):
|
||||
return '<a href="https://www.openstreetmap.org/{}/{}">{}</a>'.format(
|
||||
EXPAND_OSM_TYPE[m.group(1)[0]], m.group(2), m.group(0)
|
||||
)
|
||||
|
||||
s = RE_SHORT.sub(link, s)
|
||||
s = RE_FULL.sub(link, s)
|
||||
s = RE_COORDS.sub(
|
||||
r'(<a href="https://www.openstreetmap.org/search?query=\2%2C\1#map=18/\2/\1">pos</a>)',
|
||||
s,
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
def esc(s):
|
||||
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('Reads a log from subway validator and prepares HTML files.')
|
||||
print(
|
||||
'Usage: {} <validation.log> [<target_directory>]'.format(sys.argv[0])
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||
data = {c['name']: CityData(c) for c in json.load(f)}
|
||||
|
||||
countries = {}
|
||||
continents = {}
|
||||
c_by_c = {} # continent → set of countries
|
||||
for c in data.values():
|
||||
countries[c.country] = c + countries.get(c.country, CityData())
|
||||
continents[c.continent] = c + continents.get(c.continent, CityData())
|
||||
if c.continent not in c_by_c:
|
||||
c_by_c[c.continent] = set()
|
||||
c_by_c[c.continent].add(c.country)
|
||||
world = sum(continents.values(), CityData())
|
||||
|
||||
overground = 'traml_expected' in next(iter(data.values())).data
|
||||
date = datetime.datetime.utcnow().strftime('%d.%m.%Y %H:%M UTC')
|
||||
path = '.' if len(sys.argv) < 3 else sys.argv[2]
|
||||
index = open(os.path.join(path, 'index.html'), 'w', encoding='utf-8')
|
||||
index.write(tmpl(INDEX_HEADER, world))
|
||||
|
||||
for continent in sorted(continents.keys()):
|
||||
content = ''
|
||||
for country in sorted(c_by_c[continent]):
|
||||
country_file_name = country.lower().replace(' ', '-') + '.html'
|
||||
content += tmpl(
|
||||
INDEX_COUNTRY,
|
||||
countries[country],
|
||||
file=country_file_name,
|
||||
country=country,
|
||||
continent=continent,
|
||||
)
|
||||
country_file = open(
|
||||
os.path.join(path, country_file_name), 'w', encoding='utf-8'
|
||||
)
|
||||
country_file.write(
|
||||
tmpl(
|
||||
COUNTRY_HEADER,
|
||||
country=country,
|
||||
continent=continent,
|
||||
overground=overground,
|
||||
subways=not overground,
|
||||
)
|
||||
)
|
||||
for name, city in sorted(data.items()):
|
||||
if city.country == country:
|
||||
file_base = os.path.join(path, city.slug)
|
||||
yaml_file = (
|
||||
city.slug + '.yaml'
|
||||
if os.path.exists(file_base + '.yaml')
|
||||
else None
|
||||
)
|
||||
json_file = (
|
||||
city.slug + '.geojson'
|
||||
if os.path.exists(file_base + '.geojson')
|
||||
else None
|
||||
)
|
||||
errors = '<br>'.join([osm_links(esc(e)) for e in city.errors])
|
||||
warnings = '<br>'.join([osm_links(esc(w)) for w in city.warnings])
|
||||
notices = '<br>'.join([osm_links(esc(n)) for n in city.notices])
|
||||
country_file.write(
|
||||
tmpl(
|
||||
COUNTRY_CITY,
|
||||
city,
|
||||
city=name,
|
||||
country=country,
|
||||
continent=continent,
|
||||
yaml=yaml_file,
|
||||
json=json_file,
|
||||
subways=not overground,
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
notices=notices,
|
||||
overground=overground,
|
||||
)
|
||||
)
|
||||
country_file.write(
|
||||
tmpl(COUNTRY_FOOTER, country=country, continent=continent)
|
||||
)
|
||||
country_file.close()
|
||||
index.write(
|
||||
tmpl(
|
||||
INDEX_CONTINENT,
|
||||
continents[continent],
|
||||
content=content,
|
||||
continent=continent,
|
||||
)
|
||||
)
|
||||
|
||||
index.write(tmpl(INDEX_FOOTER))
|
||||
index.close()
|
Loading…
Add table
Reference in a new issue