UNPKG

@allmaps/render

Version:

Render functions for WebGL and image buffers

736 lines (735 loc) 24.3 kB
import { generateChecksum } from "@allmaps/id"; import { validateGeoreferencedMap, parseAnnotation } from "@allmaps/annotation"; import { webMercatorProjection, proj4 } from "@allmaps/project"; import { Image } from "@allmaps/iiif-parser"; import { RTree } from "./RTree.js"; import { WebGL2WarpedMap } from "./WebGL2WarpedMap.js"; import { mergeOptions, bboxToCenter, computeBbox, convexHull, optionKeysToUndefinedOptions, optionKeysByMapIdToUndefinedOptionsByMapId, mergePartialOptions } from "@allmaps/stdlib"; import { WarpedMapEvent, WarpedMapEventType } from "../shared/events.js"; const defaultSelectionOptions = {}; const DEFAULT_WARPED_MAP_LIST_OPTIONS = { createRTree: true, rtreeUpdatedOptions: [ "gcps", "resourceMask", "transformationType", "internalProjection", "projection" ], animatedOptions: [ "transformationType", "internalProjection", "distortionMeasure" ], projection: webMercatorProjection }; class WarpedMapList extends EventTarget { warpedMapFactory; /** * Maps in this list, indexed by their ID */ warpedMapsById; zIndices; imagesById; rtree; options; /** * Creates an instance of a WarpedMapList * * @constructor * @param warpedMapFactory - Factory function for creating WarpedMap objects * @param options - Options of this list, which will be set on newly added maps as their list options */ constructor(warpedMapFactory, options) { super(); this.warpedMapsById = /* @__PURE__ */ new Map(); this.zIndices = /* @__PURE__ */ new Map(); this.imagesById = /* @__PURE__ */ new Map(); this.warpedMapFactory = warpedMapFactory; this.options = mergeOptions( DEFAULT_WARPED_MAP_LIST_OPTIONS, options ); if (this.options.createRTree) { this.rtree = new RTree(); } } /** * Adds a georeferenced map to this list * * @param georeferencedMap - Georeferenced Map * @param mapOptions - Map options * @returns Map ID of the map that was added */ async addGeoreferencedMap(georeferencedMap, mapOptions) { const validatedGeoreferencedMapOrMaps = validateGeoreferencedMap(georeferencedMap); const validatedGeoreferencedMap = Array.isArray( validatedGeoreferencedMapOrMaps ) ? validatedGeoreferencedMapOrMaps[0] : validatedGeoreferencedMapOrMaps; return this.addGeoreferencedMapInternal( validatedGeoreferencedMap, mapOptions ); } /** * Removes a georeferenced map from this list * * @param georeferencedMap * @returns Map ID of the removed map, or an error */ async removeGeoreferencedMap(georeferencedMap) { const validatedGeoreferencedMapOrMaps = validateGeoreferencedMap(georeferencedMap); const validatedGeoreferencedMap = Array.isArray( validatedGeoreferencedMapOrMaps ) ? validatedGeoreferencedMapOrMaps[0] : validatedGeoreferencedMapOrMaps; return this.removeGeoreferencedMapInternal(validatedGeoreferencedMap); } /** * Removes a georeferenced map from the list by its ID * * @param mapId - Map ID * @returns Map ID of the removed map, or an error */ async removeGeoreferencedMapById(mapId) { return this.removeGeoreferencedMapByIdInternal(mapId); } /** * Parses an annotation and adds its georeferenced map to this list * * @param annotation - Annotation * @param mapOptions - Map options * @returns Map IDs of the maps that were added, or an error per map */ async addGeoreferenceAnnotation(annotation, mapOptions) { const results = []; const maps = parseAnnotation(annotation); const settledResults = await Promise.allSettled( maps.map((map) => this.addGeoreferencedMapInternal(map, mapOptions)) ); 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.CHANGED)); return results; } /** * Parses an annotation and removes its georeferenced map from this list * * @param annotation * @returns Map IDs of the maps that were removed, or an error per map */ 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; } /** * Adds image informations, parses them to images and adds them to the image cache * * @param imageInfos - Image informations * @returns Image IDs of the image informations that were added */ addImageInfos(imageInfos) { const result = []; for (const imageInfo of imageInfos) { const image = Image.parse(imageInfo); this.imagesById.set(image.uri, image); result.push(image.uri); } this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.IMAGEINFOSADDED)); return result; } /** * Get mapIds for selected maps * * The selectionOptions allow a.o. to: * - filter for visible maps * - filter for specific mapIds * - filter for maps whose geoBbox overlap with the specified geoBbox * - filter for maps that overlap with a given geoPoint * * @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 * * The selectionOptions allow a.o. to: * - filter for visible maps * - filter for specific mapIds * - filter for maps whose geoBbox overlap with the specified geoBbox * - filter for maps that overlap with a given geoPoint * * @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 if (this.rtree && selectionOptions.geoPoint) { mapIds = this.rtree.searchFromPoint(selectionOptions.geoPoint); } 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.options.visible : true)) { warpedMaps.push(warpedMap); } } warpedMaps.sort( (map0, map1) => this.orderMapIdsByZIndex(map0.mapId, map1.mapId) ); return warpedMaps; } /** * Get the WarpedMap instance for a map * * @param mapId - Map ID of the requested WarpedMap instance * @returns WarpedMap instance, or undefined */ getWarpedMap(mapId) { return this.warpedMapsById.get(mapId); } /** * Get the center of the bounding box of the maps in this list * * The result is returned in the list's projection, `EPSG:3857` by default * Use `{ projection: { definition: '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 * * The result is returned in the list's projection, `EPSG:3857` by default * Use `{ projection: { definition: '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 ); if (projectedGeoMaskPoints.length === 0) { return; } return computeBbox(projectedGeoMaskPoints); } /** * Get the convex hull of the maps in this list * * The result is returned in the list's projection, `EPSG:3857` by default * Use `{ projection: { definition: '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); } /** * Get the z-index of a map * * @param mapId - Map ID for which to get the z-index */ getMapZIndex(mapId) { return this.zIndices.get(mapId); } /** * Get the default options of the list */ getDefaultOptions() { return mergeOptions( DEFAULT_WARPED_MAP_LIST_OPTIONS, WebGL2WarpedMap.getDefaultOptions() ); } /** * Get the default options of a map * * These come from the default option settings for WebGL2WarpedMaps and the map's georeferenced map proporties * * @param mapId - Map ID for which the options apply */ getMapDefaultOptions(mapId) { const warpedMap = this.getWarpedMap(mapId); return warpedMap?.getDefaultAndGeoreferencedMapOptions(); } /** * Get the options of this list */ getOptions() { return this.options; } /** * Get the map-specific options of a map * * @param mapId - Map ID for which the options apply */ getMapMapOptions(mapId) { const warpedMaps = this.getWarpedMaps({ mapIds: [mapId] }); const warpedMap = Array.from(warpedMaps)[0]; return warpedMap?.mapOptions; } /** * Get the options of a map * * These options are the result of merging the default, georeferenced map, * layer and map-specific options of that map. * * @param mapId - Map ID for which the options apply */ getMapOptions(mapId) { const warpedMaps = this.getWarpedMaps({ mapIds: [mapId] }); const warpedMap = Array.from(warpedMaps)[0]; return warpedMap?.options; } /** * Set the options of this list * * Note: Map-specific options set here will be passed to newly added maps. * * @param options - List Options * @param animationOptions - Animation options */ setOptions(options, animationOptions) { this.options = mergeOptions(this.options, options); this.internalSetMapsOptionsByMapId(void 0, options, animationOptions); } /** * Set the map-specific options of maps (and the list options) * * @param mapIds - Map IDs for which the options apply * @param mapOptions - Map-specific options * @param listOptions - list options * @param animationOptions - Animation options */ setMapsOptions(mapIds, mapOptions, listOptions, animationOptions) { const optionsByMapId = /* @__PURE__ */ new Map(); for (const mapId of mapIds) { optionsByMapId.set(mapId, mapOptions); } this.internalSetMapsOptionsByMapId( optionsByMapId, listOptions, animationOptions ); } /** * Set the map-specific options of maps by map ID (and the list options) * * This is useful when when multiple (and possibly different) * map-specific options are changed at once, * but only one animation should be fired * * @param mapOptionsByMapId - Map-specific options by map ID * @param listOptions - List options * @param animationOptions - Animation options */ setMapsOptionsByMapId(mapOptionsByMapId, listOptions, animationOptions) { this.internalSetMapsOptionsByMapId( mapOptionsByMapId, listOptions, animationOptions ); } /** * Resets the list options * * An empty array resets all options, undefined resets no options. * * @param listOptionKeys - Keys of the list options to reset * @param animationOptions - Animation options */ resetOptions(listOptionKeys, animationOptions) { if (listOptionKeys && listOptionKeys.length == 0) { listOptionKeys = Object.keys(this.getDefaultOptions()); } this.setOptions( optionKeysToUndefinedOptions( listOptionKeys ), animationOptions ); } /** * Resets the map-specific options of maps (and the list options) * * An empty array resets all options, undefined resets no options. * * @param mapIds - Map IDs for which to reset the options * @param mapOptionKeys - Keys of the map-specific options to reset * @param listOptionKeys - Keys of the list options to reset * @param animationOptions - Animation options */ resetMapsOptions(mapIds, mapOptionKeys, listOptionKeys, animationOptions) { if (mapOptionKeys && mapOptionKeys.length == 0) { mapOptionKeys = Object.keys(this.getDefaultOptions()); } if (listOptionKeys && listOptionKeys.length == 0) { listOptionKeys = Object.keys(this.getDefaultOptions()); } this.setMapsOptions( mapIds, optionKeysToUndefinedOptions( mapOptionKeys ), optionKeysToUndefinedOptions( listOptionKeys ), animationOptions ); } /** * Resets the map-specific options of maps by map ID (and the list options) * * An empty array or map resets all options (for all maps), undefined resets no options. * * @param mapOptionkeysByMapId - Keys of map-specific options to reset by map ID * @param listOptionKeys - Keys of the list options to reset * @param animationOptions - Animation options */ resetMapsOptionsByMapId(mapOptionkeysByMapId, listOptionKeys, animationOptions) { if (mapOptionkeysByMapId && mapOptionkeysByMapId.size == 0) { const mapIds = this.getMapIds(); const defaultMapOptionsKeys = Object.keys(this.getDefaultOptions()); for (const mapId of mapIds) { mapOptionkeysByMapId.set(mapId, defaultMapOptionsKeys); } } if (listOptionKeys && listOptionKeys.length == 0) { listOptionKeys = Object.keys(this.getDefaultOptions()); } this.setMapsOptionsByMapId( optionKeysByMapIdToUndefinedOptionsByMapId(mapOptionkeysByMapId), optionKeysToUndefinedOptions( listOptionKeys ), animationOptions ); } /** * 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.CHANGED)); } /** * 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.CHANGED)); } /** * 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.CHANGED)); } /** * 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.CHANGED)); } /** * Order mapIds * * Use this as anonymous sort function in Array.prototype.sort() */ orderMapIdsByZIndex(mapId0, mapId1) { const zIndex0 = this.getMapZIndex(mapId0); const zIndex1 = this.getMapZIndex(mapId1); if (zIndex0 !== void 0 && zIndex1 !== void 0) { return zIndex0 - zIndex1; } return 0; } 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, mapOptions) { const mapId = await this.getOrComputeMapId(georeferencedMap); const warpedMap = this.warpedMapFactory( mapId, georeferencedMap, this.options, mapOptions ); 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, { mapIds: [mapId] }) ); return mapId; } async removeGeoreferencedMapInternal(georeferencedMap) { const mapId = await this.getOrComputeMapId(georeferencedMap); return this.removeGeoreferencedMapByIdInternal(mapId); } async removeGeoreferencedMapByIdInternal(mapId) { 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, { mapIds: [mapId] }) ); this.removeZIndexHoles(); this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.CHANGED)); 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; } } /** * Internal set map options */ internalSetMapsOptionsByMapId(mapOptionsByMapId, listOptions, animationOptions) { if (this.warpedMapsById.size === 0 || mapOptionsByMapId?.size === 0) { return; } if (animationOptions?.animate !== void 0) { this.dispatchEvent( new WarpedMapEvent(WarpedMapEventType.PREPARECHANGE, { mapIds: this.getMapIds() }) ); } let changedOptionKeys = []; const changedMapIds = []; for (const warpedMap of this.getWarpedMaps()) { let warpedMapChangedOptions; if (animationOptions?.animate === void 0) { const mapOptions = mapOptionsByMapId?.get(warpedMap.mapId); warpedMapChangedOptions = warpedMap.setMapOptions( mapOptions, listOptions, { optionKeysToOmit: this.options.animatedOptions } ); } else { const mapOptions = mapOptionsByMapId?.get(warpedMap.mapId); warpedMapChangedOptions = warpedMap.setMapOptions( mapOptions, listOptions ); } const warpedMapChangedOptionKeys = Object.keys(warpedMapChangedOptions); if (warpedMapChangedOptionKeys.length > 0) { changedOptionKeys.push(...warpedMapChangedOptionKeys); changedMapIds.push(warpedMap.mapId); } if (this.options.rtreeUpdatedOptions.some( (option) => option in warpedMapChangedOptions )) { this.addToOrUpdateRtree(warpedMap); } } changedOptionKeys = Array.from(new Set(changedOptionKeys)); if (animationOptions?.animate === void 0 || animationOptions?.animate === false) { if (changedOptionKeys.length > 0) { this.dispatchEvent( new WarpedMapEvent(WarpedMapEventType.IMMEDIATECHANGE, { mapIds: changedMapIds, optionKeys: changedOptionKeys }) ); } if (animationOptions?.animate === void 0) { this.internalSetMapsOptionsByMapId( mapOptionsByMapId, listOptions, mergePartialOptions(animationOptions, { animate: true }) ); } } else { if (changedOptionKeys.length > 0) { this.dispatchEvent( new WarpedMapEvent(WarpedMapEventType.ANIMATEDCHANGE, { mapIds: changedMapIds, optionKeys: changedOptionKeys }) ); } } } 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 IMAGELOADED event by a WarpedMap // to an IMAGELOADED of the WarpedMapList, which is listened to in the Renderer imageLoaded(mapId) { this.dispatchEvent( new WarpedMapEvent(WarpedMapEventType.IMAGELOADED, { mapIds: [mapId] }) ); } addEventListenersToWarpedMap(warpedMap) { warpedMap.addEventListener( WarpedMapEventType.IMAGELOADED, this.imageLoaded.bind(this, warpedMap.mapId) ); } removeEventListenersFromWarpedMap(warpedMap) { warpedMap.removeEventListener( WarpedMapEventType.IMAGELOADED, this.imageLoaded.bind(this, warpedMap.mapId) ); } } export { WarpedMapList }; //# sourceMappingURL=WarpedMapList.js.map