Home button fits all of Slovakia; add an overview map, top-right
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:
1 parent
ddf2ee4990
commit
96065b1237
7 files changed
+441
-31
No files matched your search
+346
@@ -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;
|
||||
Reference in new issue
Block a user