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
@@ -472,6 +472,12 @@ html, body {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-overview-map {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.measure-ctrl {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
+29
-8
@@ -14,6 +14,8 @@ import { MeasureControl } from "/js/measure.js";
|
||||
import { LAYERS_ICON } from "/js/icons.js";
|
||||
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||
import HomeButton from "/vendor/npmap-home-button/home-button.esm.js";
|
||||
import OverviewMap from "/vendor/npmap-overview/overview.esm.js";
|
||||
import * as maplibregl from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
||||
|
||||
const CENTER = [17.1077, 48.1486];
|
||||
const ZOOM = 13;
|
||||
@@ -235,14 +237,18 @@ const map = new MaplibreMap({
|
||||
// all grouped together rather than split across corners. Added in this
|
||||
// order since MapLibre stacks same-position controls in add order.
|
||||
//
|
||||
// homePosition/zoom pinned to the app's own default rather than left to the
|
||||
// button's own fallback (current center/zoom at construction) — now that
|
||||
// the URL hash can restore any view on load (see the Hash option above),
|
||||
// "current view" and "the default view" are no longer the same thing, and
|
||||
// a home button that resets to wherever you happened to load the page
|
||||
// wouldn't be a home button. This is *not* "go to my location" — that's
|
||||
// GeolocateControl, below.
|
||||
map.addControl(new HomeButton({ homePosition: CENTER, zoom: ZOOM }), "top-left");
|
||||
// bounds (rather than homePosition/zoom) so Home resets to an overview of
|
||||
// the whole country, not back to CENTER/ZOOM's initial Bratislava view —
|
||||
// now that the URL hash can restore any view on load (see the Hash option
|
||||
// above), "current view" and "the default view" are no longer the same
|
||||
// thing, and a home button that resets to wherever you happened to load
|
||||
// the page wouldn't be a home button. This is *not* "go to my location" —
|
||||
// that's GeolocateControl, below.
|
||||
const SLOVAKIA_BOUNDS = [
|
||||
[16.83, 47.73],
|
||||
[22.57, 49.61],
|
||||
];
|
||||
map.addControl(new HomeButton({ bounds: SLOVAKIA_BOUNDS }), "top-left");
|
||||
// showCompass: false — this map never rotates, so a bearing-reset button
|
||||
// (NavigationControl's default third button) would just be dead weight.
|
||||
map.addControl(new NavigationControl({ showCompass: false }), "top-left");
|
||||
@@ -257,6 +263,21 @@ map.addControl(
|
||||
"top-left",
|
||||
);
|
||||
|
||||
// A small inset map showing where the current view sits in context — NPS's
|
||||
// own overview plugin (docs/nps-resources.adoc's "overview" entry, still on
|
||||
// an unmerged branch upstream). It draws its own embedded MapLibre map, so
|
||||
// it needs the maplibregl module itself (not just the Map class) — style
|
||||
// '' (the default) means it clones whatever the main map's current style
|
||||
// is (`this._mainMap.style.serialize()`), so it stays in sync with the
|
||||
// Online/Offline toggle without a second style to maintain here. Built only
|
||||
// once the main map's own style has loaded, matching the plugin's own
|
||||
// example — its internal map is constructed eagerly in the constructor,
|
||||
// before that clone happens in onAdd.
|
||||
map.once("style.load", () => {
|
||||
const OverviewMapControl = OverviewMap(maplibregl);
|
||||
map.addControl(new OverviewMapControl({}), "top-right");
|
||||
});
|
||||
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
|
||||
const layerToggle = new LayerToggleControl(applyStyle);
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
### LICENSE.md
|
||||
|
||||
This project constitutes a work of the United States Government and is not subject to domestic copyright protection under [17 USC § 105](https://www.govinfo.gov/app/details/USCODE-2010-title17/USCODE-2010-title17-chap1-sec105).
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+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