@allmaps/render
Version:
Render functions for WebGL and image buffers
667 lines (666 loc) • 21.6 kB
JavaScript
import { generateChecksum } from "@allmaps/id";
import { validateGeoreferencedMap, parseAnnotation } from "@allmaps/annotation";
import { isEqualProjection, proj4 } from "@allmaps/project";
import { RTree } from "./RTree.js";
import { mergePartialOptions, mergeOptions, bboxToCenter, computeBbox, convexHull } from "@allmaps/stdlib";
import { WarpedMapEvent, WarpedMapEventType } from "../shared/events.js";
const defaultSelectionOptions = {
onlyVisible: true
};
const defaultWarpedMapListOptions = {
createRTree: true,
imageInformations: /* @__PURE__ */ new Map()
};
class WarpedMapList extends EventTarget {
warpedMapFactory;
/**
* Maps in this list, indexed by their ID
*/
warpedMapsById;
zIndices;
rtree;
partialWarpedMapListOptions;
/**
* Creates an instance of a WarpedMapList
*
* @constructor
* @param warpedMapFactory - Factory function for creating WarpedMap objects
* @param partialWarpedMapListOptions - Options
*/
constructor(warpedMapFactory, partialWarpedMapListOptions) {
super();
this.warpedMapsById = /* @__PURE__ */ new Map();
this.zIndices = /* @__PURE__ */ new Map();
this.warpedMapFactory = warpedMapFactory;
this.partialWarpedMapListOptions = mergePartialOptions(
defaultWarpedMapListOptions,
partialWarpedMapListOptions
);
if (this.partialWarpedMapListOptions.createRTree) {
this.rtree = new RTree();
}
}
/**
* Get mapIds for selected maps
*
* Also allows to only select maps whose geoBbox overlaps with the specified geoBbox
*
* @param partialSelectionOptions - Selection options (e.g. mapIds), defaults to all visible maps
* @returns mapIds
*/
getMapIds(partialSelectionOptions) {
return Array.from(this.getWarpedMaps(partialSelectionOptions)).map(
(warpedMap) => warpedMap.mapId
);
}
/**
* Get the WarpedMap instances for selected maps
*
* Also allows to only select maps whose geoBbox overlaps with the specified geoBbox
*
* @param partialSelectionOptions - Selection options (e.g. mapIds), defaults to all visible maps
* @returns WarpedMap instances
*/
getWarpedMaps(partialSelectionOptions) {
const selectionOptions = mergeOptions(
defaultSelectionOptions,
partialSelectionOptions
);
let mapIds;
if (selectionOptions.mapIds === void 0) {
if (this.rtree && selectionOptions.geoBbox) {
mapIds = this.rtree.searchFromBbox(selectionOptions.geoBbox);
} else {
mapIds = Array.from(this.warpedMapsById.keys());
}
} else {
mapIds = selectionOptions.mapIds;
}
const warpedMaps = [];
if (mapIds === void 0) {
return warpedMaps;
}
for (const mapId of mapIds) {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap && (selectionOptions.onlyVisible ? warpedMap.visible : true)) {
warpedMaps.push(warpedMap);
}
}
return warpedMaps;
}
/**
* Get the WarpedMap instance for a specific map
*
* @param mapId - Map ID of the requested WarpedMap instance
* @returns WarpedMap instance, or undefined
*/
getWarpedMap(mapId) {
return this.warpedMapsById.get(mapId);
}
/**
* Get the z-index for a specific map
*
* @param mapId
* @returns
*/
getMapZIndex(mapId) {
return this.zIndices.get(mapId);
}
/**
* Get the center of the bounding box of the maps in this list
*
* Use {projection: 'EPSG:4326'} to request the result in lon-lat `EPSG:4326`
*
* @param partialSelectionAndProjectionOptions - Selection (e.g. mapIds) and projection options, defaults to all visible maps and current projection
* @returns The center of the bbox of all selected maps, in the chosen projection, or undefined if there were no maps matching the selection.
*/
getMapsCenter(partialSelectionAndProjectionOptions) {
const bbox = this.getMapsBbox(partialSelectionAndProjectionOptions);
if (bbox) {
return bboxToCenter(bbox);
}
}
/**
* Get the bounding box of the maps in this list
*
* Use {projection: 'EPSG:4326'} to request the result in lon-lat `EPSG:4326`
*
* @param partialSelectionAndProjectionOptions - Selection (e.g. mapIds) and projection options, defaults to all visible maps and current projection
* @returns The bbox of all selected maps, in the chosen projection, or undefined if there were no maps matching the selection.
*/
getMapsBbox(partialSelectionAndProjectionOptions) {
const projectedGeoMaskPoints = this.getProjectedGeoMaskPoints(
partialSelectionAndProjectionOptions
);
return computeBbox(projectedGeoMaskPoints);
}
/**
* Get the convex hull of the maps in this list
*
* Use {projection: 'EPSG:4326'} to request the result in lon-lat `EPSG:4326`
*
* @param partialSelectionAndProjectionOptions - Selection (e.g. mapIds) and projection options, defaults to all visible maps and current projection
* @returns The convex hull of all selected maps, in the chosen projection, or undefined if there were no maps matching the selection.
*/
getMapsConvexHull(partialSelectionAndProjectionOptions) {
const projectedGeoMaskPoints = this.getProjectedGeoMaskPoints(
partialSelectionAndProjectionOptions
);
return convexHull(projectedGeoMaskPoints);
}
/**
* Set the Warped Map List options
*
* @param partialWarpedMapListOptions - Options
*/
setOptions(partialWarpedMapListOptions) {
this.partialWarpedMapListOptions = mergeOptions(
this.partialWarpedMapListOptions,
partialWarpedMapListOptions
);
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.OPTIONSCHANGED, this.getMapIds())
);
}
/**
* Sets the object that caches image information
*
* @param imageInformations - object that caches image information
*/
setImageInformations(imageInformations) {
this.partialWarpedMapListOptions.imageInformations = imageInformations;
}
/**
* Sets the GCPs for a specific map
*
* @param gcps - new GCPs
* @param mapId - ID of the map
*/
setMapGcps(gcps, mapId) {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.setGcps(gcps);
this.addToOrUpdateRtree(warpedMap);
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.GCPSCHANGED, mapId)
);
}
}
/**
* Sets the resource mask for a specific map
*
* @param resourceMask - the new resource mask
* @param mapId - ID of the map
*/
setMapResourceMask(resourceMask, mapId) {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.setResourceMask(resourceMask);
this.addToOrUpdateRtree(warpedMap);
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.RESOURCEMASKCHANGED, mapId)
);
}
}
/**
* Sets the transformation type for a specific map
*
* @param transformationType - the new transformation type
* @param mapId - the ID of the map
*/
setMapTransformationType(transformationType, mapId) {
this.setMapsTransformationType(transformationType, { mapIds: [mapId] });
}
/**
* Sets the transformation type for selected maps
*
* @param transformationType - the new transformation type
* @param partialSelectionOptions - Selection options (e.g. mapIds), defaults to all visible maps
*/
setMapsTransformationType(transformationType, partialSelectionOptions) {
const mapIdsChanged = [];
const warpedMaps = this.getWarpedMaps(partialSelectionOptions);
for (const warpedMap of warpedMaps) {
if (warpedMap.transformationType !== transformationType) {
mapIdsChanged.push(warpedMap.mapId);
}
}
if (mapIdsChanged.length > 0) {
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.PRECHANGE, mapIdsChanged)
);
mapIdsChanged.forEach((mapId) => {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.setTransformationType(transformationType);
this.addToOrUpdateRtree(warpedMap);
}
});
this.dispatchEvent(
new WarpedMapEvent(
WarpedMapEventType.TRANSFORMATIONCHANGED,
mapIdsChanged
)
);
}
}
/**
* Sets the distortionMeasure for a specific map
*
* @param distortionMeasure - the distortion measure
* @param mapId - the ID of the map
*/
setMapDistortionMeasure(distortionMeasure, mapId) {
this.setMapsDistortionMeasure(distortionMeasure, { mapIds: [mapId] });
}
/**
* Sets the distortion measure for selected maps
*
* @param distortionMeasure - the distortion measure
* @param partialSelectionOptions - Selection options (e.g. mapIds), defaults to all visible maps
*/
setMapsDistortionMeasure(distortionMeasure, partialSelectionOptions) {
const mapIdsChanged = [];
const warpedMaps = this.getWarpedMaps(partialSelectionOptions);
for (const warpedMap of warpedMaps) {
if (warpedMap.distortionMeasure !== distortionMeasure) {
mapIdsChanged.push(warpedMap.mapId);
}
}
if (mapIdsChanged.length > 0) {
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.PRECHANGE, mapIdsChanged)
);
mapIdsChanged.forEach((mapId) => {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.setDistortionMeasure(distortionMeasure);
}
});
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.DISTORTIONCHANGED, mapIdsChanged)
);
}
}
/**
* Sets the internal projection for a specific map
*
* @param projection - the internal projection
* @param mapId - the ID of the map
*/
setMapInternalProjection(projection, mapId) {
this.setMapsInternalProjection(projection, { mapIds: [mapId] });
}
/**
* Sets the internal projection for selected maps
*
* @param projection - the internal projection
* @param partialSelectionOptions - Selection options (e.g. mapIds), defaults to all visible maps
*/
setMapsInternalProjection(projection, partialSelectionOptions) {
const mapIdsChanged = [];
const warpedMaps = this.getWarpedMaps(partialSelectionOptions);
for (const warpedMap of warpedMaps) {
if (!isEqualProjection(warpedMap.internalProjection, projection)) {
mapIdsChanged.push(warpedMap.mapId);
}
}
if (mapIdsChanged.length > 0) {
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.PRECHANGE, mapIdsChanged)
);
mapIdsChanged.forEach((mapId) => {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.setInternalProjection(projection);
this.addToOrUpdateRtree(warpedMap);
}
});
this.dispatchEvent(
new WarpedMapEvent(
WarpedMapEventType.INTERNALPROJECTIONCHANGED,
mapIdsChanged
)
);
}
}
/**
* Sets the projection for a specific map
*
* @param projection - the projection
* @param mapId - the ID of the map
*/
setMapProjection(projection, mapId) {
this.setMapsProjection(projection, { mapIds: [mapId] });
}
/**
* Sets the projection for selected maps
*
* @param projection - the projection
* @param partialSelectionOptions - Selection options (e.g. mapIds), defaults to all visible maps
*/
setMapsProjection(projection, partialSelectionOptions) {
const mapIdsChanged = [];
const warpedMaps = this.getWarpedMaps(partialSelectionOptions);
for (const warpedMap of warpedMaps) {
if (!isEqualProjection(warpedMap.projection, projection)) {
mapIdsChanged.push(warpedMap.mapId);
}
}
if (mapIdsChanged.length > 0) {
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.PRECHANGE, mapIdsChanged)
);
mapIdsChanged.forEach((mapId) => {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.setProjection(projection);
this.addToOrUpdateRtree(warpedMap);
}
});
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.PROJECTIONCHANGED, mapIdsChanged)
);
}
}
/**
* Removes a warped map by its ID
*
* @param mapId - the ID of the map
*
* @param mapIds - Map IDs
*/
removeGeoreferencedMapById(mapId) {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
this.removeGeoreferencedMap(warpedMap);
this.removeFromRtree(warpedMap);
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.WARPEDMAPREMOVED, mapId)
);
}
}
/**
* Changes the z-index of the specified maps to bring them to front
*
* @param mapIds - Map IDs
*/
bringMapsToFront(mapIds) {
let newZIndex = this.warpedMapsById.size;
for (const mapId of mapIds) {
if (this.zIndices.has(mapId)) {
this.zIndices.set(mapId, newZIndex);
newZIndex++;
}
}
this.removeZIndexHoles();
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.ZINDICESCHANGED));
}
/**
* Changes the z-index of the specified maps to send them to back
*
* @param mapIds - Map IDs
*/
sendMapsToBack(mapIds) {
let newZIndex = -Array.from(mapIds).length;
for (const mapId of mapIds) {
if (this.zIndices.has(mapId)) {
this.zIndices.set(mapId, newZIndex);
newZIndex++;
}
}
this.removeZIndexHoles();
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.ZINDICESCHANGED));
}
/**
* Changes the z-index of the specified maps to bring them forward
*
* @param mapIds - Map IDs
*/
bringMapsForward(mapIds) {
for (const [mapId, zIndex] of this.zIndices.entries()) {
this.zIndices.set(mapId, zIndex * 2);
}
for (const mapId of mapIds) {
const zIndex = this.zIndices.get(mapId);
if (zIndex !== void 0) {
this.zIndices.set(mapId, zIndex + 3);
}
}
this.removeZIndexHoles();
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.ZINDICESCHANGED));
}
/**
* Changes the zIndex of the specified maps to send them backward
*
* @param mapIds - Map IDs
*/
sendMapsBackward(mapIds) {
for (const [mapId, zIndex] of this.zIndices.entries()) {
this.zIndices.set(mapId, zIndex * 2);
}
for (const mapId of mapIds) {
const zIndex = this.zIndices.get(mapId);
if (zIndex !== void 0) {
this.zIndices.set(mapId, zIndex - 3);
}
}
this.removeZIndexHoles();
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.ZINDICESCHANGED));
}
/**
* Changes the visibility of the specified maps to `true`
*
* @param mapIds - Map IDs
*/
showMaps(mapIds) {
for (const mapId of mapIds) {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.visible = true;
}
}
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.VISIBILITYCHANGED, mapIds)
);
}
/**
* Changes the visibility of the specified maps to `false`
*
* @param mapIds - Map IDs
*/
hideMaps(mapIds) {
for (const mapId of mapIds) {
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
warpedMap.visible = false;
}
}
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.VISIBILITYCHANGED, mapIds)
);
}
/**
* Adds a georeferenced map to this list
*
* @param georeferencedMap
* @returns
*/
async addGeoreferencedMap(georeferencedMap) {
const validatedGeoreferencedMapOrMaps = validateGeoreferencedMap(georeferencedMap);
const validatedGeoreferencedMap = Array.isArray(
validatedGeoreferencedMapOrMaps
) ? validatedGeoreferencedMapOrMaps[0] : validatedGeoreferencedMapOrMaps;
return this.addGeoreferencedMapInternal(validatedGeoreferencedMap);
}
/**
* Removes a georeferenced map from this list
*
* @param georeferencedMap
* @returns
*/
async removeGeoreferencedMap(georeferencedMap) {
const validatedGeoreferencedMapOrMaps = validateGeoreferencedMap(georeferencedMap);
const validatedGeoreferencedMap = Array.isArray(
validatedGeoreferencedMapOrMaps
) ? validatedGeoreferencedMapOrMaps[0] : validatedGeoreferencedMapOrMaps;
return this.removeGeoreferencedMapInternal(validatedGeoreferencedMap);
}
/**
* Parses an annotation and adds its georeferenced map to this list
*
* @param annotation
* @returns
*/
async addGeoreferenceAnnotation(annotation) {
const results = [];
const maps = parseAnnotation(annotation);
const settledResults = await Promise.allSettled(
maps.map((map) => this.addGeoreferencedMapInternal(map))
);
for (const settledResult of settledResults) {
if (settledResult.status === "fulfilled") {
results.push(settledResult.value);
} else {
results.push(settledResult.reason);
}
}
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.GEOREFERENCEANNOTATIONADDED)
);
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.ZINDICESCHANGED));
return results;
}
/**
* Parses an annotation and removes its georeferenced map from this list
*
* @param annotation
* @returns
*/
async removeGeoreferenceAnnotation(annotation) {
const results = [];
const maps = parseAnnotation(annotation);
for (const map of maps) {
const mapIdOrError = await this.removeGeoreferencedMapInternal(map);
results.push(mapIdOrError);
}
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.GEOREFERENCEANNOTATIONREMOVED)
);
return results;
}
clear() {
this.warpedMapsById = /* @__PURE__ */ new Map();
this.zIndices = /* @__PURE__ */ new Map();
this.rtree?.clear();
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.CLEARED));
}
destroy() {
for (const warpedMap of this.getWarpedMaps()) {
this.removeEventListenersFromWarpedMap(warpedMap);
warpedMap.destroy();
}
this.clear();
}
async addGeoreferencedMapInternal(georeferencedMap) {
const mapId = await this.getOrComputeMapId(georeferencedMap);
const warpedMap = this.warpedMapFactory(
mapId,
georeferencedMap,
this.partialWarpedMapListOptions
);
this.warpedMapsById.set(mapId, warpedMap);
this.zIndices.set(mapId, this.warpedMapsById.size - 1);
this.addToOrUpdateRtree(warpedMap);
this.addEventListenersToWarpedMap(warpedMap);
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.WARPEDMAPADDED, mapId)
);
return mapId;
}
async removeGeoreferencedMapInternal(georeferencedMap) {
const mapId = await this.getOrComputeMapId(georeferencedMap);
const warpedMap = this.warpedMapsById.get(mapId);
if (warpedMap) {
this.warpedMapsById.delete(mapId);
this.zIndices.delete(mapId);
this.removeFromRtree(warpedMap);
this.dispatchEvent(
new WarpedMapEvent(WarpedMapEventType.WARPEDMAPREMOVED, mapId)
);
this.removeZIndexHoles();
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.ZINDICESCHANGED));
warpedMap.destroy();
} else {
throw new Error(`No map found with ID ${mapId}`);
}
return mapId;
}
async getOrComputeMapId(georeferencedMap) {
const mapId = georeferencedMap.id || await generateChecksum(georeferencedMap);
return mapId;
}
getProjectedGeoMaskPoints(partialSelectionAndProjectionOptions) {
const warpedMaps = this.getWarpedMaps(partialSelectionAndProjectionOptions);
const projection = partialSelectionAndProjectionOptions?.projection;
if (projection) {
const geoMaskPoints = [];
for (const warpedMap of warpedMaps) {
geoMaskPoints.push(...warpedMap.geoMask);
}
const projectedGeoMaskPoints = geoMaskPoints.map(
(point) => proj4(projection.definition, point)
);
return projectedGeoMaskPoints;
} else {
const projectedGeoMaskPoints = [];
for (const warpedMap of warpedMaps) {
projectedGeoMaskPoints.push(...warpedMap.projectedGeoMask);
}
return projectedGeoMaskPoints;
}
}
addToOrUpdateRtree(warpedMap) {
if (this.rtree) {
this.rtree.removeItem(warpedMap.mapId);
this.rtree.addItem(warpedMap.mapId, [warpedMap.geoMask]);
}
}
removeFromRtree(warpedMap) {
if (this.rtree) {
this.rtree.removeItem(warpedMap.mapId);
}
}
removeZIndexHoles() {
const sortedZIndices = [...this.zIndices.entries()].sort(
(entryA, entryB) => entryA[1] - entryB[1]
);
let zIndex = 0;
for (const entry of sortedZIndices) {
const mapId = entry[0];
this.zIndices.set(mapId, zIndex);
zIndex++;
}
}
// This function and the listeners below transform an IMAGEINFOLOADED event by a WarpedMap
// to an IMAGEINFOLOADED of the WarpedMapList, which is listened to in the Renderer
imageInfoLoaded() {
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.IMAGEINFOLOADED));
}
addEventListenersToWarpedMap(warpedMap) {
warpedMap.addEventListener(
WarpedMapEventType.IMAGEINFOLOADED,
this.imageInfoLoaded.bind(this)
);
}
removeEventListenersFromWarpedMap(warpedMap) {
warpedMap.removeEventListener(
WarpedMapEventType.IMAGEINFOLOADED,
this.imageInfoLoaded.bind(this)
);
}
}
export {
WarpedMapList
};
//# sourceMappingURL=WarpedMapList.js.map