UNPKG

@allmaps/transform

Version:

Coordinate transformation functions

328 lines (327 loc) 18.1 kB
import { isPoint, isLineString, isPolygon, isMultiPoint, isMultiLineString, isMultiPolygon, flipY, mergeOptions } from '@allmaps/stdlib'; import { computeDistortionsFromPartialDerivatives } from '../shared/distortion.js'; import { Straight } from '../transformation-types/Straight.js'; import { Helmert } from '../transformation-types/Helmert.js'; import { Polynomial1 } from '../transformation-types/Polynomial1.js'; import { Polynomial2 } from '../transformation-types/Polynomial2.js'; import { Polynomial3 } from '../transformation-types/Polynomial3.js'; import { Projective } from '../transformation-types/Projective.js'; import { RBF } from '../transformation-types/RBF.js'; import { linearKernel, thinPlateKernel } from '../shared/kernel-functions.js'; import { euclideanNorm } from '../shared/norm-functions.js'; import { defaultGeneralGcpTransformerOptions, refinementOptionsFromBackwardTransformOptions, refinementOptionsFromForwardTransformOptions } from '../shared/transform-functions.js'; import { refineLineString, refineRing, getSourceRefinementResolution } from '../shared/refinement-functions.js'; import { generalGcpToPointForForward, generalGcpToPointForBackward, invertGeneralGcp } from '../shared/conversion-functions.js'; /** * Abstract class for Ground Control Point Transformers. * * This class contains all logic to transform geometries * made available trough the GeneralGcpTransform and GcpTransform classes. * * The extending GeneralGcpTransform class handles the general case where: * - We read in Ground Control Points using the GeneralGcp type, with `source` and `destination` fields. * - We use the terms 'forward' and 'backward' for the two transformations. * - Both source and destination space are exected to be 2D cartesian spaces with the same handedness (x- and y-axes have the same orientations). * * The extending GcpTransform class handles the typical Allmaps case where: * - We read in Ground Control Points using the Gcp type, with `resource` and `geo` fields. * - We use the terms 'toGeo' for the 'forward' transformation and 'toResource' for the 'backward' transformation. * - The `differentHandedness` setting is `true` by default, since we expect the resource coordinates to identify pixels on an image, with origin in the top left and the y-axis pointing down. * */ export class BaseGcpTransformer { generalGcpsInternal; sourcePointsInternal; destinationPointsInternal; type; transformerOptions; forwardTransformation; backwardTransformation; /** * Create a BaseGcpTransformer * * @param generalGcps - An array of General Ground Control Points (GCPs) * @param type - The transformation type * @param partialGeneralGcpTransformerOptions - General GCP Transformer options */ constructor(generalGcps, type = 'polynomial', partialGeneralGcpTransformerOptions) { this.transformerOptions = mergeOptions(defaultGeneralGcpTransformerOptions, partialGeneralGcpTransformerOptions); if (generalGcps.length === 0) { throw new Error('No control points'); } this.generalGcpsInternal = generalGcps; this.sourcePointsInternal = this.generalGcpsInternal.map((generalGcp) => { const source = this.transformerOptions.differentHandedness ? flipY(generalGcp.source) : generalGcp.source; return this.transformerOptions.preForward(source); }); this.destinationPointsInternal = this.generalGcpsInternal.map((generalGcp) => this.transformerOptions.preBackward(generalGcp.destination)); this.type = type; } /** * Get the forward transformation. Create if it doesn't exist yet. */ getForwardTransformationInternal() { if (!this.forwardTransformation) { this.forwardTransformation = this.createTransformation(this.sourcePointsInternal, this.destinationPointsInternal); } return this.forwardTransformation; } /** * Get the backward transformation. Create if it doesn't exist yet. */ getBackwardTransformationInternal() { if (!this.backwardTransformation) { this.backwardTransformation = this.createTransformation(this.destinationPointsInternal, this.sourcePointsInternal); } return this.backwardTransformation; } /** * Create the (forward or backward) transformation. * * Results in forward transformation if source and destination points are entered as such. * Results in backward if source points are entered for destination points and vice versa. * * Results in a transformation of this instance's transformation type. * * @param sourcePoints - source points * @param destinationPoints - destination points * @returns Transformation */ createTransformation(sourcePoints, destinationPoints) { if (this.type === 'straight') { return new Straight(sourcePoints, destinationPoints); } else if (this.type === 'helmert') { return new Helmert(sourcePoints, destinationPoints); } else if (this.type === 'polynomial1' || this.type === 'polynomial') { return new Polynomial1(sourcePoints, destinationPoints); } else if (this.type === 'polynomial2') { return new Polynomial2(sourcePoints, destinationPoints); } else if (this.type === 'polynomial3') { return new Polynomial3(sourcePoints, destinationPoints); } else if (this.type === 'projective') { return new Projective(sourcePoints, destinationPoints); } else if (this.type === 'thinPlateSpline') { return new RBF(sourcePoints, destinationPoints, thinPlateKernel, euclideanNorm, 'thinPlateSpline'); } else if (this.type === 'linear') { return new RBF(sourcePoints, destinationPoints, linearKernel, euclideanNorm, 'linear'); } else { throw new Error(`Unsupported transformation type: ${this.type}`); } } /** * Get the resolution of the forward transformation in source space, within a given bbox. * * This informs you in how fine the warping is, in source space. * It can be useful e.g. to create a triangulation in source space * that is fine enough for this warping. * * It is obtained by transforming forward two linestring, * namely the horizontal and vertical midlines of the given bbox. * The forward transformation will refine these lines: * it will break them in small enough pieces to obtain a near continuous result. * Returned in the length of the shortest piece, measured in source coordinates. * * @param sourceBbox - BBox in source space where the resolution is requested * @param partialGeneralGcpTransformOptions - General GCP Transform options to consider during the transformation * @returns Resolution of the forward transformation in source space */ getForwardTransformationResolutionInternal(sourceBbox, partialGeneralGcpTransformOptions) { const transformOptions = mergeOptions(this.transformerOptions, partialGeneralGcpTransformOptions); return getSourceRefinementResolution(sourceBbox, (p) => this.transformForwardInternal(p, transformOptions), refinementOptionsFromForwardTransformOptions(transformOptions)); } /** * Get the resolution of the backward transformation in destination space, within a given bbox. * * This informs you in how fine the warping is, in destination space. * It can be useful e.g. to create a triangulation in destination space * that is fine enough for this warping. * * It is obtained by transforming backward two linestring, * namely the horizontal and vertical midlines of the given bbox. * The backward transformation will refine these lines: * it will break them in small enough pieces to obtain a near continuous result. * Returned in the length of the shortest piece, measured in destination coordinates. * * @param destinationBbox - BBox in destination space where the resolution is requested * @param partialGeneralGcpTransformOptions - General GCP Transform options to consider during the transformation * @returns Resolution of the backward transformation in destination space */ getBackwardTransformationResolutionInternal(destinationBbox, partialGeneralGcpTransformOptions) { const transformOptions = mergeOptions(this.transformerOptions, partialGeneralGcpTransformOptions); return getSourceRefinementResolution(destinationBbox, (p) => this.transformBackwardInternal(p, transformOptions), refinementOptionsFromBackwardTransformOptions(transformOptions)); } /** * Transform a geometry forward * * @param geometry - Geometry to transform * @param partialGeneralGcpTransformOptions - General GCP Transform options * @param generalGcpToP - Return type function * @returns Forward transform of input geometry */ transformForwardInternal(geometry, partialGeneralGcpTransformOptions, generalGcpToP = generalGcpToPointForForward) { const transformOptions = mergeOptions(this.transformerOptions, partialGeneralGcpTransformOptions); if (!transformOptions.isMultiGeometry) { if (isPoint(geometry)) { return this.transformPointForwardInternal(geometry, transformOptions, generalGcpToP); } else if (isLineString(geometry)) { return this.transformLineStringForwardInternal(geometry, transformOptions, generalGcpToP); } else if (isPolygon(geometry)) { return this.transformPolygonForwardInternal(geometry, transformOptions, generalGcpToP); } else { throw new Error('Geometry type not supported'); } } else { if (partialGeneralGcpTransformOptions) { partialGeneralGcpTransformOptions.isMultiGeometry = false; // false for piecewise single geometries } if (isMultiPoint(geometry)) { return geometry.map((element) => this.transformForwardInternal(element, partialGeneralGcpTransformOptions, generalGcpToP)); } else if (isMultiLineString(geometry)) { return geometry.map((element) => this.transformForwardInternal(element, partialGeneralGcpTransformOptions, generalGcpToP)); } else if (isMultiPolygon(geometry)) { return geometry.map((element) => this.transformForwardInternal(element, partialGeneralGcpTransformOptions, generalGcpToP)); } else { throw new Error('Geometry type not supported'); } } } /** * Transform a geometry backward * * @param geometry - Geometry to transform * @param partialGeneralGcpTransformOptions - General GCP Transform options * @param generalGcpToP - Return type function * @returns Backward transform of input geometry */ transformBackwardInternal(geometry, partialGeneralGcpTransformOptions, generalGcpToP = generalGcpToPointForBackward) { const transformOptions = mergeOptions(this.transformerOptions, partialGeneralGcpTransformOptions); if (!transformOptions.isMultiGeometry) { if (isPoint(geometry)) { return this.transformPointBackwardInternal(geometry, transformOptions, generalGcpToP); } else if (isLineString(geometry)) { return this.transformLineStringBackwardInternal(geometry, transformOptions, generalGcpToP); } else if (isPolygon(geometry)) { return this.transformPolygonBackwardInternal(geometry, transformOptions, generalGcpToP); } else { throw new Error('Geometry type not supported'); } } else { if (partialGeneralGcpTransformOptions) { partialGeneralGcpTransformOptions.isMultiGeometry = false; // false for piecewise single geometries } if (isMultiPoint(geometry)) { return geometry.map((element) => this.transformBackwardInternal(element, partialGeneralGcpTransformOptions, generalGcpToP)); } else if (isMultiLineString(geometry)) { return geometry.map((element) => this.transformBackwardInternal(element, partialGeneralGcpTransformOptions, generalGcpToP)); } else if (isMultiPolygon(geometry)) { return geometry.map((element) => this.transformBackwardInternal(element, partialGeneralGcpTransformOptions, generalGcpToP)); } else { throw new Error('Geometry type not supported'); } } } // Handle specific geometries transformPointForwardInternal(point, generalGcpTransformOptions, generalGcpToP = generalGcpToPointForForward) { const forwardTransformation = this.getForwardTransformationInternal(); let source = this.transformerOptions.differentHandedness ? flipY(point) : point; source = generalGcpTransformOptions.preForward(source); let destination = forwardTransformation.evaluateFunction(source); destination = generalGcpTransformOptions.postForward(destination); let partialDerivativeX = undefined; let partialDerivativeY = undefined; let distortions = new Map(); if (generalGcpTransformOptions.distortionMeasures.length > 0) { partialDerivativeX = forwardTransformation.evaluatePartialDerivativeX(source); partialDerivativeY = forwardTransformation.evaluatePartialDerivativeY(source); distortions = computeDistortionsFromPartialDerivatives(generalGcpTransformOptions.distortionMeasures, partialDerivativeX, partialDerivativeY, generalGcpTransformOptions.referenceScale); } return generalGcpToP({ source: point, // don't apply differentHandedness here, this is only internally. destination, partialDerivativeX, partialDerivativeY, distortions }); } transformPointBackwardInternal(point, generalGcpTransformOptions, generalGcpToP = generalGcpToPointForBackward) { const backwardTransformation = this.getBackwardTransformationInternal(); const destination = generalGcpTransformOptions.preBackward(point); let source = backwardTransformation.evaluateFunction(destination); // apply differentHandedness here again, so it has been applied twice in total and is undone now. source = generalGcpTransformOptions.postBackward(source); source = this.transformerOptions.differentHandedness ? flipY(source) : source; let partialDerivativeX = undefined; let partialDerivativeY = undefined; let distortions = new Map(); if (generalGcpTransformOptions.distortionMeasures.length > 0) { partialDerivativeX = backwardTransformation.evaluatePartialDerivativeX(destination); partialDerivativeX = this.transformerOptions.differentHandedness ? flipY(partialDerivativeX) : partialDerivativeX; partialDerivativeY = backwardTransformation.evaluatePartialDerivativeY(destination); partialDerivativeY = this.transformerOptions.differentHandedness ? flipY(partialDerivativeY) : partialDerivativeY; distortions = computeDistortionsFromPartialDerivatives(generalGcpTransformOptions.distortionMeasures, partialDerivativeX, partialDerivativeY, generalGcpTransformOptions.referenceScale); } return generalGcpToP({ source, destination, partialDerivativeX, partialDerivativeY, distortions }); } transformLineStringForwardInternal(lineString, generalGcpTransformOptions, generalGcpToP) { return refineLineString(lineString, (p) => this.transformPointForwardInternal(p, generalGcpTransformOptions), refinementOptionsFromForwardTransformOptions(generalGcpTransformOptions)).map((generalGcp) => generalGcpToP(generalGcp)); } transformLineStringBackwardInternal(lineString, generalGcpTransformOptions, generalGcpToP) { return refineLineString(lineString, (p) => this.transformPointBackwardInternal(p, generalGcpTransformOptions), refinementOptionsFromBackwardTransformOptions(generalGcpTransformOptions)).map((generalGcp) => generalGcpToP(invertGeneralGcp(generalGcp))); } transformRingForwardInternal(ring, generalGcpTransformOptions, generalGcpToP) { return refineRing(ring, (p) => this.transformPointForwardInternal(p, generalGcpTransformOptions), refinementOptionsFromForwardTransformOptions(generalGcpTransformOptions)).map((generalGcp) => generalGcpToP(generalGcp)); } transformRingBackwardInternal(ring, generalGcpTransformOptions, generalGcpToP) { return refineRing(ring, (p) => this.transformPointBackwardInternal(p, generalGcpTransformOptions), refinementOptionsFromBackwardTransformOptions(generalGcpTransformOptions)).map((generalGcp) => generalGcpToP(invertGeneralGcp(generalGcp))); } transformPolygonForwardInternal(polygon, generalGcpTransformOptions, generalGcpToP) { return polygon.map((ring) => { return this.transformRingForwardInternal(ring, generalGcpTransformOptions, generalGcpToP); }); } transformPolygonBackwardInternal(polygon, generalGcpTransformOptions, generalGcpToP) { return polygon.map((ring) => { return this.transformRingBackwardInternal(ring, generalGcpTransformOptions, generalGcpToP); }); } }