@maplibre-yaml/core 0.1.0-alpha.0 → 0.1.1

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