@allmaps/transform
Version:
Coordinate transformation functions
153 lines (152 loc) • 7.61 kB
JavaScript
import { midPoint, distance, squaredDistance, conformLineString, conformRing, bboxToRectangle } from '@allmaps/stdlib';
// About Refinement Functions:
//
// Refinement function are used both in forward and backward transformation
// and are the generalised approach to refine lineStrings, rings etc.
// when they are transformed using a (forward or backward) 'refinement function'.
//
// See the way refinement methods are called:
// with a different refinementFunction and refinementOptions for the forward and backward case.
//
// The concepts of 'source' and 'destination' for refinement methods
// might therefore differ from the from the transform methods that called them.
// For forward transform methods, 'source' and 'destination' in the refinement context
// are the same as in their original transform context.
// For backward transform methods, they are inversed.
// Hence, in the refinement contect we always act source > destination.
export const defaultRefinementOptions = {
maxDepth: 0,
minSourceDistance: 0,
minDestinationDistance: 0,
minOffsetRatio: 0,
minOffsetDistance: Infinity,
sourceMidPointFunction: midPoint,
destinationMidPointFunction: midPoint
};
// Refine Geometries
export function refineLineString(lineString, refinementFunction, refinementOptions) {
lineString = conformLineString(lineString);
const gcps = lineString.map((point) => ({
source: point,
destination: refinementFunction(point)
}));
const gcpLines = gcpsToGcpLines(gcps, false);
const refinedGcpLines = gcpLines
.map((gcpLine) => splitGcpLineRecursively(gcpLine, refinementFunction, refinementOptions, 0))
.flat(1);
return gcpLinesToGcps(refinedGcpLines, true);
}
export function refineRing(ring, refinementFunction, refinementOptions) {
ring = conformRing(ring);
const gcps = ring.map((point) => ({
source: point,
destination: refinementFunction(point)
}));
const gcpLines = gcpsToGcpLines(gcps, true);
const refinedGcpLines = gcpLines
.map((line) => splitGcpLineRecursively(line, refinementFunction, refinementOptions, 0))
.flat(1);
return gcpLinesToGcps(refinedGcpLines, false);
}
function splitGcpLineRecursively(gcpLine, refinementFunction, refinementOptions, depth) {
const newMidGcp = newMidGcpIfShouldSplitGcpLine(gcpLine, refinementFunction, refinementOptions, depth);
if (newMidGcp) {
return [
splitGcpLineRecursively([gcpLine[0], newMidGcp], refinementFunction, refinementOptions, depth + 1),
splitGcpLineRecursively([newMidGcp, gcpLine[1]], refinementFunction, refinementOptions, depth + 1)
].flat(1);
}
else {
return [gcpLine];
}
}
// Should split line
// This function checks if a GcpLine should be splits
// and returns the new midGcp if so, or undefined otherwise
export function newMidGcpIfShouldSplitGcpLine(gcpLine, refinementFunction, refinementOptions, depth) {
if (depth >= refinementOptions.maxDepth ||
refinementOptions.maxDepth <= 0 ||
distance(gcpLine[0].source, gcpLine[1].source) <
refinementOptions.minSourceDistance ||
distance(gcpLine[0].destination, gcpLine[1].destination) <
refinementOptions.minDestinationDistance) {
return undefined;
}
const { sourceMidPoint, destinationMidPointFromRefinementFunction, destinationMidPointsDistance, destinationLineDistance } = splitGcpLinePointInfo(gcpLine, refinementFunction, refinementOptions);
const shouldSplit = shouldSplitGcpLine({
destinationMidPointsDistance,
destinationLineDistance
}, refinementOptions);
return shouldSplit
? {
source: sourceMidPoint,
destination: destinationMidPointFromRefinementFunction
}
: undefined;
}
function splitGcpLinePointInfo(gcpLine, refinementFunction, refinementOptions) {
const sourceMidPoint = refinementOptions.sourceMidPointFunction(gcpLine[0].source, gcpLine[1].source);
const destinationMidPoint = refinementOptions.destinationMidPointFunction(gcpLine[0].destination, gcpLine[1].destination);
const destinationMidPointFromRefinementFunction = refinementFunction(sourceMidPoint);
const destinationMidPointsDistance = distance(destinationMidPoint, destinationMidPointFromRefinementFunction);
const destinationLineDistance = distance(gcpLine[0].destination, gcpLine[1].destination);
return {
sourceMidPoint,
destinationMidPointFromRefinementFunction,
destinationMidPointsDistance,
destinationLineDistance
};
}
function shouldSplitGcpLine({ destinationMidPointsDistance, destinationLineDistance }, refinementOptions) {
return (destinationMidPointsDistance / destinationLineDistance >
refinementOptions.minOffsetRatio ||
destinationMidPointsDistance > refinementOptions.minOffsetDistance);
}
// Get source refinement resolution
export function getSourceRefinementResolution(sourceBbox, refinementFunction, refinementOptions) {
const sourceRectangle = bboxToRectangle(sourceBbox);
const sourceTRPoint = sourceRectangle[2];
const sourceTLPoint = sourceRectangle[3];
const sourceBRPoint = sourceRectangle[1];
const sourceBLPoint = sourceRectangle[0];
const sourceCRPoint = refinementOptions.sourceMidPointFunction(sourceTRPoint, sourceBRPoint);
const sourceCLPoint = refinementOptions.sourceMidPointFunction(sourceTLPoint, sourceBLPoint);
const sourceTCPoint = refinementOptions.sourceMidPointFunction(sourceTRPoint, sourceTLPoint);
const sourceBCPoint = refinementOptions.sourceMidPointFunction(sourceBRPoint, sourceBLPoint);
// Get horizontal and vertical lines from points
// Note: Could also add sourceHTLine, sourceHBLine, sourceVLLine, sourceVRLine
// to get a more accurate resolution (at the expense of more refinement computations),
// but testing showed this was not needed
const sourceHCLine = [sourceCRPoint, sourceCLPoint];
const sourceVCLine = [sourceTCPoint, sourceBCPoint];
const sourceMultiLine = [sourceHCLine, sourceVCLine];
// Refine lines
const sourceRefinedMultiLineString = sourceMultiLine.map((sourceLine) => refineLineString(sourceLine, refinementFunction, refinementOptions).map((generalGcp) => generalGcp.source));
if (sourceRefinedMultiLineString.every((sourceRefinedLineString) => sourceRefinedLineString.length == 2)) {
return undefined;
}
// Compute minimal line length of refinement
// Note: Tried to acchieve this by working with unflattened refined line and computing depth
// but that proved difficult for TypeScript
const sourceRefinedMultiLineStringSquaredLengths = sourceRefinedMultiLineString.map((sourceRefinedLineString) => sourceRefinedLineString
.slice(0, -1)
.map((sourcePoint, index) => squaredDistance(sourcePoint, sourceRefinedLineString[index + 1])));
const sourceRefinedMultiLineStringMinLength = Math.sqrt(Math.min(...sourceRefinedMultiLineStringSquaredLengths.flat()));
return sourceRefinedMultiLineStringMinLength;
}
// Convert
export function gcpsToGcpLines(gcps, close = false) {
const lineCount = gcps.length - (close ? 0 : 1);
const lines = [];
for (let index = 0; index < lineCount; index++) {
lines.push([gcps[index], gcps[(index + 1) % gcps.length]]);
}
return lines;
}
export function gcpLinesToGcps(lines, close = false) {
const gcps = lines.map((line) => line[0]);
if (close) {
gcps.push(lines[lines.length - 1][1]);
}
return gcps;
}