Add contour lines to the Overlays panel via maplibre-contour
5th Overlays item, generated client-side from the same online Terrarium DEM tiles Terrain shading already fetches (no offline fallback — the plugin's DemSource has no public hook to route through the pmtiles:// protocol the offline basemap uses, so this quietly renders nothing without a network, same as Radar/the Wikipedia layer). A plain vector source/layer under MapLibre's own tile lifecycle, no manual refetch-on-move code needed. Thresholds are the plugin's own generic starting point, not yet tuned against real terrain. Labels skipped for now (font-name matching across the online/offline styles' own glyph sets adds real risk for a nice-to-have, not the actual ask).
This commit is contained in:
1 parent
b4a1c6c0cc
commit
fba8ca10a2
7 files changed
+140
-2
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import { addProtocol } from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
||||
import { ONLINE_TERRAIN_TILES } from "/js/overlays.js";
|
||||
import { firstSymbolLayerId } from "/js/radar.js";
|
||||
import { CONTOUR_ICON } from "/js/icons.js";
|
||||
|
||||
const SOURCE_ID = "contour-lines";
|
||||
const LINE_LAYER_ID = "contour-lines-line";
|
||||
|
||||
// Topographic contour lines, generated client-side by the vendored
|
||||
// maplibre-contour plugin (public/vendor/maplibre-contour/) from the same
|
||||
// online Terrarium DEM tiles overlays.js's HillshadeControl already
|
||||
// fetches. Online only, unlike Terrain shading/Terrain shadow: the
|
||||
// plugin's DemSource has no public hook to route its own tile fetch
|
||||
// through the pmtiles:// protocol HillshadeControl uses for the offline
|
||||
// basemap, so — same story as the Wikipedia layer and Radar — this just
|
||||
// quietly renders nothing without a network.
|
||||
export class ContourControl {
|
||||
constructor(map) {
|
||||
this._map = map;
|
||||
this._active = false;
|
||||
this._demSource = new window.mlcontour.DemSource({
|
||||
url: ONLINE_TERRAIN_TILES[0],
|
||||
encoding: "terrarium",
|
||||
maxzoom: 15,
|
||||
worker: true,
|
||||
cacheSize: 100,
|
||||
});
|
||||
this._demSource.setupMaplibre({ addProtocol });
|
||||
|
||||
// setStyle() (switching Online/Offline) tears down all custom sources/
|
||||
// layers — put contour lines back if they were on, same as every other
|
||||
// Overlays control.
|
||||
map.on("style.load", () => {
|
||||
if (this._active) this._addLayer();
|
||||
});
|
||||
}
|
||||
|
||||
mount(li) {
|
||||
li.innerHTML = `
|
||||
<label>
|
||||
<input type="checkbox" class="overlays-panel__toggle">
|
||||
<span class="overlays-panel__icon">${CONTOUR_ICON}</span>
|
||||
<span>Contour lines</span>
|
||||
</label>
|
||||
`;
|
||||
this._checkbox = li.querySelector(".overlays-panel__toggle");
|
||||
this._checkbox.addEventListener("change", (event) => this._toggle(event.target.checked));
|
||||
}
|
||||
|
||||
_toggle(active) {
|
||||
this._active = active;
|
||||
if (active) this._addLayer();
|
||||
else this._removeLayer();
|
||||
}
|
||||
|
||||
_addLayer() {
|
||||
if (this._map.getSource(SOURCE_ID)) return;
|
||||
|
||||
// Starting-point thresholds (README's own generic example) — meters,
|
||||
// more/tighter contours at higher zoom. Expect to eyeball-tune against
|
||||
// real Tatras-scale terrain.
|
||||
const tiles = [
|
||||
this._demSource.contourProtocolUrl({
|
||||
thresholds: {
|
||||
11: [200, 1000],
|
||||
12: [100, 500],
|
||||
14: [50, 200],
|
||||
15: [20, 100],
|
||||
},
|
||||
contourLayer: "contours",
|
||||
elevationKey: "ele",
|
||||
levelKey: "level",
|
||||
}),
|
||||
];
|
||||
this._map.addSource(SOURCE_ID, { type: "vector", tiles, maxzoom: 15 });
|
||||
this._map.addLayer(
|
||||
{
|
||||
id: LINE_LAYER_ID,
|
||||
type: "line",
|
||||
source: SOURCE_ID,
|
||||
"source-layer": "contours",
|
||||
paint: {
|
||||
// Roughly the same brown as the mid-slope band of overlays.js's
|
||||
// own ELEVATION_COLOR_RAMP, so this reads as part of the same
|
||||
// terrain treatment rather than a bolted-on layer.
|
||||
"line-color": "#8a6d3b",
|
||||
"line-width": ["match", ["get", "level"], 1, 1.2, 0.6],
|
||||
"line-opacity": 0.6,
|
||||
},
|
||||
},
|
||||
firstSymbolLayerId(this._map),
|
||||
);
|
||||
}
|
||||
|
||||
_removeLayer() {
|
||||
if (this._map.getLayer(LINE_LAYER_ID)) this._map.removeLayer(LINE_LAYER_ID);
|
||||
if (this._map.getSource(SOURCE_ID)) this._map.removeSource(SOURCE_ID);
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,18 @@ export const TERRAIN_SHADOW_ICON = `
|
||||
<polygon points="0,19 6,9 9,13 13,6 22,19"/>
|
||||
</svg>`;
|
||||
|
||||
// Hand-drawn, not NPS-sourced — no contour-line pictogram in that set
|
||||
// either. Concentric rings via fill-rule evenodd, same solid-glyph
|
||||
// convention as WILDERNESS_ICON/RADAR_CLOUD_ICON/TERRAIN_SHADOW_ICON above —
|
||||
// used as the Overlays panel's Contour lines glyph (contour.js).
|
||||
export const CONTOUR_ICON = `
|
||||
<svg viewBox="0 0 22 22" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="M21,11 A10,10 0 1,0 1,11 A10,10 0 1,0 21,11 Z
|
||||
M18.5,11 A7.5,7.5 0 1,0 3.5,11 A7.5,7.5 0 1,0 18.5,11 Z
|
||||
M16,11 A5,5 0 1,0 6,11 A5,5 0 1,0 16,11 Z
|
||||
M13.5,11 A2.5,2.5 0 1,0 8.5,11 A2.5,2.5 0 1,0 13.5,11 Z"/>
|
||||
</svg>`;
|
||||
|
||||
// src/bicycle-trail-22.svg — pulled out to its own constant since it's also
|
||||
// used unrasterized, inline, as PARK4NIGHT_TAGS.vtt below.
|
||||
const BICYCLE_TRAIL_ICON = `
|
||||
|
||||
@@ -4,6 +4,7 @@ import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||
import { svgToImageData, badgeImageId, escapeHtml } from "/js/gpx.js";
|
||||
import { RadarControl } from "/js/radar.js";
|
||||
import { ShadowControl } from "/js/shadow.js";
|
||||
import { ContourControl } from "/js/contour.js";
|
||||
|
||||
const ICON_IMAGE_SIZE = 48; // rasterized once at this size, rendered much smaller by icon-size below
|
||||
const ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, sized to sit inside the marker badge below
|
||||
@@ -125,6 +126,7 @@ export class OverlaysControl {
|
||||
this._hillshade = new HillshadeControl(map);
|
||||
this._radar = new RadarControl(map);
|
||||
this._shadow = new ShadowControl(map);
|
||||
this._contour = new ContourControl(map);
|
||||
this._wikiEnabled = false; // the checkbox's own state
|
||||
this._wikiMoveHandler = null; // the moveend listener driving its live refetch, while enabled
|
||||
this._wikiRequestSeq = 0; // ignores an in-flight refetch a newer one has already superseded
|
||||
@@ -157,12 +159,14 @@ export class OverlaysControl {
|
||||
<li class="overlays-panel__item"></li>
|
||||
<li class="overlays-panel__item overlays-panel__item--expandable"></li>
|
||||
<li class="overlays-panel__item overlays-panel__item--expandable"></li>
|
||||
<li class="overlays-panel__item"></li>
|
||||
`;
|
||||
const [hillshadeItem, wikiItem, radarItem, shadowItem] = list.children;
|
||||
const [hillshadeItem, wikiItem, radarItem, shadowItem, contourItem] = list.children;
|
||||
this._hillshade.mount(hillshadeItem);
|
||||
this._mountWiki(wikiItem);
|
||||
this._radar.mount(radarItem);
|
||||
this._shadow.mount(shadowItem);
|
||||
this._contour.mount(contourItem);
|
||||
|
||||
onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active"));
|
||||
|
||||
|
||||
+3
-1
@@ -357,7 +357,9 @@ function readCachedForecast() {
|
||||
}
|
||||
}
|
||||
|
||||
function firstSymbolLayerId(map) {
|
||||
// Exported: contour.js's own contour-line layers want the same
|
||||
// "sit above base fill/roads, below place labels" placement.
|
||||
export function firstSymbolLayerId(map) {
|
||||
const layers = map.getStyle()?.layers || [];
|
||||
const symbolLayer = layers.find((layer) => layer.type === "symbol");
|
||||
return symbolLayer ? symbolLayer.id : undefined;
|
||||
|
||||
Reference in new issue
Block a user