@allmaps/transform
Version:
Coordinate transformation functions
328 lines (327 loc) • 18.1 kB
JavaScript
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);
});
}
}