UNPKG

@allmaps/render

Version:

Render functions for WebGL and image buffers

661 lines (660 loc) 23.2 kB
import { cloneDeep } from "lodash-es"; import { Image } from "@allmaps/iiif-parser"; import { webMercatorProjection, ProjectedGcpTransformer, lonLatProjection } from "@allmaps/project"; import { mergeOptionsUnlessUndefined, rectanglesToScale, getPropertyFromDoubleCacheOrComputation, getPropertyFromCacheOrComputation, omit, mergeOptions, mergePartialOptions, objectDifference, mergeTwoOptionsUnlessUndefined, computeBbox, mixLineStrings, fetchImageInfo, bboxToRectangle, sizeToRectangle } from "@allmaps/stdlib"; import { applyHomogeneousTransform } from "../shared/homogeneous-transform.js"; import { WarpedMapEvent, WarpedMapEventType } from "../shared/events.js"; const DEFAULT_WARPED_MAP_OPTIONS = { gcps: [], resourceMask: [], transformationType: "polynomial", internalProjection: webMercatorProjection, projection: webMercatorProjection, visible: true, applyMask: true, distortionMeasure: void 0 }; const DEFAULT_SPECIFIC_PROJECTED_GCP_TRANSFORMER_OPTIONS = { minOffsetRatio: 0.01, maxDepth: 5, differentHandedness: true }; const DEFAULT_PROJECTED_GCP_TRANSFORMER_OPTIONS = { ...DEFAULT_WARPED_MAP_OPTIONS, ...DEFAULT_SPECIFIC_PROJECTED_GCP_TRANSFORMER_OPTIONS }; function createWarpedMapFactory() { return (mapId, georeferencedMap, listOptions, mapOptions) => new WarpedMap(mapId, georeferencedMap, listOptions, mapOptions); } class WarpedMap extends EventTarget { mapId; georeferencedMap; defaultOptions; georeferencedMapOptions; listOptions; mapOptions; options; fetchingImageInfo; image; tileSize; abortController; mixed = false; gcps; projectedGcps; resourcePoints; geoPoints; projectedGeoPoints; projectedGeoPreviousTransformedResourcePoints; projectedGeoTransformedResourcePoints; resourceFullMask; resourceFullMaskBbox; resourceFullMaskRectangle; resourceAppliableMask; resourceAppliableMaskBbox; resourceAppliableMaskRectangle; resourceMask; resourceMaskBbox; resourceMaskRectangle; previousTransformationType; transformationType; previousInternalProjection; internalProjection; projection; projectedPreviousTransformer; projectedTransformer; projectedTransformerCache; projectedTransformerDoubleCache; geoFullMask; geoFullMaskBbox; geoFullMaskRectangle; geoAppliableMask; geoAppliableMaskBbox; geoAppliableMaskRectangle; geoMask; geoMaskBbox; geoMaskRectangle; projectedGeoFullMask; projectedGeoFullMaskBbox; projectedGeoFullMaskRectangle; projectedGeoAppliableMask; projectedGeoAppliableMaskBbox; projectedGeoAppliableMaskRectangle; projectedGeoMask; projectedGeoMaskBbox; projectedGeoMaskRectangle; resourceToProjectedGeoScale; previousDistortionMeasure; distortionMeasure; tileZoomLevelForViewport; overviewTileZoomLevelForViewport; projectedGeoBufferedViewportRectangleForViewport; projectedGeoBufferedViewportRectangleBboxForViewport; resourceBufferedViewportRingForViewport; resourceBufferedViewportRingBboxForViewport; resourceBufferedViewportRingBboxAndResourceMaskBboxIntersectionForViewport; fetchableTilesForViewport = []; overviewFetchableTilesForViewport = []; /** * Creates an instance of WarpedMap. * * @param mapId - ID of the map * @param georeferencedMap - Georeferenced map used to construct the WarpedMap * @param options - options */ constructor(mapId, georeferencedMap, listOptions = {}, mapOptions = {}) { super(); this.mapId = mapId; this.georeferencedMap = georeferencedMap; this.projectedTransformerCache = /* @__PURE__ */ new Map(); this.projectedTransformerDoubleCache = /* @__PURE__ */ new Map(); this.fetchingImageInfo = false; this.mapOptions = mapOptions; this.listOptions = listOptions; this.georeferencedMapOptions = { transformationType: georeferencedMap.transformation?.type, internalProjection: georeferencedMap.resourceCrs, gcps: georeferencedMap.gcps, resourceMask: georeferencedMap.resourceMask }; this.setDefaultOptions(); this.applyOptions({ init: true }); } /** * Get default options */ static getDefaultOptions() { return DEFAULT_WARPED_MAP_OPTIONS; } /** * Get default and georeferenced map options */ getDefaultAndGeoreferencedMapOptions() { return mergeOptionsUnlessUndefined( this.defaultOptions, this.georeferencedMapOptions ); } /** * Get scale of the warped map, in resource pixels per viewport pixels. * * @param viewport - the current viewport * @returns */ getResourceToViewportScale(viewport) { return rectanglesToScale( this.resourceMaskRectangle, this.projectedGeoMaskRectangle.map((point) => { return applyHomogeneousTransform( viewport.projectedGeoToViewportHomogeneousTransform, point ); }) ); } /** * Get scale of the warped map, in resource pixels per canvas pixels. * * @param viewport - the current viewport * @returns */ getResourceToCanvasScale(viewport) { return this.getResourceToViewportScale(viewport) / viewport.devicePixelRatio; } /** * Get the reference scaling from the forward transformation of the projected Helmert transformer * * @returns */ getReferenceScale() { const projectedHelmertTransformer = this.getProjectedTransformer("helmert"); const toProjectedGeoHelmertTransformation = projectedHelmertTransformer.getToGeoTransformation(); const helmertMeasures = toProjectedGeoHelmertTransformation.getMeasures(); return helmertMeasures.scale; } /** * Get a projected transformer of the given transformation type. * * Uses cashed projected transformers by transformation type, * and only computes a new projected transformer if none found. * * Returns a projected transformer in the current projection, * even if the cached transformer was computed in a different projection. * * Default settings apply for the options. * * @params transformationType - the transformation type * @params partialProjectedGcpTransformerOptions - options * @params useCache - whether to use the cached projected transformers previously computed * @returns A projected transformer */ getProjectedTransformer(transformationType, partialProjectedGcpTransformerOptions) { const options = mergeOptionsUnlessUndefined( DEFAULT_PROJECTED_GCP_TRANSFORMER_OPTIONS, { projection: this.projection, internalProjection: this.internalProjection }, partialProjectedGcpTransformerOptions ); return getPropertyFromDoubleCacheOrComputation( this.projectedTransformerDoubleCache, transformationType, this.projection.definition, () => { const projectedTransformer = getPropertyFromCacheOrComputation( this.projectedTransformerCache, transformationType, () => new ProjectedGcpTransformer( this.gcps, transformationType, omit(options, ["projection"]) ) ); return ProjectedGcpTransformer.setProjection( cloneDeep(projectedTransformer), options.projection ); } ); } setMapOptions(mapOptions, listOptions, animationOptions) { const optionKeysPossiblyChanged = []; if (mapOptions !== void 0 && Object.keys(mapOptions).length > 0) { this.mapOptions = mergeOptions(this.mapOptions, mapOptions); optionKeysPossiblyChanged.push(...Object.keys(mapOptions)); } if (listOptions !== void 0 && Object.keys(listOptions).length > 0) { this.listOptions = mergeOptions(this.listOptions, listOptions); optionKeysPossiblyChanged.push(...Object.keys(listOptions)); } return this.applyOptions( mergePartialOptions(animationOptions, { optionKeysPossiblyChanged }) ); } setListOptions(listOptions, animationOptions) { return this.setMapOptions(void 0, listOptions, animationOptions); } setDefaultOptions() { this.defaultOptions = WarpedMap.getDefaultOptions(); } applyOptions(animationOptions) { const options = mergeOptionsUnlessUndefined( this.defaultOptions, this.georeferencedMapOptions, this.listOptions, this.mapOptions ); const optionKeysToConsider = animationOptions?.optionKeysPossiblyChanged?.filter( (k) => animationOptions.optionKeysToOmit ? !animationOptions.optionKeysToOmit.includes(k) : true ); const changedOptions = objectDifference( options, this.options, optionKeysToConsider ); this.options = mergeTwoOptionsUnlessUndefined(this.options, changedOptions); if (animationOptions?.init) { this.gcps = this.options.gcps; this.resourceFullMask = this.getResourceFullMask(); this.resourceAppliableMask = this.georeferencedMap.resourceMask; this.resourceMask = this.options.applyMask ? this.resourceAppliableMask : this.resourceFullMask; this.updateResourceMaskProperties(); this.transformationType = this.options.transformationType; this.previousTransformationType = this.transformationType; this.internalProjection = this.options.internalProjection; this.previousInternalProjection = this.internalProjection; this.projection = this.options.projection; this.updateProjectedTransformerProperties(); } else { if ("gcps" in changedOptions) { this.setGcps(this.options.gcps); } if ("resourceMask" in changedOptions || "applyMask" in changedOptions) { const resourceFullMask = this.getResourceFullMask(); const resourceAppliableMask = this.options.resourceMask; const resourceMask = this.options.applyMask ? resourceAppliableMask : resourceFullMask; this.setResourceMask( resourceFullMask, resourceAppliableMask, resourceMask ); } if ("transformationType" in changedOptions) { this.setTransformationType(this.options.transformationType); } if ("internalProjection" in changedOptions) { this.setInternalProjection(this.options.internalProjection); } if ("projection" in changedOptions) { this.setProjection(this.options.projection); } if ("distortionMeasure" in changedOptions) { this.setDistortionMeasure(this.options.distortionMeasure); } } return changedOptions; } shouldRenderMap() { return this.options.visible !== false; } shouldRenderLines() { return this.options.visible !== false; } shouldRenderPoints() { return this.options.visible !== false; } /** * Update the ground control points loaded from a georeferenced map to new ground control points. * * @param gcps */ setGcps(gcps) { this.gcps = gcps; this.clearProjectedTransformerCaches(); this.updateProjectedTransformerProperties(); } /** * Update the resource mask loaded from a georeferenced map to a new mask. * * @param resourceMask */ setResourceMask(resourceFullMask, resourceAppliableMask, resourceMask) { this.resourceFullMask = resourceFullMask; this.resourceAppliableMask = resourceAppliableMask; this.resourceMask = resourceMask; this.updateResourceMaskProperties(); this.updateGeoMaskProperties(); this.updateProjectedGeoMaskProperties(); } /** * Set the transformationType * * @param transformationType */ setTransformationType(transformationType) { this.transformationType = transformationType; if (!this.previousTransformationType) { this.previousTransformationType = this.transformationType; } this.updateProjectedTransformerProperties(); } /** * Set the distortionMeasure * * @param distortionMeasure - the disortion measure */ setDistortionMeasure(distortionMeasure) { this.distortionMeasure = distortionMeasure; } /** * Set the internal projection * * @param projection - the internal projection */ setInternalProjection(projection) { this.internalProjection = projection || DEFAULT_PROJECTED_GCP_TRANSFORMER_OPTIONS.internalProjection || webMercatorProjection; if (!this.previousInternalProjection) { this.previousInternalProjection = this.internalProjection; } this.clearProjectedTransformerCaches(); this.updateProjectedTransformerProperties(); } /** * Set the projection * * @param projection - the projection */ setProjection(projection) { this.projection = projection || DEFAULT_PROJECTED_GCP_TRANSFORMER_OPTIONS.projection || webMercatorProjection; this.updateProjectedTransformerProperties(); } /** * Set the tile zoom level for the current viewport * * @param tileZoomLevel - tile zoom level for the current viewport */ setTileZoomLevelForViewport(tileZoomLevel) { this.tileZoomLevelForViewport = tileZoomLevel; } /** * Set the overview tile zoom level for the current viewport * * @param tileZoomLevel - tile zoom level for the current viewport */ setOverviewTileZoomLevelForViewport(tileZoomLevel) { this.overviewTileZoomLevelForViewport = tileZoomLevel; } /** * Set projectedGeoBufferedViewportRectangle for the current viewport * * @param projectedGeoBufferedViewportRectangle */ setProjectedGeoBufferedViewportRectangleForViewport(projectedGeoBufferedViewportRectangle) { this.projectedGeoBufferedViewportRectangleForViewport = projectedGeoBufferedViewportRectangle; this.projectedGeoBufferedViewportRectangleBboxForViewport = projectedGeoBufferedViewportRectangle ? computeBbox(projectedGeoBufferedViewportRectangle) : void 0; } /** * Set resourceBufferedViewportRing for the current viewport * * @param resourceBufferedViewportRing */ setResourceBufferedViewportRingForViewport(resourceBufferedViewportRing) { this.resourceBufferedViewportRingForViewport = resourceBufferedViewportRing; this.resourceBufferedViewportRingBboxForViewport = resourceBufferedViewportRing ? computeBbox(resourceBufferedViewportRing) : void 0; } /** * Set resourceBufferedViewportRingBboxAndResourceMaskBboxIntersection for the current viewport * * @param resourceBufferedViewportRingBboxAndResourceMaskBboxIntersection */ setResourceBufferedViewportRingBboxAndResourceMaskBboxIntersectionForViewport(resourceBufferedViewportRingBboxAndResourceMaskBboxIntersection) { this.resourceBufferedViewportRingBboxAndResourceMaskBboxIntersectionForViewport = resourceBufferedViewportRingBboxAndResourceMaskBboxIntersection; } /** * Set tiles for the current viewport * * @param fetchableTiles */ setFetchableTilesForViewport(fetchableTiles) { this.fetchableTilesForViewport = fetchableTiles; } /** * Set overview tiles for the current viewport * * @param overviewFetchableTiles */ setOverviewFetchableTilesForViewport(overviewFetchableTiles) { this.overviewFetchableTilesForViewport = overviewFetchableTiles; } /** * Reset the properties for the current values */ resetForViewport() { this.setTileZoomLevelForViewport(); this.setOverviewTileZoomLevelForViewport(); this.setProjectedGeoBufferedViewportRectangleForViewport(); this.setResourceBufferedViewportRingForViewport(); this.setFetchableTilesForViewport([]); this.setOverviewFetchableTilesForViewport([]); } /** * Reset previous transform properties to new ones (when completing an animation). */ resetPrevious() { this.mixed = false; this.previousTransformationType = this.transformationType; this.previousDistortionMeasure = this.distortionMeasure; this.previousInternalProjection = this.internalProjection; this.projectedPreviousTransformer = cloneDeep(this.projectedTransformer); this.projectedGeoPreviousTransformedResourcePoints = this.projectedGeoTransformedResourcePoints; } /** * Mix previous transform properties with new ones (when changing an ongoing animation). * * @param t - animation progress */ mixPreviousAndNew(t) { this.mixed = true; this.previousTransformationType = this.transformationType; this.previousDistortionMeasure = this.distortionMeasure; this.previousInternalProjection = this.internalProjection; this.projectedPreviousTransformer = cloneDeep(this.projectedTransformer); this.projectedGeoPreviousTransformedResourcePoints = mixLineStrings( this.projectedGeoTransformedResourcePoints, this.projectedGeoPreviousTransformedResourcePoints, t ); } /** * Check if this instance has parsed image * * @returns */ hasImage() { return this.image !== void 0; } /** * Load the parsed image from cache, or fetch and parse the image info to create it * * @returns */ async loadImage(imagesById) { try { const resourceId = this.georeferencedMap.resource.id; if (imagesById && imagesById.has(resourceId)) { this.image = imagesById.get(resourceId); } else { this.fetchingImageInfo = true; this.abortController = new AbortController(); const signal = this.abortController.signal; const imageInfo = await fetchImageInfo( resourceId, { signal }, this.options.fetchFn ); this.abortController = void 0; this.image = Image.parse(imageInfo); if (imagesById) { imagesById.set(resourceId, this.image); } } this.tileSize = [ Math.max(...this.image.tileZoomLevels.map((size) => size.width)), Math.max(...this.image.tileZoomLevels.map((size) => size.height)) ]; this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.IMAGELOADED)); } catch (err) { this.fetchingImageInfo = false; throw err; } finally { this.fetchingImageInfo = false; } } updateResourceMaskProperties() { this.resourceFullMaskBbox = computeBbox(this.resourceFullMask); this.resourceFullMaskRectangle = bboxToRectangle(this.resourceFullMaskBbox); this.resourceAppliableMaskBbox = computeBbox(this.resourceAppliableMask); this.resourceAppliableMaskRectangle = bboxToRectangle( this.resourceAppliableMaskBbox ); this.resourceMaskBbox = computeBbox(this.resourceMask); this.resourceMaskRectangle = bboxToRectangle(this.resourceMaskBbox); } getResourceFullMask() { const resourceWidth = this.georeferencedMap.resource.width; const resourceHeight = this.georeferencedMap.resource.height; if (resourceWidth && resourceHeight) { return sizeToRectangle([resourceWidth, resourceHeight]); } else { return bboxToRectangle(this.resourceMaskBbox); } } updateGeoMaskProperties() { this.updateFullGeoMask(); this.updateAppliableGeoMask(); this.updateGeoMask(); } updateProjectedGeoMaskProperties() { this.updateProjectedFullGeoMask(); this.updateProjectedAppliableGeoMask(); this.updateProjectedGeoMask(); this.updateResourceToProjectedGeoScale(); } updateProjectedTransformerProperties() { this.updateProjectedTransformer(); this.updateGeoMaskProperties(); this.updateProjectedGeoMaskProperties(); this.updateGcpsProperties(); } updateProjectedTransformer() { this.projectedTransformer = this.getProjectedTransformer( this.transformationType ); if (!this.projectedPreviousTransformer) { this.projectedPreviousTransformer = this.projectedTransformer; } } updateFullGeoMask() { this.geoFullMask = this.projectedTransformer.transformToGeo( [this.resourceFullMask], { projection: lonLatProjection } )[0]; this.geoFullMaskBbox = computeBbox(this.geoFullMask); this.geoFullMaskRectangle = this.projectedTransformer.transformToGeo( [this.resourceFullMaskRectangle], { maxDepth: 0, projection: lonLatProjection } )[0]; } updateAppliableGeoMask() { this.geoAppliableMask = this.projectedTransformer.transformToGeo( [this.resourceAppliableMask], { projection: lonLatProjection } )[0]; this.geoAppliableMaskBbox = computeBbox(this.geoAppliableMask); this.geoAppliableMaskRectangle = this.projectedTransformer.transformToGeo( [this.resourceAppliableMaskRectangle], { maxDepth: 0, projection: lonLatProjection } )[0]; } updateGeoMask() { this.geoMask = this.projectedTransformer.transformToGeo( [this.resourceMask], { projection: lonLatProjection } )[0]; this.geoMaskBbox = computeBbox(this.geoMask); this.geoMaskRectangle = this.projectedTransformer.transformToGeo( [this.resourceMaskRectangle], { maxDepth: 0, projection: lonLatProjection } )[0]; } updateProjectedFullGeoMask() { this.projectedGeoFullMask = this.projectedTransformer.transformToGeo([ this.resourceFullMask ])[0]; this.projectedGeoFullMaskBbox = computeBbox(this.projectedGeoFullMask); this.projectedGeoFullMaskRectangle = this.projectedTransformer.transformToGeo( [this.resourceFullMaskRectangle], { maxDepth: 0 } )[0]; } updateProjectedAppliableGeoMask() { this.projectedGeoAppliableMask = this.projectedTransformer.transformToGeo([ this.resourceAppliableMask ])[0]; this.projectedGeoAppliableMaskBbox = computeBbox( this.projectedGeoAppliableMask ); this.projectedGeoAppliableMaskRectangle = this.projectedTransformer.transformToGeo( [this.resourceAppliableMaskRectangle], { maxDepth: 0 } )[0]; } updateProjectedGeoMask() { this.projectedGeoMask = this.projectedTransformer.transformToGeo([ this.resourceMask ])[0]; this.projectedGeoMaskBbox = computeBbox(this.projectedGeoMask); this.projectedGeoMaskRectangle = this.projectedTransformer.transformToGeo( [this.resourceMaskRectangle], { maxDepth: 0 } )[0]; } updateResourceToProjectedGeoScale() { this.resourceToProjectedGeoScale = rectanglesToScale( this.resourceMaskRectangle, this.projectedGeoMaskRectangle ); } updateGcpsProperties() { this.projectedGcps = this.gcps.map(({ resource, geo }) => ({ resource, geo: this.projectedTransformer.lonLatToProjection(geo) })); this.resourcePoints = this.gcps.map((gcp) => gcp.resource); this.geoPoints = this.gcps.map((gcp) => gcp.geo); this.projectedGeoPoints = this.projectedGcps.map( (projectedGcp) => projectedGcp.geo ); this.projectedGeoTransformedResourcePoints = this.gcps.map( (projectedGcp) => this.projectedTransformer.transformToGeo(projectedGcp.resource) ); if (!this.projectedGeoPreviousTransformedResourcePoints) { this.projectedGeoPreviousTransformedResourcePoints = this.projectedGeoTransformedResourcePoints; } } clearProjectedTransformerCaches() { this.projectedTransformerCache = /* @__PURE__ */ new Map(); this.projectedTransformerDoubleCache = /* @__PURE__ */ new Map(); } destroy() { if (this.abortController) { this.abortController.abort(); } } } export { DEFAULT_WARPED_MAP_OPTIONS, WarpedMap, createWarpedMapFactory }; //# sourceMappingURL=WarpedMap.js.map