@macrostrat/column-views 3.11.0 → 3.12.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/CHANGELOG.md +169 -0
- package/dist/age-model/data.cjs +57 -0
- package/dist/age-model/data.cjs.map +1 -0
- package/dist/age-model/data.d.ts +29 -0
- package/dist/age-model/data.js +57 -0
- package/dist/age-model/data.js.map +1 -0
- package/dist/age-model/details.cjs +261 -0
- package/dist/age-model/details.cjs.map +1 -0
- package/dist/age-model/details.d.ts +39 -0
- package/dist/age-model/details.js +258 -0
- package/dist/age-model/details.js.map +1 -0
- package/dist/age-model/details.module.sass.cjs +42 -0
- package/dist/age-model/details.module.sass.cjs.map +1 -0
- package/dist/age-model/details.module.sass.js +40 -0
- package/dist/age-model/details.module.sass.js.map +1 -0
- package/dist/age-model/index.d.ts +5 -0
- package/dist/age-model/legacy.cjs +28 -0
- package/dist/age-model/legacy.cjs.map +1 -0
- package/dist/age-model/legacy.d.ts +6 -0
- package/dist/age-model/legacy.js +26 -0
- package/dist/age-model/legacy.js.map +1 -0
- package/dist/age-model/surfaces.cjs +355 -0
- package/dist/age-model/surfaces.cjs.map +1 -0
- package/dist/age-model/surfaces.d.ts +68 -0
- package/dist/age-model/surfaces.js +352 -0
- package/dist/age-model/surfaces.js.map +1 -0
- package/dist/age-model/surfaces.module.sass.cjs +45 -0
- package/dist/age-model/surfaces.module.sass.cjs.map +1 -0
- package/dist/age-model/surfaces.module.sass.js +43 -0
- package/dist/age-model/surfaces.module.sass.js.map +1 -0
- package/dist/age-model/types.cjs +229 -0
- package/dist/age-model/types.cjs.map +1 -0
- package/dist/age-model/types.d.ts +112 -0
- package/dist/age-model/types.js +229 -0
- package/dist/age-model/types.js.map +1 -0
- package/dist/column-views.css +389 -57
- package/dist/column.cjs +20 -13
- package/dist/column.cjs.map +1 -1
- package/dist/column.d.ts +21 -6
- package/dist/column.js +21 -14
- package/dist/column.js.map +1 -1
- package/dist/column.module.sass.cjs +19 -15
- package/dist/column.module.sass.cjs.map +1 -1
- package/dist/column.module.sass.js +19 -15
- package/dist/column.module.sass.js.map +1 -1
- package/dist/data-provider/store.cjs +34 -1
- package/dist/data-provider/store.cjs.map +1 -1
- package/dist/data-provider/store.d.ts +5 -0
- package/dist/data-provider/store.js +35 -2
- package/dist/data-provider/store.js.map +1 -1
- package/dist/index.cjs +54 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +54 -3
- package/dist/index.js.map +1 -1
- package/dist/notes.cjs +11 -2
- package/dist/notes.cjs.map +1 -1
- package/dist/notes.d.ts +11 -1
- package/dist/notes.js +11 -2
- package/dist/notes.js.map +1 -1
- package/dist/prepare-units/composite-scale.cjs +18 -16
- package/dist/prepare-units/composite-scale.cjs.map +1 -1
- package/dist/prepare-units/composite-scale.js +18 -16
- package/dist/prepare-units/composite-scale.js.map +1 -1
- package/dist/prepare-units/density.cjs +83 -0
- package/dist/prepare-units/density.cjs.map +1 -0
- package/dist/prepare-units/density.d.ts +92 -0
- package/dist/prepare-units/density.js +83 -0
- package/dist/prepare-units/density.js.map +1 -0
- package/dist/prepare-units/index.cjs +15 -7
- package/dist/prepare-units/index.cjs.map +1 -1
- package/dist/prepare-units/index.d.ts +1 -0
- package/dist/prepare-units/index.js +15 -7
- package/dist/prepare-units/index.js.map +1 -1
- package/dist/prepare-units/types.cjs.map +1 -1
- package/dist/prepare-units/types.d.ts +34 -13
- package/dist/prepare-units/types.js.map +1 -1
- package/dist/section.cjs +108 -24
- package/dist/section.cjs.map +1 -1
- package/dist/section.d.ts +66 -6
- package/dist/section.js +109 -25
- package/dist/section.js.map +1 -1
- package/dist/timescale-zoom.cjs +191 -0
- package/dist/timescale-zoom.cjs.map +1 -0
- package/dist/timescale-zoom.d.ts +74 -0
- package/dist/timescale-zoom.js +191 -0
- package/dist/timescale-zoom.js.map +1 -0
- package/dist/unit-details/age-range.cjs +1 -0
- package/dist/unit-details/age-range.cjs.map +1 -1
- package/dist/unit-details/age-range.d.ts +5 -1
- package/dist/unit-details/age-range.js +1 -0
- package/dist/unit-details/age-range.js.map +1 -1
- package/dist/units/composite.cjs +4 -1
- package/dist/units/composite.cjs.map +1 -1
- package/dist/units/composite.d.ts +2 -0
- package/dist/units/composite.js +4 -1
- package/dist/units/composite.js.map +1 -1
- package/package.json +7 -7
- package/src/age-model/data.ts +98 -0
- package/src/age-model/details.module.sass +108 -0
- package/src/age-model/details.ts +333 -0
- package/src/age-model/index.ts +5 -0
- package/src/age-model/legacy.ts +30 -0
- package/src/age-model/surfaces.module.sass +168 -0
- package/src/age-model/surfaces.ts +568 -0
- package/src/age-model/types.ts +392 -0
- package/src/column.module.sass +59 -8
- package/src/column.ts +49 -23
- package/src/data-provider/store.ts +50 -1
- package/src/index.ts +5 -1
- package/src/notes.ts +20 -2
- package/src/prepare-units/composite-scale.ts +34 -31
- package/src/prepare-units/density.ts +197 -0
- package/src/prepare-units/index.ts +25 -6
- package/src/prepare-units/types.ts +34 -13
- package/src/section.ts +219 -33
- package/src/timescale-zoom.ts +341 -0
- package/src/unit-details/age-range.ts +3 -3
- package/src/units/composite.ts +11 -0
- package/dist/age-model-overlay.cjs +0 -50
- package/dist/age-model-overlay.cjs.map +0 -1
- package/dist/age-model-overlay.d.ts +0 -24
- package/dist/age-model-overlay.js +0 -48
- package/dist/age-model-overlay.js.map +0 -1
- package/dist/age-model-overlay.module.sass.cjs +0 -12
- package/dist/age-model-overlay.module.sass.cjs.map +0 -1
- package/dist/age-model-overlay.module.sass.js +0 -11
- package/dist/age-model-overlay.module.sass.js.map +0 -1
- package/src/age-model-overlay.module.sass +0 -11
- package/src/age-model-overlay.ts +0 -96
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const columnComponents = require("@macrostrat/column-components");
|
|
4
|
+
const surfaceStatuses = [
|
|
5
|
+
"absolute",
|
|
6
|
+
"relative",
|
|
7
|
+
"spike",
|
|
8
|
+
"imposed",
|
|
9
|
+
"modeled",
|
|
10
|
+
"",
|
|
11
|
+
"derived"
|
|
12
|
+
];
|
|
13
|
+
const TIE_POINT_STATUSES = [
|
|
14
|
+
"absolute",
|
|
15
|
+
"relative",
|
|
16
|
+
"spike",
|
|
17
|
+
"imposed"
|
|
18
|
+
];
|
|
19
|
+
const surfaceStatusLabels = {
|
|
20
|
+
absolute: "Absolute",
|
|
21
|
+
relative: "Relative",
|
|
22
|
+
spike: "Spike",
|
|
23
|
+
imposed: "Imposed",
|
|
24
|
+
modeled: "Modeled",
|
|
25
|
+
"": "Unspecified",
|
|
26
|
+
derived: "Derived"
|
|
27
|
+
};
|
|
28
|
+
const surfaceStatusDescriptions = {
|
|
29
|
+
absolute: "Independently dated; the age is authoritative and the interval position is derived from it",
|
|
30
|
+
relative: "Calibrated to a position within a geologic interval; the age follows the timescale",
|
|
31
|
+
spike: "Tied to a formally defined boundary point",
|
|
32
|
+
imposed: "Interval-relative representation imposed on a boundary with no chronostratigraphic tie of its own (for instance, thickness interpolation)",
|
|
33
|
+
modeled: "Interpolated by the age model between calibrated surfaces",
|
|
34
|
+
"": "No status recorded",
|
|
35
|
+
derived: "Computed from unit tops and bottoms; not stored in the column's age model"
|
|
36
|
+
};
|
|
37
|
+
function surfaceToken(value) {
|
|
38
|
+
if (value == null || value === "") return "unspecified";
|
|
39
|
+
return value.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
function nullifyUnitID(id) {
|
|
42
|
+
if (id == null || id === 0) return null;
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
const INTERVAL_BOUND_TOLERANCE = 1e-3;
|
|
46
|
+
function inferTiePointStatuses(surfaces, tolerance = INTERVAL_BOUND_TOLERANCE) {
|
|
47
|
+
const extents = sectionAgeExtents(surfaces);
|
|
48
|
+
return surfaces.map((surface) => {
|
|
49
|
+
if (surface.status !== "modeled") return surface;
|
|
50
|
+
if (surface.calibration == null) return surface;
|
|
51
|
+
const { proportion } = surface;
|
|
52
|
+
const onIntervalBound = proportion != null && (Math.abs(proportion) <= tolerance || Math.abs(proportion - 1) <= tolerance);
|
|
53
|
+
if (!onIntervalBound && !boundsPackage(surface, extents)) {
|
|
54
|
+
return surface;
|
|
55
|
+
}
|
|
56
|
+
return { ...surface, status: "relative", statusInferred: true };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function boundsPackage(surface, extents) {
|
|
60
|
+
const extent = extents.get(surface.section_id ?? null);
|
|
61
|
+
if (extent == null) return false;
|
|
62
|
+
const [youngest, oldest] = extent;
|
|
63
|
+
if (surface.unitsAbove.length === 0 && surface.age === youngest) return true;
|
|
64
|
+
return surface.unitsBelow.length === 0 && surface.age === oldest;
|
|
65
|
+
}
|
|
66
|
+
function sectionAgeExtents(surfaces) {
|
|
67
|
+
const extents = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const surface of surfaces) {
|
|
69
|
+
const key = surface.section_id ?? null;
|
|
70
|
+
const extent = extents.get(key);
|
|
71
|
+
if (extent == null) {
|
|
72
|
+
extents.set(key, [surface.age, surface.age]);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
extent[0] = Math.min(extent[0], surface.age);
|
|
76
|
+
extent[1] = Math.max(extent[1], surface.age);
|
|
77
|
+
}
|
|
78
|
+
return extents;
|
|
79
|
+
}
|
|
80
|
+
function boundaryToSurface(boundary) {
|
|
81
|
+
const above = nullifyUnitID(boundary.unit_above);
|
|
82
|
+
const below = nullifyUnitID(boundary.unit_below);
|
|
83
|
+
let calibration = null;
|
|
84
|
+
if (boundary.interval_id != null) {
|
|
85
|
+
calibration = {
|
|
86
|
+
id: boundary.interval_id,
|
|
87
|
+
name: boundary.interval_name,
|
|
88
|
+
b_age: boundary.age_bottom,
|
|
89
|
+
t_age: boundary.age_top
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
id: boundary.boundary_id,
|
|
94
|
+
age: boundary.model_age,
|
|
95
|
+
position: boundary.boundary_position,
|
|
96
|
+
status: boundary.boundary_status ?? "",
|
|
97
|
+
type: boundary.boundary_type ?? "",
|
|
98
|
+
calibration,
|
|
99
|
+
proportion: boundary.rel_position,
|
|
100
|
+
unitsAbove: above == null ? [] : [above],
|
|
101
|
+
unitsBelow: below == null ? [] : [below],
|
|
102
|
+
section_id: boundary.section_id,
|
|
103
|
+
ref_id: boundary.ref_id,
|
|
104
|
+
boundary
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function surfacesFromBoundaries(boundaries, options = {}) {
|
|
108
|
+
const { inferTiePoints = true } = options;
|
|
109
|
+
if (boundaries == null) return [];
|
|
110
|
+
let surfaces = boundaries.map(boundaryToSurface);
|
|
111
|
+
if (inferTiePoints) {
|
|
112
|
+
surfaces = inferTiePointStatuses(surfaces);
|
|
113
|
+
}
|
|
114
|
+
return surfaces.sort((a, b) => a.age - b.age);
|
|
115
|
+
}
|
|
116
|
+
function surfacesFromUnits(units, options = {}) {
|
|
117
|
+
const { tolerance = 1e-3, axisType = columnComponents.ColumnAxisType.AGE } = options;
|
|
118
|
+
if (units == null) return [];
|
|
119
|
+
const byPosition = isPositionAxis(axisType);
|
|
120
|
+
const surfaces = [];
|
|
121
|
+
function keyOf(surface) {
|
|
122
|
+
if (byPosition) return surface.position ?? null;
|
|
123
|
+
return surface.age;
|
|
124
|
+
}
|
|
125
|
+
function addBound(unit, top) {
|
|
126
|
+
const age = top ? unit.t_age : unit.b_age;
|
|
127
|
+
const position = asNumber(top ? unit.t_pos : unit.b_pos);
|
|
128
|
+
const key = byPosition ? position : age;
|
|
129
|
+
if (key == null) return;
|
|
130
|
+
let surface = surfaces.find((d) => {
|
|
131
|
+
const k = keyOf(d);
|
|
132
|
+
return k != null && Math.abs(k - key) < tolerance;
|
|
133
|
+
});
|
|
134
|
+
if (surface == null) {
|
|
135
|
+
surface = {
|
|
136
|
+
id: "",
|
|
137
|
+
age,
|
|
138
|
+
position,
|
|
139
|
+
status: "derived",
|
|
140
|
+
type: "",
|
|
141
|
+
calibration: null,
|
|
142
|
+
proportion: null,
|
|
143
|
+
unitsAbove: [],
|
|
144
|
+
unitsBelow: [],
|
|
145
|
+
section_id: unit.section_id
|
|
146
|
+
};
|
|
147
|
+
surfaces.push(surface);
|
|
148
|
+
}
|
|
149
|
+
if (top) {
|
|
150
|
+
surface.unitsBelow.push(unit.unit_id);
|
|
151
|
+
} else {
|
|
152
|
+
surface.unitsAbove.push(unit.unit_id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const unit of units) {
|
|
156
|
+
addBound(unit, true);
|
|
157
|
+
addBound(unit, false);
|
|
158
|
+
}
|
|
159
|
+
surfaces.sort((a, b) => compareAlongAxis(a, b, axisType));
|
|
160
|
+
return surfaces.map((s, i) => ({ ...s, id: `derived-${i}` }));
|
|
161
|
+
}
|
|
162
|
+
function surfacePosition(surface, axisType) {
|
|
163
|
+
if (isPositionAxis(axisType)) {
|
|
164
|
+
return surface.position ?? null;
|
|
165
|
+
}
|
|
166
|
+
return surface.age ?? null;
|
|
167
|
+
}
|
|
168
|
+
function isPositionAxis(axisType) {
|
|
169
|
+
return axisType === columnComponents.ColumnAxisType.HEIGHT || axisType === columnComponents.ColumnAxisType.DEPTH;
|
|
170
|
+
}
|
|
171
|
+
function compareAlongAxis(a, b, axisType) {
|
|
172
|
+
const pa = surfacePosition(a, axisType);
|
|
173
|
+
const pb = surfacePosition(b, axisType);
|
|
174
|
+
if (pa == null || pb == null) return 0;
|
|
175
|
+
if (axisType === columnComponents.ColumnAxisType.HEIGHT) {
|
|
176
|
+
return pb - pa;
|
|
177
|
+
}
|
|
178
|
+
return pa - pb;
|
|
179
|
+
}
|
|
180
|
+
function formatProportion(proportion) {
|
|
181
|
+
if (proportion == null) return null;
|
|
182
|
+
return `${Math.round(proportion * 100)}%`;
|
|
183
|
+
}
|
|
184
|
+
function surfaceLabel(surface) {
|
|
185
|
+
const { calibration } = surface;
|
|
186
|
+
if (calibration != null) {
|
|
187
|
+
const prop = formatProportion(surface.proportion);
|
|
188
|
+
if (prop != null) {
|
|
189
|
+
return `${calibration.name} · ${prop}`;
|
|
190
|
+
}
|
|
191
|
+
return calibration.name;
|
|
192
|
+
}
|
|
193
|
+
return formatAge(surface.age);
|
|
194
|
+
}
|
|
195
|
+
function formatAge(age) {
|
|
196
|
+
if (age == null) return "";
|
|
197
|
+
let value = age;
|
|
198
|
+
let unit = "Ma";
|
|
199
|
+
if (Math.abs(age) < 1) {
|
|
200
|
+
value = age * 1e3;
|
|
201
|
+
unit = "ka";
|
|
202
|
+
}
|
|
203
|
+
const _value = value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
204
|
+
return `${_value} ${unit}`;
|
|
205
|
+
}
|
|
206
|
+
function asNumber(value) {
|
|
207
|
+
if (value == null || value === "") return null;
|
|
208
|
+
const n = Number(value);
|
|
209
|
+
if (Number.isNaN(n)) return null;
|
|
210
|
+
return n;
|
|
211
|
+
}
|
|
212
|
+
exports.INTERVAL_BOUND_TOLERANCE = INTERVAL_BOUND_TOLERANCE;
|
|
213
|
+
exports.TIE_POINT_STATUSES = TIE_POINT_STATUSES;
|
|
214
|
+
exports.boundaryToSurface = boundaryToSurface;
|
|
215
|
+
exports.compareAlongAxis = compareAlongAxis;
|
|
216
|
+
exports.formatAge = formatAge;
|
|
217
|
+
exports.formatProportion = formatProportion;
|
|
218
|
+
exports.inferTiePointStatuses = inferTiePointStatuses;
|
|
219
|
+
exports.isPositionAxis = isPositionAxis;
|
|
220
|
+
exports.nullifyUnitID = nullifyUnitID;
|
|
221
|
+
exports.surfaceLabel = surfaceLabel;
|
|
222
|
+
exports.surfacePosition = surfacePosition;
|
|
223
|
+
exports.surfaceStatusDescriptions = surfaceStatusDescriptions;
|
|
224
|
+
exports.surfaceStatusLabels = surfaceStatusLabels;
|
|
225
|
+
exports.surfaceStatuses = surfaceStatuses;
|
|
226
|
+
exports.surfaceToken = surfaceToken;
|
|
227
|
+
exports.surfacesFromBoundaries = surfacesFromBoundaries;
|
|
228
|
+
exports.surfacesFromUnits = surfacesFromUnits;
|
|
229
|
+
//# sourceMappingURL=types.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.cjs","sources":["../../src/age-model/types.ts"],"sourcesContent":["/** Surfaces: the view model for a column's age model.\n *\n * A `ColumnSurface` is one horizontal boundary in a column. It is what the\n * surfaces view renders, and it is deliberately data-agnostic: a surface can\n * come from an `/age_model` boundary (`boundaryToSurface`), be derived from\n * unit tops and bottoms when a column has no age-model rows\n * (`surfacesFromUnits`), or be built by an editor from its own state.\n */\nimport type {\n AgeModelBoundary,\n BoundaryStatus,\n BoundaryType,\n UnitLong,\n} from \"@macrostrat/api-types\";\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\n\nexport type { AgeModelBoundary, BoundaryStatus, BoundaryType };\n\n/** A boundary status, plus `derived` for surfaces computed from units rather\n * than stored in the age model. */\nexport type SurfaceStatus = BoundaryStatus | \"derived\";\n\n/** The interval a surface is calibrated against */\nexport interface SurfaceCalibration {\n id: number;\n name: string;\n b_age: number;\n t_age: number;\n}\n\nexport interface ColumnSurface {\n id: number | string;\n /** Modeled age of the surface (Ma) */\n age: number;\n /** Measured position (height or depth) along the column, when known */\n position?: number | null;\n status: SurfaceStatus;\n type: BoundaryType;\n /** The calibration interval, and the surface's position within it\n * (0 at the interval's base, 1 at its top) */\n calibration?: SurfaceCalibration | null;\n proportion?: number | null;\n /** True when `status` was inferred rather than read from the data — see\n * `inferTiePointStatuses`. */\n statusInferred?: boolean;\n unitsAbove: number[];\n unitsBelow: number[];\n section_id?: number | null;\n ref_id?: number | null;\n /** The API record, when the surface came from `/age_model` */\n boundary?: AgeModelBoundary;\n}\n\nexport const surfaceStatuses: SurfaceStatus[] = [\n \"absolute\",\n \"relative\",\n \"spike\",\n \"imposed\",\n \"modeled\",\n \"\",\n \"derived\",\n];\n\n/** The statuses at which the age model was actually constrained — the tie\n * points — as opposed to surfaces whose age falls out of the model\n * (`modeled`), carries no status, or is merely derived from units. These are\n * the surfaces worth labeling by default. */\nexport const TIE_POINT_STATUSES: SurfaceStatus[] = [\n \"absolute\",\n \"relative\",\n \"spike\",\n \"imposed\",\n];\n\nexport const surfaceStatusLabels: Record<SurfaceStatus, string> = {\n absolute: \"Absolute\",\n relative: \"Relative\",\n spike: \"Spike\",\n imposed: \"Imposed\",\n modeled: \"Modeled\",\n \"\": \"Unspecified\",\n derived: \"Derived\",\n};\n\nexport const surfaceStatusDescriptions: Record<SurfaceStatus, string> = {\n absolute:\n \"Independently dated; the age is authoritative and the interval position is derived from it\",\n relative:\n \"Calibrated to a position within a geologic interval; the age follows the timescale\",\n spike: \"Tied to a formally defined boundary point\",\n imposed:\n \"Interval-relative representation imposed on a boundary with no chronostratigraphic tie of its own (for instance, thickness interpolation)\",\n modeled: \"Interpolated by the age model between calibrated surfaces\",\n \"\": \"No status recorded\",\n derived:\n \"Computed from unit tops and bottoms; not stored in the column's age model\",\n};\n\n/** A CSS-safe token for a status or type (`angular unconformity` →\n * `angular-unconformity`, `\"\"` → `unspecified`). */\nexport function surfaceToken(value: string | null | undefined): string {\n if (value == null || value === \"\") return \"unspecified\";\n return value.replace(/[^a-z0-9]+/gi, \"-\").toLowerCase();\n}\n\n/** Legacy `unit_boundaries` rows use `0` as the \"no unit\" sentinel. */\nexport function nullifyUnitID(id: number | null | undefined): number | null {\n if (id == null || id === 0) return null;\n return id;\n}\n\n/** How close to an interval's base or top a surface must sit to count as\n * bounding it (as a fraction of the interval). */\nexport const INTERVAL_BOUND_TOLERANCE = 0.001;\n\n/** Promote the `modeled` surfaces that cannot in fact have been interpolated\n * to `relative`.\n *\n * This is a client-side affordance over a known gap in the data. An age model\n * interpolates *between* tie points, so a surface with no constraint to\n * interpolate from has to be one itself, whatever `boundary_status` says. Two\n * cases are recognizable from the boundaries alone:\n *\n * - **It sits on the base or top of its calibration interval** (proportion 0\n * or 1). A height referred directly to an interval bounds the model rather\n * than falling out of it.\n * - **It is the edge of a gap-bound package** — the youngest surface in its\n * section with no unit above, or the oldest with none below. There is\n * nothing beyond it to interpolate against.\n *\n * Both are common in columns whose age model was built by assigning positions\n * within intervals, where the importer recorded everything as `modeled`;\n * filtering to the tie-point statuses would otherwise hide real ones. Promoted\n * surfaces are marked `statusInferred`, so views can say where the status came\n * from; nothing about the underlying record changes.\n *\n * Two cases are deliberately left alone. A *mid-interval* position is\n * ambiguous: an assigned position and an interpolated one are the same record\n * there, and only the data can settle it. And an open side in the *interior*\n * of a section is a lateral relationship, not a package edge — a unit that\n * pinches out while others span past it — which the model can interpolate\n * normally.\n */\nexport function inferTiePointStatuses(\n surfaces: ColumnSurface[],\n tolerance: number = INTERVAL_BOUND_TOLERANCE,\n): ColumnSurface[] {\n const extents = sectionAgeExtents(surfaces);\n\n return surfaces.map((surface) => {\n if (surface.status !== \"modeled\") return surface;\n if (surface.calibration == null) return surface;\n\n const { proportion } = surface;\n const onIntervalBound =\n proportion != null &&\n (Math.abs(proportion) <= tolerance ||\n Math.abs(proportion - 1) <= tolerance);\n\n if (!onIntervalBound && !boundsPackage(surface, extents)) {\n return surface;\n }\n return { ...surface, status: \"relative\", statusInferred: true };\n });\n}\n\n/** An open side only means a package edge at the ends of the section. In its\n * interior it means a unit pinching out while others span past it, which the\n * age model interpolates like any other surface. */\nfunction boundsPackage(\n surface: ColumnSurface,\n extents: Map<ColumnSurface[\"section_id\"], [number, number]>,\n): boolean {\n const extent = extents.get(surface.section_id ?? null);\n if (extent == null) return false;\n const [youngest, oldest] = extent;\n if (surface.unitsAbove.length === 0 && surface.age === youngest) return true;\n return surface.unitsBelow.length === 0 && surface.age === oldest;\n}\n\n/** The youngest and oldest surface age in each section. */\nfunction sectionAgeExtents(\n surfaces: ColumnSurface[],\n): Map<ColumnSurface[\"section_id\"], [number, number]> {\n const extents = new Map<ColumnSurface[\"section_id\"], [number, number]>();\n for (const surface of surfaces) {\n const key = surface.section_id ?? null;\n const extent = extents.get(key);\n if (extent == null) {\n extents.set(key, [surface.age, surface.age]);\n continue;\n }\n extent[0] = Math.min(extent[0], surface.age);\n extent[1] = Math.max(extent[1], surface.age);\n }\n return extents;\n}\n\nexport function boundaryToSurface(boundary: AgeModelBoundary): ColumnSurface {\n const above = nullifyUnitID(boundary.unit_above);\n const below = nullifyUnitID(boundary.unit_below);\n let calibration: SurfaceCalibration | null = null;\n if (boundary.interval_id != null) {\n calibration = {\n id: boundary.interval_id,\n name: boundary.interval_name,\n b_age: boundary.age_bottom,\n t_age: boundary.age_top,\n };\n }\n return {\n id: boundary.boundary_id,\n age: boundary.model_age,\n position: boundary.boundary_position,\n status: boundary.boundary_status ?? \"\",\n type: boundary.boundary_type ?? \"\",\n calibration,\n proportion: boundary.rel_position,\n unitsAbove: above == null ? [] : [above],\n unitsBelow: below == null ? [] : [below],\n section_id: boundary.section_id,\n ref_id: boundary.ref_id,\n boundary,\n };\n}\n\nexport interface SurfacesFromBoundariesOptions {\n /** Promote `modeled` surfaces that must in fact constrain the age model to\n * `relative` (default `true`) — see `inferTiePointStatuses`. */\n inferTiePoints?: boolean;\n}\n\n/** Surfaces for a column's age-model boundaries, youngest first. */\nexport function surfacesFromBoundaries(\n boundaries: AgeModelBoundary[] | null | undefined,\n options: SurfacesFromBoundariesOptions = {},\n): ColumnSurface[] {\n const { inferTiePoints = true } = options;\n if (boundaries == null) return [];\n let surfaces = boundaries.map(boundaryToSurface);\n if (inferTiePoints) {\n surfaces = inferTiePointStatuses(surfaces);\n }\n return surfaces.sort((a, b) => a.age - b.age);\n}\n\nexport interface SurfacesFromUnitsOptions {\n /** Surfaces closer than this (in the axis's units) are merged */\n tolerance?: number;\n /** Which unit field the surfaces are keyed on: ages for age columns,\n * measured positions for height and depth columns */\n axisType?: ColumnAxisType;\n}\n\n/** Derive surfaces from unit tops and bottoms. Every distinct top or bottom\n * becomes one surface with status `derived`; units sharing a boundary share\n * the surface. This is the fallback when a column has no `unit_boundaries`. */\nexport function surfacesFromUnits<T extends UnitLong>(\n units: T[] | null | undefined,\n options: SurfacesFromUnitsOptions = {},\n): ColumnSurface[] {\n const { tolerance = 0.001, axisType = ColumnAxisType.AGE } = options;\n if (units == null) return [];\n const byPosition = isPositionAxis(axisType);\n\n const surfaces: ColumnSurface[] = [];\n\n function keyOf(surface: ColumnSurface): number | null {\n if (byPosition) return surface.position ?? null;\n return surface.age;\n }\n\n function addBound(unit: T, top: boolean) {\n const age = top ? unit.t_age : unit.b_age;\n const position = asNumber(top ? unit.t_pos : unit.b_pos);\n const key = byPosition ? position : age;\n if (key == null) return;\n\n let surface = surfaces.find((d) => {\n const k = keyOf(d);\n return k != null && Math.abs(k - key) < tolerance;\n });\n if (surface == null) {\n surface = {\n id: \"\",\n age,\n position,\n status: \"derived\",\n type: \"\",\n calibration: null,\n proportion: null,\n unitsAbove: [],\n unitsBelow: [],\n section_id: unit.section_id,\n };\n surfaces.push(surface);\n }\n // The unit whose top this is lies *below* the surface, and vice versa\n if (top) {\n surface.unitsBelow.push(unit.unit_id);\n } else {\n surface.unitsAbove.push(unit.unit_id);\n }\n }\n\n for (const unit of units) {\n addBound(unit, true);\n addBound(unit, false);\n }\n\n surfaces.sort((a, b) => compareAlongAxis(a, b, axisType));\n return surfaces.map((s, i) => ({ ...s, id: `derived-${i}` }));\n}\n\n/** The coordinate a surface is drawn at for a given axis type: its age for\n * age columns, its measured position for height and depth columns. `null`\n * when the surface has no coordinate on that axis. */\nexport function surfacePosition(\n surface: ColumnSurface,\n axisType: ColumnAxisType | null | undefined,\n): number | null {\n if (isPositionAxis(axisType)) {\n return surface.position ?? null;\n }\n return surface.age ?? null;\n}\n\nexport function isPositionAxis(\n axisType: ColumnAxisType | null | undefined,\n): boolean {\n return (\n axisType === ColumnAxisType.HEIGHT || axisType === ColumnAxisType.DEPTH\n );\n}\n\n/** Order surfaces from the top of the column downwards. */\nexport function compareAlongAxis(\n a: ColumnSurface,\n b: ColumnSurface,\n axisType: ColumnAxisType | null | undefined,\n): number {\n const pa = surfacePosition(a, axisType);\n const pb = surfacePosition(b, axisType);\n if (pa == null || pb == null) return 0;\n if (axisType === ColumnAxisType.HEIGHT) {\n // Heights increase upwards\n return pb - pa;\n }\n // Ages and depths increase downwards\n return pa - pb;\n}\n\n/** Percent of the way up its calibration interval, e.g. `85%`. */\nexport function formatProportion(\n proportion: number | null | undefined,\n): string | null {\n if (proportion == null) return null;\n return `${Math.round(proportion * 100)}%`;\n}\n\n/** A short text label for a surface: its calibration (\"Calymmian · 85%\")\n * or, failing that, its age. */\nexport function surfaceLabel(surface: ColumnSurface): string {\n const { calibration } = surface;\n if (calibration != null) {\n const prop = formatProportion(surface.proportion);\n if (prop != null) {\n return `${calibration.name} · ${prop}`;\n }\n return calibration.name;\n }\n return formatAge(surface.age);\n}\n\nexport function formatAge(age: number | null | undefined): string {\n if (age == null) return \"\";\n let value = age;\n let unit = \"Ma\";\n if (Math.abs(age) < 1) {\n value = age * 1000;\n unit = \"ka\";\n }\n const _value = value.toLocaleString(\"en-US\", { maximumFractionDigits: 2 });\n return `${_value} ${unit}`;\n}\n\nfunction asNumber(value: unknown): number | null {\n if (value == null || value === \"\") return null;\n const n = Number(value);\n if (Number.isNaN(n)) return null;\n return n;\n}\n"],"names":["ColumnAxisType"],"mappings":";;;AAqDO,MAAM,kBAAmC;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,MAAM,qBAAsC;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,sBAAqD;AAAA,EAChE,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,SAAS;AACX;AAEO,MAAM,4BAA2D;AAAA,EACtE,UACE;AAAA,EACF,UACE;AAAA,EACF,OAAO;AAAA,EACP,SACE;AAAA,EACF,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,SACE;AACJ;AAIO,SAAS,aAAa,OAA0C;AACrE,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO;AAC1C,SAAO,MAAM,QAAQ,gBAAgB,GAAG,EAAE,YAAA;AAC5C;AAGO,SAAS,cAAc,IAA8C;AAC1E,MAAI,MAAM,QAAQ,OAAO,EAAG,QAAO;AACnC,SAAO;AACT;AAIO,MAAM,2BAA2B;AA8BjC,SAAS,sBACd,UACA,YAAoB,0BACH;AACjB,QAAM,UAAU,kBAAkB,QAAQ;AAE1C,SAAO,SAAS,IAAI,CAAC,YAAY;AAC/B,QAAI,QAAQ,WAAW,UAAW,QAAO;AACzC,QAAI,QAAQ,eAAe,KAAM,QAAO;AAExC,UAAM,EAAE,eAAe;AACvB,UAAM,kBACJ,cAAc,SACb,KAAK,IAAI,UAAU,KAAK,aACvB,KAAK,IAAI,aAAa,CAAC,KAAK;AAEhC,QAAI,CAAC,mBAAmB,CAAC,cAAc,SAAS,OAAO,GAAG;AACxD,aAAO;AAAA,IACT;AACA,WAAO,EAAE,GAAG,SAAS,QAAQ,YAAY,gBAAgB,KAAA;AAAA,EAC3D,CAAC;AACH;AAKA,SAAS,cACP,SACA,SACS;AACT,QAAM,SAAS,QAAQ,IAAI,QAAQ,cAAc,IAAI;AACrD,MAAI,UAAU,KAAM,QAAO;AAC3B,QAAM,CAAC,UAAU,MAAM,IAAI;AAC3B,MAAI,QAAQ,WAAW,WAAW,KAAK,QAAQ,QAAQ,SAAU,QAAO;AACxE,SAAO,QAAQ,WAAW,WAAW,KAAK,QAAQ,QAAQ;AAC5D;AAGA,SAAS,kBACP,UACoD;AACpD,QAAM,8BAAc,IAAA;AACpB,aAAW,WAAW,UAAU;AAC9B,UAAM,MAAM,QAAQ,cAAc;AAClC,UAAM,SAAS,QAAQ,IAAI,GAAG;AAC9B,QAAI,UAAU,MAAM;AAClB,cAAQ,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,GAAG,CAAC;AAC3C;AAAA,IACF;AACA,WAAO,CAAC,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,QAAQ,GAAG;AAC3C,WAAO,CAAC,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,QAAQ,GAAG;AAAA,EAC7C;AACA,SAAO;AACT;AAEO,SAAS,kBAAkB,UAA2C;AAC3E,QAAM,QAAQ,cAAc,SAAS,UAAU;AAC/C,QAAM,QAAQ,cAAc,SAAS,UAAU;AAC/C,MAAI,cAAyC;AAC7C,MAAI,SAAS,eAAe,MAAM;AAChC,kBAAc;AAAA,MACZ,IAAI,SAAS;AAAA,MACb,MAAM,SAAS;AAAA,MACf,OAAO,SAAS;AAAA,MAChB,OAAO,SAAS;AAAA,IAAA;AAAA,EAEpB;AACA,SAAO;AAAA,IACL,IAAI,SAAS;AAAA,IACb,KAAK,SAAS;AAAA,IACd,UAAU,SAAS;AAAA,IACnB,QAAQ,SAAS,mBAAmB;AAAA,IACpC,MAAM,SAAS,iBAAiB;AAAA,IAChC;AAAA,IACA,YAAY,SAAS;AAAA,IACrB,YAAY,SAAS,OAAO,CAAA,IAAK,CAAC,KAAK;AAAA,IACvC,YAAY,SAAS,OAAO,CAAA,IAAK,CAAC,KAAK;AAAA,IACvC,YAAY,SAAS;AAAA,IACrB,QAAQ,SAAS;AAAA,IACjB;AAAA,EAAA;AAEJ;AASO,SAAS,uBACd,YACA,UAAyC,IACxB;AACjB,QAAM,EAAE,iBAAiB,KAAA,IAAS;AAClC,MAAI,cAAc,KAAM,QAAO,CAAA;AAC/B,MAAI,WAAW,WAAW,IAAI,iBAAiB;AAC/C,MAAI,gBAAgB;AAClB,eAAW,sBAAsB,QAAQ;AAAA,EAC3C;AACA,SAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAC9C;AAaO,SAAS,kBACd,OACA,UAAoC,IACnB;AACjB,QAAM,EAAE,YAAY,MAAO,WAAWA,iBAAAA,eAAe,QAAQ;AAC7D,MAAI,SAAS,KAAM,QAAO,CAAA;AAC1B,QAAM,aAAa,eAAe,QAAQ;AAE1C,QAAM,WAA4B,CAAA;AAElC,WAAS,MAAM,SAAuC;AACpD,QAAI,WAAY,QAAO,QAAQ,YAAY;AAC3C,WAAO,QAAQ;AAAA,EACjB;AAEA,WAAS,SAAS,MAAS,KAAc;AACvC,UAAM,MAAM,MAAM,KAAK,QAAQ,KAAK;AACpC,UAAM,WAAW,SAAS,MAAM,KAAK,QAAQ,KAAK,KAAK;AACvD,UAAM,MAAM,aAAa,WAAW;AACpC,QAAI,OAAO,KAAM;AAEjB,QAAI,UAAU,SAAS,KAAK,CAAC,MAAM;AACjC,YAAM,IAAI,MAAM,CAAC;AACjB,aAAO,KAAK,QAAQ,KAAK,IAAI,IAAI,GAAG,IAAI;AAAA,IAC1C,CAAC;AACD,QAAI,WAAW,MAAM;AACnB,gBAAU;AAAA,QACR,IAAI;AAAA,QACJ;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,YAAY,CAAA;AAAA,QACZ,YAAY,CAAA;AAAA,QACZ,YAAY,KAAK;AAAA,MAAA;AAEnB,eAAS,KAAK,OAAO;AAAA,IACvB;AAEA,QAAI,KAAK;AACP,cAAQ,WAAW,KAAK,KAAK,OAAO;AAAA,IACtC,OAAO;AACL,cAAQ,WAAW,KAAK,KAAK,OAAO;AAAA,IACtC;AAAA,EACF;AAEA,aAAW,QAAQ,OAAO;AACxB,aAAS,MAAM,IAAI;AACnB,aAAS,MAAM,KAAK;AAAA,EACtB;AAEA,WAAS,KAAK,CAAC,GAAG,MAAM,iBAAiB,GAAG,GAAG,QAAQ,CAAC;AACxD,SAAO,SAAS,IAAI,CAAC,GAAG,OAAO,EAAE,GAAG,GAAG,IAAI,WAAW,CAAC,GAAA,EAAK;AAC9D;AAKO,SAAS,gBACd,SACA,UACe;AACf,MAAI,eAAe,QAAQ,GAAG;AAC5B,WAAO,QAAQ,YAAY;AAAA,EAC7B;AACA,SAAO,QAAQ,OAAO;AACxB;AAEO,SAAS,eACd,UACS;AACT,SACE,aAAaA,iBAAAA,eAAe,UAAU,aAAaA,iBAAAA,eAAe;AAEtE;AAGO,SAAS,iBACd,GACA,GACA,UACQ;AACR,QAAM,KAAK,gBAAgB,GAAG,QAAQ;AACtC,QAAM,KAAK,gBAAgB,GAAG,QAAQ;AACtC,MAAI,MAAM,QAAQ,MAAM,KAAM,QAAO;AACrC,MAAI,aAAaA,iBAAAA,eAAe,QAAQ;AAEtC,WAAO,KAAK;AAAA,EACd;AAEA,SAAO,KAAK;AACd;AAGO,SAAS,iBACd,YACe;AACf,MAAI,cAAc,KAAM,QAAO;AAC/B,SAAO,GAAG,KAAK,MAAM,aAAa,GAAG,CAAC;AACxC;AAIO,SAAS,aAAa,SAAgC;AAC3D,QAAM,EAAE,gBAAgB;AACxB,MAAI,eAAe,MAAM;AACvB,UAAM,OAAO,iBAAiB,QAAQ,UAAU;AAChD,QAAI,QAAQ,MAAM;AAChB,aAAO,GAAG,YAAY,IAAI,MAAM,IAAI;AAAA,IACtC;AACA,WAAO,YAAY;AAAA,EACrB;AACA,SAAO,UAAU,QAAQ,GAAG;AAC9B;AAEO,SAAS,UAAU,KAAwC;AAChE,MAAI,OAAO,KAAM,QAAO;AACxB,MAAI,QAAQ;AACZ,MAAI,OAAO;AACX,MAAI,KAAK,IAAI,GAAG,IAAI,GAAG;AACrB,YAAQ,MAAM;AACd,WAAO;AAAA,EACT;AACA,QAAM,SAAS,MAAM,eAAe,SAAS,EAAE,uBAAuB,GAAG;AACzE,SAAO,GAAG,MAAM,IAAI,IAAI;AAC1B;AAEA,SAAS,SAAS,OAA+B;AAC/C,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO;AAC1C,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,OAAO,MAAM,CAAC,EAAG,QAAO;AAC5B,SAAO;AACT;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AgeModelBoundary, BoundaryStatus, BoundaryType, UnitLong } from '@macrostrat/api-types';
|
|
2
|
+
import { ColumnAxisType } from '@macrostrat/column-components';
|
|
3
|
+
export type { AgeModelBoundary, BoundaryStatus, BoundaryType };
|
|
4
|
+
/** A boundary status, plus `derived` for surfaces computed from units rather
|
|
5
|
+
* than stored in the age model. */
|
|
6
|
+
export type SurfaceStatus = BoundaryStatus | "derived";
|
|
7
|
+
/** The interval a surface is calibrated against */
|
|
8
|
+
export interface SurfaceCalibration {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
b_age: number;
|
|
12
|
+
t_age: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ColumnSurface {
|
|
15
|
+
id: number | string;
|
|
16
|
+
/** Modeled age of the surface (Ma) */
|
|
17
|
+
age: number;
|
|
18
|
+
/** Measured position (height or depth) along the column, when known */
|
|
19
|
+
position?: number | null;
|
|
20
|
+
status: SurfaceStatus;
|
|
21
|
+
type: BoundaryType;
|
|
22
|
+
/** The calibration interval, and the surface's position within it
|
|
23
|
+
* (0 at the interval's base, 1 at its top) */
|
|
24
|
+
calibration?: SurfaceCalibration | null;
|
|
25
|
+
proportion?: number | null;
|
|
26
|
+
/** True when `status` was inferred rather than read from the data — see
|
|
27
|
+
* `inferTiePointStatuses`. */
|
|
28
|
+
statusInferred?: boolean;
|
|
29
|
+
unitsAbove: number[];
|
|
30
|
+
unitsBelow: number[];
|
|
31
|
+
section_id?: number | null;
|
|
32
|
+
ref_id?: number | null;
|
|
33
|
+
/** The API record, when the surface came from `/age_model` */
|
|
34
|
+
boundary?: AgeModelBoundary;
|
|
35
|
+
}
|
|
36
|
+
export declare const surfaceStatuses: SurfaceStatus[];
|
|
37
|
+
/** The statuses at which the age model was actually constrained — the tie
|
|
38
|
+
* points — as opposed to surfaces whose age falls out of the model
|
|
39
|
+
* (`modeled`), carries no status, or is merely derived from units. These are
|
|
40
|
+
* the surfaces worth labeling by default. */
|
|
41
|
+
export declare const TIE_POINT_STATUSES: SurfaceStatus[];
|
|
42
|
+
export declare const surfaceStatusLabels: Record<SurfaceStatus, string>;
|
|
43
|
+
export declare const surfaceStatusDescriptions: Record<SurfaceStatus, string>;
|
|
44
|
+
/** A CSS-safe token for a status or type (`angular unconformity` →
|
|
45
|
+
* `angular-unconformity`, `""` → `unspecified`). */
|
|
46
|
+
export declare function surfaceToken(value: string | null | undefined): string;
|
|
47
|
+
/** Legacy `unit_boundaries` rows use `0` as the "no unit" sentinel. */
|
|
48
|
+
export declare function nullifyUnitID(id: number | null | undefined): number | null;
|
|
49
|
+
/** How close to an interval's base or top a surface must sit to count as
|
|
50
|
+
* bounding it (as a fraction of the interval). */
|
|
51
|
+
export declare const INTERVAL_BOUND_TOLERANCE = 0.001;
|
|
52
|
+
/** Promote the `modeled` surfaces that cannot in fact have been interpolated
|
|
53
|
+
* to `relative`.
|
|
54
|
+
*
|
|
55
|
+
* This is a client-side affordance over a known gap in the data. An age model
|
|
56
|
+
* interpolates *between* tie points, so a surface with no constraint to
|
|
57
|
+
* interpolate from has to be one itself, whatever `boundary_status` says. Two
|
|
58
|
+
* cases are recognizable from the boundaries alone:
|
|
59
|
+
*
|
|
60
|
+
* - **It sits on the base or top of its calibration interval** (proportion 0
|
|
61
|
+
* or 1). A height referred directly to an interval bounds the model rather
|
|
62
|
+
* than falling out of it.
|
|
63
|
+
* - **It is the edge of a gap-bound package** — the youngest surface in its
|
|
64
|
+
* section with no unit above, or the oldest with none below. There is
|
|
65
|
+
* nothing beyond it to interpolate against.
|
|
66
|
+
*
|
|
67
|
+
* Both are common in columns whose age model was built by assigning positions
|
|
68
|
+
* within intervals, where the importer recorded everything as `modeled`;
|
|
69
|
+
* filtering to the tie-point statuses would otherwise hide real ones. Promoted
|
|
70
|
+
* surfaces are marked `statusInferred`, so views can say where the status came
|
|
71
|
+
* from; nothing about the underlying record changes.
|
|
72
|
+
*
|
|
73
|
+
* Two cases are deliberately left alone. A *mid-interval* position is
|
|
74
|
+
* ambiguous: an assigned position and an interpolated one are the same record
|
|
75
|
+
* there, and only the data can settle it. And an open side in the *interior*
|
|
76
|
+
* of a section is a lateral relationship, not a package edge — a unit that
|
|
77
|
+
* pinches out while others span past it — which the model can interpolate
|
|
78
|
+
* normally.
|
|
79
|
+
*/
|
|
80
|
+
export declare function inferTiePointStatuses(surfaces: ColumnSurface[], tolerance?: number): ColumnSurface[];
|
|
81
|
+
export declare function boundaryToSurface(boundary: AgeModelBoundary): ColumnSurface;
|
|
82
|
+
export interface SurfacesFromBoundariesOptions {
|
|
83
|
+
/** Promote `modeled` surfaces that must in fact constrain the age model to
|
|
84
|
+
* `relative` (default `true`) — see `inferTiePointStatuses`. */
|
|
85
|
+
inferTiePoints?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/** Surfaces for a column's age-model boundaries, youngest first. */
|
|
88
|
+
export declare function surfacesFromBoundaries(boundaries: AgeModelBoundary[] | null | undefined, options?: SurfacesFromBoundariesOptions): ColumnSurface[];
|
|
89
|
+
export interface SurfacesFromUnitsOptions {
|
|
90
|
+
/** Surfaces closer than this (in the axis's units) are merged */
|
|
91
|
+
tolerance?: number;
|
|
92
|
+
/** Which unit field the surfaces are keyed on: ages for age columns,
|
|
93
|
+
* measured positions for height and depth columns */
|
|
94
|
+
axisType?: ColumnAxisType;
|
|
95
|
+
}
|
|
96
|
+
/** Derive surfaces from unit tops and bottoms. Every distinct top or bottom
|
|
97
|
+
* becomes one surface with status `derived`; units sharing a boundary share
|
|
98
|
+
* the surface. This is the fallback when a column has no `unit_boundaries`. */
|
|
99
|
+
export declare function surfacesFromUnits<T extends UnitLong>(units: T[] | null | undefined, options?: SurfacesFromUnitsOptions): ColumnSurface[];
|
|
100
|
+
/** The coordinate a surface is drawn at for a given axis type: its age for
|
|
101
|
+
* age columns, its measured position for height and depth columns. `null`
|
|
102
|
+
* when the surface has no coordinate on that axis. */
|
|
103
|
+
export declare function surfacePosition(surface: ColumnSurface, axisType: ColumnAxisType | null | undefined): number | null;
|
|
104
|
+
export declare function isPositionAxis(axisType: ColumnAxisType | null | undefined): boolean;
|
|
105
|
+
/** Order surfaces from the top of the column downwards. */
|
|
106
|
+
export declare function compareAlongAxis(a: ColumnSurface, b: ColumnSurface, axisType: ColumnAxisType | null | undefined): number;
|
|
107
|
+
/** Percent of the way up its calibration interval, e.g. `85%`. */
|
|
108
|
+
export declare function formatProportion(proportion: number | null | undefined): string | null;
|
|
109
|
+
/** A short text label for a surface: its calibration ("Calymmian · 85%")
|
|
110
|
+
* or, failing that, its age. */
|
|
111
|
+
export declare function surfaceLabel(surface: ColumnSurface): string;
|
|
112
|
+
export declare function formatAge(age: number | null | undefined): string;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { ColumnAxisType } from "@macrostrat/column-components";
|
|
2
|
+
const surfaceStatuses = [
|
|
3
|
+
"absolute",
|
|
4
|
+
"relative",
|
|
5
|
+
"spike",
|
|
6
|
+
"imposed",
|
|
7
|
+
"modeled",
|
|
8
|
+
"",
|
|
9
|
+
"derived"
|
|
10
|
+
];
|
|
11
|
+
const TIE_POINT_STATUSES = [
|
|
12
|
+
"absolute",
|
|
13
|
+
"relative",
|
|
14
|
+
"spike",
|
|
15
|
+
"imposed"
|
|
16
|
+
];
|
|
17
|
+
const surfaceStatusLabels = {
|
|
18
|
+
absolute: "Absolute",
|
|
19
|
+
relative: "Relative",
|
|
20
|
+
spike: "Spike",
|
|
21
|
+
imposed: "Imposed",
|
|
22
|
+
modeled: "Modeled",
|
|
23
|
+
"": "Unspecified",
|
|
24
|
+
derived: "Derived"
|
|
25
|
+
};
|
|
26
|
+
const surfaceStatusDescriptions = {
|
|
27
|
+
absolute: "Independently dated; the age is authoritative and the interval position is derived from it",
|
|
28
|
+
relative: "Calibrated to a position within a geologic interval; the age follows the timescale",
|
|
29
|
+
spike: "Tied to a formally defined boundary point",
|
|
30
|
+
imposed: "Interval-relative representation imposed on a boundary with no chronostratigraphic tie of its own (for instance, thickness interpolation)",
|
|
31
|
+
modeled: "Interpolated by the age model between calibrated surfaces",
|
|
32
|
+
"": "No status recorded",
|
|
33
|
+
derived: "Computed from unit tops and bottoms; not stored in the column's age model"
|
|
34
|
+
};
|
|
35
|
+
function surfaceToken(value) {
|
|
36
|
+
if (value == null || value === "") return "unspecified";
|
|
37
|
+
return value.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
function nullifyUnitID(id) {
|
|
40
|
+
if (id == null || id === 0) return null;
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
const INTERVAL_BOUND_TOLERANCE = 1e-3;
|
|
44
|
+
function inferTiePointStatuses(surfaces, tolerance = INTERVAL_BOUND_TOLERANCE) {
|
|
45
|
+
const extents = sectionAgeExtents(surfaces);
|
|
46
|
+
return surfaces.map((surface) => {
|
|
47
|
+
if (surface.status !== "modeled") return surface;
|
|
48
|
+
if (surface.calibration == null) return surface;
|
|
49
|
+
const { proportion } = surface;
|
|
50
|
+
const onIntervalBound = proportion != null && (Math.abs(proportion) <= tolerance || Math.abs(proportion - 1) <= tolerance);
|
|
51
|
+
if (!onIntervalBound && !boundsPackage(surface, extents)) {
|
|
52
|
+
return surface;
|
|
53
|
+
}
|
|
54
|
+
return { ...surface, status: "relative", statusInferred: true };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function boundsPackage(surface, extents) {
|
|
58
|
+
const extent = extents.get(surface.section_id ?? null);
|
|
59
|
+
if (extent == null) return false;
|
|
60
|
+
const [youngest, oldest] = extent;
|
|
61
|
+
if (surface.unitsAbove.length === 0 && surface.age === youngest) return true;
|
|
62
|
+
return surface.unitsBelow.length === 0 && surface.age === oldest;
|
|
63
|
+
}
|
|
64
|
+
function sectionAgeExtents(surfaces) {
|
|
65
|
+
const extents = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const surface of surfaces) {
|
|
67
|
+
const key = surface.section_id ?? null;
|
|
68
|
+
const extent = extents.get(key);
|
|
69
|
+
if (extent == null) {
|
|
70
|
+
extents.set(key, [surface.age, surface.age]);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
extent[0] = Math.min(extent[0], surface.age);
|
|
74
|
+
extent[1] = Math.max(extent[1], surface.age);
|
|
75
|
+
}
|
|
76
|
+
return extents;
|
|
77
|
+
}
|
|
78
|
+
function boundaryToSurface(boundary) {
|
|
79
|
+
const above = nullifyUnitID(boundary.unit_above);
|
|
80
|
+
const below = nullifyUnitID(boundary.unit_below);
|
|
81
|
+
let calibration = null;
|
|
82
|
+
if (boundary.interval_id != null) {
|
|
83
|
+
calibration = {
|
|
84
|
+
id: boundary.interval_id,
|
|
85
|
+
name: boundary.interval_name,
|
|
86
|
+
b_age: boundary.age_bottom,
|
|
87
|
+
t_age: boundary.age_top
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
id: boundary.boundary_id,
|
|
92
|
+
age: boundary.model_age,
|
|
93
|
+
position: boundary.boundary_position,
|
|
94
|
+
status: boundary.boundary_status ?? "",
|
|
95
|
+
type: boundary.boundary_type ?? "",
|
|
96
|
+
calibration,
|
|
97
|
+
proportion: boundary.rel_position,
|
|
98
|
+
unitsAbove: above == null ? [] : [above],
|
|
99
|
+
unitsBelow: below == null ? [] : [below],
|
|
100
|
+
section_id: boundary.section_id,
|
|
101
|
+
ref_id: boundary.ref_id,
|
|
102
|
+
boundary
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function surfacesFromBoundaries(boundaries, options = {}) {
|
|
106
|
+
const { inferTiePoints = true } = options;
|
|
107
|
+
if (boundaries == null) return [];
|
|
108
|
+
let surfaces = boundaries.map(boundaryToSurface);
|
|
109
|
+
if (inferTiePoints) {
|
|
110
|
+
surfaces = inferTiePointStatuses(surfaces);
|
|
111
|
+
}
|
|
112
|
+
return surfaces.sort((a, b) => a.age - b.age);
|
|
113
|
+
}
|
|
114
|
+
function surfacesFromUnits(units, options = {}) {
|
|
115
|
+
const { tolerance = 1e-3, axisType = ColumnAxisType.AGE } = options;
|
|
116
|
+
if (units == null) return [];
|
|
117
|
+
const byPosition = isPositionAxis(axisType);
|
|
118
|
+
const surfaces = [];
|
|
119
|
+
function keyOf(surface) {
|
|
120
|
+
if (byPosition) return surface.position ?? null;
|
|
121
|
+
return surface.age;
|
|
122
|
+
}
|
|
123
|
+
function addBound(unit, top) {
|
|
124
|
+
const age = top ? unit.t_age : unit.b_age;
|
|
125
|
+
const position = asNumber(top ? unit.t_pos : unit.b_pos);
|
|
126
|
+
const key = byPosition ? position : age;
|
|
127
|
+
if (key == null) return;
|
|
128
|
+
let surface = surfaces.find((d) => {
|
|
129
|
+
const k = keyOf(d);
|
|
130
|
+
return k != null && Math.abs(k - key) < tolerance;
|
|
131
|
+
});
|
|
132
|
+
if (surface == null) {
|
|
133
|
+
surface = {
|
|
134
|
+
id: "",
|
|
135
|
+
age,
|
|
136
|
+
position,
|
|
137
|
+
status: "derived",
|
|
138
|
+
type: "",
|
|
139
|
+
calibration: null,
|
|
140
|
+
proportion: null,
|
|
141
|
+
unitsAbove: [],
|
|
142
|
+
unitsBelow: [],
|
|
143
|
+
section_id: unit.section_id
|
|
144
|
+
};
|
|
145
|
+
surfaces.push(surface);
|
|
146
|
+
}
|
|
147
|
+
if (top) {
|
|
148
|
+
surface.unitsBelow.push(unit.unit_id);
|
|
149
|
+
} else {
|
|
150
|
+
surface.unitsAbove.push(unit.unit_id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const unit of units) {
|
|
154
|
+
addBound(unit, true);
|
|
155
|
+
addBound(unit, false);
|
|
156
|
+
}
|
|
157
|
+
surfaces.sort((a, b) => compareAlongAxis(a, b, axisType));
|
|
158
|
+
return surfaces.map((s, i) => ({ ...s, id: `derived-${i}` }));
|
|
159
|
+
}
|
|
160
|
+
function surfacePosition(surface, axisType) {
|
|
161
|
+
if (isPositionAxis(axisType)) {
|
|
162
|
+
return surface.position ?? null;
|
|
163
|
+
}
|
|
164
|
+
return surface.age ?? null;
|
|
165
|
+
}
|
|
166
|
+
function isPositionAxis(axisType) {
|
|
167
|
+
return axisType === ColumnAxisType.HEIGHT || axisType === ColumnAxisType.DEPTH;
|
|
168
|
+
}
|
|
169
|
+
function compareAlongAxis(a, b, axisType) {
|
|
170
|
+
const pa = surfacePosition(a, axisType);
|
|
171
|
+
const pb = surfacePosition(b, axisType);
|
|
172
|
+
if (pa == null || pb == null) return 0;
|
|
173
|
+
if (axisType === ColumnAxisType.HEIGHT) {
|
|
174
|
+
return pb - pa;
|
|
175
|
+
}
|
|
176
|
+
return pa - pb;
|
|
177
|
+
}
|
|
178
|
+
function formatProportion(proportion) {
|
|
179
|
+
if (proportion == null) return null;
|
|
180
|
+
return `${Math.round(proportion * 100)}%`;
|
|
181
|
+
}
|
|
182
|
+
function surfaceLabel(surface) {
|
|
183
|
+
const { calibration } = surface;
|
|
184
|
+
if (calibration != null) {
|
|
185
|
+
const prop = formatProportion(surface.proportion);
|
|
186
|
+
if (prop != null) {
|
|
187
|
+
return `${calibration.name} · ${prop}`;
|
|
188
|
+
}
|
|
189
|
+
return calibration.name;
|
|
190
|
+
}
|
|
191
|
+
return formatAge(surface.age);
|
|
192
|
+
}
|
|
193
|
+
function formatAge(age) {
|
|
194
|
+
if (age == null) return "";
|
|
195
|
+
let value = age;
|
|
196
|
+
let unit = "Ma";
|
|
197
|
+
if (Math.abs(age) < 1) {
|
|
198
|
+
value = age * 1e3;
|
|
199
|
+
unit = "ka";
|
|
200
|
+
}
|
|
201
|
+
const _value = value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
202
|
+
return `${_value} ${unit}`;
|
|
203
|
+
}
|
|
204
|
+
function asNumber(value) {
|
|
205
|
+
if (value == null || value === "") return null;
|
|
206
|
+
const n = Number(value);
|
|
207
|
+
if (Number.isNaN(n)) return null;
|
|
208
|
+
return n;
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
INTERVAL_BOUND_TOLERANCE,
|
|
212
|
+
TIE_POINT_STATUSES,
|
|
213
|
+
boundaryToSurface,
|
|
214
|
+
compareAlongAxis,
|
|
215
|
+
formatAge,
|
|
216
|
+
formatProportion,
|
|
217
|
+
inferTiePointStatuses,
|
|
218
|
+
isPositionAxis,
|
|
219
|
+
nullifyUnitID,
|
|
220
|
+
surfaceLabel,
|
|
221
|
+
surfacePosition,
|
|
222
|
+
surfaceStatusDescriptions,
|
|
223
|
+
surfaceStatusLabels,
|
|
224
|
+
surfaceStatuses,
|
|
225
|
+
surfaceToken,
|
|
226
|
+
surfacesFromBoundaries,
|
|
227
|
+
surfacesFromUnits
|
|
228
|
+
};
|
|
229
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../src/age-model/types.ts"],"sourcesContent":["/** Surfaces: the view model for a column's age model.\n *\n * A `ColumnSurface` is one horizontal boundary in a column. It is what the\n * surfaces view renders, and it is deliberately data-agnostic: a surface can\n * come from an `/age_model` boundary (`boundaryToSurface`), be derived from\n * unit tops and bottoms when a column has no age-model rows\n * (`surfacesFromUnits`), or be built by an editor from its own state.\n */\nimport type {\n AgeModelBoundary,\n BoundaryStatus,\n BoundaryType,\n UnitLong,\n} from \"@macrostrat/api-types\";\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\n\nexport type { AgeModelBoundary, BoundaryStatus, BoundaryType };\n\n/** A boundary status, plus `derived` for surfaces computed from units rather\n * than stored in the age model. */\nexport type SurfaceStatus = BoundaryStatus | \"derived\";\n\n/** The interval a surface is calibrated against */\nexport interface SurfaceCalibration {\n id: number;\n name: string;\n b_age: number;\n t_age: number;\n}\n\nexport interface ColumnSurface {\n id: number | string;\n /** Modeled age of the surface (Ma) */\n age: number;\n /** Measured position (height or depth) along the column, when known */\n position?: number | null;\n status: SurfaceStatus;\n type: BoundaryType;\n /** The calibration interval, and the surface's position within it\n * (0 at the interval's base, 1 at its top) */\n calibration?: SurfaceCalibration | null;\n proportion?: number | null;\n /** True when `status` was inferred rather than read from the data — see\n * `inferTiePointStatuses`. */\n statusInferred?: boolean;\n unitsAbove: number[];\n unitsBelow: number[];\n section_id?: number | null;\n ref_id?: number | null;\n /** The API record, when the surface came from `/age_model` */\n boundary?: AgeModelBoundary;\n}\n\nexport const surfaceStatuses: SurfaceStatus[] = [\n \"absolute\",\n \"relative\",\n \"spike\",\n \"imposed\",\n \"modeled\",\n \"\",\n \"derived\",\n];\n\n/** The statuses at which the age model was actually constrained — the tie\n * points — as opposed to surfaces whose age falls out of the model\n * (`modeled`), carries no status, or is merely derived from units. These are\n * the surfaces worth labeling by default. */\nexport const TIE_POINT_STATUSES: SurfaceStatus[] = [\n \"absolute\",\n \"relative\",\n \"spike\",\n \"imposed\",\n];\n\nexport const surfaceStatusLabels: Record<SurfaceStatus, string> = {\n absolute: \"Absolute\",\n relative: \"Relative\",\n spike: \"Spike\",\n imposed: \"Imposed\",\n modeled: \"Modeled\",\n \"\": \"Unspecified\",\n derived: \"Derived\",\n};\n\nexport const surfaceStatusDescriptions: Record<SurfaceStatus, string> = {\n absolute:\n \"Independently dated; the age is authoritative and the interval position is derived from it\",\n relative:\n \"Calibrated to a position within a geologic interval; the age follows the timescale\",\n spike: \"Tied to a formally defined boundary point\",\n imposed:\n \"Interval-relative representation imposed on a boundary with no chronostratigraphic tie of its own (for instance, thickness interpolation)\",\n modeled: \"Interpolated by the age model between calibrated surfaces\",\n \"\": \"No status recorded\",\n derived:\n \"Computed from unit tops and bottoms; not stored in the column's age model\",\n};\n\n/** A CSS-safe token for a status or type (`angular unconformity` →\n * `angular-unconformity`, `\"\"` → `unspecified`). */\nexport function surfaceToken(value: string | null | undefined): string {\n if (value == null || value === \"\") return \"unspecified\";\n return value.replace(/[^a-z0-9]+/gi, \"-\").toLowerCase();\n}\n\n/** Legacy `unit_boundaries` rows use `0` as the \"no unit\" sentinel. */\nexport function nullifyUnitID(id: number | null | undefined): number | null {\n if (id == null || id === 0) return null;\n return id;\n}\n\n/** How close to an interval's base or top a surface must sit to count as\n * bounding it (as a fraction of the interval). */\nexport const INTERVAL_BOUND_TOLERANCE = 0.001;\n\n/** Promote the `modeled` surfaces that cannot in fact have been interpolated\n * to `relative`.\n *\n * This is a client-side affordance over a known gap in the data. An age model\n * interpolates *between* tie points, so a surface with no constraint to\n * interpolate from has to be one itself, whatever `boundary_status` says. Two\n * cases are recognizable from the boundaries alone:\n *\n * - **It sits on the base or top of its calibration interval** (proportion 0\n * or 1). A height referred directly to an interval bounds the model rather\n * than falling out of it.\n * - **It is the edge of a gap-bound package** — the youngest surface in its\n * section with no unit above, or the oldest with none below. There is\n * nothing beyond it to interpolate against.\n *\n * Both are common in columns whose age model was built by assigning positions\n * within intervals, where the importer recorded everything as `modeled`;\n * filtering to the tie-point statuses would otherwise hide real ones. Promoted\n * surfaces are marked `statusInferred`, so views can say where the status came\n * from; nothing about the underlying record changes.\n *\n * Two cases are deliberately left alone. A *mid-interval* position is\n * ambiguous: an assigned position and an interpolated one are the same record\n * there, and only the data can settle it. And an open side in the *interior*\n * of a section is a lateral relationship, not a package edge — a unit that\n * pinches out while others span past it — which the model can interpolate\n * normally.\n */\nexport function inferTiePointStatuses(\n surfaces: ColumnSurface[],\n tolerance: number = INTERVAL_BOUND_TOLERANCE,\n): ColumnSurface[] {\n const extents = sectionAgeExtents(surfaces);\n\n return surfaces.map((surface) => {\n if (surface.status !== \"modeled\") return surface;\n if (surface.calibration == null) return surface;\n\n const { proportion } = surface;\n const onIntervalBound =\n proportion != null &&\n (Math.abs(proportion) <= tolerance ||\n Math.abs(proportion - 1) <= tolerance);\n\n if (!onIntervalBound && !boundsPackage(surface, extents)) {\n return surface;\n }\n return { ...surface, status: \"relative\", statusInferred: true };\n });\n}\n\n/** An open side only means a package edge at the ends of the section. In its\n * interior it means a unit pinching out while others span past it, which the\n * age model interpolates like any other surface. */\nfunction boundsPackage(\n surface: ColumnSurface,\n extents: Map<ColumnSurface[\"section_id\"], [number, number]>,\n): boolean {\n const extent = extents.get(surface.section_id ?? null);\n if (extent == null) return false;\n const [youngest, oldest] = extent;\n if (surface.unitsAbove.length === 0 && surface.age === youngest) return true;\n return surface.unitsBelow.length === 0 && surface.age === oldest;\n}\n\n/** The youngest and oldest surface age in each section. */\nfunction sectionAgeExtents(\n surfaces: ColumnSurface[],\n): Map<ColumnSurface[\"section_id\"], [number, number]> {\n const extents = new Map<ColumnSurface[\"section_id\"], [number, number]>();\n for (const surface of surfaces) {\n const key = surface.section_id ?? null;\n const extent = extents.get(key);\n if (extent == null) {\n extents.set(key, [surface.age, surface.age]);\n continue;\n }\n extent[0] = Math.min(extent[0], surface.age);\n extent[1] = Math.max(extent[1], surface.age);\n }\n return extents;\n}\n\nexport function boundaryToSurface(boundary: AgeModelBoundary): ColumnSurface {\n const above = nullifyUnitID(boundary.unit_above);\n const below = nullifyUnitID(boundary.unit_below);\n let calibration: SurfaceCalibration | null = null;\n if (boundary.interval_id != null) {\n calibration = {\n id: boundary.interval_id,\n name: boundary.interval_name,\n b_age: boundary.age_bottom,\n t_age: boundary.age_top,\n };\n }\n return {\n id: boundary.boundary_id,\n age: boundary.model_age,\n position: boundary.boundary_position,\n status: boundary.boundary_status ?? \"\",\n type: boundary.boundary_type ?? \"\",\n calibration,\n proportion: boundary.rel_position,\n unitsAbove: above == null ? [] : [above],\n unitsBelow: below == null ? [] : [below],\n section_id: boundary.section_id,\n ref_id: boundary.ref_id,\n boundary,\n };\n}\n\nexport interface SurfacesFromBoundariesOptions {\n /** Promote `modeled` surfaces that must in fact constrain the age model to\n * `relative` (default `true`) — see `inferTiePointStatuses`. */\n inferTiePoints?: boolean;\n}\n\n/** Surfaces for a column's age-model boundaries, youngest first. */\nexport function surfacesFromBoundaries(\n boundaries: AgeModelBoundary[] | null | undefined,\n options: SurfacesFromBoundariesOptions = {},\n): ColumnSurface[] {\n const { inferTiePoints = true } = options;\n if (boundaries == null) return [];\n let surfaces = boundaries.map(boundaryToSurface);\n if (inferTiePoints) {\n surfaces = inferTiePointStatuses(surfaces);\n }\n return surfaces.sort((a, b) => a.age - b.age);\n}\n\nexport interface SurfacesFromUnitsOptions {\n /** Surfaces closer than this (in the axis's units) are merged */\n tolerance?: number;\n /** Which unit field the surfaces are keyed on: ages for age columns,\n * measured positions for height and depth columns */\n axisType?: ColumnAxisType;\n}\n\n/** Derive surfaces from unit tops and bottoms. Every distinct top or bottom\n * becomes one surface with status `derived`; units sharing a boundary share\n * the surface. This is the fallback when a column has no `unit_boundaries`. */\nexport function surfacesFromUnits<T extends UnitLong>(\n units: T[] | null | undefined,\n options: SurfacesFromUnitsOptions = {},\n): ColumnSurface[] {\n const { tolerance = 0.001, axisType = ColumnAxisType.AGE } = options;\n if (units == null) return [];\n const byPosition = isPositionAxis(axisType);\n\n const surfaces: ColumnSurface[] = [];\n\n function keyOf(surface: ColumnSurface): number | null {\n if (byPosition) return surface.position ?? null;\n return surface.age;\n }\n\n function addBound(unit: T, top: boolean) {\n const age = top ? unit.t_age : unit.b_age;\n const position = asNumber(top ? unit.t_pos : unit.b_pos);\n const key = byPosition ? position : age;\n if (key == null) return;\n\n let surface = surfaces.find((d) => {\n const k = keyOf(d);\n return k != null && Math.abs(k - key) < tolerance;\n });\n if (surface == null) {\n surface = {\n id: \"\",\n age,\n position,\n status: \"derived\",\n type: \"\",\n calibration: null,\n proportion: null,\n unitsAbove: [],\n unitsBelow: [],\n section_id: unit.section_id,\n };\n surfaces.push(surface);\n }\n // The unit whose top this is lies *below* the surface, and vice versa\n if (top) {\n surface.unitsBelow.push(unit.unit_id);\n } else {\n surface.unitsAbove.push(unit.unit_id);\n }\n }\n\n for (const unit of units) {\n addBound(unit, true);\n addBound(unit, false);\n }\n\n surfaces.sort((a, b) => compareAlongAxis(a, b, axisType));\n return surfaces.map((s, i) => ({ ...s, id: `derived-${i}` }));\n}\n\n/** The coordinate a surface is drawn at for a given axis type: its age for\n * age columns, its measured position for height and depth columns. `null`\n * when the surface has no coordinate on that axis. */\nexport function surfacePosition(\n surface: ColumnSurface,\n axisType: ColumnAxisType | null | undefined,\n): number | null {\n if (isPositionAxis(axisType)) {\n return surface.position ?? null;\n }\n return surface.age ?? null;\n}\n\nexport function isPositionAxis(\n axisType: ColumnAxisType | null | undefined,\n): boolean {\n return (\n axisType === ColumnAxisType.HEIGHT || axisType === ColumnAxisType.DEPTH\n );\n}\n\n/** Order surfaces from the top of the column downwards. */\nexport function compareAlongAxis(\n a: ColumnSurface,\n b: ColumnSurface,\n axisType: ColumnAxisType | null | undefined,\n): number {\n const pa = surfacePosition(a, axisType);\n const pb = surfacePosition(b, axisType);\n if (pa == null || pb == null) return 0;\n if (axisType === ColumnAxisType.HEIGHT) {\n // Heights increase upwards\n return pb - pa;\n }\n // Ages and depths increase downwards\n return pa - pb;\n}\n\n/** Percent of the way up its calibration interval, e.g. `85%`. */\nexport function formatProportion(\n proportion: number | null | undefined,\n): string | null {\n if (proportion == null) return null;\n return `${Math.round(proportion * 100)}%`;\n}\n\n/** A short text label for a surface: its calibration (\"Calymmian · 85%\")\n * or, failing that, its age. */\nexport function surfaceLabel(surface: ColumnSurface): string {\n const { calibration } = surface;\n if (calibration != null) {\n const prop = formatProportion(surface.proportion);\n if (prop != null) {\n return `${calibration.name} · ${prop}`;\n }\n return calibration.name;\n }\n return formatAge(surface.age);\n}\n\nexport function formatAge(age: number | null | undefined): string {\n if (age == null) return \"\";\n let value = age;\n let unit = \"Ma\";\n if (Math.abs(age) < 1) {\n value = age * 1000;\n unit = \"ka\";\n }\n const _value = value.toLocaleString(\"en-US\", { maximumFractionDigits: 2 });\n return `${_value} ${unit}`;\n}\n\nfunction asNumber(value: unknown): number | null {\n if (value == null || value === \"\") return null;\n const n = Number(value);\n if (Number.isNaN(n)) return null;\n return n;\n}\n"],"names":[],"mappings":"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"}
|