package com.onthegomap.planetiler.examples; import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.FeatureMerge; import com.onthegomap.planetiler.Planetiler; import com.onthegomap.planetiler.Profile; import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.config.Arguments; import com.onthegomap.planetiler.reader.SourceFeature; import com.onthegomap.planetiler.reader.osm.OsmElement; import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; import java.nio.file.Path; import java.util.List; /** * Builds a map of bike routes from ways contained in OpenStreetMap relations tagged with * route=bicycle. *

* To run this example: *

    *
  1. Download a .osm.pbf extract (see Geofabrik download site)
  2. *
  3. then build the examples: {@code mvn clean package}
  4. *
  5. then run this example: * {@code java -cp target/*-with-deps.jar com.onthegomap.planetiler.examples.BikeRouteOverlay osm_path="path/to/data.osm.pbf" mbtiles="data/output.mbtiles"}
  6. *
  7. then run the demo tileserver: {@code tileserver-gl-light data/bikeroutes.mbtiles}
  8. *
  9. and view the output at localhost:8080
  10. *
*/ public class BikeRouteOverlay implements Profile { /* * The processing happens in 3 steps: * 1. On the first pass through the input file, store relevant information from OSM bike route relations * 2. On the second pass, emit linestrings for each OSM way contained in one of those relations * 3. Before storing each finished tile, Merge linestrings in each tile with the same tags and touching endpoints */ /* * Step 1) * * Planetiler processes the .osm.pbf input file in two passes. The first pass stores node locations, and invokes * preprocessOsmRelation for reach relation and stores information the profile needs during the second pass when we * emit map feature for ways contained in that relation. */ // Minimal container for data we extract from OSM bicycle route relations. This is held in RAM so keep it small. private record RouteRelationInfo( // OSM ID of the relation (required): @Override long id, // Values for tags extracted from the OSM relation: String name, String ref, String route, String network ) implements OsmRelationInfo {} @Override public List preprocessOsmRelation(OsmElement.Relation relation) { // If this is a "route" relation ... if (relation.hasTag("type", "route")) { // where route=bicycle or route=mtb ... if (relation.hasTag("route", "mtb", "bicycle")) { // then store a RouteRelationInfo instance with tags we'll need later return List.of(new RouteRelationInfo( relation.id(), relation.getString("name"), relation.getString("ref"), relation.getString("route"), // except map network abbreviation to a human-readable value switch (relation.getString("network", "")) { case "icn" -> "international"; case "ncn" -> "national"; case "rcn" -> "regional"; case "lcn" -> "local"; default -> "other"; } )); } } // for any other relation, return null to ignore return null; } /* * Step 2) * * On the second pass through the input .osm.pbf file, for each way in a relation that we stored data about, emit a * linestring map feature with attributes derived from the relation. */ @Override public void processFeature(SourceFeature sourceFeature, FeatureCollector features) { // ignore nodes and ways that should only be treated as polygons if (sourceFeature.canBeLine()) { // get all the RouteRelationInfo instances we returned from preprocessOsmRelation that // this way belongs to for (var routeInfo : sourceFeature.relationInfo(RouteRelationInfo.class)) { // (routeInfo.role() also has the "role" of this relation member if needed) RouteRelationInfo relation = routeInfo.relation(); // Break the output into layers named: "{bicycle,route}-route-{international,national,regional,local,other}" String layerName = relation.route + "-route-" + relation.network; features.line(layerName) .setAttr("name", relation.name) .setAttr("ref", relation.ref) .setZoomRange(0, 14) // don't filter out short line segments even at low zooms because the next step needs them // to merge lines with the same tags where the endpoints are touching .setMinPixelSize(0); } } } /* * Step 3) * * Before writing tiles to the output, first merge linestrings where the endpoints are touching that share the same * tags to improve line and text rendering in clients. */ @Override public List postProcessLayerFeatures(String layer, int zoom, List items) { // FeatureMerge has several utilities for merging geometries in a layer that share the same tags. // `mergeLineStrings` combines lines with the same tags where the endpoints touch. // Tiles are 256x256 pixels and all FeatureMerge operations work in tile pixel coordinates. return FeatureMerge.mergeLineStrings(items, 0.5, // after merging, remove lines that are still less than 0.5px long 0.1, // simplify output linestrings using a 0.1px tolerance 4 // remove any detail more than 4px outside the tile boundary ); } /* * Hooks to override metadata values in the output mbtiles file. Only name is required, the rest are optional. Bounds, * center, minzoom, maxzoom are set automatically based on input data and planetiler config. * * See: https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md#metadata) */ @Override public String name() { return "Bike Paths Overlay"; } @Override public String description() { return "An example overlay showing bicycle routes"; } @Override public boolean isOverlay() { return true; // when true sets type=overlay, otherwise type=baselayer } /* * Any time you use OpenStreetMap data, you must ensure clients display the following copyright. Most clients will * display this automatically if you populate it in the attribution metadata in the mbtiles file: */ @Override public String attribution() { return """ © OpenStreetMap contributors """.trim(); } /* * Main entrypoint for this example program */ public static void main(String[] args) throws Exception { run(Arguments.fromArgsOrConfigFile(args)); } static void run(Arguments args) throws Exception { String area = args.getString("area", "geofabrik area to download", "monaco"); // Planetiler is a convenience wrapper around the lower-level API for the most common use-cases. // See ToiletsOverlayLowLevelApi for an example using the lower-level API Planetiler.create(args) .setProfile(new BikeRouteOverlay()) // override this default with osm_path="path/to/data.osm.pbf" .addOsmSource("osm", Path.of("data", "sources", area + ".osm.pbf"), "geofabrik:" + area) // override this default with mbtiles="path/to/output.mbtiles" .overwriteOutput(Path.of("data", "bikeroutes.mbtiles")) .run(); } }