@allmaps/render
Version:
Render functions for WebGL and image buffers
579 lines (578 loc) • 19.6 kB
JavaScript
import { cloneDeep } from "lodash-es";
import { Image } from "@allmaps/iiif-parser";
import { webMercatorProjection, ProjectedGcpTransformer, lonLatProjection } from "@allmaps/project";
import { mergeOptions, mergeOptionsUnlessUndefined, computeBbox, rectanglesToScale, mergePartialOptions, getPropertyFromCacheOrComputation, mixPoints, fetchImageInfo, bboxToRectangle, sizeToRectangle } from "@allmaps/stdlib";
import { applyHomogeneousTransform } from "../shared/homogeneous-transform.js";
import { WarpedMapEvent, WarpedMapEventType } from "../shared/events.js";
const DEFAULT_PROJECTED_GCP_TRANSFORMER_OPTIONS = {
minOffsetRatio: 0.01,
minOffsetDistance: 4,
maxDepth: 5,
differentHandedness: true
};
const DEFAULT_WARPED_MAP_OPTIONS = {
visible: true,
applyMask: true,
transformationType: "polynomial",
internalProjection: webMercatorProjection,
projection: webMercatorProjection
};
function createWarpedMapFactory() {
return (mapId, georeferencedMap, options) => new WarpedMap(mapId, georeferencedMap, options);
}
class WarpedMap extends EventTarget {
mapId;
georeferencedMap;
imageInformations;
parsedImage;
loadingImageInfo;
fetchFn;
abortController;
visible;
applyMask;
mixed = false;
gcps;
projectedGcps;
resourcePoints;
geoPoints;
projectedGeoPoints;
projectedGeoPreviousTransformedResourcePoints;
projectedGeoTransformedResourcePoints;
resourceMask;
resourceMaskBbox;
resourceMaskRectangle;
resourceFullMask;
resourceFullMaskBbox;
resourceFullMaskRectangle;
previousTransformationType;
transformationType;
previousInternalProjection;
internalProjection;
projection;
projectedPreviousTransformer;
projectedTransformer;
projectedTransformerCache;
geoMask;
geoMaskBbox;
geoMaskRectangle;
geoFullMask;
geoFullMaskBbox;
geoFullMaskRectangle;
projectedGeoMask;
projectedGeoMaskBbox;
projectedGeoMaskRectangle;
projectedGeoFullMask;
projectedGeoFullMaskBbox;
projectedGeoFullMaskRectangle;
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 partialWarpedMapOptions - options
*/
constructor(mapId, georeferencedMap, partialWarpedMapOptions) {
super();
const georeferencedMapInput = {
transformationType: georeferencedMap.transformation?.type,
internalProjection: georeferencedMap.resourceCrs
};
const mapAndConstructorOptions = mergeOptions(
georeferencedMapInput,
partialWarpedMapOptions
);
const warpedMapOptions = mergeOptionsUnlessUndefined(
DEFAULT_WARPED_MAP_OPTIONS,
mapAndConstructorOptions
);
this.projectedTransformerCache = /* @__PURE__ */ new Map();
this.mapId = mapId;
this.georeferencedMap = georeferencedMap;
this.imageInformations = warpedMapOptions.imageInformations;
this.loadingImageInfo = false;
this.visible = warpedMapOptions.visible;
this.applyMask = warpedMapOptions.applyMask;
this.fetchFn = warpedMapOptions.fetchFn;
this.gcps = this.georeferencedMap.gcps;
this.resourceMask = this.applyMask ? this.georeferencedMap.resourceMask : this.getResourceFullMask();
this.updateResourceMaskProperties();
this.updateResourceFullMaskProperties();
this.transformationType = warpedMapOptions.transformationType;
this.previousTransformationType = this.transformationType;
this.internalProjection = warpedMapOptions.internalProjection;
this.previousInternalProjection = this.internalProjection;
this.projection = warpedMapOptions.projection;
this.updateProjectedTransformerProperties();
}
/**
* Get resourceMask in viewport coordinates
*
* @param viewport - the current viewport
* @returns
*/
getViewportMask(viewport) {
return this.projectedGeoMask.map((point) => {
return applyHomogeneousTransform(
viewport.projectedGeoToViewportHomogeneousTransform,
point
);
});
}
/**
* Get Bbox of resourceMask in viewport coordinates
*
* @param viewport - the current viewport
* @returns
*/
getViewportMaskBbox(viewport) {
return computeBbox(this.getViewportMask(viewport));
}
/**
* Get resourceMaskRectangle in viewport coordinates
*
* @param viewport - the current viewport
* @returns
*/
getViewportMaskRectangle(viewport) {
return this.projectedGeoMaskRectangle.map((point) => {
return applyHomogeneousTransform(
viewport.projectedGeoToViewportHomogeneousTransform,
point
);
});
}
/**
* Get resourceFullMask in viewport coordinates
*
* @param viewport - the current viewport
* @returns
*/
getViewportFullMask(viewport) {
return this.projectedGeoFullMask.map((point) => {
return applyHomogeneousTransform(
viewport.projectedGeoToViewportHomogeneousTransform,
point
);
});
}
/**
* Get bbox of rresourceFullMask in viewport coordinates
*
* @param viewport - the current viewport
* @returns
*/
getViewportFullMaskBbox(viewport) {
return computeBbox(this.getViewportFullMask(viewport));
}
/**
* Get resourceFullMaskRectangle in viewport coordinates
*
* @param viewport - the current viewport
* @returns
*/
getViewportFullMaskRectangle(viewport) {
return this.projectedGeoFullMaskRectangle.map((point) => {
return applyHomogeneousTransform(
viewport.projectedGeoToViewportHomogeneousTransform,
point
);
});
}
/**
* 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.getViewportMaskRectangle(viewport)
);
}
/**
* 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) {
partialProjectedGcpTransformerOptions = mergePartialOptions(
{
projection: this.projection,
internalProjection: this.internalProjection
},
partialProjectedGcpTransformerOptions
);
partialProjectedGcpTransformerOptions = mergePartialOptions(
DEFAULT_PROJECTED_GCP_TRANSFORMER_OPTIONS,
partialProjectedGcpTransformerOptions
);
const projectedTransformer = getPropertyFromCacheOrComputation(
this.projectedTransformerCache,
transformationType,
() => new ProjectedGcpTransformer(
this.gcps,
transformationType,
partialProjectedGcpTransformerOptions
)
);
return projectedTransformer.setProjection(this.projection);
}
/**
* 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();
this.updateGcpsProperties();
}
/**
* Update the resource mask loaded from a georeferenced map to a new mask.
*
* @param resourceMask
*/
setResourceMask(resourceMask) {
this.applyMask = true;
this.resourceMask = resourceMask;
this.updateResourceMaskProperties();
this.updateResourceFullMaskProperties();
this.updateGeoMaskProperties();
this.updateProjectedGeoMaskProperties();
}
/**
* Set the transformationType
*
* @param transformationType
*/
setTransformationType(transformationType) {
this.transformationType = 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;
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 a transformer transitions).
*/
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 transformer transition).
*
* @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 = this.projectedGeoTransformedResourcePoints.map((point, index) => {
return mixPoints(
point,
this.projectedGeoPreviousTransformedResourcePoints[index],
t
);
});
}
/**
* Check if this instance has image info
*
* @returns
*/
hasImageInfo() {
return this.parsedImage !== void 0;
}
/**
* Fetch and parse the image info, and generate the image ID
*
* @returns
*/
async loadImageInfo() {
try {
this.loadingImageInfo = true;
const imageUri = this.georeferencedMap.resource.id;
let imageInfo;
if (this.imageInformations?.get(imageUri)) {
imageInfo = this.imageInformations.get(imageUri);
} else {
this.abortController = new AbortController();
const signal = this.abortController.signal;
imageInfo = await fetchImageInfo(imageUri, { signal }, this.fetchFn);
this.abortController = void 0;
this.imageInformations?.set(imageUri, imageInfo);
}
this.parsedImage = Image.parse(imageInfo);
this.dispatchEvent(new WarpedMapEvent(WarpedMapEventType.IMAGEINFOLOADED));
} catch (err) {
this.loadingImageInfo = false;
throw err;
} finally {
this.loadingImageInfo = false;
}
}
updateResourceMaskProperties() {
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);
}
}
updateResourceFullMaskProperties() {
this.resourceFullMask = this.getResourceFullMask();
this.resourceFullMaskBbox = computeBbox(this.resourceFullMask);
this.resourceFullMaskRectangle = bboxToRectangle(this.resourceFullMaskBbox);
}
updateGeoMaskProperties() {
this.updateGeoMask();
this.updateFullGeoMask();
}
updateProjectedGeoMaskProperties() {
this.updateProjectedGeoMask();
this.updateProjectedFullGeoMask();
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;
}
}
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];
}
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];
}
updateProjectedGeoMask() {
this.projectedGeoMask = this.projectedTransformer.transformToGeo([
this.resourceMask
])[0];
this.projectedGeoMaskBbox = computeBbox(this.projectedGeoMask);
this.projectedGeoMaskRectangle = this.projectedTransformer.transformToGeo(
[this.resourceMaskRectangle],
{ maxDepth: 0 }
)[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];
}
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();
}
destroy() {
if (this.abortController) {
this.abortController.abort();
}
}
}
export {
WarpedMap,
createWarpedMapFactory
};
//# sourceMappingURL=WarpedMap.js.map