@maplibre-yaml/core 0.1.0-alpha.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.
@@ -0,0 +1,4332 @@
1
+ import maplibregl2 from 'maplibre-gl';
2
+ import { parse } from 'yaml';
3
+ import { z, ZodError } from 'zod';
4
+
5
+ // @maplibre-yaml/core - Declarative web maps with YAML
6
+
7
+
8
+ // src/data/memory-cache.ts
9
+ var MemoryCache = class _MemoryCache {
10
+ static DEFAULT_CONFIG = {
11
+ maxSize: 100,
12
+ defaultTTL: 3e5,
13
+ // 5 minutes
14
+ useConditionalRequests: true
15
+ };
16
+ config;
17
+ cache = /* @__PURE__ */ new Map();
18
+ accessOrder = [];
19
+ stats = { hits: 0, misses: 0 };
20
+ /**
21
+ * Create a new MemoryCache instance
22
+ *
23
+ * @param config - Cache configuration options
24
+ */
25
+ constructor(config) {
26
+ this.config = { ..._MemoryCache.DEFAULT_CONFIG, ...config };
27
+ }
28
+ /**
29
+ * Retrieve a cache entry
30
+ *
31
+ * @remarks
32
+ * - Returns null if key doesn't exist
33
+ * - Returns null if entry has expired (and removes it)
34
+ * - Updates access order for LRU
35
+ * - Updates cache statistics
36
+ *
37
+ * @param key - Cache key (typically a URL)
38
+ * @returns Cache entry or null if not found/expired
39
+ */
40
+ get(key) {
41
+ const entry = this.cache.get(key);
42
+ if (!entry) {
43
+ this.stats.misses++;
44
+ return null;
45
+ }
46
+ const ttl = entry.ttl ?? this.config.defaultTTL;
47
+ const age = Date.now() - entry.timestamp;
48
+ if (age > ttl) {
49
+ this.cache.delete(key);
50
+ this.removeFromAccessOrder(key);
51
+ this.stats.misses++;
52
+ return null;
53
+ }
54
+ this.updateAccessOrder(key);
55
+ this.stats.hits++;
56
+ return entry;
57
+ }
58
+ /**
59
+ * Check if a key exists in cache (without checking expiration)
60
+ *
61
+ * @param key - Cache key
62
+ * @returns True if key exists in cache
63
+ */
64
+ has(key) {
65
+ return this.cache.has(key);
66
+ }
67
+ /**
68
+ * Store a cache entry
69
+ *
70
+ * @remarks
71
+ * - Evicts least recently used entries if at capacity
72
+ * - Updates access order
73
+ *
74
+ * @param key - Cache key (typically a URL)
75
+ * @param entry - Cache entry to store
76
+ */
77
+ set(key, entry) {
78
+ while (this.cache.size >= this.config.maxSize && !this.cache.has(key)) {
79
+ const oldest = this.accessOrder.shift();
80
+ if (oldest) {
81
+ this.cache.delete(oldest);
82
+ }
83
+ }
84
+ this.cache.set(key, entry);
85
+ this.updateAccessOrder(key);
86
+ }
87
+ /**
88
+ * Delete a cache entry
89
+ *
90
+ * @param key - Cache key
91
+ * @returns True if entry was deleted, false if it didn't exist
92
+ */
93
+ delete(key) {
94
+ const existed = this.cache.delete(key);
95
+ if (existed) {
96
+ this.removeFromAccessOrder(key);
97
+ }
98
+ return existed;
99
+ }
100
+ /**
101
+ * Clear all cache entries and reset statistics
102
+ */
103
+ clear() {
104
+ this.cache.clear();
105
+ this.accessOrder = [];
106
+ this.stats = { hits: 0, misses: 0 };
107
+ }
108
+ /**
109
+ * Remove expired entries from cache
110
+ *
111
+ * @remarks
112
+ * Iterates through all entries and removes those that have exceeded their TTL.
113
+ * This is useful for periodic cleanup.
114
+ *
115
+ * @returns Number of entries removed
116
+ */
117
+ prune() {
118
+ let removed = 0;
119
+ const now = Date.now();
120
+ for (const [key, entry] of this.cache.entries()) {
121
+ const ttl = entry.ttl ?? this.config.defaultTTL;
122
+ const age = now - entry.timestamp;
123
+ if (age > ttl) {
124
+ this.cache.delete(key);
125
+ this.removeFromAccessOrder(key);
126
+ removed++;
127
+ }
128
+ }
129
+ return removed;
130
+ }
131
+ /**
132
+ * Get cache statistics
133
+ *
134
+ * @returns Current cache statistics including hit rate
135
+ */
136
+ getStats() {
137
+ const total = this.stats.hits + this.stats.misses;
138
+ const hitRate = total > 0 ? this.stats.hits / total * 100 : 0;
139
+ return {
140
+ size: this.cache.size,
141
+ hits: this.stats.hits,
142
+ misses: this.stats.misses,
143
+ hitRate: Math.round(hitRate * 100) / 100
144
+ // Round to 2 decimal places
145
+ };
146
+ }
147
+ /**
148
+ * Get conditional request headers for HTTP caching
149
+ *
150
+ * @remarks
151
+ * Returns appropriate If-None-Match and/or If-Modified-Since headers
152
+ * based on cached entry metadata. Returns empty object if:
153
+ * - Key doesn't exist in cache
154
+ * - Entry has expired
155
+ * - useConditionalRequests is false
156
+ *
157
+ * @param key - Cache key
158
+ * @returns Object with conditional headers (may be empty)
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const headers = cache.getConditionalHeaders(url);
163
+ * const response = await fetch(url, { headers });
164
+ * if (response.status === 304) {
165
+ * // Use cached data
166
+ * }
167
+ * ```
168
+ */
169
+ getConditionalHeaders(key) {
170
+ if (!this.config.useConditionalRequests) {
171
+ return {};
172
+ }
173
+ const entry = this.get(key);
174
+ if (!entry) {
175
+ return {};
176
+ }
177
+ const headers = {};
178
+ if (entry.etag) {
179
+ headers["If-None-Match"] = entry.etag;
180
+ }
181
+ if (entry.lastModified) {
182
+ headers["If-Modified-Since"] = entry.lastModified;
183
+ }
184
+ return headers;
185
+ }
186
+ /**
187
+ * Update the last access time for an entry
188
+ *
189
+ * @remarks
190
+ * Useful for keeping an entry "fresh" without modifying its data.
191
+ * Updates the access order for LRU.
192
+ *
193
+ * @param key - Cache key
194
+ */
195
+ touch(key) {
196
+ if (this.cache.has(key)) {
197
+ this.updateAccessOrder(key);
198
+ }
199
+ }
200
+ /**
201
+ * Update access order for LRU tracking
202
+ *
203
+ * @param key - Cache key
204
+ */
205
+ updateAccessOrder(key) {
206
+ this.removeFromAccessOrder(key);
207
+ this.accessOrder.push(key);
208
+ }
209
+ /**
210
+ * Remove a key from access order array
211
+ *
212
+ * @param key - Cache key
213
+ */
214
+ removeFromAccessOrder(key) {
215
+ const index = this.accessOrder.indexOf(key);
216
+ if (index !== -1) {
217
+ this.accessOrder.splice(index, 1);
218
+ }
219
+ }
220
+ };
221
+
222
+ // src/data/retry-manager.ts
223
+ var MaxRetriesExceededError = class _MaxRetriesExceededError extends Error {
224
+ /**
225
+ * Create a MaxRetriesExceededError
226
+ *
227
+ * @param lastError - The error from the final attempt
228
+ * @param attempts - Number of attempts made
229
+ */
230
+ constructor(lastError, attempts) {
231
+ super(
232
+ `Maximum retry attempts (${attempts}) exceeded. Last error: ${lastError.message}`
233
+ );
234
+ this.lastError = lastError;
235
+ this.attempts = attempts;
236
+ this.name = "MaxRetriesExceededError";
237
+ Object.setPrototypeOf(this, _MaxRetriesExceededError.prototype);
238
+ }
239
+ };
240
+ var RetryManager = class _RetryManager {
241
+ static DEFAULT_CONFIG = {
242
+ maxRetries: 10,
243
+ initialDelay: 1e3,
244
+ maxDelay: 3e4,
245
+ backoffFactor: 2,
246
+ jitter: true,
247
+ jitterFactor: 0.25
248
+ };
249
+ config;
250
+ /**
251
+ * Create a new RetryManager instance
252
+ *
253
+ * @param config - Retry configuration options
254
+ */
255
+ constructor(config) {
256
+ this.config = { ..._RetryManager.DEFAULT_CONFIG, ...config };
257
+ }
258
+ /**
259
+ * Execute a function with retry logic
260
+ *
261
+ * @typeParam T - Return type of the function
262
+ * @param fn - Async function to execute with retries
263
+ * @param callbacks - Optional lifecycle callbacks
264
+ * @returns Promise that resolves with the function's result
265
+ * @throws {MaxRetriesExceededError} When all retry attempts fail
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * const data = await retry.execute(
270
+ * () => fetchData(url),
271
+ * {
272
+ * isRetryable: (error) => {
273
+ * // Don't retry 4xx errors except 429 (rate limit)
274
+ * if (error.message.includes('429')) return true;
275
+ * if (error.message.match(/4\d\d/)) return false;
276
+ * return true;
277
+ * },
278
+ * }
279
+ * );
280
+ * ```
281
+ */
282
+ async execute(fn, callbacks) {
283
+ let lastError = null;
284
+ let attempt = 0;
285
+ while (attempt <= this.config.maxRetries) {
286
+ attempt++;
287
+ try {
288
+ const result = await fn();
289
+ callbacks?.onSuccess?.(attempt);
290
+ return result;
291
+ } catch (error) {
292
+ lastError = error instanceof Error ? error : new Error(String(error));
293
+ if (callbacks?.isRetryable && !callbacks.isRetryable(lastError)) {
294
+ throw lastError;
295
+ }
296
+ if (attempt > this.config.maxRetries) {
297
+ callbacks?.onExhausted?.(attempt, lastError);
298
+ throw new MaxRetriesExceededError(lastError, attempt);
299
+ }
300
+ const delay = this.calculateDelay(attempt);
301
+ callbacks?.onRetry?.(attempt, delay, lastError);
302
+ await this.sleep(delay);
303
+ }
304
+ }
305
+ throw new MaxRetriesExceededError(
306
+ lastError || new Error("Unknown error"),
307
+ attempt
308
+ );
309
+ }
310
+ /**
311
+ * Calculate delay for a given attempt using exponential backoff
312
+ *
313
+ * @param attempt - Current attempt number (1-indexed)
314
+ * @returns Delay in milliseconds
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const retry = new RetryManager({ initialDelay: 1000, backoffFactor: 2 });
319
+ * console.log(retry.calculateDelay(1)); // ~1000ms
320
+ * console.log(retry.calculateDelay(2)); // ~2000ms
321
+ * console.log(retry.calculateDelay(3)); // ~4000ms
322
+ * ```
323
+ */
324
+ calculateDelay(attempt) {
325
+ let delay = this.config.initialDelay * Math.pow(this.config.backoffFactor, attempt - 1);
326
+ delay = Math.min(delay, this.config.maxDelay);
327
+ if (this.config.jitter) {
328
+ const jitterRange = delay * this.config.jitterFactor;
329
+ const jitterOffset = (Math.random() * 2 - 1) * jitterRange;
330
+ delay = delay + jitterOffset;
331
+ }
332
+ return Math.max(0, Math.round(delay));
333
+ }
334
+ /**
335
+ * Reset internal state
336
+ *
337
+ * @remarks
338
+ * Currently this class is stateless, but this method is provided
339
+ * for API consistency and future extensibility.
340
+ */
341
+ reset() {
342
+ }
343
+ /**
344
+ * Sleep for specified milliseconds
345
+ *
346
+ * @param ms - Milliseconds to sleep
347
+ * @returns Promise that resolves after the delay
348
+ */
349
+ sleep(ms) {
350
+ return new Promise((resolve) => setTimeout(resolve, ms));
351
+ }
352
+ };
353
+
354
+ // src/data/data-fetcher.ts
355
+ var DataFetcher = class _DataFetcher {
356
+ static DEFAULT_CONFIG = {
357
+ cache: {
358
+ enabled: true,
359
+ defaultTTL: 3e5,
360
+ // 5 minutes
361
+ maxSize: 100
362
+ },
363
+ retry: {
364
+ enabled: true,
365
+ maxRetries: 3,
366
+ initialDelay: 1e3,
367
+ maxDelay: 1e4
368
+ },
369
+ timeout: 3e4,
370
+ defaultHeaders: {
371
+ Accept: "application/geo+json,application/json"
372
+ }
373
+ };
374
+ config;
375
+ cache;
376
+ retryManager;
377
+ activeRequests = /* @__PURE__ */ new Map();
378
+ /**
379
+ * Create a new DataFetcher instance
380
+ *
381
+ * @param config - Fetcher configuration
382
+ */
383
+ constructor(config) {
384
+ this.config = this.mergeConfig(config);
385
+ this.cache = new MemoryCache({
386
+ maxSize: this.config.cache.maxSize,
387
+ defaultTTL: this.config.cache.defaultTTL,
388
+ useConditionalRequests: true
389
+ });
390
+ this.retryManager = new RetryManager({
391
+ maxRetries: this.config.retry.maxRetries,
392
+ initialDelay: this.config.retry.initialDelay,
393
+ maxDelay: this.config.retry.maxDelay
394
+ });
395
+ }
396
+ /**
397
+ * Fetch GeoJSON data from a URL
398
+ *
399
+ * @param url - URL to fetch from
400
+ * @param options - Fetch options
401
+ * @returns Fetch result with data and metadata
402
+ * @throws {Error} On network error, timeout, invalid JSON, or non-GeoJSON response
403
+ *
404
+ * @example
405
+ * ```typescript
406
+ * const result = await fetcher.fetch(
407
+ * 'https://example.com/data.geojson',
408
+ * {
409
+ * ttl: 60000, // 1 minute cache
410
+ * onRetry: (attempt, delay, error) => {
411
+ * console.log(`Retry ${attempt} in ${delay}ms: ${error.message}`);
412
+ * },
413
+ * }
414
+ * );
415
+ * ```
416
+ */
417
+ async fetch(url, options = {}) {
418
+ const startTime = Date.now();
419
+ options.onStart?.();
420
+ try {
421
+ if (this.config.cache.enabled && !options.skipCache) {
422
+ const cached = this.cache.get(url);
423
+ if (cached) {
424
+ const result2 = {
425
+ data: cached.data,
426
+ fromCache: true,
427
+ featureCount: cached.data.features.length,
428
+ duration: Date.now() - startTime
429
+ };
430
+ options.onComplete?.(cached.data, true);
431
+ return result2;
432
+ }
433
+ }
434
+ const data = await this.fetchWithRetry(url, options);
435
+ if (this.config.cache.enabled && !options.skipCache) {
436
+ const cacheEntry = {
437
+ data,
438
+ timestamp: Date.now(),
439
+ ttl: options.ttl
440
+ };
441
+ this.cache.set(url, cacheEntry);
442
+ }
443
+ const result = {
444
+ data,
445
+ fromCache: false,
446
+ featureCount: data.features.length,
447
+ duration: Date.now() - startTime
448
+ };
449
+ options.onComplete?.(data, false);
450
+ return result;
451
+ } catch (error) {
452
+ const err = error instanceof Error ? error : new Error(String(error));
453
+ options.onError?.(err);
454
+ throw err;
455
+ }
456
+ }
457
+ /**
458
+ * Prefetch data and store in cache
459
+ *
460
+ * @remarks
461
+ * Useful for preloading data that will be needed soon.
462
+ * Does not return the data.
463
+ *
464
+ * @param url - URL to prefetch
465
+ * @param ttl - Optional custom TTL for cached entry
466
+ *
467
+ * @example
468
+ * ```typescript
469
+ * // Prefetch data for quick access later
470
+ * await fetcher.prefetch('https://example.com/data.geojson', 600000);
471
+ * ```
472
+ */
473
+ async prefetch(url, ttl) {
474
+ await this.fetch(url, { ttl, skipCache: false });
475
+ }
476
+ /**
477
+ * Invalidate cached entry for a URL
478
+ *
479
+ * @param url - URL to invalidate
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * // Force next fetch to get fresh data
484
+ * fetcher.invalidate('https://example.com/data.geojson');
485
+ * ```
486
+ */
487
+ invalidate(url) {
488
+ this.cache.delete(url);
489
+ }
490
+ /**
491
+ * Clear all cached entries
492
+ */
493
+ clearCache() {
494
+ this.cache.clear();
495
+ }
496
+ /**
497
+ * Get cache statistics
498
+ *
499
+ * @returns Cache stats including size, hits, misses, and hit rate
500
+ */
501
+ getCacheStats() {
502
+ return this.cache.getStats();
503
+ }
504
+ /**
505
+ * Abort all active requests
506
+ */
507
+ abortAll() {
508
+ for (const controller of this.activeRequests.values()) {
509
+ controller.abort();
510
+ }
511
+ this.activeRequests.clear();
512
+ }
513
+ /**
514
+ * Fetch with retry logic
515
+ */
516
+ async fetchWithRetry(url, options) {
517
+ if (!this.config.retry.enabled) {
518
+ return this.performFetch(url, options);
519
+ }
520
+ return this.retryManager.execute(
521
+ () => this.performFetch(url, options),
522
+ {
523
+ onRetry: options.onRetry,
524
+ isRetryable: (error) => this.isRetryableError(error)
525
+ }
526
+ );
527
+ }
528
+ /**
529
+ * Perform the actual HTTP fetch
530
+ */
531
+ async performFetch(url, options) {
532
+ const controller = options.signal ? new AbortController() : new AbortController();
533
+ if (options.signal) {
534
+ options.signal.addEventListener("abort", () => controller.abort());
535
+ }
536
+ const timeoutId = setTimeout(() => {
537
+ controller.abort();
538
+ }, this.config.timeout);
539
+ this.activeRequests.set(url, controller);
540
+ try {
541
+ const headers = {
542
+ ...this.config.defaultHeaders,
543
+ ...options.headers
544
+ };
545
+ if (this.config.cache.enabled && this.cache.has(url)) {
546
+ const conditionalHeaders = this.cache.getConditionalHeaders(url);
547
+ Object.assign(headers, conditionalHeaders);
548
+ }
549
+ const response = await fetch(url, {
550
+ signal: controller.signal,
551
+ headers
552
+ });
553
+ if (response.status === 304) {
554
+ const cached = this.cache.get(url);
555
+ if (cached) {
556
+ this.cache.touch(url);
557
+ return cached.data;
558
+ }
559
+ throw new Error("304 Not Modified but no cached data available");
560
+ }
561
+ if (!response.ok) {
562
+ throw new Error(
563
+ `HTTP ${response.status}: ${response.statusText} for ${url}`
564
+ );
565
+ }
566
+ let data;
567
+ try {
568
+ data = await response.json();
569
+ } catch (error) {
570
+ throw new Error(`Invalid JSON response from ${url}`);
571
+ }
572
+ if (!this.isValidGeoJSON(data)) {
573
+ throw new Error(`Response from ${url} is not valid GeoJSON`);
574
+ }
575
+ if (this.config.cache.enabled) {
576
+ const etag = response.headers.get("etag");
577
+ const lastModified = response.headers.get("last-modified");
578
+ if (etag || lastModified) {
579
+ const cached = this.cache.get(url);
580
+ if (cached) {
581
+ this.cache.set(url, {
582
+ ...cached,
583
+ etag: etag || cached.etag,
584
+ lastModified: lastModified || cached.lastModified
585
+ });
586
+ }
587
+ }
588
+ }
589
+ return data;
590
+ } finally {
591
+ clearTimeout(timeoutId);
592
+ this.activeRequests.delete(url);
593
+ }
594
+ }
595
+ /**
596
+ * Check if an error should trigger a retry
597
+ */
598
+ isRetryableError(error) {
599
+ const message = error.message.toLowerCase();
600
+ if (message.includes("http 4") && !message.includes("429")) {
601
+ return false;
602
+ }
603
+ if (message.includes("invalid json") || message.includes("not valid geojson")) {
604
+ return false;
605
+ }
606
+ return true;
607
+ }
608
+ /**
609
+ * Validate that data is a GeoJSON FeatureCollection
610
+ */
611
+ isValidGeoJSON(data) {
612
+ if (typeof data !== "object" || data === null) {
613
+ return false;
614
+ }
615
+ const obj = data;
616
+ return obj.type === "FeatureCollection" && Array.isArray(obj.features);
617
+ }
618
+ /**
619
+ * Merge partial config with defaults
620
+ */
621
+ mergeConfig(partial) {
622
+ if (!partial) return _DataFetcher.DEFAULT_CONFIG;
623
+ return {
624
+ cache: { ..._DataFetcher.DEFAULT_CONFIG.cache, ...partial.cache },
625
+ retry: { ..._DataFetcher.DEFAULT_CONFIG.retry, ...partial.retry },
626
+ timeout: partial.timeout ?? _DataFetcher.DEFAULT_CONFIG.timeout,
627
+ defaultHeaders: {
628
+ ..._DataFetcher.DEFAULT_CONFIG.defaultHeaders,
629
+ ...partial.defaultHeaders
630
+ }
631
+ };
632
+ }
633
+ };
634
+
635
+ // src/data/polling-manager.ts
636
+ var PollingManager = class {
637
+ subscriptions = /* @__PURE__ */ new Map();
638
+ visibilityListener = null;
639
+ constructor() {
640
+ this.setupVisibilityListener();
641
+ }
642
+ /**
643
+ * Start a new polling subscription.
644
+ *
645
+ * @param id - Unique identifier for the subscription
646
+ * @param config - Polling configuration
647
+ * @throws Error if a subscription with the same ID already exists
648
+ *
649
+ * @example
650
+ * ```typescript
651
+ * polling.start('layer-1', {
652
+ * interval: 5000,
653
+ * onTick: async () => {
654
+ * await updateLayerData();
655
+ * },
656
+ * });
657
+ * ```
658
+ */
659
+ start(id, config) {
660
+ if (this.subscriptions.has(id)) {
661
+ throw new Error(`Polling subscription with id "${id}" already exists`);
662
+ }
663
+ const subscription = {
664
+ config,
665
+ state: {
666
+ isActive: true,
667
+ isPaused: false,
668
+ lastTick: null,
669
+ nextTick: null,
670
+ tickCount: 0,
671
+ errorCount: 0
672
+ },
673
+ timerId: null,
674
+ isExecuting: false,
675
+ pausedByVisibility: false
676
+ };
677
+ this.subscriptions.set(id, subscription);
678
+ if (config.immediate) {
679
+ this.executeTick(id);
680
+ } else {
681
+ this.scheduleNextTick(id);
682
+ }
683
+ }
684
+ /**
685
+ * Stop a polling subscription and clean up resources.
686
+ *
687
+ * @param id - Subscription identifier
688
+ *
689
+ * @example
690
+ * ```typescript
691
+ * polling.stop('layer-1');
692
+ * ```
693
+ */
694
+ stop(id) {
695
+ const subscription = this.subscriptions.get(id);
696
+ if (!subscription) return;
697
+ if (subscription.timerId !== null) {
698
+ clearTimeout(subscription.timerId);
699
+ }
700
+ subscription.state.isActive = false;
701
+ this.subscriptions.delete(id);
702
+ }
703
+ /**
704
+ * Stop all polling subscriptions.
705
+ *
706
+ * @example
707
+ * ```typescript
708
+ * polling.stopAll();
709
+ * ```
710
+ */
711
+ stopAll() {
712
+ for (const id of this.subscriptions.keys()) {
713
+ this.stop(id);
714
+ }
715
+ }
716
+ /**
717
+ * Pause a polling subscription without stopping it.
718
+ *
719
+ * @param id - Subscription identifier
720
+ *
721
+ * @remarks
722
+ * Paused subscriptions can be resumed with {@link resume}.
723
+ * The subscription maintains its state while paused.
724
+ *
725
+ * @example
726
+ * ```typescript
727
+ * polling.pause('layer-1');
728
+ * ```
729
+ */
730
+ pause(id) {
731
+ const subscription = this.subscriptions.get(id);
732
+ if (!subscription || subscription.state.isPaused) return;
733
+ if (subscription.timerId !== null) {
734
+ clearTimeout(subscription.timerId);
735
+ subscription.timerId = null;
736
+ }
737
+ subscription.state.isPaused = true;
738
+ subscription.state.nextTick = null;
739
+ }
740
+ /**
741
+ * Pause all active polling subscriptions.
742
+ *
743
+ * @example
744
+ * ```typescript
745
+ * polling.pauseAll();
746
+ * ```
747
+ */
748
+ pauseAll() {
749
+ for (const id of this.subscriptions.keys()) {
750
+ this.pause(id);
751
+ }
752
+ }
753
+ /**
754
+ * Resume a paused polling subscription.
755
+ *
756
+ * @param id - Subscription identifier
757
+ *
758
+ * @example
759
+ * ```typescript
760
+ * polling.resume('layer-1');
761
+ * ```
762
+ */
763
+ resume(id) {
764
+ const subscription = this.subscriptions.get(id);
765
+ if (!subscription || !subscription.state.isPaused) return;
766
+ subscription.state.isPaused = false;
767
+ subscription.pausedByVisibility = false;
768
+ this.scheduleNextTick(id);
769
+ }
770
+ /**
771
+ * Resume all paused polling subscriptions.
772
+ *
773
+ * @example
774
+ * ```typescript
775
+ * polling.resumeAll();
776
+ * ```
777
+ */
778
+ resumeAll() {
779
+ for (const id of this.subscriptions.keys()) {
780
+ this.resume(id);
781
+ }
782
+ }
783
+ /**
784
+ * Trigger an immediate execution of the polling tick.
785
+ *
786
+ * @param id - Subscription identifier
787
+ * @returns Promise that resolves when the tick completes
788
+ * @throws Error if the subscription doesn't exist
789
+ *
790
+ * @remarks
791
+ * This does not affect the regular polling schedule. The next scheduled
792
+ * tick will still occur at the expected time.
793
+ *
794
+ * @example
795
+ * ```typescript
796
+ * await polling.triggerNow('layer-1');
797
+ * ```
798
+ */
799
+ async triggerNow(id) {
800
+ const subscription = this.subscriptions.get(id);
801
+ if (!subscription) {
802
+ throw new Error(`Polling subscription "${id}" not found`);
803
+ }
804
+ await this.executeTick(id);
805
+ }
806
+ /**
807
+ * Get the current state of a polling subscription.
808
+ *
809
+ * @param id - Subscription identifier
810
+ * @returns Current state or null if not found
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * const state = polling.getState('layer-1');
815
+ * if (state) {
816
+ * console.log(`Ticks: ${state.tickCount}, Errors: ${state.errorCount}`);
817
+ * }
818
+ * ```
819
+ */
820
+ getState(id) {
821
+ const subscription = this.subscriptions.get(id);
822
+ return subscription ? { ...subscription.state } : null;
823
+ }
824
+ /**
825
+ * Get all active polling subscription IDs.
826
+ *
827
+ * @returns Array of subscription IDs
828
+ *
829
+ * @example
830
+ * ```typescript
831
+ * const ids = polling.getActiveIds();
832
+ * console.log(`Active pollers: ${ids.join(', ')}`);
833
+ * ```
834
+ */
835
+ getActiveIds() {
836
+ return Array.from(this.subscriptions.keys());
837
+ }
838
+ /**
839
+ * Check if a polling subscription exists.
840
+ *
841
+ * @param id - Subscription identifier
842
+ * @returns True if the subscription exists
843
+ *
844
+ * @example
845
+ * ```typescript
846
+ * if (polling.has('layer-1')) {
847
+ * polling.pause('layer-1');
848
+ * }
849
+ * ```
850
+ */
851
+ has(id) {
852
+ return this.subscriptions.has(id);
853
+ }
854
+ /**
855
+ * Update the interval for an active polling subscription.
856
+ *
857
+ * @param id - Subscription identifier
858
+ * @param interval - New interval in milliseconds (minimum 1000ms)
859
+ * @throws Error if the subscription doesn't exist
860
+ *
861
+ * @remarks
862
+ * The new interval takes effect after the current tick completes.
863
+ *
864
+ * @example
865
+ * ```typescript
866
+ * polling.setInterval('layer-1', 10000);
867
+ * ```
868
+ */
869
+ setInterval(id, interval) {
870
+ const subscription = this.subscriptions.get(id);
871
+ if (!subscription) {
872
+ throw new Error(`Polling subscription "${id}" not found`);
873
+ }
874
+ if (interval < 1e3) {
875
+ throw new Error("Interval must be at least 1000ms");
876
+ }
877
+ subscription.config.interval = interval;
878
+ if (!subscription.state.isPaused && !subscription.isExecuting && subscription.timerId !== null) {
879
+ clearTimeout(subscription.timerId);
880
+ this.scheduleNextTick(id);
881
+ }
882
+ }
883
+ /**
884
+ * Clean up all resources and stop all polling.
885
+ *
886
+ * @remarks
887
+ * Should be called when the polling manager is no longer needed.
888
+ * After calling destroy, the polling manager should not be used.
889
+ *
890
+ * @example
891
+ * ```typescript
892
+ * polling.destroy();
893
+ * ```
894
+ */
895
+ destroy() {
896
+ this.stopAll();
897
+ this.teardownVisibilityListener();
898
+ }
899
+ /**
900
+ * Execute a single tick for a subscription.
901
+ */
902
+ async executeTick(id) {
903
+ const subscription = this.subscriptions.get(id);
904
+ if (!subscription || subscription.isExecuting) return;
905
+ subscription.isExecuting = true;
906
+ try {
907
+ await subscription.config.onTick();
908
+ subscription.state.tickCount++;
909
+ subscription.state.lastTick = Date.now();
910
+ } catch (error) {
911
+ subscription.state.errorCount++;
912
+ const err = error instanceof Error ? error : new Error(String(error));
913
+ subscription.config.onError?.(err);
914
+ } finally {
915
+ subscription.isExecuting = false;
916
+ if (this.subscriptions.has(id) && subscription.state.isActive && !subscription.state.isPaused) {
917
+ this.scheduleNextTick(id);
918
+ }
919
+ }
920
+ }
921
+ /**
922
+ * Schedule the next tick for a subscription.
923
+ */
924
+ scheduleNextTick(id) {
925
+ const subscription = this.subscriptions.get(id);
926
+ if (!subscription) return;
927
+ const nextTime = Date.now() + subscription.config.interval;
928
+ subscription.state.nextTick = nextTime;
929
+ subscription.timerId = setTimeout(() => {
930
+ this.executeTick(id);
931
+ }, subscription.config.interval);
932
+ }
933
+ /**
934
+ * Setup document visibility listener for automatic pause/resume.
935
+ */
936
+ setupVisibilityListener() {
937
+ if (typeof document === "undefined") return;
938
+ this.visibilityListener = () => {
939
+ if (document.hidden) {
940
+ this.handleVisibilityChange(true);
941
+ } else {
942
+ this.handleVisibilityChange(false);
943
+ }
944
+ };
945
+ document.addEventListener("visibilitychange", this.visibilityListener);
946
+ }
947
+ /**
948
+ * Handle document visibility changes.
949
+ */
950
+ handleVisibilityChange(hidden) {
951
+ for (const [id, subscription] of this.subscriptions) {
952
+ const pauseEnabled = subscription.config.pauseWhenHidden !== false;
953
+ if (hidden && pauseEnabled && !subscription.state.isPaused) {
954
+ subscription.pausedByVisibility = true;
955
+ this.pause(id);
956
+ } else if (!hidden && pauseEnabled && subscription.pausedByVisibility) {
957
+ this.resume(id);
958
+ }
959
+ }
960
+ }
961
+ /**
962
+ * Remove document visibility listener.
963
+ */
964
+ teardownVisibilityListener() {
965
+ if (this.visibilityListener && typeof document !== "undefined") {
966
+ document.removeEventListener("visibilitychange", this.visibilityListener);
967
+ this.visibilityListener = null;
968
+ }
969
+ }
970
+ };
971
+
972
+ // src/utils/event-emitter.ts
973
+ var EventEmitter = class {
974
+ handlers = /* @__PURE__ */ new Map();
975
+ /**
976
+ * Register an event handler
977
+ *
978
+ * @param event - Event name to listen for
979
+ * @param handler - Callback function to invoke when event is emitted
980
+ * @returns Unsubscribe function that removes this specific handler
981
+ *
982
+ * @example
983
+ * ```typescript
984
+ * const unsubscribe = emitter.on('message', (data) => {
985
+ * console.log(data.text);
986
+ * });
987
+ *
988
+ * // Later, to unsubscribe:
989
+ * unsubscribe();
990
+ * ```
991
+ */
992
+ on(event, handler) {
993
+ if (!this.handlers.has(event)) {
994
+ this.handlers.set(event, /* @__PURE__ */ new Set());
995
+ }
996
+ this.handlers.get(event).add(handler);
997
+ return () => this.off(event, handler);
998
+ }
999
+ /**
1000
+ * Register a one-time event handler
1001
+ *
1002
+ * @remarks
1003
+ * The handler will be automatically removed after being invoked once.
1004
+ *
1005
+ * @param event - Event name to listen for
1006
+ * @param handler - Callback function to invoke once
1007
+ *
1008
+ * @example
1009
+ * ```typescript
1010
+ * emitter.once('connect', () => {
1011
+ * console.log('Connected!');
1012
+ * });
1013
+ * ```
1014
+ */
1015
+ once(event, handler) {
1016
+ const onceWrapper = (data) => {
1017
+ this.off(event, onceWrapper);
1018
+ handler(data);
1019
+ };
1020
+ this.on(event, onceWrapper);
1021
+ }
1022
+ /**
1023
+ * Remove an event handler
1024
+ *
1025
+ * @param event - Event name
1026
+ * @param handler - Handler function to remove
1027
+ *
1028
+ * @example
1029
+ * ```typescript
1030
+ * const handler = (data) => console.log(data);
1031
+ * emitter.on('message', handler);
1032
+ * emitter.off('message', handler);
1033
+ * ```
1034
+ */
1035
+ off(event, handler) {
1036
+ const handlers = this.handlers.get(event);
1037
+ if (handlers) {
1038
+ handlers.delete(handler);
1039
+ if (handlers.size === 0) {
1040
+ this.handlers.delete(event);
1041
+ }
1042
+ }
1043
+ }
1044
+ /**
1045
+ * Emit an event to all registered handlers
1046
+ *
1047
+ * @remarks
1048
+ * This method is protected to ensure only the extending class can emit events.
1049
+ * All handlers are invoked synchronously in the order they were registered.
1050
+ *
1051
+ * @param event - Event name to emit
1052
+ * @param data - Event payload data
1053
+ *
1054
+ * @example
1055
+ * ```typescript
1056
+ * class MyEmitter extends EventEmitter<MyEvents> {
1057
+ * doSomething() {
1058
+ * this.emit('something-happened', { value: 42 });
1059
+ * }
1060
+ * }
1061
+ * ```
1062
+ */
1063
+ emit(event, data) {
1064
+ const handlers = this.handlers.get(event);
1065
+ if (handlers) {
1066
+ for (const handler of handlers) {
1067
+ handler(data);
1068
+ }
1069
+ }
1070
+ }
1071
+ /**
1072
+ * Remove all handlers for an event, or all handlers for all events
1073
+ *
1074
+ * @param event - Optional event name. If omitted, removes all handlers for all events.
1075
+ *
1076
+ * @example
1077
+ * ```typescript
1078
+ * // Remove all handlers for 'message' event
1079
+ * emitter.removeAllListeners('message');
1080
+ *
1081
+ * // Remove all handlers for all events
1082
+ * emitter.removeAllListeners();
1083
+ * ```
1084
+ */
1085
+ removeAllListeners(event) {
1086
+ if (event) {
1087
+ this.handlers.delete(event);
1088
+ } else {
1089
+ this.handlers.clear();
1090
+ }
1091
+ }
1092
+ /**
1093
+ * Get the number of handlers registered for an event
1094
+ *
1095
+ * @param event - Event name
1096
+ * @returns Number of registered handlers
1097
+ *
1098
+ * @example
1099
+ * ```typescript
1100
+ * const count = emitter.listenerCount('message');
1101
+ * console.log(`${count} handlers registered`);
1102
+ * ```
1103
+ */
1104
+ listenerCount(event) {
1105
+ const handlers = this.handlers.get(event);
1106
+ return handlers ? handlers.size : 0;
1107
+ }
1108
+ /**
1109
+ * Get all event names that have registered handlers
1110
+ *
1111
+ * @returns Array of event names
1112
+ *
1113
+ * @example
1114
+ * ```typescript
1115
+ * const events = emitter.eventNames();
1116
+ * console.log('Events with handlers:', events);
1117
+ * ```
1118
+ */
1119
+ eventNames() {
1120
+ return Array.from(this.handlers.keys());
1121
+ }
1122
+ /**
1123
+ * Check if an event has any registered handlers
1124
+ *
1125
+ * @param event - Event name
1126
+ * @returns True if the event has at least one handler
1127
+ *
1128
+ * @example
1129
+ * ```typescript
1130
+ * if (emitter.hasListeners('message')) {
1131
+ * emitter.emit('message', { text: 'Hello' });
1132
+ * }
1133
+ * ```
1134
+ */
1135
+ hasListeners(event) {
1136
+ return this.listenerCount(event) > 0;
1137
+ }
1138
+ };
1139
+
1140
+ // src/data/streaming/base-connection.ts
1141
+ var BaseConnection = class extends EventEmitter {
1142
+ state = "disconnected";
1143
+ config;
1144
+ retryManager;
1145
+ reconnectAttempts = 0;
1146
+ manualDisconnect = false;
1147
+ /**
1148
+ * Create a new base connection.
1149
+ *
1150
+ * @param config - Connection configuration
1151
+ */
1152
+ constructor(config) {
1153
+ super();
1154
+ this.config = {
1155
+ reconnect: true,
1156
+ reconnectConfig: {},
1157
+ ...config
1158
+ };
1159
+ this.retryManager = new RetryManager({
1160
+ maxRetries: 10,
1161
+ initialDelay: 1e3,
1162
+ maxDelay: 3e4,
1163
+ backoffFactor: 2,
1164
+ jitter: true,
1165
+ jitterFactor: 0.25,
1166
+ ...this.config.reconnectConfig
1167
+ });
1168
+ }
1169
+ /**
1170
+ * Get current connection state.
1171
+ *
1172
+ * @returns Current state
1173
+ *
1174
+ * @example
1175
+ * ```typescript
1176
+ * const state = connection.getState();
1177
+ * if (state === 'connected') {
1178
+ * // Connection is ready
1179
+ * }
1180
+ * ```
1181
+ */
1182
+ getState() {
1183
+ return this.state;
1184
+ }
1185
+ /**
1186
+ * Check if connection is currently connected.
1187
+ *
1188
+ * @returns True if connected
1189
+ *
1190
+ * @example
1191
+ * ```typescript
1192
+ * if (connection.isConnected()) {
1193
+ * connection.send(data);
1194
+ * }
1195
+ * ```
1196
+ */
1197
+ isConnected() {
1198
+ return this.state === "connected";
1199
+ }
1200
+ /**
1201
+ * Get the number of reconnection attempts.
1202
+ *
1203
+ * @returns Number of reconnect attempts
1204
+ */
1205
+ getReconnectAttempts() {
1206
+ return this.reconnectAttempts;
1207
+ }
1208
+ /**
1209
+ * Update connection state and emit state change event.
1210
+ *
1211
+ * @param newState - New connection state
1212
+ *
1213
+ * @remarks
1214
+ * Automatically emits 'stateChange' event when state changes.
1215
+ * Subclasses should call this method instead of setting state directly.
1216
+ *
1217
+ * @example
1218
+ * ```typescript
1219
+ * protected async connect() {
1220
+ * this.setState('connecting');
1221
+ * await this.establishConnection();
1222
+ * this.setState('connected');
1223
+ * }
1224
+ * ```
1225
+ */
1226
+ setState(newState) {
1227
+ if (this.state === newState) return;
1228
+ const oldState = this.state;
1229
+ this.state = newState;
1230
+ this.emit("stateChange", { from: oldState, to: newState });
1231
+ }
1232
+ /**
1233
+ * Handle disconnection and optionally attempt reconnection.
1234
+ *
1235
+ * @param reason - Reason for disconnection
1236
+ *
1237
+ * @remarks
1238
+ * This method should be called by subclasses when the connection is lost.
1239
+ * It will:
1240
+ * 1. Emit 'disconnect' event
1241
+ * 2. Attempt reconnection if enabled and not manually disconnected
1242
+ * 3. Emit 'reconnecting', 'reconnected', or 'failed' events as appropriate
1243
+ *
1244
+ * @example
1245
+ * ```typescript
1246
+ * ws.onclose = () => {
1247
+ * this.handleDisconnect('Connection closed');
1248
+ * };
1249
+ * ```
1250
+ */
1251
+ async handleDisconnect(reason) {
1252
+ const wasConnected = this.state === "connected";
1253
+ this.setState("disconnected");
1254
+ this.emit("disconnect", { reason });
1255
+ if (!this.config.reconnect || this.manualDisconnect || !wasConnected) {
1256
+ return;
1257
+ }
1258
+ await this.attemptReconnection();
1259
+ }
1260
+ /**
1261
+ * Attempt to reconnect with exponential backoff.
1262
+ */
1263
+ async attemptReconnection() {
1264
+ this.setState("reconnecting");
1265
+ this.reconnectAttempts = 0;
1266
+ try {
1267
+ await this.retryManager.execute(
1268
+ async () => {
1269
+ this.reconnectAttempts++;
1270
+ await this.connect();
1271
+ },
1272
+ {
1273
+ onRetry: (attempt, delay) => {
1274
+ this.emit("reconnecting", { attempt, delay });
1275
+ },
1276
+ onSuccess: (attempts) => {
1277
+ this.reconnectAttempts = 0;
1278
+ this.emit("reconnected", { attempts });
1279
+ }
1280
+ }
1281
+ );
1282
+ } catch (err) {
1283
+ const error = err instanceof Error ? err : new Error(String(err));
1284
+ this.setState("failed");
1285
+ this.emit("failed", {
1286
+ attempts: this.reconnectAttempts,
1287
+ lastError: error
1288
+ });
1289
+ }
1290
+ }
1291
+ /**
1292
+ * Mark disconnection as manual to prevent reconnection.
1293
+ *
1294
+ * @remarks
1295
+ * Should be called by subclasses in their disconnect() implementation
1296
+ * before closing the connection.
1297
+ *
1298
+ * @example
1299
+ * ```typescript
1300
+ * disconnect(): void {
1301
+ * this.setManualDisconnect();
1302
+ * this.ws.close();
1303
+ * }
1304
+ * ```
1305
+ */
1306
+ setManualDisconnect() {
1307
+ this.manualDisconnect = true;
1308
+ }
1309
+ /**
1310
+ * Reset manual disconnect flag.
1311
+ *
1312
+ * @remarks
1313
+ * Should be called when establishing a new connection to allow
1314
+ * automatic reconnection for subsequent disconnections.
1315
+ */
1316
+ resetManualDisconnect() {
1317
+ this.manualDisconnect = false;
1318
+ }
1319
+ };
1320
+
1321
+ // src/data/streaming/sse-connection.ts
1322
+ var SSEConnection = class extends BaseConnection {
1323
+ eventSource = null;
1324
+ lastEventId = null;
1325
+ sseConfig;
1326
+ /**
1327
+ * Create a new SSE connection.
1328
+ *
1329
+ * @param config - SSE configuration
1330
+ *
1331
+ * @example
1332
+ * ```typescript
1333
+ * const connection = new SSEConnection({
1334
+ * url: 'https://api.example.com/stream',
1335
+ * eventTypes: ['message', 'update'],
1336
+ * withCredentials: true,
1337
+ * });
1338
+ * ```
1339
+ */
1340
+ constructor(config) {
1341
+ super(config);
1342
+ this.sseConfig = {
1343
+ ...this.config,
1344
+ eventTypes: config.eventTypes ?? ["message"],
1345
+ withCredentials: config.withCredentials ?? false
1346
+ };
1347
+ }
1348
+ /**
1349
+ * Establish SSE connection.
1350
+ *
1351
+ * @remarks
1352
+ * Creates an EventSource and sets up event listeners for:
1353
+ * - Connection open
1354
+ * - Message events (for each configured event type)
1355
+ * - Error events
1356
+ *
1357
+ * The EventSource API handles reconnection automatically when the
1358
+ * connection is lost, unless explicitly closed.
1359
+ *
1360
+ * @throws Error if EventSource is not supported or connection fails
1361
+ *
1362
+ * @example
1363
+ * ```typescript
1364
+ * await connection.connect();
1365
+ * console.log('Connected to SSE stream');
1366
+ * ```
1367
+ */
1368
+ async connect() {
1369
+ if (this.eventSource !== null) {
1370
+ throw new Error("Connection already exists");
1371
+ }
1372
+ this.setState("connecting");
1373
+ try {
1374
+ this.eventSource = new EventSource(this.sseConfig.url, {
1375
+ withCredentials: this.sseConfig.withCredentials
1376
+ });
1377
+ await new Promise((resolve, reject) => {
1378
+ if (!this.eventSource) {
1379
+ reject(new Error("EventSource not created"));
1380
+ return;
1381
+ }
1382
+ const onOpen = () => {
1383
+ cleanup();
1384
+ this.setState("connected");
1385
+ this.resetManualDisconnect();
1386
+ this.emit("connect", void 0);
1387
+ resolve();
1388
+ };
1389
+ const onError = () => {
1390
+ cleanup();
1391
+ const error = new Error("Failed to connect to SSE stream");
1392
+ this.emit("error", { error });
1393
+ reject(error);
1394
+ };
1395
+ const cleanup = () => {
1396
+ this.eventSource?.removeEventListener("open", onOpen);
1397
+ this.eventSource?.removeEventListener("error", onError);
1398
+ };
1399
+ this.eventSource.addEventListener("open", onOpen);
1400
+ this.eventSource.addEventListener("error", onError);
1401
+ });
1402
+ this.setupEventListeners();
1403
+ } catch (error) {
1404
+ this.closeEventSource();
1405
+ throw error;
1406
+ }
1407
+ }
1408
+ /**
1409
+ * Close SSE connection.
1410
+ *
1411
+ * @remarks
1412
+ * Closes the EventSource and cleans up all event listeners.
1413
+ * Sets the manual disconnect flag to prevent automatic reconnection.
1414
+ *
1415
+ * @example
1416
+ * ```typescript
1417
+ * connection.disconnect();
1418
+ * console.log('Disconnected from SSE stream');
1419
+ * ```
1420
+ */
1421
+ disconnect() {
1422
+ this.setManualDisconnect();
1423
+ this.closeEventSource();
1424
+ this.handleDisconnect("Manual disconnect");
1425
+ }
1426
+ /**
1427
+ * Get the last event ID received from the server.
1428
+ *
1429
+ * @returns Last event ID or null if none received
1430
+ *
1431
+ * @remarks
1432
+ * The event ID is used by the EventSource API to resume the stream
1433
+ * from the last received event after a reconnection. The browser
1434
+ * automatically sends this ID in the `Last-Event-ID` header.
1435
+ *
1436
+ * @example
1437
+ * ```typescript
1438
+ * const lastId = connection.getLastEventId();
1439
+ * if (lastId) {
1440
+ * console.log(`Last event: ${lastId}`);
1441
+ * }
1442
+ * ```
1443
+ */
1444
+ getLastEventId() {
1445
+ return this.lastEventId;
1446
+ }
1447
+ /**
1448
+ * Setup event listeners for configured event types.
1449
+ */
1450
+ setupEventListeners() {
1451
+ if (!this.eventSource) return;
1452
+ for (const eventType of this.sseConfig.eventTypes) {
1453
+ this.eventSource.addEventListener(eventType, (event) => {
1454
+ this.handleMessage(event);
1455
+ });
1456
+ }
1457
+ this.eventSource.addEventListener("error", () => {
1458
+ this.handleError();
1459
+ });
1460
+ }
1461
+ /**
1462
+ * Handle incoming message event.
1463
+ */
1464
+ handleMessage(event) {
1465
+ if (event.lastEventId) {
1466
+ this.lastEventId = event.lastEventId;
1467
+ }
1468
+ try {
1469
+ const data = JSON.parse(event.data);
1470
+ this.emit("message", { data });
1471
+ } catch (error) {
1472
+ const parseError = new Error(
1473
+ `Failed to parse SSE message as JSON: ${event.data}`
1474
+ );
1475
+ this.emit("error", { error: parseError });
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Handle error event from EventSource.
1480
+ */
1481
+ handleError() {
1482
+ if (this.state === "connected") {
1483
+ const error = new Error("SSE connection error");
1484
+ this.emit("error", { error });
1485
+ if (this.eventSource?.readyState === EventSource.CLOSED) {
1486
+ this.closeEventSource();
1487
+ this.handleDisconnect("Connection closed by server");
1488
+ }
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Close EventSource and clean up.
1493
+ */
1494
+ closeEventSource() {
1495
+ if (this.eventSource) {
1496
+ this.eventSource.close();
1497
+ this.eventSource = null;
1498
+ }
1499
+ }
1500
+ };
1501
+
1502
+ // src/data/streaming/websocket-connection.ts
1503
+ var WebSocketConnection = class extends BaseConnection {
1504
+ ws = null;
1505
+ wsConfig;
1506
+ /**
1507
+ * Create a new WebSocket connection.
1508
+ *
1509
+ * @param config - WebSocket configuration
1510
+ *
1511
+ * @example
1512
+ * ```typescript
1513
+ * const connection = new WebSocketConnection({
1514
+ * url: 'wss://api.example.com/stream',
1515
+ * protocols: ['json', 'v1'],
1516
+ * });
1517
+ * ```
1518
+ */
1519
+ constructor(config) {
1520
+ super(config);
1521
+ this.wsConfig = {
1522
+ ...this.config,
1523
+ protocols: config.protocols
1524
+ };
1525
+ }
1526
+ /**
1527
+ * Establish WebSocket connection.
1528
+ *
1529
+ * @remarks
1530
+ * Creates a WebSocket and sets up event listeners for:
1531
+ * - Connection open
1532
+ * - Message reception
1533
+ * - Connection close
1534
+ * - Errors
1535
+ *
1536
+ * Unlike EventSource, WebSocket does not have built-in reconnection,
1537
+ * so reconnection is handled manually via the BaseConnection.
1538
+ *
1539
+ * @throws Error if WebSocket is not supported or connection fails
1540
+ *
1541
+ * @example
1542
+ * ```typescript
1543
+ * await connection.connect();
1544
+ * console.log('Connected to WebSocket');
1545
+ * ```
1546
+ */
1547
+ async connect() {
1548
+ if (this.ws !== null) {
1549
+ throw new Error("Connection already exists");
1550
+ }
1551
+ this.setState("connecting");
1552
+ try {
1553
+ this.ws = this.wsConfig.protocols ? new WebSocket(this.wsConfig.url, this.wsConfig.protocols) : new WebSocket(this.wsConfig.url);
1554
+ await new Promise((resolve, reject) => {
1555
+ if (!this.ws) {
1556
+ reject(new Error("WebSocket not created"));
1557
+ return;
1558
+ }
1559
+ const onOpen = () => {
1560
+ cleanup();
1561
+ this.setState("connected");
1562
+ this.resetManualDisconnect();
1563
+ this.emit("connect", void 0);
1564
+ resolve();
1565
+ };
1566
+ const onError = () => {
1567
+ cleanup();
1568
+ const error = new Error("Failed to connect to WebSocket");
1569
+ this.emit("error", { error });
1570
+ reject(error);
1571
+ };
1572
+ const cleanup = () => {
1573
+ if (this.ws) {
1574
+ this.ws.removeEventListener("open", onOpen);
1575
+ this.ws.removeEventListener("error", onError);
1576
+ }
1577
+ };
1578
+ this.ws.addEventListener("open", onOpen);
1579
+ this.ws.addEventListener("error", onError);
1580
+ });
1581
+ this.setupEventListeners();
1582
+ } catch (error) {
1583
+ this.closeWebSocket();
1584
+ throw error;
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Close WebSocket connection.
1589
+ *
1590
+ * @remarks
1591
+ * Closes the WebSocket with a normal closure code (1000).
1592
+ * Sets the manual disconnect flag to prevent automatic reconnection.
1593
+ *
1594
+ * @example
1595
+ * ```typescript
1596
+ * connection.disconnect();
1597
+ * console.log('Disconnected from WebSocket');
1598
+ * ```
1599
+ */
1600
+ disconnect() {
1601
+ this.setManualDisconnect();
1602
+ this.closeWebSocket();
1603
+ this.handleDisconnect("Manual disconnect");
1604
+ }
1605
+ /**
1606
+ * Send data through WebSocket.
1607
+ *
1608
+ * @param data - Data to send (will be JSON stringified)
1609
+ * @throws Error if not connected
1610
+ *
1611
+ * @remarks
1612
+ * The data is automatically converted to JSON before sending.
1613
+ * Throws an error if called when the connection is not established.
1614
+ *
1615
+ * @example
1616
+ * ```typescript
1617
+ * connection.send({ type: 'ping' });
1618
+ * connection.send({ type: 'subscribe', channel: 'updates' });
1619
+ * ```
1620
+ */
1621
+ send(data) {
1622
+ if (!this.isConnected() || !this.ws) {
1623
+ throw new Error("Cannot send: not connected");
1624
+ }
1625
+ const message = JSON.stringify(data);
1626
+ this.ws.send(message);
1627
+ }
1628
+ /**
1629
+ * Setup WebSocket event listeners.
1630
+ */
1631
+ setupEventListeners() {
1632
+ if (!this.ws) return;
1633
+ this.ws.addEventListener("message", (event) => {
1634
+ this.handleMessage(event);
1635
+ });
1636
+ this.ws.addEventListener("close", (event) => {
1637
+ this.handleClose(event);
1638
+ });
1639
+ this.ws.addEventListener("error", () => {
1640
+ this.handleError();
1641
+ });
1642
+ }
1643
+ /**
1644
+ * Handle incoming message event.
1645
+ */
1646
+ handleMessage(event) {
1647
+ try {
1648
+ const data = JSON.parse(event.data);
1649
+ this.emit("message", { data });
1650
+ } catch {
1651
+ this.emit("message", { data: event.data });
1652
+ }
1653
+ }
1654
+ /**
1655
+ * Handle close event from WebSocket.
1656
+ */
1657
+ handleClose(event) {
1658
+ this.closeWebSocket();
1659
+ const reason = event.reason || `WebSocket closed (code: ${event.code})`;
1660
+ this.handleDisconnect(reason);
1661
+ }
1662
+ /**
1663
+ * Handle error event from WebSocket.
1664
+ */
1665
+ handleError() {
1666
+ if (this.state === "connecting" || this.state === "connected") {
1667
+ const error = new Error("WebSocket connection error");
1668
+ this.emit("error", { error });
1669
+ }
1670
+ }
1671
+ /**
1672
+ * Close WebSocket and clean up.
1673
+ */
1674
+ closeWebSocket() {
1675
+ if (this.ws) {
1676
+ if (this.ws.readyState === WebSocket.OPEN) {
1677
+ this.ws.close(1e3, "Normal closure");
1678
+ }
1679
+ this.ws = null;
1680
+ }
1681
+ }
1682
+ };
1683
+
1684
+ // src/data/streaming/stream-manager.ts
1685
+ var StreamManager = class {
1686
+ subscriptions = /* @__PURE__ */ new Map();
1687
+ /**
1688
+ * Connect to a streaming source.
1689
+ *
1690
+ * @param id - Unique identifier for this connection
1691
+ * @param config - Stream configuration
1692
+ * @throws {Error} If a connection with the given id already exists
1693
+ *
1694
+ * @example
1695
+ * ```typescript
1696
+ * await manager.connect('updates', {
1697
+ * type: 'sse',
1698
+ * url: 'https://api.example.com/stream',
1699
+ * onData: (data) => console.log(data)
1700
+ * });
1701
+ * ```
1702
+ */
1703
+ async connect(id, config) {
1704
+ if (this.subscriptions.has(id)) {
1705
+ throw new Error(`Stream with id "${id}" already exists`);
1706
+ }
1707
+ const connection = this.createConnection(config);
1708
+ const state = {
1709
+ connectionState: "disconnected",
1710
+ messageCount: 0,
1711
+ lastMessage: null,
1712
+ reconnectAttempts: 0
1713
+ };
1714
+ this.subscriptions.set(id, { config, connection, state });
1715
+ this.setupEventHandlers(connection, config, state);
1716
+ await connection.connect();
1717
+ }
1718
+ /**
1719
+ * Disconnect a specific stream.
1720
+ *
1721
+ * @param id - Stream identifier
1722
+ *
1723
+ * @example
1724
+ * ```typescript
1725
+ * manager.disconnect('updates');
1726
+ * ```
1727
+ */
1728
+ disconnect(id) {
1729
+ const subscription = this.subscriptions.get(id);
1730
+ if (!subscription) {
1731
+ return;
1732
+ }
1733
+ subscription.connection.disconnect();
1734
+ this.subscriptions.delete(id);
1735
+ }
1736
+ /**
1737
+ * Disconnect all active streams.
1738
+ *
1739
+ * @example
1740
+ * ```typescript
1741
+ * manager.disconnectAll();
1742
+ * ```
1743
+ */
1744
+ disconnectAll() {
1745
+ for (const id of this.subscriptions.keys()) {
1746
+ this.disconnect(id);
1747
+ }
1748
+ }
1749
+ /**
1750
+ * Get the current state of a stream.
1751
+ *
1752
+ * @param id - Stream identifier
1753
+ * @returns Stream state or null if not found
1754
+ *
1755
+ * @example
1756
+ * ```typescript
1757
+ * const state = manager.getState('updates');
1758
+ * if (state) {
1759
+ * console.log(`State: ${state.connectionState}`);
1760
+ * console.log(`Messages: ${state.messageCount}`);
1761
+ * }
1762
+ * ```
1763
+ */
1764
+ getState(id) {
1765
+ const subscription = this.subscriptions.get(id);
1766
+ return subscription ? { ...subscription.state } : null;
1767
+ }
1768
+ /**
1769
+ * Check if a stream is currently connected.
1770
+ *
1771
+ * @param id - Stream identifier
1772
+ * @returns True if connected, false otherwise
1773
+ *
1774
+ * @example
1775
+ * ```typescript
1776
+ * if (manager.isConnected('updates')) {
1777
+ * console.log('Stream is active');
1778
+ * }
1779
+ * ```
1780
+ */
1781
+ isConnected(id) {
1782
+ const subscription = this.subscriptions.get(id);
1783
+ return subscription ? subscription.connection.isConnected() : false;
1784
+ }
1785
+ /**
1786
+ * Get all active stream IDs.
1787
+ *
1788
+ * @returns Array of active stream identifiers
1789
+ *
1790
+ * @example
1791
+ * ```typescript
1792
+ * const activeStreams = manager.getActiveIds();
1793
+ * console.log(`Active streams: ${activeStreams.join(', ')}`);
1794
+ * ```
1795
+ */
1796
+ getActiveIds() {
1797
+ return Array.from(this.subscriptions.keys());
1798
+ }
1799
+ /**
1800
+ * Send data to a WebSocket connection.
1801
+ *
1802
+ * @param id - Stream identifier
1803
+ * @param data - Data to send (will be JSON stringified)
1804
+ * @throws {Error} If stream is not a WebSocket connection or not connected
1805
+ *
1806
+ * @example
1807
+ * ```typescript
1808
+ * manager.send('ws-updates', {
1809
+ * type: 'subscribe',
1810
+ * channels: ['news', 'sports']
1811
+ * });
1812
+ * ```
1813
+ */
1814
+ send(id, data) {
1815
+ const subscription = this.subscriptions.get(id);
1816
+ if (!subscription) {
1817
+ throw new Error(`Stream "${id}" not found`);
1818
+ }
1819
+ if (!(subscription.connection instanceof WebSocketConnection)) {
1820
+ throw new Error(`Stream "${id}" is not a WebSocket connection`);
1821
+ }
1822
+ subscription.connection.send(data);
1823
+ }
1824
+ /**
1825
+ * Clean up all resources.
1826
+ *
1827
+ * @example
1828
+ * ```typescript
1829
+ * manager.destroy();
1830
+ * ```
1831
+ */
1832
+ destroy() {
1833
+ this.disconnectAll();
1834
+ }
1835
+ /**
1836
+ * Create a connection instance based on config type.
1837
+ */
1838
+ createConnection(config) {
1839
+ const reconnectConfig = config.reconnect?.enabled !== false ? {
1840
+ reconnect: true,
1841
+ retryConfig: {
1842
+ maxRetries: config.reconnect?.maxRetries ?? 10,
1843
+ initialDelay: config.reconnect?.initialDelay ?? 1e3,
1844
+ maxDelay: config.reconnect?.maxDelay ?? 3e4
1845
+ }
1846
+ } : { reconnect: false };
1847
+ if (config.type === "sse") {
1848
+ const sseConfig = {
1849
+ url: config.url,
1850
+ ...reconnectConfig,
1851
+ eventTypes: config.eventTypes
1852
+ };
1853
+ return new SSEConnection(sseConfig);
1854
+ } else {
1855
+ const wsConfig = {
1856
+ url: config.url,
1857
+ ...reconnectConfig,
1858
+ protocols: config.protocols
1859
+ };
1860
+ return new WebSocketConnection(wsConfig);
1861
+ }
1862
+ }
1863
+ /**
1864
+ * Setup event handlers for a connection.
1865
+ */
1866
+ setupEventHandlers(connection, config, state) {
1867
+ connection.on("stateChange", ({ to }) => {
1868
+ state.connectionState = to;
1869
+ config.onStateChange?.(to);
1870
+ });
1871
+ connection.on("message", ({ data }) => {
1872
+ state.messageCount++;
1873
+ state.lastMessage = Date.now();
1874
+ if (this.isFeatureCollection(data)) {
1875
+ config.onData(data);
1876
+ } else {
1877
+ const error = new Error(
1878
+ "Received data is not a valid GeoJSON FeatureCollection"
1879
+ );
1880
+ config.onError?.(error);
1881
+ }
1882
+ });
1883
+ connection.on("error", ({ error }) => {
1884
+ config.onError?.(error);
1885
+ });
1886
+ connection.on("reconnecting", () => {
1887
+ state.reconnectAttempts++;
1888
+ });
1889
+ connection.on("reconnected", () => {
1890
+ state.reconnectAttempts = 0;
1891
+ });
1892
+ connection.on("failed", ({ lastError }) => {
1893
+ config.onError?.(lastError);
1894
+ });
1895
+ }
1896
+ /**
1897
+ * Type guard to check if data is a FeatureCollection.
1898
+ */
1899
+ isFeatureCollection(data) {
1900
+ return typeof data === "object" && data !== null && "type" in data && data.type === "FeatureCollection" && "features" in data && Array.isArray(data.features);
1901
+ }
1902
+ };
1903
+
1904
+ // src/data/merge/data-merger.ts
1905
+ var DataMerger = class {
1906
+ /**
1907
+ * Merge two FeatureCollections using the specified strategy.
1908
+ *
1909
+ * @param existing - Existing feature collection
1910
+ * @param incoming - Incoming feature collection to merge
1911
+ * @param options - Merge options including strategy
1912
+ * @returns Merge result with statistics
1913
+ * @throws {Error} If merge strategy requires missing options
1914
+ *
1915
+ * @example
1916
+ * ```typescript
1917
+ * const merger = new DataMerger();
1918
+ *
1919
+ * const result = merger.merge(existingData, newData, {
1920
+ * strategy: 'merge',
1921
+ * updateKey: 'id'
1922
+ * });
1923
+ *
1924
+ * console.log(`Added: ${result.added}, Updated: ${result.updated}`);
1925
+ * console.log(`Total features: ${result.total}`);
1926
+ * ```
1927
+ */
1928
+ merge(existing, incoming, options) {
1929
+ switch (options.strategy) {
1930
+ case "replace":
1931
+ return this.mergeReplace(existing, incoming);
1932
+ case "merge":
1933
+ return this.mergeMerge(existing, incoming, options);
1934
+ case "append-window":
1935
+ return this.mergeAppendWindow(existing, incoming, options);
1936
+ default:
1937
+ throw new Error(
1938
+ `Unknown merge strategy: ${options.strategy}`
1939
+ );
1940
+ }
1941
+ }
1942
+ /**
1943
+ * Replace strategy: Complete replacement of existing data.
1944
+ */
1945
+ mergeReplace(existing, incoming) {
1946
+ return {
1947
+ data: incoming,
1948
+ added: incoming.features.length,
1949
+ updated: 0,
1950
+ removed: existing.features.length,
1951
+ total: incoming.features.length
1952
+ };
1953
+ }
1954
+ /**
1955
+ * Merge strategy: Update by key, keep unmatched features.
1956
+ */
1957
+ mergeMerge(existing, incoming, options) {
1958
+ if (!options.updateKey) {
1959
+ throw new Error("updateKey is required for merge strategy");
1960
+ }
1961
+ const updateKey = options.updateKey;
1962
+ let added = 0;
1963
+ let updated = 0;
1964
+ const existingMap = /* @__PURE__ */ new Map();
1965
+ for (const feature of existing.features) {
1966
+ const key = feature.properties?.[updateKey];
1967
+ if (key !== void 0 && key !== null) {
1968
+ existingMap.set(key, feature);
1969
+ }
1970
+ }
1971
+ for (const feature of incoming.features) {
1972
+ const key = feature.properties?.[updateKey];
1973
+ if (key !== void 0 && key !== null) {
1974
+ if (existingMap.has(key)) {
1975
+ updated++;
1976
+ } else {
1977
+ added++;
1978
+ }
1979
+ existingMap.set(key, feature);
1980
+ }
1981
+ }
1982
+ const features = Array.from(existingMap.values());
1983
+ return {
1984
+ data: {
1985
+ type: "FeatureCollection",
1986
+ features
1987
+ },
1988
+ added,
1989
+ updated,
1990
+ removed: 0,
1991
+ total: features.length
1992
+ };
1993
+ }
1994
+ /**
1995
+ * Append-window strategy: Add with time/size limits.
1996
+ */
1997
+ mergeAppendWindow(existing, incoming, options) {
1998
+ const initialCount = existing.features.length;
1999
+ let features = [...existing.features, ...incoming.features];
2000
+ if (options.windowDuration && options.timestampField) {
2001
+ const cutoffTime = Date.now() - options.windowDuration;
2002
+ features = features.filter((feature) => {
2003
+ const timestamp = feature.properties?.[options.timestampField];
2004
+ if (typeof timestamp === "number") {
2005
+ return timestamp >= cutoffTime;
2006
+ }
2007
+ return true;
2008
+ });
2009
+ features.sort((a, b) => {
2010
+ const timeA = a.properties?.[options.timestampField] ?? 0;
2011
+ const timeB = b.properties?.[options.timestampField] ?? 0;
2012
+ return timeB - timeA;
2013
+ });
2014
+ if (options.windowSize && features.length > options.windowSize) {
2015
+ features = features.slice(0, options.windowSize);
2016
+ }
2017
+ const removed2 = initialCount + incoming.features.length - features.length;
2018
+ return {
2019
+ data: {
2020
+ type: "FeatureCollection",
2021
+ features
2022
+ },
2023
+ added: incoming.features.length,
2024
+ updated: 0,
2025
+ removed: removed2,
2026
+ total: features.length
2027
+ };
2028
+ }
2029
+ if (options.windowSize) {
2030
+ if (options.timestampField) {
2031
+ features.sort((a, b) => {
2032
+ const timeA = a.properties?.[options.timestampField] ?? 0;
2033
+ const timeB = b.properties?.[options.timestampField] ?? 0;
2034
+ return timeB - timeA;
2035
+ });
2036
+ }
2037
+ if (features.length > options.windowSize) {
2038
+ features = features.slice(0, options.windowSize);
2039
+ }
2040
+ }
2041
+ const removed = initialCount + incoming.features.length - features.length;
2042
+ return {
2043
+ data: {
2044
+ type: "FeatureCollection",
2045
+ features
2046
+ },
2047
+ added: incoming.features.length,
2048
+ updated: 0,
2049
+ removed,
2050
+ total: features.length
2051
+ };
2052
+ }
2053
+ };
2054
+
2055
+ // src/ui/loading-manager.ts
2056
+ var LoadingManager = class _LoadingManager extends EventEmitter {
2057
+ config;
2058
+ subscriptions = /* @__PURE__ */ new Map();
2059
+ static DEFAULT_CONFIG = {
2060
+ showUI: false,
2061
+ messages: {
2062
+ loading: "Loading...",
2063
+ error: "Failed to load data",
2064
+ retry: "Retrying..."
2065
+ },
2066
+ spinnerStyle: "circle",
2067
+ minDisplayTime: 300
2068
+ };
2069
+ /**
2070
+ * Create a new LoadingManager.
2071
+ *
2072
+ * @param config - Loading manager configuration
2073
+ *
2074
+ * @example
2075
+ * ```typescript
2076
+ * const manager = new LoadingManager({
2077
+ * showUI: true,
2078
+ * messages: {
2079
+ * loading: 'Fetching data...',
2080
+ * error: 'Could not load data'
2081
+ * },
2082
+ * spinnerStyle: 'dots',
2083
+ * minDisplayTime: 500
2084
+ * });
2085
+ * ```
2086
+ */
2087
+ constructor(config) {
2088
+ super();
2089
+ this.config = {
2090
+ ..._LoadingManager.DEFAULT_CONFIG,
2091
+ ...config,
2092
+ messages: {
2093
+ ..._LoadingManager.DEFAULT_CONFIG.messages,
2094
+ ...config?.messages
2095
+ }
2096
+ };
2097
+ }
2098
+ /**
2099
+ * Show loading state for a layer.
2100
+ *
2101
+ * @param layerId - Layer identifier
2102
+ * @param container - Container element for UI overlay
2103
+ * @param message - Custom loading message
2104
+ *
2105
+ * @example
2106
+ * ```typescript
2107
+ * const container = document.getElementById('map');
2108
+ * manager.showLoading('earthquakes', container, 'Loading earthquake data...');
2109
+ * ```
2110
+ */
2111
+ showLoading(layerId, container, message) {
2112
+ const existingSub = this.subscriptions.get(layerId);
2113
+ if (existingSub?.state.isLoading) {
2114
+ return;
2115
+ }
2116
+ const state = {
2117
+ isLoading: true,
2118
+ startTime: Date.now(),
2119
+ message: message || this.config.messages.loading
2120
+ };
2121
+ let overlay = null;
2122
+ if (this.config.showUI) {
2123
+ overlay = this.createLoadingOverlay(
2124
+ state.message || this.config.messages.loading || "Loading..."
2125
+ );
2126
+ container.style.position = "relative";
2127
+ container.appendChild(overlay);
2128
+ }
2129
+ this.subscriptions.set(layerId, {
2130
+ state,
2131
+ container,
2132
+ overlay,
2133
+ minDisplayTimer: null
2134
+ });
2135
+ this.emit("loading:start", { layerId, message: state.message });
2136
+ }
2137
+ /**
2138
+ * Hide loading state for a layer.
2139
+ *
2140
+ * @param layerId - Layer identifier
2141
+ * @param result - Optional result information
2142
+ *
2143
+ * @example
2144
+ * ```typescript
2145
+ * manager.hideLoading('earthquakes', { fromCache: true });
2146
+ * ```
2147
+ */
2148
+ hideLoading(layerId, result) {
2149
+ const subscription = this.subscriptions.get(layerId);
2150
+ if (!subscription?.state.isLoading) {
2151
+ return;
2152
+ }
2153
+ const duration = Date.now() - (subscription.state.startTime || Date.now());
2154
+ const timeRemaining = Math.max(0, this.config.minDisplayTime - duration);
2155
+ const cleanup = () => {
2156
+ if (subscription.overlay) {
2157
+ subscription.overlay.remove();
2158
+ }
2159
+ subscription.state.isLoading = false;
2160
+ subscription.state.startTime = null;
2161
+ subscription.state.error = void 0;
2162
+ subscription.state.retryAttempt = void 0;
2163
+ this.emit("loading:complete", {
2164
+ layerId,
2165
+ duration,
2166
+ fromCache: result?.fromCache ?? false
2167
+ });
2168
+ };
2169
+ if (timeRemaining > 0 && subscription.overlay) {
2170
+ subscription.minDisplayTimer = window.setTimeout(cleanup, timeRemaining);
2171
+ } else {
2172
+ cleanup();
2173
+ }
2174
+ }
2175
+ /**
2176
+ * Show error state for a layer.
2177
+ *
2178
+ * @param layerId - Layer identifier
2179
+ * @param container - Container element for UI overlay
2180
+ * @param error - Error that occurred
2181
+ * @param onRetry - Optional retry callback
2182
+ *
2183
+ * @example
2184
+ * ```typescript
2185
+ * manager.showError('earthquakes', container, error, () => {
2186
+ * // Retry loading
2187
+ * fetchData();
2188
+ * });
2189
+ * ```
2190
+ */
2191
+ showError(layerId, container, error, onRetry) {
2192
+ const subscription = this.subscriptions.get(layerId);
2193
+ const state = subscription?.state || {
2194
+ isLoading: false,
2195
+ startTime: null
2196
+ };
2197
+ state.error = error;
2198
+ state.isLoading = false;
2199
+ if (subscription?.overlay) {
2200
+ subscription.overlay.remove();
2201
+ }
2202
+ let overlay = null;
2203
+ if (this.config.showUI) {
2204
+ overlay = this.createErrorOverlay(
2205
+ error.message || this.config.messages.error || "An error occurred",
2206
+ onRetry
2207
+ );
2208
+ container.style.position = "relative";
2209
+ container.appendChild(overlay);
2210
+ }
2211
+ this.subscriptions.set(layerId, {
2212
+ state,
2213
+ container,
2214
+ overlay,
2215
+ minDisplayTimer: null
2216
+ });
2217
+ this.emit("loading:error", {
2218
+ layerId,
2219
+ error,
2220
+ retrying: !!onRetry
2221
+ });
2222
+ }
2223
+ /**
2224
+ * Show retrying state for a layer.
2225
+ *
2226
+ * @param layerId - Layer identifier
2227
+ * @param attempt - Current retry attempt number
2228
+ * @param delay - Delay before retry in milliseconds
2229
+ *
2230
+ * @example
2231
+ * ```typescript
2232
+ * manager.showRetrying('earthquakes', 2, 2000);
2233
+ * ```
2234
+ */
2235
+ showRetrying(layerId, attempt, delay) {
2236
+ const subscription = this.subscriptions.get(layerId);
2237
+ if (subscription) {
2238
+ subscription.state.retryAttempt = attempt;
2239
+ if (subscription.overlay && this.config.showUI) {
2240
+ const message = `${this.config.messages.retry} (attempt ${attempt})`;
2241
+ const newOverlay = this.createLoadingOverlay(message);
2242
+ subscription.overlay.replaceWith(newOverlay);
2243
+ subscription.overlay = newOverlay;
2244
+ }
2245
+ }
2246
+ this.emit("loading:retry", { layerId, attempt, delay });
2247
+ }
2248
+ /**
2249
+ * Get loading state for a layer.
2250
+ *
2251
+ * @param layerId - Layer identifier
2252
+ * @returns Loading state or null if not found
2253
+ *
2254
+ * @example
2255
+ * ```typescript
2256
+ * const state = manager.getState('earthquakes');
2257
+ * if (state?.isLoading) {
2258
+ * console.log('Still loading...');
2259
+ * }
2260
+ * ```
2261
+ */
2262
+ getState(layerId) {
2263
+ const subscription = this.subscriptions.get(layerId);
2264
+ return subscription ? { ...subscription.state } : null;
2265
+ }
2266
+ /**
2267
+ * Check if a layer is currently loading.
2268
+ *
2269
+ * @param layerId - Layer identifier
2270
+ * @returns True if loading, false otherwise
2271
+ *
2272
+ * @example
2273
+ * ```typescript
2274
+ * if (manager.isLoading('earthquakes')) {
2275
+ * console.log('Loading in progress');
2276
+ * }
2277
+ * ```
2278
+ */
2279
+ isLoading(layerId) {
2280
+ return this.subscriptions.get(layerId)?.state.isLoading ?? false;
2281
+ }
2282
+ /**
2283
+ * Clear all loading states and UI.
2284
+ *
2285
+ * @example
2286
+ * ```typescript
2287
+ * manager.clearAll();
2288
+ * ```
2289
+ */
2290
+ clearAll() {
2291
+ for (const [layerId, subscription] of this.subscriptions.entries()) {
2292
+ if (subscription.minDisplayTimer) {
2293
+ clearTimeout(subscription.minDisplayTimer);
2294
+ }
2295
+ if (subscription.overlay) {
2296
+ subscription.overlay.remove();
2297
+ }
2298
+ this.subscriptions.delete(layerId);
2299
+ }
2300
+ }
2301
+ /**
2302
+ * Clean up all resources.
2303
+ *
2304
+ * @example
2305
+ * ```typescript
2306
+ * manager.destroy();
2307
+ * ```
2308
+ */
2309
+ destroy() {
2310
+ this.clearAll();
2311
+ this.removeAllListeners();
2312
+ }
2313
+ /**
2314
+ * Create loading overlay element.
2315
+ */
2316
+ createLoadingOverlay(message) {
2317
+ const overlay = document.createElement("div");
2318
+ overlay.className = "mly-loading-overlay";
2319
+ const content = document.createElement("div");
2320
+ content.className = "mly-loading-content";
2321
+ const spinner = document.createElement("div");
2322
+ spinner.className = `mly-spinner mly-spinner--${this.config.spinnerStyle}`;
2323
+ const text = document.createElement("div");
2324
+ text.className = "mly-loading-text";
2325
+ text.textContent = message;
2326
+ content.appendChild(spinner);
2327
+ content.appendChild(text);
2328
+ overlay.appendChild(content);
2329
+ return overlay;
2330
+ }
2331
+ /**
2332
+ * Create error overlay element.
2333
+ */
2334
+ createErrorOverlay(message, onRetry) {
2335
+ const overlay = document.createElement("div");
2336
+ overlay.className = "mly-loading-overlay mly-loading-overlay--error";
2337
+ const content = document.createElement("div");
2338
+ content.className = "mly-error-content";
2339
+ const icon = document.createElement("div");
2340
+ icon.className = "mly-error-icon";
2341
+ icon.textContent = "\u26A0";
2342
+ const text = document.createElement("div");
2343
+ text.className = "mly-error-text";
2344
+ text.textContent = message;
2345
+ content.appendChild(icon);
2346
+ content.appendChild(text);
2347
+ if (onRetry) {
2348
+ const button = document.createElement("button");
2349
+ button.className = "mly-retry-button";
2350
+ button.textContent = "Retry";
2351
+ button.onclick = () => {
2352
+ overlay.remove();
2353
+ onRetry();
2354
+ };
2355
+ content.appendChild(button);
2356
+ }
2357
+ overlay.appendChild(content);
2358
+ return overlay;
2359
+ }
2360
+ };
2361
+
2362
+ // src/renderer/layer-manager.ts
2363
+ var LayerManager = class {
2364
+ map;
2365
+ callbacks;
2366
+ dataFetcher;
2367
+ pollingManager;
2368
+ streamManager;
2369
+ dataMerger;
2370
+ loadingManager;
2371
+ sourceData;
2372
+ layerToSource;
2373
+ // Legacy support (deprecated)
2374
+ refreshIntervals;
2375
+ abortControllers;
2376
+ constructor(map, callbacks) {
2377
+ this.map = map;
2378
+ this.callbacks = callbacks || {};
2379
+ this.dataFetcher = new DataFetcher();
2380
+ this.pollingManager = new PollingManager();
2381
+ this.streamManager = new StreamManager();
2382
+ this.dataMerger = new DataMerger();
2383
+ this.loadingManager = new LoadingManager({ showUI: false });
2384
+ this.sourceData = /* @__PURE__ */ new Map();
2385
+ this.layerToSource = /* @__PURE__ */ new Map();
2386
+ this.refreshIntervals = /* @__PURE__ */ new Map();
2387
+ this.abortControllers = /* @__PURE__ */ new Map();
2388
+ }
2389
+ async addLayer(layer) {
2390
+ const sourceId = `${layer.id}-source`;
2391
+ this.layerToSource.set(layer.id, sourceId);
2392
+ await this.addSource(sourceId, layer);
2393
+ const layerSpec = {
2394
+ id: layer.id,
2395
+ type: layer.type,
2396
+ source: sourceId
2397
+ };
2398
+ if ("paint" in layer && layer.paint) layerSpec.paint = layer.paint;
2399
+ if ("layout" in layer && layer.layout) layerSpec.layout = layer.layout;
2400
+ if ("source-layer" in layer && layer["source-layer"])
2401
+ layerSpec["source-layer"] = layer["source-layer"];
2402
+ if (layer.minzoom !== void 0) layerSpec.minzoom = layer.minzoom;
2403
+ if (layer.maxzoom !== void 0) layerSpec.maxzoom = layer.maxzoom;
2404
+ if (layer.filter) layerSpec.filter = layer.filter;
2405
+ if (layer.visible === false) {
2406
+ layerSpec.layout = layerSpec.layout || {};
2407
+ layerSpec.layout.visibility = "none";
2408
+ }
2409
+ this.map.addLayer(layerSpec, layer.before);
2410
+ if (typeof layer.source === "object" && layer.source !== null) {
2411
+ const sourceObj = layer.source;
2412
+ if (sourceObj.type === "geojson") {
2413
+ if (sourceObj.refresh || sourceObj.refreshInterval) {
2414
+ await this.setupDataUpdates(layer.id, sourceId, sourceObj);
2415
+ }
2416
+ }
2417
+ }
2418
+ }
2419
+ async addSource(sourceId, layer) {
2420
+ if (typeof layer.source === "string") {
2421
+ if (!this.map.getSource(layer.source)) {
2422
+ throw new Error(`Source reference '${layer.source}' not found`);
2423
+ }
2424
+ return;
2425
+ }
2426
+ const source = layer.source;
2427
+ if (source.type === "geojson") {
2428
+ const geojsonSource = source;
2429
+ if (geojsonSource.url) {
2430
+ await this.addGeoJSONSourceFromURL(sourceId, layer.id, geojsonSource);
2431
+ } else if (geojsonSource.data) {
2432
+ this.map.addSource(sourceId, {
2433
+ type: "geojson",
2434
+ data: geojsonSource.data,
2435
+ cluster: geojsonSource.cluster,
2436
+ clusterRadius: geojsonSource.clusterRadius,
2437
+ clusterMaxZoom: geojsonSource.clusterMaxZoom,
2438
+ clusterMinPoints: geojsonSource.clusterMinPoints,
2439
+ clusterProperties: geojsonSource.clusterProperties
2440
+ });
2441
+ } else if (geojsonSource.stream) {
2442
+ this.map.addSource(sourceId, {
2443
+ type: "geojson",
2444
+ data: { type: "FeatureCollection", features: [] }
2445
+ });
2446
+ }
2447
+ } else if (source.type === "vector") {
2448
+ const vectorSource = source;
2449
+ const vectorSpec = { type: "vector" };
2450
+ if (vectorSource.url) vectorSpec.url = vectorSource.url;
2451
+ if (vectorSource.tiles) vectorSpec.tiles = vectorSource.tiles;
2452
+ if (vectorSource.minzoom !== void 0)
2453
+ vectorSpec.minzoom = vectorSource.minzoom;
2454
+ if (vectorSource.maxzoom !== void 0)
2455
+ vectorSpec.maxzoom = vectorSource.maxzoom;
2456
+ if (vectorSource.bounds) vectorSpec.bounds = vectorSource.bounds;
2457
+ if (vectorSource.attribution)
2458
+ vectorSpec.attribution = vectorSource.attribution;
2459
+ this.map.addSource(sourceId, vectorSpec);
2460
+ } else if (source.type === "raster") {
2461
+ const rasterSource = source;
2462
+ const rasterSpec = { type: "raster" };
2463
+ if (rasterSource.url) rasterSpec.url = rasterSource.url;
2464
+ if (rasterSource.tiles) rasterSpec.tiles = rasterSource.tiles;
2465
+ if (rasterSource.tileSize !== void 0)
2466
+ rasterSpec.tileSize = rasterSource.tileSize;
2467
+ if (rasterSource.minzoom !== void 0)
2468
+ rasterSpec.minzoom = rasterSource.minzoom;
2469
+ if (rasterSource.maxzoom !== void 0)
2470
+ rasterSpec.maxzoom = rasterSource.maxzoom;
2471
+ if (rasterSource.bounds) rasterSpec.bounds = rasterSource.bounds;
2472
+ if (rasterSource.attribution)
2473
+ rasterSpec.attribution = rasterSource.attribution;
2474
+ this.map.addSource(sourceId, rasterSpec);
2475
+ } else if (source.type === "image") {
2476
+ const imageSource = source;
2477
+ this.map.addSource(sourceId, {
2478
+ type: "image",
2479
+ url: imageSource.url,
2480
+ coordinates: imageSource.coordinates
2481
+ });
2482
+ } else if (source.type === "video") {
2483
+ const videoSource = source;
2484
+ this.map.addSource(sourceId, {
2485
+ type: "video",
2486
+ urls: videoSource.urls,
2487
+ coordinates: videoSource.coordinates
2488
+ });
2489
+ }
2490
+ }
2491
+ async addGeoJSONSourceFromURL(sourceId, layerId, config) {
2492
+ let initialData = {
2493
+ type: "FeatureCollection",
2494
+ features: []
2495
+ };
2496
+ if (config.prefetchedData) {
2497
+ initialData = config.prefetchedData;
2498
+ } else if (config.data) {
2499
+ initialData = config.data;
2500
+ }
2501
+ this.map.addSource(sourceId, {
2502
+ type: "geojson",
2503
+ data: initialData,
2504
+ cluster: config.cluster,
2505
+ clusterRadius: config.clusterRadius,
2506
+ clusterMaxZoom: config.clusterMaxZoom,
2507
+ clusterMinPoints: config.clusterMinPoints,
2508
+ clusterProperties: config.clusterProperties
2509
+ });
2510
+ this.sourceData.set(sourceId, initialData);
2511
+ if (config.url && !config.prefetchedData) {
2512
+ this.callbacks.onDataLoading?.(layerId);
2513
+ try {
2514
+ const cacheEnabled = config.cache?.enabled ?? true;
2515
+ const cacheTTL = config.cache?.ttl;
2516
+ const result = await this.dataFetcher.fetch(config.url, {
2517
+ skipCache: !cacheEnabled,
2518
+ ttl: cacheTTL
2519
+ });
2520
+ const data = result.data;
2521
+ this.sourceData.set(sourceId, data);
2522
+ const source = this.map.getSource(sourceId);
2523
+ if (source?.setData) {
2524
+ source.setData(data);
2525
+ }
2526
+ this.callbacks.onDataLoaded?.(layerId, data.features.length);
2527
+ } catch (error) {
2528
+ this.callbacks.onDataError?.(layerId, error);
2529
+ }
2530
+ } else if (config.prefetchedData) {
2531
+ this.callbacks.onDataLoaded?.(layerId, initialData.features.length);
2532
+ }
2533
+ }
2534
+ /**
2535
+ * Setup polling and/or streaming for a GeoJSON source
2536
+ */
2537
+ async setupDataUpdates(layerId, sourceId, config) {
2538
+ if (config.stream) {
2539
+ const streamConfig = config.stream;
2540
+ await this.streamManager.connect(layerId, {
2541
+ type: streamConfig.type,
2542
+ url: streamConfig.url || config.url,
2543
+ onData: (data) => {
2544
+ this.handleDataUpdate(sourceId, layerId, data, {
2545
+ strategy: config.refresh?.updateStrategy || config.updateStrategy || "replace",
2546
+ updateKey: config.refresh?.updateKey || config.updateKey,
2547
+ windowSize: config.refresh?.windowSize,
2548
+ windowDuration: config.refresh?.windowDuration,
2549
+ timestampField: config.refresh?.timestampField
2550
+ });
2551
+ },
2552
+ onError: (error) => {
2553
+ this.callbacks.onDataError?.(layerId, error);
2554
+ },
2555
+ reconnect: {
2556
+ enabled: streamConfig.reconnect !== false,
2557
+ maxRetries: streamConfig.reconnectMaxAttempts,
2558
+ initialDelay: streamConfig.reconnectDelay,
2559
+ maxDelay: streamConfig.reconnectMaxDelay
2560
+ },
2561
+ eventTypes: streamConfig.eventTypes,
2562
+ protocols: streamConfig.protocols
2563
+ });
2564
+ }
2565
+ const refreshInterval = config.refresh?.refreshInterval || config.refreshInterval;
2566
+ if (refreshInterval && config.url) {
2567
+ const url = config.url;
2568
+ const cacheEnabled = config.cache?.enabled ?? true;
2569
+ const cacheTTL = config.cache?.ttl;
2570
+ await this.pollingManager.start(layerId, {
2571
+ interval: refreshInterval,
2572
+ onTick: async () => {
2573
+ const result = await this.dataFetcher.fetch(url, {
2574
+ skipCache: !cacheEnabled,
2575
+ ttl: cacheTTL
2576
+ });
2577
+ this.handleDataUpdate(sourceId, layerId, result.data, {
2578
+ strategy: config.refresh?.updateStrategy || config.updateStrategy || "replace",
2579
+ updateKey: config.refresh?.updateKey || config.updateKey,
2580
+ windowSize: config.refresh?.windowSize,
2581
+ windowDuration: config.refresh?.windowDuration,
2582
+ timestampField: config.refresh?.timestampField
2583
+ });
2584
+ },
2585
+ onError: (error) => {
2586
+ this.callbacks.onDataError?.(layerId, error);
2587
+ }
2588
+ });
2589
+ }
2590
+ }
2591
+ /**
2592
+ * Handle incoming data updates with merge strategy
2593
+ */
2594
+ handleDataUpdate(sourceId, layerId, incoming, options) {
2595
+ const existing = this.sourceData.get(sourceId) || {
2596
+ type: "FeatureCollection",
2597
+ features: []
2598
+ };
2599
+ const mergeResult = this.dataMerger.merge(existing, incoming, options);
2600
+ this.sourceData.set(sourceId, mergeResult.data);
2601
+ const source = this.map.getSource(sourceId);
2602
+ if (source?.setData) {
2603
+ source.setData(mergeResult.data);
2604
+ }
2605
+ this.callbacks.onDataLoaded?.(layerId, mergeResult.total);
2606
+ }
2607
+ /**
2608
+ * Pause data refresh for a layer (polling)
2609
+ */
2610
+ pauseRefresh(layerId) {
2611
+ this.pollingManager.pause(layerId);
2612
+ }
2613
+ /**
2614
+ * Resume data refresh for a layer (polling)
2615
+ */
2616
+ resumeRefresh(layerId) {
2617
+ this.pollingManager.resume(layerId);
2618
+ }
2619
+ /**
2620
+ * Force immediate refresh for a layer (polling)
2621
+ */
2622
+ async refreshNow(layerId) {
2623
+ await this.pollingManager.triggerNow(layerId);
2624
+ }
2625
+ /**
2626
+ * Disconnect streaming connection for a layer
2627
+ */
2628
+ disconnectStream(layerId) {
2629
+ this.streamManager.disconnect(layerId);
2630
+ }
2631
+ removeLayer(layerId) {
2632
+ this.pollingManager.stop(layerId);
2633
+ this.streamManager.disconnect(layerId);
2634
+ this.loadingManager.hideLoading(layerId);
2635
+ this.stopRefreshInterval(layerId);
2636
+ const controller = this.abortControllers.get(layerId);
2637
+ if (controller) {
2638
+ controller.abort();
2639
+ this.abortControllers.delete(layerId);
2640
+ }
2641
+ if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
2642
+ const sourceId = this.layerToSource.get(layerId) || `${layerId}-source`;
2643
+ if (this.map.getSource(sourceId)) this.map.removeSource(sourceId);
2644
+ this.sourceData.delete(sourceId);
2645
+ this.layerToSource.delete(layerId);
2646
+ }
2647
+ setVisibility(layerId, visible) {
2648
+ if (!this.map.getLayer(layerId)) return;
2649
+ this.map.setLayoutProperty(
2650
+ layerId,
2651
+ "visibility",
2652
+ visible ? "visible" : "none"
2653
+ );
2654
+ }
2655
+ updateData(layerId, data) {
2656
+ const sourceId = `${layerId}-source`;
2657
+ const source = this.map.getSource(sourceId);
2658
+ if (source && source.setData) source.setData(data);
2659
+ }
2660
+ /**
2661
+ * @deprecated Legacy refresh method - use PollingManager instead
2662
+ */
2663
+ startRefreshInterval(layer) {
2664
+ if (typeof layer.source !== "object" || layer.source === null) {
2665
+ return;
2666
+ }
2667
+ const sourceObj = layer.source;
2668
+ if (sourceObj.type !== "geojson" || !sourceObj.url || !sourceObj.refreshInterval) {
2669
+ return;
2670
+ }
2671
+ const geojsonSource = layer.source;
2672
+ const interval = setInterval(async () => {
2673
+ const sourceId = `${layer.id}-source`;
2674
+ try {
2675
+ const cacheEnabled = geojsonSource.cache?.enabled ?? true;
2676
+ const cacheTTL = geojsonSource.cache?.ttl;
2677
+ const result = await this.dataFetcher.fetch(geojsonSource.url, {
2678
+ skipCache: !cacheEnabled,
2679
+ ttl: cacheTTL
2680
+ });
2681
+ const data = result.data;
2682
+ this.sourceData.set(sourceId, data);
2683
+ const source = this.map.getSource(sourceId);
2684
+ if (source?.setData) {
2685
+ source.setData(data);
2686
+ }
2687
+ this.callbacks.onDataLoaded?.(layer.id, data.features.length);
2688
+ } catch (error) {
2689
+ this.callbacks.onDataError?.(layer.id, error);
2690
+ }
2691
+ }, geojsonSource.refreshInterval);
2692
+ this.refreshIntervals.set(layer.id, interval);
2693
+ }
2694
+ stopRefreshInterval(layerId) {
2695
+ const interval = this.refreshIntervals.get(layerId);
2696
+ if (interval) {
2697
+ clearInterval(interval);
2698
+ this.refreshIntervals.delete(layerId);
2699
+ }
2700
+ }
2701
+ clearAllIntervals() {
2702
+ for (const interval of this.refreshIntervals.values())
2703
+ clearInterval(interval);
2704
+ this.refreshIntervals.clear();
2705
+ }
2706
+ destroy() {
2707
+ this.pollingManager.destroy();
2708
+ this.streamManager.destroy();
2709
+ this.loadingManager.destroy();
2710
+ this.sourceData.clear();
2711
+ this.layerToSource.clear();
2712
+ this.clearAllIntervals();
2713
+ for (const controller of this.abortControllers.values()) controller.abort();
2714
+ this.abortControllers.clear();
2715
+ }
2716
+ };
2717
+
2718
+ // src/renderer/popup-builder.ts
2719
+ var PopupBuilder = class {
2720
+ /**
2721
+ * Build HTML string from popup content config and feature properties
2722
+ */
2723
+ build(content, properties) {
2724
+ return content.map((item) => {
2725
+ const entries = Object.entries(item);
2726
+ if (entries.length === 0) return "";
2727
+ const entry = entries[0];
2728
+ if (!entry) return "";
2729
+ const [tag, items] = entry;
2730
+ if (!Array.isArray(items)) return "";
2731
+ const innerHTML = items.map((i) => this.buildItem(i, properties)).join("");
2732
+ return `<${tag}>${innerHTML}</${tag}>`;
2733
+ }).join("");
2734
+ }
2735
+ /**
2736
+ * Build a single content item
2737
+ */
2738
+ buildItem(item, properties) {
2739
+ if (item.str) {
2740
+ return this.escapeHtml(item.str);
2741
+ }
2742
+ if (item.property) {
2743
+ const value = properties[item.property];
2744
+ if (value !== void 0 && value !== null) {
2745
+ if (item.format && typeof value === "number") {
2746
+ return this.formatNumber(value, item.format);
2747
+ }
2748
+ return this.escapeHtml(String(value));
2749
+ }
2750
+ return item.else ? this.escapeHtml(item.else) : "";
2751
+ }
2752
+ if (item.href) {
2753
+ const text = item.text || item.href;
2754
+ const target = item.target || "_blank";
2755
+ return `<a href="${this.escapeHtml(
2756
+ item.href
2757
+ )}" target="${target}">${this.escapeHtml(text)}</a>`;
2758
+ }
2759
+ if (item.src) {
2760
+ const alt = item.alt || "";
2761
+ return `<img src="${this.escapeHtml(item.src)}" alt="${this.escapeHtml(
2762
+ alt
2763
+ )}" />`;
2764
+ }
2765
+ return "";
2766
+ }
2767
+ /**
2768
+ * Format a number according to format string
2769
+ */
2770
+ formatNumber(value, format) {
2771
+ const useThousands = format.includes(",");
2772
+ const decimalMatch = format.match(/\.(\d+)/);
2773
+ const decimals = decimalMatch && decimalMatch[1] ? parseInt(decimalMatch[1]) : 0;
2774
+ let result = value.toFixed(decimals);
2775
+ if (useThousands) {
2776
+ const parts = result.split(".");
2777
+ if (parts[0]) {
2778
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2779
+ }
2780
+ result = parts.join(".");
2781
+ }
2782
+ return result;
2783
+ }
2784
+ /**
2785
+ * Escape HTML to prevent XSS
2786
+ */
2787
+ escapeHtml(str) {
2788
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2789
+ }
2790
+ };
2791
+
2792
+ // src/renderer/event-handler.ts
2793
+ var EventHandler = class {
2794
+ map;
2795
+ callbacks;
2796
+ popupBuilder;
2797
+ activePopup;
2798
+ attachedLayers;
2799
+ boundHandlers;
2800
+ constructor(map, callbacks) {
2801
+ this.map = map;
2802
+ this.callbacks = callbacks || {};
2803
+ this.popupBuilder = new PopupBuilder();
2804
+ this.activePopup = null;
2805
+ this.attachedLayers = /* @__PURE__ */ new Set();
2806
+ this.boundHandlers = /* @__PURE__ */ new Map();
2807
+ }
2808
+ /**
2809
+ * Attach events for a layer based on its interactive config
2810
+ */
2811
+ attachEvents(layer) {
2812
+ if (!layer.interactive) return;
2813
+ const interactive = layer.interactive;
2814
+ const { hover, click } = interactive;
2815
+ const handlers = {};
2816
+ if (hover) {
2817
+ handlers.mouseenter = (e) => {
2818
+ if (hover.cursor) {
2819
+ this.map.getCanvas().style.cursor = hover.cursor;
2820
+ }
2821
+ if (e.features?.[0]) {
2822
+ this.callbacks.onHover?.(layer.id, e.features[0], e.lngLat);
2823
+ }
2824
+ };
2825
+ handlers.mouseleave = () => {
2826
+ this.map.getCanvas().style.cursor = "";
2827
+ };
2828
+ this.map.on("mouseenter", layer.id, handlers.mouseenter);
2829
+ this.map.on("mouseleave", layer.id, handlers.mouseleave);
2830
+ }
2831
+ if (click) {
2832
+ handlers.click = (e) => {
2833
+ const feature = e.features?.[0];
2834
+ if (!feature) return;
2835
+ if (click.popup) {
2836
+ this.showPopup(click.popup, feature, e.lngLat);
2837
+ }
2838
+ this.callbacks.onClick?.(layer.id, feature, e.lngLat);
2839
+ };
2840
+ this.map.on("click", layer.id, handlers.click);
2841
+ }
2842
+ this.boundHandlers.set(layer.id, handlers);
2843
+ this.attachedLayers.add(layer.id);
2844
+ }
2845
+ /**
2846
+ * Show a popup with content
2847
+ */
2848
+ showPopup(content, feature, lngLat) {
2849
+ this.activePopup?.remove();
2850
+ const html = this.popupBuilder.build(content, feature.properties);
2851
+ this.activePopup = new maplibregl2.Popup().setLngLat(lngLat).setHTML(html).addTo(this.map);
2852
+ }
2853
+ /**
2854
+ * Detach events for a layer
2855
+ */
2856
+ detachEvents(layerId) {
2857
+ const handlers = this.boundHandlers.get(layerId);
2858
+ if (!handlers) return;
2859
+ if (handlers.click) {
2860
+ this.map.off("click", layerId, handlers.click);
2861
+ }
2862
+ if (handlers.mouseenter) {
2863
+ this.map.off("mouseenter", layerId, handlers.mouseenter);
2864
+ }
2865
+ if (handlers.mouseleave) {
2866
+ this.map.off("mouseleave", layerId, handlers.mouseleave);
2867
+ }
2868
+ this.boundHandlers.delete(layerId);
2869
+ this.attachedLayers.delete(layerId);
2870
+ }
2871
+ /**
2872
+ * Clean up all event handlers
2873
+ */
2874
+ destroy() {
2875
+ for (const layerId of this.attachedLayers) {
2876
+ this.detachEvents(layerId);
2877
+ }
2878
+ this.activePopup?.remove();
2879
+ this.activePopup = null;
2880
+ }
2881
+ };
2882
+
2883
+ // src/renderer/legend-builder.ts
2884
+ var LegendBuilder = class {
2885
+ /**
2886
+ * Build legend in container from layers
2887
+ */
2888
+ build(container, layers, config) {
2889
+ const el = typeof container === "string" ? document.getElementById(container) : container;
2890
+ if (!el) return;
2891
+ const items = config?.items || this.extractItems(layers);
2892
+ let html = '<div class="maplibre-legend">';
2893
+ if (config?.title) {
2894
+ html += `<div class="legend-title">${this.escapeHtml(config.title)}</div>`;
2895
+ }
2896
+ html += '<div class="legend-items">';
2897
+ for (const item of items) {
2898
+ html += this.renderItem(item);
2899
+ }
2900
+ html += "</div></div>";
2901
+ el.innerHTML = html;
2902
+ }
2903
+ /**
2904
+ * Render a single legend item
2905
+ */
2906
+ renderItem(item) {
2907
+ const shape = item.shape || "square";
2908
+ let symbol = "";
2909
+ switch (shape) {
2910
+ case "circle":
2911
+ symbol = `<span class="legend-symbol circle" style="background:${this.escapeHtml(item.color)}"></span>`;
2912
+ break;
2913
+ case "line":
2914
+ symbol = `<span class="legend-symbol line" style="background:${this.escapeHtml(item.color)}"></span>`;
2915
+ break;
2916
+ case "icon":
2917
+ if (item.icon) {
2918
+ symbol = `<span class="legend-symbol icon">${this.escapeHtml(item.icon)}</span>`;
2919
+ } else {
2920
+ symbol = `<span class="legend-symbol square" style="background:${this.escapeHtml(item.color)}"></span>`;
2921
+ }
2922
+ break;
2923
+ default:
2924
+ symbol = `<span class="legend-symbol square" style="background:${this.escapeHtml(item.color)}"></span>`;
2925
+ }
2926
+ return `<div class="legend-item">${symbol}<span class="legend-label">${this.escapeHtml(item.label)}</span></div>`;
2927
+ }
2928
+ /**
2929
+ * Extract legend items from layers
2930
+ */
2931
+ extractItems(layers) {
2932
+ return layers.filter((l) => l.legend && typeof l.legend === "object").map((l) => l.legend);
2933
+ }
2934
+ /**
2935
+ * Escape HTML to prevent XSS
2936
+ */
2937
+ escapeHtml(str) {
2938
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2939
+ }
2940
+ };
2941
+ var ControlsManager = class {
2942
+ map;
2943
+ addedControls;
2944
+ constructor(map) {
2945
+ this.map = map;
2946
+ this.addedControls = [];
2947
+ }
2948
+ /**
2949
+ * Add controls to the map based on configuration
2950
+ */
2951
+ addControls(config) {
2952
+ if (!config) return;
2953
+ if (config.navigation) {
2954
+ const options = typeof config.navigation === "object" ? config.navigation : {};
2955
+ const position = options.position || "top-right";
2956
+ const control = new maplibregl2.NavigationControl();
2957
+ this.map.addControl(control, position);
2958
+ this.addedControls.push(control);
2959
+ }
2960
+ if (config.geolocate) {
2961
+ const options = typeof config.geolocate === "object" ? config.geolocate : {};
2962
+ const position = options.position || "top-right";
2963
+ const control = new maplibregl2.GeolocateControl({
2964
+ positionOptions: { enableHighAccuracy: true },
2965
+ trackUserLocation: true
2966
+ });
2967
+ this.map.addControl(control, position);
2968
+ this.addedControls.push(control);
2969
+ }
2970
+ if (config.scale) {
2971
+ const options = typeof config.scale === "object" ? config.scale : {};
2972
+ const position = options.position || "bottom-left";
2973
+ const control = new maplibregl2.ScaleControl();
2974
+ this.map.addControl(control, position);
2975
+ this.addedControls.push(control);
2976
+ }
2977
+ if (config.fullscreen) {
2978
+ const options = typeof config.fullscreen === "object" ? config.fullscreen : {};
2979
+ const position = options.position || "top-right";
2980
+ const control = new maplibregl2.FullscreenControl();
2981
+ this.map.addControl(control, position);
2982
+ this.addedControls.push(control);
2983
+ }
2984
+ }
2985
+ /**
2986
+ * Remove all controls from the map
2987
+ */
2988
+ removeAllControls() {
2989
+ for (const control of this.addedControls) {
2990
+ this.map.removeControl(control);
2991
+ }
2992
+ this.addedControls = [];
2993
+ }
2994
+ };
2995
+
2996
+ // src/renderer/map-renderer.ts
2997
+ var MapRenderer = class {
2998
+ map;
2999
+ layerManager;
3000
+ eventHandler;
3001
+ legendBuilder;
3002
+ controlsManager;
3003
+ eventListeners;
3004
+ isLoaded;
3005
+ constructor(container, config, layers = [], options = {}) {
3006
+ this.eventListeners = /* @__PURE__ */ new Map();
3007
+ this.isLoaded = false;
3008
+ this.map = new maplibregl2.Map({
3009
+ ...config,
3010
+ container: typeof container === "string" ? container : container,
3011
+ style: config.mapStyle,
3012
+ center: config.center,
3013
+ zoom: config.zoom,
3014
+ pitch: config.pitch ?? 0,
3015
+ bearing: config.bearing ?? 0,
3016
+ interactive: config.interactive ?? true
3017
+ });
3018
+ const layerCallbacks = {
3019
+ onDataLoading: (layerId) => this.emit("layer:data-loading", { layerId }),
3020
+ onDataLoaded: (layerId, featureCount) => this.emit("layer:data-loaded", { layerId, featureCount }),
3021
+ onDataError: (layerId, error) => this.emit("layer:data-error", { layerId, error })
3022
+ };
3023
+ const eventCallbacks = {
3024
+ onClick: (layerId, feature, lngLat) => this.emit("layer:click", { layerId, feature, lngLat }),
3025
+ onHover: (layerId, feature, lngLat) => this.emit("layer:hover", { layerId, feature, lngLat })
3026
+ };
3027
+ this.layerManager = new LayerManager(this.map, layerCallbacks);
3028
+ this.eventHandler = new EventHandler(this.map, eventCallbacks);
3029
+ this.legendBuilder = new LegendBuilder();
3030
+ this.controlsManager = new ControlsManager(this.map);
3031
+ this.map.on("load", () => {
3032
+ this.isLoaded = true;
3033
+ Promise.all(layers.map((layer) => this.addLayer(layer))).then(() => {
3034
+ this.emit("load", void 0);
3035
+ options.onLoad?.();
3036
+ }).catch((error) => {
3037
+ options.onError?.(error);
3038
+ });
3039
+ });
3040
+ this.map.on("error", (e) => {
3041
+ options.onError?.(e.error);
3042
+ });
3043
+ }
3044
+ /**
3045
+ * Get the underlying MapLibre map instance
3046
+ */
3047
+ getMap() {
3048
+ return this.map;
3049
+ }
3050
+ /**
3051
+ * Check if map is loaded
3052
+ */
3053
+ isMapLoaded() {
3054
+ return this.isLoaded;
3055
+ }
3056
+ /**
3057
+ * Add a layer to the map
3058
+ */
3059
+ async addLayer(layer) {
3060
+ await this.layerManager.addLayer(layer);
3061
+ this.eventHandler.attachEvents(layer);
3062
+ this.emit("layer:added", { layerId: layer.id });
3063
+ }
3064
+ /**
3065
+ * Remove a layer from the map
3066
+ */
3067
+ removeLayer(layerId) {
3068
+ this.eventHandler.detachEvents(layerId);
3069
+ this.layerManager.removeLayer(layerId);
3070
+ this.emit("layer:removed", { layerId });
3071
+ }
3072
+ /**
3073
+ * Set layer visibility
3074
+ */
3075
+ setLayerVisibility(layerId, visible) {
3076
+ this.layerManager.setVisibility(layerId, visible);
3077
+ }
3078
+ /**
3079
+ * Update layer data
3080
+ */
3081
+ updateLayerData(layerId, data) {
3082
+ this.layerManager.updateData(layerId, data);
3083
+ }
3084
+ /**
3085
+ * Add controls to the map
3086
+ */
3087
+ addControls(config) {
3088
+ this.controlsManager.addControls(config);
3089
+ }
3090
+ /**
3091
+ * Build legend in container
3092
+ */
3093
+ buildLegend(container, layers, config) {
3094
+ this.legendBuilder.build(container, layers, config);
3095
+ }
3096
+ /**
3097
+ * Get the legend builder instance
3098
+ */
3099
+ getLegendBuilder() {
3100
+ return this.legendBuilder;
3101
+ }
3102
+ /**
3103
+ * Register an event listener
3104
+ */
3105
+ on(event, callback) {
3106
+ if (!this.eventListeners.has(event)) {
3107
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
3108
+ }
3109
+ this.eventListeners.get(event).add(callback);
3110
+ }
3111
+ /**
3112
+ * Unregister an event listener
3113
+ */
3114
+ off(event, callback) {
3115
+ const listeners = this.eventListeners.get(event);
3116
+ if (listeners) {
3117
+ listeners.delete(callback);
3118
+ }
3119
+ }
3120
+ /**
3121
+ * Emit an event
3122
+ */
3123
+ emit(event, data) {
3124
+ const listeners = this.eventListeners.get(event);
3125
+ if (listeners) {
3126
+ for (const callback of listeners) {
3127
+ callback(data);
3128
+ }
3129
+ }
3130
+ }
3131
+ /**
3132
+ * Destroy the map and clean up resources
3133
+ */
3134
+ destroy() {
3135
+ this.eventHandler.destroy();
3136
+ this.layerManager.destroy();
3137
+ this.controlsManager.removeAllControls();
3138
+ this.eventListeners.clear();
3139
+ this.map.remove();
3140
+ }
3141
+ };
3142
+ var ValidTagNames = [
3143
+ "h1",
3144
+ "h2",
3145
+ "h3",
3146
+ "h4",
3147
+ "h5",
3148
+ "h6",
3149
+ "p",
3150
+ "span",
3151
+ "div",
3152
+ "a",
3153
+ "strong",
3154
+ "em",
3155
+ "code",
3156
+ "pre",
3157
+ "img",
3158
+ "iframe",
3159
+ "ul",
3160
+ "ol",
3161
+ "li",
3162
+ "blockquote",
3163
+ "hr",
3164
+ "br"
3165
+ ];
3166
+ var ContentElementSchema = z.object({
3167
+ // Content
3168
+ str: z.string().optional().describe("Static text string"),
3169
+ property: z.string().optional().describe("Dynamic property from context"),
3170
+ else: z.string().optional().describe("Fallback value if property missing"),
3171
+ // Styling
3172
+ classList: z.union([z.string(), z.array(z.string())]).optional().describe("CSS class names (space-separated string or array)"),
3173
+ id: z.string().optional().describe("Element ID attribute"),
3174
+ style: z.string().optional().describe("Inline CSS styles"),
3175
+ // Links
3176
+ href: z.string().url().optional().describe("Link URL"),
3177
+ target: z.string().optional().describe("Link target (_blank, _self, _parent, _top)"),
3178
+ // Media
3179
+ src: z.string().url().optional().describe("Source URL for img or iframe"),
3180
+ alt: z.string().optional().describe("Alternative text for images"),
3181
+ width: z.union([z.string(), z.number()]).optional().describe("Width (pixels or %)"),
3182
+ height: z.union([z.string(), z.number()]).optional().describe("Height (pixels or %)")
3183
+ }).passthrough().describe("Content element with styling and properties");
3184
+ var ContentItemSchema = z.record(z.enum(ValidTagNames), z.array(ContentElementSchema)).describe("Content item mapping tag to elements");
3185
+ var ContentBlockSchema = z.object({
3186
+ type: z.literal("content").describe("Block type"),
3187
+ id: z.string().optional().describe("Unique block identifier"),
3188
+ className: z.string().optional().describe("CSS class name for the block container"),
3189
+ style: z.string().optional().describe("Inline CSS styles for the block container"),
3190
+ content: z.array(ContentItemSchema).describe("Array of content items to render")
3191
+ }).describe("Content block for rich text and media");
3192
+ var LongitudeSchema = z.number().min(-180, "Longitude must be >= -180").max(180, "Longitude must be <= 180").describe("Longitude in degrees (-180 to 180)");
3193
+ var LatitudeSchema = z.number().min(-90, "Latitude must be >= -90").max(90, "Latitude must be <= 90").describe("Latitude in degrees (-90 to 90)");
3194
+ var LngLatSchema = z.tuple([LongitudeSchema, LatitudeSchema]).describe("Geographic coordinates as [longitude, latitude]");
3195
+ var LngLatBoundsSchema = z.tuple([
3196
+ LongitudeSchema,
3197
+ // west
3198
+ LatitudeSchema,
3199
+ // south
3200
+ LongitudeSchema,
3201
+ // east
3202
+ LatitudeSchema
3203
+ // north
3204
+ ]).describe("Bounding box as [west, south, east, north]");
3205
+ var ColorSchema = z.string().refine(
3206
+ (val) => {
3207
+ if (val.startsWith("#")) {
3208
+ return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
3209
+ }
3210
+ if (val.startsWith("rgb")) {
3211
+ return /^rgba?\s*\([^)]+\)$/.test(val);
3212
+ }
3213
+ if (val.startsWith("hsl")) {
3214
+ return /^hsla?\s*\([^)]+\)$/.test(val);
3215
+ }
3216
+ return true;
3217
+ },
3218
+ {
3219
+ message: "Invalid color format. Use hex (#rgb, #rrggbb), rgb(), rgba(), hsl(), hsla(), or named colors."
3220
+ }
3221
+ ).describe("CSS color value");
3222
+ var ExpressionSchema = z.array(z.any()).refine((val) => val.length > 0 && typeof val[0] === "string", {
3223
+ message: 'Expression must be an array starting with a string operator (e.g., ["get", "property"])'
3224
+ }).describe("MapLibre expression for data-driven styling");
3225
+ var NumberOrExpressionSchema = z.union([z.number(), ExpressionSchema]).describe("Number value or MapLibre expression");
3226
+ var ColorOrExpressionSchema = z.union([ColorSchema, ExpressionSchema]).describe("Color value or MapLibre expression");
3227
+ var ZoomLevelSchema = z.number().min(0, "Zoom level must be >= 0").max(24, "Zoom level must be <= 24").describe("Map zoom level (0-24)");
3228
+ var StreamConfigSchema = z.object({
3229
+ type: z.enum(["websocket", "sse"]).describe("Streaming connection type"),
3230
+ url: z.string().url().optional().describe("WebSocket or SSE endpoint URL"),
3231
+ reconnect: z.boolean().default(true).describe("Automatically reconnect on disconnect"),
3232
+ reconnectMaxAttempts: z.number().min(0).default(10).describe("Maximum number of reconnection attempts"),
3233
+ reconnectDelay: z.number().min(100).default(1e3).describe("Initial delay in milliseconds before reconnecting"),
3234
+ reconnectMaxDelay: z.number().min(1e3).default(3e4).describe("Maximum delay in milliseconds for exponential backoff"),
3235
+ eventTypes: z.array(z.string()).optional().describe("Event types to listen for (SSE only)"),
3236
+ protocols: z.union([z.string(), z.array(z.string())]).optional().describe("WebSocket sub-protocols (WebSocket only)")
3237
+ });
3238
+ var LoadingConfigSchema = z.object({
3239
+ enabled: z.boolean().default(false).describe("Enable loading UI overlays"),
3240
+ message: z.string().optional().describe("Custom loading message to display"),
3241
+ showErrorOverlay: z.boolean().default(true).describe("Show error overlay on fetch failure")
3242
+ });
3243
+ var CacheConfigSchema = z.object({
3244
+ enabled: z.boolean().default(true).describe("Enable HTTP caching"),
3245
+ ttl: z.number().positive().optional().describe("Cache TTL in milliseconds (overrides default)")
3246
+ });
3247
+ var RefreshConfigSchema = z.object({
3248
+ refreshInterval: z.number().min(1e3).optional().describe("Polling interval in milliseconds (minimum 1000ms)"),
3249
+ updateStrategy: z.enum(["replace", "merge", "append-window"]).default("replace").describe("How to merge incoming data with existing data"),
3250
+ updateKey: z.string().optional().describe("Property key for merge strategy (required for merge)"),
3251
+ windowSize: z.number().positive().optional().describe("Maximum features to keep (append-window)"),
3252
+ windowDuration: z.number().positive().optional().describe("Maximum age in milliseconds (append-window)"),
3253
+ timestampField: z.string().optional().describe("Property field containing timestamp (append-window)")
3254
+ }).refine((data) => !(data.updateStrategy === "merge" && !data.updateKey), {
3255
+ message: "updateKey is required when updateStrategy is 'merge'"
3256
+ });
3257
+ var GeoJSONSourceSchema = z.object({
3258
+ type: z.literal("geojson").describe("Source type"),
3259
+ url: z.string().url().optional().describe("URL to fetch GeoJSON data"),
3260
+ data: z.any().optional().describe("Inline GeoJSON object"),
3261
+ prefetchedData: z.any().optional().describe("Pre-fetched data from build time"),
3262
+ fetchStrategy: z.enum(["runtime", "build", "hybrid"]).default("runtime").describe("When to fetch data: runtime (default), build, or hybrid"),
3263
+ stream: StreamConfigSchema.optional().describe(
3264
+ "WebSocket/SSE streaming configuration"
3265
+ ),
3266
+ refresh: RefreshConfigSchema.optional().describe(
3267
+ "Polling refresh configuration"
3268
+ ),
3269
+ // Legacy support for direct refresh properties
3270
+ refreshInterval: z.number().min(1e3).optional().describe("Polling interval in milliseconds (legacy, use refresh.refreshInterval)"),
3271
+ updateStrategy: z.enum(["replace", "merge", "append-window"]).optional().describe("Update strategy (legacy, use refresh.updateStrategy)"),
3272
+ updateKey: z.string().optional().describe("Update key (legacy, use refresh.updateKey)"),
3273
+ loading: LoadingConfigSchema.optional().describe(
3274
+ "Loading UI configuration"
3275
+ ),
3276
+ cache: CacheConfigSchema.optional().describe("Cache configuration"),
3277
+ // MapLibre clustering options
3278
+ cluster: z.boolean().optional().describe("Enable point clustering"),
3279
+ clusterRadius: z.number().int().min(0).default(50).describe("Cluster radius in pixels"),
3280
+ clusterMaxZoom: z.number().min(0).max(24).optional().describe("Maximum zoom level to cluster points"),
3281
+ clusterMinPoints: z.number().int().min(2).optional().describe("Minimum points to form a cluster"),
3282
+ clusterProperties: z.record(z.any()).optional().describe("Aggregate cluster properties"),
3283
+ // Additional MapLibre options (passthrough)
3284
+ tolerance: z.number().optional(),
3285
+ buffer: z.number().optional(),
3286
+ lineMetrics: z.boolean().optional(),
3287
+ generateId: z.boolean().optional(),
3288
+ promoteId: z.union([z.string(), z.record(z.string())]).optional(),
3289
+ attribution: z.string().optional()
3290
+ }).passthrough().refine((data) => data.url || data.data || data.prefetchedData, {
3291
+ message: 'GeoJSON source requires at least one of: url, data, or prefetchedData. Use "url" to fetch from an endpoint, "data" for inline GeoJSON, or "prefetchedData" for build-time fetched data.'
3292
+ });
3293
+ var VectorSourceSchema = z.object({
3294
+ type: z.literal("vector").describe("Source type"),
3295
+ url: z.string().url().optional().describe("TileJSON URL"),
3296
+ tiles: z.array(z.string().url()).optional().describe("Tile URL template array"),
3297
+ minzoom: z.number().min(0).max(24).optional().describe("Minimum zoom level"),
3298
+ maxzoom: z.number().min(0).max(24).optional().describe("Maximum zoom level"),
3299
+ bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional().describe("Bounding box [west, south, east, north]"),
3300
+ scheme: z.enum(["xyz", "tms"]).optional().describe("Tile coordinate scheme"),
3301
+ attribution: z.string().optional().describe("Attribution text"),
3302
+ promoteId: z.union([z.string(), z.record(z.string())]).optional(),
3303
+ volatile: z.boolean().optional()
3304
+ }).passthrough().refine((data) => data.url || data.tiles, {
3305
+ message: 'Vector source requires either "url" (TileJSON) or "tiles" (tile URL array). Provide at least one of these properties.'
3306
+ });
3307
+ var RasterSourceSchema = z.object({
3308
+ type: z.literal("raster").describe("Source type"),
3309
+ url: z.string().url().optional().describe("TileJSON URL"),
3310
+ tiles: z.array(z.string().url()).optional().describe("Tile URL template array"),
3311
+ tileSize: z.number().int().min(1).default(512).describe("Tile size in pixels"),
3312
+ minzoom: z.number().min(0).max(24).optional().describe("Minimum zoom level"),
3313
+ maxzoom: z.number().min(0).max(24).optional().describe("Maximum zoom level"),
3314
+ bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional().describe("Bounding box [west, south, east, north]"),
3315
+ scheme: z.enum(["xyz", "tms"]).optional().describe("Tile coordinate scheme"),
3316
+ attribution: z.string().optional().describe("Attribution text"),
3317
+ volatile: z.boolean().optional()
3318
+ }).passthrough().refine((data) => data.url || data.tiles, {
3319
+ message: 'Raster source requires either "url" (TileJSON) or "tiles" (tile URL array). Provide at least one of these properties.'
3320
+ });
3321
+ var ImageSourceSchema = z.object({
3322
+ type: z.literal("image").describe("Source type"),
3323
+ url: z.string().url().describe("Image URL"),
3324
+ coordinates: z.tuple([LngLatSchema, LngLatSchema, LngLatSchema, LngLatSchema]).describe(
3325
+ "Four corner coordinates [topLeft, topRight, bottomRight, bottomLeft]"
3326
+ )
3327
+ }).passthrough();
3328
+ var VideoSourceSchema = z.object({
3329
+ type: z.literal("video").describe("Source type"),
3330
+ urls: z.array(z.string().url()).min(1).describe("Array of video URLs for browser compatibility"),
3331
+ coordinates: z.tuple([LngLatSchema, LngLatSchema, LngLatSchema, LngLatSchema]).describe(
3332
+ "Four corner coordinates [topLeft, topRight, bottomRight, bottomLeft]"
3333
+ )
3334
+ }).passthrough();
3335
+ var LayerSourceSchema = z.union([
3336
+ GeoJSONSourceSchema,
3337
+ VectorSourceSchema,
3338
+ RasterSourceSchema,
3339
+ ImageSourceSchema,
3340
+ VideoSourceSchema
3341
+ ]);
3342
+
3343
+ // src/schemas/layer.schema.ts
3344
+ var PopupContentItemSchema = z.object({
3345
+ str: z.string().optional().describe("Static text string"),
3346
+ property: z.string().optional().describe("Feature property name"),
3347
+ else: z.string().optional().describe("Fallback value if property missing"),
3348
+ format: z.string().optional().describe('Number format string (e.g., ",.0f")'),
3349
+ href: z.string().url().optional().describe("Link URL"),
3350
+ text: z.string().optional().describe("Link text"),
3351
+ src: z.string().url().optional().describe("Image/iframe source"),
3352
+ alt: z.string().optional().describe("Image alt text")
3353
+ }).passthrough().describe("Popup content item with static or dynamic values");
3354
+ var PopupContentSchema = z.array(z.record(z.array(PopupContentItemSchema))).describe("Popup content structure as array of HTML elements");
3355
+ var InteractiveConfigSchema = z.object({
3356
+ hover: z.object({
3357
+ cursor: z.string().optional().describe('CSS cursor style (e.g., "pointer")'),
3358
+ highlight: z.boolean().optional().describe("Highlight feature on hover")
3359
+ }).optional().describe("Hover behavior"),
3360
+ click: z.object({
3361
+ popup: PopupContentSchema.optional().describe(
3362
+ "Popup content to display"
3363
+ ),
3364
+ action: z.string().optional().describe("Custom action name to trigger"),
3365
+ flyTo: z.object({
3366
+ center: z.tuple([z.number(), z.number()]).optional(),
3367
+ zoom: ZoomLevelSchema.optional(),
3368
+ duration: z.number().optional()
3369
+ }).optional().describe("Fly to location on click")
3370
+ }).optional().describe("Click behavior"),
3371
+ mouseenter: z.object({
3372
+ action: z.string().optional().describe("Custom action on mouse enter")
3373
+ }).optional(),
3374
+ mouseleave: z.object({
3375
+ action: z.string().optional().describe("Custom action on mouse leave")
3376
+ }).optional()
3377
+ }).optional().describe("Interactive event configuration");
3378
+ var LegendItemSchema = z.object({
3379
+ color: z.string().describe("CSS color value"),
3380
+ label: z.string().describe("Legend label text"),
3381
+ shape: z.enum(["circle", "square", "line", "icon"]).default("square").describe("Symbol shape"),
3382
+ icon: z.string().optional().describe("Icon name or URL (for shape: icon)")
3383
+ }).describe("Legend item configuration");
3384
+ var BaseLayerPropertiesSchema = z.object({
3385
+ id: z.string().describe("Unique layer identifier"),
3386
+ label: z.string().optional().describe("Human-readable layer label"),
3387
+ source: z.union([LayerSourceSchema, z.string()]).describe("Layer source (inline definition or source ID reference)"),
3388
+ "source-layer": z.string().optional().describe("Source layer name (for vector sources)"),
3389
+ minzoom: ZoomLevelSchema.optional().describe(
3390
+ "Minimum zoom level to show layer"
3391
+ ),
3392
+ maxzoom: ZoomLevelSchema.optional().describe(
3393
+ "Maximum zoom level to show layer"
3394
+ ),
3395
+ filter: ExpressionSchema.optional().describe("MapLibre filter expression"),
3396
+ visible: z.boolean().default(true).describe("Initial visibility state"),
3397
+ toggleable: z.boolean().default(true).describe("Allow users to toggle visibility"),
3398
+ before: z.string().optional().describe("Layer ID to insert this layer before"),
3399
+ interactive: InteractiveConfigSchema.describe(
3400
+ "Interactive event configuration"
3401
+ ),
3402
+ legend: LegendItemSchema.optional().describe("Legend configuration"),
3403
+ metadata: z.record(z.any()).optional().describe("Custom metadata")
3404
+ });
3405
+ var CircleLayerSchema = BaseLayerPropertiesSchema.extend({
3406
+ type: z.literal("circle").describe("Layer type"),
3407
+ paint: z.object({
3408
+ "circle-radius": NumberOrExpressionSchema.optional(),
3409
+ "circle-color": ColorOrExpressionSchema.optional(),
3410
+ "circle-blur": NumberOrExpressionSchema.optional(),
3411
+ "circle-opacity": NumberOrExpressionSchema.optional(),
3412
+ "circle-stroke-width": NumberOrExpressionSchema.optional(),
3413
+ "circle-stroke-color": ColorOrExpressionSchema.optional(),
3414
+ "circle-stroke-opacity": NumberOrExpressionSchema.optional(),
3415
+ "circle-pitch-scale": z.enum(["map", "viewport"]).optional(),
3416
+ "circle-pitch-alignment": z.enum(["map", "viewport"]).optional(),
3417
+ "circle-translate": z.tuple([z.number(), z.number()]).optional(),
3418
+ "circle-translate-anchor": z.enum(["map", "viewport"]).optional()
3419
+ }).passthrough().optional().describe("Circle paint properties"),
3420
+ layout: z.object({}).passthrough().optional().describe("Circle layout properties")
3421
+ }).passthrough();
3422
+ var LineLayerSchema = BaseLayerPropertiesSchema.extend({
3423
+ type: z.literal("line").describe("Layer type"),
3424
+ paint: z.object({
3425
+ "line-opacity": NumberOrExpressionSchema.optional(),
3426
+ "line-color": ColorOrExpressionSchema.optional(),
3427
+ "line-width": NumberOrExpressionSchema.optional(),
3428
+ "line-gap-width": NumberOrExpressionSchema.optional(),
3429
+ "line-offset": NumberOrExpressionSchema.optional(),
3430
+ "line-blur": NumberOrExpressionSchema.optional(),
3431
+ "line-dasharray": z.array(z.number()).optional(),
3432
+ "line-pattern": z.string().optional(),
3433
+ "line-gradient": ColorOrExpressionSchema.optional(),
3434
+ "line-translate": z.tuple([z.number(), z.number()]).optional(),
3435
+ "line-translate-anchor": z.enum(["map", "viewport"]).optional()
3436
+ }).passthrough().optional().describe("Line paint properties"),
3437
+ layout: z.object({
3438
+ "line-cap": z.enum(["butt", "round", "square"]).optional(),
3439
+ "line-join": z.enum(["bevel", "round", "miter"]).optional(),
3440
+ "line-miter-limit": z.number().optional(),
3441
+ "line-round-limit": z.number().optional(),
3442
+ "line-sort-key": NumberOrExpressionSchema.optional()
3443
+ }).passthrough().optional().describe("Line layout properties")
3444
+ }).passthrough();
3445
+ var FillLayerSchema = BaseLayerPropertiesSchema.extend({
3446
+ type: z.literal("fill").describe("Layer type"),
3447
+ paint: z.object({
3448
+ "fill-antialias": z.boolean().optional(),
3449
+ "fill-opacity": NumberOrExpressionSchema.optional(),
3450
+ "fill-color": ColorOrExpressionSchema.optional(),
3451
+ "fill-outline-color": ColorOrExpressionSchema.optional(),
3452
+ "fill-translate": z.tuple([z.number(), z.number()]).optional(),
3453
+ "fill-translate-anchor": z.enum(["map", "viewport"]).optional(),
3454
+ "fill-pattern": z.string().optional()
3455
+ }).passthrough().optional().describe("Fill paint properties"),
3456
+ layout: z.object({
3457
+ "fill-sort-key": NumberOrExpressionSchema.optional()
3458
+ }).passthrough().optional().describe("Fill layout properties")
3459
+ }).passthrough();
3460
+ var SymbolLayerSchema = BaseLayerPropertiesSchema.extend({
3461
+ type: z.literal("symbol").describe("Layer type"),
3462
+ layout: z.object({
3463
+ "symbol-placement": z.enum(["point", "line", "line-center"]).optional(),
3464
+ "symbol-spacing": z.number().optional(),
3465
+ "symbol-avoid-edges": z.boolean().optional(),
3466
+ "symbol-sort-key": NumberOrExpressionSchema.optional(),
3467
+ "symbol-z-order": z.enum(["auto", "viewport-y", "source"]).optional(),
3468
+ "icon-allow-overlap": z.boolean().optional(),
3469
+ "icon-ignore-placement": z.boolean().optional(),
3470
+ "icon-optional": z.boolean().optional(),
3471
+ "icon-rotation-alignment": z.enum(["map", "viewport", "auto"]).optional(),
3472
+ "icon-size": NumberOrExpressionSchema.optional(),
3473
+ "icon-text-fit": z.enum(["none", "width", "height", "both"]).optional(),
3474
+ "icon-text-fit-padding": z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
3475
+ "icon-image": z.union([z.string(), ExpressionSchema]).optional(),
3476
+ "icon-rotate": NumberOrExpressionSchema.optional(),
3477
+ "icon-padding": z.number().optional(),
3478
+ "icon-keep-upright": z.boolean().optional(),
3479
+ "icon-offset": z.tuple([z.number(), z.number()]).optional(),
3480
+ "icon-anchor": z.enum([
3481
+ "center",
3482
+ "left",
3483
+ "right",
3484
+ "top",
3485
+ "bottom",
3486
+ "top-left",
3487
+ "top-right",
3488
+ "bottom-left",
3489
+ "bottom-right"
3490
+ ]).optional(),
3491
+ "icon-pitch-alignment": z.enum(["map", "viewport", "auto"]).optional(),
3492
+ "text-pitch-alignment": z.enum(["map", "viewport", "auto"]).optional(),
3493
+ "text-rotation-alignment": z.enum(["map", "viewport", "auto"]).optional(),
3494
+ "text-field": z.union([z.string(), ExpressionSchema]).optional(),
3495
+ "text-font": z.array(z.string()).optional(),
3496
+ "text-size": NumberOrExpressionSchema.optional(),
3497
+ "text-max-width": NumberOrExpressionSchema.optional(),
3498
+ "text-line-height": z.number().optional(),
3499
+ "text-letter-spacing": z.number().optional(),
3500
+ "text-justify": z.enum(["auto", "left", "center", "right"]).optional(),
3501
+ "text-radial-offset": z.number().optional(),
3502
+ "text-variable-anchor": z.array(
3503
+ z.enum([
3504
+ "center",
3505
+ "left",
3506
+ "right",
3507
+ "top",
3508
+ "bottom",
3509
+ "top-left",
3510
+ "top-right",
3511
+ "bottom-left",
3512
+ "bottom-right"
3513
+ ])
3514
+ ).optional(),
3515
+ "text-anchor": z.enum([
3516
+ "center",
3517
+ "left",
3518
+ "right",
3519
+ "top",
3520
+ "bottom",
3521
+ "top-left",
3522
+ "top-right",
3523
+ "bottom-left",
3524
+ "bottom-right"
3525
+ ]).optional(),
3526
+ "text-max-angle": z.number().optional(),
3527
+ "text-rotate": NumberOrExpressionSchema.optional(),
3528
+ "text-padding": z.number().optional(),
3529
+ "text-keep-upright": z.boolean().optional(),
3530
+ "text-transform": z.enum(["none", "uppercase", "lowercase"]).optional(),
3531
+ "text-offset": z.tuple([z.number(), z.number()]).optional(),
3532
+ "text-allow-overlap": z.boolean().optional(),
3533
+ "text-ignore-placement": z.boolean().optional(),
3534
+ "text-optional": z.boolean().optional()
3535
+ }).passthrough().optional().describe("Symbol layout properties"),
3536
+ paint: z.object({
3537
+ "icon-opacity": NumberOrExpressionSchema.optional(),
3538
+ "icon-color": ColorOrExpressionSchema.optional(),
3539
+ "icon-halo-color": ColorOrExpressionSchema.optional(),
3540
+ "icon-halo-width": NumberOrExpressionSchema.optional(),
3541
+ "icon-halo-blur": NumberOrExpressionSchema.optional(),
3542
+ "icon-translate": z.tuple([z.number(), z.number()]).optional(),
3543
+ "icon-translate-anchor": z.enum(["map", "viewport"]).optional(),
3544
+ "text-opacity": NumberOrExpressionSchema.optional(),
3545
+ "text-color": ColorOrExpressionSchema.optional(),
3546
+ "text-halo-color": ColorOrExpressionSchema.optional(),
3547
+ "text-halo-width": NumberOrExpressionSchema.optional(),
3548
+ "text-halo-blur": NumberOrExpressionSchema.optional(),
3549
+ "text-translate": z.tuple([z.number(), z.number()]).optional(),
3550
+ "text-translate-anchor": z.enum(["map", "viewport"]).optional()
3551
+ }).passthrough().optional().describe("Symbol paint properties")
3552
+ }).passthrough();
3553
+ var RasterLayerSchema = BaseLayerPropertiesSchema.extend({
3554
+ type: z.literal("raster").describe("Layer type"),
3555
+ paint: z.object({
3556
+ "raster-opacity": NumberOrExpressionSchema.optional(),
3557
+ "raster-hue-rotate": NumberOrExpressionSchema.optional(),
3558
+ "raster-brightness-min": NumberOrExpressionSchema.optional(),
3559
+ "raster-brightness-max": NumberOrExpressionSchema.optional(),
3560
+ "raster-saturation": NumberOrExpressionSchema.optional(),
3561
+ "raster-contrast": NumberOrExpressionSchema.optional(),
3562
+ "raster-resampling": z.enum(["linear", "nearest"]).optional(),
3563
+ "raster-fade-duration": z.number().optional()
3564
+ }).passthrough().optional().describe("Raster paint properties"),
3565
+ layout: z.object({}).passthrough().optional().describe("Raster layout properties")
3566
+ }).passthrough();
3567
+ var FillExtrusionLayerSchema = BaseLayerPropertiesSchema.extend({
3568
+ type: z.literal("fill-extrusion").describe("Layer type"),
3569
+ paint: z.object({
3570
+ "fill-extrusion-opacity": NumberOrExpressionSchema.optional(),
3571
+ "fill-extrusion-color": ColorOrExpressionSchema.optional(),
3572
+ "fill-extrusion-translate": z.tuple([z.number(), z.number()]).optional(),
3573
+ "fill-extrusion-translate-anchor": z.enum(["map", "viewport"]).optional(),
3574
+ "fill-extrusion-pattern": z.string().optional(),
3575
+ "fill-extrusion-height": NumberOrExpressionSchema.optional(),
3576
+ "fill-extrusion-base": NumberOrExpressionSchema.optional(),
3577
+ "fill-extrusion-vertical-gradient": z.boolean().optional()
3578
+ }).passthrough().optional().describe("Fill extrusion paint properties"),
3579
+ layout: z.object({}).passthrough().optional().describe("Fill extrusion layout properties")
3580
+ }).passthrough();
3581
+ var HeatmapLayerSchema = BaseLayerPropertiesSchema.extend({
3582
+ type: z.literal("heatmap").describe("Layer type"),
3583
+ paint: z.object({
3584
+ "heatmap-radius": NumberOrExpressionSchema.optional(),
3585
+ "heatmap-weight": NumberOrExpressionSchema.optional(),
3586
+ "heatmap-intensity": NumberOrExpressionSchema.optional(),
3587
+ "heatmap-color": ColorOrExpressionSchema.optional(),
3588
+ "heatmap-opacity": NumberOrExpressionSchema.optional()
3589
+ }).passthrough().optional().describe("Heatmap paint properties"),
3590
+ layout: z.object({}).passthrough().optional().describe("Heatmap layout properties")
3591
+ }).passthrough();
3592
+ var HillshadeLayerSchema = BaseLayerPropertiesSchema.extend({
3593
+ type: z.literal("hillshade").describe("Layer type"),
3594
+ paint: z.object({
3595
+ "hillshade-illumination-direction": z.number().optional(),
3596
+ "hillshade-illumination-anchor": z.enum(["map", "viewport"]).optional(),
3597
+ "hillshade-exaggeration": NumberOrExpressionSchema.optional(),
3598
+ "hillshade-shadow-color": ColorOrExpressionSchema.optional(),
3599
+ "hillshade-highlight-color": ColorOrExpressionSchema.optional(),
3600
+ "hillshade-accent-color": ColorOrExpressionSchema.optional()
3601
+ }).passthrough().optional().describe("Hillshade paint properties"),
3602
+ layout: z.object({}).passthrough().optional().describe("Hillshade layout properties")
3603
+ }).passthrough();
3604
+ var BackgroundLayerSchema = z.object({
3605
+ id: z.string().describe("Unique layer identifier"),
3606
+ type: z.literal("background").describe("Layer type"),
3607
+ paint: z.object({
3608
+ "background-color": ColorOrExpressionSchema.optional(),
3609
+ "background-pattern": z.string().optional(),
3610
+ "background-opacity": NumberOrExpressionSchema.optional()
3611
+ }).passthrough().optional().describe("Background paint properties"),
3612
+ layout: z.object({}).passthrough().optional().describe("Background layout properties"),
3613
+ metadata: z.record(z.any()).optional().describe("Custom metadata")
3614
+ }).passthrough();
3615
+ var LayerSchema = z.union([
3616
+ CircleLayerSchema,
3617
+ LineLayerSchema,
3618
+ FillLayerSchema,
3619
+ SymbolLayerSchema,
3620
+ RasterLayerSchema,
3621
+ FillExtrusionLayerSchema,
3622
+ HeatmapLayerSchema,
3623
+ HillshadeLayerSchema,
3624
+ BackgroundLayerSchema
3625
+ ]);
3626
+ var LayerReferenceSchema = z.object({
3627
+ $ref: z.string().describe('Reference to global layer (e.g., "#/layers/bikeLayer")')
3628
+ }).describe("Layer reference");
3629
+ var LayerOrReferenceSchema = z.union([
3630
+ LayerSchema,
3631
+ LayerReferenceSchema
3632
+ ]);
3633
+
3634
+ // src/schemas/map.schema.ts
3635
+ var ControlPositionSchema = z.enum([
3636
+ "top-left",
3637
+ "top-right",
3638
+ "bottom-left",
3639
+ "bottom-right"
3640
+ ]);
3641
+ var ControlConfigSchema = z.union([
3642
+ z.boolean(),
3643
+ z.object({
3644
+ enabled: z.boolean().describe("Whether control is enabled"),
3645
+ position: ControlPositionSchema.optional().describe("Control position")
3646
+ })
3647
+ ]);
3648
+ var ControlsConfigSchema = z.object({
3649
+ navigation: ControlConfigSchema.optional().describe(
3650
+ "Navigation controls (zoom, rotation)"
3651
+ ),
3652
+ geolocate: ControlConfigSchema.optional().describe("Geolocation control"),
3653
+ scale: ControlConfigSchema.optional().describe("Scale control"),
3654
+ fullscreen: ControlConfigSchema.optional().describe("Fullscreen control"),
3655
+ attribution: ControlConfigSchema.optional().describe("Attribution control")
3656
+ }).describe("Map controls configuration");
3657
+ var LegendConfigSchema = z.object({
3658
+ position: ControlPositionSchema.default("top-left").describe("Legend position"),
3659
+ title: z.string().optional().describe("Legend title"),
3660
+ collapsed: z.boolean().default(false).describe("Start collapsed"),
3661
+ items: z.array(
3662
+ z.object({
3663
+ color: z.string().describe("Item color"),
3664
+ label: z.string().describe("Item label"),
3665
+ shape: z.enum(["circle", "square", "line", "icon"]).default("square").describe("Symbol shape"),
3666
+ icon: z.string().optional().describe("Icon name/URL (for shape: icon)")
3667
+ })
3668
+ ).optional().describe("Custom legend items (overrides layer legends)")
3669
+ }).describe("Legend configuration");
3670
+ var MapConfigSchema = z.object({
3671
+ // Required
3672
+ center: LngLatSchema.describe("Initial map center [longitude, latitude]"),
3673
+ zoom: ZoomLevelSchema.describe("Initial zoom level (0-24)"),
3674
+ mapStyle: z.union([z.string().url(), z.any()]).describe("MapLibre style URL or style object"),
3675
+ // View
3676
+ pitch: z.number().min(0).max(85).default(0).describe("Camera pitch angle in degrees (0-85)"),
3677
+ bearing: z.number().min(-180).max(180).default(0).describe("Camera bearing (rotation) in degrees (-180 to 180)"),
3678
+ bounds: z.union([LngLatBoundsSchema, z.array(z.number())]).optional().describe("Fit map to bounds"),
3679
+ // Constraints
3680
+ minZoom: ZoomLevelSchema.optional().describe("Minimum zoom level"),
3681
+ maxZoom: ZoomLevelSchema.optional().describe("Maximum zoom level"),
3682
+ minPitch: z.number().min(0).max(85).optional().describe("Minimum pitch"),
3683
+ maxPitch: z.number().min(0).max(85).optional().describe("Maximum pitch"),
3684
+ maxBounds: LngLatBoundsSchema.optional().describe(
3685
+ "Maximum geographic bounds"
3686
+ ),
3687
+ // Interaction
3688
+ interactive: z.boolean().default(true).describe("Enable map interaction"),
3689
+ scrollZoom: z.boolean().optional().describe("Enable scroll to zoom"),
3690
+ boxZoom: z.boolean().optional().describe("Enable box zoom (shift+drag)"),
3691
+ dragRotate: z.boolean().optional().describe("Enable drag to rotate"),
3692
+ dragPan: z.boolean().optional().describe("Enable drag to pan"),
3693
+ keyboard: z.boolean().optional().describe("Enable keyboard shortcuts"),
3694
+ doubleClickZoom: z.boolean().optional().describe("Enable double-click zoom"),
3695
+ touchZoomRotate: z.boolean().optional().describe("Enable touch zoom/rotate"),
3696
+ touchPitch: z.boolean().optional().describe("Enable touch pitch"),
3697
+ // Display
3698
+ hash: z.boolean().optional().describe("Sync map state with URL hash"),
3699
+ attributionControl: z.boolean().optional().describe("Show attribution control"),
3700
+ logoPosition: ControlPositionSchema.optional().describe(
3701
+ "MapLibre logo position"
3702
+ ),
3703
+ fadeDuration: z.number().optional().describe("Fade duration in milliseconds"),
3704
+ crossSourceCollisions: z.boolean().optional().describe("Check for cross-source collisions"),
3705
+ // Rendering
3706
+ antialias: z.boolean().optional().describe("Enable antialiasing"),
3707
+ refreshExpiredTiles: z.boolean().optional().describe("Refresh expired tiles"),
3708
+ renderWorldCopies: z.boolean().optional().describe("Render multiple world copies"),
3709
+ locale: z.record(z.string()).optional().describe("Localization strings"),
3710
+ // Performance
3711
+ maxTileCacheSize: z.number().optional().describe("Maximum tiles to cache"),
3712
+ localIdeographFontFamily: z.string().optional().describe("Font for CJK characters"),
3713
+ trackResize: z.boolean().optional().describe("Track container resize"),
3714
+ preserveDrawingBuffer: z.boolean().optional().describe("Preserve drawing buffer"),
3715
+ failIfMajorPerformanceCaveat: z.boolean().optional().describe("Fail if major performance caveat")
3716
+ }).passthrough().describe("Map configuration with MapLibre options");
3717
+ var MapBlockSchema = z.object({
3718
+ type: z.literal("map").describe("Block type"),
3719
+ id: z.string().describe("Unique block identifier"),
3720
+ className: z.string().optional().describe("CSS class name for container"),
3721
+ style: z.string().optional().describe("Inline CSS styles for container"),
3722
+ config: MapConfigSchema.describe("Map configuration"),
3723
+ layers: z.array(LayerOrReferenceSchema).default([]).describe("Map layers"),
3724
+ controls: ControlsConfigSchema.optional().describe("Map controls"),
3725
+ legend: LegendConfigSchema.optional().describe("Legend configuration")
3726
+ }).describe("Standard map block");
3727
+ var MapFullPageBlockSchema = z.object({
3728
+ type: z.literal("map-fullpage").describe("Block type"),
3729
+ id: z.string().describe("Unique block identifier"),
3730
+ className: z.string().optional().describe("CSS class name for container"),
3731
+ style: z.string().optional().describe("Inline CSS styles for container"),
3732
+ config: MapConfigSchema.describe("Map configuration"),
3733
+ layers: z.array(LayerOrReferenceSchema).default([]).describe("Map layers"),
3734
+ controls: ControlsConfigSchema.optional().describe("Map controls"),
3735
+ legend: LegendConfigSchema.optional().describe("Legend configuration")
3736
+ }).describe("Full-page map block");
3737
+ var ChapterActionSchema = z.object({
3738
+ action: z.enum([
3739
+ "setFilter",
3740
+ "setPaintProperty",
3741
+ "setLayoutProperty",
3742
+ "flyTo",
3743
+ "easeTo",
3744
+ "fitBounds",
3745
+ "custom"
3746
+ ]).describe("Action type"),
3747
+ layer: z.string().optional().describe("Target layer ID"),
3748
+ property: z.string().optional().describe("Property name (for setPaintProperty/setLayoutProperty)"),
3749
+ value: z.any().optional().describe("Property value"),
3750
+ filter: ExpressionSchema.nullable().optional().describe("Filter expression (for setFilter, null to clear)"),
3751
+ bounds: z.array(z.number()).optional().describe("Bounds array (for fitBounds)"),
3752
+ options: z.record(z.any()).optional().describe("Additional options")
3753
+ }).describe("Chapter action for map state changes");
3754
+ var ChapterLayersSchema = z.object({
3755
+ show: z.array(z.string()).default([]).describe("Layer IDs to show"),
3756
+ hide: z.array(z.string()).default([]).describe("Layer IDs to hide")
3757
+ }).describe("Chapter layer visibility configuration");
3758
+ var ChapterSchema = z.object({
3759
+ // Required
3760
+ id: z.string().describe("Unique chapter identifier"),
3761
+ title: z.string().describe("Chapter title"),
3762
+ center: LngLatSchema.describe("Map center [longitude, latitude]"),
3763
+ zoom: z.number().describe("Zoom level"),
3764
+ // Content
3765
+ description: z.string().optional().describe("Chapter description (HTML/markdown supported)"),
3766
+ image: z.string().url().optional().describe("Hero image URL"),
3767
+ video: z.string().url().optional().describe("Video URL"),
3768
+ // Camera
3769
+ pitch: z.number().min(0).max(85).default(0).describe("Camera pitch angle (0-85)"),
3770
+ bearing: z.number().min(-180).max(180).default(0).describe("Camera bearing (-180 to 180)"),
3771
+ speed: z.number().min(0).max(2).default(0.6).describe("Animation speed multiplier (0-2)"),
3772
+ curve: z.number().min(0).max(2).default(1).describe("Animation curve (0=linear, 1=default, 2=steep)"),
3773
+ animation: z.enum(["flyTo", "easeTo", "jumpTo"]).default("flyTo").describe("Animation type"),
3774
+ // Rotation animation
3775
+ rotateAnimation: z.boolean().optional().describe("Enable continuous rotation animation"),
3776
+ spinGlobe: z.boolean().optional().describe("Spin globe animation (for low zoom levels)"),
3777
+ // Layout
3778
+ alignment: z.enum(["left", "right", "center", "full"]).default("center").describe("Content alignment"),
3779
+ hidden: z.boolean().default(false).describe("Hide chapter content (map-only)"),
3780
+ // Layers
3781
+ layers: ChapterLayersSchema.optional().describe("Layer visibility control"),
3782
+ // Actions
3783
+ onChapterEnter: z.array(ChapterActionSchema).default([]).describe("Actions when entering chapter"),
3784
+ onChapterExit: z.array(ChapterActionSchema).default([]).describe("Actions when exiting chapter"),
3785
+ // Custom
3786
+ callback: z.string().optional().describe("Custom callback function name")
3787
+ }).describe("Scrollytelling chapter");
3788
+ var ScrollytellingBlockSchema = z.object({
3789
+ type: z.literal("scrollytelling").describe("Block type"),
3790
+ id: z.string().describe("Unique block identifier"),
3791
+ className: z.string().optional().describe("CSS class name for container"),
3792
+ style: z.string().optional().describe("Inline CSS styles for container"),
3793
+ // Base config
3794
+ config: MapConfigSchema.describe("Base map configuration"),
3795
+ // Theme
3796
+ theme: z.enum(["light", "dark"]).default("light").describe("Visual theme"),
3797
+ // Markers
3798
+ showMarkers: z.boolean().default(false).describe("Show chapter markers on map"),
3799
+ markerColor: z.string().default("#3FB1CE").describe("Chapter marker color"),
3800
+ // Layers (persistent across all chapters)
3801
+ layers: z.array(LayerOrReferenceSchema).default([]).describe("Persistent layers (visible throughout story)"),
3802
+ // Chapters
3803
+ chapters: z.array(ChapterSchema).min(1, "At least one chapter is required for scrollytelling").describe("Story chapters"),
3804
+ // Footer
3805
+ footer: z.string().optional().describe("Footer content (HTML)")
3806
+ }).describe("Scrollytelling block for narrative map stories");
3807
+
3808
+ // src/schemas/page.schema.ts
3809
+ var MixedBlockSchema = z.lazy(
3810
+ () => z.object({
3811
+ type: z.literal("mixed").describe("Block type"),
3812
+ id: z.string().optional().describe("Unique block identifier"),
3813
+ className: z.string().optional().describe("CSS class name for container"),
3814
+ style: z.string().optional().describe("Inline CSS styles for container"),
3815
+ layout: z.enum(["row", "column", "grid"]).default("row").describe("Layout direction"),
3816
+ gap: z.string().optional().describe("Gap between blocks (CSS gap property)"),
3817
+ blocks: z.array(BlockSchema).describe("Child blocks")
3818
+ }).describe("Mixed block for combining multiple block types")
3819
+ );
3820
+ var BlockSchema = z.union([
3821
+ ContentBlockSchema,
3822
+ MapBlockSchema,
3823
+ MapFullPageBlockSchema,
3824
+ ScrollytellingBlockSchema,
3825
+ MixedBlockSchema
3826
+ ]);
3827
+ var PageSchema = z.object({
3828
+ path: z.string().describe('URL path (e.g., "/", "/about")'),
3829
+ title: z.string().describe("Page title"),
3830
+ description: z.string().optional().describe("Page description for SEO"),
3831
+ blocks: z.array(BlockSchema).describe("Page content blocks")
3832
+ }).describe("Page configuration");
3833
+ var GlobalConfigSchema = z.object({
3834
+ title: z.string().optional().describe("Application title"),
3835
+ description: z.string().optional().describe("Application description"),
3836
+ defaultMapStyle: z.string().url().optional().describe("Default map style URL"),
3837
+ theme: z.enum(["light", "dark"]).default("light").describe("Default theme"),
3838
+ dataFetching: z.object({
3839
+ defaultStrategy: z.enum(["runtime", "build", "hybrid"]).default("runtime").describe("Default fetch strategy"),
3840
+ timeout: z.number().min(1e3).default(3e4).describe("Default timeout in milliseconds"),
3841
+ retryAttempts: z.number().int().min(0).default(3).describe("Default retry attempts")
3842
+ }).optional().describe("Data fetching configuration")
3843
+ }).describe("Global configuration");
3844
+ var RootSchema = z.object({
3845
+ config: GlobalConfigSchema.optional().describe("Global configuration"),
3846
+ layers: z.record(LayerSchema).optional().describe("Named layer definitions for reuse"),
3847
+ sources: z.record(LayerSourceSchema).optional().describe("Named source definitions for reuse"),
3848
+ pages: z.array(PageSchema).min(1, "At least one page is required").describe("Page definitions")
3849
+ }).describe("Root configuration schema");
3850
+
3851
+ // src/parser/yaml-parser.ts
3852
+ var YAMLParser = class {
3853
+ /**
3854
+ * Parse YAML string and validate against schema
3855
+ *
3856
+ * @param yaml - YAML string to parse
3857
+ * @returns Validated configuration object
3858
+ * @throws {Error} If YAML syntax is invalid
3859
+ * @throws {ZodError} If validation fails
3860
+ *
3861
+ * @remarks
3862
+ * This method parses the YAML, validates it against the RootSchema,
3863
+ * resolves all references, and returns the validated config. If any
3864
+ * step fails, it throws an error.
3865
+ *
3866
+ * @example
3867
+ * ```typescript
3868
+ * try {
3869
+ * const config = YAMLParser.parse(yamlString);
3870
+ * console.log('Valid config:', config);
3871
+ * } catch (error) {
3872
+ * console.error('Parse error:', error.message);
3873
+ * }
3874
+ * ```
3875
+ */
3876
+ static parse(yaml) {
3877
+ let parsed;
3878
+ try {
3879
+ parsed = parse(yaml);
3880
+ } catch (error) {
3881
+ throw new Error(
3882
+ `YAML syntax error: ${error instanceof Error ? error.message : String(error)}`
3883
+ );
3884
+ }
3885
+ const validated = RootSchema.parse(parsed);
3886
+ return this.resolveReferences(validated);
3887
+ }
3888
+ /**
3889
+ * Parse YAML string and validate, returning a result object
3890
+ *
3891
+ * @param yaml - YAML string to parse
3892
+ * @returns Result object with success flag and either data or errors
3893
+ *
3894
+ * @remarks
3895
+ * This is the non-throwing version of {@link parse}. Instead of throwing
3896
+ * errors, it returns a result object that indicates success or failure.
3897
+ * Use this when you want to handle errors gracefully without try/catch.
3898
+ *
3899
+ * @example
3900
+ * ```typescript
3901
+ * const result = YAMLParser.safeParse(yamlString);
3902
+ * if (result.success) {
3903
+ * console.log('Config:', result.data);
3904
+ * } else {
3905
+ * result.errors.forEach(err => {
3906
+ * console.error(`Error at ${err.path}: ${err.message}`);
3907
+ * });
3908
+ * }
3909
+ * ```
3910
+ */
3911
+ static safeParse(yaml) {
3912
+ try {
3913
+ const data = this.parse(yaml);
3914
+ return {
3915
+ success: true,
3916
+ data,
3917
+ errors: []
3918
+ };
3919
+ } catch (error) {
3920
+ if (error instanceof ZodError) {
3921
+ return {
3922
+ success: false,
3923
+ errors: this.formatZodErrors(error)
3924
+ };
3925
+ }
3926
+ return {
3927
+ success: false,
3928
+ errors: [
3929
+ {
3930
+ path: "",
3931
+ message: error instanceof Error ? error.message : String(error)
3932
+ }
3933
+ ]
3934
+ };
3935
+ }
3936
+ }
3937
+ /**
3938
+ * Validate a JavaScript object against the schema
3939
+ *
3940
+ * @param config - JavaScript object to validate
3941
+ * @returns Validated configuration object
3942
+ * @throws {ZodError} If validation fails
3943
+ *
3944
+ * @remarks
3945
+ * This method bypasses YAML parsing and directly validates a JavaScript object.
3946
+ * Useful when you already have a parsed object (e.g., from JSON.parse) and just
3947
+ * want to validate and resolve references.
3948
+ *
3949
+ * @example
3950
+ * ```typescript
3951
+ * const jsConfig = JSON.parse(jsonString);
3952
+ * const validated = YAMLParser.validate(jsConfig);
3953
+ * ```
3954
+ */
3955
+ static validate(config) {
3956
+ const validated = RootSchema.parse(config);
3957
+ return this.resolveReferences(validated);
3958
+ }
3959
+ /**
3960
+ * Resolve $ref references to global layers and sources
3961
+ *
3962
+ * @param config - Configuration object with potential references
3963
+ * @returns Configuration with all references resolved
3964
+ * @throws {Error} If a reference cannot be resolved
3965
+ *
3966
+ * @remarks
3967
+ * References use JSON Pointer-like syntax: `#/layers/layerName` or `#/sources/sourceName`.
3968
+ * This method walks the configuration tree, finds all objects with a `$ref` property,
3969
+ * looks up the referenced item in `config.layers` or `config.sources`, and replaces
3970
+ * the reference object with the actual item.
3971
+ *
3972
+ * ## Reference Syntax
3973
+ *
3974
+ * - `#/layers/myLayer` - Reference to a layer in the global `layers` section
3975
+ * - `#/sources/mySource` - Reference to a source in the global `sources` section
3976
+ *
3977
+ * @example
3978
+ * ```typescript
3979
+ * const config = {
3980
+ * layers: {
3981
+ * myLayer: { id: 'layer1', type: 'circle', ... }
3982
+ * },
3983
+ * pages: [{
3984
+ * blocks: [{
3985
+ * type: 'map',
3986
+ * layers: [{ $ref: '#/layers/myLayer' }]
3987
+ * }]
3988
+ * }]
3989
+ * };
3990
+ *
3991
+ * const resolved = YAMLParser.resolveReferences(config);
3992
+ * // resolved.pages[0].blocks[0].layers[0] now contains the full layer object
3993
+ * ```
3994
+ */
3995
+ static resolveReferences(config) {
3996
+ const resolveInObject = (obj) => {
3997
+ if (obj == null) return obj;
3998
+ if (Array.isArray(obj)) {
3999
+ return obj.map((item) => resolveInObject(item));
4000
+ }
4001
+ if (typeof obj === "object") {
4002
+ if ("$ref" in obj && typeof obj.$ref === "string") {
4003
+ const ref = obj.$ref;
4004
+ const match = ref.match(/^#\/(layers|sources)\/(.+)$/);
4005
+ if (!match) {
4006
+ throw new Error(
4007
+ `Invalid reference format: ${ref}. Expected #/layers/name or #/sources/name`
4008
+ );
4009
+ }
4010
+ const [, section, name] = match;
4011
+ if (section === "layers") {
4012
+ if (!config.layers || !(name in config.layers)) {
4013
+ throw new Error(`Layer reference not found: ${ref}`);
4014
+ }
4015
+ return config.layers[name];
4016
+ } else if (section === "sources") {
4017
+ if (!config.sources || !(name in config.sources)) {
4018
+ throw new Error(`Source reference not found: ${ref}`);
4019
+ }
4020
+ return config.sources[name];
4021
+ }
4022
+ }
4023
+ const resolved = {};
4024
+ for (const [key, value] of Object.entries(obj)) {
4025
+ resolved[key] = resolveInObject(value);
4026
+ }
4027
+ return resolved;
4028
+ }
4029
+ return obj;
4030
+ };
4031
+ return resolveInObject(config);
4032
+ }
4033
+ /**
4034
+ * Format Zod validation errors into user-friendly messages
4035
+ *
4036
+ * @param error - Zod validation error
4037
+ * @returns Array of formatted error objects
4038
+ *
4039
+ * @remarks
4040
+ * This method transforms Zod's internal error format into human-readable
4041
+ * messages with clear paths and descriptions. It handles various Zod error
4042
+ * types and provides appropriate messages for each.
4043
+ *
4044
+ * ## Error Type Handling
4045
+ *
4046
+ * - `invalid_type`: Type mismatch (e.g., expected number, got string)
4047
+ * - `invalid_union_discriminator`: Invalid discriminator for union types
4048
+ * - `invalid_union`: None of the union options matched
4049
+ * - `too_small`: Value below minimum (arrays, strings, numbers)
4050
+ * - `too_big`: Value above maximum
4051
+ * - `invalid_string`: String format validation failed
4052
+ * - `custom`: Custom validation refinement failed
4053
+ *
4054
+ * @example
4055
+ * ```typescript
4056
+ * try {
4057
+ * RootSchema.parse(invalidConfig);
4058
+ * } catch (error) {
4059
+ * if (error instanceof ZodError) {
4060
+ * const formatted = YAMLParser.formatZodErrors(error);
4061
+ * formatted.forEach(err => {
4062
+ * console.error(`${err.path}: ${err.message}`);
4063
+ * });
4064
+ * }
4065
+ * }
4066
+ * ```
4067
+ */
4068
+ static formatZodErrors(error) {
4069
+ return error.errors.map((err) => {
4070
+ const path = err.path.join(".");
4071
+ let message;
4072
+ switch (err.code) {
4073
+ case "invalid_type":
4074
+ message = `Expected ${err.expected}, got ${err.received}`;
4075
+ break;
4076
+ case "invalid_union_discriminator":
4077
+ message = `Invalid type. Expected one of: ${err.options.join(", ")}`;
4078
+ break;
4079
+ case "invalid_union":
4080
+ message = "Value does not match any of the expected formats";
4081
+ break;
4082
+ case "too_small":
4083
+ if (err.type === "array") {
4084
+ message = `Array must have at least ${err.minimum} element(s)`;
4085
+ } else if (err.type === "string") {
4086
+ message = `String must have at least ${err.minimum} character(s)`;
4087
+ } else {
4088
+ message = `Value must be >= ${err.minimum}`;
4089
+ }
4090
+ break;
4091
+ case "too_big":
4092
+ if (err.type === "array") {
4093
+ message = `Array must have at most ${err.maximum} element(s)`;
4094
+ } else if (err.type === "string") {
4095
+ message = `String must have at most ${err.maximum} character(s)`;
4096
+ } else {
4097
+ message = `Value must be <= ${err.maximum}`;
4098
+ }
4099
+ break;
4100
+ case "invalid_string":
4101
+ if (err.validation === "url") {
4102
+ message = "Invalid URL format";
4103
+ } else if (err.validation === "email") {
4104
+ message = "Invalid email format";
4105
+ } else {
4106
+ message = `Invalid string format: ${err.validation}`;
4107
+ }
4108
+ break;
4109
+ case "custom":
4110
+ message = err.message || "Validation failed";
4111
+ break;
4112
+ default:
4113
+ message = err.message || "Validation error";
4114
+ }
4115
+ return {
4116
+ path,
4117
+ message
4118
+ };
4119
+ });
4120
+ }
4121
+ };
4122
+ YAMLParser.parse.bind(YAMLParser);
4123
+ var safeParseYAMLConfig = YAMLParser.safeParse.bind(YAMLParser);
4124
+
4125
+ // src/components/ml-map.ts
4126
+ var MLMap = class extends HTMLElement {
4127
+ renderer = null;
4128
+ container = null;
4129
+ /**
4130
+ * Observed attributes that trigger attributeChangedCallback
4131
+ */
4132
+ static get observedAttributes() {
4133
+ return ["config"];
4134
+ }
4135
+ /**
4136
+ * Called when element is added to the DOM
4137
+ */
4138
+ connectedCallback() {
4139
+ this.container = document.createElement("div");
4140
+ this.container.style.width = "100%";
4141
+ this.container.style.height = "100%";
4142
+ this.appendChild(this.container);
4143
+ const config = this.getConfig();
4144
+ if (config) {
4145
+ this.render(config);
4146
+ } else {
4147
+ this.dispatchEvent(
4148
+ new CustomEvent("error", {
4149
+ detail: { error: new Error("No valid map configuration found") }
4150
+ })
4151
+ );
4152
+ }
4153
+ }
4154
+ /**
4155
+ * Called when element is removed from the DOM
4156
+ */
4157
+ disconnectedCallback() {
4158
+ if (this.renderer) {
4159
+ this.renderer.destroy();
4160
+ this.renderer = null;
4161
+ }
4162
+ if (this.container) {
4163
+ this.container.remove();
4164
+ this.container = null;
4165
+ }
4166
+ }
4167
+ /**
4168
+ * Called when an observed attribute changes
4169
+ */
4170
+ attributeChangedCallback(name, oldValue, newValue) {
4171
+ if (name === "config" && oldValue !== newValue && this.container) {
4172
+ if (this.renderer) {
4173
+ this.renderer.destroy();
4174
+ this.renderer = null;
4175
+ }
4176
+ const config = this.getConfig();
4177
+ if (config) {
4178
+ this.render(config);
4179
+ }
4180
+ }
4181
+ }
4182
+ /**
4183
+ * Get configuration from one of three sources:
4184
+ * 1. 'config' attribute (JSON string)
4185
+ * 2. Inline <script type="application/yaml">
4186
+ * 3. Inline <script type="application/json">
4187
+ */
4188
+ getConfig() {
4189
+ const configAttr = this.getAttribute("config");
4190
+ if (configAttr) {
4191
+ try {
4192
+ const parsed = JSON.parse(configAttr);
4193
+ const result = MapBlockSchema.safeParse(parsed);
4194
+ if (result.success) {
4195
+ return result.data;
4196
+ } else {
4197
+ console.error("Invalid map config in attribute:", result.error);
4198
+ }
4199
+ } catch (error) {
4200
+ console.error("Failed to parse config attribute as JSON:", error);
4201
+ }
4202
+ }
4203
+ const yamlScript = this.querySelector(
4204
+ 'script[type="application/yaml"]'
4205
+ );
4206
+ if (yamlScript?.textContent) {
4207
+ try {
4208
+ const parsed = safeParseYAMLConfig(yamlScript.textContent);
4209
+ if (parsed.success) {
4210
+ for (const page of parsed.data.pages || []) {
4211
+ const mapBlock = page.blocks?.find(
4212
+ (block) => block.type === "map"
4213
+ );
4214
+ if (mapBlock) {
4215
+ return mapBlock;
4216
+ }
4217
+ }
4218
+ } else {
4219
+ console.error("Invalid YAML config:", parsed.errors);
4220
+ }
4221
+ } catch (error) {
4222
+ console.error("Failed to parse YAML config:", error);
4223
+ }
4224
+ }
4225
+ const jsonScript = this.querySelector(
4226
+ 'script[type="application/json"]'
4227
+ );
4228
+ if (jsonScript?.textContent) {
4229
+ try {
4230
+ const parsed = JSON.parse(jsonScript.textContent);
4231
+ const result = MapBlockSchema.safeParse(parsed);
4232
+ if (result.success) {
4233
+ return result.data;
4234
+ } else {
4235
+ console.error("Invalid JSON config:", result.error);
4236
+ }
4237
+ } catch (error) {
4238
+ console.error("Failed to parse JSON config:", error);
4239
+ }
4240
+ }
4241
+ return null;
4242
+ }
4243
+ /**
4244
+ * Render the map with the given configuration
4245
+ */
4246
+ render(config) {
4247
+ if (!this.container) return;
4248
+ try {
4249
+ this.renderer = new MapRenderer(
4250
+ this.container,
4251
+ config.config,
4252
+ config.layers || [],
4253
+ {
4254
+ onLoad: () => {
4255
+ if (config.controls) {
4256
+ this.renderer?.addControls(config.controls);
4257
+ }
4258
+ this.dispatchEvent(
4259
+ new CustomEvent("load", {
4260
+ detail: { map: this.renderer?.getMap() }
4261
+ })
4262
+ );
4263
+ },
4264
+ onError: (error) => {
4265
+ this.dispatchEvent(
4266
+ new CustomEvent("error", {
4267
+ detail: { error }
4268
+ })
4269
+ );
4270
+ }
4271
+ }
4272
+ );
4273
+ } catch (error) {
4274
+ this.dispatchEvent(
4275
+ new CustomEvent("error", {
4276
+ detail: { error }
4277
+ })
4278
+ );
4279
+ }
4280
+ }
4281
+ /**
4282
+ * Get the underlying MapRenderer instance
4283
+ */
4284
+ getRenderer() {
4285
+ return this.renderer;
4286
+ }
4287
+ /**
4288
+ * Get the underlying MapLibre Map instance
4289
+ */
4290
+ getMap() {
4291
+ return this.renderer?.getMap() ?? null;
4292
+ }
4293
+ };
4294
+ if (typeof window !== "undefined" && !customElements.get("ml-map")) {
4295
+ customElements.define("ml-map", MLMap);
4296
+ }
4297
+
4298
+ // src/components/styles.ts
4299
+ var defaultStyles = `
4300
+ ml-map {
4301
+ display: block;
4302
+ position: relative;
4303
+ width: 100%;
4304
+ height: 400px;
4305
+ }
4306
+
4307
+ ml-map > div {
4308
+ width: 100%;
4309
+ height: 100%;
4310
+ }
4311
+
4312
+ ml-map script[type="application/yaml"],
4313
+ ml-map script[type="application/json"] {
4314
+ display: none;
4315
+ }
4316
+ `;
4317
+ function injectStyles() {
4318
+ if (typeof document === "undefined") return;
4319
+ const existingStyle = document.getElementById("ml-map-styles");
4320
+ if (existingStyle) return;
4321
+ const style = document.createElement("style");
4322
+ style.id = "ml-map-styles";
4323
+ style.textContent = defaultStyles;
4324
+ document.head.appendChild(style);
4325
+ }
4326
+ if (typeof window !== "undefined") {
4327
+ injectStyles();
4328
+ }
4329
+
4330
+ export { MLMap, defaultStyles, injectStyles };
4331
+ //# sourceMappingURL=index.js.map
4332
+ //# sourceMappingURL=index.js.map