Skip to content

Instantly share code, notes, and snippets.

@planemad
Created March 19, 2020 20:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save planemad/c8bf709ce4ccf3aadfa3d1b94f81960d to your computer and use it in GitHub Desktop.
Save planemad/c8bf709ce4ccf3aadfa3d1b94f81960d to your computer and use it in GitHub Desktop.
Create a Mapbox vector tileset using OpenStreetMap data
#!/usr/bin/env bash
set -e -u -o pipefail
set -x
#
# Step 0: Setup
#
echo "Install Dependencies (Ubuntu)"
apt-get update
apt-get install software-properties-common
add-apt-repository ppa:ubuntugis/ubuntugis-unstable
apt-get install tmux vim osmium-tool gdal-bin python3-gdal python3-numpy
echo "Install Mapbox Tileset CLI"
git clone https://github.com/mapbox/tilesets-cli.git
cd tilesets-cli
mkvirtualenv tilesets-cli
pip install .
cd ..
mkdir planet-maps
cd planet-maps
#
# Step 1: Create a data extract from OpenStreetMap that contains all the required features
#
echo "# Download OSM PBF"
# Source: https://wiki.openstreetmap.org/wiki/Planet.osm#Planet.osm_mirrors
OSM_PBF="https://ftpmirror.your.org/pub/openstreetmap/pbf/planet-latest.osm.pbf"
# OSM_PBF="https://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf"
wget -O ./planet.osm.pbf $OSM_PBF
# Create a filtered extract of the planet file that has data required for the tileset
# Use osmium tags-filter to set the geometry types and OSM tags of interest
# https://wiki.openstreetmap.org/wiki/OpenRailwayMap/Tagging
extract="railways"
echo "# Creating ${extract} planet extract"
mkdir ${extract} -p
osmium tags-filter ./planet.osm.pbf nwr/railway nwr/train=yes w/historic=railway w/historic:railway w/abandoned:railway w/disused:railway w/construction:railway w/proposed:railway -o ${extract}/planet.osm.pbf
echo "# Test ${extract} extract"
ogrinfo ${extract}/planet.osm.pbf -so
# Create data layers from the planet extract
# All OSM tags can be accessed using hstore_get_value(all_tags, 'key')
echo "# Create feature layers"
# If specific tags are defined in osmconf.ini they can be used as a column directly for easy filtering
vi ./osmconf.ini
# https://wiki.openstreetmap.org/wiki/Tag:railway%3Dstation
ogr2ogr -f GeoJSONSeq ${extract}/railway-stations.geojsonl ${extract}/planet.osm.pbf \
-oo CONFIG_FILE=./osmconf.ini \
-dialect sqlite \
-sql "SELECT
coalesce(name_en,name) as name_en,
coalesce(hstore_get_value(all_tags, 'railway:ref'),hstore_get_value(all_tags, 'ref')) AS ref,
hstore_get_value(all_tags, 'wikidata') as qid,
railway,
station,
CASE WHEN platforms IS NOT NULL THEN CAST(hstore_get_value(all_tags, 'platforms') AS INT) ELSE 1 END as platforms,
network, operator,
all_tags,
CASE WHEN platforms IS NOT NULL THEN 100-CAST(hstore_get_value(all_tags, 'platforms') AS INT) ELSE 100 END as z_order,
geometry
FROM points WHERE
railway IN ('station','halt')
OR public_transport='station'
UNION ALL
SELECT
coalesce(name_en,name) as name_en,
coalesce(hstore_get_value(all_tags, 'railway:ref'),hstore_get_value(all_tags, 'ref')) AS ref,
hstore_get_value(all_tags, 'wikidata') as qid,
railway,
station,
CASE WHEN platforms IS NOT NULL THEN CAST(hstore_get_value(all_tags, 'platforms') AS INT) ELSE 1 END as platforms,
network, operator,
all_tags,
CASE WHEN platforms IS NOT NULL THEN 100-CAST(hstore_get_value(all_tags, 'platforms') AS INT) ELSE 100 END as z_order,
ST_PointOnSurface(geometry)
FROM multipolygons WHERE
railway IN ('station','halt')
OR public_transport='station'"
# https://wiki.openstreetmap.org/wiki/Railways
# https://wiki.openstreetmap.org/wiki/OpenRailwayMap/Tagging#Tracks
ogr2ogr -f GeoJSONSeq ${extract}/railway-lines.geojsonl ${extract}/planet.osm.pbf \
-oo CONFIG_FILE=./osmconf.ini \
-dialect sqlite \
-sql "SELECT
coalesce(name_en,name) as name_en,
coalesce(hstore_get_value(all_tags, 'railway:ref'),hstore_get_value(all_tags, 'ref')) AS ref,
railway,
usage,
CASE WHEN tracks IS NOT NULL THEN CAST(tracks AS INT) ELSE 0 END AS tracks,
service,
CASE WHEN maxspeed IS NOT NULL THEN
ROUND(
CAST(maxspeed AS REAL) *
CASE WHEN maxspeed LIKE '%mph' THEN 1.61 ELSE 1 END
,0)
ELSE 0 END AS maxspeed,
electrified,
gauge,
bridge,
tunnel,
network, operator,
geometry
FROM lines WHERE
railway IN ('rail','subway','light_rail','tram','narrow_gauge','monorail','miniature','preserved','funicular','construction','proposed','disused','abandoned','historic','razed')"
#
# Step 2: Use extracted data to create a Mapbox vector tile source
# https://docs.mapbox.com/help/tutorials/get-started-tilesets-api-and-cli/
#
# Signup for an account at mapbox.com and create a new secret token with tileset scope enabled
#
export MAPBOX_ACCESS_TOKEN=<token>
export mapbox_username=planemad
echo "Add tileset source"
# tilesets delete-source ${mapbox_username} planet-railway-stations
tilesets add-source ${mapbox_username} planet-railway-stations ${extract}/railway-stations.geojsonl
# tilesets delete-source ${mapbox_username} planet-railway-lines
tilesets add-source ${mapbox_username} planet-railway-lines ${extract}/railway-lines.geojsonl
echo "Verify sources"
tilesets list-sources ${mapbox_username}
#
# Step 3: Create a vector tileset recipe to convert each data source into vector tileset
#
# This step might require multiple iterations to optimize how the features are packed into the tiles
#
# Resources:
# https://docs.mapbox.com/help/troubleshooting/tileset-recipe-reference/
# https://docs.mapbox.com/help/troubleshooting/tileset-recipe-examples
#
tileset_id="railway-stations"
tileset_name="Railway Stations"
tileset_recipe='
{
"version": 1,
"layers": {
"stations": {
"source": "mapbox://tileset-source/planemad/planet-railway-stations",
"minzoom": 0,
"maxzoom": 15,
"tiles": {
"limit": [
[ "highest_where_in_distance", true, 50000, "platform" ]
]
},
"features" : {
"attributes" : {
"allowed_output" : [
"name_en", "ref", "railway", "station", "platforms"
]
}
}
}
}
}
'
echo $tileset_recipe > ${extract}/${tileset_id}-recipe.json
tileset_id="railway-lines"
tileset_name="Railway Lines"
tileset_recipe='
{
"version": 1,
"layers": {
"railways": {
"source": "mapbox://tileset-source/planemad/planet-railway-lines",
"minzoom": 1,
"maxzoom": 11,
"features" : {
"filter" : [
"case",
[
"all",
["<", ["zoom"], 5],
[ "==", [ "get", "service"], null ]
],
true,
true
],
"attributes" : {
"allowed_output" : [
"railway", "usage", "service", "electrified", "maxspeed", "tracks", "gauge"
]
}
},
"tiles": {
"union": [
{
"where": [ "==", [ "get", "service"], null ],
"group_by": ["railway","usage","electrified","tracks","gauge","service"],
"maintain_direction": false,
"aggregate": { "maxspeed": "max" }
},
{
"where": [ "!=", [ "get", "service"], null ],
"group_by": ["railway","usage","electrified","gauge","service"],
"maintain_direction": false
}
]
}
}
}
}
'
echo $tileset_recipe > ${extract}/${tileset_id}-recipe.json
exit 0
#
# Step 4: Upload recipe and publish tileset
# Needs to be run for each tileset
#
# Create tileset with recipe on first run
echo "Create tileset and add recipe"
tilesets create ${mapbox_username}.${tileset_id} --recipe ${extract}/${tileset_id}-recipe.json --name "${tileset_name}"
# Update recipe for subsequent recipe updates
echo "Update and publish tileset"
tilesets update-recipe ${mapbox_username}.${tileset_id} ${extract}/${tileset_id}-recipe.json
# Start a job to refresh the tileset with the latest data and recipe
tilesets publish ${mapbox_username}.${tileset_id}
tilesets status ${mapbox_username}.${tileset_id}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment