@maplibre-yaml/core 0.2.1 → 0.3.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/LICENSE.md +21 -0
- package/README.md +44 -16
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +192 -68
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +1 -9
- package/dist/index.js +202 -89
- package/dist/index.js.map +1 -1
- package/dist/{map-renderer-SjO3KQmx.d.ts → map-renderer-4FF3EmBO.d.ts} +130 -3
- package/dist/register.browser.js +6024 -7030
- package/dist/register.browser.js.map +1 -1
- package/dist/register.d.ts +1 -1
- package/dist/register.js +192 -68
- package/dist/register.js.map +1 -1
- package/dist/schemas/index.js +24 -2
- package/dist/schemas/index.js.map +1 -1
- package/package.json +17 -12
- package/register.js +8 -0
package/dist/register.d.ts
CHANGED
package/dist/register.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parse } from 'yaml';
|
|
2
2
|
import { z, ZodError } from 'zod';
|
|
3
|
-
import
|
|
3
|
+
import * as maplibre from 'maplibre-gl';
|
|
4
4
|
|
|
5
5
|
// @maplibre-yaml/core - Declarative web maps with YAML
|
|
6
6
|
|
|
@@ -154,8 +154,30 @@ var GeoJSONSourceSchema = z.object({
|
|
|
154
154
|
generateId: z.boolean().optional(),
|
|
155
155
|
promoteId: z.union([z.string(), z.record(z.string())]).optional(),
|
|
156
156
|
attribution: z.string().optional()
|
|
157
|
-
}).passthrough().
|
|
158
|
-
|
|
157
|
+
}).passthrough().superRefine((d, ctx) => {
|
|
158
|
+
if (!d.url && !d.data && !d.prefetchedData) {
|
|
159
|
+
ctx.addIssue({
|
|
160
|
+
code: z.ZodIssueCode.custom,
|
|
161
|
+
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.'
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (typeof d.data === "string" && /^(\.\.?|\/?src)\//.test(d.data)) {
|
|
166
|
+
ctx.addIssue({
|
|
167
|
+
code: z.ZodIssueCode.custom,
|
|
168
|
+
path: ["data"],
|
|
169
|
+
// Recommends `data: "/data/..."` rather than `url:` because the
|
|
170
|
+
// `url:` field is schema-validated with z.string().url(), which
|
|
171
|
+
// requires a fully-qualified URL (`https://...`) and rejects
|
|
172
|
+
// root-relative paths. MapLibre treats a string in `data:` as a
|
|
173
|
+
// URL and fetches it correctly. Until url: relaxes to accept
|
|
174
|
+
// root-relative paths (see follow-up), `data: "/path"` is the
|
|
175
|
+
// working pattern for files served from public/.
|
|
176
|
+
message: `GeoJSON source.data must be an inline GeoJSON object or a URL. "${d.data}" is a local source-directory path that won't resolve at runtime. Move the file to public/ and reference the public-served URL:
|
|
177
|
+
data: "/data/<filename>.geojson"
|
|
178
|
+
(\`url:\` requires a fully-qualified https:// URL and won't accept root-relative paths; use \`data:\` for runtime URLs to public assets.)`
|
|
179
|
+
});
|
|
180
|
+
}
|
|
159
181
|
});
|
|
160
182
|
var VectorSourceSchema = z.object({
|
|
161
183
|
type: z.literal("vector").describe("Source type"),
|
|
@@ -1022,6 +1044,95 @@ var YAMLParser = class {
|
|
|
1022
1044
|
};
|
|
1023
1045
|
}
|
|
1024
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Detect the document type of a YAML string and validate it against the
|
|
1049
|
+
* matching schema
|
|
1050
|
+
*
|
|
1051
|
+
* @param yaml - YAML string to parse (a map block, a scrollytelling block, or a root document)
|
|
1052
|
+
* @returns Discriminated result with the detected block type and the corresponding safeParse result
|
|
1053
|
+
*
|
|
1054
|
+
* @remarks
|
|
1055
|
+
* Dispatches on the document's top-level `type:` field:
|
|
1056
|
+
*
|
|
1057
|
+
* - `type: map` → {@link safeParseMapBlock}
|
|
1058
|
+
* - `type: scrollytelling` → {@link safeParseScrollytellingBlock}
|
|
1059
|
+
* - no `type:` but a `pages:` key → {@link safeParse} (root document)
|
|
1060
|
+
*
|
|
1061
|
+
* Any other `type:` value produces a failure result listing the valid
|
|
1062
|
+
* values, as does a document that has neither `type:` nor `pages:`.
|
|
1063
|
+
* This method never throws.
|
|
1064
|
+
*
|
|
1065
|
+
* @example
|
|
1066
|
+
* ```typescript
|
|
1067
|
+
* const { blockType, result } = YAMLParser.safeParseAny(yamlString);
|
|
1068
|
+
* if (!result.success) {
|
|
1069
|
+
* result.errors.forEach(err => console.error(`${err.path}: ${err.message}`));
|
|
1070
|
+
* } else if (blockType === 'map') {
|
|
1071
|
+
* renderMap(result.data);
|
|
1072
|
+
* }
|
|
1073
|
+
* ```
|
|
1074
|
+
*/
|
|
1075
|
+
static safeParseAny(yaml) {
|
|
1076
|
+
let parsed;
|
|
1077
|
+
try {
|
|
1078
|
+
parsed = parse(yaml);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
return {
|
|
1081
|
+
blockType: "unknown",
|
|
1082
|
+
result: {
|
|
1083
|
+
success: false,
|
|
1084
|
+
errors: [
|
|
1085
|
+
{
|
|
1086
|
+
path: "",
|
|
1087
|
+
message: `YAML syntax error: ${error instanceof Error ? error.message : String(error)}`
|
|
1088
|
+
}
|
|
1089
|
+
]
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
const doc = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1094
|
+
const type = doc?.type;
|
|
1095
|
+
if (type === "map") {
|
|
1096
|
+
return { blockType: "map", result: this.safeParseMapBlock(yaml) };
|
|
1097
|
+
}
|
|
1098
|
+
if (type === "scrollytelling") {
|
|
1099
|
+
return {
|
|
1100
|
+
blockType: "scrollytelling",
|
|
1101
|
+
result: this.safeParseScrollytellingBlock(yaml)
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
if (type !== void 0) {
|
|
1105
|
+
return {
|
|
1106
|
+
blockType: "unknown",
|
|
1107
|
+
result: {
|
|
1108
|
+
success: false,
|
|
1109
|
+
errors: [
|
|
1110
|
+
{
|
|
1111
|
+
path: "type",
|
|
1112
|
+
message: `Unknown block type: ${JSON.stringify(
|
|
1113
|
+
type
|
|
1114
|
+
)}. Expected one of: map, scrollytelling. Root documents omit "type" and use a top-level "pages:" array instead.`
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
if (doc !== null && "pages" in doc) {
|
|
1121
|
+
return { blockType: "root", result: this.safeParse(yaml) };
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
blockType: "unknown",
|
|
1125
|
+
result: {
|
|
1126
|
+
success: false,
|
|
1127
|
+
errors: [
|
|
1128
|
+
{
|
|
1129
|
+
path: "",
|
|
1130
|
+
message: 'Unable to determine document type. Expected a block with "type: map" or "type: scrollytelling", or a root document with a top-level "pages:" array.'
|
|
1131
|
+
}
|
|
1132
|
+
]
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1025
1136
|
/**
|
|
1026
1137
|
* Resolve $ref references to global layers and sources
|
|
1027
1138
|
*
|
|
@@ -1187,6 +1298,14 @@ var YAMLParser = class {
|
|
|
1187
1298
|
};
|
|
1188
1299
|
YAMLParser.parse.bind(YAMLParser);
|
|
1189
1300
|
YAMLParser.safeParse.bind(YAMLParser);
|
|
1301
|
+
YAMLParser.safeParseAny.bind(YAMLParser);
|
|
1302
|
+
var gl = maplibre.default ?? maplibre;
|
|
1303
|
+
var Map2 = gl.Map;
|
|
1304
|
+
var Popup = gl.Popup;
|
|
1305
|
+
var NavigationControl = gl.NavigationControl;
|
|
1306
|
+
var GeolocateControl = gl.GeolocateControl;
|
|
1307
|
+
var ScaleControl = gl.ScaleControl;
|
|
1308
|
+
var FullscreenControl = gl.FullscreenControl;
|
|
1190
1309
|
|
|
1191
1310
|
// src/data/memory-cache.ts
|
|
1192
1311
|
var MemoryCache = class _MemoryCache {
|
|
@@ -1712,9 +1831,17 @@ var DataFetcher = class _DataFetcher {
|
|
|
1712
1831
|
* Perform the actual HTTP fetch
|
|
1713
1832
|
*/
|
|
1714
1833
|
async performFetch(url, options) {
|
|
1715
|
-
const controller =
|
|
1716
|
-
|
|
1717
|
-
|
|
1834
|
+
const controller = new AbortController();
|
|
1835
|
+
const externalSignal = options.signal;
|
|
1836
|
+
const onExternalAbort = () => controller.abort(externalSignal?.reason);
|
|
1837
|
+
if (externalSignal) {
|
|
1838
|
+
if (externalSignal.aborted) {
|
|
1839
|
+
controller.abort(externalSignal.reason);
|
|
1840
|
+
} else {
|
|
1841
|
+
externalSignal.addEventListener("abort", onExternalAbort, {
|
|
1842
|
+
once: true
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1718
1845
|
}
|
|
1719
1846
|
const timeoutId = setTimeout(() => {
|
|
1720
1847
|
controller.abort();
|
|
@@ -1772,6 +1899,7 @@ var DataFetcher = class _DataFetcher {
|
|
|
1772
1899
|
return data;
|
|
1773
1900
|
} finally {
|
|
1774
1901
|
clearTimeout(timeoutId);
|
|
1902
|
+
externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
1775
1903
|
this.activeRequests.delete(url);
|
|
1776
1904
|
}
|
|
1777
1905
|
}
|
|
@@ -3553,9 +3681,6 @@ var LayerManager = class {
|
|
|
3553
3681
|
loadingManager;
|
|
3554
3682
|
sourceData;
|
|
3555
3683
|
layerToSource;
|
|
3556
|
-
// Legacy support (deprecated)
|
|
3557
|
-
refreshIntervals;
|
|
3558
|
-
abortControllers;
|
|
3559
3684
|
constructor(map, callbacks) {
|
|
3560
3685
|
this.map = map;
|
|
3561
3686
|
this.callbacks = callbacks || {};
|
|
@@ -3566,8 +3691,6 @@ var LayerManager = class {
|
|
|
3566
3691
|
this.loadingManager = new LoadingManager({ showUI: false });
|
|
3567
3692
|
this.sourceData = /* @__PURE__ */ new Map();
|
|
3568
3693
|
this.layerToSource = /* @__PURE__ */ new Map();
|
|
3569
|
-
this.refreshIntervals = /* @__PURE__ */ new Map();
|
|
3570
|
-
this.abortControllers = /* @__PURE__ */ new Map();
|
|
3571
3694
|
}
|
|
3572
3695
|
async addLayer(layer) {
|
|
3573
3696
|
const isSourceRef = typeof layer.source === "string";
|
|
@@ -3818,12 +3941,6 @@ var LayerManager = class {
|
|
|
3818
3941
|
this.pollingManager.stop(layerId);
|
|
3819
3942
|
this.streamManager.disconnect(layerId);
|
|
3820
3943
|
this.loadingManager.hideLoading(layerId);
|
|
3821
|
-
this.stopRefreshInterval(layerId);
|
|
3822
|
-
const controller = this.abortControllers.get(layerId);
|
|
3823
|
-
if (controller) {
|
|
3824
|
-
controller.abort();
|
|
3825
|
-
this.abortControllers.delete(layerId);
|
|
3826
|
-
}
|
|
3827
3944
|
if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
3828
3945
|
const sourceId = this.layerToSource.get(layerId) || `${layerId}-source`;
|
|
3829
3946
|
const isInlineSource = sourceId === `${layerId}-source`;
|
|
@@ -3846,61 +3963,12 @@ var LayerManager = class {
|
|
|
3846
3963
|
const source = this.map.getSource(sourceId);
|
|
3847
3964
|
if (source && source.setData) source.setData(data);
|
|
3848
3965
|
}
|
|
3849
|
-
/**
|
|
3850
|
-
* @deprecated Legacy refresh method - use PollingManager instead
|
|
3851
|
-
*/
|
|
3852
|
-
startRefreshInterval(layer) {
|
|
3853
|
-
if (typeof layer.source !== "object" || layer.source === null) {
|
|
3854
|
-
return;
|
|
3855
|
-
}
|
|
3856
|
-
const sourceObj = layer.source;
|
|
3857
|
-
if (sourceObj.type !== "geojson" || !sourceObj.url || !sourceObj.refreshInterval) {
|
|
3858
|
-
return;
|
|
3859
|
-
}
|
|
3860
|
-
const geojsonSource = layer.source;
|
|
3861
|
-
const interval = setInterval(async () => {
|
|
3862
|
-
const sourceId = `${layer.id}-source`;
|
|
3863
|
-
try {
|
|
3864
|
-
const cacheEnabled = geojsonSource.cache?.enabled ?? true;
|
|
3865
|
-
const cacheTTL = geojsonSource.cache?.ttl;
|
|
3866
|
-
const result = await this.dataFetcher.fetch(geojsonSource.url, {
|
|
3867
|
-
skipCache: !cacheEnabled,
|
|
3868
|
-
ttl: cacheTTL
|
|
3869
|
-
});
|
|
3870
|
-
const data = result.data;
|
|
3871
|
-
this.sourceData.set(sourceId, data);
|
|
3872
|
-
const source = this.map.getSource(sourceId);
|
|
3873
|
-
if (source?.setData) {
|
|
3874
|
-
source.setData(data);
|
|
3875
|
-
}
|
|
3876
|
-
this.callbacks.onDataLoaded?.(layer.id, data.features.length);
|
|
3877
|
-
} catch (error) {
|
|
3878
|
-
this.callbacks.onDataError?.(layer.id, error);
|
|
3879
|
-
}
|
|
3880
|
-
}, geojsonSource.refreshInterval);
|
|
3881
|
-
this.refreshIntervals.set(layer.id, interval);
|
|
3882
|
-
}
|
|
3883
|
-
stopRefreshInterval(layerId) {
|
|
3884
|
-
const interval = this.refreshIntervals.get(layerId);
|
|
3885
|
-
if (interval) {
|
|
3886
|
-
clearInterval(interval);
|
|
3887
|
-
this.refreshIntervals.delete(layerId);
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
clearAllIntervals() {
|
|
3891
|
-
for (const interval of this.refreshIntervals.values())
|
|
3892
|
-
clearInterval(interval);
|
|
3893
|
-
this.refreshIntervals.clear();
|
|
3894
|
-
}
|
|
3895
3966
|
destroy() {
|
|
3896
3967
|
this.pollingManager.destroy();
|
|
3897
3968
|
this.streamManager.destroy();
|
|
3898
3969
|
this.loadingManager.destroy();
|
|
3899
3970
|
this.sourceData.clear();
|
|
3900
3971
|
this.layerToSource.clear();
|
|
3901
|
-
this.clearAllIntervals();
|
|
3902
|
-
for (const controller of this.abortControllers.values()) controller.abort();
|
|
3903
|
-
this.abortControllers.clear();
|
|
3904
3972
|
}
|
|
3905
3973
|
};
|
|
3906
3974
|
|
|
@@ -4127,6 +4195,8 @@ var LegendBuilder = class {
|
|
|
4127
4195
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4128
4196
|
}
|
|
4129
4197
|
};
|
|
4198
|
+
|
|
4199
|
+
// src/renderer/controls-manager.ts
|
|
4130
4200
|
var ControlsManager = class {
|
|
4131
4201
|
map;
|
|
4132
4202
|
addedControls;
|
|
@@ -4191,10 +4261,18 @@ var MapRenderer = class {
|
|
|
4191
4261
|
controlsManager;
|
|
4192
4262
|
eventListeners;
|
|
4193
4263
|
isLoaded;
|
|
4264
|
+
containerEl;
|
|
4265
|
+
controlsAdded;
|
|
4266
|
+
legendBuilt;
|
|
4267
|
+
autoLegendContainer;
|
|
4194
4268
|
constructor(container, config, layers = [], options = {}, sources) {
|
|
4195
4269
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
4196
4270
|
this.isLoaded = false;
|
|
4197
|
-
this.
|
|
4271
|
+
this.containerEl = typeof container === "string" ? document.getElementById(container) : container;
|
|
4272
|
+
this.controlsAdded = false;
|
|
4273
|
+
this.legendBuilt = false;
|
|
4274
|
+
this.autoLegendContainer = null;
|
|
4275
|
+
this.map = new Map2({
|
|
4198
4276
|
...config,
|
|
4199
4277
|
container: typeof container === "string" ? container : container,
|
|
4200
4278
|
style: config.mapStyle,
|
|
@@ -4226,6 +4304,12 @@ var MapRenderer = class {
|
|
|
4226
4304
|
}
|
|
4227
4305
|
}
|
|
4228
4306
|
}
|
|
4307
|
+
if (options.controls && !this.controlsAdded) {
|
|
4308
|
+
this.addControls(options.controls);
|
|
4309
|
+
}
|
|
4310
|
+
if (options.legend && !this.legendBuilt) {
|
|
4311
|
+
this.buildLegend(this.createLegendContainer(options.legend), layers, options.legend);
|
|
4312
|
+
}
|
|
4229
4313
|
Promise.all(layers.map((layer) => this.addLayer(layer))).then(() => {
|
|
4230
4314
|
this.emit("load", void 0);
|
|
4231
4315
|
options.onLoad?.();
|
|
@@ -4279,16 +4363,43 @@ var MapRenderer = class {
|
|
|
4279
4363
|
}
|
|
4280
4364
|
/**
|
|
4281
4365
|
* Add controls to the map
|
|
4366
|
+
*
|
|
4367
|
+
* @remarks
|
|
4368
|
+
* Called automatically on map load when a `controls:` config was passed via
|
|
4369
|
+
* {@link MapRendererOptions}. Calling it manually marks controls as added so
|
|
4370
|
+
* the automatic invocation is skipped (no double-add).
|
|
4282
4371
|
*/
|
|
4283
4372
|
addControls(config) {
|
|
4373
|
+
this.controlsAdded = true;
|
|
4284
4374
|
this.controlsManager.addControls(config);
|
|
4285
4375
|
}
|
|
4286
4376
|
/**
|
|
4287
4377
|
* Build legend in container
|
|
4378
|
+
*
|
|
4379
|
+
* @remarks
|
|
4380
|
+
* Called automatically on map load when a `legend:` config was passed via
|
|
4381
|
+
* {@link MapRendererOptions}. Calling it manually marks the legend as built
|
|
4382
|
+
* so the automatic invocation is skipped (no double-build).
|
|
4288
4383
|
*/
|
|
4289
4384
|
buildLegend(container, layers, config) {
|
|
4385
|
+
this.legendBuilt = true;
|
|
4290
4386
|
this.legendBuilder.build(container, layers, config);
|
|
4291
4387
|
}
|
|
4388
|
+
/**
|
|
4389
|
+
* Create a positioned container inside the map element for the auto legend
|
|
4390
|
+
*/
|
|
4391
|
+
createLegendContainer(config) {
|
|
4392
|
+
const el = document.createElement("div");
|
|
4393
|
+
el.className = "ml-map-legend";
|
|
4394
|
+
const position = config.position ?? "top-left";
|
|
4395
|
+
el.style.position = "absolute";
|
|
4396
|
+
el.style.zIndex = "1";
|
|
4397
|
+
el.style[position.includes("top") ? "top" : "bottom"] = "10px";
|
|
4398
|
+
el.style[position.includes("left") ? "left" : "right"] = "10px";
|
|
4399
|
+
(this.containerEl ?? this.map.getContainer()).appendChild(el);
|
|
4400
|
+
this.autoLegendContainer = el;
|
|
4401
|
+
return el;
|
|
4402
|
+
}
|
|
4292
4403
|
/**
|
|
4293
4404
|
* Get the legend builder instance
|
|
4294
4405
|
*/
|
|
@@ -4331,6 +4442,8 @@ var MapRenderer = class {
|
|
|
4331
4442
|
this.eventHandler.destroy();
|
|
4332
4443
|
this.layerManager.destroy();
|
|
4333
4444
|
this.controlsManager.removeAllControls();
|
|
4445
|
+
this.autoLegendContainer?.remove();
|
|
4446
|
+
this.autoLegendContainer = null;
|
|
4334
4447
|
this.eventListeners.clear();
|
|
4335
4448
|
this.map.remove();
|
|
4336
4449
|
}
|
|
@@ -4514,6 +4627,15 @@ var MLMap = class extends HTMLElement {
|
|
|
4514
4627
|
* Render the map with the given configuration
|
|
4515
4628
|
*/
|
|
4516
4629
|
renderMap(mapBlock) {
|
|
4630
|
+
if (!mapBlock.config?.mapStyle) {
|
|
4631
|
+
this.handleError([
|
|
4632
|
+
{
|
|
4633
|
+
path: "config.mapStyle",
|
|
4634
|
+
message: 'mapStyle is required for standalone maps. Add it to your config, for example: mapStyle: "https://demotiles.maplibre.org/style.json" (inheriting a defaultMapStyle is a feature of the Astro builders, not the standalone <ml-map> element).'
|
|
4635
|
+
}
|
|
4636
|
+
]);
|
|
4637
|
+
return;
|
|
4638
|
+
}
|
|
4517
4639
|
if (this.renderer) {
|
|
4518
4640
|
this.renderer.destroy();
|
|
4519
4641
|
this.renderer = null;
|
|
@@ -4525,8 +4647,10 @@ var MLMap = class extends HTMLElement {
|
|
|
4525
4647
|
}
|
|
4526
4648
|
this.appendChild(this.mapContainer);
|
|
4527
4649
|
try {
|
|
4528
|
-
const { config, sources, layers = [] } = mapBlock;
|
|
4650
|
+
const { config, sources, layers = [], controls, legend } = mapBlock;
|
|
4529
4651
|
this.renderer = new MapRenderer(this.mapContainer, config, layers, {
|
|
4652
|
+
controls,
|
|
4653
|
+
legend,
|
|
4530
4654
|
onLoad: () => {
|
|
4531
4655
|
},
|
|
4532
4656
|
onError: (error) => {
|