Home button fits all of Slovakia; add an overview map, top-right
CI / check (push) Successful in 1m33s
CI / deploy (push) Successful in 27s

Home now fitBounds()s a country-wide bbox instead of resetting to
CENTER/ZOOM's Bratislava point, since Home resetting into the same
city you'd already be looking at wasn't useful.

The overview map is NPS's own overview plugin (npmap5-plugins, still
on an unmerged branch), vendored and converted from its UMD dist
build to a plain ES module. Patched one MapLibre API break found while
verifying it: the plugin reads map.transform (public in the MapLibre
~2.4 it targets) to relax the inset's polar pan limit; MapLibre 6 (this
app's vendored version) no longer exposes .transform at all, so it
threw on load. Guarded to skip that cosmetic adjustment when absent
instead.
This commit is contained in:
randy committed 2026-09-07 22:30:56 +02:00
1 parent ddf2ee4990
commit 96065b1237
7 files changed
+441 -31

No files matched your search

+346
View File
@@ -0,0 +1,346 @@
// Vendored from nationalparkservice/npmap5-plugins, branch
// maplibre-gl-overview (not yet merged to main — see
// docs/nps-resources.adoc). MIT-licensed per that branch's package.json;
// see LICENSE.md alongside this file for the repo's own notice.
//
// Converted from its UMD dist build (dist/maplibre-gl-overview.min.js) to a
// plain ES module export — the only change from upstream besides that
// wrapper swap.
const blankGeoJsonFeature = () => ({
"type": "FeatureCollection",
"features": []
});
const between = (min, value, max) => Math.min(max, Math.max(min, value));
function OverviewMap(mapLibrary) {
return class OverviewMapControl {
constructor(options) {
this._extent = blankGeoJsonFeature();
this._moving = false;
this._easingMain = false;
this._keyboardEvents = (e) => {
if (e.altKey || e.ctrlKey || e.metaKey)
return;
// The +1 directions are in Pixel space, not LngLat, in pixel space 0,0 it the top left
const events = {
"ArrowLeft": () => {
e.preventDefault();
return { xDir: 1 };
},
"ArrowRight": () => {
e.preventDefault();
return { xDir: -1 };
},
"ArrowUp": () => {
e.preventDefault();
return { yDir: 1 };
},
"ArrowDown": () => {
e.preventDefault();
return { yDir: -1 };
},
"+": () => ({ zoomDir: +1 }),
"-": () => ({ zoomDir: -1 }),
};
if (events[e.key]) {
// Update the variables
let { xDir, yDir, zoomDir } = events[e.key]();
xDir = xDir || 0;
yDir = yDir || 0;
zoomDir = zoomDir || 0;
if (this._mainMap) {
const zoomMain = this._mainMap.getZoom();
// Convert the current center of the overview map to pixel space
const overviewMapCenterPixels = this._overviewMap.project(this._overviewMap.getCenter());
// Get the point that would be panned that center
const newCenter = this._overviewMap.unproject(new this._mapLibrary.Point(overviewMapCenterPixels.x + (xDir * this._overviewMap.keyboard._panStep), overviewMapCenterPixels.y + (yDir * this._overviewMap.keyboard._panStep)));
// Get the current center of the main map
const mainMapCenterPixels = this._mainMap.project(this._mainMap.getCenter());
// Get the new point on the main map in pixels
const mainMapNewCenterPixels = this._mainMap.project(newCenter);
// Define the offset to pan to
const xyOffset = {
x: mainMapNewCenterPixels.x - mainMapCenterPixels.x,
y: mainMapNewCenterPixels.y - mainMapCenterPixels.y
};
this._mainMap.easeTo({
duration: 300,
easeId: 'keyboardHandler',
easing: (t) => t * (2 - t),
zoom: zoomDir ? Math.round(zoomMain) + zoomDir * (e.shiftKey ? 2 : 1) : zoomMain,
offset: [xyOffset.x, xyOffset.y],
center: this._mainMap.getCenter()
}, { originalEvent: e });
}
}
};
// Create a container for the overview map
this._container = document.createElement("div");
this._container.className = "maplibregl-ctrl-overview-map maplibregl-ctrl";
this._container.addEventListener("contextmenu", (e) => e.preventDefault());
// options
const defaultOptions = {
zoomLevelOffset: -5,
watchEvents: ['move', 'rotate', 'pitch'],
overlayPaint: {
'fill-color': '#d29700',
'fill-opacity': 0.75
},
selectionPaint: {
'fill-color': '#d29700',
'fill-opacity': 0.25,
'fill-outline-color': '#000000'
},
width: 150,
height: 150,
style: '',
_keyboardPanStep: 15
};
this.options = { ...defaultOptions, ...options };
this._mapLibrary = mapLibrary;
this._container.setAttribute('style', `width: ${this.options.width}px; height: ${this.options.height}px;`);
// Create the map for the overview
this._overviewMap = new mapLibrary.Map({
container: this._container,
style: this.options.style,
interactive: true,
pitchWithRotate: false,
attributionControl: false,
boxZoom: false,
dragRotate: false,
touchZoomRotate: false,
touchPitch: false,
minZoom: -2
});
this._overviewMap.on('load', () => {
// `.transform` existed as a public Map property when this
// plugin was built (~MapLibre 2.4.0); this app vendors a
// newer MapLibre where it's gone entirely, so this relaxes
// nothing and the overview keeps MapLibre's default ~85°
// Web Mercator pan limit instead — harmless, just means the
// inset can't scroll arbitrarily close to the poles.
if (this._overviewMap.transform) {
const maxLat = this._overviewMap.transform.maxValidLatitude || 85.051129;
this._overviewMap.transform.latRange = [maxLat * -2, maxLat * 2];
}
this._overviewMap.resize();
this._overviewMap.addSource('bboxSource', {
'type': 'geojson',
'data': this._extent
});
this._overviewMap.addSource('selectionSource', {
'type': 'geojson',
'data': blankGeoJsonFeature()
});
// Add a new layer to visualize the polygon.
this._overviewMap.addLayer({
'id': 'bboxLayer',
'type': 'fill',
'source': 'bboxSource',
'layout': {},
'paint': this.options.overlayPaint
});
this._overviewMap.addLayer({
'id': 'selectionFillLayer',
'type': 'fill',
'source': 'selectionSource',
'layout': {},
'paint': this.options.selectionPaint
});
this._overviewMap.addLayer({
'id': 'selectionLineLayer',
'type': 'line',
'source': 'selectionSource',
'layout': {},
'paint': {
'line-color': this.options.selectionPaint['fill-outline-color'] || '#000000',
'line-width': 2
}
});
});
}
onAdd(map) {
this._mainMap = map;
if (this.options.style === '') {
this._overviewMap.setStyle(this._mainMap.style.serialize());
}
// Disable the keyboard events, but use our own
this._overviewMap.keyboard.disable();
// we still use the _panstep from the keyboard events in out custom function
this._overviewMap.keyboard._panStep = this.options._keyboardPanStep;
//this._overviewMap.getCanvas().addEventListener('keydown', this._keyboardEvents, true);
// Update the overview map when the main map moves
this.options.watchEvents.forEach(event => map.on(event, () => this._updateOverview(map)));
/////////////////////////////
// Update Overview Map triggers
// Once the style is loaded, start listening
map.once('style.load', () => this._updateOverview(map));
// Start listening when it's added
this._updateOverview(map);
// Update the main map when the overview moves if the map is loaded (otherwise the style.load will pick up the change)
this._overviewMap.on('movestart', () => map.loaded() && this._updateMain(map));
// Make the overview map take control whenever the viewview map gets clicked
const takeControl = () => {
map.stop();
this._moving = false;
this._updateMain(map);
};
this._overviewMap.on('mousedown', takeControl);
this._overviewMap.on('touchstart', takeControl);
// Forward keydown events to the main map
this._overviewMap.getCanvas().addEventListener('keydown', (e) => {
//e.preventDefault();
map.stop();
this._keyboardEvents(e);
});
/////////////////////////////
return this._container;
}
onRemove() {
if (this._mainMap) {
//this._overviewMap.off('movestart', () => this._mainMap && this._moveOverview(this._mainMap));
this.options.watchEvents
.forEach(event => this._mainMap && this._mainMap
.off(event, () => this._updateOverview(this._mainMap)));
this._overviewMap.off('movestart', () => this._updateMain(this._mainMap));
}
if (this.options.style === '') {
this._overviewMap.setStyle('');
}
this._mainMap = undefined;
}
_updateOverview(map) {
var _a;
// Don't update the overview map if it's already moving
if (this._moving || !this._overviewMap)
return;
const pixelRatio = map.getPixelRatio();
const geojson = blankGeoJsonFeature();
if (map) {
const center = map.unproject([(map.getCanvas().width / pixelRatio) / 2, (map.getCanvas().height / pixelRatio) / 2]);
const bounds = map.getBounds();
let extentCoordinates = [
[-179.99, bounds.getNorth()],
[179.99, bounds.getNorth()],
[179.99, bounds.getSouth()],
[-179.99, bounds.getSouth()],
[-179.99, bounds.getNorth()],
];
// If the extent isn't larger that the earth, create a better one
if ((bounds.getEast() - bounds.getWest()) < 360 + (Math.abs(90 - Math.abs(90 - Math.abs(map.getBearing())))) * (map.getPitch() / 60)) {
extentCoordinates = [
map.unproject([0, 0]).toArray(),
map.unproject([map.getCanvas().width / pixelRatio, 0]).toArray(),
map.unproject([map.getCanvas().width / pixelRatio, map.getCanvas().height / pixelRatio]).toArray(),
map.unproject([0, map.getCanvas().height / pixelRatio]).toArray(),
map.unproject([0, 0]).toArray()
];
}
geojson.features.push({
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [extentCoordinates]
}
});
const newOverviewZoom = between(this._overviewMap.getMinZoom(), map.getZoom() + this.options.zoomLevelOffset, this._overviewMap.getMaxZoom());
if (!this._easingMain) {
this._moving = true;
this._overviewMap.setCenter(center);
this._overviewMap.setZoom(newOverviewZoom);
this._moving = false;
}
(_a = this._overviewMap.getSource('bboxSource')) === null || _a === void 0 ? void 0 : _a.setData(geojson);
this._extent = geojson;
}
}
_updateMain(map) {
var _a;
// Don't update the main map if it's already moving or if it doesn't exist
if (this._moving || !this._mainMap)
return;
const moveStartCenter = this._overviewMap.getCenter();
// Take control of the main map
this._mainMap.stop();
// Get the current extent, and copy it
const selectionExtent = JSON.parse(JSON.stringify(this._extent));
// Get the coordinates on the screen for the current map extent
const screenCoords = (selectionExtent.features[0]).geometry.coordinates[0].map(pos => this._overviewMap.project(pos));
// Update the GeoJSON object in the overview map to match the extent
(_a = this._overviewMap.getSource('selectionSource')) === null || _a === void 0 ? void 0 : _a.setData(selectionExtent);
// Used when overzoomed
const overZoomOffset = Math.max(this._overviewMap.getZoom() - this._mainMap.getZoom(), this.options.zoomLevelOffset);
// Called when the overview map is moving
const moving = () => {
var _a;
// Gets the current position
const currentSelectionExtentCoordinates = screenCoords
.map(pos => this._overviewMap.unproject([pos.x, pos.y]))
.map(xy => [xy.lng, xy.lat]);
// Updates the GeoJSON to match the new position
const geojson = blankGeoJsonFeature();
geojson.features.push({
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [currentSelectionExtentCoordinates]
}
});
// Updates the box on the overview map
(_a = this._overviewMap.getSource('selectionSource')) === null || _a === void 0 ? void 0 : _a.setData(geojson);
};
const doneEvents = () => {
var _a;
// Draw the current box on the overview map
(_a = this._overviewMap.getSource('selectionSource')) === null || _a === void 0 ? void 0 : _a.setData(blankGeoJsonFeature());
this._moving = false;
this._easingMain = false;
// Start up the event listeners again
this._updateOverview(map);
};
// Called when the overview map is done moving
const moveend = () => {
// Stop listening for events
this._overviewMap.off('move', moving);
this._overviewMap.off('moveend', moveend);
this._overviewMap.off('mouseup', moveend);
this._overviewMap.off('touchend', moveend);
if (this._easingMain)
return;
// If the map and the oveview map are ready, then we can update the map's center
if (this._mainMap) {
// Deal with the overview map being outside of its minZoom
let newCenter = this._overviewMap.getCenter();
const newMainZoom = between(this._mainMap.getMinZoom(), this._overviewMap.getZoom() - overZoomOffset, this._mainMap.getMaxZoom());
// Check if the overview map has moved, if so, ease the main map, otherwise finish events
if (newCenter.lng !== moveStartCenter.lng || newCenter.lat !== moveStartCenter.lat) {
// Execute the "doneEvents" code when the map stops moving
this._mainMap.once('moveend', (event) => event.doneEvents && event.doneEvents());
this._moving = true;
this._easingMain = true;
this._mainMap.easeTo({
center: newCenter,
zoom: newMainZoom
}, {
doneEvents
});
}
else {
doneEvents();
}
}
};
this._overviewMap.on('move', moving);
this._overviewMap.once('moveend', moveend);
this._overviewMap.once('mouseup', moveend);
this._overviewMap.once('touchend', moveend);
}
_getCenterAtPitch(map, pitch, rotation) {
const pixelRatio = map.getPixelRatio();
const center = map.unproject([(map.getCanvas().width / pixelRatio) / 2, (map.getCanvas().height / pixelRatio) / 2]);
return center;
}
};
}
export default OverviewMap;