Skip to content

Commit

Permalink
Start extracting a cycle path layer from OSM
Browse files Browse the repository at this point in the history
  • Loading branch information
dabreegster committed Aug 17, 2023
1 parent a185f6f commit f92e077
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 2 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ ATIP can display extra contextual layers:
- Points of interest, like schools, hospitals, sports centres, etc
- Roads with bus lanes and bus routes
- Cycle parking
- Existing cycle paths
- the [Major Road Network](https://www.data.gov.uk/dataset/95f58bfa-13d6-4657-9d6f-020589498cfd/major-road-network)
- Boundaries
- Parliament constituency boundaries, from [OS Boundary-Line](https://www.ordnancesurvey.co.uk/products/boundary-line)
Expand All @@ -107,7 +108,7 @@ is a single GeoJSON file if it's small enough, or
To run this:

1. Get `england-latest.osm.pbf` from Geofabrik. The `split_uk_osm.sh` script above does this.
2. Run `cd layers; ./generate_layers.py --osm_input=../england-latest.osm.pbf --schools --hospitals --mrn --parliamentary_constituencies --combined_authorities --local_authority_districts --local_planning_authorities --sports_spaces --railway_stations --bus_routes --cycle_parking`
2. Run `cd layers; ./generate_layers.py --osm_input=../england-latest.osm.pbf --schools --hospitals --mrn --parliamentary_constituencies --combined_authorities --local_authority_districts --local_planning_authorities --sports_spaces --railway_stations --bus_routes --cycle_parking --cycle_paths`
3. Pick an arbitrary version number, and upload the files: `for x in output/*; do aws s3 cp --dry $x s3://atip.uk/layers/v1/; done`

If you're rerunning the script for the same output, you may need to manually delete the output files from the previous run.
Expand All @@ -116,6 +117,8 @@ You can debug a PMTiles file using <https://protomaps.github.io/PMTiles>.

There's a manual step required to generate `--wards`, `--census_output_areas`, and `--imd`. See the comment in the code.

For `--cycle_paths`, you'll need about 20GB of RAM, until we switch to a streaming JSON parser.

### One-time cloud setup for PMTiles

Currently we're using S3 and Cloudfront to host files generated by this repo.
Expand Down
149 changes: 149 additions & 0 deletions layers/cycle_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from utils import *


def makeCyclePaths(osm_input):
if not osm_input:
raise Exception("You must specify --osm_input")

filename = "cycle_paths"
tmp = f"tmp_{filename}"
ensureEmptyTempDirectoryExists(tmp)

run(
[
"osmium",
"tags-filter",
osm_input,
"w/highway",
"-o",
f"{tmp}/cycle_paths.osm.pbf",
]
)
gjPath = f"{tmp}/cycle_paths.geojson"
convertPbfToGeoJson(
f"{tmp}/cycle_paths.osm.pbf", gjPath, "linestring", includeOsmID=True
)

cleanUpGeojson(
gjPath, getProps, filterFeatures=lambda f: getProps(f["properties"]) != None
)

convertGeoJsonToPmtiles(f"{tmp}/cycle_paths.geojson", "output/cycle_paths.pmtiles")


# If this has some kind of cycle-path, returns a dictionary with:
#
# - kind = track | lane | shared_use_segregated | shared_use_unsegregated
# - osm_id
# - direction = one-way | two-way | unknown
# (For cyclist traffic; unrelated to whether the path is a contraflow on a one-way street)
# - width = number in meters | unknown
#
# If there's no cycle path, returns None
def getProps(props):
if props.get("highway") == "cycleway":
kind = "track"
# Is it shared-use? Note "foot" may be missing (https://www.openstreetmap.org/way/849651126)
if props.get("foot") in ["yes", "designated"] or props.get("segregated"):
kind = getSharedUseKind(props)

return {
"kind": kind,
"osm_id": props["@id"],
"width": getSeparateWayWidth(props),
"direction": getSeparateWayDirection(props),
}
elif hasLane(props):
return {
"kind": "lane",
"osm_id": props["@id"],
"width": getLaneWidth(props),
"direction": getLaneDirection(props),
}
elif props["highway"] in ["footway", "pedestrian", "path", "track"]:
if props.get("bicycle") not in ["yes", "designated"]:
return None

return {
"kind": getSharedUseKind(props),
"osm_id": props["@id"],
"width": getSeparateWayWidth(props),
"direction": getSeparateWayDirection(props),
}
else:
return None


def getSeparateWayWidth(props):
# TODO Handle suffixes / units
if props.get("width"):
return props["width"]
if props.get("est_width"):
return props["est_width"]
return "unknown"


def getSeparateWayDirection(props):
oneway = props.get("oneway")
if oneway == "yes":
return "one-way"
elif oneway == "no":
return "two-way"
else:
return "unknown"


def valueIndicatesLane(value):
return value and value not in ["no", "separate", "share_busway"]


def hasLane(props):
# TODO Handle bicycle:lanes
for suffix in ["", ":left", ":right", ":both"]:
if valueIndicatesLane(props.get("cycleway" + suffix)):
return True
return False


# If there are two lanes, the result could capture one or both of the lanes
def getLaneWidth(props):
for suffix in ["", ":left", ":right", ":both"]:
for width in [":width", ":est_width"]:
value = props.get("cycleway" + suffix + width)
if value:
return value
return "unknown"


def getLaneDirection(props):
if valueIndicatesLane(props.get("cycleway:both")):
return "two-way"
if valueIndicatesLane(props.get("cycleway:left")) and valueIndicatesLane(
props.get("cycleway:right")
):
return "two-way"
if props.get("oneway:bicycle") == "no":
# TODO On one-way roads, this might just mean cyclists can travel both
# directions, but there's only a dedicated lane in one
return "two-way"
if (
props.get("cycleway:left:oneway") == "no"
or props.get("cycleway:right:oneway") == "no"
):
return "two-way"

# TODO opposite_track
# TODO cycleway=* + oneway=yes
# TODO bicycle:lanes
return "unknown"


def getSharedUseKind(props):
segregated = props.get("segregated")
if segregated == "yes":
return "shared_use_segregated"
elif segregated == "no":
return "shared_use_unsegregated"
else:
# Pessimistically assume unsegregated if unknown
return "shared_use_unsegregated"
6 changes: 6 additions & 0 deletions layers/generate_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from utils import *
import census
import boundaries
import cycle_paths
import osm


Expand Down Expand Up @@ -38,6 +39,7 @@ def main():
help="Path to the manually downloaded Indices_of_Multiple_Deprivation_(IMD)_2019.geojson",
type=str,
)
parser.add_argument("--cycle_paths", action="store_true")
# Inputs required for some outputs
parser.add_argument(
"-i", "--osm_input", help="Path to england-latest.osm.pbf file", type=str
Expand Down Expand Up @@ -109,6 +111,10 @@ def main():
made_any = True
census.makeIMD(args.imd)

if args.cycle_paths:
made_any = True
cycle_paths.makeCyclePaths(args.osm_input)

if not made_any:
print(
"Didn't create anything. Call with --help to see possible layers that can be created"
Expand Down
17 changes: 17 additions & 0 deletions layers/osmium_with_ids.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"attributes": {
"type": false,
"id": true,
"version": false,
"changeset": false,
"timestamp": false,
"uid": false,
"user": false,
"way_nodes": false,
},
"format_options": {},
"linear_tags": true,
"area_tags": true,
"exclude_tags": [],
"include_tags": [],
}
10 changes: 9 additions & 1 deletion layers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ def ensureEmptyTempDirectoryExists(directoryName):
os.makedirs(directoryName, exist_ok=True)


def convertPbfToGeoJson(pbfPath, geojsonPath, geometryType):
def convertPbfToGeoJson(pbfPath, geojsonPath, geometryType, includeOsmID=False):
config = []
if includeOsmID:
# Created by `osmium export --print-default-config` and changing `id`
# TODO We can do this with a CLI flag with newer osmium, but it's not
# easy to install on Ubuntu 20
config = ["--config", "osmium_with_ids.cfg"]

run(
[
"osmium",
Expand All @@ -24,6 +31,7 @@ def convertPbfToGeoJson(pbfPath, geojsonPath, geometryType):
"-o",
geojsonPath,
]
+ config
)


Expand Down

0 comments on commit f92e077

Please sign in to comment.