@motionstudies/core 0.1.0-alpha.23 → 0.1.0-alpha.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/air-continents.d.ts +63 -0
- package/air-continents.js +43 -0
- package/domain/air.d.ts +4 -0
- package/map-labels.d.ts +24 -0
- package/map-labels.js +48 -0
- package/package.json +11 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Geographic continents from the airport reference; EU is not European Union membership. */
|
|
2
|
+
export declare const AIR_CONTINENTS: {
|
|
3
|
+
readonly AF: 'Africa';
|
|
4
|
+
readonly AN: 'Antarctica';
|
|
5
|
+
readonly AS: 'Asia';
|
|
6
|
+
readonly EU: 'Europe';
|
|
7
|
+
readonly NA: 'North America';
|
|
8
|
+
readonly OC: 'Oceania';
|
|
9
|
+
readonly SA: 'South America';
|
|
10
|
+
};
|
|
11
|
+
export type AirContinent = keyof typeof AIR_CONTINENTS;
|
|
12
|
+
export type AirContinentSource = 'ourairports' | 'override';
|
|
13
|
+
export type AirContinentStatus = 'known' | 'unknown-endpoint' | 'unknown-continent';
|
|
14
|
+
export interface AirContinentLabel {
|
|
15
|
+
readonly continent: AirContinent | null;
|
|
16
|
+
readonly status: AirContinentStatus;
|
|
17
|
+
}
|
|
18
|
+
export declare function isAirContinent(value: unknown): value is AirContinent;
|
|
19
|
+
/** Older releases and unobserved endpoints remain explicitly unknown. */
|
|
20
|
+
export declare function classifyAirEndpoint(endpoint?: {
|
|
21
|
+
readonly continent?: unknown;
|
|
22
|
+
} | null): AirContinentLabel;
|
|
23
|
+
/** Pass the canonical track index, never overlapping playback chunks. Counts are track segments, not flights. */
|
|
24
|
+
export declare function summarizeAirContinents(tracks: Iterable<{
|
|
25
|
+
readonly origin?: {
|
|
26
|
+
readonly continent?: unknown;
|
|
27
|
+
};
|
|
28
|
+
readonly destination?: {
|
|
29
|
+
readonly continent?: unknown;
|
|
30
|
+
};
|
|
31
|
+
}>): {
|
|
32
|
+
trackSegments: number;
|
|
33
|
+
origin: {
|
|
34
|
+
known: number;
|
|
35
|
+
unknownEndpoint: number;
|
|
36
|
+
unknownContinent: number;
|
|
37
|
+
continents: {
|
|
38
|
+
AF: number;
|
|
39
|
+
AN: number;
|
|
40
|
+
AS: number;
|
|
41
|
+
EU: number;
|
|
42
|
+
NA: number;
|
|
43
|
+
OC: number;
|
|
44
|
+
SA: number;
|
|
45
|
+
};
|
|
46
|
+
knownFraction: number | null;
|
|
47
|
+
};
|
|
48
|
+
destination: {
|
|
49
|
+
known: number;
|
|
50
|
+
unknownEndpoint: number;
|
|
51
|
+
unknownContinent: number;
|
|
52
|
+
continents: {
|
|
53
|
+
AF: number;
|
|
54
|
+
AN: number;
|
|
55
|
+
AS: number;
|
|
56
|
+
EU: number;
|
|
57
|
+
NA: number;
|
|
58
|
+
OC: number;
|
|
59
|
+
SA: number;
|
|
60
|
+
};
|
|
61
|
+
knownFraction: number | null;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Geographic continents from the airport reference; EU is not European Union membership. */
|
|
2
|
+
export const AIR_CONTINENTS = {
|
|
3
|
+
AF: 'Africa', AN: 'Antarctica', AS: 'Asia', EU: 'Europe',
|
|
4
|
+
NA: 'North America', OC: 'Oceania', SA: 'South America',
|
|
5
|
+
};
|
|
6
|
+
export function isAirContinent(value) {
|
|
7
|
+
return typeof value === 'string' && Object.hasOwn(AIR_CONTINENTS, value);
|
|
8
|
+
}
|
|
9
|
+
/** Older releases and unobserved endpoints remain explicitly unknown. */
|
|
10
|
+
export function classifyAirEndpoint(endpoint) {
|
|
11
|
+
if (!endpoint)
|
|
12
|
+
return { continent: null, status: 'unknown-endpoint' };
|
|
13
|
+
if (!isAirContinent(endpoint.continent))
|
|
14
|
+
return { continent: null, status: 'unknown-continent' };
|
|
15
|
+
return { continent: endpoint.continent, status: 'known' };
|
|
16
|
+
}
|
|
17
|
+
/** Pass the canonical track index, never overlapping playback chunks. Counts are track segments, not flights. */
|
|
18
|
+
export function summarizeAirContinents(tracks) {
|
|
19
|
+
const empty = () => ({
|
|
20
|
+
known: 0, unknownEndpoint: 0, unknownContinent: 0,
|
|
21
|
+
continents: { AF: 0, AN: 0, AS: 0, EU: 0, NA: 0, OC: 0, SA: 0 },
|
|
22
|
+
});
|
|
23
|
+
const counts = { origin: empty(), destination: empty() };
|
|
24
|
+
let trackSegments = 0;
|
|
25
|
+
for (const track of tracks) {
|
|
26
|
+
trackSegments++;
|
|
27
|
+
for (const side of ['origin', 'destination']) {
|
|
28
|
+
const label = classifyAirEndpoint(track[side]), count = counts[side];
|
|
29
|
+
if (label.continent) {
|
|
30
|
+
count.known++;
|
|
31
|
+
count.continents[label.continent]++;
|
|
32
|
+
}
|
|
33
|
+
else if (label.status === 'unknown-endpoint')
|
|
34
|
+
count.unknownEndpoint++;
|
|
35
|
+
else
|
|
36
|
+
count.unknownContinent++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const coverage = (side) => ({
|
|
40
|
+
...counts[side], knownFraction: trackSegments ? counts[side].known / trackSegments : null,
|
|
41
|
+
});
|
|
42
|
+
return { trackSegments, origin: coverage('origin'), destination: coverage('destination') };
|
|
43
|
+
}
|
package/domain/air.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AirContinent, AirContinentSource } from '../air-continents.ts';
|
|
1
2
|
export type AirSample = readonly [
|
|
2
3
|
time: number,
|
|
3
4
|
longitude: number,
|
|
@@ -13,6 +14,9 @@ export interface AirEndpoint {
|
|
|
13
14
|
/** Study-local seconds at the observed approach/departure boundary; not a scheduled time. */
|
|
14
15
|
readonly time: number;
|
|
15
16
|
readonly evidence: 'observed-endpoint';
|
|
17
|
+
/** Airport geography, separate from the inferred route evidence. Absent in older releases. */
|
|
18
|
+
readonly continent?: AirContinent;
|
|
19
|
+
readonly continentSource?: AirContinentSource;
|
|
16
20
|
}
|
|
17
21
|
export interface AirTrack {
|
|
18
22
|
readonly id: string;
|
package/map-labels.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Renderer-independent hierarchy shared by station and airport map labels. */
|
|
2
|
+
export interface MapLabelPriority {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly rank: number;
|
|
5
|
+
readonly priority: number;
|
|
6
|
+
readonly retained: boolean;
|
|
7
|
+
readonly distance: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MapLabelBox {
|
|
10
|
+
readonly left: number;
|
|
11
|
+
readonly right: number;
|
|
12
|
+
readonly top: number;
|
|
13
|
+
readonly bottom: number;
|
|
14
|
+
}
|
|
15
|
+
export interface MapLabelCandidate extends MapLabelPriority {
|
|
16
|
+
readonly box: MapLabelBox;
|
|
17
|
+
}
|
|
18
|
+
export declare function compareMapLabelCandidates(first: MapLabelPriority, second: MapLabelPriority): number;
|
|
19
|
+
export declare function mapLabelBudget(cameraHeight: number): number;
|
|
20
|
+
export declare function mapLabelRankLimit(cameraHeight: number): number;
|
|
21
|
+
export declare function mapLabelBoxesOverlap(box: MapLabelBox, other: MapLabelBox, horizontalGap?: number, verticalGap?: number): boolean;
|
|
22
|
+
/** Candidate boxes include the edition's full hit area. Selected candidates must
|
|
23
|
+
* already have their higher priority; all labels respect viewport and obstacles. */
|
|
24
|
+
export declare function selectMapLabels<T extends MapLabelCandidate>(candidates: readonly T[], budget: number, viewport: MapLabelBox, obstacles?: readonly MapLabelBox[]): T[];
|
package/map-labels.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function compareMapLabelCandidates(first, second) {
|
|
2
|
+
return first.priority - second.priority || Number(second.retained) - Number(first.retained)
|
|
3
|
+
|| first.rank - second.rank || first.distance - second.distance || first.name.localeCompare(second.name, 'de-CH');
|
|
4
|
+
}
|
|
5
|
+
export function mapLabelBudget(cameraHeight) {
|
|
6
|
+
if (cameraHeight >= 30)
|
|
7
|
+
return 8;
|
|
8
|
+
if (cameraHeight >= 22)
|
|
9
|
+
return 20;
|
|
10
|
+
if (cameraHeight >= 15)
|
|
11
|
+
return 48;
|
|
12
|
+
return 96;
|
|
13
|
+
}
|
|
14
|
+
export function mapLabelRankLimit(cameraHeight) {
|
|
15
|
+
if (cameraHeight >= 30)
|
|
16
|
+
return 8;
|
|
17
|
+
if (cameraHeight >= 22)
|
|
18
|
+
return 20;
|
|
19
|
+
if (cameraHeight >= 15)
|
|
20
|
+
return 48;
|
|
21
|
+
if (cameraHeight >= 14)
|
|
22
|
+
return 96;
|
|
23
|
+
return Number.POSITIVE_INFINITY;
|
|
24
|
+
}
|
|
25
|
+
export function mapLabelBoxesOverlap(box, other, horizontalGap = 5, verticalGap = 4) {
|
|
26
|
+
return box.left < other.right + horizontalGap && box.right > other.left - horizontalGap
|
|
27
|
+
&& box.top < other.bottom + verticalGap && box.bottom > other.top - verticalGap;
|
|
28
|
+
}
|
|
29
|
+
/** Candidate boxes include the edition's full hit area. Selected candidates must
|
|
30
|
+
* already have their higher priority; all labels respect viewport and obstacles. */
|
|
31
|
+
export function selectMapLabels(candidates, budget, viewport, obstacles = []) {
|
|
32
|
+
const selected = [], occupied = [...obstacles];
|
|
33
|
+
const limit = Number.isFinite(budget) ? Math.max(0, Math.floor(budget)) : 0;
|
|
34
|
+
for (const candidate of [...candidates].sort(compareMapLabelCandidates)) {
|
|
35
|
+
if (selected.length >= limit)
|
|
36
|
+
break;
|
|
37
|
+
const box = candidate.box;
|
|
38
|
+
if (![box.left, box.right, box.top, box.bottom].every(Number.isFinite) || box.right <= box.left || box.bottom <= box.top)
|
|
39
|
+
continue;
|
|
40
|
+
if (box.left < viewport.left || box.right > viewport.right || box.top < viewport.top || box.bottom > viewport.bottom)
|
|
41
|
+
continue;
|
|
42
|
+
if (occupied.some(other => mapLabelBoxesOverlap(box, other)))
|
|
43
|
+
continue;
|
|
44
|
+
selected.push(candidate);
|
|
45
|
+
occupied.push(box);
|
|
46
|
+
}
|
|
47
|
+
return selected;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionstudies/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.25",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Transport contracts and motion primitives for Motion Studies.",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"tag": "next"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
|
+
"./air-continents": {
|
|
24
|
+
"types": "./air-continents.d.ts",
|
|
25
|
+
"import": "./air-continents.js",
|
|
26
|
+
"default": "./air-continents.js"
|
|
27
|
+
},
|
|
23
28
|
"./air-search": {
|
|
24
29
|
"types": "./air-search.d.ts",
|
|
25
30
|
"import": "./air-search.js",
|
|
@@ -219,6 +224,11 @@
|
|
|
219
224
|
"types": "./render-frame.d.ts",
|
|
220
225
|
"import": "./render-frame.js",
|
|
221
226
|
"default": "./render-frame.js"
|
|
227
|
+
},
|
|
228
|
+
"./map-labels": {
|
|
229
|
+
"types": "./map-labels.d.ts",
|
|
230
|
+
"import": "./map-labels.js",
|
|
231
|
+
"default": "./map-labels.js"
|
|
222
232
|
}
|
|
223
233
|
},
|
|
224
234
|
"files": [
|