@allmaps/render
Version:
Render functions for WebGL and image buffers
661 lines (660 loc) • 23.2 kB
JavaScript
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