Add a 3D pitch toggle, with raised terrain following it

This commit is contained in:
randy committed 2026-09-12 12:50:09 +02:00
1 parent f5eb9a4a8d
commit 5c3151c2f5
3 files changed
+122

No files matched your search

+62
View File
@@ -355,6 +355,65 @@ class ZoomLevelControl {
}
}
// Tilts the camera between a flat top-down view and a fixed 3D-perspective
// pitch — bearing stays untouched (this map never rotates, see the
// NavigationControl comment below), so this is purely "look at it from an
// angle", not a rotate/compass control. Independent of the "Terrain
// shading" overlay toggle (overlays.js) — that adds a raster-dem hillshade/
// color-relief layer, this just moves the camera; combining both gives a
// real sense of relief but neither depends on the other.
const PITCH_3D = 60;
class PitchToggleControl {
onAdd(map) {
this._map = map;
this._container = document.createElement("div");
this._container.className = "maplibregl-ctrl maplibregl-ctrl-group pitch-toggle-ctrl";
this._button = document.createElement("button");
this._button.type = "button";
this._button.className = "pitch-toggle-ctrl__button";
this._button.addEventListener("click", () => this._toggle());
this._container.appendChild(this._button);
this._update = () => this._render();
map.on("pitchend", this._update);
this._render();
return this._container;
}
onRemove() {
this._map.off("pitchend", this._update);
this._container.remove();
}
getDefaultPosition() {
return "top-left";
}
// Set once overlaysControl exists (it's built later in this file) —
// lets raised terrain (OverlaysControl.syncTerrain, see overlays.js)
// follow the pitch toggle without this control needing to know anything
// about hillshade/DEM sources itself.
setOverlaysControl(overlaysControl) {
this._overlaysControl = overlaysControl;
this._overlaysControl.syncTerrain(this._map.getPitch() > 0);
}
_toggle() {
this._map.easeTo({ pitch: this._map.getPitch() > 0 ? 0 : PITCH_3D, duration: 300 });
}
_render() {
const is3d = this._map.getPitch() > 0;
this._button.textContent = is3d ? "2D" : "3D";
this._button.title = is3d ? "Switch to 2D view" : "Switch to 3D view";
this._button.setAttribute("aria-label", this._button.title);
this._button.classList.toggle("active", is3d);
this._overlaysControl?.syncTerrain(is3d);
}
}
const map = new MaplibreMap({
container: "map",
style: ONLINE_STYLE,
@@ -391,6 +450,8 @@ map.addControl(new HomeButton({ bounds: SLOVAKIA_BOUNDS }), "top-left");
// (NavigationControl's default third button) would just be dead weight.
map.addControl(new NavigationControl({ showCompass: false }), "top-left");
map.addControl(new ZoomLevelControl(), "top-left");
const pitchToggle = new PitchToggleControl();
map.addControl(pitchToggle, "top-left");
map.addControl(new MeasureControl(), "top-left");
// Writing a note needs a login (POST /api/gpx/notes) — no point adding a
// button that can only ever end in a 401 for a logged-out viewer, same
@@ -487,6 +548,7 @@ sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title=
new GpxPanelControl(map, profilePanel).mount(sidebar);
const overlaysControl = new OverlaysControl(map);
overlaysControl.mount(sidebar);
pitchToggle.setOverlaysControl(overlaysControl);
// Running a plugin needs a login (POST /api/plugins/:name/run) — same
// "hide the write-only affordance" story as NotesControl above.
if (isLoggedIn()) new PluginsControl(map).mount(sidebar);