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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1214 @@
1
+ # @maplibre-yaml/core
2
+
3
+ > Declarative web maps with YAML configuration. Build interactive MapLibre maps using simple, readable YAML syntax.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@maplibre-yaml/core.svg)](https://www.npmjs.com/package/@maplibre-yaml/core)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ ### =� **Declarative Map Configuration**
11
+ Define your entire maplayers, sources, controls, and interactivityin clean, readable YAML syntax.
12
+
13
+ ### =� **Comprehensive Data Management**
14
+ - **HTTP Fetching** with automatic retry and caching
15
+ - **Real-time Updates** via Server-Sent Events (SSE) and WebSocket
16
+ - **Polling** with configurable intervals and merge strategies
17
+ - **Smart Merging** - Replace, merge by key, or window-based appending
18
+
19
+ ### <� **Rich Visualization**
20
+ - Support for all MapLibre layer types (circle, line, fill, symbol, heatmap, etc.)
21
+ - Dynamic styling with expressions
22
+ - Multiple data sources (GeoJSON, Vector Tiles, Raster, etc.)
23
+
24
+ ### = **Dynamic Interactions**
25
+ - Click handlers and popups
26
+ - Layer visibility toggling
27
+ - Data-driven legends
28
+ - Map controls (navigation, scale, geolocation, fullscreen)
29
+
30
+ ### � **Performance Optimized**
31
+ - LRU caching with TTL
32
+ - Request deduplication
33
+ - Non-overlapping polling execution
34
+ - Automatic reconnection for streaming
35
+
36
+ ### =� **Framework Integration**
37
+ - Vanilla JavaScript/TypeScript
38
+ - Web Components (`<ml-map>`)
39
+ - Astro components (via `@maplibre-yaml/astro`)
40
+ - Framework agnostic core
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install @maplibre-yaml/core maplibre-gl
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ### Using Web Components
51
+
52
+ ```html
53
+ <!DOCTYPE html>
54
+ <html>
55
+ <head>
56
+ <link rel="stylesheet" href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css">
57
+ </head>
58
+ <body>
59
+ <ml-map config="./map.yaml"></ml-map>
60
+
61
+ <script type="module">
62
+ import '@maplibre-yaml/core/components';
63
+ </script>
64
+ </body>
65
+ </html>
66
+ ```
67
+
68
+ ### Using JavaScript API
69
+
70
+ ```typescript
71
+ import { YAMLParser, MapRenderer } from '@maplibre-yaml/core';
72
+
73
+ const yaml = `
74
+ type: map
75
+ config:
76
+ center: [-122.4, 37.8]
77
+ zoom: 12
78
+ style: https://demotiles.maplibre.org/style.json
79
+ layers:
80
+ - id: points
81
+ type: circle
82
+ source:
83
+ type: geojson
84
+ url: https://example.com/data.geojson
85
+ paint:
86
+ circle-radius: 6
87
+ circle-color: "#3b82f6"
88
+ `;
89
+
90
+ const parser = new YAMLParser();
91
+ const config = await parser.parse(yaml);
92
+
93
+ const container = document.getElementById('map');
94
+ const renderer = new MapRenderer(container, config);
95
+ ```
96
+
97
+ ## YAML Configuration
98
+
99
+ ### Basic Map
100
+
101
+ ```yaml
102
+ type: map
103
+ id: my-map
104
+ config:
105
+ center: [-74.006, 40.7128]
106
+ zoom: 10
107
+ style: https://demotiles.maplibre.org/style.json
108
+
109
+ layers:
110
+ - id: cities
111
+ type: circle
112
+ source:
113
+ type: geojson
114
+ data:
115
+ type: FeatureCollection
116
+ features:
117
+ - type: Feature
118
+ geometry:
119
+ type: Point
120
+ coordinates: [-74.006, 40.7128]
121
+ properties:
122
+ name: New York
123
+ paint:
124
+ circle-radius: 8
125
+ circle-color: "#ef4444"
126
+ ```
127
+
128
+ ### Real-time Data with Polling
129
+
130
+ ```yaml
131
+ layers:
132
+ - id: vehicles
133
+ type: circle
134
+ source:
135
+ type: geojson
136
+ url: https://api.transit.example.com/vehicles.geojson
137
+ refresh:
138
+ refreshInterval: 5000 # Poll every 5 seconds
139
+ updateStrategy: merge # Merge by key
140
+ updateKey: vehicleId # Unique identifier
141
+ cache:
142
+ enabled: false # Disable cache for real-time data
143
+ loading:
144
+ enabled: true
145
+ message: Loading vehicles...
146
+ paint:
147
+ circle-radius: 6
148
+ circle-color:
149
+ - match
150
+ - ["get", "status"]
151
+ - "active"
152
+ - "#22c55e"
153
+ - "delayed"
154
+ - "#f59e0b"
155
+ - "#6b7280"
156
+ ```
157
+
158
+ ### Real-time Streaming with WebSocket
159
+
160
+ ```yaml
161
+ layers:
162
+ - id: live-events
163
+ type: circle
164
+ source:
165
+ type: geojson
166
+ url: https://api.example.com/initial-events.geojson
167
+ stream:
168
+ type: websocket
169
+ url: wss://api.example.com/events
170
+ reconnect: true
171
+ reconnectMaxAttempts: 10
172
+ refresh:
173
+ updateStrategy: append-window
174
+ windowSize: 100 # Keep last 100 events
175
+ windowDuration: 300000 # Keep events from last 5 minutes
176
+ timestampField: timestamp
177
+ paint:
178
+ circle-radius: 8
179
+ circle-color: "#8b5cf6"
180
+ ```
181
+
182
+ ### Server-Sent Events (SSE)
183
+
184
+ ```yaml
185
+ layers:
186
+ - id: sensors
187
+ type: heatmap
188
+ source:
189
+ type: geojson
190
+ url: https://api.example.com/sensors.geojson
191
+ stream:
192
+ type: sse
193
+ url: https://api.example.com/sensor-updates
194
+ eventTypes:
195
+ - temperature
196
+ - humidity
197
+ reconnect: true
198
+ refresh:
199
+ updateStrategy: merge
200
+ updateKey: sensorId
201
+ paint:
202
+ heatmap-weight:
203
+ - interpolate
204
+ - ["linear"]
205
+ - ["get", "temperature"]
206
+ - 0
207
+ - 0
208
+ - 100
209
+ - 1
210
+ ```
211
+
212
+ ### Interactive Features
213
+
214
+ ```yaml
215
+ layers:
216
+ - id: locations
217
+ type: symbol
218
+ source:
219
+ type: geojson
220
+ url: https://example.com/locations.geojson
221
+ layout:
222
+ icon-image: marker
223
+ icon-size: 1.5
224
+ interactions:
225
+ - type: click
226
+ popup:
227
+ content: |
228
+ <h3>{{name}}</h3>
229
+ <p>{{description}}</p>
230
+ <p><strong>Type:</strong> {{category}}</p>
231
+ ```
232
+
233
+ ### Data Merge Strategies
234
+
235
+ #### Replace Strategy
236
+ Complete replacement of all data on each update:
237
+ ```yaml
238
+ refresh:
239
+ updateStrategy: replace
240
+ ```
241
+
242
+ #### Merge Strategy
243
+ Update or add features by unique key:
244
+ ```yaml
245
+ refresh:
246
+ updateStrategy: merge
247
+ updateKey: id # Feature property to use as unique identifier
248
+ ```
249
+
250
+ #### Append-Window Strategy
251
+ Append new data with size and/or time-based windowing:
252
+ ```yaml
253
+ refresh:
254
+ updateStrategy: append-window
255
+ windowSize: 100 # Keep last 100 features
256
+ windowDuration: 300000 # Keep features from last 5 minutes
257
+ timestampField: timestamp # Feature property containing timestamp
258
+ ```
259
+
260
+ ## API Reference
261
+
262
+ ### Core Classes
263
+
264
+ #### `YAMLParser`
265
+ Parse and validate YAML map configurations.
266
+
267
+ ```typescript
268
+ import { YAMLParser } from '@maplibre-yaml/core';
269
+
270
+ const parser = new YAMLParser();
271
+ const config = await parser.parse(yamlString);
272
+ const errors = parser.validate(config);
273
+ ```
274
+
275
+ #### `MapRenderer`
276
+ Render maps from parsed configurations.
277
+
278
+ ```typescript
279
+ import { MapRenderer } from '@maplibre-yaml/core';
280
+
281
+ const renderer = new MapRenderer(container, config, {
282
+ onLoad: () => console.log('Map loaded'),
283
+ onError: (error) => console.error('Map error:', error),
284
+ });
285
+
286
+ // Update visibility
287
+ renderer.setLayerVisibility('layer-id', false);
288
+
289
+ // Update data
290
+ renderer.updateLayerData('layer-id', newGeoJSON);
291
+
292
+ // Clean up
293
+ renderer.destroy();
294
+ ```
295
+
296
+ ### Data Management
297
+
298
+ #### `DataFetcher`
299
+ HTTP data fetching with caching and retry.
300
+
301
+ ```typescript
302
+ import { DataFetcher } from '@maplibre-yaml/core';
303
+
304
+ const fetcher = new DataFetcher({
305
+ cache: {
306
+ enabled: true,
307
+ defaultTTL: 300000, // 5 minutes
308
+ maxSize: 50
309
+ },
310
+ retry: {
311
+ enabled: true,
312
+ maxRetries: 3,
313
+ initialDelay: 1000,
314
+ maxDelay: 10000
315
+ }
316
+ });
317
+
318
+ const result = await fetcher.fetch('https://example.com/data.geojson', {
319
+ ttl: 60000, // Override default TTL
320
+ onRetry: (attempt, delay, error) => {
321
+ console.log(`Retry attempt ${attempt} after ${delay}ms`);
322
+ }
323
+ });
324
+ ```
325
+
326
+ #### `PollingManager`
327
+ Manage periodic data refresh.
328
+
329
+ ```typescript
330
+ import { PollingManager } from '@maplibre-yaml/core';
331
+
332
+ const polling = new PollingManager();
333
+
334
+ polling.start('layer-id', {
335
+ interval: 5000,
336
+ onTick: async () => {
337
+ const data = await fetchData();
338
+ updateMap(data);
339
+ },
340
+ onError: (error) => console.error('Polling error:', error),
341
+ immediate: true, // Execute immediately on start
342
+ pauseWhenHidden: true // Pause when tab is hidden
343
+ });
344
+
345
+ // Control polling
346
+ polling.pause('layer-id');
347
+ polling.resume('layer-id');
348
+ await polling.triggerNow('layer-id');
349
+ polling.stop('layer-id');
350
+ ```
351
+
352
+ #### `StreamManager`
353
+ Manage WebSocket and SSE connections.
354
+
355
+ ```typescript
356
+ import { StreamManager } from '@maplibre-yaml/core';
357
+
358
+ const streams = new StreamManager();
359
+
360
+ await streams.connect('stream-id', {
361
+ type: 'websocket',
362
+ url: 'wss://example.com/stream',
363
+ onData: (data) => {
364
+ console.log('Received:', data);
365
+ },
366
+ onStateChange: (state) => {
367
+ console.log('Connection state:', state);
368
+ },
369
+ reconnect: {
370
+ enabled: true,
371
+ maxRetries: 10,
372
+ initialDelay: 1000,
373
+ maxDelay: 30000
374
+ }
375
+ });
376
+
377
+ // Send data (WebSocket only)
378
+ streams.send('stream-id', { type: 'subscribe', channel: 'updates' });
379
+
380
+ // Disconnect
381
+ streams.disconnect('stream-id');
382
+ ```
383
+
384
+ #### `DataMerger`
385
+ Merge GeoJSON data with different strategies.
386
+
387
+ ```typescript
388
+ import { DataMerger } from '@maplibre-yaml/core';
389
+
390
+ const merger = new DataMerger();
391
+
392
+ const result = merger.merge(existingData, incomingData, {
393
+ strategy: 'merge',
394
+ updateKey: 'id'
395
+ });
396
+
397
+ console.log(`Added: ${result.added}, Updated: ${result.updated}`);
398
+ console.log(`Total features: ${result.total}`);
399
+ ```
400
+
401
+ #### `LoadingManager`
402
+ Manage loading states with optional UI.
403
+
404
+ ```typescript
405
+ import { LoadingManager } from '@maplibre-yaml/core';
406
+
407
+ const loading = new LoadingManager({
408
+ showUI: true,
409
+ messages: {
410
+ loading: 'Loading data...',
411
+ error: 'Failed to load data',
412
+ retry: 'Retrying...'
413
+ },
414
+ spinnerStyle: 'circle',
415
+ minDisplayTime: 300 // Minimum 300ms display to prevent flashing
416
+ });
417
+
418
+ // Listen to events
419
+ loading.on('loading:start', ({ layerId }) => {
420
+ console.log('Loading started:', layerId);
421
+ });
422
+
423
+ loading.on('loading:complete', ({ layerId, duration, fromCache }) => {
424
+ console.log('Loading complete:', layerId, duration, fromCache);
425
+ });
426
+
427
+ // Show loading
428
+ loading.showLoading('layer-id', container, 'Loading...');
429
+
430
+ // Show progress
431
+ loading.updateProgress('layer-id', 50, 100);
432
+
433
+ // Hide loading
434
+ loading.hideLoading('layer-id', { fromCache: false });
435
+
436
+ // Show error with retry
437
+ loading.showError('layer-id', container, new Error('Failed'), () => {
438
+ retryLoad();
439
+ });
440
+ ```
441
+
442
+ ### Layer Manager Integration
443
+
444
+ The `LayerManager` integrates all data management components:
445
+
446
+ ```typescript
447
+ import { LayerManager } from '@maplibre-yaml/core';
448
+
449
+ const layerManager = new LayerManager(map, {
450
+ onDataLoading: (layerId) => console.log('Loading:', layerId),
451
+ onDataLoaded: (layerId, count) => console.log('Loaded:', layerId, count),
452
+ onDataError: (layerId, error) => console.error('Error:', layerId, error)
453
+ });
454
+
455
+ // Add layer with all data features
456
+ await layerManager.addLayer({
457
+ id: 'my-layer',
458
+ type: 'circle',
459
+ source: {
460
+ type: 'geojson',
461
+ url: 'https://example.com/data.geojson',
462
+ refresh: {
463
+ refreshInterval: 5000,
464
+ updateStrategy: 'merge',
465
+ updateKey: 'id'
466
+ },
467
+ cache: {
468
+ enabled: true,
469
+ ttl: 60000
470
+ },
471
+ stream: {
472
+ type: 'websocket',
473
+ url: 'wss://example.com/updates',
474
+ reconnect: true
475
+ }
476
+ }
477
+ });
478
+
479
+ // Control data updates
480
+ layerManager.pauseRefresh('my-layer');
481
+ layerManager.resumeRefresh('my-layer');
482
+ await layerManager.refreshNow('my-layer');
483
+ layerManager.disconnectStream('my-layer');
484
+ ```
485
+
486
+ ## Schema Validation
487
+
488
+ All YAML configurations are validated using Zod schemas:
489
+
490
+ ```typescript
491
+ import { MapConfigSchema } from '@maplibre-yaml/core/schemas';
492
+
493
+ try {
494
+ const config = MapConfigSchema.parse(yamlData);
495
+ // Config is valid and type-safe
496
+ } catch (error) {
497
+ console.error('Validation errors:', error.issues);
498
+ }
499
+ ```
500
+
501
+ ## Schemas
502
+
503
+ The schema system is a core component of `@maplibre-yaml/core`, providing type-safe validation and excellent developer experience. All schemas are built with [Zod](https://zod.dev) and automatically generate TypeScript types.
504
+
505
+ ### Schema Architecture
506
+
507
+ ```
508
+ MapConfigSchema
509
+ ├── PageConfigSchema (scrollytelling)
510
+ │ ├── SectionSchema
511
+ │ └── StepSchema
512
+ └── MapSchema (single map)
513
+ ├── MapConfigurationSchema
514
+ ├── LayerSchema[]
515
+ ├── SourceSchema[]
516
+ ├── LegendSchema
517
+ ├── ControlsSchema
518
+ └── InteractionSchema
519
+ ```
520
+
521
+ ### Map Configuration Schema
522
+
523
+ The top-level `MapConfigSchema` validates complete map configurations:
524
+
525
+ ```typescript
526
+ import { MapConfigSchema } from '@maplibre-yaml/core/schemas';
527
+
528
+ const config = MapConfigSchema.parse({
529
+ type: 'map',
530
+ id: 'my-map',
531
+ config: {
532
+ center: [-122.4, 37.8],
533
+ zoom: 12,
534
+ pitch: 0,
535
+ bearing: 0,
536
+ style: 'https://demotiles.maplibre.org/style.json',
537
+ minZoom: 0,
538
+ maxZoom: 22,
539
+ bounds: [[-180, -90], [180, 90]],
540
+ maxBounds: [[-180, -90], [180, 90]],
541
+ fitBoundsOptions: {
542
+ padding: 50,
543
+ maxZoom: 15
544
+ }
545
+ },
546
+ layers: [],
547
+ sources: [],
548
+ controls: {},
549
+ legend: {},
550
+ interactions: []
551
+ });
552
+ ```
553
+
554
+ ### Layer Schema
555
+
556
+ Supports all MapLibre layer types with full paint and layout properties:
557
+
558
+ ```typescript
559
+ import { LayerSchema } from '@maplibre-yaml/core/schemas';
560
+
561
+ // Circle layer
562
+ const circleLayer = LayerSchema.parse({
563
+ id: 'points',
564
+ type: 'circle',
565
+ source: 'points-source',
566
+ paint: {
567
+ 'circle-radius': 6,
568
+ 'circle-color': '#3b82f6',
569
+ 'circle-opacity': 0.8,
570
+ 'circle-stroke-width': 2,
571
+ 'circle-stroke-color': '#ffffff'
572
+ },
573
+ layout: {
574
+ visibility: 'visible'
575
+ },
576
+ minzoom: 0,
577
+ maxzoom: 22,
578
+ filter: ['==', ['get', 'type'], 'poi']
579
+ });
580
+
581
+ // Symbol layer with expressions
582
+ const symbolLayer = LayerSchema.parse({
583
+ id: 'labels',
584
+ type: 'symbol',
585
+ source: 'places',
586
+ layout: {
587
+ 'text-field': ['get', 'name'],
588
+ 'text-size': 12,
589
+ 'text-anchor': 'top',
590
+ 'text-offset': [0, 1],
591
+ 'icon-image': 'marker',
592
+ 'icon-size': 1
593
+ },
594
+ paint: {
595
+ 'text-color': '#000000',
596
+ 'text-halo-color': '#ffffff',
597
+ 'text-halo-width': 2
598
+ }
599
+ });
600
+
601
+ // Heatmap layer
602
+ const heatmapLayer = LayerSchema.parse({
603
+ id: 'density',
604
+ type: 'heatmap',
605
+ source: 'points',
606
+ paint: {
607
+ 'heatmap-weight': [
608
+ 'interpolate',
609
+ ['linear'],
610
+ ['get', 'value'],
611
+ 0, 0,
612
+ 100, 1
613
+ ],
614
+ 'heatmap-intensity': 1,
615
+ 'heatmap-radius': 30,
616
+ 'heatmap-opacity': 0.7
617
+ }
618
+ });
619
+ ```
620
+
621
+ **Supported Layer Types:**
622
+ - `circle` - Point data as circles
623
+ - `line` - Linear features
624
+ - `fill` - Polygon fills
625
+ - `fill-extrusion` - 3D buildings/polygons
626
+ - `symbol` - Icons and text labels
627
+ - `heatmap` - Density visualization
628
+ - `hillshade` - Terrain shading
629
+ - `raster` - Raster tiles
630
+ - `background` - Map background
631
+
632
+ ### Source Schema
633
+
634
+ Multiple source types with comprehensive configuration options:
635
+
636
+ #### GeoJSON Source
637
+
638
+ ```typescript
639
+ import { GeoJSONSourceSchema } from '@maplibre-yaml/core/schemas';
640
+
641
+ const source = GeoJSONSourceSchema.parse({
642
+ type: 'geojson',
643
+
644
+ // Data options (one required)
645
+ url: 'https://example.com/data.geojson',
646
+ // OR data: { type: 'FeatureCollection', features: [] },
647
+ // OR prefetchedData: { type: 'FeatureCollection', features: [] },
648
+
649
+ // Fetch strategy
650
+ fetchStrategy: 'runtime', // 'runtime' | 'build' | 'hybrid'
651
+
652
+ // Real-time updates
653
+ refresh: {
654
+ refreshInterval: 5000,
655
+ updateStrategy: 'merge', // 'replace' | 'merge' | 'append-window'
656
+ updateKey: 'id',
657
+ windowSize: 100,
658
+ windowDuration: 300000,
659
+ timestampField: 'timestamp'
660
+ },
661
+
662
+ // Streaming
663
+ stream: {
664
+ type: 'websocket', // 'websocket' | 'sse'
665
+ url: 'wss://example.com/stream',
666
+ reconnect: true,
667
+ reconnectMaxAttempts: 10,
668
+ reconnectDelay: 1000,
669
+ reconnectMaxDelay: 30000,
670
+ eventTypes: ['update', 'delete'],
671
+ protocols: ['v1', 'v2']
672
+ },
673
+
674
+ // Caching
675
+ cache: {
676
+ enabled: true,
677
+ ttl: 300000 // 5 minutes
678
+ },
679
+
680
+ // Loading UI
681
+ loading: {
682
+ enabled: true,
683
+ message: 'Loading data...',
684
+ showErrorOverlay: true
685
+ },
686
+
687
+ // Clustering
688
+ cluster: true,
689
+ clusterRadius: 50,
690
+ clusterMaxZoom: 14,
691
+ clusterMinPoints: 2,
692
+ clusterProperties: {
693
+ sum: ['+', ['get', 'value']],
694
+ max: ['max', ['get', 'value']]
695
+ },
696
+
697
+ // Spatial index
698
+ tolerance: 0.375,
699
+ buffer: 128,
700
+ lineMetrics: false,
701
+ generateId: false
702
+ });
703
+ ```
704
+
705
+ #### Vector Tile Source
706
+
707
+ ```typescript
708
+ import { VectorSourceSchema } from '@maplibre-yaml/core/schemas';
709
+
710
+ const source = VectorSourceSchema.parse({
711
+ type: 'vector',
712
+ tiles: [
713
+ 'https://tiles.example.com/{z}/{x}/{y}.pbf'
714
+ ],
715
+ // OR url: 'https://tiles.example.com/tiles.json',
716
+ minzoom: 0,
717
+ maxzoom: 14,
718
+ bounds: [-180, -85.0511, 180, 85.0511],
719
+ attribution: '© Example Maps'
720
+ });
721
+ ```
722
+
723
+ #### Raster Source
724
+
725
+ ```typescript
726
+ import { RasterSourceSchema } from '@maplibre-yaml/core/schemas';
727
+
728
+ const source = RasterSourceSchema.parse({
729
+ type: 'raster',
730
+ tiles: [
731
+ 'https://tiles.example.com/{z}/{x}/{y}.png'
732
+ ],
733
+ tileSize: 256,
734
+ minzoom: 0,
735
+ maxzoom: 18,
736
+ attribution: '© Example Imagery'
737
+ });
738
+ ```
739
+
740
+ #### Image & Video Sources
741
+
742
+ ```typescript
743
+ import { ImageSourceSchema, VideoSourceSchema } from '@maplibre-yaml/core/schemas';
744
+
745
+ // Image overlay
746
+ const imageSource = ImageSourceSchema.parse({
747
+ type: 'image',
748
+ url: 'https://example.com/overlay.png',
749
+ coordinates: [
750
+ [-122.5, 37.9], // top-left
751
+ [-122.3, 37.9], // top-right
752
+ [-122.3, 37.7], // bottom-right
753
+ [-122.5, 37.7] // bottom-left
754
+ ]
755
+ });
756
+
757
+ // Video overlay
758
+ const videoSource = VideoSourceSchema.parse({
759
+ type: 'video',
760
+ urls: [
761
+ 'https://example.com/video.mp4',
762
+ 'https://example.com/video.webm'
763
+ ],
764
+ coordinates: [
765
+ [-122.5, 37.9],
766
+ [-122.3, 37.9],
767
+ [-122.3, 37.7],
768
+ [-122.5, 37.7]
769
+ ]
770
+ });
771
+ ```
772
+
773
+ ### Controls Schema
774
+
775
+ Configure map controls with the `ControlsSchema`:
776
+
777
+ ```typescript
778
+ import { ControlsSchema } from '@maplibre-yaml/core/schemas';
779
+
780
+ const controls = ControlsSchema.parse({
781
+ navigation: {
782
+ enabled: true,
783
+ position: 'top-right',
784
+ showCompass: true,
785
+ showZoom: true,
786
+ visualizePitch: true
787
+ },
788
+ scale: {
789
+ enabled: true,
790
+ position: 'bottom-left',
791
+ maxWidth: 100,
792
+ unit: 'metric' // 'metric' | 'imperial' | 'nautical'
793
+ },
794
+ geolocation: {
795
+ enabled: true,
796
+ position: 'top-right',
797
+ trackUserLocation: true,
798
+ showUserHeading: true,
799
+ showAccuracyCircle: true
800
+ },
801
+ fullscreen: {
802
+ enabled: true,
803
+ position: 'top-right'
804
+ }
805
+ });
806
+ ```
807
+
808
+ ### Legend Schema
809
+
810
+ Create dynamic legends with the `LegendSchema`:
811
+
812
+ ```typescript
813
+ import { LegendSchema } from '@maplibre-yaml/core/schemas';
814
+
815
+ const legend = LegendSchema.parse({
816
+ enabled: true,
817
+ position: 'bottom-left',
818
+ title: 'Map Legend',
819
+ entries: [
820
+ {
821
+ label: 'Active',
822
+ color: '#22c55e',
823
+ shape: 'circle' // 'circle' | 'square' | 'line'
824
+ },
825
+ {
826
+ label: 'Delayed',
827
+ color: '#f59e0b',
828
+ shape: 'circle'
829
+ },
830
+ {
831
+ label: 'Inactive',
832
+ color: '#6b7280',
833
+ shape: 'circle'
834
+ }
835
+ ],
836
+ style: {
837
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
838
+ padding: '10px',
839
+ borderRadius: '4px'
840
+ }
841
+ });
842
+ ```
843
+
844
+ ### Interaction Schema
845
+
846
+ Define click handlers and popups:
847
+
848
+ ```typescript
849
+ import { InteractionSchema } from '@maplibre-yaml/core/schemas';
850
+
851
+ const interaction = InteractionSchema.parse({
852
+ type: 'click',
853
+ layers: ['points', 'polygons'],
854
+ popup: {
855
+ content: `
856
+ <h3>{{name}}</h3>
857
+ <p>{{description}}</p>
858
+ <p><strong>Category:</strong> {{category}}</p>
859
+ <p><strong>Value:</strong> {{value}}</p>
860
+ `,
861
+ closeButton: true,
862
+ closeOnClick: true,
863
+ maxWidth: '300px',
864
+ className: 'custom-popup'
865
+ },
866
+ action: 'popup' // 'popup' | 'toggle-visibility' | 'fly-to'
867
+ });
868
+ ```
869
+
870
+ ### Scrollytelling Schema (Page Configuration)
871
+
872
+ For narrative-driven maps with synchronized scrolling:
873
+
874
+ ```typescript
875
+ import { PageConfigSchema } from '@maplibre-yaml/core/schemas';
876
+
877
+ const page = PageConfigSchema.parse({
878
+ type: 'page',
879
+ id: 'story-map',
880
+ title: 'Urban Growth Story',
881
+ description: 'Explore urban development over time',
882
+ sections: [
883
+ {
884
+ id: 'intro',
885
+ type: 'text',
886
+ title: 'Introduction',
887
+ content: 'This story explores urban development...',
888
+ style: {
889
+ backgroundColor: '#f9fafb',
890
+ padding: '60px'
891
+ }
892
+ },
893
+ {
894
+ id: 'map-section',
895
+ type: 'map',
896
+ config: {
897
+ center: [-122.4, 37.8],
898
+ zoom: 12,
899
+ style: 'https://demotiles.maplibre.org/style.json'
900
+ },
901
+ steps: [
902
+ {
903
+ id: 'step1',
904
+ content: '## 1990s\nUrban core development began...',
905
+ map: {
906
+ center: [-122.4, 37.8],
907
+ zoom: 13,
908
+ pitch: 0,
909
+ bearing: 0,
910
+ duration: 2000
911
+ },
912
+ layers: {
913
+ show: ['buildings-1990'],
914
+ hide: ['buildings-2000', 'buildings-2010']
915
+ }
916
+ },
917
+ {
918
+ id: 'step2',
919
+ content: '## 2000s\nSuburban expansion accelerated...',
920
+ map: {
921
+ center: [-122.3, 37.9],
922
+ zoom: 12,
923
+ pitch: 45,
924
+ bearing: -17.6,
925
+ duration: 2000
926
+ },
927
+ layers: {
928
+ show: ['buildings-1990', 'buildings-2000'],
929
+ hide: ['buildings-2010']
930
+ }
931
+ }
932
+ ]
933
+ }
934
+ ]
935
+ });
936
+ ```
937
+
938
+ ### Schema Composition
939
+
940
+ Schemas can be composed for complex configurations:
941
+
942
+ ```typescript
943
+ import {
944
+ MapSchema,
945
+ LayerSchema,
946
+ GeoJSONSourceSchema,
947
+ ControlsSchema,
948
+ LegendSchema
949
+ } from '@maplibre-yaml/core/schemas';
950
+
951
+ // Build configuration programmatically
952
+ const layer = LayerSchema.parse({ /* ... */ });
953
+ const source = GeoJSONSourceSchema.parse({ /* ... */ });
954
+ const controls = ControlsSchema.parse({ /* ... */ });
955
+
956
+ const map = MapSchema.parse({
957
+ type: 'map',
958
+ id: 'composed-map',
959
+ config: { /* ... */ },
960
+ layers: [layer],
961
+ sources: [{ id: 'my-source', ...source }],
962
+ controls
963
+ });
964
+ ```
965
+
966
+ ### Validation and Error Handling
967
+
968
+ Schemas provide detailed validation errors:
969
+
970
+ ```typescript
971
+ import { MapConfigSchema } from '@maplibre-yaml/core/schemas';
972
+ import { ZodError } from 'zod';
973
+
974
+ try {
975
+ const config = MapConfigSchema.parse(invalidConfig);
976
+ } catch (error) {
977
+ if (error instanceof ZodError) {
978
+ error.issues.forEach(issue => {
979
+ console.error(
980
+ `${issue.path.join('.')}: ${issue.message}`
981
+ );
982
+ });
983
+ // Example output:
984
+ // layers.0.source: Required
985
+ // config.center: Expected array, received string
986
+ // layers.0.paint.circle-radius: Expected number, received string
987
+ }
988
+ }
989
+ ```
990
+
991
+ ### Safe Parsing
992
+
993
+ Use `safeParse` for non-throwing validation:
994
+
995
+ ```typescript
996
+ import { LayerSchema } from '@maplibre-yaml/core/schemas';
997
+
998
+ const result = LayerSchema.safeParse(data);
999
+
1000
+ if (result.success) {
1001
+ // result.data is typed and valid
1002
+ console.log('Valid layer:', result.data);
1003
+ } else {
1004
+ // result.error contains validation issues
1005
+ console.error('Validation failed:', result.error.issues);
1006
+ }
1007
+ ```
1008
+
1009
+ ### Type Inference
1010
+
1011
+ Schemas automatically generate TypeScript types:
1012
+
1013
+ ```typescript
1014
+ import { LayerSchema, GeoJSONSourceSchema } from '@maplibre-yaml/core/schemas';
1015
+ import type { z } from 'zod';
1016
+
1017
+ // Infer types from schemas
1018
+ type Layer = z.infer<typeof LayerSchema>;
1019
+ type GeoJSONSource = z.infer<typeof GeoJSONSourceSchema>;
1020
+
1021
+ // Use inferred types
1022
+ const createLayer = (layer: Layer) => {
1023
+ // layer is fully typed with autocomplete
1024
+ console.log(layer.id, layer.type, layer.paint);
1025
+ };
1026
+ ```
1027
+
1028
+ ### Custom Validation
1029
+
1030
+ Extend schemas with custom validation:
1031
+
1032
+ ```typescript
1033
+ import { LayerSchema } from '@maplibre-yaml/core/schemas';
1034
+ import { z } from 'zod';
1035
+
1036
+ // Add custom refinement
1037
+ const CustomLayerSchema = LayerSchema.refine(
1038
+ (layer) => {
1039
+ if (layer.type === 'circle' && layer.paint) {
1040
+ const radius = layer.paint['circle-radius'];
1041
+ return typeof radius === 'number' && radius > 0;
1042
+ }
1043
+ return true;
1044
+ },
1045
+ {
1046
+ message: 'Circle radius must be a positive number'
1047
+ }
1048
+ );
1049
+ ```
1050
+
1051
+ ### Schema Defaults
1052
+
1053
+ Many schemas include sensible defaults:
1054
+
1055
+ ```typescript
1056
+ import { GeoJSONSourceSchema } from '@maplibre-yaml/core/schemas';
1057
+
1058
+ const source = GeoJSONSourceSchema.parse({
1059
+ type: 'geojson',
1060
+ url: 'https://example.com/data.geojson'
1061
+ // Defaults applied:
1062
+ // - fetchStrategy: 'runtime'
1063
+ // - cluster: false
1064
+ // - clusterRadius: 50
1065
+ // - tolerance: 0.375
1066
+ // - cache.enabled: true
1067
+ // - loading.enabled: false
1068
+ });
1069
+
1070
+ console.log(source.fetchStrategy); // 'runtime'
1071
+ console.log(source.cluster); // false
1072
+ console.log(source.clusterRadius); // 50
1073
+ ```
1074
+
1075
+ ### Available Schemas
1076
+
1077
+ All schemas are exported from `@maplibre-yaml/core/schemas`:
1078
+
1079
+ ```typescript
1080
+ import {
1081
+ // Top-level
1082
+ MapConfigSchema,
1083
+ PageConfigSchema,
1084
+
1085
+ // Map components
1086
+ MapSchema,
1087
+ MapConfigurationSchema,
1088
+
1089
+ // Layers
1090
+ LayerSchema,
1091
+ CircleLayerSchema,
1092
+ LineLayerSchema,
1093
+ FillLayerSchema,
1094
+ SymbolLayerSchema,
1095
+ HeatmapLayerSchema,
1096
+
1097
+ // Sources
1098
+ SourceSchema,
1099
+ GeoJSONSourceSchema,
1100
+ VectorSourceSchema,
1101
+ RasterSourceSchema,
1102
+ ImageSourceSchema,
1103
+ VideoSourceSchema,
1104
+
1105
+ // Configuration
1106
+ ControlsSchema,
1107
+ LegendSchema,
1108
+ InteractionSchema,
1109
+
1110
+ // Scrollytelling
1111
+ SectionSchema,
1112
+ StepSchema,
1113
+
1114
+ // Data management
1115
+ RefreshConfigSchema,
1116
+ StreamConfigSchema,
1117
+ CacheConfigSchema,
1118
+ LoadingConfigSchema
1119
+ } from '@maplibre-yaml/core/schemas';
1120
+ ```
1121
+
1122
+ ## TypeScript Support
1123
+
1124
+ Full TypeScript support with exported types:
1125
+
1126
+ ```typescript
1127
+ import type {
1128
+ MapConfig,
1129
+ LayerConfig,
1130
+ GeoJSONSourceConfig,
1131
+ MergeStrategy,
1132
+ PollingConfig,
1133
+ StreamConfig,
1134
+ LoadingConfig
1135
+ } from '@maplibre-yaml/core';
1136
+
1137
+ const config: MapConfig = {
1138
+ type: 'map',
1139
+ id: 'my-map',
1140
+ config: {
1141
+ center: [-122.4, 37.8],
1142
+ zoom: 12
1143
+ },
1144
+ layers: []
1145
+ };
1146
+ ```
1147
+
1148
+ ## Performance Considerations
1149
+
1150
+ ### Caching
1151
+ - Default cache TTL: 5 minutes
1152
+ - Cache respects `Cache-Control` headers
1153
+ - Cache size limit: 50 entries (LRU eviction)
1154
+ - Disable cache for real-time data
1155
+
1156
+ ### Polling
1157
+ - Minimum interval: 1000ms (1 second)
1158
+ - Non-overlapping execution (waits for previous tick to complete)
1159
+ - Automatic pause when document is hidden
1160
+ - Configurable error handling and continuation
1161
+
1162
+ ### Streaming
1163
+ - Automatic reconnection with exponential backoff
1164
+ - Connection state tracking
1165
+ - Graceful degradation on connection loss
1166
+ - Efficient binary frame handling (WebSocket)
1167
+
1168
+ ### Memory Management
1169
+ - All managers implement proper cleanup via `destroy()`
1170
+ - Automatic timer and connection cleanup
1171
+ - DOM element removal
1172
+ - No memory leaks in long-running applications
1173
+
1174
+ ## Browser Support
1175
+
1176
+ - Modern browsers with ES2022 support
1177
+ - Native `fetch`, `EventSource`, and `WebSocket` APIs
1178
+ - ResizeObserver for responsive layouts
1179
+ - Document visibility API for polling optimization
1180
+
1181
+ ## Bundle Size
1182
+
1183
+ - Core package: ~136KB (unminified)
1184
+ - Tree-shakeable ES modules
1185
+ - Zero runtime dependencies (peer dependency: maplibre-gl)
1186
+
1187
+ ## Examples
1188
+
1189
+ See the [examples directory](../../examples) for complete working examples:
1190
+
1191
+ - Basic static map
1192
+ - Real-time vehicle tracking
1193
+ - Sensor data heatmap with SSE
1194
+ - Interactive point-of-interest map
1195
+ - Multi-layer dashboard
1196
+
1197
+ ## Contributing
1198
+
1199
+ Contributions are welcome! Please read the [contributing guidelines](../../CONTRIBUTING.md) first.
1200
+
1201
+ ## License
1202
+
1203
+ MIT � [Your Name]
1204
+
1205
+ ## Related Packages
1206
+
1207
+ - [`@maplibre-yaml/astro`](../astro) - Astro component integration
1208
+ - [`@maplibre-yaml/cli`](../cli) - CLI tools for YAML validation and processing
1209
+
1210
+ ## Resources
1211
+
1212
+ - [MapLibre GL JS Documentation](https://maplibre.org/maplibre-gl-js-docs/)
1213
+ - [GeoJSON Specification](https://geojson.org/)
1214
+ - [YAML Specification](https://yaml.org/)