@kispace-io/gs-lib 1.1.8 → 1.2.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.
Files changed (61) hide show
  1. package/dist/base-map-builder.d.ts.map +1 -1
  2. package/dist/gs-model.d.ts +6 -0
  3. package/dist/gs-model.d.ts.map +1 -1
  4. package/dist/index.d.ts +3 -5
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +890 -288
  7. package/dist/index.js.map +1 -1
  8. package/dist/map-renderer.d.ts +94 -0
  9. package/dist/map-renderer.d.ts.map +1 -0
  10. package/dist/ml/gs-gs2ml.d.ts +96 -0
  11. package/dist/ml/gs-gs2ml.d.ts.map +1 -0
  12. package/dist/ml/gs-ml-adapters.d.ts +41 -0
  13. package/dist/ml/gs-ml-adapters.d.ts.map +1 -0
  14. package/dist/ml/gs-ml-lib.d.ts +17 -0
  15. package/dist/ml/gs-ml-lib.d.ts.map +1 -0
  16. package/dist/ml/gs-ml2gs.d.ts +10 -0
  17. package/dist/ml/gs-ml2gs.d.ts.map +1 -0
  18. package/dist/ml/gs-mlns.d.ts +10 -0
  19. package/dist/ml/gs-mlns.d.ts.map +1 -0
  20. package/dist/ml/index.d.ts +9 -0
  21. package/dist/ml/index.d.ts.map +1 -0
  22. package/dist/ml/maplibre-map-renderer.d.ts +66 -0
  23. package/dist/ml/maplibre-map-renderer.d.ts.map +1 -0
  24. package/dist/{gs-gs2ol.d.ts → ol/gs-gs2ol.d.ts} +2 -2
  25. package/dist/ol/gs-gs2ol.d.ts.map +1 -0
  26. package/dist/ol/gs-ol-adapters.d.ts.map +1 -0
  27. package/dist/{gs-lib.d.ts → ol/gs-ol-lib.d.ts} +4 -4
  28. package/dist/ol/gs-ol-lib.d.ts.map +1 -0
  29. package/dist/{gs-ol2gs.d.ts → ol/gs-ol2gs.d.ts} +1 -1
  30. package/dist/ol/gs-ol2gs.d.ts.map +1 -0
  31. package/dist/ol/gs-olns.d.ts.map +1 -0
  32. package/dist/ol/index.d.ts +9 -0
  33. package/dist/ol/index.d.ts.map +1 -0
  34. package/dist/ol/openlayers-map-renderer.d.ts +68 -0
  35. package/dist/ol/openlayers-map-renderer.d.ts.map +1 -0
  36. package/package.json +6 -2
  37. package/src/base-map-builder.ts +8 -9
  38. package/src/gs-model.ts +7 -1
  39. package/src/index.ts +12 -7
  40. package/src/map-renderer.ts +115 -0
  41. package/src/ml/gs-gs2ml.ts +717 -0
  42. package/src/ml/gs-ml-adapters.ts +134 -0
  43. package/src/ml/gs-ml-lib.ts +124 -0
  44. package/src/ml/gs-ml2gs.ts +66 -0
  45. package/src/ml/gs-mlns.ts +50 -0
  46. package/src/ml/index.ts +41 -0
  47. package/src/ml/maplibre-map-renderer.ts +428 -0
  48. package/src/{gs-gs2ol.ts → ol/gs-gs2ol.ts} +10 -4
  49. package/src/{gs-lib.ts → ol/gs-ol-lib.ts} +7 -6
  50. package/src/{gs-ol2gs.ts → ol/gs-ol2gs.ts} +1 -1
  51. package/src/ol/index.ts +21 -0
  52. package/src/ol/openlayers-map-renderer.ts +719 -0
  53. package/dist/gs-gs2ol.d.ts.map +0 -1
  54. package/dist/gs-lib.d.ts.map +0 -1
  55. package/dist/gs-ol-adapters.d.ts.map +0 -1
  56. package/dist/gs-ol2gs.d.ts.map +0 -1
  57. package/dist/gs-olns.d.ts.map +0 -1
  58. /package/dist/{gs-ol-adapters.d.ts → ol/gs-ol-adapters.d.ts} +0 -0
  59. /package/dist/{gs-olns.d.ts → ol/gs-olns.d.ts} +0 -0
  60. /package/src/{gs-ol-adapters.ts → ol/gs-ol-adapters.ts} +0 -0
  61. /package/src/{gs-olns.ts → ol/gs-olns.ts} +0 -0
@@ -0,0 +1,717 @@
1
+ /**
2
+ * GsMap to MapLibre conversion utilities
3
+ * Converts the geo!space domain model to MapLibre GL structures
4
+ */
5
+
6
+ import {
7
+ Map,
8
+ LngLatLike,
9
+ StyleSpecification,
10
+ LayerSpecification,
11
+ SourceSpecification
12
+ } from 'maplibre-gl';
13
+ import { v4 as uuidv4 } from '@kispace-io/appspace/externals/third-party';
14
+ import { subscribe, publish } from '@kispace-io/appspace/core/events';
15
+ import { GsMlControl, GsMlControlAdapter, GsMlOverlayAdapter } from './gs-ml-adapters';
16
+
17
+ import {
18
+ GsMap,
19
+ GsLayer,
20
+ GsSource,
21
+ GsSourceType,
22
+ GsLayerType,
23
+ GsFeature,
24
+ GsStyle,
25
+ KEY_UUID
26
+ } from '../gs-model';
27
+ import { lit } from '../gs-litns';
28
+ import { rtUtils } from '../index';
29
+ import proj4 from 'proj4';
30
+
31
+ // Key constants for MapLibre
32
+ export const ML_KEY_GS_LAYER_UUID = 'gs-layer-uuid';
33
+ export const ML_KEY_GS_SOURCE_UUID = 'gs-source-uuid';
34
+
35
+ // Standard projection identifiers
36
+ export const EPSG_3857 = 'EPSG:3857'; // Web Mercator (meters)
37
+ export const EPSG_4326 = 'EPSG:4326'; // WGS84 (lon/lat degrees)
38
+
39
+ /**
40
+ * Convert coordinates to EPSG:4326 (WGS84)
41
+ * MapLibre uses WGS84 (lon/lat in degrees)
42
+ * Uses proj4js for accurate coordinate transformation
43
+ *
44
+ * @param coords - Source coordinates
45
+ * @param sourceProjection - Source projection (default: EPSG:3857)
46
+ */
47
+ export function toWgs84(coords: [number, number], sourceProjection: string = EPSG_3857): [number, number] {
48
+ if (sourceProjection === EPSG_4326) {
49
+ return coords; // Already in WGS84
50
+ }
51
+ return proj4(sourceProjection, EPSG_4326, coords) as [number, number];
52
+ }
53
+
54
+ /**
55
+ * Convert coordinates to EPSG:3857 (Web Mercator)
56
+ * GsMap stores coordinates in Web Mercator (meters)
57
+ * Uses proj4js for accurate coordinate transformation
58
+ *
59
+ * @param coords - Source coordinates
60
+ * @param sourceProjection - Source projection (default: EPSG:4326)
61
+ */
62
+ export function toWebMercator(coords: [number, number], sourceProjection: string = EPSG_4326): [number, number] {
63
+ if (sourceProjection === EPSG_3857) {
64
+ return coords; // Already in Web Mercator
65
+ }
66
+ return proj4(sourceProjection, EPSG_3857, coords) as [number, number];
67
+ }
68
+
69
+ /**
70
+ * Generic coordinate transformation between any two projections
71
+ * Uses proj4js for accurate coordinate transformation
72
+ *
73
+ * @param coords - Source coordinates
74
+ * @param fromProjection - Source projection
75
+ * @param toProjection - Target projection
76
+ */
77
+ export function transformCoords(
78
+ coords: [number, number],
79
+ fromProjection: string,
80
+ toProjection: string
81
+ ): [number, number] {
82
+ if (fromProjection === toProjection) {
83
+ return coords;
84
+ }
85
+ return proj4(fromProjection, toProjection, coords) as [number, number];
86
+ }
87
+
88
+ /**
89
+ * Convert coordinates to WGS84 for any geometry type
90
+ *
91
+ * @param coords - Geometry coordinates
92
+ * @param geometryType - GeoJSON geometry type
93
+ * @param sourceProjection - Source projection (default: EPSG:3857)
94
+ */
95
+ function convertCoordinatesToWgs84(coords: any, geometryType: string, sourceProjection: string = EPSG_3857): any {
96
+ switch (geometryType) {
97
+ case 'Point':
98
+ return toWgs84(coords as [number, number], sourceProjection);
99
+ case 'LineString':
100
+ case 'MultiPoint':
101
+ return (coords as [number, number][]).map(c => toWgs84(c, sourceProjection));
102
+ case 'Polygon':
103
+ case 'MultiLineString':
104
+ return (coords as [number, number][][]).map(ring => ring.map(c => toWgs84(c, sourceProjection)));
105
+ case 'MultiPolygon':
106
+ return (coords as [number, number][][][]).map(poly => poly.map(ring => ring.map(c => toWgs84(c, sourceProjection))));
107
+ default:
108
+ return coords;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Convert GsFeature to GeoJSON Feature (with coordinate conversion to WGS84)
114
+ */
115
+ /**
116
+ * Convert GsFeature to GeoJSON Feature (with coordinate conversion to WGS84)
117
+ *
118
+ * @param feature - GsFeature to convert
119
+ * @param sourceProjection - Source projection of coordinates (default: EPSG:3857)
120
+ */
121
+ export function toGeoJsonFeature(feature: GsFeature, sourceProjection: string = EPSG_3857): GeoJSON.Feature {
122
+ const convertedCoords = convertCoordinatesToWgs84(
123
+ feature.geometry.coordinates,
124
+ feature.geometry.type,
125
+ sourceProjection
126
+ );
127
+
128
+ return {
129
+ type: 'Feature',
130
+ id: feature.uuid,
131
+ geometry: {
132
+ type: feature.geometry.type,
133
+ coordinates: convertedCoords
134
+ } as GeoJSON.Geometry,
135
+ properties: {
136
+ ...feature.state,
137
+ [KEY_UUID]: feature.uuid
138
+ }
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Convert array of GsFeatures to GeoJSON FeatureCollection
144
+ */
145
+ /**
146
+ * Convert array of GsFeatures to GeoJSON FeatureCollection
147
+ *
148
+ * @param features - Array of GsFeatures
149
+ * @param sourceProjection - Source projection of coordinates (default: EPSG:3857)
150
+ */
151
+ export function toGeoJsonFeatureCollection(features: GsFeature[], sourceProjection: string = EPSG_3857): GeoJSON.FeatureCollection {
152
+ return {
153
+ type: 'FeatureCollection',
154
+ features: features.map(f => toGeoJsonFeature(f, sourceProjection))
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Build WMS tile URL from GsSource
160
+ */
161
+ function buildWmsTileUrl(source: GsSource): string {
162
+ const baseUrl = source.url!;
163
+ const params = new URLSearchParams({
164
+ SERVICE: 'WMS',
165
+ VERSION: '1.1.1',
166
+ REQUEST: 'GetMap',
167
+ FORMAT: 'image/png',
168
+ TRANSPARENT: 'true',
169
+ SRS: 'EPSG:3857',
170
+ WIDTH: '256',
171
+ HEIGHT: '256',
172
+ BBOX: '{bbox-epsg-3857}',
173
+ ...source.state
174
+ });
175
+ const separator = baseUrl.includes('?') ? '&' : '?';
176
+ return `${baseUrl}${separator}${params.toString()}`;
177
+ }
178
+
179
+ /**
180
+ * Convert GsSource to MapLibre SourceSpecification
181
+ */
182
+ export function toMlSource(source: GsSource, _layer?: GsLayer): SourceSpecification | null {
183
+ switch (source.type) {
184
+ case GsSourceType.OSM:
185
+ return {
186
+ type: 'raster',
187
+ tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
188
+ tileSize: 256,
189
+ attribution: '© OpenStreetMap contributors'
190
+ };
191
+
192
+ case GsSourceType.XYZ:
193
+ return {
194
+ type: 'raster',
195
+ tiles: [source.url!],
196
+ tileSize: 256
197
+ };
198
+
199
+ case GsSourceType.GeoJSON:
200
+ return {
201
+ type: 'geojson',
202
+ data: source.url!
203
+ };
204
+
205
+ case GsSourceType.Features:
206
+ return {
207
+ type: 'geojson',
208
+ data: toGeoJsonFeatureCollection(source.features || [])
209
+ };
210
+
211
+ case GsSourceType.WMS:
212
+ return {
213
+ type: 'raster',
214
+ tiles: [buildWmsTileUrl(source)],
215
+ tileSize: 256
216
+ };
217
+
218
+ case GsSourceType.WMTS:
219
+ // WMTS needs tile URL construction - simplified for now
220
+ return {
221
+ type: 'raster',
222
+ tiles: [source.url!],
223
+ tileSize: 256
224
+ };
225
+
226
+ case GsSourceType.BM:
227
+ // Basemap.de style - handled separately as full style
228
+ return null;
229
+
230
+ case GsSourceType.GeoTIFF:
231
+ // GeoTIFF requires special handling with maplibre-gl-cog
232
+ console.warn('GeoTIFF source type not yet supported in MapLibre renderer');
233
+ return null;
234
+
235
+ case GsSourceType.GPX:
236
+ case GsSourceType.KML:
237
+ // These need format conversion - would require togeojson library
238
+ console.warn(`${source.type} source type requires conversion to GeoJSON`);
239
+ return null;
240
+
241
+ default:
242
+ console.warn(`Unknown source type: ${source.type}`);
243
+ return null;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Convert GsStyle to MapLibre paint properties for fill layers
249
+ */
250
+ export function toMlFillPaint(style?: GsStyle): Record<string, any> {
251
+ const paint: Record<string, any> = {};
252
+
253
+ if (style?.fill?.color) {
254
+ paint['fill-color'] = style.fill.color;
255
+ } else {
256
+ paint['fill-color'] = 'rgba(0, 100, 255, 0.3)';
257
+ }
258
+
259
+ if (style?.stroke?.color) {
260
+ paint['fill-outline-color'] = style.stroke.color;
261
+ }
262
+
263
+ return paint;
264
+ }
265
+
266
+ /**
267
+ * Convert GsStyle to MapLibre paint properties for line layers
268
+ */
269
+ export function toMlLinePaint(style?: GsStyle): Record<string, any> {
270
+ const paint: Record<string, any> = {};
271
+
272
+ if (style?.stroke?.color) {
273
+ paint['line-color'] = style.stroke.color;
274
+ } else {
275
+ paint['line-color'] = 'rgba(0, 100, 255, 0.8)';
276
+ }
277
+
278
+ if (style?.stroke?.width) {
279
+ paint['line-width'] = style.stroke.width;
280
+ } else {
281
+ paint['line-width'] = 2;
282
+ }
283
+
284
+ if (style?.stroke?.lineDash) {
285
+ paint['line-dasharray'] = style.stroke.lineDash;
286
+ }
287
+
288
+ return paint;
289
+ }
290
+
291
+ /**
292
+ * Convert GsStyle to MapLibre paint properties for circle layers
293
+ */
294
+ export function toMlCirclePaint(style?: GsStyle): Record<string, any> {
295
+ const paint: Record<string, any> = {};
296
+
297
+ const image = style?.image;
298
+ if (image?.type === 'circle') {
299
+ paint['circle-radius'] = image.radius || 5;
300
+
301
+ if (image.fill?.color) {
302
+ paint['circle-color'] = image.fill.color;
303
+ } else {
304
+ paint['circle-color'] = 'rgba(0, 100, 255, 0.8)';
305
+ }
306
+
307
+ if (image.stroke?.color) {
308
+ paint['circle-stroke-color'] = image.stroke.color;
309
+ } else {
310
+ paint['circle-stroke-color'] = 'white';
311
+ }
312
+
313
+ if (image.stroke?.width) {
314
+ paint['circle-stroke-width'] = image.stroke.width;
315
+ } else {
316
+ paint['circle-stroke-width'] = 2;
317
+ }
318
+ } else {
319
+ // Default circle style
320
+ paint['circle-radius'] = 5;
321
+ paint['circle-color'] = 'rgba(0, 100, 255, 0.8)';
322
+ paint['circle-stroke-color'] = 'white';
323
+ paint['circle-stroke-width'] = 2;
324
+ }
325
+
326
+ return paint;
327
+ }
328
+
329
+ /**
330
+ * Convert GsStyle to MapLibre paint properties for fill-extrusion layers (3D buildings)
331
+ * Uses data-driven styling to get height and color from feature properties
332
+ */
333
+ export function toMlFillExtrusionPaint(style?: GsStyle): Record<string, any> {
334
+ const paint: Record<string, any> = {};
335
+
336
+ // Use data-driven color from feature properties, or fall back to style/default
337
+ paint['fill-extrusion-color'] = [
338
+ 'case',
339
+ ['has', 'color'],
340
+ ['get', 'color'],
341
+ style?.fill?.color || 'rgba(74, 144, 217, 0.8)'
342
+ ];
343
+
344
+ // Get height from feature properties, default to 10 meters
345
+ paint['fill-extrusion-height'] = [
346
+ 'case',
347
+ ['has', 'height'],
348
+ ['get', 'height'],
349
+ 10
350
+ ];
351
+
352
+ // Base height (for floating buildings), default to 0
353
+ paint['fill-extrusion-base'] = [
354
+ 'case',
355
+ ['has', 'base_height'],
356
+ ['get', 'base_height'],
357
+ 0
358
+ ];
359
+
360
+ // Opacity
361
+ paint['fill-extrusion-opacity'] = 0.85;
362
+
363
+ return paint;
364
+ }
365
+
366
+ /**
367
+ * Convert GsLayer to MapLibre LayerSpecification(s)
368
+ * Returns an array because vector layers may need multiple MapLibre layers
369
+ * (one for each geometry type: fill, line, circle, fill-extrusion)
370
+ */
371
+ export function toMlLayers(layer: GsLayer, sourceId: string, defaultStyle?: GsStyle): LayerSpecification[] {
372
+ const layers: LayerSpecification[] = [];
373
+ const baseId = layer.uuid || sourceId;
374
+
375
+ if (layer.type === GsLayerType.TILE) {
376
+ layers.push({
377
+ id: baseId,
378
+ type: 'raster',
379
+ source: sourceId,
380
+ layout: {
381
+ visibility: layer.visible !== false ? 'visible' : 'none'
382
+ }
383
+ } as LayerSpecification);
384
+ } else if (layer.type === GsLayerType.VECTOR) {
385
+ // Add fill layer for polygons WITHOUT height property (flat 2D polygons)
386
+ layers.push({
387
+ id: `${baseId}-fill`,
388
+ type: 'fill',
389
+ source: sourceId,
390
+ filter: ['all',
391
+ ['any',
392
+ ['==', ['geometry-type'], 'Polygon'],
393
+ ['==', ['geometry-type'], 'MultiPolygon']
394
+ ],
395
+ ['!', ['has', 'height']]
396
+ ],
397
+ paint: toMlFillPaint(defaultStyle),
398
+ layout: {
399
+ visibility: layer.visible !== false ? 'visible' : 'none'
400
+ },
401
+ metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
402
+ } as LayerSpecification);
403
+
404
+ // Add fill-extrusion layer for polygons WITH height property (3D buildings)
405
+ layers.push({
406
+ id: `${baseId}-extrusion`,
407
+ type: 'fill-extrusion',
408
+ source: sourceId,
409
+ filter: ['all',
410
+ ['any',
411
+ ['==', ['geometry-type'], 'Polygon'],
412
+ ['==', ['geometry-type'], 'MultiPolygon']
413
+ ],
414
+ ['has', 'height']
415
+ ],
416
+ paint: toMlFillExtrusionPaint(defaultStyle),
417
+ layout: {
418
+ visibility: layer.visible !== false ? 'visible' : 'none'
419
+ },
420
+ metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
421
+ } as LayerSpecification);
422
+
423
+ // Add line layer for linestrings
424
+ layers.push({
425
+ id: `${baseId}-line`,
426
+ type: 'line',
427
+ source: sourceId,
428
+ filter: ['any',
429
+ ['==', ['geometry-type'], 'LineString'],
430
+ ['==', ['geometry-type'], 'MultiLineString']
431
+ ],
432
+ paint: toMlLinePaint(defaultStyle),
433
+ layout: {
434
+ visibility: layer.visible !== false ? 'visible' : 'none'
435
+ },
436
+ metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
437
+ } as LayerSpecification);
438
+
439
+ // Add circle layer for points
440
+ layers.push({
441
+ id: `${baseId}-circle`,
442
+ type: 'circle',
443
+ source: sourceId,
444
+ filter: ['any',
445
+ ['==', ['geometry-type'], 'Point'],
446
+ ['==', ['geometry-type'], 'MultiPoint']
447
+ ],
448
+ paint: toMlCirclePaint(defaultStyle),
449
+ layout: {
450
+ visibility: layer.visible !== false ? 'visible' : 'none'
451
+ },
452
+ metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
453
+ } as LayerSpecification);
454
+ } else if (layer.type === GsLayerType.GROUP) {
455
+ // GROUP layers with BM source use external style URL
456
+ // This is handled separately by loading the style
457
+ console.log('GROUP layer detected - style URL:', layer.source.url);
458
+ }
459
+
460
+ return layers;
461
+ }
462
+
463
+ /**
464
+ * Build initial MapLibre style from GsMap
465
+ */
466
+ export function buildInitialStyle(gsMap: GsMap): StyleSpecification {
467
+ const sources: Record<string, SourceSpecification> = {};
468
+ const layers: LayerSpecification[] = [];
469
+
470
+ // Get default style for features (using point style as default for all geometry types)
471
+ const defaultStyle = gsMap.styles?.['default-point'];
472
+
473
+ // Process each layer
474
+ gsMap.layers.forEach((layer, index) => {
475
+ // Skip GROUP layers - they load external styles
476
+ if (layer.type === GsLayerType.GROUP && layer.source.type === GsSourceType.BM) {
477
+ return;
478
+ }
479
+
480
+ const sourceId = layer.uuid || `layer-${index}`;
481
+ const source = toMlSource(layer.source, layer);
482
+
483
+ if (source) {
484
+ sources[sourceId] = source;
485
+ const mlLayers = toMlLayers(layer, sourceId, defaultStyle);
486
+ layers.push(...mlLayers);
487
+ }
488
+ });
489
+
490
+ return {
491
+ version: 8,
492
+ sources,
493
+ layers,
494
+ glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf'
495
+ };
496
+ }
497
+
498
+
499
+ export type MlImporter = (src: string) => Promise<any>;
500
+ export const DefaultMlImporter: MlImporter = (src: string) => import(src);
501
+
502
+ /**
503
+ * Import and initialize a control module for MapLibre
504
+ */
505
+ export async function importMlControlSource(
506
+ map: Map,
507
+ control: GsMlControl,
508
+ src: string,
509
+ env?: any,
510
+ importer?: MlImporter
511
+ ): Promise<void> {
512
+ const adapter = control.getAdapter();
513
+ const ml = await import('./gs-mlns');
514
+
515
+ return (importer || DefaultMlImporter)(src).then((mod) => {
516
+ const init = () => {
517
+ const vars: any = {
518
+ ...lit,
519
+ lit: lit,
520
+ style: adapter.style.bind(adapter),
521
+ render: adapter.render.bind(adapter),
522
+ map: map,
523
+ element: adapter.getElement(),
524
+ querySelector: adapter.getElement().querySelector.bind(adapter.getElement()),
525
+ querySelectorAll: adapter.getElement().querySelectorAll.bind(adapter.getElement()),
526
+ ml: ml,
527
+ env: env || {},
528
+ utils: {
529
+ uuid: uuidv4
530
+ },
531
+ asset: (path: string) => {
532
+ return rtUtils.resolveUrl(`assets/${path}`);
533
+ },
534
+ signal: (_name: string) => {},
535
+ events: (topic: string, callback: Function | any) => {
536
+ if (callback instanceof Function) {
537
+ const token = subscribe(topic, callback);
538
+ return token;
539
+ } else {
540
+ return publish(topic, callback);
541
+ }
542
+ },
543
+ settings: (key: string, callback?: Function | any) => {
544
+ const storageKey = 'gs-settings';
545
+
546
+ const loadSettings = (): any => {
547
+ try {
548
+ const stored = localStorage.getItem(storageKey);
549
+ return stored ? JSON.parse(stored) : {};
550
+ } catch {
551
+ return {};
552
+ }
553
+ };
554
+
555
+ const saveSettings = (settings: any): void => {
556
+ try {
557
+ localStorage.setItem(storageKey, JSON.stringify(settings));
558
+ } catch (error) {
559
+ console.error('Failed to save settings:', error);
560
+ }
561
+ };
562
+
563
+ const settings = loadSettings();
564
+
565
+ if (callback === undefined) {
566
+ return settings[key];
567
+ }
568
+ if (callback instanceof Function) {
569
+ vars.events(key, callback);
570
+ callback(settings[key]);
571
+ return settings[key];
572
+ }
573
+ settings[key] = callback;
574
+ saveSettings(settings);
575
+ return publish(key, callback);
576
+ },
577
+ control: adapter,
578
+ state: <T>(initialValue: T) => {
579
+ if (typeof initialValue === 'object' && initialValue !== null && !Array.isArray(initialValue)) {
580
+ const values: any = { ...initialValue };
581
+ return new Proxy({} as any, {
582
+ get(_target, prop: string) {
583
+ return values[prop];
584
+ },
585
+ set(_target, prop: string, newValue: any) {
586
+ if (values[prop] !== newValue) {
587
+ values[prop] = newValue;
588
+ adapter.render();
589
+ }
590
+ return true;
591
+ }
592
+ });
593
+ } else {
594
+ let value = initialValue;
595
+ return {
596
+ get value() { return value; },
597
+ set value(newValue: any) {
598
+ if (value !== newValue) {
599
+ value = newValue;
600
+ adapter.render();
601
+ }
602
+ }
603
+ };
604
+ }
605
+ }
606
+ };
607
+
608
+ const templateFunction = mod instanceof Function ? mod : mod.default;
609
+ if (templateFunction) {
610
+ const component = templateFunction(vars);
611
+ if (component instanceof Function) {
612
+ adapter.render(component);
613
+ } else {
614
+ adapter.render(component);
615
+ }
616
+ }
617
+ };
618
+
619
+ // Initialize after map is loaded
620
+ if (map.loaded()) {
621
+ init();
622
+ } else {
623
+ map.on('load', init);
624
+ }
625
+ });
626
+ }
627
+
628
+ /**
629
+ * Import and initialize an overlay module for MapLibre
630
+ */
631
+ export async function importMlOverlaySource(
632
+ map: Map,
633
+ adapter: GsMlOverlayAdapter,
634
+ src: string,
635
+ env?: any,
636
+ importer?: MlImporter
637
+ ): Promise<void> {
638
+ const ml = await import('./gs-mlns');
639
+
640
+ return (importer || DefaultMlImporter)(src).then((mod) => {
641
+ const init = () => {
642
+ const vars: any = {
643
+ ...lit,
644
+ lit: lit,
645
+ style: adapter.style.bind(adapter),
646
+ render: adapter.render.bind(adapter),
647
+ map: map,
648
+ element: adapter.getElement(),
649
+ querySelector: adapter.getElement().querySelector.bind(adapter.getElement()),
650
+ querySelectorAll: adapter.getElement().querySelectorAll.bind(adapter.getElement()),
651
+ ml: ml,
652
+ env: env || {},
653
+ utils: {
654
+ uuid: uuidv4
655
+ },
656
+ asset: (path: string) => {
657
+ return rtUtils.resolveUrl(`assets/${path}`);
658
+ },
659
+ signal: (_name: string) => {},
660
+ events: (topic: string, callback: Function | any) => {
661
+ if (callback instanceof Function) {
662
+ return subscribe(topic, callback);
663
+ } else {
664
+ return publish(topic, callback);
665
+ }
666
+ },
667
+ overlay: adapter,
668
+ state: <T>(initialValue: T) => {
669
+ if (typeof initialValue === 'object' && initialValue !== null && !Array.isArray(initialValue)) {
670
+ const values: any = { ...initialValue };
671
+ return new Proxy({} as any, {
672
+ get(_target, prop: string) {
673
+ return values[prop];
674
+ },
675
+ set(_target, prop: string, newValue: any) {
676
+ if (values[prop] !== newValue) {
677
+ values[prop] = newValue;
678
+ adapter.render();
679
+ }
680
+ return true;
681
+ }
682
+ });
683
+ } else {
684
+ let value = initialValue;
685
+ return {
686
+ get value() { return value; },
687
+ set value(newValue: any) {
688
+ if (value !== newValue) {
689
+ value = newValue;
690
+ adapter.render();
691
+ }
692
+ }
693
+ };
694
+ }
695
+ }
696
+ };
697
+
698
+ const templateFunction = mod instanceof Function ? mod : mod.default;
699
+ if (templateFunction) {
700
+ const component = templateFunction(vars);
701
+ if (component instanceof Function) {
702
+ adapter.render(component);
703
+ } else {
704
+ adapter.render(component);
705
+ }
706
+ }
707
+ };
708
+
709
+ if (map.loaded()) {
710
+ init();
711
+ } else {
712
+ map.on('load', init);
713
+ }
714
+ });
715
+ }
716
+
717
+