Color-code, cluster and add icons to POI waypoints
CI / check (push) Successful in 47s
CI / deploy (push) Successful in 16s

A park4night/OsmAnd POI export (e.g. a p4n2gpx area export) tags every
waypoint with a category and a per-category color/icon in its <extensions>
block, all of which was previously thrown away — every waypoint rendered as
the same plain red dot, unreadable once a file has a few thousand of them.

Maptool::Gpx now captures category/color/icon (waypoint_style), matched by
namespace URI rather than local element name: a p4n2gpx file carries both
<osmand:icon> and <locus:icon> side by side, and REXML strips prefixes from
Element#name, so both report as just "icon" — matching on that alone was
silently picking the (useless) Locus drawable path instead of the OsmAnd
category key. Regression test added for this.

On the map: waypoints are colored by category (falling back to the plain
track color for anything uncategorized), a small hand-drawn icon set
(public/js/icons.js's POI_ICONS, rasterized to SDF sprites at runtime) draws
a glyph on top for the categories it covers, the points source is clustered
so a dense export collapses into count bubbles instead of a solid blob, and
a legend in the side panel lists whichever categories are currently visible.

Splitting each file's GeoJSON into a lines source and a points source
(rather than one mixed source with a geometry-type filter per layer) was
needed for clustering, since MapLibre's cluster support only understands a
source made entirely of points.
This commit is contained in:
randy committed 2026-09-07 16:18:06 +02:00
1 parent 708c08259b
commit 0b18dc018a
8 files changed
+434 -35

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="test">
<wpt lat="48.1" lon="17.2">
<name>Free spot</name>
<type>Free motorhome area</type>
<extensions>
<osmand:color xmlns:osmand="https://osmand.net">#2E7D32</osmand:color>
<osmand:icon xmlns:osmand="https://osmand.net">tourism_camp_site</osmand:icon>
</extensions>
</wpt>
<wpt lat="48.101" lon="17.201">
<name>Plain waypoint</name>
</wpt>
<wpt lat="48.102" lon="17.202">
<name>p4n2gpx export</name>
<type>Daily parking only</type>
<extensions>
<osmand:color xmlns:osmand="https://osmand.net">#546E7A</osmand:color>
<osmand:icon xmlns:osmand="https://osmand.net">amenity_parking</osmand:icon>
<locus:icon xmlns:locus="http://www.locusmap.eu">res/drawable-hdpi/ic_parking_small.png</locus:icon>
</extensions>
</wpt>
</gpx>
+33
View File
@@ -41,6 +41,39 @@ class GpxTest < Minitest::Test
assert_equal [17.2, 48.1], [profile[0]["lon"], profile[0]["lat"]]
end
def test_waypoint_captures_category_and_color_from_poi_extensions
geojson = Maptool::Gpx.to_geojson(fixture_path("sample_poi.gpx"))
free_spot = geojson["features"].find { |f| f["properties"]["name"] == "Free spot" }
assert_equal "Free motorhome area", free_spot["properties"]["category"]
assert_equal "#2E7D32", free_spot["properties"]["color"]
assert_equal "tourism_camp_site", free_spot["properties"]["icon"]
end
# Regression test for a real bug: a p4n2gpx export's <extensions> carries
# both an <osmand:icon> (a category key this app can render) and a
# <locus:icon> (a Locus-app drawable path) side by side. REXML strips
# namespace prefixes from Element#name, so both elements report the same
# local name "icon" — matching on that alone picked whichever happened to
# come last in the file (locus, since it followed osmand), leaving every
# waypoint's icon set to a meaningless drawable path that never matched
# any category. Must match by namespace URI instead.
def test_waypoint_icon_ignores_non_osmand_extensions_with_the_same_local_name
geojson = Maptool::Gpx.to_geojson(fixture_path("sample_poi.gpx"))
export = geojson["features"].find { |f| f["properties"]["name"] == "p4n2gpx export" }
assert_equal "amenity_parking", export["properties"]["icon"]
end
def test_waypoint_without_category_or_color_leaves_them_nil
geojson = Maptool::Gpx.to_geojson(fixture_path("sample_poi.gpx"))
plain = geojson["features"].find { |f| f["properties"]["name"] == "Plain waypoint" }
assert_nil plain["properties"]["category"]
assert_nil plain["properties"]["color"]
assert_nil plain["properties"]["icon"]
end
def test_kinds_detects_routes
assert_equal ["route"], Maptool::Gpx.kinds(fixture_path("sample_route.gpx"))
end