esri-leaflet-vector
Version:
Esri vector basemap and vector tile layer plugin for Leaflet.
339 lines (278 loc) • 8.82 kB
JavaScript
import {
DomEvent,
DomUtil,
extend,
latLngBounds,
Layer,
setOptions,
Util,
} from "leaflet";
import maplibregl from "maplibre-gl";
export const setRTLTextPlugin = (url, callback, deferred) => {
maplibregl.setRTLTextPlugin(url, callback, deferred);
};
export const MaplibreGLJSLayer = Layer.extend({
options: {
updateInterval: 32,
// How much to extend the overlay view (relative to map size)
// e.g. 0.1 would be 10% of map view in each direction
padding: 0.1,
// whether or not to register the mouse and keyboard
// events on the mapbox overlay
interactive: false,
// set the tilepane as the default pane to draw gl tiles
pane: "tilePane",
},
initialize(options) {
setOptions(this, options);
// setup throttling the update event when panning
this._throttledUpdate = Util.throttle(
this._update,
this.options.updateInterval,
this,
);
},
onAdd(map) {
if (!this._container) {
this._initContainer();
}
const paneName = this.getPaneName();
map.getPane(paneName).appendChild(this._container);
this._initGL();
this._offset = this._map.containerPointToLayerPoint([0, 0]);
// work around https://github.com/mapbox/mapbox-gl-leaflet/issues/47
if (map.options.zoomAnimation) {
DomEvent.on(
map._proxy,
DomUtil.TRANSITION_END,
this._transitionEnd,
this,
);
}
},
onRemove(map) {
if (this._map._proxy && this._map.options.zoomAnimation) {
DomEvent.off(
this._map._proxy,
DomUtil.TRANSITION_END,
this._transitionEnd,
this,
);
}
const paneName = this.getPaneName();
map.getPane(paneName).removeChild(this._container);
this._container = null;
this._glMap.remove();
this._glMap = null;
},
getEvents() {
return {
move: this._throttledUpdate, // sensibly throttle updating while panning
zoomanim: this._animateZoom, // applies the zoom animation to the <canvas>
zoom: this._pinchZoom, // animate every zoom event for smoother pinch-zooming
zoomstart: this._zoomStart, // flag starting a zoom to disable panning
zoomend: this._zoomEnd,
resize: this._resize,
};
},
getMaplibreMap() {
return this._glMap;
},
getCanvas() {
return this._glMap.getCanvas();
},
getSize() {
return this._map.getSize().multiplyBy(1 + this.options.padding * 2);
},
getOpacity() {
return this.options.opacity;
},
setOpacity(opacity) {
this.options.opacity = opacity;
this._container.style.opacity = opacity;
},
getBounds() {
const halfSize = this.getSize().multiplyBy(0.5);
const center = this._map.latLngToContainerPoint(this._map.getCenter());
return latLngBounds(
this._map.containerPointToLatLng(center.subtract(halfSize)),
this._map.containerPointToLatLng(center.add(halfSize)),
);
},
getContainer() {
return this._container;
},
// returns the pane name set in options if it is a valid pane, defaults to tilePane
getPaneName() {
return this._map.getPane(this.options.pane)
? this.options.pane
: "tilePane";
},
_resize() {
return this._glMap._resize;
},
_initContainer() {
if (this._container) {
return;
}
this._container = DomUtil.create("div", "leaflet-gl-layer");
const size = this.getSize();
const offset = this._map.getSize().multiplyBy(this.options.padding);
this._container.style.width = `${size.x}px`;
this._container.style.height = `${size.y}px`;
const topLeft = this._map
.containerPointToLayerPoint([0, 0])
.subtract(offset);
DomUtil.setPosition(this._container, topLeft);
},
_initGL() {
if (this._glMap) {
return;
}
const center = this._map.getCenter();
const options = extend({}, this.options, {
container: this._container,
center: [center.lng, center.lat],
zoom: this._map.getZoom() - 1,
attributionControl: false,
});
this._glMap = new maplibregl.Map(options);
// Listen for style data error (401 Unauthorized)
this._glMap.on("error", (error) => {
if (error.error && error.error.status === 401) {
console.warn(
"Invalid or expired API key. Please check that API key is not expired and has the basemaps privilege assigned.",
);
}
});
// Fire event for Maplibre "styledata" event.
this._glMap.once("styledata", () => {
this.fire("styleLoaded");
});
// allow GL base map to pan beyond min/max latitudes
this._glMap.transform.latRange = null;
this._glMap.transform.maxValidLatitude = Infinity;
this._transformGL(this._glMap);
if (this._glMap._canvas.canvas) {
// older versions of mapbox-gl surfaced the canvas differently
this._glMap._actualCanvas = this._glMap._canvas.canvas;
} else {
this._glMap._actualCanvas = this._glMap._canvas;
}
// treat child <canvas> element like L.ImageOverlay
const canvas = this._glMap._actualCanvas;
DomUtil.addClass(canvas, "leaflet-image-layer");
DomUtil.addClass(canvas, "leaflet-zoom-animated");
if (this.options.interactive) {
DomUtil.addClass(canvas, "leaflet-interactive");
}
if (this.options.className) {
DomUtil.addClass(canvas, this.options.className);
}
},
_update() {
if (!this._map) {
return;
}
// update the offset, so we can correct for it later when we zoom
this._offset = this._map.containerPointToLayerPoint([0, 0]);
if (this._zooming) {
return;
}
const size = this.getSize();
const container = this._container;
const gl = this._glMap;
const offset = this._map.getSize().multiplyBy(this.options.padding);
const topLeft = this._map
.containerPointToLayerPoint([0, 0])
.subtract(offset);
DomUtil.setPosition(container, topLeft);
this._transformGL(gl);
if (gl.transform.width !== size.x || gl.transform.height !== size.y) {
container.style.width = `${size.x}px`;
container.style.height = `${size.y}px`;
if (gl._resize !== null && gl._resize !== undefined) {
gl._resize();
} else {
gl.resize();
}
} else {
// older versions of mapbox-gl surfaced update publicly
gl._update();
}
},
_transformGL(gl) {
const center = this._map.getCenter();
// gl.setView([center.lat, center.lng], this._map.getZoom() - 1, 0);
// calling setView directly causes sync issues because it uses requestAnimFrame
const tr = gl.transform;
tr.center = maplibregl.LngLat.convert([center.lng, center.lat]);
tr.zoom = this._map.getZoom() - 1;
},
// update the map constantly during a pinch zoom
_pinchZoom() {
this._glMap.jumpTo({
zoom: this._map.getZoom() - 1,
center: this._map.getCenter(),
});
},
// borrowed from L.ImageOverlay
// https://github.com/Leaflet/Leaflet/blob/master/src/layer/ImageOverlay.js#L139-L144
_animateZoom(e) {
const scale = this._map.getZoomScale(e.zoom);
const padding = this._map
.getSize()
.multiplyBy(this.options.padding * scale);
const viewHalf = this.getSize()._divideBy(2);
// corrections for padding (scaled), adapted from
// https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L1490-L1508
const topLeft = this._map
.project(e.center, e.zoom)
._subtract(viewHalf)
._add(this._map._getMapPanePos().add(padding))
._round();
const offset = this._map
.project(this._map.getBounds().getNorthWest(), e.zoom)
._subtract(topLeft);
DomUtil.setTransform(
this._glMap._actualCanvas,
offset.subtract(this._offset),
scale,
);
},
_zoomStart() {
this._zooming = true;
},
_zoomEnd() {
const scale = this._map.getZoomScale(this._map.getZoom());
DomUtil.setTransform(this._glMap._actualCanvas, null, scale);
this._zooming = false;
this._update();
},
_transitionEnd() {
Util.requestAnimFrame(function () {
const zoom = this._map.getZoom();
const center = this._map.getCenter();
const offset = this._map.latLngToContainerPoint(
this._map.getBounds().getNorthWest(),
);
// reset the scale and offset
DomUtil.setTransform(this._glMap._actualCanvas, offset, 1);
// enable panning once the gl map is ready again
this._glMap.once(
"moveend",
Util.bind(function () {
this._zoomEnd();
}, this),
);
// update the map position
this._glMap.jumpTo({
center,
zoom: zoom - 1,
});
}, this);
},
});
export function maplibreGLJSLayer(options) {
return new MaplibreGLJSLayer(options);
}