@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.10
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/LICENSE +21 -0
- package/README.md +713 -2
- package/dist/core/basemap.svelte.d.ts +38 -0
- package/dist/core/basemap.svelte.js +163 -0
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.js +13 -0
- package/dist/core/create-map.svelte.d.ts +274 -0
- package/dist/core/create-map.svelte.js +591 -0
- package/dist/core/gl.d.ts +97 -0
- package/dist/core/gl.js +1 -0
- package/dist/core/interactions.svelte.d.ts +50 -0
- package/dist/core/interactions.svelte.js +330 -0
- package/dist/core/plugin-host.svelte.d.ts +71 -0
- package/dist/core/plugin-host.svelte.js +445 -0
- package/dist/core/plugin.d.ts +190 -0
- package/dist/core/plugin.js +1 -0
- package/dist/core/scoped-context.d.ts +7 -0
- package/dist/core/scoped-context.js +34 -0
- package/dist/core/shape-view.d.ts +27 -0
- package/dist/core/shape-view.js +323 -0
- package/dist/data/format.d.ts +10 -0
- package/dist/data/format.js +37 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +9 -0
- package/dist/data/messages/en.json +61 -0
- package/dist/data/messages.d.ts +10 -0
- package/dist/data/messages.js +17 -0
- package/dist/data/metadata.d.ts +22 -0
- package/dist/data/metadata.js +110 -0
- package/dist/data/refs.d.ts +18 -0
- package/dist/data/refs.js +61 -0
- package/dist/data/scale-types.d.ts +7 -0
- package/dist/data/scale-types.js +1 -0
- package/dist/data/scale.d.ts +13 -0
- package/dist/data/scale.js +27 -0
- package/dist/data/selection.d.ts +13 -0
- package/dist/data/selection.js +52 -0
- package/dist/data/static-source.d.ts +4 -0
- package/dist/data/static-source.js +3 -0
- package/dist/data/types.d.ts +74 -0
- package/dist/data/types.js +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +42 -0
- package/dist/plugins/icons.d.ts +21 -0
- package/dist/plugins/icons.js +73 -0
- package/dist/plugins/points-clusters.d.ts +60 -0
- package/dist/plugins/points-clusters.js +93 -0
- package/dist/plugins/points.svelte.d.ts +105 -0
- package/dist/plugins/points.svelte.js +502 -0
- package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
- package/dist/plugins/scale-resolution.svelte.js +111 -0
- package/dist/plugins/shapes.svelte.d.ts +34 -0
- package/dist/plugins/shapes.svelte.js +325 -0
- package/dist/plugins/shared.d.ts +6 -0
- package/dist/plugins/shared.js +18 -0
- package/dist/plugins/style.d.ts +57 -0
- package/dist/plugins/style.js +193 -0
- package/dist/presets/CollocationTooltip.svelte +132 -0
- package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
- package/dist/presets/FireTooltip.svelte +70 -0
- package/dist/presets/FireTooltip.svelte.d.ts +13 -0
- package/dist/presets/MonitorClusterTooltip.svelte +168 -0
- package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorReadingBox.svelte +58 -0
- package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
- package/dist/presets/MonitorTooltip.svelte +88 -0
- package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
- package/dist/presets/calheatscore.d.ts +24 -0
- package/dist/presets/calheatscore.js +22 -0
- package/dist/presets/cluster-area.d.ts +13 -0
- package/dist/presets/cluster-area.js +34 -0
- package/dist/presets/collocation-sites.d.ts +18 -0
- package/dist/presets/collocation-sites.js +67 -0
- package/dist/presets/fire-groups.d.ts +26 -0
- package/dist/presets/fire-groups.js +51 -0
- package/dist/presets/forecast-zones.d.ts +25 -0
- package/dist/presets/forecast-zones.js +23 -0
- package/dist/presets/hms-fire.d.ts +30 -0
- package/dist/presets/hms-fire.js +86 -0
- package/dist/presets/hms-smoke.d.ts +20 -0
- package/dist/presets/hms-smoke.js +22 -0
- package/dist/presets/monitors.d.ts +48 -0
- package/dist/presets/monitors.js +243 -0
- package/dist/presets/reactive-props.svelte.d.ts +4 -0
- package/dist/presets/reactive-props.svelte.js +7 -0
- package/dist/presets/regions.d.ts +24 -0
- package/dist/presets/regions.js +22 -0
- package/dist/stores/abort.d.ts +16 -0
- package/dist/stores/abort.js +52 -0
- package/dist/stores/collocation-store.svelte.d.ts +25 -0
- package/dist/stores/collocation-store.svelte.js +72 -0
- package/dist/stores/hms-stores.svelte.d.ts +38 -0
- package/dist/stores/hms-stores.svelte.js +97 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/load-once.svelte.d.ts +25 -0
- package/dist/stores/load-once.svelte.js +93 -0
- package/dist/stores/monitors-store.svelte.d.ts +85 -0
- package/dist/stores/monitors-store.svelte.js +340 -0
- package/dist/theme.css +34 -0
- package/dist/ui/FeatureList.svelte +501 -0
- package/dist/ui/FeatureList.svelte.d.ts +14 -0
- package/dist/ui/Legend.svelte +214 -0
- package/dist/ui/Legend.svelte.d.ts +22 -0
- package/dist/ui/LegendBar.svelte +161 -0
- package/dist/ui/LegendBar.svelte.d.ts +11 -0
- package/dist/ui/MapControls.svelte +135 -0
- package/dist/ui/MapControls.svelte.d.ts +11 -0
- package/dist/ui/MapTooltip.svelte +397 -0
- package/dist/ui/MapTooltip.svelte.d.ts +37 -0
- package/dist/ui/PickNotice.svelte +56 -0
- package/dist/ui/PickNotice.svelte.d.ts +7 -0
- package/dist/ui/SelectionBar.svelte +123 -0
- package/dist/ui/SelectionBar.svelte.d.ts +10 -0
- package/dist/ui/TooltipItem.svelte +66 -0
- package/dist/ui/TooltipItem.svelte.d.ts +11 -0
- package/dist/ui/ViewSummary.svelte +97 -0
- package/dist/ui/ViewSummary.svelte.d.ts +7 -0
- package/dist/ui/card.d.ts +29 -0
- package/dist/ui/card.js +1 -0
- package/dist/ui/controls.css +45 -0
- package/dist/ui/data-credits.svelte.d.ts +9 -0
- package/dist/ui/data-credits.svelte.js +52 -0
- package/dist/ui/feature-list.d.ts +46 -0
- package/dist/ui/feature-list.js +115 -0
- package/dist/ui/legend-data.d.ts +41 -0
- package/dist/ui/legend-data.js +40 -0
- package/dist/ui/level-chip.d.ts +11 -0
- package/dist/ui/level-chip.js +15 -0
- package/dist/ui/value-format.d.ts +8 -0
- package/dist/ui/value-format.js +20 -0
- package/dist/ui/view-summary.d.ts +11 -0
- package/dist/ui/view-summary.js +33 -0
- package/dist/view/MapView.svelte +443 -0
- package/dist/view/MapView.svelte.d.ts +51 -0
- package/dist/view/create-gl-map.d.ts +34 -0
- package/dist/view/create-gl-map.js +186 -0
- package/dist/view/list-placement.d.ts +21 -0
- package/dist/view/list-placement.js +13 -0
- package/package.json +104 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Geometry } from "geojson";
|
|
2
|
+
import type { Bounds } from "./create-map.svelte.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shapes against the view: a geometry's bounding box, whether it overlaps the view, and a point of
|
|
5
|
+
* it inside the view. Coordinates are planar lng/lat (as `labelPoint`); no antimeridian handling.
|
|
6
|
+
*/
|
|
7
|
+
/** `[west, south, east, north]`. */
|
|
8
|
+
export type Bbox = [west: number, south: number, east: number, north: number];
|
|
9
|
+
/** The geometry's bounding box, or `null` when it has no coordinates. */
|
|
10
|
+
export declare function geometryBbox(geometry: Geometry): Bbox | null;
|
|
11
|
+
/** Inside an outer ring and outside its holes, for any polygon of the geometry. */
|
|
12
|
+
export declare function containsPoint(geometry: Geometry, p: [number, number]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the item is in view: a shape (`geometry`) when it intersects `bounds` (bbox pre-check,
|
|
15
|
+
* then the exact test; holes count), anything else when its `lngLat` is inside. Edges included.
|
|
16
|
+
*/
|
|
17
|
+
export declare function intersectsBounds(item: {
|
|
18
|
+
lngLat: [number, number];
|
|
19
|
+
geometry?: Geometry;
|
|
20
|
+
bbox?: Bbox;
|
|
21
|
+
}, bounds: Bounds): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* A point of the shape inside `view`: `center` when the shape contains it; otherwise a point well
|
|
24
|
+
* inside the shape's visible part (polygons: the largest clipped one, never in a hole; lines: the
|
|
25
|
+
* middle of the longest visible piece); `null` when nothing of it is visible.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shapeAnchor(geometry: Geometry, view: Bounds, center: [number, number]): [number, number] | null;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/** The geometry's bounding box, or `null` when it has no coordinates. */
|
|
2
|
+
export function geometryBbox(geometry) {
|
|
3
|
+
let west = Infinity;
|
|
4
|
+
let south = Infinity;
|
|
5
|
+
let east = -Infinity;
|
|
6
|
+
let north = -Infinity;
|
|
7
|
+
const add = (p) => {
|
|
8
|
+
if (p[0] < west)
|
|
9
|
+
west = p[0];
|
|
10
|
+
if (p[0] > east)
|
|
11
|
+
east = p[0];
|
|
12
|
+
if (p[1] < south)
|
|
13
|
+
south = p[1];
|
|
14
|
+
if (p[1] > north)
|
|
15
|
+
north = p[1];
|
|
16
|
+
};
|
|
17
|
+
const walk = (g) => {
|
|
18
|
+
switch (g.type) {
|
|
19
|
+
case "Point":
|
|
20
|
+
add(g.coordinates);
|
|
21
|
+
break;
|
|
22
|
+
case "MultiPoint":
|
|
23
|
+
case "LineString":
|
|
24
|
+
g.coordinates.forEach(add);
|
|
25
|
+
break;
|
|
26
|
+
case "MultiLineString":
|
|
27
|
+
case "Polygon":
|
|
28
|
+
g.coordinates.forEach((ring) => ring.forEach(add));
|
|
29
|
+
break;
|
|
30
|
+
case "MultiPolygon":
|
|
31
|
+
g.coordinates.forEach((poly) => poly.forEach((ring) => ring.forEach(add)));
|
|
32
|
+
break;
|
|
33
|
+
case "GeometryCollection":
|
|
34
|
+
g.geometries.forEach(walk);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
walk(geometry);
|
|
39
|
+
return west <= east ? [west, south, east, north] : null;
|
|
40
|
+
}
|
|
41
|
+
const inRect = (p, b) => p[0] >= b.west && p[0] <= b.east && p[1] >= b.south && p[1] <= b.north;
|
|
42
|
+
/** The polygons of a geometry (each an outer ring and holes), or `null` for a non-polygon. */
|
|
43
|
+
function polygonsOf(g) {
|
|
44
|
+
if (g.type === "Polygon")
|
|
45
|
+
return [g.coordinates];
|
|
46
|
+
if (g.type === "MultiPolygon")
|
|
47
|
+
return g.coordinates;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function linesOf(g) {
|
|
51
|
+
if (g.type === "LineString")
|
|
52
|
+
return [g.coordinates];
|
|
53
|
+
if (g.type === "MultiLineString")
|
|
54
|
+
return g.coordinates;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** Ring edges, closing an unclosed ring. Fewer than 3 points: no area, so no edges. */
|
|
58
|
+
function ringEdges(ring) {
|
|
59
|
+
const n = ring.length;
|
|
60
|
+
if (n < 3)
|
|
61
|
+
return [];
|
|
62
|
+
const edges = [];
|
|
63
|
+
for (let i = 0; i < n - 1; i++)
|
|
64
|
+
edges.push([ring[i], ring[i + 1]]);
|
|
65
|
+
const [a, z] = [ring[0], ring[n - 1]];
|
|
66
|
+
if (a[0] !== z[0] || a[1] !== z[1])
|
|
67
|
+
edges.push([z, a]);
|
|
68
|
+
return edges;
|
|
69
|
+
}
|
|
70
|
+
function inRing(p, ring) {
|
|
71
|
+
let inside = false;
|
|
72
|
+
for (const [a, b] of ringEdges(ring)) {
|
|
73
|
+
if (a[1] > p[1] !== b[1] > p[1]) {
|
|
74
|
+
const x = ((b[0] - a[0]) * (p[1] - a[1])) / (b[1] - a[1]) + a[0];
|
|
75
|
+
if (p[0] < x)
|
|
76
|
+
inside = !inside;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return inside;
|
|
80
|
+
}
|
|
81
|
+
function inPolygon(p, polygon) {
|
|
82
|
+
const [outer, ...holes] = polygon;
|
|
83
|
+
return !!outer && inRing(p, outer) && !holes.some((hole) => inRing(p, hole));
|
|
84
|
+
}
|
|
85
|
+
/** Inside an outer ring and outside its holes, for any polygon of the geometry. */
|
|
86
|
+
export function containsPoint(geometry, p) {
|
|
87
|
+
return polygonsOf(geometry)?.some((polygon) => inPolygon(p, polygon)) ?? false;
|
|
88
|
+
}
|
|
89
|
+
const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
90
|
+
const onSegment = (p, a, b) => Math.min(a[0], b[0]) <= p[0] &&
|
|
91
|
+
p[0] <= Math.max(a[0], b[0]) &&
|
|
92
|
+
Math.min(a[1], b[1]) <= p[1] &&
|
|
93
|
+
p[1] <= Math.max(a[1], b[1]);
|
|
94
|
+
/** Whether segments ab and cd meet, touching and collinear overlaps included. */
|
|
95
|
+
function segmentsMeet(a, b, c, d) {
|
|
96
|
+
const d1 = cross(c, d, a);
|
|
97
|
+
const d2 = cross(c, d, b);
|
|
98
|
+
const d3 = cross(a, b, c);
|
|
99
|
+
const d4 = cross(a, b, d);
|
|
100
|
+
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
|
|
101
|
+
return true;
|
|
102
|
+
return ((d1 === 0 && onSegment(a, c, d)) ||
|
|
103
|
+
(d2 === 0 && onSegment(b, c, d)) ||
|
|
104
|
+
(d3 === 0 && onSegment(c, a, b)) ||
|
|
105
|
+
(d4 === 0 && onSegment(d, a, b)));
|
|
106
|
+
}
|
|
107
|
+
function rectCorners(b) {
|
|
108
|
+
return [
|
|
109
|
+
[b.west, b.south],
|
|
110
|
+
[b.east, b.south],
|
|
111
|
+
[b.east, b.north],
|
|
112
|
+
[b.west, b.north]
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function segmentHitsRect(a, b, r) {
|
|
116
|
+
if (inRect(a, r) || inRect(b, r))
|
|
117
|
+
return true;
|
|
118
|
+
const c = rectCorners(r);
|
|
119
|
+
return c.some((p, i) => segmentsMeet(a, b, p, c[(i + 1) % 4]));
|
|
120
|
+
}
|
|
121
|
+
function polygonHitsRect(polygon, r) {
|
|
122
|
+
const edges = polygon.flatMap(ringEdges);
|
|
123
|
+
if (edges.length === 0)
|
|
124
|
+
return false;
|
|
125
|
+
if (edges.some(([a]) => inRect(a, r)))
|
|
126
|
+
return true;
|
|
127
|
+
if (rectCorners(r).some((p) => inPolygon(p, polygon)))
|
|
128
|
+
return true;
|
|
129
|
+
return edges.some(([a, b]) => segmentHitsRect(a, b, r));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether the item is in view: a shape (`geometry`) when it intersects `bounds` (bbox pre-check,
|
|
133
|
+
* then the exact test; holes count), anything else when its `lngLat` is inside. Edges included.
|
|
134
|
+
*/
|
|
135
|
+
export function intersectsBounds(item, bounds) {
|
|
136
|
+
const { geometry } = item;
|
|
137
|
+
const polygons = geometry && polygonsOf(geometry);
|
|
138
|
+
const lines = geometry && linesOf(geometry);
|
|
139
|
+
if (!geometry || (!polygons && !lines && geometry.type !== "Point")) {
|
|
140
|
+
return inRect(item.lngLat, bounds);
|
|
141
|
+
}
|
|
142
|
+
if (geometry.type === "Point")
|
|
143
|
+
return inRect(geometry.coordinates, bounds);
|
|
144
|
+
const box = item.bbox ?? geometryBbox(geometry);
|
|
145
|
+
if (!box ||
|
|
146
|
+
box[0] > bounds.east ||
|
|
147
|
+
box[2] < bounds.west ||
|
|
148
|
+
box[1] > bounds.north ||
|
|
149
|
+
box[3] < bounds.south) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (polygons)
|
|
153
|
+
return polygons.some((polygon) => polygonHitsRect(polygon, bounds));
|
|
154
|
+
return lines.some((line) => line.some((p, i) => i === 0 ? line.length === 1 && inRect(p, bounds) : segmentHitsRect(line[i - 1], p, bounds)));
|
|
155
|
+
}
|
|
156
|
+
/** Sutherland–Hodgman: the ring clipped to the rectangle (open, no repeated first point). */
|
|
157
|
+
function clipRing(ring, r) {
|
|
158
|
+
let pts = ringEdges(ring).map(([a]) => [a[0], a[1]]);
|
|
159
|
+
const sides = [
|
|
160
|
+
[(p) => p[0] >= r.west, (a, b) => atX(a, b, r.west)],
|
|
161
|
+
[(p) => p[0] <= r.east, (a, b) => atX(a, b, r.east)],
|
|
162
|
+
[(p) => p[1] >= r.south, (a, b) => atY(a, b, r.south)],
|
|
163
|
+
[(p) => p[1] <= r.north, (a, b) => atY(a, b, r.north)]
|
|
164
|
+
];
|
|
165
|
+
for (const [keep, cut] of sides) {
|
|
166
|
+
const out = [];
|
|
167
|
+
for (let i = 0; i < pts.length; i++) {
|
|
168
|
+
const cur = pts[i];
|
|
169
|
+
const prev = pts[(i + pts.length - 1) % pts.length];
|
|
170
|
+
if (keep(cur)) {
|
|
171
|
+
if (!keep(prev))
|
|
172
|
+
out.push(cut(prev, cur));
|
|
173
|
+
out.push(cur);
|
|
174
|
+
}
|
|
175
|
+
else if (keep(prev)) {
|
|
176
|
+
out.push(cut(prev, cur));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
pts = out;
|
|
180
|
+
if (pts.length === 0)
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
return pts;
|
|
184
|
+
}
|
|
185
|
+
const atX = (a, b, x) => [
|
|
186
|
+
x,
|
|
187
|
+
a[1] + ((b[1] - a[1]) * (x - a[0])) / (b[0] - a[0])
|
|
188
|
+
];
|
|
189
|
+
const atY = (a, b, y) => [
|
|
190
|
+
a[0] + ((b[0] - a[0]) * (y - a[1])) / (b[1] - a[1]),
|
|
191
|
+
y
|
|
192
|
+
];
|
|
193
|
+
function openArea(pts) {
|
|
194
|
+
let area = 0;
|
|
195
|
+
for (let i = 0; i < pts.length; i++) {
|
|
196
|
+
const [a, b] = [pts[i], pts[(i + 1) % pts.length]];
|
|
197
|
+
area += a[0] * b[1] - b[0] * a[1];
|
|
198
|
+
}
|
|
199
|
+
return Math.abs(area) / 2;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* A point well inside the clipped polygon (outer ring, then holes): the midpoint of the widest
|
|
203
|
+
* interior span of a horizontal line through its middle. When that line has none (a C shape,
|
|
204
|
+
* bands around a hole), the widest span among lines between neighboring vertex heights.
|
|
205
|
+
*/
|
|
206
|
+
function polygonInteriorPoint(rings) {
|
|
207
|
+
const ys = [...new Set(rings.flatMap((ring) => ring.map((p) => p[1])))].sort((a, b) => a - b);
|
|
208
|
+
if (ys.length < 2)
|
|
209
|
+
return null;
|
|
210
|
+
const middle = widestSpan(rings, ys, [(ys[0] + ys[ys.length - 1]) / 2]);
|
|
211
|
+
if (middle)
|
|
212
|
+
return middle;
|
|
213
|
+
const gaps = [];
|
|
214
|
+
for (let i = 1; i < ys.length; i++)
|
|
215
|
+
gaps.push((ys[i - 1] + ys[i]) / 2);
|
|
216
|
+
return widestSpan(rings, ys, gaps);
|
|
217
|
+
}
|
|
218
|
+
/** The midpoint of the widest interior span on any of the horizontal `lines`. */
|
|
219
|
+
function widestSpan(rings, ys, lines) {
|
|
220
|
+
let best = null;
|
|
221
|
+
let width = 0;
|
|
222
|
+
for (let y of lines) {
|
|
223
|
+
// On a vertex height the crossings double up; step off it.
|
|
224
|
+
if (ys.includes(y))
|
|
225
|
+
y += (ys[ys.length - 1] - ys[0]) * 1e-9;
|
|
226
|
+
const xs = [];
|
|
227
|
+
for (const ring of rings) {
|
|
228
|
+
for (let i = 0; i < ring.length; i++) {
|
|
229
|
+
const [a, b] = [ring[i], ring[(i + 1) % ring.length]];
|
|
230
|
+
if (a[1] > y !== b[1] > y)
|
|
231
|
+
xs.push(atY(a, b, y)[0]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
xs.sort((a, b) => a - b);
|
|
235
|
+
for (let i = 0; i + 1 < xs.length; i += 2) {
|
|
236
|
+
if (xs[i + 1] - xs[i] > width) {
|
|
237
|
+
width = xs[i + 1] - xs[i];
|
|
238
|
+
best = [(xs[i] + xs[i + 1]) / 2, y];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return best;
|
|
243
|
+
}
|
|
244
|
+
/** Liang–Barsky: the segment clipped to the rectangle, or `null` when it misses. */
|
|
245
|
+
function clipSegment(a, b, r) {
|
|
246
|
+
const dx = b[0] - a[0];
|
|
247
|
+
const dy = b[1] - a[1];
|
|
248
|
+
let t0 = 0;
|
|
249
|
+
let t1 = 1;
|
|
250
|
+
const checks = [
|
|
251
|
+
[-dx, a[0] - r.west],
|
|
252
|
+
[dx, r.east - a[0]],
|
|
253
|
+
[-dy, a[1] - r.south],
|
|
254
|
+
[dy, r.north - a[1]]
|
|
255
|
+
];
|
|
256
|
+
for (const [p, q] of checks) {
|
|
257
|
+
if (p === 0) {
|
|
258
|
+
if (q < 0)
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const t = q / p;
|
|
263
|
+
if (p < 0)
|
|
264
|
+
t0 = Math.max(t0, t);
|
|
265
|
+
else
|
|
266
|
+
t1 = Math.min(t1, t);
|
|
267
|
+
if (t0 > t1)
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return [
|
|
272
|
+
[a[0] + t0 * dx, a[1] + t0 * dy],
|
|
273
|
+
[a[0] + t1 * dx, a[1] + t1 * dy]
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* A point of the shape inside `view`: `center` when the shape contains it; otherwise a point well
|
|
278
|
+
* inside the shape's visible part (polygons: the largest clipped one, never in a hole; lines: the
|
|
279
|
+
* middle of the longest visible piece); `null` when nothing of it is visible.
|
|
280
|
+
*/
|
|
281
|
+
export function shapeAnchor(geometry, view, center) {
|
|
282
|
+
const polygons = polygonsOf(geometry);
|
|
283
|
+
if (polygons) {
|
|
284
|
+
if (inRect(center, view) && containsPoint(geometry, center))
|
|
285
|
+
return center;
|
|
286
|
+
// Visible parts, largest first; the first that yields a point wins.
|
|
287
|
+
const parts = [];
|
|
288
|
+
for (const polygon of polygons) {
|
|
289
|
+
if (!polygon[0] || ringEdges(polygon[0]).length === 0)
|
|
290
|
+
continue;
|
|
291
|
+
const rings = polygon.map((ring) => clipRing(ring, view));
|
|
292
|
+
const area = rings[0].length >= 3 ? openArea(rings[0]) : 0;
|
|
293
|
+
if (area > 0)
|
|
294
|
+
parts.push({ rings: rings.filter((r, i) => i === 0 || r.length >= 3), area });
|
|
295
|
+
}
|
|
296
|
+
parts.sort((a, b) => b.area - a.area);
|
|
297
|
+
for (const { rings } of parts) {
|
|
298
|
+
const at = polygonInteriorPoint(rings);
|
|
299
|
+
if (at)
|
|
300
|
+
return at;
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const lines = linesOf(geometry);
|
|
305
|
+
if (lines) {
|
|
306
|
+
let best = null;
|
|
307
|
+
let length = -1;
|
|
308
|
+
for (const line of lines) {
|
|
309
|
+
for (let i = 1; i < line.length; i++) {
|
|
310
|
+
const piece = clipSegment(line[i - 1], line[i], view);
|
|
311
|
+
if (!piece)
|
|
312
|
+
continue;
|
|
313
|
+
const len = Math.hypot(piece[1][0] - piece[0][0], piece[1][1] - piece[0][1]);
|
|
314
|
+
if (len > length) {
|
|
315
|
+
length = len;
|
|
316
|
+
best = [(piece[0][0] + piece[1][0]) / 2, (piece[0][1] + piece[1][1]) / 2];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return best;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface FormatProvider {
|
|
2
|
+
number(value: number, options?: Intl.NumberFormatOptions): string;
|
|
3
|
+
/** Formats in Pacific time like sjvair.com's map: "4:00 pm Friday Nov 06, 2026". */
|
|
4
|
+
dateTime(value: Date | string): string;
|
|
5
|
+
/** "PT" only when the device zone isn't Pacific. */
|
|
6
|
+
zoneLabel(): string | null;
|
|
7
|
+
/** How long ago (or until) `value` is from `now` (default: the current time), e.g. "5 minutes ago". */
|
|
8
|
+
relativeTime(value: Date | string, now?: Date): string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createDefaultFormat(deviceZone?: string): FormatProvider;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const PACIFIC = "America/Los_Angeles";
|
|
2
|
+
export function createDefaultFormat(deviceZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
|
|
3
|
+
const dateFormat = new Intl.DateTimeFormat("en-US", {
|
|
4
|
+
timeZone: PACIFIC,
|
|
5
|
+
hour: "numeric",
|
|
6
|
+
minute: "2-digit",
|
|
7
|
+
hour12: true,
|
|
8
|
+
weekday: "long",
|
|
9
|
+
month: "short",
|
|
10
|
+
day: "2-digit",
|
|
11
|
+
year: "numeric"
|
|
12
|
+
});
|
|
13
|
+
// date-fns' "h:mm aaa EEEE MMM dd, yyyy", assembled from the parts.
|
|
14
|
+
const dateTime = (value) => {
|
|
15
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
16
|
+
const part = {};
|
|
17
|
+
for (const { type, value: text } of dateFormat.formatToParts(date))
|
|
18
|
+
part[type] = text;
|
|
19
|
+
return `${part.hour}:${part.minute} ${part.dayPeriod?.toLowerCase()} ${part.weekday} ${part.month} ${part.day}, ${part.year}`;
|
|
20
|
+
};
|
|
21
|
+
const relative = new Intl.RelativeTimeFormat("en-US", { numeric: "always" });
|
|
22
|
+
return {
|
|
23
|
+
number: (value, options) => new Intl.NumberFormat("en-US", options).format(value),
|
|
24
|
+
dateTime,
|
|
25
|
+
zoneLabel: () => (deviceZone === PACIFIC ? null : "PT"),
|
|
26
|
+
relativeTime: (value, now = new Date()) => {
|
|
27
|
+
const ms = (typeof value === "string" ? new Date(value) : value).getTime() - now.getTime();
|
|
28
|
+
const minutes = Math.round(ms / 60_000);
|
|
29
|
+
if (Math.abs(minutes) < 60)
|
|
30
|
+
return relative.format(minutes, "minute");
|
|
31
|
+
const hours = Math.round(ms / 3_600_000);
|
|
32
|
+
if (Math.abs(hours) < 24)
|
|
33
|
+
return relative.format(hours, "hour");
|
|
34
|
+
return relative.format(Math.round(ms / 86_400_000), "day");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./scale-types.js";
|
|
3
|
+
export * from "./refs.js";
|
|
4
|
+
export * from "./scale.js";
|
|
5
|
+
export * from "./selection.js";
|
|
6
|
+
export * from "./static-source.js";
|
|
7
|
+
export * from "./messages.js";
|
|
8
|
+
export * from "./format.js";
|
|
9
|
+
export * from "./metadata.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./scale-types.js";
|
|
3
|
+
export * from "./refs.js";
|
|
4
|
+
export * from "./scale.js";
|
|
5
|
+
export * from "./selection.js";
|
|
6
|
+
export * from "./static-source.js";
|
|
7
|
+
export * from "./messages.js";
|
|
8
|
+
export * from "./format.js";
|
|
9
|
+
export * from "./metadata.js";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://inlang.com/schema/inlang-message-format",
|
|
3
|
+
"mapsdk_list_button": "List",
|
|
4
|
+
"mapsdk_list_button_label": "List of features on the map",
|
|
5
|
+
"mapsdk_list_label": "Features on the map",
|
|
6
|
+
"mapsdk_list_sort_name": "Name",
|
|
7
|
+
"mapsdk_list_sort_value": "Value",
|
|
8
|
+
"mapsdk_list_row": "{name}, {value} {units}, {level}",
|
|
9
|
+
"mapsdk_list_row_no_units": "{name}, {value}, {level}",
|
|
10
|
+
"mapsdk_list_row_no_value": "{name}, no reading",
|
|
11
|
+
"mapsdk_list_row_level": "{name}, {level}",
|
|
12
|
+
"mapsdk_list_row_detail": "{name}, {detail}",
|
|
13
|
+
"mapsdk_list_empty": "Nothing on the map in this view",
|
|
14
|
+
"mapsdk_selected_count_one": "{count} selected",
|
|
15
|
+
"mapsdk_selected_count_other": "{count} selected",
|
|
16
|
+
"mapsdk_selection_clear": "Clear",
|
|
17
|
+
"mapsdk_selection_clear_layer": "Clear selection: {layer}",
|
|
18
|
+
"mapsdk_selected_in_layer": "{layer}: {selected}",
|
|
19
|
+
"mapsdk_selection_cleared": "Selection cleared",
|
|
20
|
+
"mapsdk_summary_one": "{count} {layer}",
|
|
21
|
+
"mapsdk_summary_other": "{count} {layer}",
|
|
22
|
+
"mapsdk_summary_level": "{count} {level}",
|
|
23
|
+
"mapsdk_summary_with_levels": "{layer}: {levels}",
|
|
24
|
+
"mapsdk_no_level": "No level",
|
|
25
|
+
"mapsdk_zoom_in": "Zoom in",
|
|
26
|
+
"mapsdk_zoom_out": "Zoom out",
|
|
27
|
+
"mapsdk_map_label": "Map. Use arrow keys to pan, plus and minus to zoom.",
|
|
28
|
+
"mapsdk_unavailable": "{layer} isn't available right now",
|
|
29
|
+
"mapsdk_loading": "Loading",
|
|
30
|
+
"mapsdk_error_retry": "Try again",
|
|
31
|
+
"mapsdk_legend_label": "Legend",
|
|
32
|
+
"mapsdk_legend_threshold": "from {value}",
|
|
33
|
+
"mapsdk_controls_label": "Map controls",
|
|
34
|
+
"mapsdk_selection_label": "Selection",
|
|
35
|
+
"mapsdk_no_reading": "No reading",
|
|
36
|
+
"mapsdk_time_with_zone": "{time} {zone}",
|
|
37
|
+
"mapsdk_monitor_cluster_average": "(cluster avg)",
|
|
38
|
+
"mapsdk_monitor_cluster_title": "{type} Monitors",
|
|
39
|
+
"mapsdk_monitor_cluster_title_sjvair": "SJVAir {type} Monitors",
|
|
40
|
+
"mapsdk_monitor_cluster_count_one": "{count} monitor",
|
|
41
|
+
"mapsdk_monitor_cluster_count_other": "{count} monitors",
|
|
42
|
+
"mapsdk_monitor_cluster_count_area_one": "{count} monitor within a {area} mi² area",
|
|
43
|
+
"mapsdk_monitor_cluster_count_area_other": "{count} monitors within a {area} mi² area",
|
|
44
|
+
"mapsdk_monitor_cluster_max": "Max: {value} {units}",
|
|
45
|
+
"mapsdk_monitor_cluster_min": "Min: {value} {units}",
|
|
46
|
+
"mapsdk_monitor_interval_avg": "({interval} avg)",
|
|
47
|
+
"mapsdk_monitor_updated": "Last updated {when}",
|
|
48
|
+
"mapsdk_hms_smoke_name": "Smoke",
|
|
49
|
+
"mapsdk_hms_fire_detection": "Fire detection",
|
|
50
|
+
"mapsdk_hms_fire_box_header": "Fire ({units})",
|
|
51
|
+
"mapsdk_hms_fire_box_header_no_units": "Fire",
|
|
52
|
+
"mapsdk_hms_fire_average": "Average",
|
|
53
|
+
"mapsdk_level_with_units": "{level} {units}",
|
|
54
|
+
"mapsdk_hms_fire_detections_one": "{count} fire detection",
|
|
55
|
+
"mapsdk_hms_fire_detections_other": "{count} fire detections",
|
|
56
|
+
"mapsdk_collocation_reference": "Reference monitor",
|
|
57
|
+
"mapsdk_collocation_colocated": "Collocated monitor",
|
|
58
|
+
"mapsdk_collocation_interval_avg": "({interval} avg)",
|
|
59
|
+
"mapsdk_collocation_updated": "Last updated {when}",
|
|
60
|
+
"mapsdk_card_close": "Close"
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import en from "./messages/en.json";
|
|
2
|
+
type Catalog = Omit<typeof en, "$schema">;
|
|
3
|
+
type Literal = keyof Catalog;
|
|
4
|
+
type PluralBase<K> = K extends `${infer B}_one` ? B : never;
|
|
5
|
+
/** Every catalog key, plus the base key of each `_one`/`_other` plural message. */
|
|
6
|
+
export type MessageKey = Literal | PluralBase<Literal>;
|
|
7
|
+
export type MessageParams = Record<string, string | number>;
|
|
8
|
+
export type MessageProvider = (key: MessageKey, params?: MessageParams) => string;
|
|
9
|
+
export declare const defaultMessages: MessageProvider;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import en from "./messages/en.json";
|
|
2
|
+
const catalog = en;
|
|
3
|
+
const plurals = new Intl.PluralRules("en-US");
|
|
4
|
+
function interpolate(template, params) {
|
|
5
|
+
if (!params)
|
|
6
|
+
return template;
|
|
7
|
+
return template.replace(/\{(\w+)\}/g, (whole, name) => Object.hasOwn(params, name) ? String(params[name]) : whole);
|
|
8
|
+
}
|
|
9
|
+
export const defaultMessages = (key, params) => {
|
|
10
|
+
let resolved = key;
|
|
11
|
+
if (!Object.hasOwn(catalog, key) && typeof params?.count === "number") {
|
|
12
|
+
const variant = `${key}_${plurals.select(params.count) === "one" ? "one" : "other"}`;
|
|
13
|
+
if (Object.hasOwn(catalog, variant))
|
|
14
|
+
resolved = variant;
|
|
15
|
+
}
|
|
16
|
+
return interpolate(catalog[resolved] ?? key, params);
|
|
17
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Scale, ScaleRef } from "./scale-types.js";
|
|
2
|
+
export interface EntryTypeInfo {
|
|
3
|
+
label: string;
|
|
4
|
+
units: string;
|
|
5
|
+
/**
|
|
6
|
+
* Decimal places to show values with (the catalog's display hint), or null when the metadata
|
|
7
|
+
* gives none.
|
|
8
|
+
*/
|
|
9
|
+
precision: number | null;
|
|
10
|
+
}
|
|
11
|
+
export interface MetadataProvider {
|
|
12
|
+
scale(ref: ScaleRef, signal?: AbortSignal): Promise<Scale | "unavailable">;
|
|
13
|
+
entryType(ref: ScaleRef, signal?: AbortSignal): Promise<EntryTypeInfo | "unavailable">;
|
|
14
|
+
monitorTypeLabel(type: string, signal?: AbortSignal): Promise<string | "unavailable">;
|
|
15
|
+
/**
|
|
16
|
+
* How often a monitor type reports (e.g. "2 min"), for the tooltip's "(N avg)" note. Optional:
|
|
17
|
+
* the note is left out when a host's provider lacks it.
|
|
18
|
+
*/
|
|
19
|
+
monitorTypeInterval?(type: string, signal?: AbortSignal): Promise<string | "unavailable">;
|
|
20
|
+
attribution(dataset: string, signal?: AbortSignal): Promise<string | "unavailable">;
|
|
21
|
+
}
|
|
22
|
+
export declare function createDefaultMetadata(): MetadataProvider;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { getCalHeatScoreMeta, getDatasetCatalog, getForecastsMeta, getHMSMeta, getMonitorsMeta } from "@sjvair/sdk";
|
|
2
|
+
const AIR_QUALITY = "air-quality";
|
|
3
|
+
/** Datasets whose meta carries `scales` keyed by entry-type id. */
|
|
4
|
+
const SCALE_DATASETS = ["hms-smoke", "hms-fire", "calheatscore", "forecasts"];
|
|
5
|
+
function hasScales(dataset) {
|
|
6
|
+
return SCALE_DATASETS.includes(dataset);
|
|
7
|
+
}
|
|
8
|
+
function abortError() {
|
|
9
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
10
|
+
}
|
|
11
|
+
/** Resolve with `promise`, or reject early if `signal` aborts (without touching `promise`). */
|
|
12
|
+
function untilAborted(promise, signal) {
|
|
13
|
+
if (!signal)
|
|
14
|
+
return promise;
|
|
15
|
+
if (signal.aborted)
|
|
16
|
+
return Promise.reject(abortError());
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const onAbort = () => reject(abortError());
|
|
19
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
20
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch once, share the result, and forget failures so the next call retries. The shared fetch
|
|
25
|
+
* carries no caller signal, so one caller's abort cannot reject it for the others.
|
|
26
|
+
*/
|
|
27
|
+
function memoize(fetch) {
|
|
28
|
+
let cached;
|
|
29
|
+
return (signal) => {
|
|
30
|
+
if (!cached) {
|
|
31
|
+
const p = fetch();
|
|
32
|
+
cached = p;
|
|
33
|
+
p.catch(() => {
|
|
34
|
+
if (cached === p)
|
|
35
|
+
cached = undefined;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return untilAborted(cached, signal);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function createDefaultMetadata() {
|
|
42
|
+
const monitorsMeta = memoize(() => getMonitorsMeta());
|
|
43
|
+
const catalog = memoize(() => getDatasetCatalog());
|
|
44
|
+
// The catalog splits HMS into two datasets; one meta carries both scales.
|
|
45
|
+
const hmsMeta = memoize(() => getHMSMeta());
|
|
46
|
+
const datasetScales = {
|
|
47
|
+
"hms-smoke": hmsMeta,
|
|
48
|
+
"hms-fire": hmsMeta,
|
|
49
|
+
calheatscore: memoize(() => getCalHeatScoreMeta()),
|
|
50
|
+
forecasts: memoize(() => getForecastsMeta())
|
|
51
|
+
};
|
|
52
|
+
/** The value, `"unavailable"` when `read` finds nothing or the fetch fails; only the caller's abort rejects. */
|
|
53
|
+
async function guarded(signal, read) {
|
|
54
|
+
try {
|
|
55
|
+
return (await read()) ?? "unavailable";
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (signal?.aborted)
|
|
59
|
+
throw error;
|
|
60
|
+
return "unavailable";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The catalog's display precision for an entry type, or null. Only the caller's abort rejects. */
|
|
64
|
+
async function precisionOf(ref, signal) {
|
|
65
|
+
try {
|
|
66
|
+
const { datasets } = await catalog(signal);
|
|
67
|
+
const entry = datasets
|
|
68
|
+
.find((d) => d.id === ref.dataset)
|
|
69
|
+
?.entry_types.find((e) => e.id === ref.entryType);
|
|
70
|
+
const precision = entry?.display?.precision;
|
|
71
|
+
return typeof precision === "number" ? precision : null;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
throw error;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function entryMeta(ref, signal) {
|
|
80
|
+
if (ref.dataset !== AIR_QUALITY)
|
|
81
|
+
return undefined;
|
|
82
|
+
return (await monitorsMeta(signal)).entries[ref.entryType];
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
scale: (ref, signal) => guarded(signal, async () => {
|
|
86
|
+
if (hasScales(ref.dataset)) {
|
|
87
|
+
const { scales } = await datasetScales[ref.dataset](signal);
|
|
88
|
+
return Object.hasOwn(scales, ref.entryType) ? scales[ref.entryType] : undefined;
|
|
89
|
+
}
|
|
90
|
+
const entry = await entryMeta(ref, signal);
|
|
91
|
+
return entry?.scale ?? undefined;
|
|
92
|
+
}),
|
|
93
|
+
entryType: (ref, signal) => guarded(signal, async () => {
|
|
94
|
+
let info;
|
|
95
|
+
if (hasScales(ref.dataset)) {
|
|
96
|
+
const { scales } = await datasetScales[ref.dataset](signal);
|
|
97
|
+
const scale = Object.hasOwn(scales, ref.entryType) ? scales[ref.entryType] : undefined;
|
|
98
|
+
info = scale && { label: scale.label, units: scale.units };
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const entry = await entryMeta(ref, signal);
|
|
102
|
+
info = entry && { label: entry.label, units: entry.units ?? "" };
|
|
103
|
+
}
|
|
104
|
+
return info && { ...info, precision: await precisionOf(ref, signal) };
|
|
105
|
+
}),
|
|
106
|
+
monitorTypeLabel: (type, signal) => guarded(signal, async () => (await monitorsMeta(signal)).monitors[type]?.label),
|
|
107
|
+
monitorTypeInterval: (type, signal) => guarded(signal, async () => (await monitorsMeta(signal)).monitors[type]?.expected_interval),
|
|
108
|
+
attribution: (dataset, signal) => guarded(signal, async () => (await catalog(signal)).datasets.find((d) => d.id === dataset)?.source || undefined)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FeatureRef } from "./types.js";
|
|
2
|
+
/** A place as `@sjvair/sdk` and the dashboard name it: a monitor, or a region by type and code. */
|
|
3
|
+
export type PlaceRef = {
|
|
4
|
+
kind: "monitor";
|
|
5
|
+
id: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "region";
|
|
8
|
+
type: string;
|
|
9
|
+
code: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function encodeRef(ref: FeatureRef): string;
|
|
12
|
+
export declare function decodeRef(key: string): FeatureRef | null;
|
|
13
|
+
export declare function selectionId(ref: FeatureRef): string | null;
|
|
14
|
+
export declare function toPlaceRef(ref: FeatureRef): PlaceRef | null;
|
|
15
|
+
export declare function fromPlaceRef(place: {
|
|
16
|
+
kind: string;
|
|
17
|
+
[k: string]: unknown;
|
|
18
|
+
}): FeatureRef | null;
|