caelus 0.16.0 → 0.18.0
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/README.md +2 -2
- package/accuracy.json +15 -15
- package/dist/src/anchored.d.ts +49 -0
- package/dist/src/anchored.js +40 -0
- package/dist/src/astrocartography.js +6 -1
- package/dist/src/brief.d.ts +90 -0
- package/dist/src/brief.js +85 -0
- package/dist/src/chart.d.ts +5 -0
- package/dist/src/chart.js +20 -4
- package/dist/src/counterfactual.d.ts +82 -0
- package/dist/src/counterfactual.js +105 -0
- package/dist/src/eclipses.d.ts +103 -0
- package/dist/src/eclipses.js +260 -2
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +6 -0
- package/dist/src/interpret.d.ts +156 -0
- package/dist/src/interpret.js +179 -0
- package/dist/src/interpretation.d.ts +137 -0
- package/dist/src/interpretation.js +250 -0
- package/dist/src/node-loader.js +5 -2
- package/dist/src/parans.d.ts +28 -0
- package/dist/src/parans.js +84 -0
- package/dist/src/provenance.d.ts +135 -0
- package/dist/src/provenance.js +159 -0
- package/package.json +1 -1
package/dist/src/parans.js
CHANGED
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
* `parans-golden`.
|
|
14
14
|
*/
|
|
15
15
|
import { riseSet } from "./events.js";
|
|
16
|
+
import { gast } from "./houses.js";
|
|
17
|
+
import { DEG, mod } from "./core.js";
|
|
16
18
|
export const PARAN_ANGLES = ["rise", "mtransit", "set", "itransit"];
|
|
17
19
|
export const DEFAULT_PARAN_BODIES = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn"];
|
|
20
|
+
const TWO_PI = 2 * Math.PI;
|
|
21
|
+
// Local sidereal time advances one turn per sidereal day.
|
|
22
|
+
const SID_RATE = 360.98564736629 * DEG;
|
|
23
|
+
// Standard rise/set: the geometric horizon lifted by ~34' of refraction.
|
|
24
|
+
const RISE_ALT = -0.5667 * DEG;
|
|
18
25
|
/**
|
|
19
26
|
* Co-angular pairs over the 24 hours from `jd` (UT) at latitude `lat`: every
|
|
20
27
|
* pair of different bodies whose angle crossings fall within `toleranceMin`
|
|
@@ -57,3 +64,80 @@ export function parans(engine, jd, lat, bodies = DEFAULT_PARAN_BODIES, tolerance
|
|
|
57
64
|
out.sort((x, y) => (x.a < y.a ? -1 : x.a > y.a ? 1 : x.b < y.b ? -1 : x.b > y.b ? 1 : x.jd - y.jd));
|
|
58
65
|
return out;
|
|
59
66
|
}
|
|
67
|
+
/** The UT instant in `[jd, jd+1)` when apparent sidereal time = `target` (rad). */
|
|
68
|
+
function timeAtLst(engine, jd, target) {
|
|
69
|
+
const dlst = mod(target - gast(engine.data, jd), TWO_PI);
|
|
70
|
+
let t = jd + dlst / SID_RATE;
|
|
71
|
+
for (let i = 0; i < 2; i++) {
|
|
72
|
+
const err = mod(gast(engine.data, t) - target + Math.PI, TWO_PI) - Math.PI;
|
|
73
|
+
t -= err / SID_RATE;
|
|
74
|
+
}
|
|
75
|
+
return t;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The four angle crossings of a fixed `star` over the day from `jd` at latitude
|
|
79
|
+
* `lat`: the meridian transits always occur; `rise`/`set` are absent when the
|
|
80
|
+
* star is circumpolar or never rises.
|
|
81
|
+
*/
|
|
82
|
+
export function starAngleTimes(engine, star, jd, lat) {
|
|
83
|
+
const fs = engine.fixedStar(star, jd);
|
|
84
|
+
const alpha = mod(fs.ra * DEG, TWO_PI);
|
|
85
|
+
const delta = fs.dec * DEG;
|
|
86
|
+
const phi = lat * DEG;
|
|
87
|
+
const out = {
|
|
88
|
+
mtransit: timeAtLst(engine, jd, alpha),
|
|
89
|
+
itransit: timeAtLst(engine, jd, mod(alpha + Math.PI, TWO_PI)),
|
|
90
|
+
};
|
|
91
|
+
const denom = Math.cos(phi) * Math.cos(delta);
|
|
92
|
+
if (denom !== 0) {
|
|
93
|
+
const cosH0 = (Math.sin(RISE_ALT) - Math.sin(phi) * Math.sin(delta)) / denom;
|
|
94
|
+
if (cosH0 >= -1 && cosH0 <= 1) {
|
|
95
|
+
const h0 = Math.acos(cosH0);
|
|
96
|
+
out.rise = timeAtLst(engine, jd, mod(alpha - h0, TWO_PI));
|
|
97
|
+
out.set = timeAtLst(engine, jd, mod(alpha + h0, TWO_PI));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Star-to-body parans over the day from `jd` (UT) at latitude `lat`: a fixed
|
|
104
|
+
* star and a moving body simultaneously on angles within `toleranceMin`
|
|
105
|
+
* minutes — Brady's fixed-star parans. Ordered by (star, body, jd).
|
|
106
|
+
*
|
|
107
|
+
* @param engine An engine whose data pack includes the fixed-star catalog.
|
|
108
|
+
* @param jd Julian Day in UT (the 24-hour window starts here).
|
|
109
|
+
* @param lat Geographic latitude in degrees, north positive.
|
|
110
|
+
* @param stars Catalog star names to test (see {@link Engine.starNames}).
|
|
111
|
+
* @param bodies Bodies to consider; defaults to the seven classical planets.
|
|
112
|
+
* @param toleranceMin The paran window in minutes (default 30).
|
|
113
|
+
*/
|
|
114
|
+
export function starParans(engine, jd, lat, stars, bodies = DEFAULT_PARAN_BODIES, toleranceMin = 30) {
|
|
115
|
+
const bodyEvents = [];
|
|
116
|
+
for (const b of bodies) {
|
|
117
|
+
for (const kind of PARAN_ANGLES) {
|
|
118
|
+
const t = riseSet(engine, b, jd, lat, 0, kind);
|
|
119
|
+
if (t !== null && t < jd + 1)
|
|
120
|
+
bodyEvents.push([b, kind, t]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const s of stars) {
|
|
125
|
+
const at = starAngleTimes(engine, s, jd, lat);
|
|
126
|
+
for (const [sa, ts] of Object.entries(at)) {
|
|
127
|
+
if (!(ts >= jd && ts < jd + 1))
|
|
128
|
+
continue;
|
|
129
|
+
for (const [b, ba, tb] of bodyEvents) {
|
|
130
|
+
const gap = Math.abs(ts - tb) * 1440;
|
|
131
|
+
if (gap <= toleranceMin) {
|
|
132
|
+
out.push({
|
|
133
|
+
star: s, star_angle: sa, body: b, body_angle: ba,
|
|
134
|
+
jd: Math.round(((ts + tb) / 2) * 1e6) / 1e6,
|
|
135
|
+
gap_min: Math.round(gap * 1e4) / 1e4,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
out.sort((x, y) => (x.star < y.star ? -1 : x.star > y.star ? 1 : x.body < y.body ? -1 : x.body > y.body ? 1 : x.jd - y.jd));
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine provenance -- what a chart is, and when and where it is anchored.
|
|
3
|
+
*
|
|
4
|
+
* A chart silently asserts "a real instant at a real place." That is wrong for
|
|
5
|
+
* most interesting cases: forecasts, fictional or mythic subjects, archetypes,
|
|
6
|
+
* counterfactuals, charts with only an approximate or relative time. This module
|
|
7
|
+
* makes the chart's grounding first-class so the rest of the system can act on
|
|
8
|
+
* it -- route generation (ephemeris vs the compiler's symbolic synthesis), frame
|
|
9
|
+
* interpretation honestly, and degrade gracefully when no instant exists.
|
|
10
|
+
*
|
|
11
|
+
* It does not compute charts; it resolves a {@link TemporalAnchor} /
|
|
12
|
+
* {@link SpatialAnchor} to a usable instant / place (or reports that none can be
|
|
13
|
+
* derived, and why). Pure and deterministic.
|
|
14
|
+
*/
|
|
15
|
+
/** What a chart's subject *is* -- its epistemic / ontological status. */
|
|
16
|
+
export type Realm = "observed" | "reported" | "planned" | "forecast" | "fictional" | "mythic" | "counterfactual" | "archetypal" | "conceptual";
|
|
17
|
+
/** How a chart's time is known. */
|
|
18
|
+
export type TemporalAnchor = {
|
|
19
|
+
kind: "instant";
|
|
20
|
+
utc: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "range";
|
|
23
|
+
earliest: string;
|
|
24
|
+
latest: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "relative";
|
|
27
|
+
relation: "before" | "after" | "during";
|
|
28
|
+
anchorId: string;
|
|
29
|
+
offset?: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "narrative";
|
|
32
|
+
calendar?: string;
|
|
33
|
+
value: string;
|
|
34
|
+
sequence?: number;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "symbolic";
|
|
37
|
+
rationale: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "none";
|
|
40
|
+
reason: "atemporal" | "time_irrelevant" | "intentionally_unset";
|
|
41
|
+
};
|
|
42
|
+
/** How a chart's place is known -- the spatial twin of {@link TemporalAnchor}. */
|
|
43
|
+
export type SpatialAnchor = {
|
|
44
|
+
kind: "geo";
|
|
45
|
+
lat: number;
|
|
46
|
+
lonEast: number;
|
|
47
|
+
altM?: number;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "named";
|
|
50
|
+
placeId: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "region";
|
|
53
|
+
lat: number;
|
|
54
|
+
lonEast: number;
|
|
55
|
+
radiusKm: number;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "relative";
|
|
58
|
+
relation: "near" | "at";
|
|
59
|
+
anchorId: string;
|
|
60
|
+
} | {
|
|
61
|
+
kind: "fictional";
|
|
62
|
+
value: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "none";
|
|
65
|
+
reason: "heliocentric" | "atemporal" | "intentionally_unset";
|
|
66
|
+
};
|
|
67
|
+
/** Resolved coordinates a chart can be computed at. */
|
|
68
|
+
export interface GeoPlace {
|
|
69
|
+
lat: number;
|
|
70
|
+
lonEast: number;
|
|
71
|
+
altM?: number;
|
|
72
|
+
}
|
|
73
|
+
/** Lookups an anchor may need to resolve: prior instants/places for `relative`
|
|
74
|
+
* anchors, calendar resolvers for `narrative` times, a gazetteer for `named`
|
|
75
|
+
* places. All optional; an anchor that needs a missing one resolves to null. */
|
|
76
|
+
export interface AnchorRegistry {
|
|
77
|
+
/** `anchorId` -> a resolved instant (UT Julian Day). */
|
|
78
|
+
instants?: Record<string, number>;
|
|
79
|
+
/** Calendar name -> `value` -> UT Julian Day (or null when unmappable). */
|
|
80
|
+
calendars?: Record<string, (value: string) => number | null>;
|
|
81
|
+
/** `anchorId` -> a resolved place. */
|
|
82
|
+
places?: Record<string, GeoPlace>;
|
|
83
|
+
/** Named-place resolver (e.g. the gazetteer). */
|
|
84
|
+
gazetteer?: (placeId: string) => GeoPlace | null;
|
|
85
|
+
}
|
|
86
|
+
/** How trustworthy a resolved instant/place is for computation. */
|
|
87
|
+
export type Certainty = "exact" | "approximate" | "representative" | "none";
|
|
88
|
+
/** The outcome of resolving a {@link TemporalAnchor}. */
|
|
89
|
+
export interface ResolvedTime {
|
|
90
|
+
/** A concrete UT Julian Day to compute with, or null when none can be derived. */
|
|
91
|
+
jd: number | null;
|
|
92
|
+
certainty: Certainty;
|
|
93
|
+
/** Bounds in UT JD, for ranges (and relatives with a known reference). */
|
|
94
|
+
earliest?: number;
|
|
95
|
+
latest?: number;
|
|
96
|
+
/** How `jd` was derived, or why it is null. */
|
|
97
|
+
note?: string;
|
|
98
|
+
}
|
|
99
|
+
/** The outcome of resolving a {@link SpatialAnchor}. */
|
|
100
|
+
export interface ResolvedPlace {
|
|
101
|
+
place: GeoPlace | null;
|
|
102
|
+
certainty: Certainty;
|
|
103
|
+
/** For a `region`, its radius in km. */
|
|
104
|
+
radiusKm?: number;
|
|
105
|
+
note?: string;
|
|
106
|
+
}
|
|
107
|
+
/** ISO-8601 timestamp -> UT Julian Day, or null when unparseable. */
|
|
108
|
+
export declare function isoToJd(iso: string): number | null;
|
|
109
|
+
/**
|
|
110
|
+
* Parse a duration offset into days. Accepts a compact single unit
|
|
111
|
+
* (`"3d"`, `"-2h"`, `"1.5y"`, `"6mo"`, `"90m"`) or an ISO-8601 duration
|
|
112
|
+
* (`"P1Y2M10DT2H30M"`). Calendar units use mean lengths (year 365.2425 d,
|
|
113
|
+
* month 30.436875 d). Returns `NaN` when unparseable.
|
|
114
|
+
*/
|
|
115
|
+
export declare function parseOffset(offset: string): number;
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a {@link TemporalAnchor} to a usable instant, using `registry` for
|
|
118
|
+
* relative references and narrative calendars. The result always reports its
|
|
119
|
+
* {@link Certainty}; `jd` is null exactly when no instant can be derived
|
|
120
|
+
* (`symbolic`, `none`, an unknown reference, or an unmappable calendar).
|
|
121
|
+
*/
|
|
122
|
+
export declare function resolveTime(anchor: TemporalAnchor, registry?: AnchorRegistry): ResolvedTime;
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a {@link SpatialAnchor} to coordinates, using `registry` for relative
|
|
125
|
+
* references and the gazetteer for named places. `place` is null when no
|
|
126
|
+
* coordinates can be derived (`fictional`, `none`, an unknown reference).
|
|
127
|
+
*/
|
|
128
|
+
export declare function resolvePlace(anchor: SpatialAnchor, registry?: AnchorRegistry): ResolvedPlace;
|
|
129
|
+
/** Realms whose charts come from a time + place (the ephemeris path). The rest
|
|
130
|
+
* (`archetypal`, `conceptual`, `mythic`) are better generated from constraints
|
|
131
|
+
* via the compiler, since they have no instant to compute from. */
|
|
132
|
+
export declare const TIME_ANCHORED_REALMS: ReadonlySet<Realm>;
|
|
133
|
+
/** Whether a realm is normally grounded in an instant (ephemeris) rather than
|
|
134
|
+
* synthesized from symbolic constraints. */
|
|
135
|
+
export declare function isTimeAnchored(realm: Realm): boolean;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine provenance -- what a chart is, and when and where it is anchored.
|
|
3
|
+
*
|
|
4
|
+
* A chart silently asserts "a real instant at a real place." That is wrong for
|
|
5
|
+
* most interesting cases: forecasts, fictional or mythic subjects, archetypes,
|
|
6
|
+
* counterfactuals, charts with only an approximate or relative time. This module
|
|
7
|
+
* makes the chart's grounding first-class so the rest of the system can act on
|
|
8
|
+
* it -- route generation (ephemeris vs the compiler's symbolic synthesis), frame
|
|
9
|
+
* interpretation honestly, and degrade gracefully when no instant exists.
|
|
10
|
+
*
|
|
11
|
+
* It does not compute charts; it resolves a {@link TemporalAnchor} /
|
|
12
|
+
* {@link SpatialAnchor} to a usable instant / place (or reports that none can be
|
|
13
|
+
* derived, and why). Pure and deterministic.
|
|
14
|
+
*/
|
|
15
|
+
const JD_UNIX_EPOCH = 2440587.5; // JD of 1970-01-01T00:00:00Z
|
|
16
|
+
/** ISO-8601 timestamp -> UT Julian Day, or null when unparseable. */
|
|
17
|
+
export function isoToJd(iso) {
|
|
18
|
+
const ms = Date.parse(iso);
|
|
19
|
+
return Number.isNaN(ms) ? null : JD_UNIX_EPOCH + ms / 86400000;
|
|
20
|
+
}
|
|
21
|
+
const UNIT_DAYS = {
|
|
22
|
+
y: 365.2425, mo: 30.436875, w: 7, d: 1, h: 1 / 24, m: 1 / 1440, s: 1 / 86400,
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Parse a duration offset into days. Accepts a compact single unit
|
|
26
|
+
* (`"3d"`, `"-2h"`, `"1.5y"`, `"6mo"`, `"90m"`) or an ISO-8601 duration
|
|
27
|
+
* (`"P1Y2M10DT2H30M"`). Calendar units use mean lengths (year 365.2425 d,
|
|
28
|
+
* month 30.436875 d). Returns `NaN` when unparseable.
|
|
29
|
+
*/
|
|
30
|
+
export function parseOffset(offset) {
|
|
31
|
+
const s = offset.trim();
|
|
32
|
+
if (/^[+-]?P/i.test(s)) {
|
|
33
|
+
const m = s.match(/^([+-]?)P(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i);
|
|
34
|
+
if (!m || s.replace(/^[+-]/, "") === "P")
|
|
35
|
+
return NaN;
|
|
36
|
+
const [, sign, y, mo, w, d, h, mi, sec] = m;
|
|
37
|
+
const n = (v) => (v ? parseFloat(v) : 0);
|
|
38
|
+
const days = n(y) * UNIT_DAYS.y + n(mo) * UNIT_DAYS.mo + n(w) * UNIT_DAYS.w
|
|
39
|
+
+ n(d) + n(h) / 24 + n(mi) / 1440 + n(sec) / 86400;
|
|
40
|
+
return sign === "-" ? -days : days;
|
|
41
|
+
}
|
|
42
|
+
const m = s.match(/^([+-]?\d*\.?\d+)\s*(mo|[ywdhms])$/i);
|
|
43
|
+
if (!m)
|
|
44
|
+
return NaN;
|
|
45
|
+
return parseFloat(m[1]) * UNIT_DAYS[m[2].toLowerCase()];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a {@link TemporalAnchor} to a usable instant, using `registry` for
|
|
49
|
+
* relative references and narrative calendars. The result always reports its
|
|
50
|
+
* {@link Certainty}; `jd` is null exactly when no instant can be derived
|
|
51
|
+
* (`symbolic`, `none`, an unknown reference, or an unmappable calendar).
|
|
52
|
+
*/
|
|
53
|
+
export function resolveTime(anchor, registry = {}) {
|
|
54
|
+
switch (anchor.kind) {
|
|
55
|
+
case "instant": {
|
|
56
|
+
const jd = isoToJd(anchor.utc);
|
|
57
|
+
return jd === null
|
|
58
|
+
? { jd: null, certainty: "none", note: `unparseable utc ${anchor.utc}` }
|
|
59
|
+
: { jd, certainty: "exact" };
|
|
60
|
+
}
|
|
61
|
+
case "range": {
|
|
62
|
+
const e = isoToJd(anchor.earliest);
|
|
63
|
+
const l = isoToJd(anchor.latest);
|
|
64
|
+
if (e === null || l === null) {
|
|
65
|
+
return { jd: null, certainty: "none", note: "unparseable range bound" };
|
|
66
|
+
}
|
|
67
|
+
const [lo, hi] = e <= l ? [e, l] : [l, e];
|
|
68
|
+
return {
|
|
69
|
+
jd: (lo + hi) / 2, certainty: "representative", earliest: lo, latest: hi,
|
|
70
|
+
note: "midpoint of the range",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
case "relative": {
|
|
74
|
+
const base = registry.instants?.[anchor.anchorId];
|
|
75
|
+
if (base === undefined) {
|
|
76
|
+
return { jd: null, certainty: "none", note: `unknown anchor ${anchor.anchorId}` };
|
|
77
|
+
}
|
|
78
|
+
if (anchor.relation === "during") {
|
|
79
|
+
return { jd: base, certainty: "representative", note: `during ${anchor.anchorId}` };
|
|
80
|
+
}
|
|
81
|
+
if (anchor.offset === undefined) {
|
|
82
|
+
return {
|
|
83
|
+
jd: base, certainty: "approximate",
|
|
84
|
+
note: `${anchor.relation} ${anchor.anchorId} with no offset; using the reference instant`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const off = parseOffset(anchor.offset);
|
|
88
|
+
if (Number.isNaN(off)) {
|
|
89
|
+
return { jd: null, certainty: "none", note: `unparseable offset ${anchor.offset}` };
|
|
90
|
+
}
|
|
91
|
+
const jd = anchor.relation === "before" ? base - off : base + off;
|
|
92
|
+
return { jd, certainty: "approximate", note: `${anchor.offset} ${anchor.relation} ${anchor.anchorId}` };
|
|
93
|
+
}
|
|
94
|
+
case "narrative": {
|
|
95
|
+
const resolver = anchor.calendar ? registry.calendars?.[anchor.calendar] : undefined;
|
|
96
|
+
if (!resolver) {
|
|
97
|
+
return {
|
|
98
|
+
jd: null, certainty: "none",
|
|
99
|
+
note: `no resolver for calendar ${anchor.calendar ?? "(unspecified)"}`
|
|
100
|
+
+ (anchor.sequence !== undefined ? `; sequence ${anchor.sequence}` : ""),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const jd = resolver(anchor.value);
|
|
104
|
+
return jd === null
|
|
105
|
+
? { jd: null, certainty: "none", note: `calendar ${anchor.calendar} could not map ${anchor.value}` }
|
|
106
|
+
: { jd, certainty: "approximate", note: `${anchor.calendar}: ${anchor.value}` };
|
|
107
|
+
}
|
|
108
|
+
case "symbolic":
|
|
109
|
+
return { jd: null, certainty: "none", note: anchor.rationale };
|
|
110
|
+
case "none":
|
|
111
|
+
return { jd: null, certainty: "none", note: anchor.reason };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a {@link SpatialAnchor} to coordinates, using `registry` for relative
|
|
116
|
+
* references and the gazetteer for named places. `place` is null when no
|
|
117
|
+
* coordinates can be derived (`fictional`, `none`, an unknown reference).
|
|
118
|
+
*/
|
|
119
|
+
export function resolvePlace(anchor, registry = {}) {
|
|
120
|
+
switch (anchor.kind) {
|
|
121
|
+
case "geo":
|
|
122
|
+
return {
|
|
123
|
+
place: { lat: anchor.lat, lonEast: anchor.lonEast, altM: anchor.altM },
|
|
124
|
+
certainty: "exact",
|
|
125
|
+
};
|
|
126
|
+
case "named": {
|
|
127
|
+
const place = registry.gazetteer?.(anchor.placeId) ?? null;
|
|
128
|
+
return place
|
|
129
|
+
? { place, certainty: "approximate", note: `gazetteer: ${anchor.placeId}` }
|
|
130
|
+
: { place: null, certainty: "none", note: `unknown place ${anchor.placeId}` };
|
|
131
|
+
}
|
|
132
|
+
case "region":
|
|
133
|
+
return {
|
|
134
|
+
place: { lat: anchor.lat, lonEast: anchor.lonEast }, certainty: "representative",
|
|
135
|
+
radiusKm: anchor.radiusKm, note: `centre of a ${anchor.radiusKm} km region`,
|
|
136
|
+
};
|
|
137
|
+
case "relative": {
|
|
138
|
+
const place = registry.places?.[anchor.anchorId] ?? null;
|
|
139
|
+
return place
|
|
140
|
+
? { place, certainty: "approximate", note: `${anchor.relation} ${anchor.anchorId}` }
|
|
141
|
+
: { place: null, certainty: "none", note: `unknown place anchor ${anchor.anchorId}` };
|
|
142
|
+
}
|
|
143
|
+
case "fictional":
|
|
144
|
+
return { place: null, certainty: "none", note: anchor.value };
|
|
145
|
+
case "none":
|
|
146
|
+
return { place: null, certainty: "none", note: anchor.reason };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Realms whose charts come from a time + place (the ephemeris path). The rest
|
|
150
|
+
* (`archetypal`, `conceptual`, `mythic`) are better generated from constraints
|
|
151
|
+
* via the compiler, since they have no instant to compute from. */
|
|
152
|
+
export const TIME_ANCHORED_REALMS = new Set([
|
|
153
|
+
"observed", "reported", "planned", "forecast", "counterfactual",
|
|
154
|
+
]);
|
|
155
|
+
/** Whether a realm is normally grounded in an instant (ephemeris) rather than
|
|
156
|
+
* synthesized from symbolic constraints. */
|
|
157
|
+
export function isTimeAnchored(realm) {
|
|
158
|
+
return TIME_ANCHORED_REALMS.has(realm);
|
|
159
|
+
}
|