Skip to content

Commit fdea644

Browse files
authored
Add OSM elevation to point POIs (#619)
* Add OSM elevation to point POIs Natural peak features are usually OSM point features, but the POI layer only copied the OSM ele tag while building the named-polygon point-on-surface output. That meant point peaks were emitted without the elevation attribute even when the source feature had ele set. Move the elevation assignment into the common POI attribute chain so all OSM POIs can carry ele through to the output feature. This preserves the existing named-polygon behavior and lets Planetiler omit the attribute naturally when ele is absent. Add a regression test covering natural=peak with ele to make sure peak POIs expose elevation. Verification: ran PoisTest, full mvn package, spotless:check, git diff --check, and before/after Saarland PMTiles scans. The Saarland z15 scan changed from 0/489 peak features with elevation before the fix to 425/489 after the fix; the remaining peaks lack OSM ele in the source. AI assistance: this change was prepared with help from OpenAI Codex. * Parse POI elevation as numeric OSM ele represents elevation in metres, so expose POI elevation as a numeric tile value instead of passing through raw tag text. This applies both to polygon-derived POIs that already carried elevation and to point POIs added by the previous change. Add a conservative parser that accepts clean signed decimal values, including an optional trailing m suffix with or without whitespace, and emits rounded integer metres. Omit elevation when ele contains other units or free text. Update POI tests to assert integer elevation for peak and named polygon output and cover decimal, metre-suffixed, and invalid values. AI assistance: This change was prepared with help from OpenAI Codex.
1 parent a9fa48f commit fdea644

2 files changed

Lines changed: 84 additions & 5 deletions

File tree

tiles/src/main/java/com/protomaps/basemap/layers/Pois.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@
2525
import com.protomaps.basemap.names.OsmNames;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.regex.Pattern;
2829

2930
@SuppressWarnings("java:S1192")
3031
public class Pois implements ForwardingProfile.LayerPostProcessor {
3132

33+
// Matches numeric metre values with an optional trailing "m" suffix, e.g. "569", "569.5", "569 m", or "569m".
34+
private static final Pattern ELEVATION_PATTERN =
35+
Pattern.compile("^([+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+))(?:\\s*m)?$");
36+
3237
private Map<String, int[][]> qrankGrading = Map.of(
3338
"station", new int[][]{{10, 50000}, {12, 20000}, {13, 10000}},
3439
"aerodrome", new int[][]{{10, 50000}, {12, 20000}, {13, 5000}, {14, 2500}},
@@ -46,6 +51,25 @@ public Pois(QrankDb qrankDb) {
4651

4752
public static final String LAYER_NAME = "pois";
4853

54+
static Integer parseElevation(String elevation) {
55+
if (elevation == null) {
56+
return null;
57+
}
58+
59+
var matcher = ELEVATION_PATTERN.matcher(elevation.trim());
60+
if (!matcher.matches()) {
61+
return null;
62+
}
63+
64+
var parsed = parseDoubleOrNull(matcher.group(1));
65+
if (parsed == null) {
66+
return null;
67+
}
68+
69+
var rounded = Math.round(parsed);
70+
return rounded >= Integer.MIN_VALUE && rounded <= Integer.MAX_VALUE ? (int) rounded : null;
71+
}
72+
4973
private static final Expression WITH_OPERATOR_USFS = with("operator", "United States Forest Service",
5074
"US Forest Service", "U.S. Forest Service", "USDA Forest Service", "United States Department of Agriculture",
5175
"US National Forest Service", "United State Forest Service", "U.S. National Forest Service");
@@ -501,9 +525,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) {
501525

502526
// Assign outputFeature
503527
if (hasNamedPolygon) {
504-
outputFeature = features.pointOnSurface(this.name())
505-
//.setAttr("area_debug", wayArea) // DEBUG
506-
.setAttr("elevation", sf.getString("ele"));
528+
outputFeature = features.pointOnSurface(this.name());
507529
} else if (sf.isPoint()) {
508530
outputFeature = features.point(this.name());
509531
} else {
@@ -525,7 +547,8 @@ public void processOsm(SourceFeature sf, FeatureCollector features) {
525547
.setZoomRange(Math.min(minZoom, 15), 15)
526548
// Core OSM tags for different kinds of places
527549
// Special airport only tag (to indicate if it's an airport with regular commercial flights)
528-
.setAttr("iata", sf.getString("iata"));
550+
.setAttr("iata", sf.getString("iata"))
551+
.setAttr("elevation", parseElevation(sf.getString("ele")));
529552

530553
// Core Tilezen schema properties
531554
if (!kindDetail.equals("pm:undefined"))

tiles/src/test/java/com/protomaps/basemap/layers/PoisTest.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.protomaps.basemap.layers;
22

33
import static com.onthegomap.planetiler.TestUtils.*;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
45

56
import com.onthegomap.planetiler.reader.SimpleFeature;
67
import java.util.HashMap;
@@ -197,6 +198,61 @@ void zoom_14_peak() {
197198
)));
198199
}
199200

201+
@Test
202+
void peakElevation() {
203+
assertFeatures(14,
204+
List.of(Map.of("kind", "peak", "min_zoom", 14, "elevation", 569)),
205+
process(SimpleFeature.create(
206+
newPoint(1, 1),
207+
new HashMap<>(Map.of("natural", "peak", "ele", "569")),
208+
"osm", null, 0
209+
)));
210+
}
211+
212+
@Test
213+
void peakElevationDecimal() {
214+
assertFeatures(14,
215+
List.of(Map.of("kind", "peak", "min_zoom", 14, "elevation", 570)),
216+
process(SimpleFeature.create(
217+
newPoint(1, 1),
218+
new HashMap<>(Map.of("natural", "peak", "ele", "569.5")),
219+
"osm", null, 0
220+
)));
221+
}
222+
223+
@Test
224+
void peakElevationMeterSuffix() {
225+
assertFeatures(14,
226+
List.of(Map.of("kind", "peak", "min_zoom", 14, "elevation", 569)),
227+
process(SimpleFeature.create(
228+
newPoint(1, 1),
229+
new HashMap<>(Map.of("natural", "peak", "ele", "569 m")),
230+
"osm", null, 0
231+
)));
232+
}
233+
234+
@Test
235+
void peakElevationMeterSuffixWithoutWhitespace() {
236+
assertFeatures(14,
237+
List.of(Map.of("kind", "peak", "min_zoom", 14, "elevation", 569)),
238+
process(SimpleFeature.create(
239+
newPoint(1, 1),
240+
new HashMap<>(Map.of("natural", "peak", "ele", "569m")),
241+
"osm", null, 0
242+
)));
243+
}
244+
245+
@Test
246+
void peakElevationNonMeterUnitOmitted() {
247+
var features = process(SimpleFeature.create(
248+
newPoint(1, 1),
249+
new HashMap<>(Map.of("natural", "peak", "ele", "569 ft")),
250+
"osm", null, 0
251+
));
252+
253+
assertFalse(toMap(features.iterator().next(), 14).containsKey("elevation"));
254+
}
255+
200256
@Test
201257
void zoom_14_golfCourse() {
202258
assertFeatures(14,
@@ -629,7 +685,7 @@ void zoom_8_forest_largeArea() {
629685
@Test
630686
void zoom_12_tallBuilding_100mHeight() {
631687
assertFeatures(12,
632-
List.of(Map.of("kind", "office", "min_zoom", 12, "name", "Skyscraper", "elevation", "100")),
688+
List.of(Map.of("kind", "office", "min_zoom", 12, "name", "Skyscraper", "elevation", 100)),
633689
process(SimpleFeature.create(
634690
AREA_12K_SQ_M,
635691
new HashMap<>(Map.of(

0 commit comments

Comments
 (0)