@aiaiai-pt/design-system 0.16.0 → 0.17.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.
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
-->
|
|
13
13
|
<script>
|
|
14
14
|
import { fromLonLat } from 'ol/proj.js';
|
|
15
|
-
import { createTileLayer, createMapStyles, watchTheme, renderMapError } from './map-utils.js';
|
|
15
|
+
import { createTileLayer, createMapStyles, createOverlayLayers, watchTheme, renderMapError } from './map-utils.js';
|
|
16
16
|
|
|
17
17
|
let {
|
|
18
18
|
/** @type {[number, number]} — [longitude, latitude] WGS84 */
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
polygon = undefined,
|
|
26
26
|
/** @type {import('./map-utils.js').TileSourceConfig} */
|
|
27
27
|
tileSource = { type: 'osm' },
|
|
28
|
+
/** @type {import('./map-utils.js').OverlayLayerDef[]} — ordered GeoJSON
|
|
29
|
+
* overlays rendered between the tiles and the marker/polygon layer.
|
|
30
|
+
* Each entry: inline `data` or a `url` (e.g. the platform's
|
|
31
|
+
* `/{app}/public/layers/{id}/features`), optional flat or GeoStyler
|
|
32
|
+
* `style`. See map-utils OverlayLayerDef. */
|
|
33
|
+
layers = [],
|
|
28
34
|
/** @type {string} */
|
|
29
35
|
height = '100%',
|
|
30
36
|
/** @type {string} */
|
|
@@ -71,6 +77,9 @@
|
|
|
71
77
|
]);
|
|
72
78
|
if (disposed) return;
|
|
73
79
|
|
|
80
|
+
const overlayLayers = await createOverlayLayers(layers, styles);
|
|
81
|
+
if (disposed) return;
|
|
82
|
+
|
|
74
83
|
/** @type {Feature[]} */
|
|
75
84
|
const features = [];
|
|
76
85
|
|
|
@@ -94,7 +103,7 @@
|
|
|
94
103
|
|
|
95
104
|
map = new OlMap({
|
|
96
105
|
target: container,
|
|
97
|
-
layers: [tileLayer, vectorLayer],
|
|
106
|
+
layers: [tileLayer, ...overlayLayers, vectorLayer],
|
|
98
107
|
view: new View({
|
|
99
108
|
center: fromLonLat(center),
|
|
100
109
|
zoom,
|
|
@@ -105,6 +114,9 @@
|
|
|
105
114
|
disposeTheme = watchTheme(() => {
|
|
106
115
|
styles.refresh();
|
|
107
116
|
vectorLayer.getSource()?.changed();
|
|
117
|
+
// Token-styled overlays (no custom style) re-read via the shared
|
|
118
|
+
// styles object; poke their sources so OL repaints.
|
|
119
|
+
for (const l of overlayLayers) l.getSource()?.changed();
|
|
108
120
|
});
|
|
109
121
|
} catch (err) { renderMapError(container, 'MapDisplay', /** @type {Error} */ (err)); } })();
|
|
110
122
|
|
|
@@ -21,6 +21,7 @@ declare const MapDisplay: import("svelte").Component<{
|
|
|
21
21
|
marker?: any;
|
|
22
22
|
polygon?: any;
|
|
23
23
|
tileSource?: Record<string, any>;
|
|
24
|
+
layers?: any[];
|
|
24
25
|
height?: string;
|
|
25
26
|
class?: string;
|
|
26
27
|
} & Record<string, any>, {}, "">;
|
|
@@ -30,6 +31,7 @@ type $$ComponentProps = {
|
|
|
30
31
|
marker?: any;
|
|
31
32
|
polygon?: any;
|
|
32
33
|
tileSource?: Record<string, any>;
|
|
34
|
+
layers?: any[];
|
|
33
35
|
height?: string;
|
|
34
36
|
class?: string;
|
|
35
37
|
} & Record<string, any>;
|
|
@@ -22,7 +22,16 @@
|
|
|
22
22
|
|
|
23
23
|
<script>
|
|
24
24
|
import { fromLonLat, toLonLat } from 'ol/proj.js';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createTileLayer,
|
|
27
|
+
createMapStyles,
|
|
28
|
+
createOverlayLayers,
|
|
29
|
+
createBoundaryLayer,
|
|
30
|
+
boundaryToRings,
|
|
31
|
+
pointInRings,
|
|
32
|
+
watchTheme,
|
|
33
|
+
renderMapError,
|
|
34
|
+
} from './map-utils.js';
|
|
26
35
|
import GeoSearch from './GeoSearch.svelte';
|
|
27
36
|
|
|
28
37
|
let {
|
|
@@ -50,6 +59,18 @@
|
|
|
50
59
|
searchViewbox = undefined,
|
|
51
60
|
/** @type {import('./map-utils.js').TileSourceConfig} */
|
|
52
61
|
tileSource = { type: 'osm' },
|
|
62
|
+
/** @type {import('./map-utils.js').OverlayLayerDef[]} — ordered GeoJSON
|
|
63
|
+
* overlays between the tiles and the draw layer (see MapDisplay). */
|
|
64
|
+
layers = [],
|
|
65
|
+
/** @type {any} — tenant boundary: GeoJSON Polygon/MultiPolygon/Feature/
|
|
66
|
+
* FeatureCollection or a raw ring [[lon,lat],...]. Rendered as a dashed
|
|
67
|
+
* --map-boundary-* overlay; point placements are tested against it. */
|
|
68
|
+
boundary = undefined,
|
|
69
|
+
/** @type {((outside: boolean, coords: [number, number]) => void) | undefined}
|
|
70
|
+
* Fired on every point placement (map click or search pick) when a
|
|
71
|
+
* `boundary` is set. The pin still lands — surfacing/blocking is the
|
|
72
|
+
* consumer's call (the intake hard-gate is server-side regardless). */
|
|
73
|
+
onoutofbounds = undefined,
|
|
53
74
|
/** @type {((coords: [number, number] | number[][]) => void) | undefined} */
|
|
54
75
|
onchange = undefined,
|
|
55
76
|
/** @type {((displayName: string) => void) | undefined} — the resolved
|
|
@@ -103,6 +124,7 @@
|
|
|
103
124
|
_vectorSource.clear();
|
|
104
125
|
_vectorSource.addFeature(new _Feature({ geometry: new _Point(fromLonLat([lon, lat])) }));
|
|
105
126
|
value = [lon, lat];
|
|
127
|
+
checkBoundary([lon, lat]);
|
|
106
128
|
onchange?.([lon, lat]);
|
|
107
129
|
}
|
|
108
130
|
}
|
|
@@ -112,6 +134,14 @@
|
|
|
112
134
|
searchCoords = [lon, lat];
|
|
113
135
|
}
|
|
114
136
|
|
|
137
|
+
const boundaryRings = $derived(boundaryToRings(boundary));
|
|
138
|
+
|
|
139
|
+
/** @param {[number, number]} coords */
|
|
140
|
+
function checkBoundary(coords) {
|
|
141
|
+
if (!boundaryRings.length || !onoutofbounds) return;
|
|
142
|
+
onoutofbounds(!pointInRings(coords, boundaryRings), coords);
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
$effect(() => {
|
|
116
146
|
if (!container || disabled) return;
|
|
117
147
|
|
|
@@ -148,6 +178,12 @@
|
|
|
148
178
|
]);
|
|
149
179
|
if (disposed) return;
|
|
150
180
|
|
|
181
|
+
const [overlayLayers, boundaryLayer] = await Promise.all([
|
|
182
|
+
createOverlayLayers(layers, styles),
|
|
183
|
+
createBoundaryLayer(boundaryRings, container),
|
|
184
|
+
]);
|
|
185
|
+
if (disposed) return;
|
|
186
|
+
|
|
151
187
|
const vectorSource = new VectorSource();
|
|
152
188
|
_vectorSource = vectorSource;
|
|
153
189
|
_Feature = Feature;
|
|
@@ -191,6 +227,7 @@
|
|
|
191
227
|
const wgs84 = /** @type {[number, number]} */ (toLonLat(coords));
|
|
192
228
|
value = wgs84;
|
|
193
229
|
handleMapPointPlaced(wgs84[0], wgs84[1]);
|
|
230
|
+
checkBoundary(wgs84);
|
|
194
231
|
onchange?.(wgs84);
|
|
195
232
|
} else {
|
|
196
233
|
const coords = /** @type {import('ol/geom/Polygon.js').default} */ (geom).getCoordinates()[0];
|
|
@@ -203,7 +240,12 @@
|
|
|
203
240
|
|
|
204
241
|
map = new OlMap({
|
|
205
242
|
target: container,
|
|
206
|
-
layers: [
|
|
243
|
+
layers: [
|
|
244
|
+
tileLayer,
|
|
245
|
+
...overlayLayers,
|
|
246
|
+
...(boundaryLayer ? [boundaryLayer] : []),
|
|
247
|
+
vectorLayer,
|
|
248
|
+
],
|
|
207
249
|
view: new View({
|
|
208
250
|
center: initialCenter,
|
|
209
251
|
zoom,
|
|
@@ -216,6 +258,7 @@
|
|
|
216
258
|
disposeTheme = watchTheme(() => {
|
|
217
259
|
styles.refresh();
|
|
218
260
|
vectorSource.changed();
|
|
261
|
+
for (const l of overlayLayers) l.getSource()?.changed();
|
|
219
262
|
});
|
|
220
263
|
} catch (err) { renderMapError(container, 'MapPicker', /** @type {Error} */ (err)); } })();
|
|
221
264
|
|
|
@@ -34,6 +34,9 @@ declare const MapPicker: import("svelte").Component<{
|
|
|
34
34
|
searchProviderUrl?: any;
|
|
35
35
|
searchViewbox?: any;
|
|
36
36
|
tileSource?: Record<string, any>;
|
|
37
|
+
layers?: any[];
|
|
38
|
+
boundary?: any;
|
|
39
|
+
onoutofbounds?: any;
|
|
37
40
|
onchange?: any;
|
|
38
41
|
onaddress?: any;
|
|
39
42
|
height?: string;
|
|
@@ -53,6 +56,9 @@ type $$ComponentProps = {
|
|
|
53
56
|
searchProviderUrl?: any;
|
|
54
57
|
searchViewbox?: any;
|
|
55
58
|
tileSource?: Record<string, any>;
|
|
59
|
+
layers?: any[];
|
|
60
|
+
boundary?: any;
|
|
61
|
+
onoutofbounds?: any;
|
|
56
62
|
onchange?: any;
|
|
57
63
|
onaddress?: any;
|
|
58
64
|
height?: string;
|
package/components/map-utils.js
CHANGED
|
@@ -336,3 +336,264 @@ export function getHeatmapGradient(el) {
|
|
|
336
336
|
|
|
337
337
|
return ["rgba(251, 227, 142, 0)", "#fbe38e", "#fb923c", "#e85a28", "#ae2a1e"];
|
|
338
338
|
}
|
|
339
|
+
|
|
340
|
+
// ─── Overlay Layers (#187 D-c) ───────────────────────────────────
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @typedef {object} OverlayLayerStyle
|
|
344
|
+
* Flat style subset applied to a whole overlay layer. Colors are CSS color
|
|
345
|
+
* strings (they come from layer DATA — geolayers GeoStyler styles — not
|
|
346
|
+
* from design tokens; absent values fall back to the DS-tokened defaults).
|
|
347
|
+
* @property {string} [pointColor]
|
|
348
|
+
* @property {number} [pointRadius]
|
|
349
|
+
* @property {string} [strokeColor]
|
|
350
|
+
* @property {number} [strokeWidth]
|
|
351
|
+
* @property {string} [fillColor]
|
|
352
|
+
*
|
|
353
|
+
* @typedef {object} OverlayLayerDef
|
|
354
|
+
* One ordered overlay rendered between the tile layer and the component's
|
|
355
|
+
* own interactive vector layer. v1 is GeoJSON-only — the platform's citizen
|
|
356
|
+
* data plane serves GeoJSON (`/{app}/public/layers/{id}/features`); WMS/tile
|
|
357
|
+
* overlays arrive with the per-tenant raster principal (spec open question).
|
|
358
|
+
* @property {string} [id]
|
|
359
|
+
* @property {'geojson'} [type]
|
|
360
|
+
* @property {object} [data] — inline GeoJSON (Feature/FeatureCollection)
|
|
361
|
+
* @property {string} [url] — fetched when `data` is absent
|
|
362
|
+
* @property {OverlayLayerStyle | object} [style] — flat style or GeoStyler
|
|
363
|
+
* JSON (best-effort subset via `geoStylerToFlat`)
|
|
364
|
+
* @property {boolean} [visible]
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Best-effort GeoStyler → flat style. Reads the first symbolizer of the
|
|
369
|
+
* first rule — enough for the platform's single-rule layer styles. Unknown
|
|
370
|
+
* shapes return {} so the DS-tokened defaults apply.
|
|
371
|
+
*
|
|
372
|
+
* @param {any} style
|
|
373
|
+
* @returns {OverlayLayerStyle}
|
|
374
|
+
*/
|
|
375
|
+
export function geoStylerToFlat(style) {
|
|
376
|
+
if (!style || typeof style !== "object") return {};
|
|
377
|
+
if (!Array.isArray(style.rules)) return /** @type {OverlayLayerStyle} */ (style);
|
|
378
|
+
const sym = style.rules?.[0]?.symbolizers?.[0];
|
|
379
|
+
if (!sym || typeof sym !== "object") return {};
|
|
380
|
+
/** @type {OverlayLayerStyle} */
|
|
381
|
+
const flat = {};
|
|
382
|
+
if (sym.kind === "Mark" || sym.kind === "Icon") {
|
|
383
|
+
if (typeof sym.color === "string") flat.pointColor = sym.color;
|
|
384
|
+
if (typeof sym.radius === "number") flat.pointRadius = sym.radius;
|
|
385
|
+
if (typeof sym.strokeColor === "string") flat.strokeColor = sym.strokeColor;
|
|
386
|
+
if (typeof sym.strokeWidth === "number") flat.strokeWidth = sym.strokeWidth;
|
|
387
|
+
} else if (sym.kind === "Line") {
|
|
388
|
+
if (typeof sym.color === "string") flat.strokeColor = sym.color;
|
|
389
|
+
if (typeof sym.width === "number") flat.strokeWidth = sym.width;
|
|
390
|
+
} else if (sym.kind === "Fill") {
|
|
391
|
+
if (typeof sym.color === "string") flat.fillColor = sym.color;
|
|
392
|
+
if (typeof sym.outlineColor === "string") flat.strokeColor = sym.outlineColor;
|
|
393
|
+
if (typeof sym.outlineWidth === "number") flat.strokeWidth = sym.outlineWidth;
|
|
394
|
+
}
|
|
395
|
+
return flat;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Builds OL vector layers for `layers` overlay defs, ordered as given.
|
|
400
|
+
* URL-sourced layers load asynchronously into their source; a fetch failure
|
|
401
|
+
* leaves that overlay empty and logs a warning (the map still renders).
|
|
402
|
+
*
|
|
403
|
+
* @param {OverlayLayerDef[]} defs
|
|
404
|
+
* @param {import('./map-utils.js').MapStyles} styles — DS default styles
|
|
405
|
+
* @returns {Promise<import('ol/layer/Vector.js').default[]>}
|
|
406
|
+
*/
|
|
407
|
+
export async function createOverlayLayers(defs, styles) {
|
|
408
|
+
if (!Array.isArray(defs) || defs.length === 0) return [];
|
|
409
|
+
|
|
410
|
+
const [
|
|
411
|
+
{ default: VectorLayer },
|
|
412
|
+
{ default: VectorSource },
|
|
413
|
+
{ default: GeoJSON },
|
|
414
|
+
{ default: Style },
|
|
415
|
+
{ default: CircleStyle },
|
|
416
|
+
{ default: Fill },
|
|
417
|
+
{ default: Stroke },
|
|
418
|
+
] = await Promise.all([
|
|
419
|
+
import("ol/layer/Vector.js"),
|
|
420
|
+
import("ol/source/Vector.js"),
|
|
421
|
+
import("ol/format/GeoJSON.js"),
|
|
422
|
+
import("ol/style/Style.js"),
|
|
423
|
+
import("ol/style/Circle.js"),
|
|
424
|
+
import("ol/style/Fill.js"),
|
|
425
|
+
import("ol/style/Stroke.js"),
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
const format = new GeoJSON({ featureProjection: "EPSG:3857" });
|
|
429
|
+
|
|
430
|
+
/** @param {OverlayLayerDef} def */
|
|
431
|
+
function buildStyleFn(def) {
|
|
432
|
+
const flat = geoStylerToFlat(def.style);
|
|
433
|
+
const hasCustom = Object.keys(flat).length > 0;
|
|
434
|
+
if (!hasCustom) {
|
|
435
|
+
return (/** @type {any} */ feature) => {
|
|
436
|
+
const type = feature.getGeometry()?.getType();
|
|
437
|
+
return type === "Point" || type === "MultiPoint"
|
|
438
|
+
? styles.marker
|
|
439
|
+
: styles.polygon;
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const stroke = new Stroke({
|
|
443
|
+
color: flat.strokeColor ?? flat.pointColor ?? "#3366cc",
|
|
444
|
+
width: flat.strokeWidth ?? 2,
|
|
445
|
+
});
|
|
446
|
+
const fill = new Fill({
|
|
447
|
+
color: flat.fillColor ?? "rgba(51,102,204,0.15)",
|
|
448
|
+
});
|
|
449
|
+
const point = new Style({
|
|
450
|
+
image: new CircleStyle({
|
|
451
|
+
radius: flat.pointRadius ?? 6,
|
|
452
|
+
fill: new Fill({ color: flat.pointColor ?? flat.strokeColor ?? "#3366cc" }),
|
|
453
|
+
stroke,
|
|
454
|
+
}),
|
|
455
|
+
});
|
|
456
|
+
const shape = new Style({ fill, stroke });
|
|
457
|
+
return (/** @type {any} */ feature) => {
|
|
458
|
+
const type = feature.getGeometry()?.getType();
|
|
459
|
+
return type === "Point" || type === "MultiPoint" ? point : shape;
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return defs
|
|
464
|
+
.filter((def) => def && def.visible !== false)
|
|
465
|
+
.map((def) => {
|
|
466
|
+
const source = new VectorSource();
|
|
467
|
+
if (def.data) {
|
|
468
|
+
source.addFeatures(format.readFeatures(def.data));
|
|
469
|
+
} else if (def.url) {
|
|
470
|
+
fetch(def.url)
|
|
471
|
+
.then((r) => {
|
|
472
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
473
|
+
return r.json();
|
|
474
|
+
})
|
|
475
|
+
.then((geojson) => source.addFeatures(format.readFeatures(geojson)))
|
|
476
|
+
.catch((err) =>
|
|
477
|
+
console.warn(
|
|
478
|
+
`[map] overlay layer ${def.id ?? def.url} failed to load:`,
|
|
479
|
+
err,
|
|
480
|
+
),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return new VectorLayer({ source, style: buildStyleFn(def) });
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ─── Boundary (#187 D-e prep) ────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Normalises a boundary prop into polygon rings (lon/lat WGS84).
|
|
491
|
+
* Accepts: raw ring `number[][]`, GeoJSON Polygon / MultiPolygon,
|
|
492
|
+
* Feature, or FeatureCollection (first polygonal feature per entry).
|
|
493
|
+
*
|
|
494
|
+
* @param {any} boundary
|
|
495
|
+
* @returns {number[][][]} — array of outer rings (one per polygon)
|
|
496
|
+
*/
|
|
497
|
+
export function boundaryToRings(boundary) {
|
|
498
|
+
if (!boundary) return [];
|
|
499
|
+
if (Array.isArray(boundary)) {
|
|
500
|
+
// Raw ring: [[lon,lat], ...]
|
|
501
|
+
return boundary.length >= 3 ? [boundary] : [];
|
|
502
|
+
}
|
|
503
|
+
if (boundary.type === "FeatureCollection") {
|
|
504
|
+
return (boundary.features ?? []).flatMap((/** @type {any} */ f) =>
|
|
505
|
+
boundaryToRings(f),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (boundary.type === "Feature") return boundaryToRings(boundary.geometry);
|
|
509
|
+
if (boundary.type === "Polygon") {
|
|
510
|
+
const ring = boundary.coordinates?.[0];
|
|
511
|
+
return ring && ring.length >= 3 ? [ring] : [];
|
|
512
|
+
}
|
|
513
|
+
if (boundary.type === "MultiPolygon") {
|
|
514
|
+
return (boundary.coordinates ?? [])
|
|
515
|
+
.map((/** @type {any} */ poly) => poly?.[0])
|
|
516
|
+
.filter((/** @type {any} */ r) => r && r.length >= 3);
|
|
517
|
+
}
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Ray-casting point-in-polygon over WGS84 rings (outer rings only — holes
|
|
523
|
+
* are out of scope for the boundary gate; the BFF hard-gate is authoritative).
|
|
524
|
+
*
|
|
525
|
+
* @param {[number, number]} lonLat
|
|
526
|
+
* @param {number[][][]} rings — from `boundaryToRings`
|
|
527
|
+
* @returns {boolean} true when the point is inside ANY ring
|
|
528
|
+
*/
|
|
529
|
+
export function pointInRings(lonLat, rings) {
|
|
530
|
+
const [x, y] = lonLat;
|
|
531
|
+
for (const ring of rings) {
|
|
532
|
+
let inside = false;
|
|
533
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
534
|
+
const [xi, yi] = ring[i];
|
|
535
|
+
const [xj, yj] = ring[j];
|
|
536
|
+
const intersects =
|
|
537
|
+
yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
538
|
+
if (intersects) inside = !inside;
|
|
539
|
+
}
|
|
540
|
+
if (inside) return true;
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Builds the boundary overlay layer (dashed outline, faint fill — distinct
|
|
547
|
+
* from the selection polygon so "the edge of the allowed area" reads as
|
|
548
|
+
* context, not content). Token-driven: --map-boundary-*.
|
|
549
|
+
*
|
|
550
|
+
* @param {number[][][]} rings
|
|
551
|
+
* @param {Element} el — token read context
|
|
552
|
+
* @returns {Promise<import('ol/layer/Vector.js').default | null>}
|
|
553
|
+
*/
|
|
554
|
+
export async function createBoundaryLayer(rings, el) {
|
|
555
|
+
if (!rings.length) return null;
|
|
556
|
+
const [
|
|
557
|
+
{ default: VectorLayer },
|
|
558
|
+
{ default: VectorSource },
|
|
559
|
+
{ default: Feature },
|
|
560
|
+
{ default: Polygon },
|
|
561
|
+
{ default: Style },
|
|
562
|
+
{ default: Fill },
|
|
563
|
+
{ default: Stroke },
|
|
564
|
+
{ fromLonLat },
|
|
565
|
+
] = await Promise.all([
|
|
566
|
+
import("ol/layer/Vector.js"),
|
|
567
|
+
import("ol/source/Vector.js"),
|
|
568
|
+
import("ol/Feature.js"),
|
|
569
|
+
import("ol/geom/Polygon.js"),
|
|
570
|
+
import("ol/style/Style.js"),
|
|
571
|
+
import("ol/style/Fill.js"),
|
|
572
|
+
import("ol/style/Stroke.js"),
|
|
573
|
+
import("ol/proj.js"),
|
|
574
|
+
]);
|
|
575
|
+
|
|
576
|
+
const dash = cssPx(el, "--map-boundary-dash", 8);
|
|
577
|
+
const style = new Style({
|
|
578
|
+
fill: new Fill({
|
|
579
|
+
color: cssVar(el, "--map-boundary-fill", "rgba(255,107,53,0.06)"),
|
|
580
|
+
}),
|
|
581
|
+
stroke: new Stroke({
|
|
582
|
+
color: cssVar(el, "--map-boundary-stroke", "#ff6b35"),
|
|
583
|
+
width: cssPx(el, "--map-boundary-stroke-width", 2),
|
|
584
|
+
lineDash: [dash, dash * 0.75],
|
|
585
|
+
}),
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const features = rings.map(
|
|
589
|
+
(ring) =>
|
|
590
|
+
new Feature({
|
|
591
|
+
geometry: new Polygon([ring.map((c) => fromLonLat(c))]),
|
|
592
|
+
}),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
return new VectorLayer({
|
|
596
|
+
source: new VectorSource({ features }),
|
|
597
|
+
style,
|
|
598
|
+
});
|
|
599
|
+
}
|
package/package.json
CHANGED
package/tokens/components.css
CHANGED
|
@@ -797,6 +797,12 @@
|
|
|
797
797
|
--map-polygon-stroke: var(--color-accent);
|
|
798
798
|
--map-polygon-stroke-width: var(--border-width-thick);
|
|
799
799
|
|
|
800
|
+
/* Boundary overlay (#187 — tenant geofence context, not content) */
|
|
801
|
+
--map-boundary-fill: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
|
802
|
+
--map-boundary-stroke: var(--map-polygon-stroke);
|
|
803
|
+
--map-boundary-stroke-width: var(--map-polygon-stroke-width);
|
|
804
|
+
--map-boundary-dash: var(--space-sm);
|
|
805
|
+
|
|
800
806
|
/* Heatmap (accent-derived sequential ramp — follows theme) */
|
|
801
807
|
--map-heatmap-stop-1: var(--color-accent-subtle);
|
|
802
808
|
--map-heatmap-stop-2: color-mix(
|