Skip to content

Commit 8954cf0

Browse files
xpath: extract geotrace decoding (partial; prep for sharing logic)
This doesn’t introduce a codec, because the XPath call sites wouldn’t use it directly. But a hypothetical codec would presumably call into `fromEncodedGeotrace`.
1 parent 3874fb2 commit 8954cf0

File tree

3 files changed

+115
-59
lines changed

3 files changed

+115
-59
lines changed

packages/xpath/src/functions/xforms/geo.ts

Lines changed: 21 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,9 @@
1-
import { pairwise } from 'itertools-ts/lib/single';
21
import type { XPathNode } from '../../adapter/interface/XPathNode.ts';
32
import { EvaluationContext } from '../../context/EvaluationContext.ts';
4-
import { JRCompatibleGeoValueError } from '../../error/JRCompatibleGeoValueError.ts';
53
import type { EvaluableArgument } from '../../evaluator/functions/FunctionImplementation.ts';
64
import { NumberFunction } from '../../evaluator/functions/NumberFunction.ts';
7-
import type { GeopointCoordinates } from '../../lib/geo/Geopoint.ts';
8-
import { geopointCodec } from '../../lib/geo/geopointCodec.ts';
9-
10-
const evaluatePoints = <T extends XPathNode>(
11-
context: EvaluationContext<T>,
12-
expression: EvaluableArgument
13-
): GeopointCoordinates[] => {
14-
const results = expression.evaluate(context);
15-
16-
const stringResults = [...results].map((result) => result.toString());
17-
const geopointStrings = stringResults.flatMap((result) => {
18-
const string = result.toString().trim();
19-
20-
return string.split(/\s*;\s*/);
21-
});
22-
23-
return geopointStrings.map((string) => {
24-
return geopointCodec.decodeValue(string);
25-
});
26-
};
27-
28-
interface Line {
29-
readonly start: GeopointCoordinates;
30-
readonly end: GeopointCoordinates;
31-
}
32-
33-
const evaluateLines = <T extends XPathNode>(
34-
context: EvaluationContext<T>,
35-
expression: readonly EvaluableArgument[]
36-
): Line[] => {
37-
const points = expression.flatMap((el) => evaluatePoints(context, el));
38-
if (points.length < 2) {
39-
throw new JRCompatibleGeoValueError();
40-
}
41-
42-
return Array.from(pairwise(points)).map((line) => {
43-
const [start, end] = line;
44-
45-
return {
46-
start,
47-
end,
48-
};
49-
});
50-
};
5+
import { Geotrace } from '../../lib/geo/Geotrace.ts';
6+
import type { GeotraceLine } from '../../lib/geo/GeotraceLine.ts';
517

528
const EARTH_EQUATORIAL_RADIUS_METERS = 6_378_100;
539
const PRECISION = 100;
@@ -70,7 +26,7 @@ const toAbsolutePrecision = (value: number, precision: number) => {
7026
return Math.abs(toPrecision(value, precision));
7127
};
7228

73-
const geodesicArea = (lines: readonly Line[]): number => {
29+
const geodesicArea = (lines: readonly GeotraceLine[]): number => {
7430
const [firstLine, ...rest] = lines;
7531
const lastLine = rest[rest.length - 1];
7632

@@ -81,7 +37,7 @@ const geodesicArea = (lines: readonly Line[]): number => {
8137
const { start } = firstLine;
8238
const { end } = lastLine;
8339

84-
let shape: readonly Line[];
40+
let shape: readonly GeotraceLine[];
8541

8642
if (start.latitude === end.latitude && start.longitude === end.longitude) {
8743
shape = lines;
@@ -101,19 +57,24 @@ const geodesicArea = (lines: readonly Line[]): number => {
10157
return (total * EARTH_EQUATORIAL_RADIUS_METERS * EARTH_EQUATORIAL_RADIUS_METERS) / 2;
10258
};
10359

104-
export const area = new NumberFunction(
105-
'area',
106-
[{ arityType: 'required' }],
107-
(context, [expression]) => {
108-
const lines = evaluateLines(context, [expression!]);
60+
const evaluateArgumentValues = <T extends XPathNode>(
61+
context: EvaluationContext<T>,
62+
args: readonly EvaluableArgument[]
63+
): readonly string[] => {
64+
const evaluations = args.flatMap((arg) => [...arg.evaluate(context)]);
10965

110-
const areaResult = geodesicArea(lines);
66+
return evaluations.map((evaluation) => evaluation.toString());
67+
};
11168

112-
return toAbsolutePrecision(areaResult, PRECISION);
113-
}
114-
);
69+
export const area = new NumberFunction('area', [{ arityType: 'required' }], (context, args) => {
70+
const values = evaluateArgumentValues(context, args);
71+
const { lines } = Geotrace.fromEncodedValues(values);
72+
const areaResult = geodesicArea(lines);
73+
74+
return toAbsolutePrecision(areaResult, PRECISION);
75+
});
11576

116-
const geodesicDistance = (line: Line): number => {
77+
const geodesicDistance = (line: GeotraceLine): number => {
11778
const { start, end } = line;
11879
const deltaLambda = toRadians(start.longitude - end.longitude);
11980
const phi0 = toRadians(start.latitude);
@@ -140,7 +101,8 @@ export const distance = new NumberFunction(
140101
'distance',
141102
[{ arityType: 'required' }, { arityType: 'variadic' }],
142103
(context, args) => {
143-
const lines = evaluateLines(context, args);
104+
const values = evaluateArgumentValues(context, args);
105+
const { lines } = Geotrace.fromEncodedValues(values);
144106
const distances = lines.map(geodesicDistance);
145107

146108
return toAbsolutePrecision(sum(distances), PRECISION);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { JRCompatibleGeoValueError } from '../../error/JRCompatibleGeoValueError.ts';
2+
import { Geopoint } from './Geopoint.ts';
3+
import { GeotraceLine } from './GeotraceLine.ts';
4+
5+
export type GeotracePoints = readonly [Geopoint, Geopoint, ...Geopoint[]];
6+
7+
const isGeotracePoints = (geopoints: readonly Geopoint[]): geopoints is GeotracePoints => {
8+
return geopoints.length >= 2;
9+
};
10+
11+
type AssertGeotracePoints = (geopoints: readonly Geopoint[]) => asserts geopoints is GeotracePoints;
12+
13+
const assertGeotracePoints: AssertGeotracePoints = (geopoints) => {
14+
if (!isGeotracePoints(geopoints)) {
15+
throw new JRCompatibleGeoValueError();
16+
}
17+
};
18+
19+
const collectLines = (geopoints: GeotracePoints): readonly GeotraceLine[] => {
20+
return geopoints.reduce((acc, geopoint, i) => {
21+
if (i === 0) {
22+
return acc;
23+
}
24+
25+
// Non-null assertion safe: we ensure at least 2 points, and skip index 0.
26+
const start = geopoints[i - 1]!;
27+
const end = geopoint;
28+
29+
acc.push(
30+
new GeotraceLine({
31+
start,
32+
end,
33+
})
34+
);
35+
36+
return acc;
37+
}, Array<GeotraceLine>());
38+
};
39+
40+
export class Geotrace {
41+
static fromEncodedGeotrace(encoded: string): Geotrace {
42+
const geopoints = encoded.split(/\s*;\s*/).map((value) => {
43+
return Geopoint.fromNodeValue(value);
44+
});
45+
46+
return this.fromGeopoints(geopoints);
47+
}
48+
49+
static fromEncodedValues(values: readonly string[]): Geotrace {
50+
const [head, ...tail] = values;
51+
52+
if (head == null) {
53+
throw new JRCompatibleGeoValueError();
54+
}
55+
56+
if (tail.length === 0) {
57+
return this.fromEncodedGeotrace(head);
58+
}
59+
60+
const geopoints = values.map((value) => {
61+
return Geopoint.fromNodeValue(value);
62+
});
63+
64+
return this.fromGeopoints(geopoints);
65+
}
66+
67+
static fromGeopoints(geopoints: readonly Geopoint[]): Geotrace {
68+
assertGeotracePoints(geopoints);
69+
70+
return new this(geopoints);
71+
}
72+
73+
readonly lines: readonly GeotraceLine[];
74+
75+
private constructor(readonly geopoints: GeotracePoints) {
76+
this.lines = collectLines(geopoints);
77+
}
78+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Geopoint } from './Geopoint.ts';
2+
3+
interface GeotraceLinePoints {
4+
readonly start: Geopoint;
5+
readonly end: Geopoint;
6+
}
7+
8+
export class GeotraceLine implements GeotraceLinePoints {
9+
readonly start: Geopoint;
10+
readonly end: Geopoint;
11+
12+
constructor(points: GeotraceLinePoints) {
13+
this.start = points.start;
14+
this.end = points.end;
15+
}
16+
}

0 commit comments

Comments
 (0)