@maplibre-yaml/core 0.1.0-alpha.0

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