@artsdatabanken/nbic-map-component 0.3.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/dist/index.js ADDED
@@ -0,0 +1,3711 @@
1
+ import {
2
+ __export
3
+ } from "./chunk-MLKGABMK.js";
4
+
5
+ // src/core/projections.ts
6
+ import proj4 from "proj4";
7
+ import { register } from "ol/proj/proj4";
8
+ import { get as getProjection } from "ol/proj";
9
+ proj4.defs("EPSG:25833", "+proj=utm +zone=33 +ellps=GRS80 +units=m +no_defs +type=crs");
10
+ proj4.defs("EPSG:25832", "+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs +type=crs");
11
+ proj4.defs("EPSG:25835", "+proj=utm +zone=35 +ellps=GRS80 +units=m +no_defs +type=crs");
12
+ proj4.defs("EPSG:32632", "+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs +type=crs");
13
+ proj4.defs("EPSG:32633", "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs +type=crs");
14
+ register(proj4);
15
+ var extent32 = [-2e6, 35e5, 3545984, 9045984];
16
+ var extent33 = [-25e5, 35e5, 3045984, 9045984];
17
+ var extent35 = [-35e5, 35e5, 2045984, 9045984];
18
+ var extentGeographic = [-180, -90, 180, 90];
19
+ var extentGoogle = [
20
+ -2003750834e-2,
21
+ -2003750834e-2,
22
+ 2003750834e-2,
23
+ 2003750834e-2
24
+ ];
25
+ var mapBounds = [
26
+ {
27
+ epsg: "EPSG:25832",
28
+ extent: extent32
29
+ },
30
+ {
31
+ epsg: "EPSG:25833",
32
+ extent: extent33
33
+ },
34
+ {
35
+ epsg: "EPSG:25835",
36
+ extent: extent35
37
+ },
38
+ {
39
+ epsg: "EPSG:32632",
40
+ extent: extent32
41
+ },
42
+ {
43
+ epsg: "EPSG:32633",
44
+ extent: extent33
45
+ },
46
+ {
47
+ epsg: "EPSG:32635",
48
+ extent: extent35
49
+ },
50
+ {
51
+ epsg: "EPSG:4326",
52
+ extent: extentGeographic
53
+ },
54
+ {
55
+ epsg: "EPSG:3857",
56
+ extent: extentGoogle
57
+ },
58
+ {
59
+ epsg: "EPSG:900913",
60
+ extent: extentGoogle
61
+ }
62
+ ];
63
+
64
+ // src/core/state/store.ts
65
+ function createEmitter() {
66
+ const listeners = {};
67
+ return {
68
+ on(type, cb) {
69
+ (listeners[type] ??= []).push(cb);
70
+ return () => this.off(type, cb);
71
+ },
72
+ once(type, cb) {
73
+ const wrapper = (payload) => {
74
+ cb(payload);
75
+ this.off(type, wrapper);
76
+ };
77
+ this.on(type, wrapper);
78
+ },
79
+ off(type, cb) {
80
+ listeners[type] = (listeners[type] ?? []).filter((fn) => fn !== cb);
81
+ },
82
+ emit(type, payload) {
83
+ (listeners[type] ?? []).forEach((fn) => fn(payload));
84
+ }
85
+ };
86
+ }
87
+
88
+ // src/core/ol/OlMapEngine.ts
89
+ import OlMap from "ol/Map";
90
+ import { View } from "ol";
91
+
92
+ // src/core/ol/adapters/layers.ts
93
+ import VectorSource3 from "ol/source/Vector";
94
+ import VectorLayer from "ol/layer/Vector";
95
+ import TileLayer from "ol/layer/Tile";
96
+ import TileSource from "ol/source/Tile";
97
+
98
+ // src/core/ol/adapters/sources.ts
99
+ import OSM from "ol/source/OSM";
100
+ import XYZ from "ol/source/XYZ";
101
+ import WMTS from "ol/source/WMTS";
102
+ import VectorSource2 from "ol/source/Vector";
103
+ import GeoJSON2 from "ol/format/GeoJSON";
104
+
105
+ // src/core/ol/adapters/wmts-grid.ts
106
+ import WMTSTileGrid from "ol/tilegrid/WMTS";
107
+ import { getTopLeft, getWidth } from "ol/extent";
108
+ function makeGridFromExtent(extent, tileSize = 256, levels = 19) {
109
+ const size = getWidth(extent) / tileSize;
110
+ const resolutions = Array.from({ length: levels }, (_, z2) => size / Math.pow(2, z2));
111
+ const matrixIds = Array.from({ length: levels }, (_, z2) => z2.toString());
112
+ return new WMTSTileGrid({
113
+ origin: getTopLeft(extent),
114
+ resolutions,
115
+ matrixIds,
116
+ tileSize,
117
+ extent
118
+ });
119
+ }
120
+
121
+ // src/core/ol/adapters/wfs-loader.ts
122
+ import { bbox as bboxStrategy } from "ol/loadingstrategy";
123
+ import VectorSource from "ol/source/Vector";
124
+ import GeoJSON from "ol/format/GeoJSON";
125
+ import GML32 from "ol/format/GML32";
126
+ import GML3 from "ol/format/GML3";
127
+ import { get as getProjection2 } from "ol/proj";
128
+ import { getWidth as getWidth2 } from "ol/extent";
129
+ function zoomFromResolution(resolution, projectionLike) {
130
+ const proj = getProjection2(projectionLike) || void 0;
131
+ const baseRes = proj?.getExtent() ? getWidth2(proj.getExtent()) / 256 : 156543.03392804097;
132
+ const res = resolution > 0 ? resolution : baseRes;
133
+ const zoom = Math.log2(baseRes / res);
134
+ return Number.isFinite(zoom) ? zoom : 0;
135
+ }
136
+ function zoomGatedBboxStrategy(minZoom) {
137
+ return (extent, resolution, projection) => {
138
+ if (typeof minZoom === "number") {
139
+ const z2 = zoomFromResolution(resolution, projection);
140
+ if (z2 < minZoom) return [];
141
+ }
142
+ return bboxStrategy(extent, resolution);
143
+ };
144
+ }
145
+ function buildGetFeatureUrl(o, extent, srsName, format) {
146
+ const version = o.version ?? "2.0.0";
147
+ const is20 = version === "2.0.0";
148
+ const qp = new URLSearchParams({
149
+ service: "WFS",
150
+ request: "GetFeature",
151
+ version,
152
+ [is20 ? "typenames" : "typename"]: o.typeName,
153
+ srsName
154
+ });
155
+ qp.set("bbox", `${extent.join(",")},${srsName}`);
156
+ if (format === "json") {
157
+ qp.set("outputFormat", o.outputFormat ?? "application/json");
158
+ } else {
159
+ qp.set("outputFormat", o.outputFormat ?? "text/xml; subtype=gml/3.2.1");
160
+ }
161
+ if (is20) {
162
+ const count = o.maxFeatures ?? o.pageSizeDefault ?? 2e3;
163
+ qp.set("count", String(count));
164
+ } else if (o.maxFeatures) {
165
+ qp.set("maxFeatures", String(o.maxFeatures));
166
+ }
167
+ if (o.params) Object.entries(o.params).forEach(([k, v]) => qp.set(k, String(v)));
168
+ const sep = o.url.includes("?") ? "&" : "?";
169
+ return `${o.url}${sep}${qp.toString()}`;
170
+ }
171
+ function normalizeGmlXml(xmlText) {
172
+ const parser = new DOMParser();
173
+ const xmlDoc = parser.parseFromString(xmlText, "text/xml");
174
+ const serializer = new XMLSerializer();
175
+ const normalized = serializer.serializeToString(xmlDoc).replace(/srsdimension/g, "srsDimension").replace(/wfs:member/g, "wfs:featureMember");
176
+ return parser.parseFromString(normalized, "text/xml");
177
+ }
178
+ var dftCache = /* @__PURE__ */ new Map();
179
+ async function describeFeatureTypeOnce(baseUrl, typeName, version = "2.0.0") {
180
+ const cacheKey = `${baseUrl}::${version}::${typeName}`;
181
+ const existing = dftCache.get(cacheKey);
182
+ if (existing) return existing;
183
+ const is20 = version === "2.0.0";
184
+ const qp = new URLSearchParams({
185
+ service: "WFS",
186
+ request: "DescribeFeatureType",
187
+ version,
188
+ [is20 ? "typenames" : "typename"]: typeName,
189
+ outputFormat: "application/gml+xml; version=3.2"
190
+ });
191
+ const sep = baseUrl.includes("?") ? "&" : "?";
192
+ const url = `${baseUrl}${sep}${qp.toString()}`;
193
+ const res = await fetch(url);
194
+ const text = await res.text();
195
+ const doc = new DOMParser().parseFromString(text, "application/xml");
196
+ const schemaEl = doc.querySelector("schema, xs\\:schema");
197
+ const featureNS = schemaEl?.getAttribute("targetNamespace") || "http://mapserver.gis.umn.edu/mapserver";
198
+ const local = typeName.includes(":") ? typeName.split(":")[1] : typeName;
199
+ let geometryName;
200
+ const elDef = Array.from(doc.querySelectorAll("element, xs\\:element")).find((e) => e.getAttribute("name") === local) || null;
201
+ const typeRef = elDef?.getAttribute("type") || null;
202
+ if (typeRef) {
203
+ const localType = typeRef.includes(":") ? typeRef.split(":")[1] : typeRef;
204
+ const complex = Array.from(doc.querySelectorAll("complexType, xs\\:complexType")).find((ct) => ct.getAttribute("name") === localType);
205
+ if (complex) {
206
+ const propEls = Array.from(complex.querySelectorAll("element, xs\\:element"));
207
+ const geomProp = propEls.find((pe) => {
208
+ const t = pe.getAttribute("type") ?? "";
209
+ return /gml:.*(Geometry|Point|LineString|Curve|Surface|Polygon|Multi)/i.test(t);
210
+ });
211
+ if (geomProp) geometryName = geomProp.getAttribute("name") || void 0;
212
+ }
213
+ }
214
+ const info = { featureNS: featureNS ?? "", featureType: local ?? "", geometryName };
215
+ dftCache.set(cacheKey, info);
216
+ return info;
217
+ }
218
+ function createWfsVectorSource(opts, viewProjection) {
219
+ const preferJson = !opts.outputFormat || /json/i.test(opts.outputFormat);
220
+ const source = new VectorSource({
221
+ // Gate at the strategy level so OL re-calls the loader automatically when zoom crosses the gate.
222
+ strategy: zoomGatedBboxStrategy(opts.minZoomToLoad)
223
+ });
224
+ source.setLoader(async (extent, _resolution, proj) => {
225
+ const srs = opts.srsName ?? String(proj ?? viewProjection);
226
+ const tryOnce = async (fmt) => {
227
+ const url = buildGetFeatureUrl(opts, extent, srs, fmt);
228
+ const resp = await fetch(url, { headers: opts.headers });
229
+ const text = await resp.text();
230
+ const ct = resp.headers.get("content-type") ?? "";
231
+ return { ok: resp.ok, text, ct, url };
232
+ };
233
+ try {
234
+ if (preferJson) {
235
+ const r = await tryOnce("json");
236
+ const looksXml = r.text.trim().startsWith("<");
237
+ const notJsonCT = !/json/i.test(r.ct);
238
+ if (r.ok && !looksXml && !notJsonCT) {
239
+ const features2 = new GeoJSON({
240
+ dataProjection: srs,
241
+ featureProjection: String(proj ?? viewProjection)
242
+ }).readFeatures(r.text);
243
+ source.addFeatures(features2);
244
+ return;
245
+ }
246
+ }
247
+ const r2 = await tryOnce("gml");
248
+ if (!r2.ok) {
249
+ throw new Error(`WFS error ${r2.ct} from ${r2.url}: ${r2.text.slice(0, 200)}`);
250
+ }
251
+ const xmlDoc = normalizeGmlXml(r2.text);
252
+ const version = opts.version ?? "2.0.0";
253
+ const dft = {
254
+ featureNS: opts.featureNS,
255
+ featureType: opts.featureType,
256
+ geometryName: opts.geometryName
257
+ };
258
+ if (!dft.featureNS || !dft.featureType) {
259
+ const info = await describeFeatureTypeOnce(opts.url, opts.typeName, version);
260
+ dft.featureNS = dft.featureNS ?? info.featureNS;
261
+ dft.featureType = dft.featureType ?? info.featureType;
262
+ dft.geometryName = dft.geometryName ?? info.geometryName;
263
+ }
264
+ const isGml32 = /gml\/3\.2/i.test(r2.ct) || /version=3\.2/i.test(r2.text) || r2.text.includes("http://www.opengis.net/gml/3.2");
265
+ const gml = isGml32 ? new GML32({ featureNS: dft.featureNS, featureType: dft.featureType, srsName: srs }) : new GML3({ featureNS: dft.featureNS, featureType: dft.featureType, srsName: srs });
266
+ const tbl = gml["FEATURE_COLLECTION_PARSERS"];
267
+ if (tbl && tbl["http://www.opengis.net/gml/3.2"]) {
268
+ tbl["http://www.opengis.net/gml"] = tbl["http://www.opengis.net/gml/3.2"];
269
+ tbl["http://www.opengis.net/wfs/2.0"] = tbl["http://www.opengis.net/gml/3.2"];
270
+ }
271
+ let features = gml.readFeatures(xmlDoc, {
272
+ dataProjection: srs,
273
+ featureProjection: String(proj ?? viewProjection),
274
+ ...dft.geometryName ? { geometryName: dft.geometryName } : {}
275
+ });
276
+ if (!features.length) {
277
+ const serialized = new XMLSerializer().serializeToString(xmlDoc);
278
+ features = gml.readFeatures(serialized, {
279
+ dataProjection: srs,
280
+ featureProjection: String(proj ?? viewProjection),
281
+ ...dft.geometryName ? { geometryName: dft.geometryName } : {}
282
+ });
283
+ }
284
+ if (features.length) {
285
+ source.addFeatures(features);
286
+ } else {
287
+ console.warn("[WFS] Parsed 0 features", {
288
+ url: r2.url,
289
+ featureNS: dft.featureNS,
290
+ featureType: dft.featureType,
291
+ geometryName: dft.geometryName,
292
+ isGml32
293
+ });
294
+ }
295
+ } catch (err) {
296
+ console.error("WFS loader error", err);
297
+ }
298
+ });
299
+ return source;
300
+ }
301
+
302
+ // src/core/ol/adapters/sources.ts
303
+ import { get as getProj } from "ol/proj";
304
+ import WMTSTileGrid2 from "ol/tilegrid/WMTS";
305
+ import { getTopLeft as getTopLeft2, getWidth as getWidth3 } from "ol/extent";
306
+
307
+ // src/core/ol/adapters/wmts-presets.ts
308
+ function wmtsPreset(key) {
309
+ const matrixXMin = -5118180;
310
+ const matrixYMin = 7224500;
311
+ const matrixWidth = 5545984;
312
+ const matrixHeight = 2772992;
313
+ const matrixExtent = [
314
+ matrixXMin,
315
+ matrixYMin,
316
+ matrixXMin + matrixWidth,
317
+ matrixYMin + matrixHeight
318
+ ];
319
+ switch (key) {
320
+ case "npolar-svalbard-32633": {
321
+ return {
322
+ layerExtent: [369976, 8221306, 878234, 9010718],
323
+ matrixExtent,
324
+ matrixIds: Array.from({ length: 18 }, (_, z2) => z2.toString())
325
+ };
326
+ }
327
+ case "npolar-janmayen-25833": {
328
+ return {
329
+ layerExtent: [-393783, 7978220, 276963, 8084965],
330
+ matrixExtent,
331
+ matrixIds: Array.from({ length: 18 }, (_, z2) => z2.toString())
332
+ };
333
+ }
334
+ }
335
+ }
336
+
337
+ // src/core/ol/adapters/sources.ts
338
+ function resolveOlSource(input, resolveRef = (id) => {
339
+ throw new Error(`Source ref '${id}' cannot be resolved (no registry wired).`);
340
+ }) {
341
+ if ("ref" in input) {
342
+ return resolveRef(input.ref);
343
+ }
344
+ return toOlSource(input);
345
+ }
346
+ function toOlSource(def) {
347
+ switch (def.type) {
348
+ case "osm":
349
+ return new OSM();
350
+ case "xyz": {
351
+ const opts = def.options;
352
+ return new XYZ({
353
+ url: opts.url,
354
+ minZoom: opts.minZoom,
355
+ maxZoom: opts.maxZoom,
356
+ attributions: opts.attributions,
357
+ tileSize: opts.tileSize
358
+ });
359
+ }
360
+ case "wmts": {
361
+ let buildGridFromPreset2 = function(p, tileSize2, levels2) {
362
+ const matrixExtent = p.matrixExtent ?? p.extent;
363
+ const layerExtent = p.layerExtent ?? p.extent;
364
+ return new WMTSTileGrid2({
365
+ origin: getTopLeft2(matrixExtent),
366
+ resolutions: Array.from(
367
+ { length: levels2 },
368
+ (_, z2) => getWidth3(matrixExtent) / tileSize2 / Math.pow(2, z2)
369
+ ),
370
+ matrixIds: p.matrixIds,
371
+ tileSize: tileSize2,
372
+ extent: layerExtent
373
+ // clip to visible data
374
+ });
375
+ };
376
+ var buildGridFromPreset = buildGridFromPreset2;
377
+ const o = def.options;
378
+ const tileSize = o.tileSize ?? 256;
379
+ const levels = o.levels ?? 18;
380
+ let tileGrid;
381
+ const wantsDefault028 = o.matrixSetId === "default028mm" || o.matrixSet === "default028mm";
382
+ if (!tileGrid && wantsDefault028) {
383
+ const presetKeyByProj = {
384
+ "EPSG:32633": "npolar-svalbard-32633",
385
+ "EPSG:25833": "npolar-janmayen-25833"
386
+ };
387
+ const key = o.projection ? presetKeyByProj[o.projection] : void 0;
388
+ if (key) {
389
+ const p = wmtsPreset(key);
390
+ tileGrid = buildGridFromPreset2(p, tileSize, levels);
391
+ }
392
+ }
393
+ if (!tileGrid) {
394
+ const proj = o.projection ? getProj(o.projection) : void 0;
395
+ const extent = o.extent ?? proj?.getExtent() ?? (o.projection ? mapBounds.find((m) => m.epsg === o.projection)?.extent : void 0);
396
+ if (!extent) {
397
+ throw new Error(
398
+ `WMTS ${o.layer}: missing projection extent; set options.extent or ensure projection is registered with an extent.`
399
+ );
400
+ }
401
+ tileGrid = makeGridFromExtent(extent, tileSize, levels);
402
+ }
403
+ const isRest = /\/1\.0\.0\/?$/i.test(o.url) || /\/wmts\/1\.0\.0\//i.test(o.url);
404
+ const baseUrl = o.url.replace(/\/?$/, "/");
405
+ const src = new WMTS({
406
+ layer: o.layer,
407
+ matrixSet: o.matrixSetId ?? o.matrixSet ?? "EPSG:3857",
408
+ format: o.format ?? "image/png",
409
+ style: o.style ?? "default",
410
+ tileGrid,
411
+ wrapX: o.wrapX ?? false,
412
+ // UTM usually false
413
+ transition: 0,
414
+ attributions: o.attribution,
415
+ ...o.projection ? { projection: getProj(o.projection) } : {},
416
+ ...isRest ? { urls: [baseUrl], requestEncoding: "REST" } : { url: o.url, requestEncoding: "KVP" },
417
+ crossOrigin: "anonymous",
418
+ tileLoadFunction: o.tileLoadFunction ? (tile, src2) => o.tileLoadFunction(tile, src2) : void 0
419
+ });
420
+ if (o.urlParamOverrides && Object.keys(o.urlParamOverrides).length) {
421
+ const orig = src.getTileUrlFunction();
422
+ src.setTileUrlFunction((coord, pr, pj) => {
423
+ const u = new URL(String(orig(coord, pr, pj)), window.location.origin);
424
+ for (const [k, v] of Object.entries(o.urlParamOverrides)) u.searchParams.set(k, v);
425
+ return u.toString();
426
+ });
427
+ }
428
+ return src;
429
+ }
430
+ case "geojson": {
431
+ const opts = def.options;
432
+ if (opts.text && opts.text.trim().length) {
433
+ const fmt = new GeoJSON2({ dataProjection: opts.dataProjection, featureProjection: opts.featureProjection });
434
+ const features = fmt.readFeatures(opts.text);
435
+ const src = new VectorSource2();
436
+ if (features.length) src.addFeatures(features);
437
+ return src;
438
+ }
439
+ if (opts.url) {
440
+ return new VectorSource2({
441
+ url: opts.url,
442
+ format: new GeoJSON2({ dataProjection: opts.dataProjection, featureProjection: opts.featureProjection })
443
+ });
444
+ }
445
+ throw new Error("geojson source: provide either options.url or options.text");
446
+ }
447
+ case "wfs": {
448
+ const o = def.options;
449
+ const viewProj = o.srsName ?? "EPSG:3857";
450
+ return createWfsVectorSource(o, viewProj);
451
+ }
452
+ case "memory": {
453
+ return new VectorSource2();
454
+ }
455
+ default: {
456
+ const type = def.type;
457
+ throw new Error(`Unknown source type: ${type}`);
458
+ }
459
+ }
460
+ }
461
+
462
+ // src/core/ol/adapters/styles.ts
463
+ import { Style, Fill, Stroke, Circle as CircleStyle, Text, Icon, RegularShape } from "ol/style";
464
+ import { Circle as CircleGeom } from "ol/geom";
465
+ import Point from "ol/geom/Point";
466
+ import * as olColor from "ol/color";
467
+
468
+ // src/core/ol/adapters/gradient.ts
469
+ import { asArray, asString } from "ol/color";
470
+ function makeGradientRenderer(color, outerScale = 1.4, stops = [[0, 0], [0.6, 0.2], [1, 0.8]], strokeWidth = 1) {
471
+ const withAlpha = (c, a) => {
472
+ const arr = asArray(c);
473
+ arr[3] = a;
474
+ return asString(arr);
475
+ };
476
+ const rf = (pixelCoords, state) => {
477
+ const coords = pixelCoords;
478
+ if (!coords || coords.length < 2) return;
479
+ const center = coords[0];
480
+ const edge = coords[1];
481
+ if (!center || !edge || center.length < 2 || edge.length < 2) return;
482
+ const x = center[0];
483
+ const y = center[1];
484
+ const x1 = edge[0];
485
+ const y1 = edge[1];
486
+ const ctx = state.context;
487
+ const dx = x1 - x, dy = y1 - y;
488
+ const radius = Math.sqrt(dx * dx + dy * dy);
489
+ const innerRadius = 0;
490
+ const outerRadius = radius * outerScale;
491
+ const gradient = ctx.createRadialGradient(x, y, innerRadius, x, y, outerRadius);
492
+ for (const [stop, alpha] of stops) {
493
+ gradient.addColorStop(stop, withAlpha(color, alpha));
494
+ }
495
+ ctx.beginPath();
496
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, true);
497
+ ctx.fillStyle = gradient;
498
+ ctx.fill();
499
+ ctx.beginPath();
500
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, true);
501
+ ctx.lineWidth = strokeWidth;
502
+ ctx.strokeStyle = withAlpha(color, 1);
503
+ ctx.stroke();
504
+ };
505
+ return rf;
506
+ }
507
+
508
+ // src/core/ol/adapters/styles.ts
509
+ var fallback = new Style({
510
+ fill: new Fill({ color: "rgba(0,0,0,0.1)" }),
511
+ stroke: new Stroke({ color: "#666", width: 1 })
512
+ });
513
+ var thumbCache = /* @__PURE__ */ new Map();
514
+ function getInnerFeatureFromCluster(feature) {
515
+ const members = feature.get?.("features");
516
+ if (members && members.length === 1) {
517
+ const m0 = members[0];
518
+ return m0 ?? feature;
519
+ }
520
+ return feature;
521
+ }
522
+ function getClusterSize(feature) {
523
+ const members = feature.get?.("features");
524
+ return members?.length ?? 0;
525
+ }
526
+ function makeThumbnailMarkerStyle(thumbnail, opts) {
527
+ const cached = thumbCache.get(thumbnail);
528
+ if (cached) return cached;
529
+ const styles = [
530
+ new Style({
531
+ image: new RegularShape({
532
+ stroke: new Stroke({ color: opts.frameBorderColor, width: opts.frameBorderWidth }),
533
+ fill: new Fill({ color: opts.frameFillColor }),
534
+ points: 4,
535
+ radius: opts.frameRadius,
536
+ angle: Math.PI / 4,
537
+ displacement: opts.frameDisplacement
538
+ })
539
+ }),
540
+ new Style({
541
+ image: new RegularShape({
542
+ fill: new Fill({ color: opts.pinFillColor }),
543
+ points: 3,
544
+ radius: opts.pinRadius,
545
+ rotation: opts.pinRotation,
546
+ angle: 0
547
+ })
548
+ }),
549
+ new Style({
550
+ image: new Icon({
551
+ anchor: [0.52, 1],
552
+ anchorXUnits: "fraction",
553
+ anchorYUnits: "fraction",
554
+ src: thumbnail,
555
+ opacity: 1,
556
+ scale: opts.imageScale,
557
+ offsetOrigin: "top-left",
558
+ offset: [0, 0],
559
+ size: opts.imageSize,
560
+ crossOrigin: opts.crossOrigin
561
+ })
562
+ })
563
+ ];
564
+ thumbCache.set(thumbnail, styles);
565
+ return styles;
566
+ }
567
+ function toOlStyle(def, label) {
568
+ if ("ref" in def) {
569
+ throw new Error(`Style ref '${def.ref}' cannot be resolved (no StyleRegistry wired).`);
570
+ }
571
+ if (def.type === "raw") {
572
+ const inst = def.options?.instance;
573
+ return inst ?? fallback;
574
+ }
575
+ if (def.type !== "simple") return fallback;
576
+ const opts = def.options ?? {};
577
+ if (opts.thumbnailMarker) {
578
+ const t = opts.thumbnailMarker;
579
+ const property = t.property ?? "thumbnail";
580
+ const imageSize = t.imageSize ?? [100, 100];
581
+ const imageScale = t.imageScale ?? 0.26;
582
+ const frameRadius = t.frameRadius ?? 22;
583
+ const frameBorderWidth = t.frameBorderWidth ?? 3;
584
+ const frameBorderColor = t.frameBorderColor ?? "rgba(255,255,255,1)";
585
+ const frameFillColor = t.frameFillColor ?? "#ffffff";
586
+ const frameDisplacement = t.frameDisplacement ?? [-1, 13];
587
+ const pinRadius = t.pinRadius ?? 10;
588
+ const pinFillColor = t.pinFillColor ?? "#ffffff";
589
+ const pinRotation = t.pinRotation ?? Math.PI / 3;
590
+ const crossOrigin = t.crossOrigin ?? "anonymous";
591
+ const fill = new Fill({ color: opts.fillColor ?? "rgba(0,0,0,0.2)" });
592
+ const stroke = new Stroke({ color: opts.strokeColor ?? "#333", width: opts.strokeWidth ?? 1 });
593
+ const image = opts.circle ? new CircleStyle({
594
+ radius: opts.circle.radius ?? 4,
595
+ fill: new Fill({ color: opts.circle.fillColor ?? "#3388ff" }),
596
+ stroke: new Stroke({ color: opts.circle.strokeColor ?? "#222", width: opts.circle.strokeWidth ?? 1 })
597
+ }) : void 0;
598
+ return (feature) => {
599
+ const size = getClusterSize(feature);
600
+ if (size > 1) return void 0;
601
+ const inner = getInnerFeatureFromCluster(feature);
602
+ const thumb = inner.get?.(property);
603
+ const url = typeof thumb === "string" ? thumb : "";
604
+ if (!url) {
605
+ return new Style({ fill, stroke, image, text });
606
+ }
607
+ return makeThumbnailMarkerStyle(url, {
608
+ frameRadius,
609
+ frameBorderWidth,
610
+ frameBorderColor,
611
+ frameFillColor,
612
+ frameDisplacement,
613
+ pinRadius,
614
+ pinFillColor,
615
+ pinRotation,
616
+ imageSize,
617
+ imageScale,
618
+ crossOrigin
619
+ });
620
+ };
621
+ }
622
+ const baseFill = new Fill({ color: opts.fillColor ?? "rgba(0,0,0,0.2)" });
623
+ const baseStroke = new Stroke({ color: opts.strokeColor ?? "#333", width: opts.strokeWidth ?? 1 });
624
+ const text = opts.text || label !== void 0 ? new Text({
625
+ text: opts.text?.label ?? (label != null ? String(label) : ""),
626
+ font: opts.text?.font ?? "14px sans-serif",
627
+ fill: new Fill({ color: opts.text?.fillColor ?? "#fff" }),
628
+ stroke: opts.text?.strokeColor ? new Stroke({ color: opts.text.strokeColor, width: opts.text.strokeWidth ?? 2 }) : void 0,
629
+ offsetX: opts.text?.offsetX ?? 0,
630
+ offsetY: opts.text?.offsetY ?? 0
631
+ }) : void 0;
632
+ if (opts.icon) {
633
+ const i = opts.icon;
634
+ return new Style({
635
+ image: new Icon({
636
+ src: i.src,
637
+ scale: i.scale ?? 1,
638
+ anchor: i.anchor ?? [0.5, 1],
639
+ anchorXUnits: i.anchorXUnits ?? "fraction",
640
+ anchorYUnits: i.anchorYUnits ?? "fraction",
641
+ size: i.size,
642
+ rotation: i.rotation ?? 0,
643
+ opacity: i.opacity ?? 1,
644
+ color: i.color
645
+ }),
646
+ text
647
+ });
648
+ }
649
+ if (opts.circle && opts.circle.units !== "map") {
650
+ const c = opts.circle;
651
+ const image = new CircleStyle({
652
+ radius: c.radius ?? 4,
653
+ fill: new Fill({ color: c.fillColor ?? "#3388ff" }),
654
+ stroke: new Stroke({ color: c.strokeColor ?? "#222", width: c.strokeWidth ?? 1 })
655
+ });
656
+ return new Style({ image, text, fill: baseFill, stroke: baseStroke });
657
+ }
658
+ if (opts.circle?.units === "map") {
659
+ const c = opts.circle;
660
+ const circleStroke = new Stroke({
661
+ color: c.strokeColor ?? opts.strokeColor ?? "#333",
662
+ width: c.strokeWidth ?? opts.strokeWidth ?? 1
663
+ });
664
+ const circleFill = new Fill({
665
+ color: c.fillColor ?? opts.fillColor ?? "rgba(0,0,0,0.2)"
666
+ });
667
+ return (feature) => {
668
+ const geom = feature.getGeometry();
669
+ if (!(geom instanceof Point)) {
670
+ return new Style({ stroke: baseStroke, fill: baseFill, text });
671
+ }
672
+ let radius = 0;
673
+ if (c.radiusProperty) {
674
+ const v = feature.get(c.radiusProperty);
675
+ const n = Number(v);
676
+ if (Number.isFinite(n)) radius = n;
677
+ }
678
+ if (!(radius > 0)) radius = c.radius ?? 0;
679
+ if (!(radius > 0)) return new Style({ stroke: baseStroke, fill: baseFill, text });
680
+ const circleGeom = new CircleGeom(geom.getCoordinates(), radius);
681
+ if (c.gradient) {
682
+ const renderer = makeGradientRenderer(
683
+ olColor.asArray(c.gradient.color),
684
+ c.gradient.outerScale ?? 1.4,
685
+ c.gradient.stops ?? [[0, 0], [0.6, 0.2], [1, 0.8]],
686
+ c.strokeWidth ?? 1
687
+ );
688
+ const s2 = new Style({ renderer, text });
689
+ s2.setGeometry(circleGeom);
690
+ return s2;
691
+ }
692
+ const s = new Style({ stroke: circleStroke, fill: circleFill, text });
693
+ s.setGeometry(circleGeom);
694
+ return s;
695
+ };
696
+ }
697
+ return new Style({ fill: baseFill, stroke: baseStroke, text });
698
+ }
699
+
700
+ // src/core/ol/adapters/draw-style.ts
701
+ import { Style as Style2, Icon as Icon2, Circle as CircleStyle2, Fill as Fill2, Stroke as Stroke2, Text as Text2 } from "ol/style";
702
+ function makeDrawStyle(opts) {
703
+ const stroke = new Stroke2({
704
+ color: opts?.strokeColor ?? "#1f6feb",
705
+ width: opts?.strokeWidth ?? 2
706
+ });
707
+ const fill = new Fill2({
708
+ color: opts?.fillColor ?? "rgba(31,111,235,0.15)"
709
+ });
710
+ const image = new CircleStyle2({
711
+ radius: opts?.pointRadius ?? 6,
712
+ fill,
713
+ stroke
714
+ });
715
+ const s = new Style2();
716
+ s.setImage(image);
717
+ s.setFill(fill);
718
+ s.setStroke(stroke);
719
+ if (opts?.text) {
720
+ s.setText(
721
+ new Text2({
722
+ text: opts.text.label ?? "",
723
+ font: opts.text.font ?? "14px sans-serif",
724
+ fill: new Fill2({ color: opts.text.fillColor ?? "#111" }),
725
+ stroke: opts.text.strokeColor ? new Stroke2({
726
+ color: opts.text.strokeColor,
727
+ width: opts.text.strokeWidth ?? 2
728
+ }) : void 0,
729
+ offsetX: opts.text.offsetX ?? 0,
730
+ offsetY: opts.text.offsetY ?? 0
731
+ })
732
+ );
733
+ }
734
+ if (opts?.icon?.src) {
735
+ const image2 = new Icon2({
736
+ src: opts.icon.src,
737
+ scale: opts.icon.scale ?? 1,
738
+ size: opts.icon.size,
739
+ anchor: opts.icon.anchor ?? [0.5, 1],
740
+ anchorXUnits: opts.icon.anchorXUnits ?? "fraction",
741
+ anchorYUnits: opts.icon.anchorYUnits ?? "fraction",
742
+ rotation: opts.icon.rotation ?? 0,
743
+ opacity: opts.icon.opacity ?? 1,
744
+ color: opts.icon.color
745
+ });
746
+ return new Style2({ image: image2, stroke, fill });
747
+ }
748
+ return s;
749
+ }
750
+ function makeUpdatedDrawStyle(opts) {
751
+ const stroke = new Stroke2({
752
+ color: opts?.strokeColor ?? "#1f6feb",
753
+ width: opts?.strokeWidth ?? 2
754
+ });
755
+ const fill = new Fill2({
756
+ color: opts?.fillColor ?? "rgba(31,111,235,0.15)"
757
+ });
758
+ const image = new CircleStyle2({
759
+ radius: opts?.pointRadius ?? 6,
760
+ fill,
761
+ stroke
762
+ });
763
+ const s = new Style2();
764
+ s.setImage(image);
765
+ s.setFill(fill);
766
+ s.setStroke(stroke);
767
+ if (opts?.text) {
768
+ s.setText(
769
+ new Text2({
770
+ text: opts.text.label ?? "",
771
+ font: opts.text.font ?? "14px sans-serif",
772
+ fill: new Fill2({ color: opts.text.fillColor ?? "#111" }),
773
+ stroke: opts.text.strokeColor ? new Stroke2({
774
+ color: opts.text.strokeColor,
775
+ width: opts.text.strokeWidth ?? 2
776
+ }) : void 0,
777
+ offsetX: opts.text.offsetX ?? 0,
778
+ offsetY: opts.text.offsetY ?? 0
779
+ })
780
+ );
781
+ }
782
+ return s;
783
+ }
784
+
785
+ // src/core/ol/adapters/layers.ts
786
+ import { createDefaultStyle } from "ol/style/Style";
787
+ import ClusterSource from "ol/source/Cluster";
788
+ import { Style as Style3, Circle as CircleStyle3, Fill as Fill3, Stroke as Stroke3, Text as Text3 } from "ol/style";
789
+ function styleFromFeature(feature, resolution) {
790
+ const nbic = typeof feature.get === "function" ? feature.get("nbic:style") : void 0;
791
+ return nbic ? makeDrawStyle(nbic) : createDefaultStyle(feature, resolution);
792
+ }
793
+ function toOlLayer(def, sourceFn = resolveOlSource, styleFn = toOlStyle) {
794
+ const src = sourceFn(def.source);
795
+ let layer;
796
+ switch (def.kind) {
797
+ case "tile":
798
+ case "raster":
799
+ if (src instanceof TileSource) {
800
+ layer = new TileLayer({ source: src, visible: def.visible ?? true, opacity: def.opacity ?? 1 });
801
+ } else {
802
+ throw new Error("Source for tile/raster layer must be a TileSource");
803
+ }
804
+ break;
805
+ case "vector":
806
+ if (src instanceof VectorSource3) {
807
+ let sumCount2 = function(members, field = "count") {
808
+ if (!members?.length) return 0;
809
+ let total = 0;
810
+ for (const m of members) {
811
+ const v = m.get?.(field);
812
+ const n = Number(v);
813
+ total += Number.isFinite(n) ? n : 1;
814
+ }
815
+ return total;
816
+ };
817
+ var sumCount = sumCount2;
818
+ const finalSource = def.cluster?.enabled ? new ClusterSource({
819
+ distance: def.cluster.distance ?? 40,
820
+ minDistance: def.cluster.minDistance,
821
+ source: src
822
+ }) : src;
823
+ const explicitLayerStyle = def.style ? styleFn(def.style) : void 0;
824
+ const explicitClusterStyle = def.cluster?.enabled && def.cluster.style ? styleFn(def.cluster.style) : void 0;
825
+ const countField = def.cluster?.countField ?? "count";
826
+ const style = (feature, resolution) => {
827
+ const members = feature.get?.("features");
828
+ if (def.cluster?.enabled && members) {
829
+ const total = sumCount2(members, countField) || members.length;
830
+ if (members.length === 1 && !def.cluster.keepSingleAsCluster) {
831
+ const inner = members[0];
832
+ if (inner && typeof explicitLayerStyle === "function") {
833
+ return explicitLayerStyle(inner, resolution);
834
+ }
835
+ if (explicitLayerStyle) {
836
+ if (typeof explicitLayerStyle === "function" && inner) {
837
+ return explicitLayerStyle(inner, resolution);
838
+ }
839
+ return typeof explicitLayerStyle === "function" ? inner ? explicitLayerStyle(inner, resolution) : void 0 : explicitLayerStyle;
840
+ }
841
+ const innerHasPerFeature = !!inner?.get && !!inner.get("nbic:style");
842
+ if (innerHasPerFeature) {
843
+ return styleFromFeature(inner, resolution);
844
+ }
845
+ if (inner) {
846
+ return styleFromFeature(inner, resolution);
847
+ }
848
+ return void 0;
849
+ }
850
+ if (typeof explicitClusterStyle === "function") {
851
+ return explicitClusterStyle(feature, resolution);
852
+ }
853
+ if (explicitClusterStyle) {
854
+ if (def.cluster.style) {
855
+ return styleFn(def.cluster.style, String(def.cluster.maxClusterPoints ? total > def.cluster.maxClusterPoints ? def.cluster.maxClusterPoints + "+" : total : total));
856
+ } else {
857
+ return explicitClusterStyle;
858
+ }
859
+ }
860
+ return new Style3({
861
+ image: new CircleStyle3({
862
+ radius: 12,
863
+ fill: new Fill3({ color: "#3399CC" }),
864
+ stroke: new Stroke3({ color: "#fff", width: 2 })
865
+ }),
866
+ text: new Text3({
867
+ text: String(def.cluster.maxClusterPoints ? total > def.cluster.maxClusterPoints ? def.cluster.maxClusterPoints + "+" : total : total),
868
+ fill: new Fill3({ color: "#fff" })
869
+ })
870
+ });
871
+ }
872
+ const hasPerFeature = typeof feature.get === "function" && !!feature.get("nbic:style");
873
+ if (hasPerFeature) {
874
+ return styleFromFeature(feature, resolution);
875
+ }
876
+ if (typeof explicitLayerStyle === "function") {
877
+ return explicitLayerStyle(feature, resolution);
878
+ }
879
+ if (explicitLayerStyle) {
880
+ return explicitLayerStyle;
881
+ }
882
+ return styleFromFeature(feature, resolution);
883
+ };
884
+ layer = new VectorLayer({
885
+ source: finalSource,
886
+ visible: def.visible !== false,
887
+ zIndex: def.zIndexPinned ? def.zIndex : void 0,
888
+ style,
889
+ // styleFn as unknown as StyleLike,
890
+ properties: { id: def.id }
891
+ });
892
+ if (def.source && typeof def.source === "object") {
893
+ const sourceDef = def.source;
894
+ if (sourceDef.type === "geojson" && sourceDef.options.dataProjection) {
895
+ layer.set("nbic:dataProjection", sourceDef.options.dataProjection);
896
+ }
897
+ }
898
+ } else {
899
+ throw new Error("Source for vector layer must be a VectorSource");
900
+ }
901
+ break;
902
+ default:
903
+ throw new Error(`Unknown layer kind: ${def.kind}`);
904
+ }
905
+ layer.set("id", def.id);
906
+ if (def.hover?.style) layer.set("nbic:hoverStyle", def.hover.style);
907
+ if (def.hover?.clusterStyle) layer.set("nbic:hoverClusterStyle", def.hover.clusterStyle);
908
+ if (def.hover?.singleClusterStyle) layer.set("nbic:hoverSingleClusterStyle", def.hover.singleClusterStyle);
909
+ if (def.hover?.hitTolerance != null) layer.set("nbic:hoverHitTol", def.hover.hitTolerance);
910
+ if (def.hover?.clusterBehavior) layer.set("nbic:hoverClusterBehavior", def.hover.clusterBehavior);
911
+ if (def.cluster?.keepSingleAsCluster) layer.set("nbic:keepSingleAsCluster", true);
912
+ if (def.hover?.cursor) layer.set("nbic:hoverCursor", def.hover.cursor);
913
+ if (def.minZoom !== void 0 && "setMinZoom" in layer) layer.setMinZoom(def.minZoom);
914
+ if (def.maxZoom !== void 0 && "setMaxZoom" in layer) layer.setMaxZoom(def.maxZoom);
915
+ if (def.zIndex !== void 0) layer.set("nbic:declaredZIndex", def.zIndex);
916
+ if (def.zIndexPinned) layer.set("nbic:zIndexPinned", true);
917
+ return layer;
918
+ }
919
+
920
+ // src/core/ol/interactions/HoverInfo.ts
921
+ import { unByKey } from "ol/Observable";
922
+ import VectorLayer2 from "ol/layer/Vector";
923
+ import VectorSource4 from "ol/source/Vector";
924
+ import Feature from "ol/Feature";
925
+ import { Style as Style4, Stroke as Stroke4, Fill as Fill4, Circle as CircleStyle4 } from "ol/style";
926
+
927
+ // src/api/events.ts
928
+ var MapEvents = {
929
+ Ready: "ready",
930
+ Destroy: "destroy",
931
+ CameraChanged: "camera:changed",
932
+ ExtentChanged: "extent:changed",
933
+ LayerAdded: "layer:added",
934
+ LayerRemoved: "layer:removed",
935
+ PointerMove: "pointer:move",
936
+ PointerClick: "pointer:click",
937
+ BaselayerChanged: "baselayer:changed",
938
+ BaseChanged: "base:changed",
939
+ HoverInfo: "hover:info",
940
+ Error: "error",
941
+ DrawStart: "draw:start",
942
+ DrawEnd: "draw:end",
943
+ DrawImported: "draw:imported",
944
+ DrawCleared: "draw:cleared",
945
+ DrawVertex: "draw:vertex",
946
+ DrawVertexRemoved: "draw:vertexRemoved",
947
+ GeoPosition: "geo:position",
948
+ GeoError: "geo:error",
949
+ FullscreenChange: "fullscreen:change",
950
+ ScaleLineControl: "controls:scaleline",
951
+ ZoomControl: "controls:zoom",
952
+ AttributionControl: "controls:attribution",
953
+ BufferCreated: "buffer:created",
954
+ BufferInteractiveStart: "buffer:interactive:start",
955
+ EditModified: "edit:modified"
956
+ };
957
+
958
+ // src/core/ol/interactions/HoverInfo.ts
959
+ import { getUid } from "ol/util";
960
+ var DEFAULTS = {
961
+ hitTolerance: 5,
962
+ outlineColor: "transparent",
963
+ outlineWidth: 0,
964
+ fillColor: "transparent"
965
+ };
966
+ var HoverInfoController = class {
967
+ map;
968
+ events;
969
+ opts = { ...DEFAULTS };
970
+ hoverLayer = null;
971
+ hoverSource = null;
972
+ pointerMoveKey = null;
973
+ lastHoverKey = null;
974
+ viewportLeaveHandler = null;
975
+ defaultCursor = "";
976
+ setCursor(cursor) {
977
+ const el = this.map.getTargetElement();
978
+ if (!el) return;
979
+ if (!this.defaultCursor) this.defaultCursor = el.style.cursor || "";
980
+ el.style.cursor = cursor ?? this.defaultCursor;
981
+ }
982
+ constructor(map, events) {
983
+ this.map = map;
984
+ this.events = events;
985
+ }
986
+ activate(options) {
987
+ if (options) this.opts = { ...this.opts, ...options };
988
+ this.ensureLayer();
989
+ this.bindPointerMove();
990
+ this.bindViewportLeave();
991
+ }
992
+ deactivate() {
993
+ this.unbindPointerMove();
994
+ this.unbindViewportLeave();
995
+ this.destroyLayer();
996
+ }
997
+ bindViewportLeave() {
998
+ if (this.viewportLeaveHandler) return;
999
+ const vp = this.map.getViewport();
1000
+ this.viewportLeaveHandler = () => {
1001
+ if (this.lastHoverKey != null) this.clearHover();
1002
+ };
1003
+ vp.addEventListener("pointerleave", this.viewportLeaveHandler);
1004
+ vp.addEventListener("mouseleave", this.viewportLeaveHandler);
1005
+ }
1006
+ unbindViewportLeave() {
1007
+ if (!this.viewportLeaveHandler) return;
1008
+ const vp = this.map.getViewport();
1009
+ vp.removeEventListener("pointerleave", this.viewportLeaveHandler);
1010
+ vp.removeEventListener("mouseleave", this.viewportLeaveHandler);
1011
+ this.viewportLeaveHandler = null;
1012
+ }
1013
+ setOptions(options) {
1014
+ this.opts = { ...this.opts, ...options };
1015
+ }
1016
+ destroy() {
1017
+ this.deactivate();
1018
+ }
1019
+ // ---- internals ----
1020
+ ensureLayer() {
1021
+ if (this.hoverLayer) return;
1022
+ this.hoverSource = new VectorSource4();
1023
+ this.hoverLayer = new VectorLayer2({
1024
+ source: this.hoverSource,
1025
+ properties: { "nbic:role": "hover" },
1026
+ zIndex: 9999,
1027
+ updateWhileInteracting: true,
1028
+ style: (f) => {
1029
+ const s = f.get("nbic:hoverStyle");
1030
+ const stroke = new Stroke4({
1031
+ color: s?.strokeColor ?? this.opts.outlineColor,
1032
+ width: s?.strokeWidth ?? this.opts.outlineWidth
1033
+ });
1034
+ const fill = new Fill4({
1035
+ color: s?.fillColor ?? this.opts.fillColor
1036
+ });
1037
+ const g = f.getGeometry();
1038
+ const type = g?.getType();
1039
+ if (type === "Point" || type === "MultiPoint") {
1040
+ const radius = (
1041
+ // let a per-layer hover style override radius if you add it to DrawStyleOptions later
1042
+ s?.pointRadius ?? 8
1043
+ );
1044
+ return new Style4({
1045
+ image: new CircleStyle4({ radius, fill, stroke })
1046
+ });
1047
+ }
1048
+ return new Style4({ stroke, fill });
1049
+ }
1050
+ });
1051
+ this.map.addLayer(this.hoverLayer);
1052
+ }
1053
+ clearHover() {
1054
+ this.lastHoverKey = null;
1055
+ this.hoverSource?.clear(true);
1056
+ this.events.emit("hover:info", null);
1057
+ }
1058
+ destroyLayer() {
1059
+ if (this.hoverLayer) this.map.removeLayer(this.hoverLayer);
1060
+ this.hoverLayer = null;
1061
+ this.hoverSource = null;
1062
+ this.lastHoverKey = null;
1063
+ }
1064
+ bindPointerMove() {
1065
+ if (this.pointerMoveKey) return;
1066
+ this.pointerMoveKey = this.map.on("pointermove", (evt) => {
1067
+ const pixel = evt.pixel;
1068
+ let picked = null;
1069
+ this.map.forEachFeatureAtPixel(
1070
+ pixel,
1071
+ (f, l) => {
1072
+ if (!l) return void 0;
1073
+ picked = { feature: f, layer: l };
1074
+ return f;
1075
+ },
1076
+ {
1077
+ hitTolerance: this.opts.hitTolerance,
1078
+ layerFilter: (l) => l.get("nbic:role") !== "hover"
1079
+ }
1080
+ );
1081
+ if (!picked) {
1082
+ if ((this.hoverSource?.getFeatures().length ?? 0) > 0) this.clearHover();
1083
+ this.setCursor("default");
1084
+ return;
1085
+ }
1086
+ const { feature: top, layer } = picked;
1087
+ const topF = top;
1088
+ const id = topF.getId?.();
1089
+ const key = typeof id === "string" || typeof id === "number" ? id : getUid(topF);
1090
+ if (key === this.lastHoverKey) return;
1091
+ const keepSingleAsCluster = !!layer.get("nbic:keepSingleAsCluster");
1092
+ const hoverBehavior = layer.get("nbic:hoverClusterBehavior");
1093
+ const baseHover = layer.get("nbic:hoverStyle");
1094
+ const clusterHover = layer.get("nbic:hoverClusterStyle");
1095
+ const singleClusterHover = layer.get("nbic:hoverSingleClusterStyle");
1096
+ const cursor = layer.get("nbic:hoverCursor");
1097
+ this.setCursor(cursor ?? null);
1098
+ const members = topF.get("features");
1099
+ let geom = null;
1100
+ let hoverStyleToApply = baseHover;
1101
+ const topHover = topF.get("nbic:hoverStyle");
1102
+ const isCluster = !!members?.length;
1103
+ if (isCluster) {
1104
+ if (members.length >= 2) {
1105
+ geom = topF.getGeometry()?.clone() ?? null;
1106
+ hoverStyleToApply = clusterHover ?? baseHover;
1107
+ } else {
1108
+ const preferBubble = keepSingleAsCluster || hoverBehavior === "bubble";
1109
+ const inner = members[0];
1110
+ const innerHover = inner?.get("nbic:hoverStyle");
1111
+ if (preferBubble) {
1112
+ geom = topF.getGeometry()?.clone() ?? null;
1113
+ hoverStyleToApply = singleClusterHover ?? clusterHover ?? baseHover;
1114
+ } else {
1115
+ geom = inner?.getGeometry()?.clone() ?? null;
1116
+ hoverStyleToApply = innerHover ?? baseHover;
1117
+ }
1118
+ }
1119
+ } else {
1120
+ geom = topF.getGeometry()?.clone() ?? null;
1121
+ hoverStyleToApply = topHover ?? baseHover;
1122
+ }
1123
+ this.clearHover();
1124
+ if (!geom || !this.hoverSource) return;
1125
+ const hf = new Feature({ geometry: geom });
1126
+ if (hoverStyleToApply) hf.set("nbic:hoverStyle", hoverStyleToApply);
1127
+ this.hoverSource.addFeature(hf);
1128
+ this.lastHoverKey = key;
1129
+ this.events.emit(MapEvents.HoverInfo, {
1130
+ coordinate: evt.coordinate,
1131
+ items: [{ feature: topF, layer }]
1132
+ });
1133
+ });
1134
+ }
1135
+ unbindPointerMove() {
1136
+ if (this.pointerMoveKey) {
1137
+ unByKey(this.pointerMoveKey);
1138
+ this.pointerMoveKey = null;
1139
+ }
1140
+ this.clearHover();
1141
+ }
1142
+ };
1143
+
1144
+ // src/core/ol/OlMapEngine.ts
1145
+ import Collection from "ol/Collection";
1146
+
1147
+ // src/core/ol/layers/LayerRegistry.ts
1148
+ function hasGetSource(obj) {
1149
+ return !!obj && typeof obj.getSource === "function";
1150
+ }
1151
+ function hasGetFeatures(obj) {
1152
+ return !!obj && typeof obj.getFeatures === "function";
1153
+ }
1154
+ var LayerRegistry = class {
1155
+ index = /* @__PURE__ */ new Map();
1156
+ add(id, layer) {
1157
+ this.index.set(id, layer);
1158
+ }
1159
+ remove(id) {
1160
+ this.index.delete(id);
1161
+ }
1162
+ get(id) {
1163
+ return this.index.get(id);
1164
+ }
1165
+ clear() {
1166
+ this.index.clear();
1167
+ }
1168
+ getVectorSource(id) {
1169
+ const layer = this.index.get(id);
1170
+ if (!layer || !hasGetSource(layer)) return null;
1171
+ const src0 = layer.getSource();
1172
+ if (!src0) return null;
1173
+ const inner = hasGetSource(src0) ? src0.getSource() : src0;
1174
+ return hasGetFeatures(inner) ? inner : null;
1175
+ }
1176
+ listIds() {
1177
+ return Array.from(this.index.keys());
1178
+ }
1179
+ /** Reorder overlays and bases. You’ll usually call this from BaseLayersController. */
1180
+ reorder(order, bases) {
1181
+ let overlayZ = 0;
1182
+ let baseZ = bases.baseBand;
1183
+ for (const id of order) {
1184
+ const l = this.index.get(id);
1185
+ if (!l) continue;
1186
+ const role = l.get?.("nbic:role");
1187
+ if (role === "hover" || role === "draw" || role === "draw-vertices") continue;
1188
+ const pinned = !!l.get?.("nbic:zIndexPinned");
1189
+ if (pinned) continue;
1190
+ if (bases.isBase(l)) {
1191
+ l.setZIndex(baseZ++);
1192
+ } else {
1193
+ l.setZIndex(overlayZ++);
1194
+ }
1195
+ }
1196
+ }
1197
+ };
1198
+
1199
+ // src/core/ol/layers/BaseLayerController.ts
1200
+ var BaseLayersController = class {
1201
+ constructor(events) {
1202
+ this.events = events;
1203
+ }
1204
+ baseIds = /* @__PURE__ */ new Set();
1205
+ superBaseIds = /* @__PURE__ */ new Set();
1206
+ regionalBaseIds = /* @__PURE__ */ new Set();
1207
+ activeRegional = null;
1208
+ activeSuper = null;
1209
+ _findLayer = () => null;
1210
+ /** zIndex bands (keep bases below overlays) */
1211
+ baseBand = -1e4;
1212
+ superBaseBand = -2e4;
1213
+ isBase(l) {
1214
+ return l.get("nbic:role") === "base";
1215
+ }
1216
+ roleOf(l) {
1217
+ return l.get("nbic:baseRole");
1218
+ }
1219
+ registerBase(layer, role, def) {
1220
+ const id = layer.get("id");
1221
+ layer.set("nbic:role", "base");
1222
+ layer.set("nbic:baseRole", role);
1223
+ this.baseIds.add(id);
1224
+ if (role === "super") this.superBaseIds.add(id);
1225
+ if (role === "regional") this.regionalBaseIds.add(id);
1226
+ if (def.zIndex === void 0) {
1227
+ if (role === "super") layer.setZIndex(this.superBaseBand + this.superBaseIds.size);
1228
+ else layer.setZIndex(this.baseBand + this.regionalBaseIds.size);
1229
+ }
1230
+ if (role === "super") {
1231
+ layer.setVisible(def.visible !== false);
1232
+ if (layer.getVisible()) this.activeSuper = id;
1233
+ } else {
1234
+ const wantsVisible = def.visible ?? layer.getVisible();
1235
+ if (wantsVisible) this.setActiveRegional(id);
1236
+ else this.ensureOneRegionalVisible();
1237
+ }
1238
+ this.emitChanged();
1239
+ }
1240
+ onRemoved(id) {
1241
+ this.baseIds.delete(id);
1242
+ this.superBaseIds.delete(id);
1243
+ this.regionalBaseIds.delete(id);
1244
+ if (this.activeRegional === id) this.activeRegional = null;
1245
+ if (this.activeSuper === id) this.activeSuper = null;
1246
+ this.emitChanged();
1247
+ }
1248
+ setActiveRegional(id) {
1249
+ if (!this.regionalBaseIds.has(id)) return;
1250
+ for (const rid of this.regionalBaseIds) {
1251
+ const lyr = this.findLayer(rid);
1252
+ if (!lyr) continue;
1253
+ lyr.setVisible(rid === id);
1254
+ }
1255
+ this.activeRegional = id;
1256
+ this.emitChanged();
1257
+ }
1258
+ setVisibility(layer, visible) {
1259
+ const id = layer.get("id");
1260
+ const role = this.roleOf(layer);
1261
+ if (role === "super") {
1262
+ layer.setVisible(visible);
1263
+ if (visible) this.activeSuper = id;
1264
+ this.emitChanged();
1265
+ return;
1266
+ }
1267
+ if (visible) this.setActiveRegional(id);
1268
+ else {
1269
+ layer.setVisible(false);
1270
+ this.ensureOneRegionalVisible();
1271
+ this.emitChanged();
1272
+ }
1273
+ }
1274
+ /** Bridge: allow LayerRegistry.reorder to query base-ness. */
1275
+ isBaseLayer = (l) => this.isBase(l);
1276
+ ensureOneRegionalVisible() {
1277
+ let first = null;
1278
+ for (const id of this.regionalBaseIds) {
1279
+ const l = this.findLayer(id);
1280
+ if (!l) continue;
1281
+ if (l.getVisible() && !first) first = id;
1282
+ else l.setVisible(id === first);
1283
+ }
1284
+ this.activeRegional = first;
1285
+ }
1286
+ /** Engine should call this after creation to connect real lookup (e.g. LayerRegistry). */
1287
+ bindFind(fn) {
1288
+ this._findLayer = fn;
1289
+ }
1290
+ findLayer(id) {
1291
+ return this._findLayer(id);
1292
+ }
1293
+ clear() {
1294
+ this.baseIds.clear();
1295
+ this.superBaseIds.clear();
1296
+ this.regionalBaseIds.clear();
1297
+ this.activeRegional = null;
1298
+ this.activeSuper = null;
1299
+ this._findLayer = () => null;
1300
+ this.emitChanged();
1301
+ }
1302
+ emitChanged() {
1303
+ this.events.emit("base:changed", { regional: this.activeRegional, super: this.activeSuper });
1304
+ }
1305
+ };
1306
+
1307
+ // src/core/ol/interactions/DrawController.ts
1308
+ import VectorSource5 from "ol/source/Vector";
1309
+ import VectorLayer3 from "ol/layer/Vector";
1310
+ import Draw, { createBox } from "ol/interaction/Draw";
1311
+ import Modify from "ol/interaction/Modify";
1312
+ import Snap from "ol/interaction/Snap";
1313
+ import GeoJSON3 from "ol/format/GeoJSON";
1314
+ import Point2 from "ol/geom/Point";
1315
+ import { Feature as Feature2 } from "ol";
1316
+ import { unByKey as unByKey2 } from "ol/Observable";
1317
+ import { listen } from "ol/events";
1318
+ import buffer from "@turf/buffer";
1319
+ import { Style as Style5, Circle as CircleStyle5, Fill as Fill5, Stroke as Stroke5 } from "ol/style";
1320
+ import { LineString, Polygon, MultiLineString, MultiPolygon } from "ol/geom";
1321
+ import { getUid as getUid2 } from "ol/util";
1322
+ function mapKindToDraw(kind) {
1323
+ switch (kind) {
1324
+ case "Point":
1325
+ return { type: "Point" };
1326
+ case "LineString":
1327
+ return { type: "LineString" };
1328
+ case "Polygon":
1329
+ return { type: "Polygon" };
1330
+ case "Circle":
1331
+ return { type: "Circle" };
1332
+ case "Text":
1333
+ return { type: "Point" };
1334
+ case "Box":
1335
+ return { type: "Circle", geometryFunction: createBox() };
1336
+ }
1337
+ }
1338
+ function vertexCount(geom) {
1339
+ if (geom instanceof Point2) return 1;
1340
+ if (geom instanceof LineString) {
1341
+ const coords = geom.getCoordinates();
1342
+ return coords.length;
1343
+ }
1344
+ if (geom instanceof Polygon) {
1345
+ const rings = geom.getCoordinates();
1346
+ if (rings.length === 0) return 0;
1347
+ const firstRing = rings[0];
1348
+ if (!firstRing) return 0;
1349
+ return Math.max(0, firstRing.length - 1);
1350
+ }
1351
+ return 0;
1352
+ }
1353
+ function lastVertex(geom) {
1354
+ if (geom instanceof Point2) {
1355
+ const c = geom.getCoordinates();
1356
+ if (!Array.isArray(c) || c.length < 2) return null;
1357
+ return [c[0], c[1]];
1358
+ }
1359
+ if (geom instanceof LineString) {
1360
+ const coords = geom.getCoordinates();
1361
+ if (!Array.isArray(coords) || coords.length === 0) return null;
1362
+ const last = coords[coords.length - 1];
1363
+ if (!Array.isArray(last) || last.length < 2) return null;
1364
+ return [last[0], last[1]];
1365
+ }
1366
+ if (geom instanceof Polygon) {
1367
+ const rings = geom.getCoordinates();
1368
+ if (!Array.isArray(rings) || rings.length === 0) return null;
1369
+ const ring = rings[0];
1370
+ if (!Array.isArray(ring) || ring.length <= 1) return null;
1371
+ const last = ring[ring.length - 2];
1372
+ if (!Array.isArray(last) || last.length < 2) return null;
1373
+ return [last[0], last[1]];
1374
+ }
1375
+ return null;
1376
+ }
1377
+ function flattenOnce(arr) {
1378
+ const out = [];
1379
+ for (const a of arr) out.push(...a);
1380
+ return out;
1381
+ }
1382
+ function flattenTwice(arr) {
1383
+ const out = [];
1384
+ for (const a of arr) for (const b of a) out.push(...b);
1385
+ return out;
1386
+ }
1387
+ var DrawController = class {
1388
+ constructor(events) {
1389
+ this.events = events;
1390
+ }
1391
+ map;
1392
+ source;
1393
+ layer;
1394
+ draw;
1395
+ modify;
1396
+ snap;
1397
+ fmt = new GeoJSON3();
1398
+ currentDrawStyle = makeDrawStyle(void 0);
1399
+ vertexLayer;
1400
+ vertexSrc;
1401
+ attach(map) {
1402
+ this.map = map;
1403
+ }
1404
+ detach() {
1405
+ this.stop();
1406
+ this.map = void 0;
1407
+ }
1408
+ getLayer() {
1409
+ this.ensureLayer();
1410
+ return this.layer;
1411
+ }
1412
+ // --- Modify undo history ---
1413
+ editUndoStack = /* @__PURE__ */ new Map();
1414
+ // key -> snapshots
1415
+ editRedoStack = /* @__PURE__ */ new Map();
1416
+ // optional (keep for future)
1417
+ lastEditedKey = null;
1418
+ featureKey(f) {
1419
+ const id = f.getId();
1420
+ if (typeof id === "string") return id;
1421
+ if (typeof id === "number") return String(id);
1422
+ return String(getUid2(f));
1423
+ }
1424
+ pushGeometrySnapshot(f) {
1425
+ const g = f.getGeometry();
1426
+ if (!g) return;
1427
+ const key = this.featureKey(f);
1428
+ this.lastEditedKey = key;
1429
+ const stack = this.editUndoStack.get(key) ?? [];
1430
+ stack.push(g.clone());
1431
+ const MAX = 50;
1432
+ if (stack.length > MAX) stack.splice(0, stack.length - MAX);
1433
+ this.editUndoStack.set(key, stack);
1434
+ this.editRedoStack.set(key, []);
1435
+ }
1436
+ restoreGeometrySnapshot(f, geom) {
1437
+ f.setGeometry(geom.clone());
1438
+ }
1439
+ // ————— helpers —————
1440
+ ensureLayer() {
1441
+ if (!this.map || this.layer) return;
1442
+ this.source = new VectorSource5();
1443
+ this.layer = new VectorLayer3({
1444
+ source: this.source,
1445
+ properties: { "nbic:role": "draw", id: "draw-layer" },
1446
+ zIndex: 9e3,
1447
+ style: this.currentDrawStyle,
1448
+ updateWhileInteracting: true
1449
+ });
1450
+ this.layer.set("id", "draw-layer");
1451
+ this.map.addLayer(this.layer);
1452
+ }
1453
+ setStyle(f, s) {
1454
+ if (!s) return;
1455
+ f.set("nbic:style", s);
1456
+ f.setStyle(makeDrawStyle(s));
1457
+ }
1458
+ makeVertexStyle(opts) {
1459
+ const strokeColor = opts?.strokeColor ?? "#1976d2";
1460
+ const strokeWidth = opts?.strokeWidth ?? 2;
1461
+ const fillColor = opts?.fillColor ?? "#ffffff";
1462
+ const radius = opts?.pointRadius ?? 5;
1463
+ return new Style5({
1464
+ image: new CircleStyle5({
1465
+ radius,
1466
+ fill: new Fill5({ color: fillColor }),
1467
+ stroke: new Stroke5({ color: strokeColor, width: strokeWidth })
1468
+ })
1469
+ });
1470
+ }
1471
+ ensureVertexLayer(styleOpts) {
1472
+ if (!this.map || this.vertexLayer) return;
1473
+ this.vertexSrc = new VectorSource5();
1474
+ this.vertexLayer = new VectorLayer3({
1475
+ source: this.vertexSrc,
1476
+ properties: { "nbic:role": "draw-vertices" },
1477
+ zIndex: 9001,
1478
+ style: this.makeVertexStyle(styleOpts),
1479
+ updateWhileInteracting: true
1480
+ });
1481
+ this.map.addLayer(this.vertexLayer);
1482
+ }
1483
+ destroyVertexLayer() {
1484
+ if (this.vertexLayer && this.map) this.map.removeLayer(this.vertexLayer);
1485
+ this.vertexLayer = void 0;
1486
+ this.vertexSrc = void 0;
1487
+ }
1488
+ verticesOfGeometry(g) {
1489
+ if (g instanceof LineString) {
1490
+ return g.getCoordinates();
1491
+ }
1492
+ if (g instanceof Polygon) {
1493
+ const rings = g.getCoordinates();
1494
+ return flattenOnce(rings);
1495
+ }
1496
+ if (g instanceof MultiLineString) {
1497
+ const lines = g.getCoordinates();
1498
+ return flattenOnce(lines);
1499
+ }
1500
+ if (g instanceof MultiPolygon) {
1501
+ const polys = g.getCoordinates();
1502
+ return flattenTwice(polys);
1503
+ }
1504
+ return [];
1505
+ }
1506
+ rebuildVertexOverlay() {
1507
+ if (!this.vertexSrc || !this.source) return;
1508
+ this.vertexSrc.clear(true);
1509
+ const feats = this.source.getFeatures();
1510
+ for (const f of feats) {
1511
+ const g = f.getGeometry();
1512
+ if (!g) continue;
1513
+ const verts = this.verticesOfGeometry(g);
1514
+ for (let i = 0; i < verts.length; i++) {
1515
+ const coord = verts[i];
1516
+ if (!coord) continue;
1517
+ const v = new Feature2({ geometry: new Point2(coord) });
1518
+ const pid = f.getId();
1519
+ if (pid != null) v.set("nbic:parentId", pid);
1520
+ v.set("nbic:vertexIndex", i);
1521
+ this.vertexSrc.addFeature(v);
1522
+ }
1523
+ }
1524
+ }
1525
+ createPointFeature(coord, props, style) {
1526
+ const f = new Feature2({ geometry: new Point2(coord) });
1527
+ if (props) Object.entries(props).forEach(([k, v]) => f.set(k, v));
1528
+ this.setStyle(f, style);
1529
+ return f;
1530
+ }
1531
+ // Turf buffer around any feature (returns OL Feature in view projection)
1532
+ async turfBufferFeature(feature, opts) {
1533
+ if (!this.map) return null;
1534
+ const viewProj = String(this.map.getView().getProjection().getCode());
1535
+ const gj = this.fmt.writeFeatureObject(feature, {
1536
+ dataProjection: "EPSG:4326",
1537
+ featureProjection: viewProj
1538
+ });
1539
+ const turfOpts = { units: opts.units ?? "meters", steps: opts.steps ?? 64 };
1540
+ const buffered = buffer(gj, opts.distance, turfOpts);
1541
+ if (!buffered) return null;
1542
+ const out = this.fmt.readFeature(buffered, {
1543
+ dataProjection: "EPSG:4326",
1544
+ featureProjection: viewProj
1545
+ });
1546
+ return out;
1547
+ }
1548
+ // ————— public API —————
1549
+ async start(opts) {
1550
+ if (!this.map) return;
1551
+ const drawParams = mapKindToDraw(opts.kind);
1552
+ this.ensureLayer();
1553
+ this.stop();
1554
+ this.currentDrawStyle = makeDrawStyle(opts.style);
1555
+ this.layer.setStyle(this.currentDrawStyle);
1556
+ this.modify = new Modify({ source: this.source });
1557
+ this.map.addInteraction(this.modify);
1558
+ this.modify.setActive(false);
1559
+ this.draw = new Draw({
1560
+ source: this.source,
1561
+ type: drawParams.type,
1562
+ geometryFunction: drawParams.geometryFunction,
1563
+ style: this.currentDrawStyle
1564
+ });
1565
+ this.map.addInteraction(this.draw);
1566
+ const snapTol = Math.max(2, Math.min(25, 10));
1567
+ if (opts.snap ?? true) {
1568
+ this.snap = new Snap({ source: this.source, pixelTolerance: snapTol });
1569
+ this.map.addInteraction(this.snap);
1570
+ }
1571
+ let keyHandler = null;
1572
+ this.draw.on("drawstart", (e) => {
1573
+ this.events.emit("draw:start", { kind: opts.kind });
1574
+ const sketch = e.feature;
1575
+ const geom = sketch.getGeometry();
1576
+ if (!geom) return;
1577
+ let lastLen = vertexCount(geom);
1578
+ const changeKey = geom.on("change", () => {
1579
+ const len = vertexCount(geom);
1580
+ if (len > lastLen) {
1581
+ const coord = lastVertex(geom);
1582
+ if (coord) {
1583
+ this.events.emit("draw:vertex", {
1584
+ kind: opts.kind,
1585
+ index: len - 1,
1586
+ // 0-based, correct for lines/polygons
1587
+ coordinate: coord
1588
+ // MapCoord
1589
+ });
1590
+ }
1591
+ } else if (len < lastLen) {
1592
+ this.events.emit("draw:vertexRemoved", {
1593
+ kind: opts.kind,
1594
+ index: len
1595
+ // new last index after removal
1596
+ });
1597
+ }
1598
+ lastLen = len;
1599
+ });
1600
+ keyHandler = (ev) => {
1601
+ const ctrlZ = (ev.key === "z" || ev.key === "Z") && (ev.ctrlKey || ev.metaKey);
1602
+ if (ev.key === "Backspace" || ctrlZ) {
1603
+ ev.preventDefault();
1604
+ this.draw?.removeLastPoint();
1605
+ } else if (ev.key === "Enter") {
1606
+ ev.preventDefault();
1607
+ this.draw?.finishDrawing();
1608
+ } else if (ev.key === "Escape") {
1609
+ ev.preventDefault();
1610
+ this.draw.abortDrawing?.();
1611
+ }
1612
+ };
1613
+ window.addEventListener("keydown", keyHandler);
1614
+ const detachPerSketch = () => {
1615
+ unByKey2(changeKey);
1616
+ if (keyHandler) {
1617
+ window.removeEventListener("keydown", keyHandler);
1618
+ keyHandler = null;
1619
+ }
1620
+ };
1621
+ this.draw.once("drawend", detachPerSketch);
1622
+ this.draw.once("drawabort", detachPerSketch);
1623
+ });
1624
+ this.draw.on("drawend", async (e) => {
1625
+ const f = e.feature;
1626
+ if (!f.getId()) {
1627
+ f.setId(crypto.randomUUID());
1628
+ }
1629
+ this.setStyle(f, opts.style);
1630
+ this.modify?.setActive(true);
1631
+ const b = opts.buffer;
1632
+ const wantsInteractive = !!b && (b === true || typeof b === "object" && b.interactive) && ["Point", "LineString", "Polygon"].includes(f.getGeometry()?.getType() || "");
1633
+ if (wantsInteractive && this.map && this.source) {
1634
+ const params = b === true ? {} : b;
1635
+ const g = f.getGeometry();
1636
+ const gType = g.getType();
1637
+ const previewStyle = params.style ?? { strokeColor: "#0080ff", strokeWidth: 2, fillColor: "rgba(0,128,255,0.12)" };
1638
+ let preview = null;
1639
+ let lastDist = 0;
1640
+ const THROTTLE_MS = 90;
1641
+ let lastRun = 0;
1642
+ const distanceToGeometry = (px) => {
1643
+ if (gType === "Point") {
1644
+ const c = g.getCoordinates();
1645
+ const dx2 = px[0] - c[0], dy2 = px[1] - c[1];
1646
+ return Math.sqrt(dx2 * dx2 + dy2 * dy2);
1647
+ }
1648
+ const closest = g.getClosestPoint(px);
1649
+ const dx = px[0] - closest[0], dy = px[1] - closest[1];
1650
+ return Math.sqrt(dx * dx + dy * dy);
1651
+ };
1652
+ this.modify?.setActive(false);
1653
+ this.snap?.setActive?.(false);
1654
+ let moveKey = this.map.on("pointermove", async (evt) => {
1655
+ const now = performance.now();
1656
+ if (now - lastRun < THROTTLE_MS) return;
1657
+ lastRun = now;
1658
+ const dist = distanceToGeometry(evt.coordinate);
1659
+ if (dist <= 0) return;
1660
+ lastDist = dist;
1661
+ const buffered = await this.turfBufferFeature(f, {
1662
+ distance: dist,
1663
+ units: params.units ?? "meters",
1664
+ steps: params.steps ?? 64
1665
+ });
1666
+ if (!buffered) return;
1667
+ buffered.set("nbic:style", previewStyle);
1668
+ buffered.setStyle(makeDrawStyle(previewStyle));
1669
+ if (preview) this.source.removeFeature(preview);
1670
+ preview = buffered;
1671
+ this.source.addFeature(preview);
1672
+ });
1673
+ const pointerDownListener = listen(this.map, "pointerdown", () => finalize());
1674
+ const detach = () => {
1675
+ if (moveKey) {
1676
+ unByKey2(moveKey);
1677
+ moveKey = void 0;
1678
+ }
1679
+ if (pointerDownListener) {
1680
+ unByKey2(pointerDownListener);
1681
+ }
1682
+ };
1683
+ const finalize = () => {
1684
+ detach();
1685
+ if (preview) {
1686
+ if (params.replaceOriginal) this.source.removeFeature(f);
1687
+ this.events.emit("buffer:created", { baseFeature: f, bufferFeature: preview, distance: lastDist, units: params.units ?? "meters" });
1688
+ preview = null;
1689
+ }
1690
+ this.stop();
1691
+ };
1692
+ const esc = (e2) => {
1693
+ if (e2.key !== "Escape") return;
1694
+ if (moveKey) {
1695
+ unByKey2(moveKey);
1696
+ moveKey = void 0;
1697
+ }
1698
+ if (preview) {
1699
+ this.source.removeFeature(preview);
1700
+ preview = null;
1701
+ }
1702
+ window.removeEventListener("keydown", esc);
1703
+ this.modify?.setActive(true);
1704
+ this.snap?.setActive?.(true);
1705
+ this.draw?.setActive(true);
1706
+ };
1707
+ window.addEventListener("keydown", esc);
1708
+ this.events.emit("buffer:interactive:start", { mode: gType });
1709
+ return;
1710
+ }
1711
+ const wantsNonInteractive = !!b && typeof b === "object" && !b.interactive;
1712
+ if (wantsNonInteractive && this.map && this.source) {
1713
+ const bb = b;
1714
+ const buffered = await this.turfBufferFeature(f, {
1715
+ distance: bb.distance ?? 50,
1716
+ units: bb.units ?? "meters",
1717
+ steps: bb.steps ?? 64
1718
+ });
1719
+ if (buffered) {
1720
+ const finalStyle = bb.style ?? { strokeColor: "#0057ff", strokeWidth: 2, fillColor: "rgba(0,87,255,0.16)" };
1721
+ buffered.set("nbic:style", finalStyle);
1722
+ buffered.setStyle(makeDrawStyle(finalStyle));
1723
+ this.source.addFeature(buffered);
1724
+ if (bb.replaceOriginal) this.source.removeFeature(f);
1725
+ this.events.emit("buffer:created", { baseFeature: f, bufferFeature: buffered, distance: bb.distance ?? 0, units: bb.units ?? "meters" });
1726
+ }
1727
+ }
1728
+ this.events.emit("draw:end", { feature: f });
1729
+ if (this.vertexLayer) this.rebuildVertexOverlay();
1730
+ });
1731
+ }
1732
+ stop() {
1733
+ if (!this.map) return;
1734
+ if (this.draw) {
1735
+ this.map.removeInteraction(this.draw);
1736
+ this.draw = void 0;
1737
+ }
1738
+ if (this.modify) {
1739
+ this.map.removeInteraction(this.modify);
1740
+ this.modify = void 0;
1741
+ }
1742
+ if (this.snap) {
1743
+ this.map.removeInteraction(this.snap);
1744
+ this.snap = void 0;
1745
+ }
1746
+ }
1747
+ undoLastPoint() {
1748
+ this.draw?.removeLastPoint();
1749
+ }
1750
+ finishCurrent() {
1751
+ this.draw?.finishDrawing();
1752
+ }
1753
+ abortCurrent() {
1754
+ this.draw?.abortDrawing();
1755
+ }
1756
+ undoEdit(target) {
1757
+ if (!this.source) return;
1758
+ let f = null;
1759
+ if (target && "feature" in target) {
1760
+ f = target.feature;
1761
+ } else if (target && "featureId" in target) {
1762
+ const found = this.source.getFeatureById(target.featureId);
1763
+ f = found ?? null;
1764
+ } else {
1765
+ if (!this.lastEditedKey) return;
1766
+ const feats = this.source.getFeatures();
1767
+ for (const cand of feats) {
1768
+ const key2 = this.featureKey(cand);
1769
+ if (key2 === this.lastEditedKey) {
1770
+ f = cand;
1771
+ break;
1772
+ }
1773
+ }
1774
+ }
1775
+ if (!f) return;
1776
+ const key = this.featureKey(f);
1777
+ const stack = this.editUndoStack.get(key);
1778
+ if (!stack || stack.length === 0) return;
1779
+ const cur = f.getGeometry();
1780
+ if (cur) {
1781
+ const redo = this.editRedoStack.get(key) ?? [];
1782
+ redo.push(cur.clone());
1783
+ this.editRedoStack.set(key, redo);
1784
+ }
1785
+ const prev = stack.pop();
1786
+ if (!prev) return;
1787
+ this.restoreGeometrySnapshot(f, prev);
1788
+ if (this.vertexLayer) this.rebuildVertexOverlay();
1789
+ this.events.emit("edit:undo", { feature: f });
1790
+ }
1791
+ enableEditing(options) {
1792
+ if (!this.map) return;
1793
+ this.ensureLayer();
1794
+ let showHandles = true;
1795
+ let persistent = false;
1796
+ let vertexStyleOpts;
1797
+ if (typeof options === "boolean") {
1798
+ showHandles = options;
1799
+ } else if (options) {
1800
+ showHandles = options.showVertices !== false;
1801
+ persistent = !!options.showVerticesPersistent;
1802
+ vertexStyleOpts = options.vertexStyle;
1803
+ }
1804
+ if (this.modify) {
1805
+ this.map.removeInteraction(this.modify);
1806
+ this.modify = void 0;
1807
+ }
1808
+ const modifyStyle = showHandles ? this.makeVertexStyle(vertexStyleOpts) : () => void 0;
1809
+ this.modify = new Modify({ source: this.source, style: modifyStyle });
1810
+ this.map.addInteraction(this.modify);
1811
+ this.modify.on("modifystart", (e) => {
1812
+ const feats = e.features;
1813
+ for (let i = 0; i < feats.getLength(); i++) {
1814
+ const f = feats.item(i);
1815
+ this.pushGeometrySnapshot(f);
1816
+ }
1817
+ });
1818
+ this.modify.on("modifyend", (e) => {
1819
+ this.events.emit("edit:modified", {
1820
+ count: e.features.getLength(),
1821
+ feature: e.features.item(0)
1822
+ });
1823
+ if (persistent) this.rebuildVertexOverlay();
1824
+ });
1825
+ if (!this.snap) {
1826
+ this.snap = new Snap({ source: this.source });
1827
+ this.map.addInteraction(this.snap);
1828
+ }
1829
+ if (persistent) {
1830
+ this.ensureVertexLayer(vertexStyleOpts);
1831
+ this.rebuildVertexOverlay();
1832
+ this.source.on("addfeature", () => this.rebuildVertexOverlay());
1833
+ this.source.on("removefeature", () => this.rebuildVertexOverlay());
1834
+ this.source.on("changefeature", () => this.rebuildVertexOverlay());
1835
+ } else {
1836
+ this.destroyVertexLayer();
1837
+ }
1838
+ }
1839
+ disableEditing() {
1840
+ if (!this.map) return;
1841
+ if (this.modify) {
1842
+ this.map.removeInteraction(this.modify);
1843
+ this.modify = void 0;
1844
+ }
1845
+ if (this.snap) {
1846
+ this.map.removeInteraction(this.snap);
1847
+ this.snap = void 0;
1848
+ }
1849
+ this.destroyVertexLayer();
1850
+ }
1851
+ clear() {
1852
+ const count = this.source?.getFeatures().length ?? 0;
1853
+ this.source?.clear(true);
1854
+ this.events.emit("draw:cleared", { count });
1855
+ if (this.vertexLayer) this.rebuildVertexOverlay();
1856
+ }
1857
+ exportGeoJSON(map, opts) {
1858
+ const json = this.fmt.writeFeatures(this.source?.getFeatures() ?? [], {
1859
+ featureProjection: String(map.getView().getProjection() ?? "EPSG:3857")
1860
+ });
1861
+ return opts?.pretty ? JSON.stringify(JSON.parse(json), null, 2) : json;
1862
+ }
1863
+ importGeoJSON(map, geojson, opts) {
1864
+ this.ensureLayer();
1865
+ const features = this.fmt.readFeatures(geojson, {
1866
+ featureProjection: String(map.getView().getProjection())
1867
+ });
1868
+ if (opts?.clearExisting) this.source.clear(true);
1869
+ for (const f of features) {
1870
+ const styleOpts = f.get("nbic:style");
1871
+ if (styleOpts) f.setStyle(makeDrawStyle(styleOpts));
1872
+ const label = f.get("label");
1873
+ if (label && styleOpts?.text) f.setStyle(makeDrawStyle({ ...styleOpts, text: { ...styleOpts.text, label } }));
1874
+ }
1875
+ this.source.addFeatures(features);
1876
+ this.events.emit("draw:imported", { count: features.length });
1877
+ }
1878
+ setFeatureStyle(feature, style) {
1879
+ if (!feature) return;
1880
+ feature.set("nbic:style", style);
1881
+ feature.setStyle(makeUpdatedDrawStyle(style));
1882
+ }
1883
+ clearFeatureStyle(feature) {
1884
+ feature.unset("nbic:style", true);
1885
+ feature.setStyle(void 0);
1886
+ }
1887
+ };
1888
+
1889
+ // src/core/ol/controls/ControlsController.ts
1890
+ import Zoom from "ol/control/Zoom";
1891
+ import ScaleLine from "ol/control/ScaleLine";
1892
+ import Attribution from "ol/control/Attribution";
1893
+ import FullScreen from "ol/control/FullScreen";
1894
+ var ControlsController = class {
1895
+ map;
1896
+ zoom;
1897
+ scale;
1898
+ attr;
1899
+ fs;
1900
+ custom = /* @__PURE__ */ new Map();
1901
+ attach(map, events, init) {
1902
+ this.map = map;
1903
+ if (init?.zoom) this.ensureZoom(map, events);
1904
+ if (init?.scaleLine) this.ensureScaleLine(map, events);
1905
+ if (init?.attribution) this.ensureAttribution(map, events);
1906
+ if (init?.fullscreen) this.ensureFullScreen(map);
1907
+ }
1908
+ detach() {
1909
+ if (!this.map) return;
1910
+ if (this.zoom) this.map.removeControl(this.zoom);
1911
+ if (this.scale) this.map.removeControl(this.scale);
1912
+ if (this.attr) this.map.removeControl(this.attr);
1913
+ if (this.fs) this.map.removeControl(this.fs);
1914
+ this.zoom = this.scale = this.attr = this.fs = void 0;
1915
+ this.map = void 0;
1916
+ }
1917
+ ensureZoom(map, events) {
1918
+ if (this.zoom) return;
1919
+ this.zoom = new Zoom();
1920
+ map.addControl(this.zoom);
1921
+ events?.emit("controls:zoom", { visible: true });
1922
+ }
1923
+ removeZoom(map, events) {
1924
+ if (!this.zoom) return;
1925
+ map.removeControl(this.zoom);
1926
+ this.zoom = void 0;
1927
+ events?.emit("controls:zoom", { visible: false });
1928
+ }
1929
+ ensureScaleLine(map, events) {
1930
+ if (this.scale) return;
1931
+ this.scale = new ScaleLine();
1932
+ map.addControl(this.scale);
1933
+ events?.emit("controls:scaleline", { visible: true });
1934
+ }
1935
+ removeScaleLine(map, events) {
1936
+ if (!this.scale) return;
1937
+ map.removeControl(this.scale);
1938
+ this.scale = void 0;
1939
+ events?.emit("controls:scaleline", { visible: false });
1940
+ }
1941
+ ensureAttribution(map, events) {
1942
+ if (this.attr) return;
1943
+ this.attr = new Attribution({ collapsible: true, collapsed: false });
1944
+ map.addControl(this.attr);
1945
+ events?.emit("controls:attribution", { visible: true });
1946
+ }
1947
+ removeAttribution(map, events) {
1948
+ if (!this.attr) return;
1949
+ map.removeControl(this.attr);
1950
+ this.attr = void 0;
1951
+ events?.emit("controls:attribution", { visible: false });
1952
+ }
1953
+ ensureFullScreen(map) {
1954
+ if (this.fs) return;
1955
+ this.fs = new FullScreen();
1956
+ map.addControl(this.fs);
1957
+ }
1958
+ removeFullScreen(map) {
1959
+ if (!this.fs) return;
1960
+ map.removeControl(this.fs);
1961
+ this.fs = void 0;
1962
+ }
1963
+ /** Add a custom OL control. Returns the resolved ID. */
1964
+ adopt(control, id) {
1965
+ if (!this.map) throw new Error("ControlsController: map not attached");
1966
+ const existingId = control.get?.("id");
1967
+ const resolved = id ?? existingId ?? `custom:${Math.random().toString(36).slice(2, 10)}`;
1968
+ const old = this.custom.get(resolved);
1969
+ if (old) this.map.removeControl(old);
1970
+ control.set?.("id", resolved);
1971
+ this.map.addControl(control);
1972
+ this.custom.set(resolved, control);
1973
+ return resolved;
1974
+ }
1975
+ /** Remove a custom control by id (no-op if not found). */
1976
+ remove(id) {
1977
+ if (!this.map) return false;
1978
+ const c = this.custom.get(id);
1979
+ if (!c) return false;
1980
+ this.map.removeControl(c);
1981
+ this.custom.delete(id);
1982
+ return true;
1983
+ }
1984
+ /** Get a custom control instance by id. */
1985
+ get(id) {
1986
+ return this.custom.get(id);
1987
+ }
1988
+ /** List all adopted custom control ids. */
1989
+ listIds() {
1990
+ return Array.from(this.custom.keys());
1991
+ }
1992
+ };
1993
+
1994
+ // src/core/ol/geo/GeoController.ts
1995
+ import Geolocation from "ol/Geolocation";
1996
+ import VectorSource6 from "ol/source/Vector";
1997
+ import VectorLayer4 from "ol/layer/Vector";
1998
+ import CircleGeom2 from "ol/geom/Circle";
1999
+ import Point3 from "ol/geom/Point";
2000
+ import { Style as Style6, Fill as Fill6, Stroke as Stroke6, Circle as CircleStyle6 } from "ol/style";
2001
+ import { Feature as Feature3 } from "ol";
2002
+ var GeoController = class {
2003
+ constructor(events) {
2004
+ this.events = events;
2005
+ }
2006
+ map;
2007
+ geo;
2008
+ src;
2009
+ layer;
2010
+ follow = false;
2011
+ attach(map) {
2012
+ this.map = map;
2013
+ }
2014
+ detach() {
2015
+ this.deactivate();
2016
+ this.map = void 0;
2017
+ }
2018
+ ensureLayer() {
2019
+ if (!this.map || this.layer) return;
2020
+ this.src = new VectorSource6();
2021
+ this.layer = new VectorLayer4({
2022
+ source: this.src,
2023
+ properties: { "nbic:role": "geolocation" },
2024
+ zIndex: 9998,
2025
+ style: (f) => {
2026
+ const g = f.getGeometry();
2027
+ if (g instanceof CircleGeom2) {
2028
+ return new Style6({
2029
+ fill: new Fill6({ color: "rgba(33,150,243,0.15)" }),
2030
+ stroke: new Stroke6({ color: "#2196f3", width: 1 })
2031
+ });
2032
+ }
2033
+ return new Style6({
2034
+ image: new CircleStyle6({
2035
+ radius: 6,
2036
+ fill: new Fill6({ color: "#2196f3" }),
2037
+ stroke: new Stroke6({ color: "#fff", width: 2 })
2038
+ })
2039
+ });
2040
+ }
2041
+ });
2042
+ this.map.addLayer(this.layer);
2043
+ }
2044
+ activate(follow) {
2045
+ if (!this.map) return;
2046
+ if (!this.geo) {
2047
+ this.geo = new Geolocation({
2048
+ projection: this.map.getView().getProjection(),
2049
+ tracking: false,
2050
+ trackingOptions: { enableHighAccuracy: true, maximumAge: 1e4, timeout: 1e4 }
2051
+ });
2052
+ this.geo.on("change:position", () => {
2053
+ const p = this.geo.getPosition();
2054
+ const acc = this.geo.getAccuracy();
2055
+ if (!p) {
2056
+ this.events.emit("geo:position", null);
2057
+ return;
2058
+ }
2059
+ this.ensureLayer();
2060
+ this.src.clear();
2061
+ this.src.addFeatures([
2062
+ new Feature3({ geometry: new CircleGeom2(p, acc || 0) }),
2063
+ new Feature3({ geometry: new Point3(p) })
2064
+ ]);
2065
+ if (this.follow) this.map.getView().setCenter(p);
2066
+ this.events.emit("geo:position", { coordinate: p, accuracy: acc ?? void 0 });
2067
+ });
2068
+ this.geo.on("error", (e) => {
2069
+ this.events.emit("geo:error", { message: e.message ?? "Geolocation error" });
2070
+ });
2071
+ }
2072
+ this.follow = !!follow;
2073
+ this.geo.setTracking(true);
2074
+ }
2075
+ deactivate() {
2076
+ if (this.geo) this.geo.setTracking(false);
2077
+ if (this.layer && this.map) this.map.removeLayer(this.layer);
2078
+ this.geo = void 0;
2079
+ this.src = void 0;
2080
+ this.layer = void 0;
2081
+ }
2082
+ async zoomToGeolocation(maxZoom = 14) {
2083
+ if (!this.map) return false;
2084
+ this.activate(true);
2085
+ return new Promise((resolve) => {
2086
+ const once = () => {
2087
+ const p = this.geo.getPosition();
2088
+ if (p) {
2089
+ this.ensureLayer();
2090
+ this.map.getView().animate({ center: p, zoom: Math.max(this.map.getView().getZoom() ?? 0, maxZoom), duration: 300 });
2091
+ resolve(true);
2092
+ } else resolve(false);
2093
+ this.geo.un("change:position", once);
2094
+ this.geo.setTracking(false);
2095
+ };
2096
+ this.geo.on("change:position", once);
2097
+ });
2098
+ }
2099
+ };
2100
+
2101
+ // src/core/ol/zoom/ZoomController.ts
2102
+ import VectorSource7 from "ol/source/Vector";
2103
+ import { getCenter } from "ol/extent";
2104
+ var ZoomController = class {
2105
+ map = null;
2106
+ attach(map) {
2107
+ this.map = map;
2108
+ }
2109
+ detach() {
2110
+ this.map = null;
2111
+ }
2112
+ fitExtent(ext, opts) {
2113
+ if (!this.map) return false;
2114
+ const view = this.map.getView();
2115
+ const size = this.map.getSize();
2116
+ if (!view || !size) return false;
2117
+ const paddingPx = opts?.paddingPx ?? 40;
2118
+ const durationMs = opts?.durationMs ?? 250;
2119
+ const fitOpts = {
2120
+ padding: [paddingPx, paddingPx, paddingPx, paddingPx],
2121
+ duration: durationMs,
2122
+ maxZoom: Infinity
2123
+ };
2124
+ if (typeof opts?.maxZoom === "number") fitOpts.maxZoom = opts.maxZoom;
2125
+ const minSizePx = opts?.minSizePx ?? 64;
2126
+ const res = view.getResolution() ?? 1;
2127
+ const minMapUnits = res * minSizePx;
2128
+ const e = ext.slice();
2129
+ if (e[0] === e[2] || e[1] === e[3]) {
2130
+ const [cx, cy] = getCenter(e);
2131
+ e[0] = cx ?? 0 - minMapUnits / 2;
2132
+ e[2] = cx ?? 0 + minMapUnits / 2;
2133
+ e[1] = cy ?? 0 - minMapUnits / 2;
2134
+ e[3] = cy ?? 0 + minMapUnits / 2;
2135
+ }
2136
+ view.fit(e, fitOpts);
2137
+ return true;
2138
+ }
2139
+ fitGeometry(geom, opts) {
2140
+ return this.fitExtent(geom.getExtent(), opts);
2141
+ }
2142
+ zoomToFeature(layer, featureId, opts) {
2143
+ const src = layer.getSource();
2144
+ if (!src) return false;
2145
+ const feat = src.getFeatureById(featureId);
2146
+ const geom = feat?.getGeometry();
2147
+ return geom ? this.fitGeometry(geom, opts) : false;
2148
+ }
2149
+ zoomToLayer(layer, opts) {
2150
+ const src = layer.getSource();
2151
+ if (!src) return false;
2152
+ if (!(src instanceof VectorSource7)) return console.warn("zoomToLayer: not a vector source"), false;
2153
+ const feats = src.getFeatures();
2154
+ if (!feats.length) return false;
2155
+ let ext = feats[0]?.getGeometry()?.getExtent()?.slice();
2156
+ for (let i = 1; i < feats.length; i++) {
2157
+ const g = feats[i]?.getGeometry();
2158
+ if (!g) continue;
2159
+ const e = g.getExtent();
2160
+ if (!ext) ext = e.slice();
2161
+ else {
2162
+ ext[0] = Math.min(ext[0] ?? Infinity, e[0] ?? Infinity);
2163
+ ext[1] = Math.min(ext[1] ?? Infinity, e[1] ?? Infinity);
2164
+ ext[2] = Math.max(ext[2] ?? -Infinity, e[2] ?? -Infinity);
2165
+ ext[3] = Math.max(ext[3] ?? -Infinity, e[3] ?? -Infinity);
2166
+ }
2167
+ }
2168
+ return ext ? this.fitExtent(ext, opts) : false;
2169
+ }
2170
+ // Convenience by id (uses your registry/lookup)
2171
+ zoomToFeatureById(reg, layerId, featureId, opts) {
2172
+ const l = reg.get(layerId);
2173
+ if (!l) return false;
2174
+ return this.zoomToFeature(l, featureId, opts);
2175
+ }
2176
+ zoomToLayerById(reg, layerId, opts) {
2177
+ const l = reg.get(layerId);
2178
+ if (!l) return false;
2179
+ return this.zoomToLayer(l, opts);
2180
+ }
2181
+ zoomToExtent(ext, opts) {
2182
+ return this.fitExtent(ext, opts);
2183
+ }
2184
+ };
2185
+
2186
+ // src/core/ol/OlMapEngine.ts
2187
+ import GeoJSON4 from "ol/format/GeoJSON";
2188
+ import VectorSource8 from "ol/source/Vector";
2189
+ import Cluster2 from "ol/source/Cluster";
2190
+
2191
+ // src/core/ol/utils/picking.ts
2192
+ function isPickableLayer(l) {
2193
+ if (!l || typeof l.get !== "function") return false;
2194
+ const get = l.get.bind(l);
2195
+ const role = get("nbic:role");
2196
+ if (role === "hover") return false;
2197
+ const vis = l.getVisible?.();
2198
+ if (vis === false) return false;
2199
+ if (get("nbic:pickable") === false) return false;
2200
+ return true;
2201
+ }
2202
+
2203
+ // src/core/ol/utils/coords.ts
2204
+ import { transform, transformExtent } from "ol/proj";
2205
+ import { getCenter as getCenter2 } from "ol/extent";
2206
+ function toViewCoord(map, coord, from) {
2207
+ const viewProj = String(map.getView().getProjection().getCode());
2208
+ if (!from || from === viewProj) return coord;
2209
+ return transform(coord, from, viewProj);
2210
+ }
2211
+ function toDataCoord(map, coord, to = "EPSG:4326") {
2212
+ const viewProj = String(map.getView().getProjection().getCode());
2213
+ if (to === viewProj) return coord;
2214
+ return transform(coord, viewProj, to);
2215
+ }
2216
+ function transformCoordsArrayFrom(coords, from, to) {
2217
+ return coords.map((c) => transform(c, from, to));
2218
+ }
2219
+ function transformCoordsFrom(coords, from, to) {
2220
+ return transform(coords, from, to);
2221
+ }
2222
+ function transformExtentFrom(extent, from, to) {
2223
+ return transformExtent(extent, from, to);
2224
+ }
2225
+ function getCenterFromExtent(extent) {
2226
+ return getCenter2(extent);
2227
+ }
2228
+
2229
+ // src/core/ol/OlMapEngine.ts
2230
+ import { getCenter as getCenter3 } from "ol/extent";
2231
+
2232
+ // src/core/ol/select/SelectController.ts
2233
+ import Cluster from "ol/source/Cluster";
2234
+ var SelectController = class {
2235
+ constructor(events) {
2236
+ this.events = events;
2237
+ }
2238
+ map;
2239
+ selected = null;
2240
+ selectedPrevStyle = void 0;
2241
+ selectedPrevNbicStyle = void 0;
2242
+ attach(map) {
2243
+ this.map = map;
2244
+ }
2245
+ detach() {
2246
+ this.clear();
2247
+ this.map = void 0;
2248
+ }
2249
+ getSelection() {
2250
+ return this.selected;
2251
+ }
2252
+ clear(registry) {
2253
+ if (!this.selected) return;
2254
+ if (registry) {
2255
+ const prev = this.findFeatureById(registry, this.selected.layerId, this.selected.featureId, this.selected.coordinates);
2256
+ if (prev) this.restoreFeature(prev);
2257
+ }
2258
+ this.selected = null;
2259
+ this.selectedPrevStyle = void 0;
2260
+ this.selectedPrevNbicStyle = void 0;
2261
+ this.events.emit("select:changed", null);
2262
+ }
2263
+ /** Select feature by its id (restores previous selection automatically). */
2264
+ selectById(registry, layerId, featureId, style, coordinates) {
2265
+ if (!this.map) return false;
2266
+ if (this.selected) {
2267
+ const prev = this.findFeatureById(registry, this.selected.layerId, this.selected.featureId, this.selected.coordinates);
2268
+ if (prev) this.restoreFeature(prev);
2269
+ this.selected = null;
2270
+ this.selectedPrevStyle = void 0;
2271
+ this.selectedPrevNbicStyle = void 0;
2272
+ }
2273
+ const f = this.findFeatureById(registry, layerId, featureId, coordinates);
2274
+ if (!f) return false;
2275
+ this.selected = { layerId, featureId, coordinates };
2276
+ this.selectedPrevStyle = f.getStyle();
2277
+ this.selectedPrevNbicStyle = f.get("nbic:style");
2278
+ if (style) {
2279
+ f.set("nbic:style", style);
2280
+ f.setStyle(makeDrawStyle(style));
2281
+ } else {
2282
+ f.setStyle(
2283
+ makeDrawStyle({
2284
+ strokeColor: "#00c853",
2285
+ strokeWidth: 3,
2286
+ fillColor: "rgba(0,200,83,0.15)"
2287
+ })
2288
+ );
2289
+ }
2290
+ this.events.emit("select:changed", { layerId, featureId });
2291
+ return true;
2292
+ }
2293
+ restoreFeature(feature) {
2294
+ if (this.selectedPrevNbicStyle === void 0) {
2295
+ feature.unset("nbic:style", true);
2296
+ } else {
2297
+ feature.set("nbic:style", this.selectedPrevNbicStyle);
2298
+ }
2299
+ feature.setStyle(this.selectedPrevStyle);
2300
+ }
2301
+ /**
2302
+ * If the clicked thing is a cluster output feature, unwrap single-member clusters
2303
+ * and return the inner feature id for selection.
2304
+ */
2305
+ resolveSelectableIdFromClusterFeature(feature) {
2306
+ const members = feature.get("features");
2307
+ if (!members?.length) return null;
2308
+ if (members.length === 1) {
2309
+ const id = members[0]?.getId();
2310
+ return typeof id === "string" || typeof id === "number" ? id : null;
2311
+ }
2312
+ return null;
2313
+ }
2314
+ // ---- internals ----
2315
+ findFeatureById(registry, layerId, featureId, coordinates) {
2316
+ const src = registry.getVectorSource(layerId);
2317
+ if (!src) return null;
2318
+ if (src instanceof Cluster) {
2319
+ const inner = src.getSource();
2320
+ const f2 = inner?.getFeatureById(featureId);
2321
+ return f2 ?? null;
2322
+ }
2323
+ let f = null;
2324
+ if (coordinates) {
2325
+ const features = src.getFeaturesAtCoordinate(coordinates);
2326
+ f = features ? features[0] : null;
2327
+ } else {
2328
+ f = src.getFeatureById(featureId);
2329
+ if (!f) {
2330
+ f = src.getFeatureByUid(featureId.toString());
2331
+ }
2332
+ }
2333
+ return f ?? null;
2334
+ }
2335
+ };
2336
+
2337
+ // src/core/ol/OlMapEngine.ts
2338
+ function resolveVectorSource(layer) {
2339
+ const src = layer.getSource();
2340
+ if (!src) return null;
2341
+ if (src instanceof Cluster2) {
2342
+ const inner = src.getSource();
2343
+ return inner ?? null;
2344
+ }
2345
+ return src;
2346
+ }
2347
+ function createOlEngine(events) {
2348
+ let map;
2349
+ let hover = null;
2350
+ const registry = new LayerRegistry();
2351
+ const bases = new BaseLayersController(events);
2352
+ const draw = new DrawController(events);
2353
+ const controls = new ControlsController();
2354
+ const geo = new GeoController(events);
2355
+ const zoom = new ZoomController();
2356
+ const select = new SelectController(events);
2357
+ bases.bindFind((id) => registry.get(id) ?? null);
2358
+ return {
2359
+ async init(init) {
2360
+ const view = new View({
2361
+ projection: init.projection ?? "EPSG:3857",
2362
+ center: init.center ?? [0, 0],
2363
+ zoom: init.zoom ?? 2,
2364
+ minZoom: init.minZoom,
2365
+ maxZoom: init.maxZoom
2366
+ });
2367
+ map = new OlMap({
2368
+ target: init.target,
2369
+ view,
2370
+ controls: new Collection([])
2371
+ });
2372
+ controls.attach(map, events, init.controls);
2373
+ draw.attach(map);
2374
+ const dl = draw.getLayer();
2375
+ if (dl) registry.add("draw-layer", dl);
2376
+ geo.attach(map);
2377
+ zoom.attach(map);
2378
+ select.attach(map);
2379
+ map.on("moveend", () => {
2380
+ if (!map) return;
2381
+ const v = map.getView();
2382
+ const payload = {
2383
+ center: v.getCenter(),
2384
+ zoom: v.getZoom() ?? 0
2385
+ };
2386
+ events.emit("camera:changed", payload);
2387
+ const ex = v.calculateExtent(map.getSize() ?? void 0);
2388
+ events.emit("extent:changed", { extent: ex });
2389
+ });
2390
+ map.on("singleclick", (evt) => {
2391
+ if (!map) return;
2392
+ const features = [];
2393
+ map.forEachFeatureAtPixel(
2394
+ evt.pixel,
2395
+ (f, l) => {
2396
+ if (!f || !isPickableLayer(l)) return void 0;
2397
+ features.push({
2398
+ feature: f,
2399
+ layer: l,
2400
+ featureId: f.getId(),
2401
+ layerId: l.get("id"),
2402
+ properties: f.getProperties()
2403
+ });
2404
+ return void 0;
2405
+ },
2406
+ { hitTolerance: 5, layerFilter: isPickableLayer }
2407
+ );
2408
+ const clickCoordinate = evt.coordinate;
2409
+ events.emit("pointer:click", features.length ? { features, clickCoordinate } : { clickCoordinate, features: null });
2410
+ });
2411
+ },
2412
+ destroy() {
2413
+ draw.detach();
2414
+ geo.detach();
2415
+ controls.detach();
2416
+ map?.setTarget(void 0);
2417
+ map = void 0;
2418
+ registry.clear();
2419
+ bases.clear();
2420
+ select.detach();
2421
+ },
2422
+ getCamera() {
2423
+ const v = map.getView();
2424
+ return {
2425
+ center: v.getCenter(),
2426
+ zoom: v.getZoom() ?? 0
2427
+ };
2428
+ },
2429
+ getExtent() {
2430
+ const v = map.getView();
2431
+ return v.calculateExtent(map.getSize() ?? void 0);
2432
+ },
2433
+ setCenter(center) {
2434
+ map.getView().setCenter(center);
2435
+ },
2436
+ setZoom(zoom2) {
2437
+ map.getView().setZoom(zoom2);
2438
+ },
2439
+ fitExtent(ext, padding) {
2440
+ map.getView().fit(ext, { padding: [padding, padding, padding, padding] });
2441
+ },
2442
+ // layers
2443
+ listLayerIds: () => registry.listIds(),
2444
+ listBaseLayerIds: () => registry.listIds().filter((id) => {
2445
+ const l = registry.get(id);
2446
+ return !!l && bases.isBase(l);
2447
+ }),
2448
+ listOverlayLayerIds: () => registry.listIds().filter((id) => {
2449
+ const l = registry.get(id);
2450
+ return !!l && !bases.isBase(l);
2451
+ }),
2452
+ getLayerById: (id) => registry.get(id) ?? null,
2453
+ addLayer(def) {
2454
+ const layer = toOlLayer(def);
2455
+ layer.set("id", def.id);
2456
+ layer.set("nbic:pickable", def?.pickable !== false);
2457
+ map.addLayer(layer);
2458
+ registry.add(def.id, layer);
2459
+ if (def.base) {
2460
+ const role = def.base === "super" ? "super" : "regional";
2461
+ bases.registerBase(layer, role, def);
2462
+ } else if (def.zIndexPinned && def.zIndex !== void 0) {
2463
+ layer.setZIndex(def.zIndex);
2464
+ }
2465
+ },
2466
+ removeLayer(id) {
2467
+ const l = registry.get(id);
2468
+ if (!l) return;
2469
+ map.removeLayer(l);
2470
+ registry.remove(id);
2471
+ bases.onRemoved(id);
2472
+ },
2473
+ adoptLayer(id, layer, opts) {
2474
+ if (!map || !layer) return;
2475
+ layer.set("id", id);
2476
+ layer.set("nbic:external", true);
2477
+ layer.set("nbic:role", opts?.role ?? "overlay");
2478
+ layer.set("nbic:pickable", opts?.pickable !== false);
2479
+ map.addLayer(layer);
2480
+ registry.add(id, layer);
2481
+ if (opts?.base) {
2482
+ const role = opts.base === "super" ? "super" : "regional";
2483
+ bases.registerBase(layer, role, { id, base: role });
2484
+ } else if (opts?.zIndex !== void 0) {
2485
+ layer.setZIndex(opts.zIndex);
2486
+ }
2487
+ },
2488
+ ejectLayer(id) {
2489
+ const l = registry.get(id);
2490
+ if (!l) return;
2491
+ map.removeLayer(l);
2492
+ registry.remove(id);
2493
+ bases.onRemoved(id);
2494
+ },
2495
+ getFeatureCount(layerId) {
2496
+ const src = registry.getVectorSource(layerId);
2497
+ if (!src) return 0;
2498
+ const feats = src.getFeatures();
2499
+ if (!feats.length) return 0;
2500
+ if (src instanceof Cluster2) {
2501
+ const maybeCluster = src.getSource?.();
2502
+ if (maybeCluster && typeof maybeCluster.getFeatures === "function") {
2503
+ return maybeCluster.getFeatures().length;
2504
+ }
2505
+ }
2506
+ return feats.length;
2507
+ },
2508
+ getFeatures(layerId) {
2509
+ const src = registry.getVectorSource(layerId);
2510
+ if (!src) return [];
2511
+ if (src instanceof Cluster2) {
2512
+ const maybeCluster = src.getSource?.();
2513
+ if (maybeCluster && typeof maybeCluster.getFeatures === "function") {
2514
+ return maybeCluster.getFeatures();
2515
+ }
2516
+ }
2517
+ return src.getFeatures();
2518
+ },
2519
+ adoptControl(control, id) {
2520
+ return controls.adopt(control, id);
2521
+ },
2522
+ removeControl(id) {
2523
+ return controls.remove(id);
2524
+ },
2525
+ listCustomControlIds() {
2526
+ return controls.listIds();
2527
+ },
2528
+ getCustomControl(id) {
2529
+ return controls.get(id);
2530
+ },
2531
+ updateGeoJSONLayer(layerId, geojson, opts) {
2532
+ const layer = registry.get(layerId);
2533
+ if (!map || !layer) return false;
2534
+ const viewProj = String(map.getView().getProjection().getCode());
2535
+ const dataProj = opts?.dataProjection ?? layer.get("nbic:dataProjection") ?? viewProj;
2536
+ const fmt = new GeoJSON4();
2537
+ const text = typeof geojson === "string" ? geojson : JSON.stringify(geojson);
2538
+ const incoming = fmt.readFeatures(text, {
2539
+ dataProjection: dataProj,
2540
+ featureProjection: viewProj
2541
+ });
2542
+ const idProp = opts?.idProperty;
2543
+ if (idProp) {
2544
+ for (const f of incoming) {
2545
+ const id = f.get(idProp);
2546
+ if (id !== void 0) f.setId(id);
2547
+ }
2548
+ }
2549
+ const currentSrc = layer.getSource();
2550
+ const clusterMeta = layer.get("nbic:cluster");
2551
+ const isClustered = currentSrc instanceof Cluster2;
2552
+ const curVector = isClustered ? currentSrc.getSource() : currentSrc;
2553
+ const mode = opts?.mode ?? "replace";
2554
+ let nextVector;
2555
+ if (mode === "replace") {
2556
+ nextVector = new VectorSource8({ features: incoming });
2557
+ } else {
2558
+ const existing = curVector?.getFeatures() ?? [];
2559
+ const byId = /* @__PURE__ */ new Map();
2560
+ for (const f of existing) {
2561
+ const id = f.getId();
2562
+ if (id !== void 0 && id !== null) byId.set(id, f);
2563
+ }
2564
+ const merged = [];
2565
+ const seen = /* @__PURE__ */ new Set();
2566
+ for (const nf of incoming) {
2567
+ const id = nf.getId();
2568
+ if (id === void 0 || id === null) {
2569
+ merged.push(nf);
2570
+ continue;
2571
+ }
2572
+ seen.add(id);
2573
+ const cur = byId.get(id);
2574
+ if (!cur) {
2575
+ merged.push(nf);
2576
+ } else {
2577
+ const g = nf.getGeometry();
2578
+ if (g) cur.setGeometry(g);
2579
+ const props = nf.getProperties();
2580
+ delete props.geometry;
2581
+ cur.setProperties(props);
2582
+ if (!opts?.keepStyles) {
2583
+ const s = nf.get("nbic:style");
2584
+ if (s) cur.setStyle(cur.getStyle());
2585
+ }
2586
+ merged.push(cur);
2587
+ }
2588
+ }
2589
+ for (const f of existing) {
2590
+ const id = f.getId();
2591
+ if (id === void 0 || id === null) merged.push(f);
2592
+ }
2593
+ nextVector = new VectorSource8({ features: merged });
2594
+ }
2595
+ let nextSource;
2596
+ if (isClustered || clusterMeta) {
2597
+ const distance = currentSrc instanceof Cluster2 ? currentSrc.getDistance?.() ?? clusterMeta?.distance ?? 40 : clusterMeta?.distance ?? 40;
2598
+ const minDistance = currentSrc instanceof Cluster2 ? currentSrc.getMinDistance?.() ?? clusterMeta?.minDistance ?? 0 : clusterMeta?.minDistance ?? 0;
2599
+ nextSource = new Cluster2({
2600
+ distance,
2601
+ minDistance,
2602
+ source: nextVector
2603
+ });
2604
+ } else {
2605
+ nextSource = nextVector;
2606
+ }
2607
+ layer.setSource(nextSource);
2608
+ return true;
2609
+ },
2610
+ setLayerVisibility(id, visible) {
2611
+ const l = registry.get(id);
2612
+ if (!l) return;
2613
+ if (!bases.isBase(l)) {
2614
+ l.setVisible(visible);
2615
+ return;
2616
+ }
2617
+ bases.setVisibility(l, visible);
2618
+ },
2619
+ setActiveBase(id) {
2620
+ bases.setActiveRegional(id);
2621
+ },
2622
+ reorderLayers(order) {
2623
+ registry.reorder(order, bases);
2624
+ },
2625
+ // hover info
2626
+ activateHoverInfo(options) {
2627
+ if (!map) return;
2628
+ if (!hover) hover = new HoverInfoController(map, events);
2629
+ hover.activate(options);
2630
+ },
2631
+ deactivateHoverInfo() {
2632
+ hover?.deactivate();
2633
+ },
2634
+ // picking
2635
+ pickAt(pixel) {
2636
+ if (!map) return null;
2637
+ const coordinate = map.getCoordinateFromPixel(pixel);
2638
+ if (!coordinate) return null;
2639
+ return map.forEachFeatureAtPixel(
2640
+ pixel,
2641
+ (feature, layer) => {
2642
+ if (!isPickableLayer(layer)) return void 0;
2643
+ return {
2644
+ layerId: layer.get("id"),
2645
+ featureId: feature.getId(),
2646
+ properties: feature.getProperties(),
2647
+ coordinate
2648
+ };
2649
+ },
2650
+ { hitTolerance: 5, layerFilter: isPickableLayer }
2651
+ ) ?? null;
2652
+ },
2653
+ // drawing
2654
+ startDrawing: (opts) => draw.start(opts),
2655
+ stopDrawing: () => draw.stop(),
2656
+ enableDrawEditing: (opts) => draw.enableEditing(opts),
2657
+ disableDrawEditing: () => draw.disableEditing(),
2658
+ undoLastPoint: () => draw.undoLastPoint(),
2659
+ undoEdit: () => draw.undoEdit(),
2660
+ finishCurrent: () => draw.finishCurrent(),
2661
+ abortCurrent: () => draw.abortCurrent(),
2662
+ clearDrawn: () => draw.clear(),
2663
+ exportDrawnGeoJSON: (opts) => draw.exportGeoJSON(map, opts),
2664
+ importDrawnGeoJSON: (geojson, opts) => draw.importGeoJSON(map, geojson, opts),
2665
+ setDrawnFeatureStyle: (feature, style) => draw.setFeatureStyle(feature, style),
2666
+ clearDrawnFeatureStyle: (feature) => draw.clearFeatureStyle(feature),
2667
+ setFeatureStyle(layerId, featureId, style) {
2668
+ const layer = registry.get(layerId);
2669
+ if (!layer) return false;
2670
+ const src = resolveVectorSource(layer);
2671
+ if (!src) return false;
2672
+ const feat = src.getFeatureById(featureId);
2673
+ if (!feat) return false;
2674
+ feat.set("nbic:style", style);
2675
+ feat.setStyle(makeDrawStyle(style));
2676
+ return true;
2677
+ },
2678
+ clearFeatureStyle(layerId, featureId) {
2679
+ const layer = registry.get(layerId);
2680
+ if (!layer) return false;
2681
+ const src = resolveVectorSource(layer);
2682
+ if (!src) return false;
2683
+ const feat = src.getFeatureById(featureId);
2684
+ if (!feat) return false;
2685
+ feat.unset("nbic:style", true);
2686
+ feat.setStyle(void 0);
2687
+ return true;
2688
+ },
2689
+ // geometry analysis
2690
+ analyzeSelfIntersections: async (feature) => {
2691
+ if (!map) return { valid: true };
2692
+ const { checkSelfIntersections } = await import("./geometry-analysis-ERCBMJB7.js");
2693
+ return checkSelfIntersections(map, feature);
2694
+ },
2695
+ // full-screen
2696
+ enterFullScreen() {
2697
+ if (!map) return;
2698
+ const el = map.getTargetElement();
2699
+ if (el && !document.fullscreenElement) {
2700
+ void el.requestFullscreen().catch(() => {
2701
+ });
2702
+ }
2703
+ },
2704
+ leaveFullScreen() {
2705
+ if (document.fullscreenElement) {
2706
+ void document.exitFullscreen().catch(() => {
2707
+ });
2708
+ }
2709
+ },
2710
+ // controls
2711
+ showScaleLine: () => controls.ensureScaleLine(map, events),
2712
+ hideScaleLine: () => controls.removeScaleLine(map, events),
2713
+ showZoomControl: () => controls.ensureZoom(map, events),
2714
+ hideZoomControl: () => controls.removeZoom(map, events),
2715
+ showAttribution: () => controls.ensureAttribution(map, events),
2716
+ hideAttribution: () => controls.removeAttribution(map, events),
2717
+ // geolocation
2718
+ activateGeolocation: (follow) => geo.activate(follow),
2719
+ deactivateGeolocation: () => geo.deactivate(),
2720
+ zoomToGeolocation: (maxZoom) => geo.zoomToGeolocation(maxZoom),
2721
+ // vector layers
2722
+ getVectorLayerSource: (layerId) => registry.getVectorSource(layerId),
2723
+ addPoint(layerId, coordinate, props, style, opts) {
2724
+ const src = registry.getVectorSource(layerId);
2725
+ if (!map || !src) return false;
2726
+ const viewCoord = toViewCoord(map, coordinate, opts?.dataProjection);
2727
+ const f = draw.createPointFeature(viewCoord, props, style);
2728
+ src.addFeature(f);
2729
+ return true;
2730
+ },
2731
+ removeAllFromLayer(layerId) {
2732
+ const src = registry.getVectorSource(layerId);
2733
+ if (!src) return false;
2734
+ src.clear(true);
2735
+ return true;
2736
+ },
2737
+ // Transform coordinates
2738
+ transformCoordsFrom: (coord, from, to) => {
2739
+ return transformCoordsFrom(coord, from, to);
2740
+ },
2741
+ transformCoordsArrayFrom: (coords, from, to) => {
2742
+ return transformCoordsArrayFrom(coords, from, to);
2743
+ },
2744
+ transformExtentFrom: (extent, from, to) => {
2745
+ return transformExtentFrom(extent, from, to);
2746
+ },
2747
+ // Zoom
2748
+ zoomToFeature: (layerId, featureId, opts) => zoom.zoomToFeatureById(registry, layerId, featureId, opts),
2749
+ zoomToLayer: (layerId, opts) => zoom.zoomToLayerById(registry, layerId, opts),
2750
+ zoomToExtent: (extent, opts) => zoom.zoomToExtent(extent, opts),
2751
+ fitGeometry: (geom, opts) => zoom.fitGeometry(geom, opts),
2752
+ // Select
2753
+ selectFeature: (layerId, featureId, style, coordinates) => select.selectById(registry, layerId, featureId, style, coordinates),
2754
+ clearSelection: () => select.clear(registry),
2755
+ getSelection: () => select.getSelection(),
2756
+ // Utils
2757
+ getTargetElement: () => {
2758
+ return map?.getTargetElement() ?? null;
2759
+ },
2760
+ getCenterFromExtent: (extent) => {
2761
+ return getCenter3(extent);
2762
+ }
2763
+ };
2764
+ }
2765
+
2766
+ // src/api/MapAPI.ts
2767
+ var MapAPI = class {
2768
+ engine;
2769
+ events;
2770
+ layerDefs = /* @__PURE__ */ new Map();
2771
+ constructor(init, engine) {
2772
+ this.events = createEmitter();
2773
+ this.engine = engine ?? createOlEngine(this.events);
2774
+ this.engine.init(init).then(() => {
2775
+ this.events.emit("ready", void 0);
2776
+ }).catch((err) => {
2777
+ this.events.emit("error", { scope: "init", message: String(err), detail: err });
2778
+ });
2779
+ }
2780
+ // Explicitly typed pass-throughs (avoid binding issues and keep types)
2781
+ on(type, cb) {
2782
+ return this.events.on(type, cb);
2783
+ }
2784
+ off(type, cb) {
2785
+ return this.events.off(type, cb);
2786
+ }
2787
+ once(type, cb) {
2788
+ return this.events.once(type, cb);
2789
+ }
2790
+ destroy() {
2791
+ this.engine.destroy();
2792
+ this.events.emit("destroy", void 0);
2793
+ }
2794
+ getCamera() {
2795
+ return this.engine.getCamera();
2796
+ }
2797
+ getExtent() {
2798
+ return this.engine.getExtent();
2799
+ }
2800
+ /** Return all layers marked as base (in insertion order). */
2801
+ getBaseLayerIds() {
2802
+ return this.engine.listBaseLayerIds();
2803
+ }
2804
+ getOverlayLayerIds() {
2805
+ return this.engine.listOverlayLayerIds();
2806
+ }
2807
+ /** If you still want full LayerDef[] for config-only layers, keep these: */
2808
+ getBaseLayers() {
2809
+ return Array.from(this.layerDefs.values()).filter((l) => !!l.base);
2810
+ }
2811
+ getOverlayLayers() {
2812
+ return Array.from(this.layerDefs.values()).filter((l) => !l.base);
2813
+ }
2814
+ /** Or add engine-backed variants that return live OL layers: */
2815
+ getLayerById(id) {
2816
+ return this.engine.getLayerById(id);
2817
+ }
2818
+ adoptControl(control, id) {
2819
+ return this.engine.adoptControl(control, id);
2820
+ }
2821
+ removeControl(id) {
2822
+ return this.engine.removeControl(id);
2823
+ }
2824
+ listCustomControlIds() {
2825
+ return this.engine.listCustomControlIds();
2826
+ }
2827
+ getCustomControl(id) {
2828
+ return this.engine.getCustomControl(id);
2829
+ }
2830
+ setCenter(center) {
2831
+ this.engine.setCenter(center);
2832
+ }
2833
+ setZoom(zoom) {
2834
+ this.engine.setZoom(zoom);
2835
+ }
2836
+ fitExtent(extent, padding = 16) {
2837
+ this.engine.fitExtent(extent, padding);
2838
+ }
2839
+ addLayer(layer) {
2840
+ this.layerDefs.set(layer.id, layer);
2841
+ this.engine.addLayer(layer);
2842
+ this.events.emit("layer:added", { layerId: layer.id });
2843
+ }
2844
+ removeLayer(layerId) {
2845
+ this.layerDefs.delete(layerId);
2846
+ this.engine.removeLayer(layerId);
2847
+ this.events.emit("layer:removed", { layerId });
2848
+ }
2849
+ updateGeoJSONLayer(layerId, geojson, opts) {
2850
+ this.engine.updateGeoJSONLayer(layerId, geojson, opts);
2851
+ }
2852
+ setLayerVisibility(layerId, visible) {
2853
+ this.engine.setLayerVisibility(layerId, visible);
2854
+ }
2855
+ reorderLayers(order) {
2856
+ this.engine.reorderLayers(order);
2857
+ }
2858
+ addPoint(layerId, coord, properties, style, opts) {
2859
+ return this.engine.addPoint(layerId, coord, properties, style, opts);
2860
+ }
2861
+ removeAllFromLayer(layerId) {
2862
+ return this.engine.removeAllFromLayer(layerId);
2863
+ }
2864
+ adoptLayer(id, layer, opts) {
2865
+ this.engine.adoptLayer(id, layer, opts);
2866
+ }
2867
+ ejectLayer(id) {
2868
+ this.engine.ejectLayer(id);
2869
+ this.layerDefs.delete(id);
2870
+ this.events.emit("layer:removed", { layerId: id });
2871
+ }
2872
+ getFeatureCount(layerId) {
2873
+ return this.engine.getFeatureCount(layerId);
2874
+ }
2875
+ getFeatures(layerId) {
2876
+ return this.engine.getFeatures(layerId);
2877
+ }
2878
+ activateHoverInfo(options) {
2879
+ this.engine.activateHoverInfo(options);
2880
+ }
2881
+ deactivateHoverInfo() {
2882
+ this.engine.deactivateHoverInfo();
2883
+ }
2884
+ pickAt(pixel) {
2885
+ return this.engine.pickAt(pixel);
2886
+ }
2887
+ startDrawing(opts) {
2888
+ this.engine.startDrawing(opts);
2889
+ }
2890
+ stopDrawing() {
2891
+ this.engine.stopDrawing();
2892
+ }
2893
+ enableDrawEditing(opts) {
2894
+ this.engine.enableDrawEditing(opts);
2895
+ }
2896
+ disableDrawEditing() {
2897
+ this.engine.disableDrawEditing();
2898
+ }
2899
+ setDrawnFeatureStyle(feature, style) {
2900
+ return this.engine.setDrawnFeatureStyle(feature, style);
2901
+ }
2902
+ clearDrawnFeatureStyle(feature) {
2903
+ return this.engine.clearDrawnFeatureStyle(feature);
2904
+ }
2905
+ setFeatureStyle(layerId, featureId, style) {
2906
+ return this.engine.setFeatureStyle(layerId, featureId, style);
2907
+ }
2908
+ clearFeatureStyle(layerId, featureId) {
2909
+ return this.engine.clearFeatureStyle(layerId, featureId);
2910
+ }
2911
+ undoLastPoint() {
2912
+ this.engine.undoLastPoint();
2913
+ }
2914
+ undoEdit() {
2915
+ this.engine.undoEdit();
2916
+ }
2917
+ finishCurrent() {
2918
+ this.engine.finishCurrent();
2919
+ }
2920
+ abortCurrent() {
2921
+ this.engine.abortCurrent();
2922
+ }
2923
+ clearDrawn() {
2924
+ this.engine.clearDrawn();
2925
+ }
2926
+ exportDrawnGeoJSON(opts) {
2927
+ return this.engine.exportDrawnGeoJSON(opts);
2928
+ }
2929
+ importDrawnGeoJSON(text, opts) {
2930
+ this.engine.importDrawnGeoJSON(text, opts);
2931
+ }
2932
+ // Geometry analysis
2933
+ analyzeSelfIntersections(feature) {
2934
+ return this.engine.analyzeSelfIntersections(feature);
2935
+ }
2936
+ enterFullScreen() {
2937
+ this.engine.enterFullScreen();
2938
+ }
2939
+ leaveFullScreen() {
2940
+ this.engine.leaveFullScreen();
2941
+ }
2942
+ showScaleLine() {
2943
+ this.engine.showScaleLine();
2944
+ }
2945
+ hideScaleLine() {
2946
+ this.engine.hideScaleLine();
2947
+ }
2948
+ showZoomControl() {
2949
+ this.engine.showZoomControl();
2950
+ }
2951
+ hideZoomControl() {
2952
+ this.engine.hideZoomControl();
2953
+ }
2954
+ showAttribution() {
2955
+ this.engine.showAttribution();
2956
+ }
2957
+ hideAttribution() {
2958
+ this.engine.hideAttribution();
2959
+ }
2960
+ activateGeolocation(follow) {
2961
+ this.engine.activateGeolocation(follow);
2962
+ }
2963
+ deactivateGeolocation() {
2964
+ this.engine.deactivateGeolocation();
2965
+ }
2966
+ zoomToGeolocation(maxZoom) {
2967
+ return this.engine.zoomToGeolocation(maxZoom);
2968
+ }
2969
+ // Transform coordinates
2970
+ transformCoordsFrom(coord, from, to) {
2971
+ return this.engine.transformCoordsFrom(coord, from, to);
2972
+ }
2973
+ transformCoordsArrayFrom(coords, from, to) {
2974
+ return this.engine.transformCoordsArrayFrom(coords, from, to);
2975
+ }
2976
+ transformExtentFrom(extent, from, to) {
2977
+ return this.engine.transformExtentFrom(extent, from, to);
2978
+ }
2979
+ // Zoom
2980
+ zoomToFeature(layerId, featureId, opts) {
2981
+ return this.engine.zoomToFeature(layerId, featureId, opts);
2982
+ }
2983
+ zoomToLayer(layerId, opts) {
2984
+ return this.engine.zoomToLayer(layerId, opts);
2985
+ }
2986
+ zoomToExtent(extent, opts) {
2987
+ return this.engine.zoomToExtent(extent, opts);
2988
+ }
2989
+ fitGeometry(geom, opts) {
2990
+ return this.engine.fitGeometry(geom, opts);
2991
+ }
2992
+ // Select
2993
+ selectFeature(layerId, featureId, style, coordinates) {
2994
+ return this.engine.selectFeature(layerId, featureId, style, coordinates);
2995
+ }
2996
+ clearSelection() {
2997
+ this.engine.clearSelection();
2998
+ }
2999
+ getSelection() {
3000
+ return this.engine.getSelection();
3001
+ }
3002
+ // Utils
3003
+ getTargetElement() {
3004
+ return this.engine.getTargetElement();
3005
+ }
3006
+ getCenterFromExtent(extent) {
3007
+ return this.engine.getCenterFromExtent(extent);
3008
+ }
3009
+ };
3010
+
3011
+ // src/api/config.schema.ts
3012
+ import { z } from "zod";
3013
+ var SourceRefSchema = z.object({ ref: z.string().min(1) });
3014
+ var InlineSourceSchema = z.object({
3015
+ type: z.enum(["wmts", "wfs", "osm", "xyz", "geojson"]),
3016
+ options: z.record(z.string(), z.unknown()).optional()
3017
+ });
3018
+ var MapConfigSchema = z.object({
3019
+ version: z.literal(1),
3020
+ id: z.string().min(1),
3021
+ projection: z.string().default("EPSG:3857"),
3022
+ center: z.tuple([z.number(), z.number()]),
3023
+ zoom: z.number(),
3024
+ minZoom: z.number().optional(),
3025
+ maxZoom: z.number(),
3026
+ size: z.enum(["Large", "Small"]).optional(),
3027
+ tabIndex: z.number().int().optional(),
3028
+ controls: z.object({
3029
+ zoom: z.boolean().optional(),
3030
+ scaleBar: z.boolean().optional(),
3031
+ scaleLine: z.boolean().optional(),
3032
+ geolocation: z.boolean().optional(),
3033
+ attribution: z.boolean().optional(),
3034
+ rotate: z.boolean().optional(),
3035
+ fullscreen: z.boolean().optional(),
3036
+ mousePosition: z.boolean().optional(),
3037
+ draw: z.object({
3038
+ enabled: z.boolean(),
3039
+ polygonType: z.enum(["box", "custom"]).optional()
3040
+ }).optional()
3041
+ }).default({}),
3042
+ wmts: z.array(z.object({
3043
+ id: z.string(),
3044
+ title: z.string().optional(),
3045
+ url: z.string().url(),
3046
+ layer: z.string(),
3047
+ matrixSet: z.string(),
3048
+ format: z.string().optional(),
3049
+ style: z.string().optional(),
3050
+ tileSize: z.number().optional(),
3051
+ origin: z.tuple([z.number(), z.number()]).optional(),
3052
+ resolutions: z.array(z.number()).optional()
3053
+ })).optional(),
3054
+ wfs: z.array(z.object({
3055
+ id: z.string(),
3056
+ title: z.string().optional(),
3057
+ url: z.string().url(),
3058
+ typeName: z.string(),
3059
+ srsName: z.string().optional(),
3060
+ outputFormat: z.string().optional(),
3061
+ strategy: z.enum(["all", "bbox"]).default("bbox").optional(),
3062
+ maxFeatures: z.number().int().optional(),
3063
+ styleId: z.string().optional()
3064
+ })).optional(),
3065
+ layers: z.array(z.object({
3066
+ id: z.string(),
3067
+ kind: z.enum(["tile", "vector", "raster"]),
3068
+ source: z.union([SourceRefSchema, InlineSourceSchema]),
3069
+ // ← key bit
3070
+ style: z.union([
3071
+ z.string(),
3072
+ // style ref id
3073
+ z.object({ type: z.string(), options: z.record(z.string(), z.unknown()).optional() })
3074
+ ]).optional(),
3075
+ visible: z.boolean().optional(),
3076
+ zIndex: z.number().optional(),
3077
+ base: z.boolean().optional()
3078
+ })).optional()
3079
+ }).refine((v) => v.minZoom === void 0 || v.minZoom <= v.maxZoom, {
3080
+ path: ["minZoom"],
3081
+ message: "minZoom must be \u2264 maxZoom"
3082
+ });
3083
+
3084
+ // src/core/registry/SourceRegistry.ts
3085
+ var SourceRegistry = class {
3086
+ static registry = /* @__PURE__ */ new Map();
3087
+ static register(id, factory) {
3088
+ this.registry.set(id, factory);
3089
+ }
3090
+ static get(id) {
3091
+ const factory = this.registry.get(id);
3092
+ if (!factory) {
3093
+ throw new Error(`Source '${id}' not found in SourceRegistry`);
3094
+ }
3095
+ return factory();
3096
+ }
3097
+ static clear() {
3098
+ this.registry.clear();
3099
+ }
3100
+ };
3101
+
3102
+ // src/api/config-loader.ts
3103
+ function initMapFromConfig(map, rawJson) {
3104
+ const cfg = MapConfigSchema.parse(rawJson);
3105
+ cfg.wmts?.forEach(
3106
+ (w) => SourceRegistry.register(w.id, () => ({ type: "wmts", options: w }))
3107
+ );
3108
+ cfg.wfs?.forEach(
3109
+ (w) => SourceRegistry.register(w.id, () => ({ type: "wfs", options: w }))
3110
+ );
3111
+ cfg.layers?.forEach((layer) => {
3112
+ const source = "ref" in layer.source ? { ref: layer.source.ref } : (() => {
3113
+ switch (layer.source.type) {
3114
+ case "osm":
3115
+ return { type: "osm" };
3116
+ case "xyz":
3117
+ return { type: "xyz", options: layer.source.options ?? {} };
3118
+ case "wmts":
3119
+ return { type: "wmts", options: layer.source.options ?? {} };
3120
+ case "wfs":
3121
+ return { type: "wfs", options: layer.source.options ?? {} };
3122
+ case "geojson":
3123
+ return { type: "geojson", options: layer.source.options ?? {} };
3124
+ default: {
3125
+ const src = layer.source;
3126
+ const type = typeof src === "object" && src !== null && "type" in src ? src.type : "undefined";
3127
+ throw new Error(`Unknown source type: ${type}`);
3128
+ }
3129
+ }
3130
+ })();
3131
+ const style = typeof layer.style === "string" ? { ref: layer.style } : layer.style;
3132
+ const ld = {
3133
+ id: layer.id,
3134
+ kind: layer.kind,
3135
+ source,
3136
+ style,
3137
+ visible: layer.visible ?? true,
3138
+ zIndex: layer.zIndex,
3139
+ base: layer.base ?? false
3140
+ };
3141
+ map.addLayer(ld);
3142
+ });
3143
+ }
3144
+
3145
+ // src/factory.ts
3146
+ function createMap(target, rawConfig) {
3147
+ const cfg = MapConfigSchema.parse(rawConfig);
3148
+ const map = new MapAPI({
3149
+ target,
3150
+ projection: cfg.projection,
3151
+ center: cfg.center,
3152
+ zoom: cfg.zoom,
3153
+ minZoom: cfg.minZoom,
3154
+ maxZoom: cfg.maxZoom,
3155
+ controls: cfg.controls
3156
+ });
3157
+ initMapFromConfig(map, cfg);
3158
+ return map;
3159
+ }
3160
+
3161
+ // src/core/geojson/index.ts
3162
+ var geojson_exports = {};
3163
+ __export(geojson_exports, {
3164
+ sumProperty: () => sumProperty,
3165
+ toFeature: () => toFeature,
3166
+ toFeatureCollection: () => toFeatureCollection
3167
+ });
3168
+
3169
+ // src/core/geojson/builders.ts
3170
+ function isFiniteNumber(n) {
3171
+ return typeof n === "number" && Number.isFinite(n);
3172
+ }
3173
+ function validLonLat(lon, lat) {
3174
+ return isFiniteNumber(lon) && isFiniteNumber(lat) && lon >= -180 && lon <= 180 && lat >= -90 && lat <= 90;
3175
+ }
3176
+ function toFeature(item, index, opts) {
3177
+ const skipInvalid = opts.skipInvalid ?? true;
3178
+ let geometry = null;
3179
+ if (opts.kind === "Point") {
3180
+ const p = opts.getPoint(item, index);
3181
+ if (!p || !validLonLat(p.lon, p.lat)) {
3182
+ if (skipInvalid) return null;
3183
+ geometry = { type: "Point", coordinates: [0, 0] };
3184
+ } else {
3185
+ geometry = { type: "Point", coordinates: [p.lon, p.lat] };
3186
+ }
3187
+ } else if (opts.kind === "LineString") {
3188
+ const coords = opts.getLine(item, index);
3189
+ if (!coords || coords.length < 2) {
3190
+ if (skipInvalid) return null;
3191
+ geometry = { type: "LineString", coordinates: [] };
3192
+ } else {
3193
+ geometry = { type: "LineString", coordinates: coords };
3194
+ }
3195
+ } else {
3196
+ const rings = opts.getPolygon(item, index);
3197
+ if (!rings || !rings.length || !rings[0]?.length) {
3198
+ if (skipInvalid) return null;
3199
+ geometry = { type: "Polygon", coordinates: [] };
3200
+ } else {
3201
+ geometry = { type: "Polygon", coordinates: rings };
3202
+ }
3203
+ }
3204
+ const properties = opts.props?.(item, index) ?? {};
3205
+ const f = { type: "Feature", properties, geometry };
3206
+ const id = opts.id?.(item, index);
3207
+ if (id !== void 0) f.id = id;
3208
+ return f;
3209
+ }
3210
+ function toFeatureCollection(items, opts) {
3211
+ const filterNull = opts.filterNull ?? true;
3212
+ const features = items.map((item, i) => toFeature(item, i, opts)).filter((f) => filterNull ? !!f : true);
3213
+ return { type: "FeatureCollection", features };
3214
+ }
3215
+ function sumProperty(features, field = "count") {
3216
+ let total = 0;
3217
+ for (const f of features) {
3218
+ const v = f.properties?.[field];
3219
+ const n = Number(v);
3220
+ total += Number.isFinite(n) ? n : 1;
3221
+ }
3222
+ return total;
3223
+ }
3224
+
3225
+ // src/core/ol/utils/index.ts
3226
+ var utils_exports = {};
3227
+ __export(utils_exports, {
3228
+ areaFromLonLatRing: () => areaFromLonLatRing,
3229
+ areaToString: () => areaToString,
3230
+ ensureClosedRing: () => ensureClosedRing,
3231
+ getCenterFromExtent: () => getCenterFromExtent,
3232
+ getCenterFromPolygon: () => getCenterFromPolygon,
3233
+ getCentroid: () => getCentroid,
3234
+ getExtentFromPolygon: () => getExtentFromPolygon,
3235
+ isExtentInside: () => isExtentInside,
3236
+ isPointInsideExtent: () => isPointInsideExtent,
3237
+ toDataCoord: () => toDataCoord,
3238
+ toViewCoord: () => toViewCoord,
3239
+ transformCoordsArrayFrom: () => transformCoordsArrayFrom,
3240
+ transformCoordsFrom: () => transformCoordsFrom,
3241
+ transformExtentFrom: () => transformExtentFrom
3242
+ });
3243
+
3244
+ // src/core/ol/utils/area.ts
3245
+ import area from "@turf/area";
3246
+ import { Feature as Feature5 } from "ol";
3247
+ import { Polygon as Polygon2 } from "ol/geom";
3248
+ import { getCenter as getCenter4 } from "ol/extent";
3249
+ function ensureClosedRing(ring) {
3250
+ const first = ring[0];
3251
+ const last = ring[ring.length - 1];
3252
+ const isClosed = first[0] === last[0] && first[1] === last[1];
3253
+ return isClosed ? ring : [...ring, first];
3254
+ }
3255
+ function areaFromLonLatRing(ring) {
3256
+ if (!ring.length) return 0;
3257
+ const coords = ensureClosedRing(ring);
3258
+ const poly = {
3259
+ type: "Feature",
3260
+ properties: {},
3261
+ geometry: {
3262
+ type: "Polygon",
3263
+ coordinates: [coords]
3264
+ }
3265
+ };
3266
+ return area(poly);
3267
+ }
3268
+ function areaToString(m2) {
3269
+ if (m2 > 1e4) {
3270
+ return Math.round(m2 / 1e6 * 100) / 100 + " km\xB2";
3271
+ } else {
3272
+ return Math.round(m2 * 100) / 100 + " m\xB2";
3273
+ }
3274
+ }
3275
+ function getCenterFromPolygon(polygonCoordinates) {
3276
+ const closedRing = ensureClosedRing(polygonCoordinates);
3277
+ const polygon = new Feature5({
3278
+ geometry: new Polygon2([closedRing])
3279
+ });
3280
+ if (polygon) {
3281
+ const extent = polygon.getGeometry()?.getExtent();
3282
+ if (extent) {
3283
+ const coordinate = getCenter4(extent);
3284
+ return coordinate;
3285
+ }
3286
+ }
3287
+ return null;
3288
+ }
3289
+ function getCentroid(coordinates) {
3290
+ if (!coordinates || coordinates.length === 0) {
3291
+ return null;
3292
+ }
3293
+ let sumX = 0;
3294
+ let sumY = 0;
3295
+ for (const point of coordinates) {
3296
+ sumX += point[0];
3297
+ sumY += point[1];
3298
+ }
3299
+ const centroidX = sumX / coordinates.length;
3300
+ const centroidY = sumY / coordinates.length;
3301
+ return [centroidX, centroidY];
3302
+ }
3303
+ function getExtentFromPolygon(polygonCoordinates) {
3304
+ if (!polygonCoordinates?.length) return null;
3305
+ const closedRing = ensureClosedRing(polygonCoordinates);
3306
+ const polygon = new Feature5({
3307
+ geometry: new Polygon2([closedRing])
3308
+ });
3309
+ return polygon.getGeometry()?.getExtent() ?? null;
3310
+ }
3311
+ function isExtentInside(inner, outer) {
3312
+ if (inner === void 0 || outer === void 0) return false;
3313
+ if (inner.length !== 4 || outer.length !== 4) return false;
3314
+ return inner[0] >= outer[0] && // minX
3315
+ inner[1] >= outer[1] && // minY
3316
+ inner[2] <= outer[2] && // maxX
3317
+ inner[3] <= outer[3];
3318
+ }
3319
+ function isPointInsideExtent(coord, extent) {
3320
+ if (coord === void 0 || extent === void 0) return false;
3321
+ if (extent.length !== 4) return false;
3322
+ const [x, y] = coord;
3323
+ return x >= extent[0] && // minX
3324
+ y >= extent[1] && // minY
3325
+ x <= extent[2] && // maxX
3326
+ y <= extent[3];
3327
+ }
3328
+
3329
+ // src/presets/index.ts
3330
+ var presets_exports = {};
3331
+ __export(presets_exports, {
3332
+ administrativeEnheterFylke: () => administrativeEnheterFylke,
3333
+ administrativeEnheterKommune: () => administrativeEnheterKommune,
3334
+ janmayenBaseLayer: () => janmayenBaseLayer,
3335
+ nib: () => nib,
3336
+ norwayCoordinate: () => norwayCoordinate,
3337
+ osm: () => osm,
3338
+ svalbardBaseLayer: () => svalbardBaseLayer,
3339
+ topo4graatoneBaseLayer: () => topo4graatoneBaseLayer,
3340
+ topografiskBaseLayer: () => topografiskBaseLayer
3341
+ });
3342
+
3343
+ // src/presets/layers/base/baseLayers.ts
3344
+ var svalbardBaseLayer = {
3345
+ id: "svalbard",
3346
+ kind: "tile",
3347
+ base: "regional",
3348
+ visible: false,
3349
+ opacity: 1,
3350
+ source: {
3351
+ type: "wmts",
3352
+ options: {
3353
+ url: "https://geodata.npolar.no/arcgis/rest/services/Basisdata/NP_Basiskart_Svalbard_WMTS_32633/MapServer/WMTS?",
3354
+ layer: "Basisdata_NP_Basiskart_Svalbard_WMTS_32633",
3355
+ projection: "EPSG:32633",
3356
+ matrixSetId: "default028mm",
3357
+ // critical for ArcGIS service
3358
+ format: "image/jpgpng",
3359
+ style: "default",
3360
+ wrapX: true,
3361
+ // levels: 17,
3362
+ attribution: "\xA9 Kartverket / Norsk Polarinstitutt"
3363
+ }
3364
+ }
3365
+ };
3366
+ var janmayenBaseLayer = {
3367
+ id: "janmayen",
3368
+ kind: "tile",
3369
+ base: "regional",
3370
+ visible: false,
3371
+ opacity: 1,
3372
+ source: {
3373
+ type: "wmts",
3374
+ options: {
3375
+ url: "https://geodata.npolar.no/arcgis/rest/services/Basisdata/NP_Basiskart_JanMayen_WMTS_25833/MapServer/WMTS?",
3376
+ layer: "NP_Basiskart_JanMayen_WMTS_25833",
3377
+ projection: "EPSG:25833",
3378
+ matrixSetId: "default028mm",
3379
+ // critical for ArcGIS service
3380
+ format: "image/jpgpng",
3381
+ style: "default",
3382
+ // levels: 17,
3383
+ wrapX: true,
3384
+ attribution: "\xA9 Kartverket / Norsk Polarinstitutt"
3385
+ }
3386
+ }
3387
+ };
3388
+ var topografiskBaseLayer = {
3389
+ id: "topografisk",
3390
+ name: "Topografisk",
3391
+ kind: "tile",
3392
+ base: "regional",
3393
+ visible: true,
3394
+ source: {
3395
+ type: "wmts",
3396
+ options: {
3397
+ url: "https://cache.kartverket.no/v1/service",
3398
+ layer: "topo",
3399
+ format: "image/png",
3400
+ style: "default",
3401
+ matrixSet: "webmercator",
3402
+ //'utm33n',
3403
+ projection: "EPSG:3857",
3404
+ //'EPSG:25833',
3405
+ wrapX: true,
3406
+ opacity: 1,
3407
+ attribution: "Kartverket"
3408
+ }
3409
+ }
3410
+ };
3411
+ var topo4graatoneBaseLayer = {
3412
+ id: "topo4graatone",
3413
+ name: "Topografisk Gr\xE5tone",
3414
+ kind: "tile",
3415
+ base: "regional",
3416
+ visible: false,
3417
+ source: {
3418
+ type: "wmts",
3419
+ options: {
3420
+ url: "https://cache.kartverket.no/v1/service",
3421
+ layer: "topograatone",
3422
+ format: "image/png",
3423
+ style: "default",
3424
+ matrixSet: "webmercator",
3425
+ //'utm33n',
3426
+ projection: "EPSG:3857",
3427
+ //'EPSG:25833',
3428
+ wrapX: true,
3429
+ opacity: 1,
3430
+ attribution: "Kartverket"
3431
+ }
3432
+ }
3433
+ };
3434
+ var nib = {
3435
+ id: "nib",
3436
+ name: "Norge i Bilder",
3437
+ kind: "tile",
3438
+ base: "regional",
3439
+ visible: false,
3440
+ source: {
3441
+ type: "wmts",
3442
+ options: {
3443
+ url: "https://tilecache.norgeibilder.no/wmts/webmercator",
3444
+ layer: "Nibcache_web_mercator_v2",
3445
+ format: "image/png",
3446
+ style: "default",
3447
+ matrixSet: "GoogleMapsCompatible",
3448
+ projection: "EPSG:3857",
3449
+ wrapX: true,
3450
+ opacity: 1,
3451
+ attribution: "Kartverket"
3452
+ }
3453
+ }
3454
+ };
3455
+ var osm = {
3456
+ id: "osm",
3457
+ name: "OSM",
3458
+ kind: "tile",
3459
+ base: "super",
3460
+ visible: true,
3461
+ source: {
3462
+ type: "osm"
3463
+ }
3464
+ };
3465
+
3466
+ // src/presets/layers/overlay/overlayLayers.ts
3467
+ var norwayCoordinate = [10.4, 63.44];
3468
+ var administrativeEnheterKommune = {
3469
+ id: "kommune",
3470
+ kind: "vector",
3471
+ visible: true,
3472
+ base: false,
3473
+ source: {
3474
+ type: "wfs",
3475
+ options: {
3476
+ url: "https://wfs.geonorge.no/skwms1/wfs.administrative_enheter",
3477
+ outputFormat: "application/gml+xml; version=3.2",
3478
+ // or 'text/xml; subtype=gml/3.1.1'
3479
+ typeName: "app:Kommune",
3480
+ srsName: "EPSG:3857",
3481
+ minZoomToLoad: 1,
3482
+ strategy: "bbox"
3483
+ }
3484
+ },
3485
+ pickable: false,
3486
+ maxZoom: 14,
3487
+ minZoom: 7,
3488
+ zIndex: -2,
3489
+ zIndexPinned: true,
3490
+ hover: {
3491
+ style: { strokeColor: "transparent", fillColor: "transparent" }
3492
+ },
3493
+ style: { type: "simple", options: { strokeColor: "#6d6099", strokeWidth: 0.5, fillColor: "transparent" } }
3494
+ };
3495
+ var administrativeEnheterFylke = {
3496
+ id: "fylker",
3497
+ kind: "vector",
3498
+ visible: true,
3499
+ base: false,
3500
+ source: {
3501
+ type: "wfs",
3502
+ options: {
3503
+ url: "https://wfs.geonorge.no/skwms1/wfs.administrative_enheter",
3504
+ outputFormat: "application/gml+xml; version=3.2",
3505
+ // or 'text/xml; subtype=gml/3.1.1'
3506
+ typeName: "app:Fylke",
3507
+ srsName: "EPSG:3857",
3508
+ minZoomToLoad: 1,
3509
+ strategy: "bbox"
3510
+ }
3511
+ },
3512
+ pickable: false,
3513
+ maxZoom: 14,
3514
+ minZoom: 1,
3515
+ zIndex: -1,
3516
+ zIndexPinned: true,
3517
+ hover: {
3518
+ style: { strokeColor: "transparent", fillColor: "transparent" }
3519
+ },
3520
+ style: { type: "simple", options: { strokeColor: "#9e7795", strokeWidth: 1.5, fillColor: "transparent" } }
3521
+ };
3522
+
3523
+ // src/tokens/nibToken.ts
3524
+ var DEFAULT_CONFIG = {
3525
+ tokenUrl: "https://artskart.artsdatabanken.no/appapi/api/token/gettoken2",
3526
+ storageKey: "nbic-map-nib-token",
3527
+ expiryMarginMinutes: 10,
3528
+ retryIntervalMs: 6e4
3529
+ };
3530
+ function createNibToken(config) {
3531
+ const cfg = { ...DEFAULT_CONFIG, ...config };
3532
+ let currentToken = null;
3533
+ let refreshTimer = null;
3534
+ let isRefreshing = false;
3535
+ let refreshPromise = null;
3536
+ let abortController = null;
3537
+ let initialized = false;
3538
+ const listeners = [];
3539
+ function loadStoredToken() {
3540
+ try {
3541
+ if (typeof localStorage === "undefined") return null;
3542
+ const stored = localStorage.getItem(cfg.storageKey);
3543
+ if (!stored) return null;
3544
+ const parsed = JSON.parse(stored);
3545
+ if (typeof parsed.value === "string" && typeof parsed.expires === "number" && typeof parsed.when === "number") {
3546
+ return parsed;
3547
+ }
3548
+ return null;
3549
+ } catch {
3550
+ return null;
3551
+ }
3552
+ }
3553
+ function storeToken(token) {
3554
+ try {
3555
+ if (typeof localStorage === "undefined") return;
3556
+ localStorage.setItem(cfg.storageKey, JSON.stringify(token));
3557
+ } catch {
3558
+ }
3559
+ }
3560
+ function isTokenValid(token) {
3561
+ if (!token?.value || !token.expires) return false;
3562
+ const marginMs = cfg.expiryMarginMinutes * 60 * 1e3;
3563
+ return token.expires > Date.now() + marginMs;
3564
+ }
3565
+ async function fetchToken() {
3566
+ abortController = new AbortController();
3567
+ const response = await fetch(cfg.tokenUrl, {
3568
+ method: "GET",
3569
+ headers: {
3570
+ "Accept": "application/json"
3571
+ },
3572
+ signal: abortController.signal
3573
+ });
3574
+ if (!response.ok) {
3575
+ throw new Error(`Token fetch failed: ${response.status} ${response.statusText}`);
3576
+ }
3577
+ const data = await response.json();
3578
+ if (!data.TokenValue) {
3579
+ throw new Error("Invalid token response: missing TokenValue");
3580
+ }
3581
+ if (data.ClientId === "Could Not Get Client IP address" || data.TokenValue === "Could Not Get Client IP address") {
3582
+ throw new Error("Server could not determine client IP address for token generation");
3583
+ }
3584
+ return {
3585
+ value: data.TokenValue,
3586
+ expires: new Date(data.ExpireTimestamp).getTime(),
3587
+ when: Date.now()
3588
+ };
3589
+ }
3590
+ function notifyListeners(token) {
3591
+ for (const cb of listeners) {
3592
+ try {
3593
+ cb(token);
3594
+ } catch {
3595
+ }
3596
+ }
3597
+ }
3598
+ function scheduleRefresh() {
3599
+ if (refreshTimer) {
3600
+ clearTimeout(refreshTimer);
3601
+ refreshTimer = null;
3602
+ }
3603
+ if (!currentToken?.expires) return;
3604
+ const marginMs = cfg.expiryMarginMinutes * 60 * 1e3;
3605
+ const refreshIn = Math.max(
3606
+ currentToken.expires - Date.now() - marginMs,
3607
+ cfg.retryIntervalMs
3608
+ );
3609
+ refreshTimer = setTimeout(() => {
3610
+ refreshToken().catch(() => {
3611
+ });
3612
+ }, refreshIn);
3613
+ }
3614
+ async function init() {
3615
+ if (initialized) return;
3616
+ currentToken = loadStoredToken();
3617
+ if (isTokenValid(currentToken)) {
3618
+ initialized = true;
3619
+ scheduleRefresh();
3620
+ return;
3621
+ }
3622
+ try {
3623
+ await refreshToken();
3624
+ } catch {
3625
+ }
3626
+ initialized = true;
3627
+ }
3628
+ function getToken() {
3629
+ return currentToken?.value ?? "";
3630
+ }
3631
+ function getTokenInfo() {
3632
+ return currentToken ? { ...currentToken } : null;
3633
+ }
3634
+ async function refreshToken(force = false) {
3635
+ if (isRefreshing && refreshPromise) {
3636
+ return refreshPromise;
3637
+ }
3638
+ if (!force && currentToken?.when && Date.now() - currentToken.when < cfg.retryIntervalMs) {
3639
+ return currentToken?.value ?? "";
3640
+ }
3641
+ isRefreshing = true;
3642
+ refreshPromise = (async () => {
3643
+ try {
3644
+ const newToken = await fetchToken();
3645
+ currentToken = newToken;
3646
+ storeToken(newToken);
3647
+ notifyListeners(newToken.value);
3648
+ config?.onTokenFetched?.(newToken.value);
3649
+ scheduleRefresh();
3650
+ return newToken.value;
3651
+ } catch (error) {
3652
+ const err = error instanceof Error ? error : new Error(String(error));
3653
+ config?.onError?.(err);
3654
+ if (currentToken) {
3655
+ scheduleRefresh();
3656
+ }
3657
+ throw err;
3658
+ } finally {
3659
+ isRefreshing = false;
3660
+ refreshPromise = null;
3661
+ abortController = null;
3662
+ }
3663
+ })();
3664
+ return refreshPromise;
3665
+ }
3666
+ function onTokenChange(callback) {
3667
+ listeners.push(callback);
3668
+ return () => {
3669
+ const idx = listeners.indexOf(callback);
3670
+ if (idx >= 0) {
3671
+ listeners.splice(idx, 1);
3672
+ }
3673
+ };
3674
+ }
3675
+ function dispose() {
3676
+ if (refreshTimer) {
3677
+ clearTimeout(refreshTimer);
3678
+ refreshTimer = null;
3679
+ }
3680
+ if (abortController) {
3681
+ abortController.abort();
3682
+ abortController = null;
3683
+ }
3684
+ listeners.length = 0;
3685
+ currentToken = null;
3686
+ refreshPromise = null;
3687
+ isRefreshing = false;
3688
+ initialized = false;
3689
+ }
3690
+ function isInitialized() {
3691
+ return initialized;
3692
+ }
3693
+ return {
3694
+ init,
3695
+ getToken,
3696
+ getTokenInfo,
3697
+ refreshToken,
3698
+ onTokenChange,
3699
+ dispose,
3700
+ isInitialized
3701
+ };
3702
+ }
3703
+ export {
3704
+ MapEvents,
3705
+ MapAPI as NbicMapComponent,
3706
+ createMap,
3707
+ createNibToken,
3708
+ geojson_exports as nbicMapGeojson,
3709
+ presets_exports as nbicMapPresets,
3710
+ utils_exports as nbicMapUtils
3711
+ };