@allmaps/leaflet
Version:
Allmaps Leaflet plugin
837 lines (836 loc) • 24.6 kB
JavaScript
import * as L from "leaflet";
import { WebGL2Renderer } from "@allmaps/render/webgl2";
import { Viewport, WarpedMapEventType, WarpedMapEvent } from "@allmaps/render";
import { isValidHttpUrl, hexToFractionalRgb, rectangleToSize, sizesToScale } from "@allmaps/stdlib";
const NO_RENDERER_ERROR_MESSAGE = "Renderer not defined. Add the layer to a map before calling this function.";
const NO_CANVAS_ERROR_MESSAGE = "Canvas not defined. Add the layer to a map before calling this function.";
const DEFAULT_PANE = "tilePane";
const DEFAULT_OPACITY = 1;
function assertRenderer(renderer) {
if (!renderer) {
throw new Error(NO_RENDERER_ERROR_MESSAGE);
}
}
function assertCanvas(canvas) {
if (!canvas) {
throw new Error(NO_CANVAS_ERROR_MESSAGE);
}
}
class WarpedMapLayer extends L.Layer {
container;
canvas;
gl;
renderer;
_annotationOrAnnotationUrl;
resizeObserver;
options = {
opacity: DEFAULT_OPACITY,
interactive: false,
className: "",
pane: DEFAULT_PANE,
zIndex: 1
};
/**
* Creates a WarpedMapLayer
* @param annotationOrAnnotationUrl - Georeference Annotation or URL of a Georeference Annotation
* @param options - Options for the layer
*/
constructor(annotationOrAnnotationUrl, options) {
super();
this.initialize(annotationOrAnnotationUrl, options);
}
initialize(annotationOrAnnotationUrl, options) {
this._annotationOrAnnotationUrl = annotationOrAnnotationUrl;
L.setOptions(this, options);
this._initGl();
}
/**
* Contains all code code that creates DOM elements for the layer and adds them to map panes where they belong.
*/
onAdd(map) {
if (!this._map || !this.container) {
return this;
}
const paneName = this.getPaneName();
const pane = this._map.getPane(paneName);
pane?.appendChild(this.container);
map.on("zoomend viewreset move", this._update, this);
map.on("zoomanim", this._animateZoom, this);
map.on("unload", this._unload, this);
this.resizeObserver = new ResizeObserver(this._resized.bind(this));
this.resizeObserver.observe(this._map.getContainer(), {
box: "content-box"
});
if (this._annotationOrAnnotationUrl) {
if (typeof this._annotationOrAnnotationUrl === "string" && isValidHttpUrl(this._annotationOrAnnotationUrl)) {
this.addGeoreferenceAnnotationByUrl(
this._annotationOrAnnotationUrl
).then(() => this._update());
} else {
this.addGeoreferenceAnnotation(this._annotationOrAnnotationUrl).then(
() => this._update()
);
}
}
return this;
}
/**
* Contains all cleanup code that removes the layer's elements from the DOM.
*/
onRemove(map) {
if (this.container) {
this.container.remove();
}
map.off("zoomend viewreset move", this._update, this);
map.off("zoomanim", this._animateZoom, this);
return this;
}
/**
* Adds a [Georeference Annotation](https://iiif.io/api/extension/georef/).
* @param annotation - Georeference Annotation
* @returns - the map IDs of the maps that were added, or an error per map
*/
async addGeoreferenceAnnotation(annotation) {
assertRenderer(this.renderer);
const results = await this.renderer.warpedMapList.addGeoreferenceAnnotation(annotation);
this._update();
return results;
}
/**
* Removes a [Georeference Annotation](https://iiif.io/api/extension/georef/).
* @param annotation - Georeference Annotation
* @returns - the map IDs of the maps that were removed, or an error per map
*/
async removeGeoreferenceAnnotation(annotation) {
assertRenderer(this.renderer);
const results = await this.renderer.warpedMapList.removeGeoreferenceAnnotation(annotation);
this._update();
return results;
}
/**
* Adds a [Georeference Annotation](https://iiif.io/api/extension/georef/) by URL.
* @param annotationUrl - Georeference Annotation
* @returns The map IDs of the maps that were added, or an error per map
*/
async addGeoreferenceAnnotationByUrl(annotationUrl) {
const annotation = await fetch(annotationUrl).then(
(response) => response.json()
);
return this.addGeoreferenceAnnotation(annotation);
}
/**
* Removes a [Georeference Annotation](https://iiif.io/api/extension/georef/) by URL.
* @param annotationUrl - Georeference Annotation
* @returns The map IDs of the maps that were removed, or an error per map
*/
async removeGeoreferenceAnnotationByUrl(annotationUrl) {
const annotation = await fetch(annotationUrl).then(
(response) => response.json()
);
const results = this.removeGeoreferenceAnnotation(annotation);
return results;
}
/**
* Adds a Georeferenced map.
* @param georeferencedMap - Georeferenced map
* @returns The map ID of the map that was added, or an error
*/
async addGeoreferencedMap(georeferencedMap) {
assertRenderer(this.renderer);
const result = this.renderer.warpedMapList.addGeoreferencedMap(georeferencedMap);
this._update();
return result;
}
/**
* Removes a Georeferenced map.
* @param georeferencedMap - Georeferenced map
* @returns The map ID of the map that was removed, or an error
*/
async removeGeoreferencedMap(georeferencedMap) {
assertRenderer(this.renderer);
const result = this.renderer.warpedMapList.removeGeoreferencedMap(georeferencedMap);
this._update();
return result;
}
/**
* Gets the HTML container element of the layer
* @returns HTML Div Element
*/
getContainer() {
return this.container;
}
/**
* Gets the HTML canvas element of the layer
* @returns HTML Canvas Element
*/
getCanvas() {
return this.canvas;
}
/**
* Returns the WarpedMapList object that contains a list of the warped maps of all loaded maps
*/
getWarpedMapList() {
assertRenderer(this.renderer);
return this.renderer.warpedMapList;
}
/**
* Returns a single map's warped map
* @param mapId - ID of the map
* @returns the warped map
*/
getWarpedMap(mapId) {
assertRenderer(this.renderer);
return this.renderer.warpedMapList.getWarpedMap(mapId);
}
/**
* Make a single map visible
* @param mapId - ID of the map
*/
showMap(mapId) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.showMaps([mapId]);
this._update();
}
/**
* Make multiple maps visible
* @param mapIds - IDs of the maps
*/
showMaps(mapIds) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.showMaps(mapIds);
this._update();
}
/**
* Make a single map invisible
* @param mapId - ID of the map
*/
hideMap(mapId) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.hideMaps([mapId]);
this._update();
}
/**
* Make multiple maps invisible
* @param mapIds - IDs of the maps
*/
hideMaps(mapIds) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.hideMaps(mapIds);
this._update();
}
/**
* Returns the visibility of a single map
* @returns - whether the map is visible
*/
isMapVisible(mapId) {
assertRenderer(this.renderer);
const warpedMap = this.renderer.warpedMapList.getWarpedMap(mapId);
return warpedMap?.visible;
}
/**
* Sets the resource mask of a single map
* @param mapId - ID of the map
* @param resourceMask - new resource mask
*/
setMapResourceMask(mapId, resourceMask) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.setMapResourceMask(resourceMask, mapId);
this._update();
}
/**
* Sets the transformation type of multiple maps
* @param mapIds - IDs of the maps
* @param transformation - new transformation type
*/
setMapsTransformationType(mapIds, transformation) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.setMapsTransformationType(transformation, {
mapIds
});
this._update();
}
/**
* Sets the distortion measure of multiple maps
* @param mapIds - IDs of the maps
* @param distortionMeasure - new transformation type
*/
setMapsDistortionMeasure(mapIds, distortionMeasure) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.setMapsDistortionMeasure(distortionMeasure, {
mapIds
});
this._update();
}
/**
* Returns the bounds of all visible maps (inside or outside of the Viewport), in latitude/longitude coordinates.
* @returns - L.LatLngBounds in array form of all visible maps
*/
getBounds() {
assertRenderer(this.renderer);
const bbox = this.renderer.warpedMapList.getMapsBbox({
projection: { definition: "EPSG:4326" }
});
if (bbox) {
return [
[bbox[1], bbox[0]],
[bbox[3], bbox[2]]
];
}
}
/**
* Bring maps to front
* @param mapIds - IDs of the maps
*/
bringMapsToFront(mapIds) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.bringMapsToFront(mapIds);
this._update();
}
/**
* Send maps to back
* @param mapIds - IDs of the maps
*/
sendMapsToBack(mapIds) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.sendMapsToBack(mapIds);
this._update();
}
/**
* Bring maps forward
* @param mapIds - IDs of the maps
*/
bringMapsForward(mapIds) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.bringMapsForward(mapIds);
this._update();
}
/**
* Send maps backward
* @param mapIds - IDs of the maps
*/
sendMapsBackward(mapIds) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.sendMapsBackward(mapIds);
this._update();
}
/**
* Brings the layer in front of other overlays (in the same map pane).
*/
bringToFront() {
if (this._map && this.container) {
L.DomUtil.toFront(this.container);
}
return this;
}
/**
* Brings the layer to the back of other overlays (in the same map pane).
*/
bringToBack() {
if (this._map && this.container) {
L.DomUtil.toBack(this.container);
}
return this;
}
/**
* Returns the z-index of a single map
* @param mapId - ID of the map
* @returns - z-index of the map
*/
getMapZIndex(mapId) {
assertRenderer(this.renderer);
return this.renderer.warpedMapList.getMapZIndex(mapId);
}
/**
* Gets the z-index of the layer.
*/
getZIndex() {
return this.options.zIndex;
}
/**
* Changes the z-index of the layer.
* @param value - z-index
*/
setZIndex(value) {
this.options.zIndex = value;
this._updateZIndex();
return this;
}
/**
* Sets the object that caches image information
*
* @param imageInformations - Object that caches image information
*/
setImageInformations(imageInformations) {
assertRenderer(this.renderer);
this.renderer.warpedMapList.setImageInformations(imageInformations);
}
/**
* Gets the pane name the layer is attached to. Defaults to 'tilePane'
* @returns Pane name
*/
getPaneName() {
return this.options.pane || DEFAULT_PANE;
}
/**
* Gets the opacity of the layer
* @returns Layer opacity
*/
getOpacity() {
return this.options.opacity || DEFAULT_OPACITY;
}
/**
* Sets the opacity of the layer
* @param opacity - Layer opacity
*/
setOpacity(opacity) {
this.options.opacity = opacity;
this._update();
return this;
}
/**
* Resets the opacity of the layer to fully opaque
*/
resetOpacity() {
this.options.opacity = 1;
this._update();
return this;
}
/**
* Sets the options
*
* @param options - Options
*/
setOptions(options) {
assertRenderer(this.renderer);
this.renderer.setOptions(options);
}
/**
* Gets the opacity of a single map
* @param mapId - ID of the map
* @returns opacity of the map
*/
getMapOpacity(mapId) {
assertRenderer(this.renderer);
return this.renderer.getMapOpacity(mapId);
}
/**
* Sets the opacity of a single map
* @param mapId - ID of the map
* @param opacity - opacity between 0 and 1, where 0 is fully transparent and 1 is fully opaque
*/
setMapOpacity(mapId, opacity) {
assertRenderer(this.renderer);
this.renderer.setMapOpacity(mapId, opacity);
this._update();
return this;
}
/**
* Resets the opacity of a single map to 1
* @param mapId - ID of the map
*/
resetMapOpacity(mapId) {
assertRenderer(this.renderer);
this.renderer.resetMapOpacity(mapId);
this._update();
return this;
}
/**
* Sets the saturation of a single map
* @param saturation - saturation between 0 and 1, where 0 is grayscale and 1 are the original colors
*/
setSaturation(saturation) {
assertRenderer(this.renderer);
this.renderer.setSaturation(saturation);
this._update();
return this;
}
/**
* Resets the saturation of a single map to the original colors
*/
resetSaturation() {
assertRenderer(this.renderer);
this.renderer.resetSaturation();
this._update();
return this;
}
/**
* Sets the saturation of a single map
* @param mapId - ID of the map
* @param saturation - saturation between 0 and 1, where 0 is grayscale and 1 are the original colors
*/
setMapSaturation(mapId, saturation) {
assertRenderer(this.renderer);
this.renderer.setMapSaturation(mapId, saturation);
this._update();
return this;
}
/**
* Resets the saturation of a single map to the original colors
* @param mapId - ID of the map
*/
resetMapSaturation(mapId) {
assertRenderer(this.renderer);
this.renderer.resetMapSaturation(mapId);
this._update();
return this;
}
/**
* Removes a color from all maps
* @param options - remove color options
* @param options.hexColor - hex color to remove
* @param options.threshold - threshold between 0 and 1
* @param options.hardness - hardness between 0 and 1
*/
setRemoveColor(options) {
assertRenderer(this.renderer);
const color = options.hexColor ? hexToFractionalRgb(options.hexColor) : void 0;
this.renderer.setRemoveColorOptions({
color,
threshold: options.threshold,
hardness: options.hardness
});
this._update();
return this;
}
/**
* Resets the color removal for all maps
*/
resetRemoveColor() {
assertRenderer(this.renderer);
this.renderer.resetRemoveColorOptions();
this._update();
return this;
}
/**
* Removes a color from a single map
* @param mapId - ID of the map
* @param options - remove color options
* @param options.hexColor - hex color to remove
* @param options.threshold - threshold between 0 and 1
* @param options.hardness - hardness between 0 and 1
*/
setMapRemoveColor(mapId, options) {
assertRenderer(this.renderer);
const color = options.hexColor ? hexToFractionalRgb(options.hexColor) : void 0;
this.renderer.setMapRemoveColorOptions(mapId, {
color,
threshold: options.threshold,
hardness: options.hardness
});
this._update();
return this;
}
/**
* Resets the color removal for a single map
* @param mapId - ID of the map
*/
resetMapRemoveColor(mapId) {
assertRenderer(this.renderer);
this.renderer.resetMapRemoveColorOptions(mapId);
return this;
}
/**
* Sets the colorization for all maps
* @param hexColor - desired hex color
*/
setColorize(hexColor) {
assertRenderer(this.renderer);
const color = hexToFractionalRgb(hexColor);
if (color) {
this.renderer.setColorizeOptions({ color });
this._update();
}
return this;
}
/**
* Resets the colorization for all maps
*/
resetColorize() {
assertRenderer(this.renderer);
this.renderer.resetColorizeOptions();
this._update();
return this;
}
/**
* Sets the colorization for a single map
* @param mapId - ID of the map
* @param hexColor - desired hex color
*/
setMapColorize(mapId, hexColor) {
assertRenderer(this.renderer);
const color = hexToFractionalRgb(hexColor);
if (color) {
this.renderer.setMapColorizeOptions(mapId, { color });
this._update();
}
return this;
}
/**
* Resets the colorization of a single map
* @param mapId - ID of the map
*/
resetMapColorize(mapId) {
assertRenderer(this.renderer);
this.renderer.resetMapColorizeOptions(mapId);
this._update();
return this;
}
/**
* Removes all warped maps from the layer
*/
clear() {
assertRenderer(this.renderer);
this.renderer.clear();
this._update();
return this;
}
_initGl() {
this.container = L.DomUtil.create("div");
this.container.classList.add("leaflet-layer");
this.container.classList.add("allmaps-warped-map-layer");
if (this.options.zIndex) {
this._updateZIndex();
}
this.canvas = L.DomUtil.create("canvas", void 0, this.container);
this.canvas.classList.add("leaflet-zoom-animated");
this.canvas.classList.add("leaflet-image-layer");
if (this.options.interactive) {
this.canvas.classList.add("leaflet-interactive");
}
if (this.options.className) {
this.canvas.classList.add(this.options.className);
}
this.gl = this.canvas.getContext("webgl2", {
premultipliedAlpha: true
});
if (!this.gl) {
throw new Error("WebGL 2 not available");
}
this.renderer = new WebGL2Renderer(this.gl);
this._addEventListeners();
}
_resized(entries) {
if (!this.canvas) {
return;
}
for (const entry of entries) {
const width = entry.contentRect.width;
const height = entry.contentRect.height;
const dpr = window.devicePixelRatio;
const displayWidth = Math.round(width * dpr);
const displayHeight = Math.round(height * dpr);
this.canvas.width = displayWidth;
this.canvas.height = displayHeight;
this.canvas.style.width = width + "px";
this.canvas.style.height = height + "px";
}
this._update();
}
// Note: borrowed from L.ImageOverlay
// https://github.com/Leaflet/Leaflet/blob/3b62c7ec96242ee4040cf438a8101a48f8da316d/src/layer/ImageOverlay.js#L225
_animateZoom(e) {
if (!this.canvas) {
return;
}
const scale = this._map.getZoomScale(e.zoom);
const offset = this._map._latLngBoundsToNewLayerBounds(
this._map.getBounds(),
e.zoom,
e.center
).min;
L.DomUtil.setTransform(this.canvas, offset, scale);
}
_updateZIndex() {
if (this.container && this.options.zIndex !== void 0) {
this.container.style.zIndex = String(this.options.zIndex);
}
}
_update() {
if (!this._map || !this.renderer || !this.canvas || !this._map.options.crs) {
return;
}
const topLeft = this._map.containerPointToLayerPoint([0, 0]);
L.DomUtil.setPosition(this.canvas, topLeft);
this.renderer.setOpacity(this.getOpacity());
const viewportSizeAsPoint = this._map.getSize();
const viewportSize = [viewportSizeAsPoint.x, viewportSizeAsPoint.y];
const geoCenterAsPoint = this._map.getCenter();
const projectedGeoCenterAsPoint = this._map.options.crs.project(geoCenterAsPoint);
const projectedGeoCenter = [
projectedGeoCenterAsPoint.x,
projectedGeoCenterAsPoint.y
];
const geoBboxAsLatLngBounds = this._map.getBounds();
const projectedNorthEastAsPoint = this._map.options.crs.project(
geoBboxAsLatLngBounds.getNorthEast()
);
const projectedNorthWestAsPoint = this._map.options.crs.project(
geoBboxAsLatLngBounds.getNorthWest()
);
const projectedSouthWestAsPoint = this._map.options.crs.project(
geoBboxAsLatLngBounds.getSouthWest()
);
const projectedSouthEastAsPoint = this._map.options.crs.project(
geoBboxAsLatLngBounds.getSouthEast()
);
const projectedGeoRectangle = [
[projectedNorthEastAsPoint.x, projectedNorthEastAsPoint.y],
[projectedNorthWestAsPoint.x, projectedNorthWestAsPoint.y],
[projectedSouthWestAsPoint.x, projectedSouthWestAsPoint.y],
[projectedSouthEastAsPoint.x, projectedSouthEastAsPoint.y]
];
const projectedGeoSize = rectangleToSize(projectedGeoRectangle);
const projectedGeoPerViewportScale = sizesToScale(
projectedGeoSize,
viewportSize
);
const devicePixelRatio = window.devicePixelRatio;
const viewport = new Viewport(
viewportSize,
projectedGeoCenter,
projectedGeoPerViewportScale,
{ devicePixelRatio }
);
this.renderer.render(viewport);
return this.container;
}
_contextLost(event) {
event.preventDefault();
this.renderer?.contextLost();
}
_contextRestored(event) {
event.preventDefault();
this.renderer?.contextRestored();
}
_addEventListeners() {
assertRenderer(this.renderer);
assertCanvas(this.canvas);
this.canvas.addEventListener(
"webglcontextlost",
this._contextLost.bind(this)
);
this.canvas.addEventListener(
"webglcontextrestored",
this._contextRestored.bind(this)
);
this.renderer.addEventListener(
WarpedMapEventType.CHANGED,
this._update.bind(this)
);
this.renderer.addEventListener(
WarpedMapEventType.IMAGEINFOLOADED,
this._update.bind(this)
);
this.renderer.addEventListener(
WarpedMapEventType.WARPEDMAPENTER,
this._passWarpedMapEvent.bind(this)
);
this.renderer.addEventListener(
WarpedMapEventType.WARPEDMAPLEAVE,
this._passWarpedMapEvent.bind(this)
);
this.renderer.tileCache.addEventListener(
WarpedMapEventType.FIRSTMAPTILELOADED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.tileCache.addEventListener(
WarpedMapEventType.ALLREQUESTEDTILESLOADED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.warpedMapList.addEventListener(
WarpedMapEventType.WARPEDMAPADDED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.warpedMapList.addEventListener(
WarpedMapEventType.WARPEDMAPREMOVED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.warpedMapList.addEventListener(
WarpedMapEventType.VISIBILITYCHANGED,
this._update.bind(this)
);
this.renderer.warpedMapList.addEventListener(
WarpedMapEventType.CLEARED,
this._update.bind(this)
);
}
_removeEventListeners() {
assertRenderer(this.renderer);
assertCanvas(this.canvas);
this.canvas.addEventListener(
"webglcontextlost",
this._contextLost.bind(this)
);
this.canvas.addEventListener(
"webglcontextrestored",
this._contextRestored.bind(this)
);
this.renderer.removeEventListener(
WarpedMapEventType.CHANGED,
this._update.bind(this)
);
this.renderer.removeEventListener(
WarpedMapEventType.IMAGEINFOLOADED,
this._update.bind(this)
);
this.renderer.removeEventListener(
WarpedMapEventType.WARPEDMAPENTER,
this._passWarpedMapEvent.bind(this)
);
this.renderer.removeEventListener(
WarpedMapEventType.WARPEDMAPLEAVE,
this._passWarpedMapEvent.bind(this)
);
this.renderer.tileCache.removeEventListener(
WarpedMapEventType.FIRSTMAPTILELOADED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.tileCache.removeEventListener(
WarpedMapEventType.ALLREQUESTEDTILESLOADED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.warpedMapList.removeEventListener(
WarpedMapEventType.WARPEDMAPADDED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.warpedMapList.removeEventListener(
WarpedMapEventType.WARPEDMAPREMOVED,
this._passWarpedMapEvent.bind(this)
);
this.renderer.warpedMapList.removeEventListener(
WarpedMapEventType.VISIBILITYCHANGED,
this._update.bind(this)
);
this.renderer.warpedMapList.removeEventListener(
WarpedMapEventType.CLEARED,
this._update.bind(this)
);
}
_passWarpedMapEvent(event) {
if (event instanceof WarpedMapEvent) {
if (this._map) {
this._map.fire(event.type, event.data);
}
}
}
_unload() {
assertRenderer(this.renderer);
if (!this.gl) {
return;
}
this.renderer.destroy();
const extension = this.gl.getExtension("WEBGL_lose_context");
if (extension) {
extension.loseContext();
}
const canvas = this.gl.canvas;
canvas.width = 1;
canvas.height = 1;
this.resizeObserver?.disconnect();
this._removeEventListeners();
}
}
export {
WarpedMapLayer
};
//# sourceMappingURL=WarpedMapLayer.js.map