@humanforest/ui 0.3.4 → 0.3.6
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/package.json +2 -2
- package/preset.gen.js +2 -1
- package/src/address/autofill.ts +1 -1
- package/src/address/geocode.ts +1 -1
- package/src/address/validate.ts +1 -1
- package/src/contexts/contexts.ts +1 -1
- package/src/filter/filterSchema.ts +1 -1
- package/src/logo/FSubLogo.vue +3 -3
- package/src/map/FMapSearch.vue +3 -3
- package/src/map/bin.ts +3 -3
- package/src/map/density.ts +11 -9
- package/src/map/draw.ts +9 -9
- package/src/map/ease.ts +2 -2
- package/src/map/mapSearch.ts +2 -2
- package/src/map/regionSelect.ts +2 -2
- package/src/map/sources/mapboxPlaces.ts +4 -1
- package/src/map/track.ts +14 -14
- package/src/map/useMapDraw.ts +4 -4
- package/src/map/useRegionSelect.ts +1 -1
- package/src/map/vehicle.ts +3 -3
- package/src/themes/prose.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanforest/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"preset.gen.js"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@humanforest/tokens": "^0.3.
|
|
12
|
+
"@humanforest/tokens": "^0.3.6",
|
|
13
13
|
"@internationalized/date": "^3.12.2",
|
|
14
14
|
"@mapbox/search-js-core": "^1.6.0",
|
|
15
15
|
"@turf/boolean-point-in-polygon": "^7.4.0",
|
package/preset.gen.js
CHANGED
package/src/address/autofill.ts
CHANGED
|
@@ -72,7 +72,7 @@ export function addressAutofill(options: AddressAutofillOptions): AddressAutofil
|
|
|
72
72
|
signal,
|
|
73
73
|
});
|
|
74
74
|
session = new SessionToken();
|
|
75
|
-
return features.length ? fromAutofill(features[0]) : null;
|
|
75
|
+
return features.length ? fromAutofill(features[0]!) : null;
|
|
76
76
|
},
|
|
77
77
|
};
|
|
78
78
|
}
|
package/src/address/geocode.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function geocoder(options: GeocoderOptions): Geocoder {
|
|
|
44
44
|
// the building, then the street, then the postcode, then the city. The first is the answer;
|
|
45
45
|
// the rest are its context, and `fromGeocodingFeature` already reads that off `context`.
|
|
46
46
|
const { features } = await client.reverse(at, { limit: 1, signal });
|
|
47
|
-
return features.length ? fromGeocodingFeature(features[0]) : null;
|
|
47
|
+
return features.length ? fromGeocodingFeature(features[0]!) : null;
|
|
48
48
|
},
|
|
49
49
|
|
|
50
50
|
async forward(query, signal) {
|
package/src/address/validate.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function addressValidator(options: AddressValidatorOptions): AddressValid
|
|
|
62
62
|
const { features } = await client.validate(query, { sessionToken: new SessionToken(), signal });
|
|
63
63
|
if (!features.length) return null;
|
|
64
64
|
|
|
65
|
-
const feature = features[0]
|
|
65
|
+
const feature = features[0]!;
|
|
66
66
|
// MatchCode's fields are typed as const-enum members; read as plain strings, since the enum has
|
|
67
67
|
// no runtime object to compare against (see the note at the top of this file).
|
|
68
68
|
const code = (feature.properties?.match_code ?? {}) as unknown as Record<string, string>;
|
package/src/contexts/contexts.ts
CHANGED
|
@@ -88,7 +88,7 @@ const splitToken = (token: string): { prefixes: string[]; leaf: string } => {
|
|
|
88
88
|
} else cur += ch;
|
|
89
89
|
}
|
|
90
90
|
parts.push(cur);
|
|
91
|
-
return { prefixes: parts.slice(0, -1), leaf: parts[parts.length - 1] };
|
|
91
|
+
return { prefixes: parts.slice(0, -1), leaf: parts[parts.length - 1]! };
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const MARKER_RE = /^forest-[\w-]+$/;
|
|
@@ -45,7 +45,7 @@ export type FilterSchema = Record<string, FilterField>;
|
|
|
45
45
|
* The first entry of each list is that kind's default operator, so a freshly added clause reads the
|
|
46
46
|
* way it would in Linear: an enum starts at `is`, and picking a second value promotes it to `is any of`.
|
|
47
47
|
*/
|
|
48
|
-
export const OPERATORS_BY_KIND: Record<FieldKind, readonly Operator[]> = {
|
|
48
|
+
export const OPERATORS_BY_KIND: Record<FieldKind, readonly [Operator, ...Operator[]]> = {
|
|
49
49
|
enum: ['is', 'isNot', 'isAnyOf', 'isNoneOf'],
|
|
50
50
|
text: ['contains', 'doesNotContain', 'is', 'isNot'],
|
|
51
51
|
number: ['is', 'isNot', 'isAbove', 'isBelow', 'isBetween'],
|
package/src/logo/FSubLogo.vue
CHANGED
|
@@ -65,9 +65,9 @@ const joined = computed(() => gtRun(word.value.toLowerCase(), leadAdvance.value)
|
|
|
65
65
|
const ic = iconGeometry();
|
|
66
66
|
// the icon letter is sized to the square's cap height, so it's set twice: once to measure, once at size
|
|
67
67
|
const icon = computed(() => {
|
|
68
|
-
const probe = setWord(word.value
|
|
68
|
+
const probe = setWord(word.value.charAt(0), { font: 'mohr', size: SUBLOGO_MOHR_SIZE });
|
|
69
69
|
const size = (SUBLOGO_MOHR_SIZE * ic.letterCap) / (probe.inkBottom - probe.inkTop);
|
|
70
|
-
const run = setWord(word.value
|
|
70
|
+
const run = setWord(word.value.charAt(0), { font: 'mohr', size });
|
|
71
71
|
return {
|
|
72
72
|
run,
|
|
73
73
|
x: ic.cx - (run.inkLeft + (run.inkRight - run.inkLeft) / 2),
|
|
@@ -118,7 +118,7 @@ const joinedLight = computed(() => ramp(props.ink != null ? props.ink : props.fa
|
|
|
118
118
|
const joinedDark = computed(() => {
|
|
119
119
|
if (props.ink != null) return ramp(props.face);
|
|
120
120
|
const i = STEPS.indexOf(props.face);
|
|
121
|
-
return ramp(i > 0 ? STEPS[i - 1] : props.face);
|
|
121
|
+
return ramp(i > 0 ? STEPS[i - 1]! : props.face);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
const vars = computed(() => ({
|
package/src/map/FMapSearch.vue
CHANGED
|
@@ -190,7 +190,7 @@ function onFocusOut() {
|
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function sourceOf(item: PaletteItem): MapSearchSource {
|
|
193
|
+
function sourceOf(item: PaletteItem): MapSearchSource | undefined {
|
|
194
194
|
// A promoted row carries no source of its own, so find the group its result actually came from.
|
|
195
195
|
const id =
|
|
196
196
|
state.value?.groups.find((g) => item.result && g.results.includes(item.result))?.sourceId ??
|
|
@@ -223,7 +223,8 @@ async function choose(value: unknown) {
|
|
|
223
223
|
|
|
224
224
|
if (geometry) moveTo(geometry);
|
|
225
225
|
collapse();
|
|
226
|
-
|
|
226
|
+
// With no source left to attribute it to, a pick still moves the camera but emits nothing.
|
|
227
|
+
if (source) emit('select', result, source, geometry);
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
// ★ Centring is a lie on a real ops screen — a side panel and this panel cover part of the map, so the
|
|
@@ -289,7 +290,6 @@ onBeforeUnmount(cancel);
|
|
|
289
290
|
sideOffset: 8,
|
|
290
291
|
// The field keeps focus while the panel opens — the roving highlight lives on the input via
|
|
291
292
|
// aria-activedescendant, so moving focus into the content would break the combobox pattern.
|
|
292
|
-
trapFocus: false,
|
|
293
293
|
onOpenAutoFocus: (e: Event) => e.preventDefault(),
|
|
294
294
|
}"
|
|
295
295
|
:ui="{ content: ui.panel() }"
|
package/src/map/bin.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const H3_EDGE_M: Record<number, number> = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
/** The resolutions this ladder will choose between. Below 6 a cell is bigger than London. */
|
|
39
|
-
export const H3_RES_RANGE: readonly number[] = [6, 7, 8, 9, 10, 11];
|
|
39
|
+
export const H3_RES_RANGE: readonly [number, ...number[]] = [6, 7, 8, 9, 10, 11];
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* How wide a cell should be ON SCREEN, in CSS px.
|
|
@@ -57,7 +57,7 @@ export const metresPerPixel = (zoom: number, lat: number): number =>
|
|
|
57
57
|
|
|
58
58
|
/** How wide a cell of this resolution draws, in CSS px, at a zoom and latitude. */
|
|
59
59
|
export const cellWidthPx = (res: number, zoom: number, lat: number): number =>
|
|
60
|
-
(2 * H3_EDGE_M[res]) / metresPerPixel(zoom, lat);
|
|
60
|
+
(2 * (H3_EDGE_M[res] ?? NaN)) / metresPerPixel(zoom, lat);
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* The resolution to bin at for a zoom — the rung whose drawn cell lands nearest the ideal width.
|
|
@@ -173,7 +173,7 @@ export function robustDomain(values: readonly number[], percentile = 0.95): Doma
|
|
|
173
173
|
const finite = values.filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
|
|
174
174
|
if (!finite.length) return { domain: [0, 0], clamped: 0, percentile };
|
|
175
175
|
const idx = Math.min(finite.length - 1, Math.max(0, Math.ceil(percentile * finite.length) - 1));
|
|
176
|
-
const top = finite[idx]
|
|
176
|
+
const top = finite[idx]!;
|
|
177
177
|
return {
|
|
178
178
|
domain: [0, top],
|
|
179
179
|
clamped: finite.filter((v) => v > top).length,
|
package/src/map/density.ts
CHANGED
|
@@ -170,6 +170,8 @@ export function densityLayers(opts: DensityOptions): OverlayLayer[] {
|
|
|
170
170
|
alpha = DENSITY_ALPHA,
|
|
171
171
|
} = opts;
|
|
172
172
|
|
|
173
|
+
// rampStops caps the count at the ramp's length, so buckets past it take the top stop.
|
|
174
|
+
const stops = densityStops(steps, ramp);
|
|
173
175
|
const layers: OverlayLayer[] = [
|
|
174
176
|
{
|
|
175
177
|
id: `${prefix}-fill`,
|
|
@@ -180,7 +182,7 @@ export function densityLayers(opts: DensityOptions): OverlayLayer[] {
|
|
|
180
182
|
// an unvalued cell falls into the `step`'s first bucket and is published as a counted zero.
|
|
181
183
|
filter: ['has', prop],
|
|
182
184
|
paint: {
|
|
183
|
-
'fill-color': stepColour(prop, domain, steps, (i) =>
|
|
185
|
+
'fill-color': stepColour(prop, domain, steps, (i) => stops[i] ?? stops.at(-1)!),
|
|
184
186
|
'fill-opacity': alpha,
|
|
185
187
|
// fill-emissive-strength is injected by useMapOverlay — `fill` is a shaded type, and an unlit
|
|
186
188
|
// overlay paints at about a fifth of its luminance under the night preset.
|
|
@@ -272,7 +274,7 @@ export function balanceLayers(opts: BalanceOptions): OverlayLayer[] {
|
|
|
272
274
|
slot: MAP_SLOT.route,
|
|
273
275
|
filter,
|
|
274
276
|
paint: {
|
|
275
|
-
'fill-color': stepColour(prop, domain, BALANCE_STEPS, (i) => balanceStops(ramp)[i]),
|
|
277
|
+
'fill-color': stepColour(prop, domain, BALANCE_STEPS, (i) => balanceStops(ramp)[i]!),
|
|
276
278
|
'fill-opacity': alpha,
|
|
277
279
|
},
|
|
278
280
|
},
|
|
@@ -347,17 +349,17 @@ export function densityLegend(opts: LegendOptions): RampLegend {
|
|
|
347
349
|
return {
|
|
348
350
|
kind: 'density',
|
|
349
351
|
unit,
|
|
350
|
-
cellM: res != null ? Math.round(2 * H3_EDGE_M[res]) : undefined,
|
|
352
|
+
cellM: res != null ? Math.round(2 * (H3_EDGE_M[res] ?? NaN)) : undefined,
|
|
351
353
|
clamped,
|
|
352
354
|
percentile,
|
|
353
355
|
stops: edges.slice(0, -1).map((from, i) => ({
|
|
354
356
|
key: String(i),
|
|
355
|
-
colour: stops[i]
|
|
357
|
+
colour: stops[i] ?? stops.at(-1)!,
|
|
356
358
|
from,
|
|
357
|
-
to: edges[i + 1]
|
|
359
|
+
to: edges[i + 1]!,
|
|
358
360
|
// The top bucket is open — everything at or above its floor is painted there, including the
|
|
359
361
|
// clamped readings, so a closed label would understate it.
|
|
360
|
-
label: i === steps - 1 ? `${fmt(from, span)}+` : `${fmt(from, span)}–${fmt(edges[i + 1]
|
|
362
|
+
label: i === steps - 1 ? `${fmt(from, span)}+` : `${fmt(from, span)}–${fmt(edges[i + 1]!, span)}`,
|
|
361
363
|
})),
|
|
362
364
|
};
|
|
363
365
|
}
|
|
@@ -378,16 +380,16 @@ export function balanceLegend(opts: LegendOptions & { low?: string; high?: strin
|
|
|
378
380
|
return {
|
|
379
381
|
kind: 'balance',
|
|
380
382
|
unit,
|
|
381
|
-
cellM: res != null ? Math.round(2 * H3_EDGE_M[res]) : undefined,
|
|
383
|
+
cellM: res != null ? Math.round(2 * (H3_EDGE_M[res] ?? NaN)) : undefined,
|
|
382
384
|
clamped,
|
|
383
385
|
percentile,
|
|
384
386
|
stops: edges.slice(0, -1).map((from, i) => {
|
|
385
|
-
const to = edges[i + 1]
|
|
387
|
+
const to = edges[i + 1]!;
|
|
386
388
|
const centre = i === mid;
|
|
387
389
|
const arm = i < mid ? low : high;
|
|
388
390
|
return {
|
|
389
391
|
key: String(i),
|
|
390
|
-
colour: stops[i]
|
|
392
|
+
colour: stops[i]!,
|
|
391
393
|
from,
|
|
392
394
|
to,
|
|
393
395
|
label: centre
|
package/src/map/draw.ts
CHANGED
|
@@ -76,8 +76,8 @@ const toDeg = (rad: number) => (rad * 180) / Math.PI;
|
|
|
76
76
|
/** GeoJSON's closed ring: the open one with its first position repeated at the end. */
|
|
77
77
|
export function closeRing(ring: DrawRing): LngLat[] {
|
|
78
78
|
if (ring.length < 3) return [...ring];
|
|
79
|
-
const
|
|
80
|
-
const last = ring[ring.length - 1]
|
|
79
|
+
const first = ring[0]!;
|
|
80
|
+
const last = ring[ring.length - 1]!;
|
|
81
81
|
return first[0] === last[0] && first[1] === last[1] ? [...ring] : [...ring, [first[0], first[1]]];
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -102,7 +102,7 @@ export function drawFeatureToGeoJSON(feature: DrawFeature): GeoJSON.Feature<GeoJ
|
|
|
102
102
|
export function midpoints(ring: DrawRing): LngLat[] {
|
|
103
103
|
if (ring.length < 2) return [];
|
|
104
104
|
return ring.map((a, i) => {
|
|
105
|
-
const b = ring[(i + 1) % ring.length]
|
|
105
|
+
const b = ring[(i + 1) % ring.length]!;
|
|
106
106
|
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2] as LngLat;
|
|
107
107
|
});
|
|
108
108
|
}
|
|
@@ -188,13 +188,13 @@ export function selfIntersects(ring: DrawRing): boolean {
|
|
|
188
188
|
const n = ring.length;
|
|
189
189
|
if (n < 4) return false;
|
|
190
190
|
for (let i = 0; i < n; i++) {
|
|
191
|
-
const a = ring[i]
|
|
192
|
-
const b = ring[(i + 1) % n]
|
|
191
|
+
const a = ring[i]!;
|
|
192
|
+
const b = ring[(i + 1) % n]!;
|
|
193
193
|
for (let j = i + 1; j < n; j++) {
|
|
194
194
|
// share a vertex — adjacent edges, and the wrap-around pair the modulo makes adjacent too
|
|
195
195
|
if (j === i || (j + 1) % n === i || (i + 1) % n === j) continue;
|
|
196
|
-
const c = ring[j]
|
|
197
|
-
const d = ring[(j + 1) % n]
|
|
196
|
+
const c = ring[j]!;
|
|
197
|
+
const d = ring[(j + 1) % n]!;
|
|
198
198
|
if (segmentsCross(a, b, c, d)) return true;
|
|
199
199
|
}
|
|
200
200
|
}
|
|
@@ -209,7 +209,7 @@ export function selfIntersects(ring: DrawRing): boolean {
|
|
|
209
209
|
* paints the problem instead and refuses the commit.
|
|
210
210
|
*/
|
|
211
211
|
export function ringProblem(ring: DrawRing): DrawProblem | null {
|
|
212
|
-
const distinct = ring.filter((p, i) => i === 0 || !samePoint(p, ring[i - 1]));
|
|
212
|
+
const distinct = ring.filter((p, i) => i === 0 || !samePoint(p, ring[i - 1]!));
|
|
213
213
|
if (distinct.length < 3) return 'too-few';
|
|
214
214
|
if (selfIntersects(ring)) return 'self-intersects';
|
|
215
215
|
return null;
|
|
@@ -352,5 +352,5 @@ export function undoHistory<T>(h: DrawHistory<T>): DrawHistory<T> {
|
|
|
352
352
|
export function redoHistory<T>(h: DrawHistory<T>): DrawHistory<T> {
|
|
353
353
|
if (!h.future.length) return h;
|
|
354
354
|
const [present, ...future] = h.future;
|
|
355
|
-
return { past: [...h.past, h.present], present, future };
|
|
355
|
+
return { past: [...h.past, h.present], present: present as T, future };
|
|
356
356
|
}
|
package/src/map/ease.ts
CHANGED
|
@@ -47,9 +47,9 @@ const LINEAR = (t: number) => t;
|
|
|
47
47
|
export const parseEasing = (value: string): ((t: number) => number) => {
|
|
48
48
|
const m = value.trim().match(/^cubic-bezier\(([^)]+)\)$/);
|
|
49
49
|
if (!m) return LINEAR;
|
|
50
|
-
const parts = m[1]
|
|
50
|
+
const parts = m[1]!.split(',').map((n) => Number.parseFloat(n));
|
|
51
51
|
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return LINEAR;
|
|
52
|
-
return cubicBezier(parts[0]
|
|
52
|
+
return cubicBezier(parts[0]!, parts[1]!, parts[2]!, parts[3]!);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
/** Reads a CSS time token (`240ms`, `0.24s`) off an element, in ms. 0 for anything unparseable. */
|
package/src/map/mapSearch.ts
CHANGED
|
@@ -195,7 +195,7 @@ export function paletteGroups(state: MapSearchState): PaletteGroup[] {
|
|
|
195
195
|
icon: 'i-lucide-target',
|
|
196
196
|
disabled: true,
|
|
197
197
|
}
|
|
198
|
-
: { ...skeletons(BEST)[0]
|
|
198
|
+
: { ...skeletons(BEST)[0]!, id: `${BEST}-skeleton` };
|
|
199
199
|
|
|
200
200
|
out.push({ id: BEST, ignoreFilter: true, items: [item] });
|
|
201
201
|
}
|
|
@@ -261,7 +261,7 @@ function withGroup(
|
|
|
261
261
|
const i = state.groups.findIndex((g) => g.sourceId === sourceId);
|
|
262
262
|
if (i < 0) return state;
|
|
263
263
|
const groups = state.groups.slice();
|
|
264
|
-
groups[i] = { ...groups[i]
|
|
264
|
+
groups[i] = { ...groups[i]!, ...patch };
|
|
265
265
|
return { ...state, groups };
|
|
266
266
|
}
|
|
267
267
|
|
package/src/map/regionSelect.ts
CHANGED
|
@@ -156,8 +156,8 @@ export function worthQuerying(box: ScreenBox): boolean {
|
|
|
156
156
|
export function pathPolygon(path: readonly ScreenPoint[]): GeoJSON.Feature<GeoJSON.Polygon> | null {
|
|
157
157
|
if (path.length < 3) return null;
|
|
158
158
|
const ring: ScreenPoint[] = [...path];
|
|
159
|
-
const first = ring[0]
|
|
160
|
-
const last = ring[ring.length - 1]
|
|
159
|
+
const first = ring[0]!;
|
|
160
|
+
const last = ring[ring.length - 1]!;
|
|
161
161
|
if (first[0] !== last[0] || first[1] !== last[1]) ring.push([first[0], first[1]]);
|
|
162
162
|
return polygon([ring]);
|
|
163
163
|
}
|
|
@@ -63,7 +63,10 @@ export function mapboxPlaces(options: MapboxPlacesOptions): MapSearchSource {
|
|
|
63
63
|
signal: retrieveSignal,
|
|
64
64
|
});
|
|
65
65
|
session = new SessionToken();
|
|
66
|
-
|
|
66
|
+
// Nothing to place is a failed resolve, which FMapSearch answers by keeping its panel open.
|
|
67
|
+
const feature = features[0];
|
|
68
|
+
if (!feature) throw new Error(`mapboxPlaces: retrieve returned no feature for "${s.name}"`);
|
|
69
|
+
return feature.geometry;
|
|
67
70
|
},
|
|
68
71
|
}));
|
|
69
72
|
},
|
package/src/map/track.ts
CHANGED
|
@@ -79,8 +79,8 @@ export function bearingBetween(a: Fix, b: Fix): number {
|
|
|
79
79
|
/** A zero span for an empty track rather than a throw: a scrubber renders before its data arrives. */
|
|
80
80
|
export function trackSpan(track: Track): TrackSpan {
|
|
81
81
|
if (!track.length) return { start: 0, end: 0, ms: 0 };
|
|
82
|
-
const start = track[0]
|
|
83
|
-
const end = track[track.length - 1]
|
|
82
|
+
const start = track[0]!.at;
|
|
83
|
+
const end = track[track.length - 1]!.at;
|
|
84
84
|
return { start, end, ms: Math.max(0, end - start) };
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -91,7 +91,7 @@ function indexAt(track: Track, at: number): number {
|
|
|
91
91
|
let best = -1;
|
|
92
92
|
while (lo <= hi) {
|
|
93
93
|
const mid = (lo + hi) >> 1;
|
|
94
|
-
if (track[mid]
|
|
94
|
+
if (track[mid]!.at <= at) {
|
|
95
95
|
best = mid;
|
|
96
96
|
lo = mid + 1;
|
|
97
97
|
} else {
|
|
@@ -115,19 +115,19 @@ function indexAt(track: Track, at: number): number {
|
|
|
115
115
|
export function positionAt(track: Track, at: number, maxGapMs = MAX_GAP_MS): TrackPoint | null {
|
|
116
116
|
if (!track.length) return null;
|
|
117
117
|
|
|
118
|
-
const first = track[0]
|
|
119
|
-
const last = track[track.length - 1]
|
|
118
|
+
const first = track[0]!;
|
|
119
|
+
const last = track[track.length - 1]!;
|
|
120
120
|
if (track.length === 1) return { lng: first.lng, lat: first.lat, bearing: 0, gap: false };
|
|
121
121
|
if (at <= first.at) {
|
|
122
|
-
return { lng: first.lng, lat: first.lat, bearing: bearingBetween(first, track[1]), gap: false };
|
|
122
|
+
return { lng: first.lng, lat: first.lat, bearing: bearingBetween(first, track[1]!), gap: false };
|
|
123
123
|
}
|
|
124
124
|
if (at >= last.at) {
|
|
125
|
-
return { lng: last.lng, lat: last.lat, bearing: bearingBetween(track[track.length - 2]
|
|
125
|
+
return { lng: last.lng, lat: last.lat, bearing: bearingBetween(track[track.length - 2]!, last), gap: false };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const i = indexAt(track, at);
|
|
129
|
-
const a = track[i]
|
|
130
|
-
const b = track[i + 1]
|
|
129
|
+
const a = track[i]!;
|
|
130
|
+
const b = track[i + 1]!;
|
|
131
131
|
const bearing = bearingBetween(a, b);
|
|
132
132
|
const dt = b.at - a.at;
|
|
133
133
|
if (dt > maxGapMs) return { lng: a.lng, lat: a.lat, bearing, gap: true };
|
|
@@ -155,10 +155,10 @@ export function progressAt(track: Track, at: number): number {
|
|
|
155
155
|
/** Split where the signal was lost. Every consumer that draws a track goes through this first. */
|
|
156
156
|
export function trackSegments(track: Track, maxGapMs = MAX_GAP_MS): Fix[][] {
|
|
157
157
|
if (!track.length) return [];
|
|
158
|
-
const out: Fix[][] = [[track[0]]];
|
|
158
|
+
const out: Fix[][] = [[track[0]!]];
|
|
159
159
|
for (let i = 1; i < track.length; i++) {
|
|
160
|
-
if (track[i]
|
|
161
|
-
else out[out.length - 1]
|
|
160
|
+
if (track[i]!.at - track[i - 1]!.at > maxGapMs) out.push([track[i]!]);
|
|
161
|
+
else out[out.length - 1]!.push(track[i]!);
|
|
162
162
|
}
|
|
163
163
|
return out;
|
|
164
164
|
}
|
|
@@ -181,7 +181,7 @@ export function trackLine(
|
|
|
181
181
|
if (!parts.length) return null;
|
|
182
182
|
const geometry: GeoJSON.LineString | GeoJSON.MultiLineString =
|
|
183
183
|
parts.length === 1
|
|
184
|
-
? { type: 'LineString', coordinates: parts[0] }
|
|
184
|
+
? { type: 'LineString', coordinates: parts[0]! }
|
|
185
185
|
: { type: 'MultiLineString', coordinates: parts };
|
|
186
186
|
return { type: 'Feature', properties: {}, geometry };
|
|
187
187
|
}
|
|
@@ -247,7 +247,7 @@ export function trackBounds(track: Track): [[number, number], [number, number]]
|
|
|
247
247
|
export function trackDistance(track: Track, maxGapMs = MAX_GAP_MS): number {
|
|
248
248
|
let total = 0;
|
|
249
249
|
for (const seg of trackSegments(track, maxGapMs)) {
|
|
250
|
-
for (let i = 1; i < seg.length; i++) total += metresBetween(seg[i - 1]
|
|
250
|
+
for (let i = 1; i < seg.length; i++) total += metresBetween(seg[i - 1]!, seg[i]!);
|
|
251
251
|
}
|
|
252
252
|
return total;
|
|
253
253
|
}
|
package/src/map/useMapDraw.ts
CHANGED
|
@@ -216,7 +216,7 @@ export function useMapDraw(map: ShallowRef<MapboxMap | null>, options: () => Map
|
|
|
216
216
|
candidates.map((c) => c.at),
|
|
217
217
|
at,
|
|
218
218
|
);
|
|
219
|
-
return hit == null ? raw : candidates[hit]
|
|
219
|
+
return hit == null ? raw : candidates[hit]!.lngLat;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// ── the draft ────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -232,7 +232,7 @@ export function useMapDraw(map: ShallowRef<MapboxMap | null>, options: () => Map
|
|
|
232
232
|
}
|
|
233
233
|
// Clicking the first vertex closes the ring — the gesture every drawing tool has, and the reason
|
|
234
234
|
// the draft's own vertices are snap candidates.
|
|
235
|
-
if (ring.length >= 3 && apart(toScreen(m, ring[0]), at) <= HANDLE_HIT_PX) {
|
|
235
|
+
if (ring.length >= 3 && apart(toScreen(m, ring[0]!), at) <= HANDLE_HIT_PX) {
|
|
236
236
|
finish();
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
@@ -320,7 +320,7 @@ export function useMapDraw(map: ShallowRef<MapboxMap | null>, options: () => Map
|
|
|
320
320
|
function featureAt(m: MapboxMap, at: ScreenPoint): DrawFeature | null {
|
|
321
321
|
const p = turfPoint(toLngLat(m, at));
|
|
322
322
|
for (let i = features.value.length - 1; i >= 0; i--) {
|
|
323
|
-
const f = features.value[i]
|
|
323
|
+
const f = features.value[i]!;
|
|
324
324
|
if (f.ring.length < 3) continue;
|
|
325
325
|
if (booleanPointInPolygon(p, turfPolygon([closeRing(f.ring)]))) return f;
|
|
326
326
|
}
|
|
@@ -367,7 +367,7 @@ export function useMapDraw(map: ShallowRef<MapboxMap | null>, options: () => Map
|
|
|
367
367
|
// that was not there when the reader started moving.
|
|
368
368
|
const f = features.value.find((x) => x.id === handle.id);
|
|
369
369
|
if (f) {
|
|
370
|
-
const mid = midpoints(f.ring)[handle.index]
|
|
370
|
+
const mid = midpoints(f.ring)[handle.index]!;
|
|
371
371
|
replaceRing(handle.id, insertVertex(f.ring, handle.index, mid));
|
|
372
372
|
drag = { kind: 'vertex', id: handle.id, index: handle.index + 1 };
|
|
373
373
|
}
|
|
@@ -166,7 +166,7 @@ export function useRegionSelect(
|
|
|
166
166
|
const p = canvasPoint(m, e);
|
|
167
167
|
// A lasso is everything between press and release; a box is its two corners, and a circle its
|
|
168
168
|
// centre and a point on the rim — both of which only ever need the first point and the current one.
|
|
169
|
-
path.value = shape.value === 'lasso' ? [...path.value, p] : [path.value[0], p];
|
|
169
|
+
path.value = shape.value === 'lasso' ? [...path.value, p] : [path.value[0] ?? p, p];
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
function onUp(e: PointerEvent) {
|
package/src/map/vehicle.ts
CHANGED
|
@@ -640,8 +640,8 @@ export function paintMarkerSvg(
|
|
|
640
640
|
// vehicle's manifest here painted a place's glyphs with the vehicle's param names, which resolved to
|
|
641
641
|
// nothing and left every glyph transparent.
|
|
642
642
|
const declared = [...raw.matchAll(/<m:parameter[^>]*m:name="([^"]+)"[^>]*m:value="([^"]+)"/g)].map((m) => ({
|
|
643
|
-
name: m[1]
|
|
644
|
-
sentinel: m[2]
|
|
643
|
+
name: m[1]!,
|
|
644
|
+
sentinel: m[2]!,
|
|
645
645
|
}));
|
|
646
646
|
const table = declared.length ? declared : markerManifest.params;
|
|
647
647
|
for (const p of table) {
|
|
@@ -780,7 +780,7 @@ export function unavailableReasons(v: VehicleFacts): string[] {
|
|
|
780
780
|
export function unavailableReason(v: VehicleFacts): string | null {
|
|
781
781
|
const reasons = unavailableReasons(v);
|
|
782
782
|
if (!reasons.length) return null;
|
|
783
|
-
if (reasons.length === 1) return reasons[0]
|
|
783
|
+
if (reasons.length === 1) return reasons[0]!;
|
|
784
784
|
return `${reasons.slice(0, -1).join(', ')} and ${reasons.at(-1)}`;
|
|
785
785
|
}
|
|
786
786
|
|
package/src/themes/prose.ts
CHANGED
|
@@ -26,6 +26,10 @@ export const proseTheme = {
|
|
|
26
26
|
codePreview: {
|
|
27
27
|
slots: {
|
|
28
28
|
preview: 'ed-demo',
|
|
29
|
+
// Stock squares `code > div > pre`, the top of a bare block joined under the pane. A collapsed
|
|
30
|
+
// block wraps that <pre> one level deeper, so the wrapper and the <pre> both kept 12px top
|
|
31
|
+
// corners and notched the joint at each end.
|
|
32
|
+
code: '[&>div]:rounded-t-none [&>div>div>pre]:rounded-t-none',
|
|
29
33
|
},
|
|
30
34
|
},
|
|
31
35
|
|