@maplibre-yaml/core 0.1.3-beta.0 → 0.1.3

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 CHANGED
@@ -1,44 +1,10 @@
1
1
  # @maplibre-yaml/core
2
2
 
3
- > Declarative web maps with YAML configuration. Build interactive MapLibre maps using simple, readable YAML syntax.
3
+ Declarative web maps with YAML configuration. Build interactive MapLibre maps using simple, readable YAML syntax.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@maplibre-yaml/core.svg)](https://www.npmjs.com/package/@maplibre-yaml/core)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
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
8
  ## Installation
43
9
 
44
10
  ```bash
@@ -47,35 +13,39 @@ npm install @maplibre-yaml/core maplibre-gl
47
13
 
48
14
  ## Quick Start
49
15
 
50
- ### Using Web Components
16
+ ### Web Component
17
+
18
+ The simplest way to use maplibre-yaml is with the `<ml-map>` web component:
51
19
 
52
20
  ```html
53
21
  <!DOCTYPE html>
54
22
  <html>
55
23
  <head>
56
24
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css">
25
+ <style>
26
+ ml-map { display: block; height: 400px; }
27
+ </style>
57
28
  </head>
58
29
  <body>
59
- <ml-map config="./map.yaml"></ml-map>
60
-
30
+ <ml-map src="/map.yaml"></ml-map>
61
31
  <script type="module">
62
- import '@maplibre-yaml/core/components';
32
+ import '@maplibre-yaml/core/register';
63
33
  </script>
64
34
  </body>
65
35
  </html>
66
36
  ```
67
37
 
68
- ### Using JavaScript API
38
+ ### JavaScript API
69
39
 
70
40
  ```typescript
71
- import { YAMLParser, MapRenderer } from '@maplibre-yaml/core';
41
+ import { parseYAMLConfig, MapRenderer } from '@maplibre-yaml/core';
72
42
 
73
43
  const yaml = `
74
44
  type: map
75
45
  config:
76
46
  center: [-122.4, 37.8]
77
47
  zoom: 12
78
- style: https://demotiles.maplibre.org/style.json
48
+ mapStyle: https://demotiles.maplibre.org/style.json
79
49
  layers:
80
50
  - id: points
81
51
  type: circle
@@ -87,13 +57,22 @@ layers:
87
57
  circle-color: "#3b82f6"
88
58
  `;
89
59
 
90
- const parser = new YAMLParser();
91
- const config = await parser.parse(yaml);
92
-
60
+ const config = parseYAMLConfig(yaml);
93
61
  const container = document.getElementById('map');
94
62
  const renderer = new MapRenderer(container, config);
95
63
  ```
96
64
 
65
+ ## Entry Points
66
+
67
+ The package provides multiple entry points for different use cases:
68
+
69
+ | Import | Purpose |
70
+ |--------|---------|
71
+ | `@maplibre-yaml/core` | Main entry: parser, renderer, data utilities |
72
+ | `@maplibre-yaml/core/register` | Auto-registers `<ml-map>` web component |
73
+ | `@maplibre-yaml/core/schemas` | Zod schemas for validation and types |
74
+ | `@maplibre-yaml/core/components` | Manual component registration |
75
+
97
76
  ## YAML Configuration
98
77
 
99
78
  ### Basic Map
@@ -104,7 +83,7 @@ id: my-map
104
83
  config:
105
84
  center: [-74.006, 40.7128]
106
85
  zoom: 10
107
- style: https://demotiles.maplibre.org/style.json
86
+ mapStyle: https://demotiles.maplibre.org/style.json
108
87
 
109
88
  layers:
110
89
  - id: cities
@@ -135,14 +114,9 @@ layers:
135
114
  type: geojson
136
115
  url: https://api.transit.example.com/vehicles.geojson
137
116
  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...
117
+ refreshInterval: 5000
118
+ updateStrategy: merge
119
+ updateKey: vehicleId
146
120
  paint:
147
121
  circle-radius: 6
148
122
  circle-color:
@@ -155,7 +129,7 @@ layers:
155
129
  - "#6b7280"
156
130
  ```
157
131
 
158
- ### Real-time Streaming with WebSocket
132
+ ### Streaming with WebSocket
159
133
 
160
134
  ```yaml
161
135
  layers:
@@ -163,53 +137,21 @@ layers:
163
137
  type: circle
164
138
  source:
165
139
  type: geojson
166
- url: https://api.example.com/initial-events.geojson
140
+ url: https://api.example.com/initial.geojson
167
141
  stream:
168
142
  type: websocket
169
143
  url: wss://api.example.com/events
170
144
  reconnect: true
171
- reconnectMaxAttempts: 10
172
145
  refresh:
173
146
  updateStrategy: append-window
174
- windowSize: 100 # Keep last 100 events
175
- windowDuration: 300000 # Keep events from last 5 minutes
176
- timestampField: timestamp
147
+ windowSize: 100
148
+ windowDuration: 300000
177
149
  paint:
178
150
  circle-radius: 8
179
151
  circle-color: "#8b5cf6"
180
152
  ```
181
153
 
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
154
+ ### Interactive Popups
213
155
 
214
156
  ```yaml
215
157
  layers:
@@ -220,111 +162,71 @@ layers:
220
162
  url: https://example.com/locations.geojson
221
163
  layout:
222
164
  icon-image: marker
223
- icon-size: 1.5
224
165
  interactions:
225
166
  - type: click
226
167
  popup:
227
168
  content: |
228
169
  <h3>{{name}}</h3>
229
170
  <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
171
  ```
259
172
 
260
173
  ## API Reference
261
174
 
262
- ### Core Classes
263
-
264
- #### `YAMLParser`
265
- Parse and validate YAML map configurations.
175
+ ### Parser
266
176
 
267
177
  ```typescript
268
- import { YAMLParser } from '@maplibre-yaml/core';
178
+ import { YAMLParser, parseYAMLConfig, safeParseYAMLConfig } from '@maplibre-yaml/core';
179
+
180
+ // Simple parsing
181
+ const config = parseYAMLConfig(yamlString);
182
+
183
+ // Safe parsing with error handling
184
+ const result = safeParseYAMLConfig(yamlString);
185
+ if (result.success) {
186
+ console.log(result.data);
187
+ } else {
188
+ console.error(result.error);
189
+ }
269
190
 
191
+ // Using the class
270
192
  const parser = new YAMLParser();
271
- const config = await parser.parse(yamlString);
272
- const errors = parser.validate(config);
193
+ const config = parser.parse(yamlString);
273
194
  ```
274
195
 
275
- #### `MapRenderer`
276
- Render maps from parsed configurations.
196
+ ### Renderer
277
197
 
278
198
  ```typescript
279
199
  import { MapRenderer } from '@maplibre-yaml/core';
280
200
 
281
201
  const renderer = new MapRenderer(container, config, {
282
202
  onLoad: () => console.log('Map loaded'),
283
- onError: (error) => console.error('Map error:', error),
203
+ onError: (error) => console.error(error),
284
204
  });
285
205
 
286
- // Update visibility
206
+ // Control layers
287
207
  renderer.setLayerVisibility('layer-id', false);
288
-
289
- // Update data
290
208
  renderer.updateLayerData('layer-id', newGeoJSON);
291
209
 
292
- // Clean up
210
+ // Cleanup
293
211
  renderer.destroy();
294
212
  ```
295
213
 
296
214
  ### Data Management
297
215
 
298
- #### `DataFetcher`
299
- HTTP data fetching with caching and retry.
216
+ #### DataFetcher
300
217
 
301
218
  ```typescript
302
219
  import { DataFetcher } from '@maplibre-yaml/core';
303
220
 
304
221
  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
- }
222
+ cache: { enabled: true, defaultTTL: 300000, maxSize: 50 },
223
+ retry: { enabled: true, maxRetries: 3 }
316
224
  });
317
225
 
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
- });
226
+ const result = await fetcher.fetch('https://example.com/data.geojson');
324
227
  ```
325
228
 
326
- #### `PollingManager`
327
- Manage periodic data refresh.
229
+ #### PollingManager
328
230
 
329
231
  ```typescript
330
232
  import { PollingManager } from '@maplibre-yaml/core';
@@ -333,24 +235,16 @@ const polling = new PollingManager();
333
235
 
334
236
  polling.start('layer-id', {
335
237
  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
238
+ onTick: async () => updateMap(await fetchData()),
239
+ pauseWhenHidden: true
343
240
  });
344
241
 
345
- // Control polling
346
242
  polling.pause('layer-id');
347
243
  polling.resume('layer-id');
348
- await polling.triggerNow('layer-id');
349
244
  polling.stop('layer-id');
350
245
  ```
351
246
 
352
- #### `StreamManager`
353
- Manage WebSocket and SSE connections.
247
+ #### StreamManager
354
248
 
355
249
  ```typescript
356
250
  import { StreamManager } from '@maplibre-yaml/core';
@@ -360,855 +254,194 @@ const streams = new StreamManager();
360
254
  await streams.connect('stream-id', {
361
255
  type: 'websocket',
362
256
  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
- }
257
+ onData: (data) => console.log(data),
258
+ reconnect: { enabled: true, maxRetries: 10 }
375
259
  });
376
260
 
377
- // Send data (WebSocket only)
378
- streams.send('stream-id', { type: 'subscribe', channel: 'updates' });
379
-
380
- // Disconnect
381
261
  streams.disconnect('stream-id');
382
262
  ```
383
263
 
384
- #### `DataMerger`
385
- Merge GeoJSON data with different strategies.
264
+ #### DataMerger
386
265
 
387
266
  ```typescript
388
267
  import { DataMerger } from '@maplibre-yaml/core';
389
268
 
390
269
  const merger = new DataMerger();
391
-
392
- const result = merger.merge(existingData, incomingData, {
270
+ const result = merger.merge(existing, incoming, {
393
271
  strategy: 'merge',
394
272
  updateKey: 'id'
395
273
  });
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
274
  ```
553
275
 
554
- ### Layer Schema
276
+ ### Schemas
555
277
 
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:
278
+ All schemas are built with Zod and provide TypeScript types:
941
279
 
942
280
  ```typescript
943
281
  import {
944
- MapSchema,
282
+ // Layer schemas
945
283
  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
- ```
284
+ CircleLayerSchema,
285
+ LineLayerSchema,
286
+ FillLayerSchema,
287
+ SymbolLayerSchema,
288
+ RasterLayerSchema,
289
+ FillExtrusionLayerSchema,
290
+ HeatmapLayerSchema,
291
+ HillshadeLayerSchema,
292
+ BackgroundLayerSchema,
990
293
 
991
- ### Safe Parsing
294
+ // Source schemas
295
+ GeoJSONSourceSchema,
296
+ VectorSourceSchema,
297
+ RasterSourceSchema,
298
+ ImageSourceSchema,
299
+ VideoSourceSchema,
992
300
 
993
- Use `safeParse` for non-throwing validation:
301
+ // Map schemas
302
+ MapConfigSchema,
303
+ MapBlockSchema,
304
+ ControlsConfigSchema,
305
+ LegendConfigSchema,
306
+
307
+ // Scrollytelling schemas
308
+ ChapterSchema,
309
+ ScrollytellingBlockSchema,
310
+
311
+ // Page schemas
312
+ PageSchema,
313
+ RootSchema,
314
+ GlobalConfigSchema,
315
+
316
+ // Base schemas
317
+ LngLatSchema,
318
+ ColorSchema,
319
+ ExpressionSchema,
320
+ } from '@maplibre-yaml/core/schemas';
994
321
 
995
- ```typescript
996
- import { LayerSchema } from '@maplibre-yaml/core/schemas';
322
+ // Validation
323
+ const layer = LayerSchema.parse(data);
997
324
 
325
+ // Safe parsing
998
326
  const result = LayerSchema.safeParse(data);
999
-
1000
327
  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);
328
+ console.log(result.data);
1006
329
  }
1007
330
  ```
1008
331
 
1009
- ### Type Inference
1010
-
1011
- Schemas automatically generate TypeScript types:
332
+ Type inference:
1012
333
 
1013
334
  ```typescript
1014
- import { LayerSchema, GeoJSONSourceSchema } from '@maplibre-yaml/core/schemas';
1015
335
  import type { z } from 'zod';
336
+ import { LayerSchema, MapBlockSchema } from '@maplibre-yaml/core/schemas';
1016
337
 
1017
- // Infer types from schemas
1018
338
  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
- };
339
+ type MapBlock = z.infer<typeof MapBlockSchema>;
1026
340
  ```
1027
341
 
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:
342
+ Exported types:
1054
343
 
1055
344
  ```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
345
+ import type {
346
+ // Map types
347
+ MapConfig,
348
+ MapBlock,
349
+ MapFullPageBlock,
350
+ ControlPosition,
351
+ ControlsConfig,
352
+ LegendConfig,
353
+
354
+ // Scrollytelling types
355
+ Chapter,
356
+ ChapterAction,
357
+ ChapterLayers,
358
+ ScrollytellingBlock,
359
+
360
+ // Page types
361
+ Page,
362
+ Block,
363
+ MixedBlock,
364
+ GlobalConfig,
365
+ RootConfig,
366
+ } from '@maplibre-yaml/core/schemas';
1073
367
  ```
1074
368
 
1075
- ### Available Schemas
1076
-
1077
- All schemas are exported from `@maplibre-yaml/core/schemas`:
369
+ ### Web Components
1078
370
 
1079
371
  ```typescript
1080
- import {
1081
- // Top-level
1082
- MapConfigSchema,
1083
- PageConfigSchema,
372
+ import { MLMap, registerMLMap } from '@maplibre-yaml/core/components';
1084
373
 
1085
- // Map components
1086
- MapSchema,
1087
- MapConfigurationSchema,
374
+ // Register with default name
375
+ registerMLMap();
1088
376
 
1089
- // Layers
1090
- LayerSchema,
1091
- CircleLayerSchema,
1092
- LineLayerSchema,
1093
- FillLayerSchema,
1094
- SymbolLayerSchema,
1095
- HeatmapLayerSchema,
377
+ // Or with custom name
378
+ customElements.define('my-map', MLMap);
379
+ ```
1096
380
 
1097
- // Sources
1098
- SourceSchema,
1099
- GeoJSONSourceSchema,
1100
- VectorSourceSchema,
1101
- RasterSourceSchema,
1102
- ImageSourceSchema,
1103
- VideoSourceSchema,
381
+ Component usage:
1104
382
 
1105
- // Configuration
1106
- ControlsSchema,
1107
- LegendSchema,
1108
- InteractionSchema,
383
+ ```html
384
+ <!-- External YAML file -->
385
+ <ml-map src="/map.yaml"></ml-map>
1109
386
 
1110
- // Scrollytelling
1111
- SectionSchema,
1112
- StepSchema,
387
+ <!-- Inline YAML -->
388
+ <ml-map>
389
+ <script type="text/yaml">
390
+ type: map
391
+ config:
392
+ center: [0, 0]
393
+ zoom: 2
394
+ mapStyle: "https://..."
395
+ </script>
396
+ </ml-map>
1113
397
 
1114
- // Data management
1115
- RefreshConfigSchema,
1116
- StreamConfigSchema,
1117
- CacheConfigSchema,
1118
- LoadingConfigSchema
1119
- } from '@maplibre-yaml/core/schemas';
398
+ <!-- JSON attribute -->
399
+ <ml-map config='{"type":"map",...}'></ml-map>
1120
400
  ```
1121
401
 
1122
- ## TypeScript Support
402
+ ## Merge Strategies
1123
403
 
1124
- Full TypeScript support with exported types:
404
+ | Strategy | Description |
405
+ |----------|-------------|
406
+ | `replace` | Replace all data on each update |
407
+ | `merge` | Update/add features by unique key |
408
+ | `append-window` | Append with size/time-based windowing |
1125
409
 
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
410
+ ```yaml
411
+ refresh:
412
+ updateStrategy: merge
413
+ updateKey: id
1155
414
 
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
415
+ # OR for windowed appending
416
+ refresh:
417
+ updateStrategy: append-window
418
+ windowSize: 100
419
+ windowDuration: 300000
420
+ timestampField: timestamp
421
+ ```
1161
422
 
1162
- ### Streaming
1163
- - Automatic reconnection with exponential backoff
1164
- - Connection state tracking
1165
- - Graceful degradation on connection loss
1166
- - Efficient binary frame handling (WebSocket)
423
+ ## Layer Types
1167
424
 
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
425
+ - `circle` - Point data as circles
426
+ - `line` - Linear features
427
+ - `fill` - Polygon fills
428
+ - `fill-extrusion` - 3D buildings/polygons
429
+ - `symbol` - Icons and text labels
430
+ - `heatmap` - Density visualization
431
+ - `hillshade` - Terrain shading
432
+ - `raster` - Raster tiles
433
+ - `background` - Map background
1173
434
 
1174
435
  ## Browser Support
1175
436
 
1176
437
  - Modern browsers with ES2022 support
1177
438
  - 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
439
 
1205
440
  ## Related Packages
1206
441
 
1207
442
  - [`@maplibre-yaml/astro`](../astro) - Astro component integration
1208
- - [`@maplibre-yaml/cli`](../cli) - CLI tools for YAML validation and processing
443
+ - [`@maplibre-yaml/cli`](../cli) - CLI for YAML validation
1209
444
 
1210
- ## Resources
445
+ ## License
1211
446
 
1212
- - [MapLibre GL JS Documentation](https://maplibre.org/maplibre-gl-js-docs/)
1213
- - [GeoJSON Specification](https://geojson.org/)
1214
- - [YAML Specification](https://yaml.org/)
447
+ MIT