maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
159 lines (133 loc) • 5.18 kB
text/typescript
import {throttle} from '../util/throttle.ts';
import {LngLat} from '../geo/lng_lat.ts';
import type {Map} from './map.ts';
/**
* Adds the map's position to its page's location hash.
* Passed as an option to the map object.
*
* @group Markers and Controls
*/
export class Hash {
_map: Map;
_hashName: string;
constructor(hashName?: string | null) {
this._hashName = hashName && encodeURIComponent(hashName);
}
/**
* Map element to listen for coordinate changes
*
* @param map - The map object
*/
addTo(map: Map): this {
this._map = map;
addEventListener('hashchange', this._onHashChange, false);
this._map.on('moveend', this._updateHash);
return this;
}
/**
* Removes hash
*/
remove(): this {
removeEventListener('hashchange', this._onHashChange, false);
this._map.off('moveend', this._updateHash);
clearTimeout(this._updateHash());
this._removeHash();
delete this._map;
return this;
}
getHashString(mapFeedback?: boolean): string {
const center = this._map.getCenter(),
zoom = Math.round(this._map.getZoom() * 100) / 100,
// derived from equation: 512px * 2^z / 360 / 10^d < 0.5px
precision = Math.ceil((zoom * Math.LN2 + Math.log(512 / 360 / 0.5)) / Math.LN10),
m = Math.pow(10, precision),
lng = Math.round(center.lng * m) / m,
lat = Math.round(center.lat * m) / m,
bearing = this._map.getBearing(),
pitch = this._map.getPitch();
let hash = '';
if (mapFeedback) {
// new map feedback site has some constraints that don't allow
// us to use the same hash format as we do for the Map hash option.
hash += `/${lng}/${lat}/${zoom}`;
} else {
hash += `${zoom}/${lat}/${lng}`;
}
if (bearing || pitch) hash += (`/${Math.round(bearing * 10) / 10}`);
if (pitch) hash += (`/${Math.round(pitch)}`);
if (this._hashName) {
const params = this._getHashParams();
params.set(this._hashName, hash);
return `#${decodeURIComponent(params.toString()).replace(/=&/g, '&').replace(/=$/g, '')}`;
}
return `#${hash}`;
}
_getHashParams = (): URLSearchParams => {
return new URLSearchParams(window.location.hash.replace('#', ''));
};
_getCurrentHash = (): string[] => {
const params = this._getHashParams();
if (this._hashName) {
return (params.get(this._hashName) || '').split('/');
}
// For unnamed hashes, get the first key
const hash = [...params.keys()][0] ?? '';
return hash.split('/');
};
_onHashChange = (): boolean => {
const hash = this._getCurrentHash();
if (!this._isValidHash(hash)) {
return false;
}
const bearing = this._map.dragRotate.isEnabled() && this._map.touchZoomRotate.isEnabled() ? +(hash[3] || 0) : this._map.getBearing();
this._map.jumpTo({
center: [+hash[2], +hash[1]],
zoom: +hash[0],
bearing,
pitch: +(hash[4] || 0)
});
return true;
};
_updateHashUnthrottled = (): void => {
const location = window.location.href.replace(/(#.*)?$/, this.getHashString());
window.history.replaceState(window.history.state, null, location);
};
_removeHash = (): void => {
const params = this._getHashParams();
if (this._hashName) {
params.delete(this._hashName);
} else {
// For unnamed hash (#zoom/lat/lng&other=params), remove first entry
const keys = Array.from(params.keys());
if (keys.length > 0) {
params.delete(keys[0]);
}
}
const newHash = decodeURIComponent(params.toString()).replace(/=&/g, '&').replace(/=$/g, '');
const replaceString = newHash ? `#${newHash}` : '';
let location = window.location.href.replace(/(#.+)?$/, replaceString);
location = location.replace('&&', '&');
window.history.replaceState(window.history.state, null, location);
};
/**
* Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds.
*/
_updateHash: () => ReturnType<typeof setTimeout> = throttle(this._updateHashUnthrottled, 30 * 1000 / 100);
_isValidHash(hash: string[]): boolean {
if (hash.length < 3 || hash.some(h => isNaN(+h))) {
return false;
}
// LngLat() throws error if latitude is out of range, and it's valid if it succeeds.
try {
new LngLat(+hash[2], +hash[1]);
} catch {
return false;
}
const zoom = +hash[0];
const bearing = +(hash[3] || 0);
const pitch = +(hash[4] || 0);
return zoom >= this._map.getMinZoom() && zoom <= this._map.getMaxZoom() &&
bearing >= -180 && bearing <= 180 &&
pitch >= this._map.getMinPitch() && pitch <= this._map.getMaxPitch();
};
}