@allmaps/transform
Version:
Coordinate transformation functions
158 lines (157 loc) • 8.27 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,
minOffsetRatio: 0,
minOffsetDistance: Infinity,
minLineDistance: Infinity,
sourceMidPointFunction: midPoint,
destinationMidPointFunction: midPoint,
destinationDistanceFunction: distance
};
// 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) {
return undefined;
}
const { sourceMidPoint, destinationMidPointFromRefinementFunction, destinationMidPointsDistance, destinationLineDistance, destinationRefinedLineDistance } = splitGcpLinePointInfo(gcpLine, refinementFunction, refinementOptions);
const shouldSplit = shouldSplitGcpLine({
destinationMidPointsDistance,
destinationLineDistance,
destinationRefinedLineDistance
}, 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 destinationLineDistance = refinementOptions.destinationDistanceFunction(gcpLine[0].destination, gcpLine[1].destination);
const destinationRefinedLineDistance = refinementOptions.destinationDistanceFunction(refinementFunction(gcpLine[0].source), refinementFunction(gcpLine[1].source));
const destinationMidPointsDistance = refinementOptions.destinationDistanceFunction(destinationMidPoint, destinationMidPointFromRefinementFunction);
return {
sourceMidPoint,
destinationMidPointFromRefinementFunction,
destinationMidPointsDistance,
destinationLineDistance,
destinationRefinedLineDistance
};
}
function shouldSplitGcpLine({ destinationMidPointsDistance, destinationLineDistance, destinationRefinedLineDistance }, refinementOptions) {
return (destinationMidPointsDistance / destinationLineDistance >
refinementOptions.minOffsetRatio ||
destinationMidPointsDistance > refinementOptions.minOffsetDistance ||
destinationRefinedLineDistance > refinementOptions.minLineDistance);
}
// Get source refinement resolution
export function getSourceRefinementResolution(sourceBbox, refinementFunction, refinementOptions) {
const sourceRectangle = bboxToRectangle(sourceBbox);
const sourcePointNE = sourceRectangle[2];
const sourcePointNW = sourceRectangle[3];
const sourcePointSE = sourceRectangle[1];
const sourcePointSW = sourceRectangle[0];
const sourcePointCE = refinementOptions.sourceMidPointFunction(sourcePointNE, sourcePointSE);
const sourcePointCW = refinementOptions.sourceMidPointFunction(sourcePointNW, sourcePointSW);
const sourcePointNC = refinementOptions.sourceMidPointFunction(sourcePointNE, sourcePointNW);
const sourcePointSC = refinementOptions.sourceMidPointFunction(sourcePointSE, sourcePointSW);
// Get horizontal and vertical lines from points
const sourceHorizontalLine = [sourcePointCE, sourcePointCW];
const sourceVerticalLine = [sourcePointNC, sourcePointSC];
// Refine lines
const sourceRefinedHorizontalLineString = refineLineString(sourceHorizontalLine, refinementFunction, refinementOptions).map((generalGcp) => generalGcp.source);
const sourceRefinedVerticalLineString = refineLineString(sourceVerticalLine, refinementFunction, refinementOptions).map((generalGcp) => generalGcp.source);
if (sourceRefinedHorizontalLineString.length === 2 &&
sourceRefinedVerticalLineString.length === 2) {
return undefined;
}
// Compute minimal line length of refinement
const sourceMinHorizontalLineSquaredLengths = [];
for (let i = 0; i < sourceRefinedHorizontalLineString.length - 1; i++) {
sourceMinHorizontalLineSquaredLengths.push(squaredDistance(sourceRefinedHorizontalLineString[i], sourceRefinedHorizontalLineString[i + 1]));
}
const sourceMinHorizontalLineLength = Math.sqrt(Math.min(...sourceMinHorizontalLineSquaredLengths));
const sourceMinVerticalLineSquaredLengths = [];
for (let i = 0; i < sourceRefinedVerticalLineString.length - 1; i++) {
sourceMinVerticalLineSquaredLengths.push(squaredDistance(sourceRefinedVerticalLineString[i], sourceRefinedVerticalLineString[i + 1]));
}
const sourceMinVerticalLineLength = Math.sqrt(Math.min(...sourceMinVerticalLineSquaredLengths));
// Compute cols and rows by comparing minimal line length to original length
// Note: Tried to acchieve this by working with unflattened refined line and computing depth
// but that proved difficult for TypeScript
const sourceMinLineLength = Math.min(sourceMinHorizontalLineLength, sourceMinVerticalLineLength);
return sourceMinLineLength;
}
// 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;
}