@india-boundary-corrector/layer-configs 0.0.4 → 0.0.5

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.cjs CHANGED
@@ -22,6 +22,8 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  LayerConfig: () => LayerConfig,
24
24
  LayerConfigRegistry: () => LayerConfigRegistry,
25
+ LineStyle: () => LineStyle,
26
+ configsJson: () => configs_default,
25
27
  layerConfigs: () => layerConfigs
26
28
  });
27
29
  module.exports = __toCommonJS(index_exports);
@@ -151,7 +153,166 @@ function templateToTemplateRegex(template) {
151
153
  });
152
154
  return new RegExp("^" + pattern + "(\\?.*)?$", "i");
153
155
  }
156
+ function isValidColor(color) {
157
+ if (typeof color !== "string" || !color.trim()) return false;
158
+ if (typeof CSS !== "undefined" && CSS.supports) {
159
+ return CSS.supports("color", color);
160
+ }
161
+ const trimmed = color.trim().toLowerCase();
162
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(trimmed)) return true;
163
+ if (/^(rgb|hsl)a?\(/.test(trimmed)) return true;
164
+ if (/^[a-z]+$/.test(trimmed)) return true;
165
+ return false;
166
+ }
167
+ var LineStyle = class _LineStyle {
168
+ /**
169
+ * Validate a LineStyle configuration object.
170
+ * @param {Object} obj - The object to validate
171
+ * @param {number} [index] - Optional index for error messages (when validating in an array)
172
+ * @throws {Error} If validation fails
173
+ */
174
+ static validateJSON(obj, index) {
175
+ const prefix = index !== void 0 ? `lineStyles[${index}]` : "LineStyle";
176
+ if (!obj || typeof obj !== "object") {
177
+ throw new Error(`${prefix}: must be an object`);
178
+ }
179
+ if (!obj.color || typeof obj.color !== "string") {
180
+ throw new Error(`${prefix}: color must be a non-empty string`);
181
+ }
182
+ if (!isValidColor(obj.color)) {
183
+ throw new Error(`${prefix}: color "${obj.color}" is not a valid CSS color`);
184
+ }
185
+ if (obj.widthFraction !== void 0 && (typeof obj.widthFraction !== "number" || obj.widthFraction <= 0)) {
186
+ throw new Error(`${prefix}: widthFraction must be a positive number`);
187
+ }
188
+ if (obj.dashArray !== void 0 && !Array.isArray(obj.dashArray)) {
189
+ throw new Error(`${prefix}: dashArray must be an array`);
190
+ }
191
+ if (obj.alpha !== void 0 && (typeof obj.alpha !== "number" || obj.alpha < 0 || obj.alpha > 1)) {
192
+ throw new Error(`${prefix}: alpha must be a number between 0 and 1`);
193
+ }
194
+ if (obj.startZoom !== void 0 && (typeof obj.startZoom !== "number" || obj.startZoom < 0)) {
195
+ throw new Error(`${prefix}: startZoom must be a non-negative number`);
196
+ }
197
+ if (obj.endZoom !== void 0 && (typeof obj.endZoom !== "number" || obj.endZoom < 0)) {
198
+ throw new Error(`${prefix}: endZoom must be a non-negative number`);
199
+ }
200
+ }
201
+ /**
202
+ * @param {Object} options
203
+ * @param {string} options.color - CSS color string
204
+ * @param {number} [options.widthFraction=1.0] - Multiplier for base line width
205
+ * @param {number[]} [options.dashArray] - Dash pattern for dashed lines
206
+ * @param {number} [options.alpha=1.0] - Opacity (0-1)
207
+ * @param {number} [options.startZoom] - Minimum zoom level for this style
208
+ * @param {number} [options.endZoom=Infinity] - Maximum zoom level for this style
209
+ */
210
+ constructor({ color, widthFraction = 1, dashArray, alpha = 1, startZoom, endZoom = Infinity }) {
211
+ this.color = color;
212
+ this.widthFraction = widthFraction;
213
+ this.dashArray = dashArray;
214
+ this.alpha = alpha;
215
+ this.startZoom = startZoom;
216
+ this.endZoom = endZoom;
217
+ }
218
+ /**
219
+ * Check if this style is active at the given zoom level.
220
+ * @param {number} z - Zoom level
221
+ * @returns {boolean}
222
+ */
223
+ isActiveAtZoom(z) {
224
+ return z >= this.startZoom && z <= this.endZoom;
225
+ }
226
+ /**
227
+ * Serialize to plain object.
228
+ * @returns {Object}
229
+ */
230
+ toJSON() {
231
+ const obj = { color: this.color };
232
+ if (this.widthFraction !== 1) obj.widthFraction = this.widthFraction;
233
+ if (this.dashArray) obj.dashArray = this.dashArray;
234
+ if (this.alpha !== 1) obj.alpha = this.alpha;
235
+ if (this.startZoom !== void 0) obj.startZoom = this.startZoom;
236
+ if (this.endZoom !== Infinity) obj.endZoom = this.endZoom;
237
+ return obj;
238
+ }
239
+ /**
240
+ * Create from plain object with validation.
241
+ * @param {Object} obj
242
+ * @param {number} [defaultStartZoom=0] - Default startZoom if not specified
243
+ * @param {number} [index] - Optional index for error messages
244
+ * @returns {LineStyle}
245
+ */
246
+ static fromJSON(obj, defaultStartZoom = 0, index) {
247
+ _LineStyle.validateJSON(obj, index);
248
+ return new _LineStyle({
249
+ ...obj,
250
+ startZoom: obj.startZoom ?? defaultStartZoom
251
+ });
252
+ }
253
+ };
154
254
  var LayerConfig = class _LayerConfig {
255
+ /**
256
+ * Validate a LayerConfig configuration object.
257
+ * Also validates all lineStyles within the config.
258
+ * @param {Object} obj - The object to validate
259
+ * @throws {Error} If validation fails
260
+ */
261
+ static validateJSON(obj) {
262
+ if (!obj || typeof obj !== "object") {
263
+ throw new Error("LayerConfig: must be an object");
264
+ }
265
+ if (!obj.id || typeof obj.id !== "string") {
266
+ throw new Error("LayerConfig: id must be a non-empty string");
267
+ }
268
+ if (obj.id.includes("/")) {
269
+ throw new Error(`LayerConfig: id cannot contain slashes: "${obj.id}"`);
270
+ }
271
+ const id = obj.id;
272
+ if (obj.startZoom !== void 0 && (typeof obj.startZoom !== "number" || obj.startZoom < 0)) {
273
+ throw new Error(`LayerConfig "${id}": startZoom must be a non-negative number`);
274
+ }
275
+ if (obj.zoomThreshold !== void 0 && (typeof obj.zoomThreshold !== "number" || obj.zoomThreshold < 0)) {
276
+ throw new Error(`LayerConfig "${id}": zoomThreshold must be a non-negative number`);
277
+ }
278
+ const startZoom = obj.startZoom ?? 0;
279
+ const zoomThreshold = obj.zoomThreshold ?? 5;
280
+ if (startZoom > zoomThreshold) {
281
+ throw new Error(`LayerConfig "${id}": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);
282
+ }
283
+ if (obj.lineWidthStops !== void 0) {
284
+ if (!obj.lineWidthStops || typeof obj.lineWidthStops !== "object" || Array.isArray(obj.lineWidthStops)) {
285
+ throw new Error(`LayerConfig "${id}": lineWidthStops must be an object`);
286
+ }
287
+ const stopKeys = Object.keys(obj.lineWidthStops);
288
+ if (stopKeys.length < 2) {
289
+ throw new Error(`LayerConfig "${id}": lineWidthStops must have at least 2 entries`);
290
+ }
291
+ for (const key of stopKeys) {
292
+ const zoom = Number(key);
293
+ if (!Number.isInteger(zoom) || zoom < 0) {
294
+ throw new Error(`LayerConfig "${id}": lineWidthStops keys must be non-negative integers, got "${key}"`);
295
+ }
296
+ if (typeof obj.lineWidthStops[key] !== "number" || obj.lineWidthStops[key] <= 0) {
297
+ throw new Error(`LayerConfig "${id}": lineWidthStops values must be positive numbers`);
298
+ }
299
+ }
300
+ }
301
+ if (obj.lineStyles !== void 0) {
302
+ if (!Array.isArray(obj.lineStyles) || obj.lineStyles.length === 0) {
303
+ throw new Error(`LayerConfig "${id}": lineStyles must be a non-empty array`);
304
+ }
305
+ for (let i = 0; i < obj.lineStyles.length; i++) {
306
+ LineStyle.validateJSON(obj.lineStyles[i], i);
307
+ }
308
+ }
309
+ if (obj.delWidthFactor !== void 0 && (typeof obj.delWidthFactor !== "number" || obj.delWidthFactor <= 0)) {
310
+ throw new Error(`LayerConfig "${id}": delWidthFactor must be a positive number`);
311
+ }
312
+ if (obj.lineExtensionFactor !== void 0 && (typeof obj.lineExtensionFactor !== "number" || obj.lineExtensionFactor < 0)) {
313
+ throw new Error(`LayerConfig "${id}": lineExtensionFactor must be a non-negative number`);
314
+ }
315
+ }
155
316
  constructor({
156
317
  id,
157
318
  startZoom = 0,
@@ -174,66 +335,30 @@ var LayerConfig = class _LayerConfig {
174
335
  // Set to 0 to disable extension
175
336
  lineExtensionFactor = 0.5
176
337
  }) {
177
- if (!id || typeof id !== "string") {
178
- throw new Error("LayerConfig requires a non-empty string id");
179
- }
180
- if (id.includes("/")) {
181
- throw new Error(`LayerConfig id cannot contain slashes: "${id}"`);
182
- }
183
338
  this.id = id;
184
339
  this.startZoom = startZoom;
185
340
  this.zoomThreshold = zoomThreshold;
186
- if (startZoom > zoomThreshold) {
187
- throw new Error(`LayerConfig "${id}": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);
188
- }
189
341
  const templates = Array.isArray(tileUrlTemplates) ? tileUrlTemplates : tileUrlTemplates ? [tileUrlTemplates] : [];
190
342
  this.tileUrlTemplates = templates;
191
343
  this._compiledPatterns = templates.map((t) => templateToRegex(t));
192
344
  this._templatePatterns = templates.map((t) => templateToTemplateRegex(t));
193
- if (!lineWidthStops || typeof lineWidthStops !== "object" || Array.isArray(lineWidthStops)) {
194
- throw new Error(`LayerConfig "${id}": lineWidthStops must be an object`);
195
- }
196
- const stopKeys = Object.keys(lineWidthStops);
197
- if (stopKeys.length < 2) {
198
- throw new Error(`LayerConfig "${id}": lineWidthStops must have at least 2 entries`);
199
- }
200
- for (const key of stopKeys) {
201
- const zoom = Number(key);
202
- if (!Number.isInteger(zoom) || zoom < 0) {
203
- throw new Error(`LayerConfig "${id}": lineWidthStops keys must be non-negative integers, got "${key}"`);
204
- }
205
- if (typeof lineWidthStops[key] !== "number" || lineWidthStops[key] <= 0) {
206
- throw new Error(`LayerConfig "${id}": lineWidthStops values must be positive numbers`);
207
- }
208
- }
209
345
  this.lineWidthStops = lineWidthStops;
210
- if (!Array.isArray(lineStyles) || lineStyles.length === 0) {
211
- throw new Error(`LayerConfig "${id}": lineStyles must be a non-empty array`);
212
- }
213
- for (let i = 0; i < lineStyles.length; i++) {
214
- const style = lineStyles[i];
215
- if (!style || typeof style !== "object") {
216
- throw new Error(`LayerConfig "${id}": lineStyles[${i}] must be an object`);
217
- }
218
- if (!style.color || typeof style.color !== "string") {
219
- throw new Error(`LayerConfig "${id}": lineStyles[${i}].color must be a non-empty string`);
220
- }
221
- }
222
- this.lineStyles = lineStyles.map((style) => ({
223
- ...style,
224
- startZoom: style.startZoom ?? startZoom,
225
- endZoom: style.endZoom ?? Infinity
226
- }));
346
+ this.lineStyles = lineStyles.map(
347
+ (style) => style instanceof LineStyle ? style : new LineStyle({
348
+ ...style,
349
+ startZoom: style.startZoom ?? startZoom
350
+ })
351
+ );
227
352
  this.delWidthFactor = delWidthFactor;
228
353
  this.lineExtensionFactor = lineExtensionFactor;
229
354
  }
230
355
  /**
231
356
  * Get line styles active at a given zoom level
232
357
  * @param {number} z - Zoom level
233
- * @returns {Array<{color: string, widthFraction?: number, dashArray?: number[]}>}
358
+ * @returns {LineStyle[]}
234
359
  */
235
360
  getLineStylesForZoom(z) {
236
- return this.lineStyles.filter((style) => z >= style.startZoom && z <= style.endZoom);
361
+ return this.lineStyles.filter((style) => style.isActiveAtZoom(z));
237
362
  }
238
363
  /**
239
364
  * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)
@@ -296,17 +421,19 @@ var LayerConfig = class _LayerConfig {
296
421
  zoomThreshold: this.zoomThreshold,
297
422
  tileUrlTemplates: this.tileUrlTemplates,
298
423
  lineWidthStops: this.lineWidthStops,
299
- lineStyles: this.lineStyles,
424
+ lineStyles: this.lineStyles.map((s) => s.toJSON()),
300
425
  delWidthFactor: this.delWidthFactor,
301
426
  lineExtensionFactor: this.lineExtensionFactor
302
427
  };
303
428
  }
304
429
  /**
305
- * Create a LayerConfig from a plain object (e.g., from postMessage)
430
+ * Create a LayerConfig from a plain object with validation.
306
431
  * @param {Object} obj
307
432
  * @returns {LayerConfig}
433
+ * @throws {Error} If validation fails
308
434
  */
309
435
  static fromJSON(obj) {
436
+ _LayerConfig.validateJSON(obj);
310
437
  return new _LayerConfig(obj);
311
438
  }
312
439
  };
@@ -406,6 +533,8 @@ for (const configData of configs_default) {
406
533
  0 && (module.exports = {
407
534
  LayerConfig,
408
535
  LayerConfigRegistry,
536
+ LineStyle,
537
+ configsJson,
409
538
  layerConfigs
410
539
  });
411
540
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.js","../src/configs.json","../src/layerconfig.js"],"sourcesContent":["import configsJson from './configs.json' with { type: 'json' };\nimport { LayerConfig } from './layerconfig.js';\n\nexport { LayerConfig } from './layerconfig.js';\n\n/**\n * Layer configuration registry\n */\nexport class LayerConfigRegistry {\n constructor() {\n this.registry = {};\n }\n\n /**\n * Get a layer config by id\n */\n get(id) {\n return this.registry[id];\n }\n\n /**\n * Register a new layer config\n */\n register(config) {\n this.registry[config.id] = config;\n }\n\n /**\n * Remove a layer config by id\n */\n remove(id) {\n if (!this.registry[id]) return false;\n delete this.registry[id];\n return true;\n }\n\n /**\n * Detect layer config from tile URL templates (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n */\n detectFromTemplates(templates) {\n if (!templates || (Array.isArray(templates) && templates.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTemplate(templates)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Detect layer config from actual tile URLs (with numeric coordinates)\n * @param {string | string[]} urls - Single tile URL or array of tile URLs\n */\n detectFromTileUrls(urls) {\n if (!urls || (Array.isArray(urls) && urls.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTileUrl(urls)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Get all available layer config ids\n */\n getAvailableIds() {\n return Object.keys(this.registry);\n }\n\n /**\n * Create a new registry with all configs from this registry plus extra configs.\n * @param {LayerConfig[]} extraLayerConfigs - Additional configs to add\n * @returns {LayerConfigRegistry} A new registry with merged configs\n */\n createMergedRegistry(extraLayerConfigs) {\n const registry = new LayerConfigRegistry();\n \n for (const id of this.getAvailableIds()) {\n registry.register(this.get(id));\n }\n \n if (extraLayerConfigs && extraLayerConfigs.length > 0) {\n for (const config of extraLayerConfigs) {\n registry.register(config);\n }\n }\n \n return registry;\n }\n\n /**\n * Parse a tile URL into its components: layer config and coordinates\n * @param {string} url - Tile URL to parse\n * @returns {{ layerConfig: LayerConfig, coords: { z: number, x: number, y: number } } | null}\n */\n parseTileUrl(url) {\n // Check if URL matches any layer config\n const layerConfig = this.detectFromTileUrls([url]);\n if (!layerConfig) return null;\n \n // Extract tile coordinates using the matched config\n const coords = layerConfig.extractCoords(url);\n if (!coords) return null;\n \n return { layerConfig, coords };\n }\n}\n\n// Default registry with built-in configs loaded from JSON\nexport const layerConfigs = new LayerConfigRegistry();\nfor (const configData of configsJson) {\n layerConfigs.register(new LayerConfig(configData));\n}\n\n","[\n {\n \"id\": \"cartodb-dark\",\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_nolabels/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"10\": 2.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(40, 40, 40)\" }\n ]\n },\n {\n \"id\": \"cartodb-light\",\n \"startZoom\": 0,\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.25, \"2\": 0.25, \"3\": 0.5, \"4\": 0.75, \"5\": 1.0 },\n \"lineStyles\": [\n { \"color\": \"rgb(235, 214, 214)\", \"alpha\": 0.2, \"startZoom\": 6, \"widthFraction\": 5 },\n { \"color\": \"rgb(235, 214, 214)\" }\n ]\n },\n {\n \"id\": \"open-topo\",\n \"startZoom\": 4,\n \"zoomThreshold\": 4,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png\",\n \"https://tile.opentopomap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"4\": 0.75, \"5\": 1.0, \"6\": 1.25, \"7\": 1.5, \"8\": 1.75, \"9\": 1.25, \"10\": 1.25, \"13\": 1.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(83, 83, 83)\", \"startZoom\": 7, \"endZoom\": 8, \"alpha\": 0.4, \"widthFraction\": 4 },\n { \"color\": \"rgb(83, 83, 83)\", \"endZoom\": 8 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9, \"widthFraction\": 9, \"alpha\": 0.2 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9 }\n ]\n },\n {\n \"id\": \"osm-carto\",\n \"startZoom\": 1,\n \"zoomThreshold\": 1,\n \"tileUrlTemplates\": [\n \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\",\n \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"2\": 0.6, \"3\": 0.7, \"4\": 1.0, \"10\": 3.75 },\n \"lineStyles\": [\n { \"color\": \"rgb(200, 180, 200)\" },\n { \"color\": \"rgb(160, 120, 160)\", \"widthFraction\": 0.333, \"dashArray\": [30, 2, 8, 2] }\n ]\n },\n {\n \"id\": \"osm-hot\",\n \"startZoom\": 2,\n \"zoomThreshold\": 2,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"2\": 3.0, \"3\": 3.0, \"7\": 3.0, \"8\": 3.5, \"9\": 3.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(149, 175, 180)\" },\n { \"color\": \"rgb(89, 117, 123)\", \"widthFraction\": 0.33 }\n ]\n }\n]\n","/**\n * Convert a tile URL template to a regex pattern and capture group names.\n * Supports {z}, {x}, {y}, {s} (Leaflet subdomain), {a-c}/{1-4} (OpenLayers subdomain), and {r} (retina) placeholders.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {{ pattern: RegExp, groups: string[] }}\n */\nfunction templateToRegex(template) {\n const groups = [];\n // Escape regex special chars, then replace placeholders\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n // Don't escape our placeholders\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} etc (OpenLayers style subdomain)\n .replace(/\\{[a-z0-9]-[a-z0-9]\\}/gi, () => {\n groups.push('s');\n return '([a-z0-9]+)';\n })\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n groups.push(lowerName);\n if (lowerName === 's') {\n // Subdomain: single letter or short string\n return '([a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Retina suffix: optional @2x or similar\n return '(@\\\\d+x)?';\n }\n // z, x, y: numeric\n return '(\\\\d+)';\n });\n \n // Allow optional query string at end\n return { pattern: new RegExp('^' + pattern + '(\\\\?.*)?$', 'i'), groups };\n}\n\n/**\n * Convert a tile URL template to a regex that matches the template itself.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {RegExp}\n */\nfunction templateToTemplateRegex(template) {\n // Escape regex special chars, then replace placeholders with literal match\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} (OpenLayers style subdomain)\n .replace(/\\{([a-z0-9])-([a-z0-9])\\}/gi, (_, start, end) => `(\\\\{${start}-${end}\\\\}|\\\\{s\\\\}|[a-z0-9]+)`)\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n if (lowerName === 's') {\n // Match {s} placeholder, {a-c} style placeholder, or actual subdomain\n return '(\\\\{s\\\\}|\\\\{[a-z0-9]-[a-z0-9]\\\\}|[a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Match {r} placeholder or actual retina or empty\n return '(\\\\{r\\\\}|@\\\\d+x)?';\n }\n // Match {z}, {x}, {y} placeholders\n return `\\\\{${lowerName}\\\\}`;\n });\n \n // Allow optional query string at end\n return new RegExp('^' + pattern + '(\\\\?.*)?$', 'i');\n}\n\n/**\n * Base class for layer configurations\n * \n * Supports separate styling for NE (Natural Earth) data at low zoom levels\n * and OSM data at higher zoom levels, split by zoomThreshold.\n */\nexport class LayerConfig {\n constructor({\n id,\n startZoom = 0,\n zoomThreshold = 5,\n // Tile URL templates for matching (e.g., \"https://{s}.tile.example.com/{z}/{x}/{y}.png\")\n tileUrlTemplates = [],\n // Line width stops: map of zoom level to line width (at least 2 entries)\n // Note: interpolated/extrapolated line width is capped at a minimum of 0.5\n lineWidthStops = { 1: 0.5, 10: 2.5 },\n // Line styles array - each element describes a line to draw\n // { color: string, widthFraction?: number, dashArray?: number[], startZoom?: number, endZoom?: number }\n // Lines are drawn in array order. startZoom defaults to layerConfig startZoom, endZoom defaults to Infinity\n lineStyles = [{ color: 'green', widthFraction: 1.0 }],\n // Factor to multiply line width for deletion blur (default 1.5)\n // Higher values leave gaps where wiped lines meet existing lines\n // Lower values mean wiped lines show through\n delWidthFactor = 1.5,\n // Factor to extend add lines by (multiplied by deletion line width)\n // Helps cover gaps where deleted lines meet the new boundary\n // Set to 0 to disable extension\n lineExtensionFactor = 0.5,\n }) {\n if (!id || typeof id !== 'string') {\n throw new Error('LayerConfig requires a non-empty string id');\n }\n if (id.includes('/')) {\n throw new Error(`LayerConfig id cannot contain slashes: \"${id}\"`);\n }\n\n this.id = id;\n this.startZoom = startZoom;\n this.zoomThreshold = zoomThreshold;\n\n if (startZoom > zoomThreshold) {\n throw new Error(`LayerConfig \"${id}\": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);\n }\n\n // Normalize to array\n const templates = Array.isArray(tileUrlTemplates) ? tileUrlTemplates : \n (tileUrlTemplates ? [tileUrlTemplates] : []);\n this.tileUrlTemplates = templates;\n \n // Pre-compile regex patterns for matching tile URLs (with actual coords)\n this._compiledPatterns = templates.map(t => templateToRegex(t));\n \n // Pre-compile regex patterns for matching template URLs (with {z}/{x}/{y} placeholders)\n this._templatePatterns = templates.map(t => templateToTemplateRegex(t));\n\n // Validate lineWidthStops\n if (!lineWidthStops || typeof lineWidthStops !== 'object' || Array.isArray(lineWidthStops)) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must be an object`);\n }\n const stopKeys = Object.keys(lineWidthStops);\n if (stopKeys.length < 2) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must have at least 2 entries`);\n }\n for (const key of stopKeys) {\n const zoom = Number(key);\n if (!Number.isInteger(zoom) || zoom < 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops keys must be non-negative integers, got \"${key}\"`);\n }\n if (typeof lineWidthStops[key] !== 'number' || lineWidthStops[key] <= 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops values must be positive numbers`);\n }\n }\n this.lineWidthStops = lineWidthStops;\n\n // Validate lineStyles\n if (!Array.isArray(lineStyles) || lineStyles.length === 0) {\n throw new Error(`LayerConfig \"${id}\": lineStyles must be a non-empty array`);\n }\n for (let i = 0; i < lineStyles.length; i++) {\n const style = lineStyles[i];\n if (!style || typeof style !== 'object') {\n throw new Error(`LayerConfig \"${id}\": lineStyles[${i}] must be an object`);\n }\n if (!style.color || typeof style.color !== 'string') {\n throw new Error(`LayerConfig \"${id}\": lineStyles[${i}].color must be a non-empty string`);\n }\n }\n \n // Line styles - normalize startZoom/endZoom defaults\n this.lineStyles = lineStyles.map(style => ({\n ...style,\n startZoom: style.startZoom ?? startZoom,\n endZoom: style.endZoom ?? Infinity,\n }));\n \n // Deletion width factor\n this.delWidthFactor = delWidthFactor;\n \n // Line extension factor\n this.lineExtensionFactor = lineExtensionFactor;\n }\n\n /**\n * Get line styles active at a given zoom level\n * @param {number} z - Zoom level\n * @returns {Array<{color: string, widthFraction?: number, dashArray?: number[]}>}\n */\n getLineStylesForZoom(z) {\n return this.lineStyles.filter(style => z >= style.startZoom && z <= style.endZoom);\n }\n\n /**\n * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n * @returns {boolean}\n */\n matchTemplate(templates) {\n if (this._templatePatterns.length === 0) return false;\n \n const urls = Array.isArray(templates) ? templates : [templates];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._templatePatterns.some(pattern => pattern.test(url))\n );\n }\n\n /**\n * Check if this config matches the given tile URLs (with actual coordinates)\n * @param {string | string[]} tiles - Single tile URL or array of tile URLs\n * @returns {boolean}\n */\n matchTileUrl(tiles) {\n if (this._compiledPatterns.length === 0) return false;\n \n const urls = Array.isArray(tiles) ? tiles : [tiles];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._compiledPatterns.some(({ pattern }) => pattern.test(url))\n );\n }\n\n /**\n * Extract tile coordinates (z, x, y) from a URL using this config's templates\n * @param {string} url - Tile URL to extract coordinates from\n * @returns {{ z: number, x: number, y: number } | null}\n */\n extractCoords(url) {\n for (const { pattern, groups } of this._compiledPatterns) {\n const match = url.match(pattern);\n if (match) {\n const result = {};\n for (let i = 0; i < groups.length; i++) {\n const name = groups[i];\n const value = match[i + 1];\n if (name === 'z' || name === 'x' || name === 'y') {\n result[name] = parseInt(value, 10);\n }\n }\n if ('z' in result && 'x' in result && 'y' in result) {\n return { z: result.z, x: result.x, y: result.y };\n }\n }\n }\n return null;\n }\n\n /**\n * Serialize the config to a plain object for postMessage\n * @returns {Object}\n */\n toJSON() {\n return {\n id: this.id,\n startZoom: this.startZoom,\n zoomThreshold: this.zoomThreshold,\n tileUrlTemplates: this.tileUrlTemplates,\n lineWidthStops: this.lineWidthStops,\n lineStyles: this.lineStyles,\n delWidthFactor: this.delWidthFactor,\n lineExtensionFactor: this.lineExtensionFactor,\n };\n }\n\n /**\n * Create a LayerConfig from a plain object (e.g., from postMessage)\n * @param {Object} obj\n * @returns {LayerConfig}\n */\n static fromJSON(obj) {\n return new LayerConfig(obj);\n }\n}\n\nexport default LayerConfig;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA,EACE;AAAA,IACE,IAAM;AAAA,IACN,eAAiB;AAAA,IACjB,kBAAoB;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,gBAAkB,EAAE,KAAK,KAAK,MAAM,IAAI;AAAA,IACxC,YAAc;AAAA,MACZ,EAAE,OAAS,kBAAkB;AAAA,IAC/B;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAM;AAAA,IACN,WAAa;AAAA,IACb,eAAiB;AAAA,IACjB,kBAAoB;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,gBAAkB,EAAE,KAAK,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAM,KAAK,EAAI;AAAA,IACxE,YAAc;AAAA,MACZ,EAAE,OAAS,sBAAsB,OAAS,KAAK,WAAa,GAAG,eAAiB,EAAE;AAAA,MAClF,EAAE,OAAS,qBAAqB;AAAA,IAClC;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAM;AAAA,IACN,WAAa;AAAA,IACb,eAAiB;AAAA,IACjB,kBAAoB;AAAA,MAClB;AAAA,MACA;AAAA,IACF;AAAA,IACA,gBAAkB,EAAE,KAAK,MAAM,KAAK,GAAK,KAAK,MAAM,KAAK,KAAK,KAAK,MAAM,KAAK,MAAM,MAAM,MAAM,MAAM,IAAI;AAAA,IAC1G,YAAc;AAAA,MACZ,EAAE,OAAS,mBAAmB,WAAa,GAAG,SAAW,GAAG,OAAS,KAAK,eAAiB,EAAE;AAAA,MAC7F,EAAE,OAAS,mBAAmB,SAAW,EAAE;AAAA,MAC3C,EAAE,OAAS,qBAAqB,WAAa,GAAG,eAAiB,GAAG,OAAS,IAAI;AAAA,MACjF,EAAE,OAAS,qBAAqB,WAAa,EAAE;AAAA,IACjD;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAM;AAAA,IACN,WAAa;AAAA,IACb,eAAiB;AAAA,IACjB,kBAAoB;AAAA,MAClB;AAAA,MACA;AAAA,IACF;AAAA,IACA,gBAAkB,EAAE,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,GAAK,MAAM,KAAK;AAAA,IACvE,YAAc;AAAA,MACZ,EAAE,OAAS,qBAAqB;AAAA,MAChC,EAAE,OAAS,sBAAsB,eAAiB,OAAO,WAAa,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE;AAAA,IACtF;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAM;AAAA,IACN,WAAa;AAAA,IACb,eAAiB;AAAA,IACjB,kBAAoB;AAAA,MAClB;AAAA,IACF;AAAA,IACA,gBAAkB,EAAE,KAAK,GAAK,KAAK,GAAK,KAAK,GAAK,KAAK,KAAK,KAAK,IAAI;AAAA,IACrE,YAAc;AAAA,MACZ,EAAE,OAAS,qBAAqB;AAAA,MAChC,EAAE,OAAS,qBAAqB,eAAiB,KAAK;AAAA,IACxD;AAAA,EACF;AACF;;;AC9EA,SAAS,gBAAgB,UAAU;AACjC,QAAM,SAAS,CAAC;AAEhB,MAAI,UAAU,SACX,QAAQ,uBAAuB,CAAC,SAAS;AAExC,QAAI,SAAS,OAAO,SAAS,IAAK,QAAO;AACzC,WAAO,OAAO;AAAA,EAChB,CAAC,EAEA,QAAQ,eAAe,WAAW,EAClC,QAAQ,cAAc,WAAW,EAEjC,QAAQ,2BAA2B,MAAM;AACxC,WAAO,KAAK,GAAG;AACf,WAAO;AAAA,EACT,CAAC,EACA,QAAQ,qBAAqB,CAAC,GAAG,SAAS;AACzC,UAAM,YAAY,KAAK,YAAY;AACnC,WAAO,KAAK,SAAS;AACrB,QAAI,cAAc,KAAK;AAErB,aAAO;AAAA,IACT;AACA,QAAI,cAAc,KAAK;AAErB,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT,CAAC;AAGH,SAAO,EAAE,SAAS,IAAI,OAAO,MAAM,UAAU,aAAa,GAAG,GAAG,OAAO;AACzE;AAOA,SAAS,wBAAwB,UAAU;AAEzC,MAAI,UAAU,SACX,QAAQ,uBAAuB,CAAC,SAAS;AACxC,QAAI,SAAS,OAAO,SAAS,IAAK,QAAO;AACzC,WAAO,OAAO;AAAA,EAChB,CAAC,EAEA,QAAQ,eAAe,WAAW,EAClC,QAAQ,cAAc,WAAW,EAEjC,QAAQ,+BAA+B,CAAC,GAAG,OAAO,QAAQ,OAAO,KAAK,IAAI,GAAG,wBAAwB,EACrG,QAAQ,qBAAqB,CAAC,GAAG,SAAS;AACzC,UAAM,YAAY,KAAK,YAAY;AACnC,QAAI,cAAc,KAAK;AAErB,aAAO;AAAA,IACT;AACA,QAAI,cAAc,KAAK;AAErB,aAAO;AAAA,IACT;AAEA,WAAO,MAAM,SAAS;AAAA,EACxB,CAAC;AAGH,SAAO,IAAI,OAAO,MAAM,UAAU,aAAa,GAAG;AACpD;AAQO,IAAM,cAAN,MAAM,aAAY;AAAA,EACvB,YAAY;AAAA,IACV;AAAA,IACA,YAAY;AAAA,IACZ,gBAAgB;AAAA;AAAA,IAEhB,mBAAmB,CAAC;AAAA;AAAA;AAAA,IAGpB,iBAAiB,EAAE,GAAG,KAAK,IAAI,IAAI;AAAA;AAAA;AAAA;AAAA,IAInC,aAAa,CAAC,EAAE,OAAO,SAAS,eAAe,EAAI,CAAC;AAAA;AAAA;AAAA;AAAA,IAIpD,iBAAiB;AAAA;AAAA;AAAA;AAAA,IAIjB,sBAAsB;AAAA,EACxB,GAAG;AACD,QAAI,CAAC,MAAM,OAAO,OAAO,UAAU;AACjC,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AACA,QAAI,GAAG,SAAS,GAAG,GAAG;AACpB,YAAM,IAAI,MAAM,2CAA2C,EAAE,GAAG;AAAA,IAClE;AAEA,SAAK,KAAK;AACV,SAAK,YAAY;AACjB,SAAK,gBAAgB;AAErB,QAAI,YAAY,eAAe;AAC7B,YAAM,IAAI,MAAM,gBAAgB,EAAE,iBAAiB,SAAS,+BAA+B,aAAa,GAAG;AAAA,IAC7G;AAGA,UAAM,YAAY,MAAM,QAAQ,gBAAgB,IAAI,mBACjC,mBAAmB,CAAC,gBAAgB,IAAI,CAAC;AAC5D,SAAK,mBAAmB;AAGxB,SAAK,oBAAoB,UAAU,IAAI,OAAK,gBAAgB,CAAC,CAAC;AAG9D,SAAK,oBAAoB,UAAU,IAAI,OAAK,wBAAwB,CAAC,CAAC;AAGtE,QAAI,CAAC,kBAAkB,OAAO,mBAAmB,YAAY,MAAM,QAAQ,cAAc,GAAG;AAC1F,YAAM,IAAI,MAAM,gBAAgB,EAAE,qCAAqC;AAAA,IACzE;AACA,UAAM,WAAW,OAAO,KAAK,cAAc;AAC3C,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,IAAI,MAAM,gBAAgB,EAAE,gDAAgD;AAAA,IACpF;AACA,eAAW,OAAO,UAAU;AAC1B,YAAM,OAAO,OAAO,GAAG;AACvB,UAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,GAAG;AACvC,cAAM,IAAI,MAAM,gBAAgB,EAAE,8DAA8D,GAAG,GAAG;AAAA,MACxG;AACA,UAAI,OAAO,eAAe,GAAG,MAAM,YAAY,eAAe,GAAG,KAAK,GAAG;AACvE,cAAM,IAAI,MAAM,gBAAgB,EAAE,mDAAmD;AAAA,MACvF;AAAA,IACF;AACA,SAAK,iBAAiB;AAGtB,QAAI,CAAC,MAAM,QAAQ,UAAU,KAAK,WAAW,WAAW,GAAG;AACzD,YAAM,IAAI,MAAM,gBAAgB,EAAE,yCAAyC;AAAA,IAC7E;AACA,aAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,YAAM,QAAQ,WAAW,CAAC;AAC1B,UAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,cAAM,IAAI,MAAM,gBAAgB,EAAE,iBAAiB,CAAC,qBAAqB;AAAA,MAC3E;AACA,UAAI,CAAC,MAAM,SAAS,OAAO,MAAM,UAAU,UAAU;AACnD,cAAM,IAAI,MAAM,gBAAgB,EAAE,iBAAiB,CAAC,oCAAoC;AAAA,MAC1F;AAAA,IACF;AAGA,SAAK,aAAa,WAAW,IAAI,YAAU;AAAA,MACzC,GAAG;AAAA,MACH,WAAW,MAAM,aAAa;AAAA,MAC9B,SAAS,MAAM,WAAW;AAAA,IAC5B,EAAE;AAGF,SAAK,iBAAiB;AAGtB,SAAK,sBAAsB;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,qBAAqB,GAAG;AACtB,WAAO,KAAK,WAAW,OAAO,WAAS,KAAK,MAAM,aAAa,KAAK,MAAM,OAAO;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,WAAW;AACvB,QAAI,KAAK,kBAAkB,WAAW,EAAG,QAAO;AAEhD,UAAM,OAAO,MAAM,QAAQ,SAAS,IAAI,YAAY,CAAC,SAAS;AAC9D,QAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,WAAO,KAAK;AAAA,MAAK,SACf,KAAK,kBAAkB,KAAK,aAAW,QAAQ,KAAK,GAAG,CAAC;AAAA,IAC1D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAa,OAAO;AAClB,QAAI,KAAK,kBAAkB,WAAW,EAAG,QAAO;AAEhD,UAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAClD,QAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,WAAO,KAAK;AAAA,MAAK,SACf,KAAK,kBAAkB,KAAK,CAAC,EAAE,QAAQ,MAAM,QAAQ,KAAK,GAAG,CAAC;AAAA,IAChE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,KAAK;AACjB,eAAW,EAAE,SAAS,OAAO,KAAK,KAAK,mBAAmB;AACxD,YAAM,QAAQ,IAAI,MAAM,OAAO;AAC/B,UAAI,OAAO;AACT,cAAM,SAAS,CAAC;AAChB,iBAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,gBAAM,OAAO,OAAO,CAAC;AACrB,gBAAM,QAAQ,MAAM,IAAI,CAAC;AACzB,cAAI,SAAS,OAAO,SAAS,OAAO,SAAS,KAAK;AAChD,mBAAO,IAAI,IAAI,SAAS,OAAO,EAAE;AAAA,UACnC;AAAA,QACF;AACA,YAAI,OAAO,UAAU,OAAO,UAAU,OAAO,QAAQ;AACnD,iBAAO,EAAE,GAAG,OAAO,GAAG,GAAG,OAAO,GAAG,GAAG,OAAO,EAAE;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS;AACP,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,WAAW,KAAK;AAAA,MAChB,eAAe,KAAK;AAAA,MACpB,kBAAkB,KAAK;AAAA,MACvB,gBAAgB,KAAK;AAAA,MACrB,YAAY,KAAK;AAAA,MACjB,gBAAgB,KAAK;AAAA,MACrB,qBAAqB,KAAK;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,SAAS,KAAK;AACnB,WAAO,IAAI,aAAY,GAAG;AAAA,EAC5B;AACF;;;AFtQO,IAAM,sBAAN,MAAM,qBAAoB;AAAA,EAC/B,cAAc;AACZ,SAAK,WAAW,CAAC;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,IAAI;AACN,WAAO,KAAK,SAAS,EAAE;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,QAAQ;AACf,SAAK,SAAS,OAAO,EAAE,IAAI;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,IAAI;AACT,QAAI,CAAC,KAAK,SAAS,EAAE,EAAG,QAAO;AAC/B,WAAO,KAAK,SAAS,EAAE;AACvB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,oBAAoB,WAAW;AAC7B,QAAI,CAAC,aAAc,MAAM,QAAQ,SAAS,KAAK,UAAU,WAAW,EAAI,QAAO;AAE/E,eAAW,UAAU,OAAO,OAAO,KAAK,QAAQ,GAAG;AACjD,UAAI,OAAO,cAAc,SAAS,GAAG;AACnC,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,MAAM;AACvB,QAAI,CAAC,QAAS,MAAM,QAAQ,IAAI,KAAK,KAAK,WAAW,EAAI,QAAO;AAEhE,eAAW,UAAU,OAAO,OAAO,KAAK,QAAQ,GAAG;AACjD,UAAI,OAAO,aAAa,IAAI,GAAG;AAC7B,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAkB;AAChB,WAAO,OAAO,KAAK,KAAK,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,qBAAqB,mBAAmB;AACtC,UAAM,WAAW,IAAI,qBAAoB;AAEzC,eAAW,MAAM,KAAK,gBAAgB,GAAG;AACvC,eAAS,SAAS,KAAK,IAAI,EAAE,CAAC;AAAA,IAChC;AAEA,QAAI,qBAAqB,kBAAkB,SAAS,GAAG;AACrD,iBAAW,UAAU,mBAAmB;AACtC,iBAAS,SAAS,MAAM;AAAA,MAC1B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAa,KAAK;AAEhB,UAAM,cAAc,KAAK,mBAAmB,CAAC,GAAG,CAAC;AACjD,QAAI,CAAC,YAAa,QAAO;AAGzB,UAAM,SAAS,YAAY,cAAc,GAAG;AAC5C,QAAI,CAAC,OAAQ,QAAO;AAEpB,WAAO,EAAE,aAAa,OAAO;AAAA,EAC/B;AACF;AAGO,IAAM,eAAe,IAAI,oBAAoB;AACpD,WAAW,cAAc,iBAAa;AACpC,eAAa,SAAS,IAAI,YAAY,UAAU,CAAC;AACnD;","names":[]}
1
+ {"version":3,"sources":["../src/index.js","../src/configs.json","../src/layerconfig.js"],"sourcesContent":["import configsJson from './configs.json' with { type: 'json' };\nimport { LayerConfig, LineStyle } from './layerconfig.js';\n\nexport { LayerConfig, LineStyle } from './layerconfig.js';\n\n// Export raw configs for testing/inspection\nexport { configsJson };\n\n/**\n * Layer configuration registry\n */\nexport class LayerConfigRegistry {\n constructor() {\n this.registry = {};\n }\n\n /**\n * Get a layer config by id\n */\n get(id) {\n return this.registry[id];\n }\n\n /**\n * Register a new layer config\n */\n register(config) {\n this.registry[config.id] = config;\n }\n\n /**\n * Remove a layer config by id\n */\n remove(id) {\n if (!this.registry[id]) return false;\n delete this.registry[id];\n return true;\n }\n\n /**\n * Detect layer config from tile URL templates (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n */\n detectFromTemplates(templates) {\n if (!templates || (Array.isArray(templates) && templates.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTemplate(templates)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Detect layer config from actual tile URLs (with numeric coordinates)\n * @param {string | string[]} urls - Single tile URL or array of tile URLs\n */\n detectFromTileUrls(urls) {\n if (!urls || (Array.isArray(urls) && urls.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTileUrl(urls)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Get all available layer config ids\n */\n getAvailableIds() {\n return Object.keys(this.registry);\n }\n\n /**\n * Create a new registry with all configs from this registry plus extra configs.\n * @param {LayerConfig[]} extraLayerConfigs - Additional configs to add\n * @returns {LayerConfigRegistry} A new registry with merged configs\n */\n createMergedRegistry(extraLayerConfigs) {\n const registry = new LayerConfigRegistry();\n \n for (const id of this.getAvailableIds()) {\n registry.register(this.get(id));\n }\n \n if (extraLayerConfigs && extraLayerConfigs.length > 0) {\n for (const config of extraLayerConfigs) {\n registry.register(config);\n }\n }\n \n return registry;\n }\n\n /**\n * Parse a tile URL into its components: layer config and coordinates\n * @param {string} url - Tile URL to parse\n * @returns {{ layerConfig: LayerConfig, coords: { z: number, x: number, y: number } } | null}\n */\n parseTileUrl(url) {\n // Check if URL matches any layer config\n const layerConfig = this.detectFromTileUrls([url]);\n if (!layerConfig) return null;\n \n // Extract tile coordinates using the matched config\n const coords = layerConfig.extractCoords(url);\n if (!coords) return null;\n \n return { layerConfig, coords };\n }\n}\n\n// Default registry with built-in configs loaded from JSON\nexport const layerConfigs = new LayerConfigRegistry();\nfor (const configData of configsJson) {\n layerConfigs.register(new LayerConfig(configData));\n}\n\n","[\n {\n \"id\": \"cartodb-dark\",\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_nolabels/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"10\": 2.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(40, 40, 40)\" }\n ]\n },\n {\n \"id\": \"cartodb-light\",\n \"startZoom\": 0,\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.25, \"2\": 0.25, \"3\": 0.5, \"4\": 0.75, \"5\": 1.0 },\n \"lineStyles\": [\n { \"color\": \"rgb(235, 214, 214)\", \"alpha\": 0.2, \"startZoom\": 6, \"widthFraction\": 5 },\n { \"color\": \"rgb(235, 214, 214)\" }\n ]\n },\n {\n \"id\": \"open-topo\",\n \"startZoom\": 4,\n \"zoomThreshold\": 4,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png\",\n \"https://tile.opentopomap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"4\": 0.75, \"5\": 1.0, \"6\": 1.25, \"7\": 1.5, \"8\": 1.75, \"9\": 1.25, \"10\": 1.25, \"13\": 1.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(83, 83, 83)\", \"startZoom\": 7, \"endZoom\": 8, \"alpha\": 0.4, \"widthFraction\": 4 },\n { \"color\": \"rgb(83, 83, 83)\", \"endZoom\": 8 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9, \"widthFraction\": 9, \"alpha\": 0.2 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9 }\n ]\n },\n {\n \"id\": \"osm-carto\",\n \"startZoom\": 1,\n \"zoomThreshold\": 1,\n \"tileUrlTemplates\": [\n \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\",\n \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"2\": 0.6, \"3\": 0.7, \"4\": 1.0, \"10\": 3.75 },\n \"lineStyles\": [\n { \"color\": \"rgb(200, 180, 200)\" },\n { \"color\": \"rgb(160, 120, 160)\", \"widthFraction\": 0.333, \"dashArray\": [30, 2, 8, 2] }\n ]\n },\n {\n \"id\": \"osm-hot\",\n \"startZoom\": 2,\n \"zoomThreshold\": 2,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"2\": 3.0, \"3\": 3.0, \"7\": 3.0, \"8\": 3.5, \"9\": 3.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(149, 175, 180)\" },\n { \"color\": \"rgb(89, 117, 123)\", \"widthFraction\": 0.33 }\n ]\n }\n]\n","/**\n * Convert a tile URL template to a regex pattern and capture group names.\n * Supports {z}, {x}, {y}, {s} (Leaflet subdomain), {a-c}/{1-4} (OpenLayers subdomain), and {r} (retina) placeholders.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {{ pattern: RegExp, groups: string[] }}\n */\nfunction templateToRegex(template) {\n const groups = [];\n // Escape regex special chars, then replace placeholders\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n // Don't escape our placeholders\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} etc (OpenLayers style subdomain)\n .replace(/\\{[a-z0-9]-[a-z0-9]\\}/gi, () => {\n groups.push('s');\n return '([a-z0-9]+)';\n })\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n groups.push(lowerName);\n if (lowerName === 's') {\n // Subdomain: single letter or short string\n return '([a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Retina suffix: optional @2x or similar\n return '(@\\\\d+x)?';\n }\n // z, x, y: numeric\n return '(\\\\d+)';\n });\n \n // Allow optional query string at end\n return { pattern: new RegExp('^' + pattern + '(\\\\?.*)?$', 'i'), groups };\n}\n\n/**\n * Convert a tile URL template to a regex that matches the template itself.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {RegExp}\n */\nfunction templateToTemplateRegex(template) {\n // Escape regex special chars, then replace placeholders with literal match\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} (OpenLayers style subdomain)\n .replace(/\\{([a-z0-9])-([a-z0-9])\\}/gi, (_, start, end) => `(\\\\{${start}-${end}\\\\}|\\\\{s\\\\}|[a-z0-9]+)`)\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n if (lowerName === 's') {\n // Match {s} placeholder, {a-c} style placeholder, or actual subdomain\n return '(\\\\{s\\\\}|\\\\{[a-z0-9]-[a-z0-9]\\\\}|[a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Match {r} placeholder or actual retina or empty\n return '(\\\\{r\\\\}|@\\\\d+x)?';\n }\n // Match {z}, {x}, {y} placeholders\n return `\\\\{${lowerName}\\\\}`;\n });\n \n // Allow optional query string at end\n return new RegExp('^' + pattern + '(\\\\?.*)?$', 'i');\n}\n\n/**\n * Check if a string is a valid CSS color using the browser's CSS parser.\n * Falls back to a basic regex check in non-browser environments.\n * @param {string} color\n * @returns {boolean}\n */\nfunction isValidColor(color) {\n if (typeof color !== 'string' || !color.trim()) return false;\n \n // Use CSS.supports if available (modern browsers)\n if (typeof CSS !== 'undefined' && CSS.supports) {\n return CSS.supports('color', color);\n }\n \n // Fallback: basic validation for common formats\n const trimmed = color.trim().toLowerCase();\n // Hex colors\n if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(trimmed)) return true;\n // rgb/rgba/hsl/hsla functions\n if (/^(rgb|hsl)a?\\(/.test(trimmed)) return true;\n // Named colors (just check it's alphabetic)\n if (/^[a-z]+$/.test(trimmed)) return true;\n return false;\n}\n\n/**\n * Represents a line style for drawing boundaries.\n */\nexport class LineStyle {\n /**\n * Validate a LineStyle configuration object.\n * @param {Object} obj - The object to validate\n * @param {number} [index] - Optional index for error messages (when validating in an array)\n * @throws {Error} If validation fails\n */\n static validateJSON(obj, index) {\n const prefix = index !== undefined ? `lineStyles[${index}]` : 'LineStyle';\n \n if (!obj || typeof obj !== 'object') {\n throw new Error(`${prefix}: must be an object`);\n }\n \n if (!obj.color || typeof obj.color !== 'string') {\n throw new Error(`${prefix}: color must be a non-empty string`);\n }\n \n if (!isValidColor(obj.color)) {\n throw new Error(`${prefix}: color \"${obj.color}\" is not a valid CSS color`);\n }\n \n if (obj.widthFraction !== undefined && (typeof obj.widthFraction !== 'number' || obj.widthFraction <= 0)) {\n throw new Error(`${prefix}: widthFraction must be a positive number`);\n }\n \n if (obj.dashArray !== undefined && !Array.isArray(obj.dashArray)) {\n throw new Error(`${prefix}: dashArray must be an array`);\n }\n \n if (obj.alpha !== undefined && (typeof obj.alpha !== 'number' || obj.alpha < 0 || obj.alpha > 1)) {\n throw new Error(`${prefix}: alpha must be a number between 0 and 1`);\n }\n \n if (obj.startZoom !== undefined && (typeof obj.startZoom !== 'number' || obj.startZoom < 0)) {\n throw new Error(`${prefix}: startZoom must be a non-negative number`);\n }\n \n if (obj.endZoom !== undefined && (typeof obj.endZoom !== 'number' || obj.endZoom < 0)) {\n throw new Error(`${prefix}: endZoom must be a non-negative number`);\n }\n }\n\n /**\n * @param {Object} options\n * @param {string} options.color - CSS color string\n * @param {number} [options.widthFraction=1.0] - Multiplier for base line width\n * @param {number[]} [options.dashArray] - Dash pattern for dashed lines\n * @param {number} [options.alpha=1.0] - Opacity (0-1)\n * @param {number} [options.startZoom] - Minimum zoom level for this style\n * @param {number} [options.endZoom=Infinity] - Maximum zoom level for this style\n */\n constructor({ color, widthFraction = 1.0, dashArray, alpha = 1.0, startZoom, endZoom = Infinity }) {\n this.color = color;\n this.widthFraction = widthFraction;\n this.dashArray = dashArray;\n this.alpha = alpha;\n this.startZoom = startZoom;\n this.endZoom = endZoom;\n }\n\n /**\n * Check if this style is active at the given zoom level.\n * @param {number} z - Zoom level\n * @returns {boolean}\n */\n isActiveAtZoom(z) {\n return z >= this.startZoom && z <= this.endZoom;\n }\n\n /**\n * Serialize to plain object.\n * @returns {Object}\n */\n toJSON() {\n const obj = { color: this.color };\n if (this.widthFraction !== 1.0) obj.widthFraction = this.widthFraction;\n if (this.dashArray) obj.dashArray = this.dashArray;\n if (this.alpha !== 1.0) obj.alpha = this.alpha;\n if (this.startZoom !== undefined) obj.startZoom = this.startZoom;\n if (this.endZoom !== Infinity) obj.endZoom = this.endZoom;\n return obj;\n }\n\n /**\n * Create from plain object with validation.\n * @param {Object} obj\n * @param {number} [defaultStartZoom=0] - Default startZoom if not specified\n * @param {number} [index] - Optional index for error messages\n * @returns {LineStyle}\n */\n static fromJSON(obj, defaultStartZoom = 0, index) {\n LineStyle.validateJSON(obj, index);\n return new LineStyle({\n ...obj,\n startZoom: obj.startZoom ?? defaultStartZoom,\n });\n }\n}\n\n/**\n * Base class for layer configurations\n * \n * Supports separate styling for NE (Natural Earth) data at low zoom levels\n * and OSM data at higher zoom levels, split by zoomThreshold.\n */\nexport class LayerConfig {\n /**\n * Validate a LayerConfig configuration object.\n * Also validates all lineStyles within the config.\n * @param {Object} obj - The object to validate\n * @throws {Error} If validation fails\n */\n static validateJSON(obj) {\n if (!obj || typeof obj !== 'object') {\n throw new Error('LayerConfig: must be an object');\n }\n \n // Validate id (required)\n if (!obj.id || typeof obj.id !== 'string') {\n throw new Error('LayerConfig: id must be a non-empty string');\n }\n if (obj.id.includes('/')) {\n throw new Error(`LayerConfig: id cannot contain slashes: \"${obj.id}\"`);\n }\n \n const id = obj.id;\n \n // Validate zoom parameters (optional, but if provided must be valid)\n if (obj.startZoom !== undefined && (typeof obj.startZoom !== 'number' || obj.startZoom < 0)) {\n throw new Error(`LayerConfig \"${id}\": startZoom must be a non-negative number`);\n }\n if (obj.zoomThreshold !== undefined && (typeof obj.zoomThreshold !== 'number' || obj.zoomThreshold < 0)) {\n throw new Error(`LayerConfig \"${id}\": zoomThreshold must be a non-negative number`);\n }\n // Check startZoom <= zoomThreshold (using defaults if not provided)\n const startZoom = obj.startZoom ?? 0;\n const zoomThreshold = obj.zoomThreshold ?? 5;\n if (startZoom > zoomThreshold) {\n throw new Error(`LayerConfig \"${id}\": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);\n }\n \n // Validate lineWidthStops (optional, but if provided must be valid)\n if (obj.lineWidthStops !== undefined) {\n if (!obj.lineWidthStops || typeof obj.lineWidthStops !== 'object' || Array.isArray(obj.lineWidthStops)) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must be an object`);\n }\n const stopKeys = Object.keys(obj.lineWidthStops);\n if (stopKeys.length < 2) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must have at least 2 entries`);\n }\n for (const key of stopKeys) {\n const zoom = Number(key);\n if (!Number.isInteger(zoom) || zoom < 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops keys must be non-negative integers, got \"${key}\"`);\n }\n if (typeof obj.lineWidthStops[key] !== 'number' || obj.lineWidthStops[key] <= 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops values must be positive numbers`);\n }\n }\n }\n \n // Validate lineStyles (optional, but if provided must be valid)\n if (obj.lineStyles !== undefined) {\n if (!Array.isArray(obj.lineStyles) || obj.lineStyles.length === 0) {\n throw new Error(`LayerConfig \"${id}\": lineStyles must be a non-empty array`);\n }\n // Validate each lineStyle\n for (let i = 0; i < obj.lineStyles.length; i++) {\n LineStyle.validateJSON(obj.lineStyles[i], i);\n }\n }\n \n // Validate optional numeric fields\n if (obj.delWidthFactor !== undefined && (typeof obj.delWidthFactor !== 'number' || obj.delWidthFactor <= 0)) {\n throw new Error(`LayerConfig \"${id}\": delWidthFactor must be a positive number`);\n }\n if (obj.lineExtensionFactor !== undefined && (typeof obj.lineExtensionFactor !== 'number' || obj.lineExtensionFactor < 0)) {\n throw new Error(`LayerConfig \"${id}\": lineExtensionFactor must be a non-negative number`);\n }\n }\n\n constructor({\n id,\n startZoom = 0,\n zoomThreshold = 5,\n // Tile URL templates for matching (e.g., \"https://{s}.tile.example.com/{z}/{x}/{y}.png\")\n tileUrlTemplates = [],\n // Line width stops: map of zoom level to line width (at least 2 entries)\n // Note: interpolated/extrapolated line width is capped at a minimum of 0.5\n lineWidthStops = { 1: 0.5, 10: 2.5 },\n // Line styles array - each element describes a line to draw\n // { color: string, widthFraction?: number, dashArray?: number[], startZoom?: number, endZoom?: number }\n // Lines are drawn in array order. startZoom defaults to layerConfig startZoom, endZoom defaults to Infinity\n lineStyles = [{ color: 'green', widthFraction: 1.0 }],\n // Factor to multiply line width for deletion blur (default 1.5)\n // Higher values leave gaps where wiped lines meet existing lines\n // Lower values mean wiped lines show through\n delWidthFactor = 1.5,\n // Factor to extend add lines by (multiplied by deletion line width)\n // Helps cover gaps where deleted lines meet the new boundary\n // Set to 0 to disable extension\n lineExtensionFactor = 0.5,\n }) {\n this.id = id;\n this.startZoom = startZoom;\n this.zoomThreshold = zoomThreshold;\n\n // Normalize to array\n const templates = Array.isArray(tileUrlTemplates) ? tileUrlTemplates : \n (tileUrlTemplates ? [tileUrlTemplates] : []);\n this.tileUrlTemplates = templates;\n \n // Pre-compile regex patterns for matching tile URLs (with actual coords)\n this._compiledPatterns = templates.map(t => templateToRegex(t));\n \n // Pre-compile regex patterns for matching template URLs (with {z}/{x}/{y} placeholders)\n this._templatePatterns = templates.map(t => templateToTemplateRegex(t));\n\n this.lineWidthStops = lineWidthStops;\n \n // Convert to LineStyle instances with defaults\n this.lineStyles = lineStyles.map(style => \n style instanceof LineStyle ? style : new LineStyle({\n ...style,\n startZoom: style.startZoom ?? startZoom,\n })\n );\n \n // Deletion width factor\n this.delWidthFactor = delWidthFactor;\n \n // Line extension factor\n this.lineExtensionFactor = lineExtensionFactor;\n }\n\n /**\n * Get line styles active at a given zoom level\n * @param {number} z - Zoom level\n * @returns {LineStyle[]}\n */\n getLineStylesForZoom(z) {\n return this.lineStyles.filter(style => style.isActiveAtZoom(z));\n }\n\n /**\n * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n * @returns {boolean}\n */\n matchTemplate(templates) {\n if (this._templatePatterns.length === 0) return false;\n \n const urls = Array.isArray(templates) ? templates : [templates];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._templatePatterns.some(pattern => pattern.test(url))\n );\n }\n\n /**\n * Check if this config matches the given tile URLs (with actual coordinates)\n * @param {string | string[]} tiles - Single tile URL or array of tile URLs\n * @returns {boolean}\n */\n matchTileUrl(tiles) {\n if (this._compiledPatterns.length === 0) return false;\n \n const urls = Array.isArray(tiles) ? tiles : [tiles];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._compiledPatterns.some(({ pattern }) => pattern.test(url))\n );\n }\n\n /**\n * Extract tile coordinates (z, x, y) from a URL using this config's templates\n * @param {string} url - Tile URL to extract coordinates from\n * @returns {{ z: number, x: number, y: number } | null}\n */\n extractCoords(url) {\n for (const { pattern, groups } of this._compiledPatterns) {\n const match = url.match(pattern);\n if (match) {\n const result = {};\n for (let i = 0; i < groups.length; i++) {\n const name = groups[i];\n const value = match[i + 1];\n if (name === 'z' || name === 'x' || name === 'y') {\n result[name] = parseInt(value, 10);\n }\n }\n if ('z' in result && 'x' in result && 'y' in result) {\n return { z: result.z, x: result.x, y: result.y };\n }\n }\n }\n return null;\n }\n\n /**\n * Serialize the config to a plain object for postMessage\n * @returns {Object}\n */\n toJSON() {\n return {\n id: this.id,\n startZoom: this.startZoom,\n zoomThreshold: this.zoomThreshold,\n tileUrlTemplates: this.tileUrlTemplates,\n lineWidthStops: this.lineWidthStops,\n lineStyles: this.lineStyles.map(s => s.toJSON()),\n delWidthFactor: this.delWidthFactor,\n lineExtensionFactor: this.lineExtensionFactor,\n };\n }\n\n /**\n * Create a LayerConfig from a plain object with validation.\n * @param {Object} obj\n * @returns {LayerConfig}\n * @throws {Error} If validation fails\n */\n static fromJSON(obj) {\n LayerConfig.validateJSON(obj);\n return new LayerConfig(obj);\n }\n}\n\nexport default LayerConfig;\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -123,7 +123,166 @@ function templateToTemplateRegex(template) {
123
123
  });
124
124
  return new RegExp("^" + pattern + "(\\?.*)?$", "i");
125
125
  }
126
+ function isValidColor(color) {
127
+ if (typeof color !== "string" || !color.trim()) return false;
128
+ if (typeof CSS !== "undefined" && CSS.supports) {
129
+ return CSS.supports("color", color);
130
+ }
131
+ const trimmed = color.trim().toLowerCase();
132
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(trimmed)) return true;
133
+ if (/^(rgb|hsl)a?\(/.test(trimmed)) return true;
134
+ if (/^[a-z]+$/.test(trimmed)) return true;
135
+ return false;
136
+ }
137
+ var LineStyle = class _LineStyle {
138
+ /**
139
+ * Validate a LineStyle configuration object.
140
+ * @param {Object} obj - The object to validate
141
+ * @param {number} [index] - Optional index for error messages (when validating in an array)
142
+ * @throws {Error} If validation fails
143
+ */
144
+ static validateJSON(obj, index) {
145
+ const prefix = index !== void 0 ? `lineStyles[${index}]` : "LineStyle";
146
+ if (!obj || typeof obj !== "object") {
147
+ throw new Error(`${prefix}: must be an object`);
148
+ }
149
+ if (!obj.color || typeof obj.color !== "string") {
150
+ throw new Error(`${prefix}: color must be a non-empty string`);
151
+ }
152
+ if (!isValidColor(obj.color)) {
153
+ throw new Error(`${prefix}: color "${obj.color}" is not a valid CSS color`);
154
+ }
155
+ if (obj.widthFraction !== void 0 && (typeof obj.widthFraction !== "number" || obj.widthFraction <= 0)) {
156
+ throw new Error(`${prefix}: widthFraction must be a positive number`);
157
+ }
158
+ if (obj.dashArray !== void 0 && !Array.isArray(obj.dashArray)) {
159
+ throw new Error(`${prefix}: dashArray must be an array`);
160
+ }
161
+ if (obj.alpha !== void 0 && (typeof obj.alpha !== "number" || obj.alpha < 0 || obj.alpha > 1)) {
162
+ throw new Error(`${prefix}: alpha must be a number between 0 and 1`);
163
+ }
164
+ if (obj.startZoom !== void 0 && (typeof obj.startZoom !== "number" || obj.startZoom < 0)) {
165
+ throw new Error(`${prefix}: startZoom must be a non-negative number`);
166
+ }
167
+ if (obj.endZoom !== void 0 && (typeof obj.endZoom !== "number" || obj.endZoom < 0)) {
168
+ throw new Error(`${prefix}: endZoom must be a non-negative number`);
169
+ }
170
+ }
171
+ /**
172
+ * @param {Object} options
173
+ * @param {string} options.color - CSS color string
174
+ * @param {number} [options.widthFraction=1.0] - Multiplier for base line width
175
+ * @param {number[]} [options.dashArray] - Dash pattern for dashed lines
176
+ * @param {number} [options.alpha=1.0] - Opacity (0-1)
177
+ * @param {number} [options.startZoom] - Minimum zoom level for this style
178
+ * @param {number} [options.endZoom=Infinity] - Maximum zoom level for this style
179
+ */
180
+ constructor({ color, widthFraction = 1, dashArray, alpha = 1, startZoom, endZoom = Infinity }) {
181
+ this.color = color;
182
+ this.widthFraction = widthFraction;
183
+ this.dashArray = dashArray;
184
+ this.alpha = alpha;
185
+ this.startZoom = startZoom;
186
+ this.endZoom = endZoom;
187
+ }
188
+ /**
189
+ * Check if this style is active at the given zoom level.
190
+ * @param {number} z - Zoom level
191
+ * @returns {boolean}
192
+ */
193
+ isActiveAtZoom(z) {
194
+ return z >= this.startZoom && z <= this.endZoom;
195
+ }
196
+ /**
197
+ * Serialize to plain object.
198
+ * @returns {Object}
199
+ */
200
+ toJSON() {
201
+ const obj = { color: this.color };
202
+ if (this.widthFraction !== 1) obj.widthFraction = this.widthFraction;
203
+ if (this.dashArray) obj.dashArray = this.dashArray;
204
+ if (this.alpha !== 1) obj.alpha = this.alpha;
205
+ if (this.startZoom !== void 0) obj.startZoom = this.startZoom;
206
+ if (this.endZoom !== Infinity) obj.endZoom = this.endZoom;
207
+ return obj;
208
+ }
209
+ /**
210
+ * Create from plain object with validation.
211
+ * @param {Object} obj
212
+ * @param {number} [defaultStartZoom=0] - Default startZoom if not specified
213
+ * @param {number} [index] - Optional index for error messages
214
+ * @returns {LineStyle}
215
+ */
216
+ static fromJSON(obj, defaultStartZoom = 0, index) {
217
+ _LineStyle.validateJSON(obj, index);
218
+ return new _LineStyle({
219
+ ...obj,
220
+ startZoom: obj.startZoom ?? defaultStartZoom
221
+ });
222
+ }
223
+ };
126
224
  var LayerConfig = class _LayerConfig {
225
+ /**
226
+ * Validate a LayerConfig configuration object.
227
+ * Also validates all lineStyles within the config.
228
+ * @param {Object} obj - The object to validate
229
+ * @throws {Error} If validation fails
230
+ */
231
+ static validateJSON(obj) {
232
+ if (!obj || typeof obj !== "object") {
233
+ throw new Error("LayerConfig: must be an object");
234
+ }
235
+ if (!obj.id || typeof obj.id !== "string") {
236
+ throw new Error("LayerConfig: id must be a non-empty string");
237
+ }
238
+ if (obj.id.includes("/")) {
239
+ throw new Error(`LayerConfig: id cannot contain slashes: "${obj.id}"`);
240
+ }
241
+ const id = obj.id;
242
+ if (obj.startZoom !== void 0 && (typeof obj.startZoom !== "number" || obj.startZoom < 0)) {
243
+ throw new Error(`LayerConfig "${id}": startZoom must be a non-negative number`);
244
+ }
245
+ if (obj.zoomThreshold !== void 0 && (typeof obj.zoomThreshold !== "number" || obj.zoomThreshold < 0)) {
246
+ throw new Error(`LayerConfig "${id}": zoomThreshold must be a non-negative number`);
247
+ }
248
+ const startZoom = obj.startZoom ?? 0;
249
+ const zoomThreshold = obj.zoomThreshold ?? 5;
250
+ if (startZoom > zoomThreshold) {
251
+ throw new Error(`LayerConfig "${id}": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);
252
+ }
253
+ if (obj.lineWidthStops !== void 0) {
254
+ if (!obj.lineWidthStops || typeof obj.lineWidthStops !== "object" || Array.isArray(obj.lineWidthStops)) {
255
+ throw new Error(`LayerConfig "${id}": lineWidthStops must be an object`);
256
+ }
257
+ const stopKeys = Object.keys(obj.lineWidthStops);
258
+ if (stopKeys.length < 2) {
259
+ throw new Error(`LayerConfig "${id}": lineWidthStops must have at least 2 entries`);
260
+ }
261
+ for (const key of stopKeys) {
262
+ const zoom = Number(key);
263
+ if (!Number.isInteger(zoom) || zoom < 0) {
264
+ throw new Error(`LayerConfig "${id}": lineWidthStops keys must be non-negative integers, got "${key}"`);
265
+ }
266
+ if (typeof obj.lineWidthStops[key] !== "number" || obj.lineWidthStops[key] <= 0) {
267
+ throw new Error(`LayerConfig "${id}": lineWidthStops values must be positive numbers`);
268
+ }
269
+ }
270
+ }
271
+ if (obj.lineStyles !== void 0) {
272
+ if (!Array.isArray(obj.lineStyles) || obj.lineStyles.length === 0) {
273
+ throw new Error(`LayerConfig "${id}": lineStyles must be a non-empty array`);
274
+ }
275
+ for (let i = 0; i < obj.lineStyles.length; i++) {
276
+ LineStyle.validateJSON(obj.lineStyles[i], i);
277
+ }
278
+ }
279
+ if (obj.delWidthFactor !== void 0 && (typeof obj.delWidthFactor !== "number" || obj.delWidthFactor <= 0)) {
280
+ throw new Error(`LayerConfig "${id}": delWidthFactor must be a positive number`);
281
+ }
282
+ if (obj.lineExtensionFactor !== void 0 && (typeof obj.lineExtensionFactor !== "number" || obj.lineExtensionFactor < 0)) {
283
+ throw new Error(`LayerConfig "${id}": lineExtensionFactor must be a non-negative number`);
284
+ }
285
+ }
127
286
  constructor({
128
287
  id,
129
288
  startZoom = 0,
@@ -146,66 +305,30 @@ var LayerConfig = class _LayerConfig {
146
305
  // Set to 0 to disable extension
147
306
  lineExtensionFactor = 0.5
148
307
  }) {
149
- if (!id || typeof id !== "string") {
150
- throw new Error("LayerConfig requires a non-empty string id");
151
- }
152
- if (id.includes("/")) {
153
- throw new Error(`LayerConfig id cannot contain slashes: "${id}"`);
154
- }
155
308
  this.id = id;
156
309
  this.startZoom = startZoom;
157
310
  this.zoomThreshold = zoomThreshold;
158
- if (startZoom > zoomThreshold) {
159
- throw new Error(`LayerConfig "${id}": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);
160
- }
161
311
  const templates = Array.isArray(tileUrlTemplates) ? tileUrlTemplates : tileUrlTemplates ? [tileUrlTemplates] : [];
162
312
  this.tileUrlTemplates = templates;
163
313
  this._compiledPatterns = templates.map((t) => templateToRegex(t));
164
314
  this._templatePatterns = templates.map((t) => templateToTemplateRegex(t));
165
- if (!lineWidthStops || typeof lineWidthStops !== "object" || Array.isArray(lineWidthStops)) {
166
- throw new Error(`LayerConfig "${id}": lineWidthStops must be an object`);
167
- }
168
- const stopKeys = Object.keys(lineWidthStops);
169
- if (stopKeys.length < 2) {
170
- throw new Error(`LayerConfig "${id}": lineWidthStops must have at least 2 entries`);
171
- }
172
- for (const key of stopKeys) {
173
- const zoom = Number(key);
174
- if (!Number.isInteger(zoom) || zoom < 0) {
175
- throw new Error(`LayerConfig "${id}": lineWidthStops keys must be non-negative integers, got "${key}"`);
176
- }
177
- if (typeof lineWidthStops[key] !== "number" || lineWidthStops[key] <= 0) {
178
- throw new Error(`LayerConfig "${id}": lineWidthStops values must be positive numbers`);
179
- }
180
- }
181
315
  this.lineWidthStops = lineWidthStops;
182
- if (!Array.isArray(lineStyles) || lineStyles.length === 0) {
183
- throw new Error(`LayerConfig "${id}": lineStyles must be a non-empty array`);
184
- }
185
- for (let i = 0; i < lineStyles.length; i++) {
186
- const style = lineStyles[i];
187
- if (!style || typeof style !== "object") {
188
- throw new Error(`LayerConfig "${id}": lineStyles[${i}] must be an object`);
189
- }
190
- if (!style.color || typeof style.color !== "string") {
191
- throw new Error(`LayerConfig "${id}": lineStyles[${i}].color must be a non-empty string`);
192
- }
193
- }
194
- this.lineStyles = lineStyles.map((style) => ({
195
- ...style,
196
- startZoom: style.startZoom ?? startZoom,
197
- endZoom: style.endZoom ?? Infinity
198
- }));
316
+ this.lineStyles = lineStyles.map(
317
+ (style) => style instanceof LineStyle ? style : new LineStyle({
318
+ ...style,
319
+ startZoom: style.startZoom ?? startZoom
320
+ })
321
+ );
199
322
  this.delWidthFactor = delWidthFactor;
200
323
  this.lineExtensionFactor = lineExtensionFactor;
201
324
  }
202
325
  /**
203
326
  * Get line styles active at a given zoom level
204
327
  * @param {number} z - Zoom level
205
- * @returns {Array<{color: string, widthFraction?: number, dashArray?: number[]}>}
328
+ * @returns {LineStyle[]}
206
329
  */
207
330
  getLineStylesForZoom(z) {
208
- return this.lineStyles.filter((style) => z >= style.startZoom && z <= style.endZoom);
331
+ return this.lineStyles.filter((style) => style.isActiveAtZoom(z));
209
332
  }
210
333
  /**
211
334
  * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)
@@ -268,17 +391,19 @@ var LayerConfig = class _LayerConfig {
268
391
  zoomThreshold: this.zoomThreshold,
269
392
  tileUrlTemplates: this.tileUrlTemplates,
270
393
  lineWidthStops: this.lineWidthStops,
271
- lineStyles: this.lineStyles,
394
+ lineStyles: this.lineStyles.map((s) => s.toJSON()),
272
395
  delWidthFactor: this.delWidthFactor,
273
396
  lineExtensionFactor: this.lineExtensionFactor
274
397
  };
275
398
  }
276
399
  /**
277
- * Create a LayerConfig from a plain object (e.g., from postMessage)
400
+ * Create a LayerConfig from a plain object with validation.
278
401
  * @param {Object} obj
279
402
  * @returns {LayerConfig}
403
+ * @throws {Error} If validation fails
280
404
  */
281
405
  static fromJSON(obj) {
406
+ _LayerConfig.validateJSON(obj);
282
407
  return new _LayerConfig(obj);
283
408
  }
284
409
  };
@@ -377,6 +502,8 @@ for (const configData of configs_default) {
377
502
  export {
378
503
  LayerConfig,
379
504
  LayerConfigRegistry,
505
+ LineStyle,
506
+ configs_default as configsJson,
380
507
  layerConfigs
381
508
  };
382
509
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/configs.json","../src/layerconfig.js","../src/index.js"],"sourcesContent":["[\n {\n \"id\": \"cartodb-dark\",\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_nolabels/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"10\": 2.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(40, 40, 40)\" }\n ]\n },\n {\n \"id\": \"cartodb-light\",\n \"startZoom\": 0,\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.25, \"2\": 0.25, \"3\": 0.5, \"4\": 0.75, \"5\": 1.0 },\n \"lineStyles\": [\n { \"color\": \"rgb(235, 214, 214)\", \"alpha\": 0.2, \"startZoom\": 6, \"widthFraction\": 5 },\n { \"color\": \"rgb(235, 214, 214)\" }\n ]\n },\n {\n \"id\": \"open-topo\",\n \"startZoom\": 4,\n \"zoomThreshold\": 4,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png\",\n \"https://tile.opentopomap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"4\": 0.75, \"5\": 1.0, \"6\": 1.25, \"7\": 1.5, \"8\": 1.75, \"9\": 1.25, \"10\": 1.25, \"13\": 1.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(83, 83, 83)\", \"startZoom\": 7, \"endZoom\": 8, \"alpha\": 0.4, \"widthFraction\": 4 },\n { \"color\": \"rgb(83, 83, 83)\", \"endZoom\": 8 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9, \"widthFraction\": 9, \"alpha\": 0.2 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9 }\n ]\n },\n {\n \"id\": \"osm-carto\",\n \"startZoom\": 1,\n \"zoomThreshold\": 1,\n \"tileUrlTemplates\": [\n \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\",\n \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"2\": 0.6, \"3\": 0.7, \"4\": 1.0, \"10\": 3.75 },\n \"lineStyles\": [\n { \"color\": \"rgb(200, 180, 200)\" },\n { \"color\": \"rgb(160, 120, 160)\", \"widthFraction\": 0.333, \"dashArray\": [30, 2, 8, 2] }\n ]\n },\n {\n \"id\": \"osm-hot\",\n \"startZoom\": 2,\n \"zoomThreshold\": 2,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"2\": 3.0, \"3\": 3.0, \"7\": 3.0, \"8\": 3.5, \"9\": 3.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(149, 175, 180)\" },\n { \"color\": \"rgb(89, 117, 123)\", \"widthFraction\": 0.33 }\n ]\n }\n]\n","/**\n * Convert a tile URL template to a regex pattern and capture group names.\n * Supports {z}, {x}, {y}, {s} (Leaflet subdomain), {a-c}/{1-4} (OpenLayers subdomain), and {r} (retina) placeholders.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {{ pattern: RegExp, groups: string[] }}\n */\nfunction templateToRegex(template) {\n const groups = [];\n // Escape regex special chars, then replace placeholders\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n // Don't escape our placeholders\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} etc (OpenLayers style subdomain)\n .replace(/\\{[a-z0-9]-[a-z0-9]\\}/gi, () => {\n groups.push('s');\n return '([a-z0-9]+)';\n })\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n groups.push(lowerName);\n if (lowerName === 's') {\n // Subdomain: single letter or short string\n return '([a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Retina suffix: optional @2x or similar\n return '(@\\\\d+x)?';\n }\n // z, x, y: numeric\n return '(\\\\d+)';\n });\n \n // Allow optional query string at end\n return { pattern: new RegExp('^' + pattern + '(\\\\?.*)?$', 'i'), groups };\n}\n\n/**\n * Convert a tile URL template to a regex that matches the template itself.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {RegExp}\n */\nfunction templateToTemplateRegex(template) {\n // Escape regex special chars, then replace placeholders with literal match\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} (OpenLayers style subdomain)\n .replace(/\\{([a-z0-9])-([a-z0-9])\\}/gi, (_, start, end) => `(\\\\{${start}-${end}\\\\}|\\\\{s\\\\}|[a-z0-9]+)`)\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n if (lowerName === 's') {\n // Match {s} placeholder, {a-c} style placeholder, or actual subdomain\n return '(\\\\{s\\\\}|\\\\{[a-z0-9]-[a-z0-9]\\\\}|[a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Match {r} placeholder or actual retina or empty\n return '(\\\\{r\\\\}|@\\\\d+x)?';\n }\n // Match {z}, {x}, {y} placeholders\n return `\\\\{${lowerName}\\\\}`;\n });\n \n // Allow optional query string at end\n return new RegExp('^' + pattern + '(\\\\?.*)?$', 'i');\n}\n\n/**\n * Base class for layer configurations\n * \n * Supports separate styling for NE (Natural Earth) data at low zoom levels\n * and OSM data at higher zoom levels, split by zoomThreshold.\n */\nexport class LayerConfig {\n constructor({\n id,\n startZoom = 0,\n zoomThreshold = 5,\n // Tile URL templates for matching (e.g., \"https://{s}.tile.example.com/{z}/{x}/{y}.png\")\n tileUrlTemplates = [],\n // Line width stops: map of zoom level to line width (at least 2 entries)\n // Note: interpolated/extrapolated line width is capped at a minimum of 0.5\n lineWidthStops = { 1: 0.5, 10: 2.5 },\n // Line styles array - each element describes a line to draw\n // { color: string, widthFraction?: number, dashArray?: number[], startZoom?: number, endZoom?: number }\n // Lines are drawn in array order. startZoom defaults to layerConfig startZoom, endZoom defaults to Infinity\n lineStyles = [{ color: 'green', widthFraction: 1.0 }],\n // Factor to multiply line width for deletion blur (default 1.5)\n // Higher values leave gaps where wiped lines meet existing lines\n // Lower values mean wiped lines show through\n delWidthFactor = 1.5,\n // Factor to extend add lines by (multiplied by deletion line width)\n // Helps cover gaps where deleted lines meet the new boundary\n // Set to 0 to disable extension\n lineExtensionFactor = 0.5,\n }) {\n if (!id || typeof id !== 'string') {\n throw new Error('LayerConfig requires a non-empty string id');\n }\n if (id.includes('/')) {\n throw new Error(`LayerConfig id cannot contain slashes: \"${id}\"`);\n }\n\n this.id = id;\n this.startZoom = startZoom;\n this.zoomThreshold = zoomThreshold;\n\n if (startZoom > zoomThreshold) {\n throw new Error(`LayerConfig \"${id}\": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);\n }\n\n // Normalize to array\n const templates = Array.isArray(tileUrlTemplates) ? tileUrlTemplates : \n (tileUrlTemplates ? [tileUrlTemplates] : []);\n this.tileUrlTemplates = templates;\n \n // Pre-compile regex patterns for matching tile URLs (with actual coords)\n this._compiledPatterns = templates.map(t => templateToRegex(t));\n \n // Pre-compile regex patterns for matching template URLs (with {z}/{x}/{y} placeholders)\n this._templatePatterns = templates.map(t => templateToTemplateRegex(t));\n\n // Validate lineWidthStops\n if (!lineWidthStops || typeof lineWidthStops !== 'object' || Array.isArray(lineWidthStops)) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must be an object`);\n }\n const stopKeys = Object.keys(lineWidthStops);\n if (stopKeys.length < 2) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must have at least 2 entries`);\n }\n for (const key of stopKeys) {\n const zoom = Number(key);\n if (!Number.isInteger(zoom) || zoom < 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops keys must be non-negative integers, got \"${key}\"`);\n }\n if (typeof lineWidthStops[key] !== 'number' || lineWidthStops[key] <= 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops values must be positive numbers`);\n }\n }\n this.lineWidthStops = lineWidthStops;\n\n // Validate lineStyles\n if (!Array.isArray(lineStyles) || lineStyles.length === 0) {\n throw new Error(`LayerConfig \"${id}\": lineStyles must be a non-empty array`);\n }\n for (let i = 0; i < lineStyles.length; i++) {\n const style = lineStyles[i];\n if (!style || typeof style !== 'object') {\n throw new Error(`LayerConfig \"${id}\": lineStyles[${i}] must be an object`);\n }\n if (!style.color || typeof style.color !== 'string') {\n throw new Error(`LayerConfig \"${id}\": lineStyles[${i}].color must be a non-empty string`);\n }\n }\n \n // Line styles - normalize startZoom/endZoom defaults\n this.lineStyles = lineStyles.map(style => ({\n ...style,\n startZoom: style.startZoom ?? startZoom,\n endZoom: style.endZoom ?? Infinity,\n }));\n \n // Deletion width factor\n this.delWidthFactor = delWidthFactor;\n \n // Line extension factor\n this.lineExtensionFactor = lineExtensionFactor;\n }\n\n /**\n * Get line styles active at a given zoom level\n * @param {number} z - Zoom level\n * @returns {Array<{color: string, widthFraction?: number, dashArray?: number[]}>}\n */\n getLineStylesForZoom(z) {\n return this.lineStyles.filter(style => z >= style.startZoom && z <= style.endZoom);\n }\n\n /**\n * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n * @returns {boolean}\n */\n matchTemplate(templates) {\n if (this._templatePatterns.length === 0) return false;\n \n const urls = Array.isArray(templates) ? templates : [templates];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._templatePatterns.some(pattern => pattern.test(url))\n );\n }\n\n /**\n * Check if this config matches the given tile URLs (with actual coordinates)\n * @param {string | string[]} tiles - Single tile URL or array of tile URLs\n * @returns {boolean}\n */\n matchTileUrl(tiles) {\n if (this._compiledPatterns.length === 0) return false;\n \n const urls = Array.isArray(tiles) ? tiles : [tiles];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._compiledPatterns.some(({ pattern }) => pattern.test(url))\n );\n }\n\n /**\n * Extract tile coordinates (z, x, y) from a URL using this config's templates\n * @param {string} url - Tile URL to extract coordinates from\n * @returns {{ z: number, x: number, y: number } | null}\n */\n extractCoords(url) {\n for (const { pattern, groups } of this._compiledPatterns) {\n const match = url.match(pattern);\n if (match) {\n const result = {};\n for (let i = 0; i < groups.length; i++) {\n const name = groups[i];\n const value = match[i + 1];\n if (name === 'z' || name === 'x' || name === 'y') {\n result[name] = parseInt(value, 10);\n }\n }\n if ('z' in result && 'x' in result && 'y' in result) {\n return { z: result.z, x: result.x, y: result.y };\n }\n }\n }\n return null;\n }\n\n /**\n * Serialize the config to a plain object for postMessage\n * @returns {Object}\n */\n toJSON() {\n return {\n id: this.id,\n startZoom: this.startZoom,\n zoomThreshold: this.zoomThreshold,\n tileUrlTemplates: this.tileUrlTemplates,\n lineWidthStops: this.lineWidthStops,\n lineStyles: this.lineStyles,\n delWidthFactor: this.delWidthFactor,\n lineExtensionFactor: this.lineExtensionFactor,\n };\n }\n\n /**\n * Create a LayerConfig from a plain object (e.g., from postMessage)\n * @param {Object} obj\n * @returns {LayerConfig}\n */\n static fromJSON(obj) {\n return new LayerConfig(obj);\n }\n}\n\nexport default LayerConfig;\n","import configsJson from './configs.json' with { type: 'json' };\nimport { LayerConfig } from './layerconfig.js';\n\nexport { LayerConfig } from './layerconfig.js';\n\n/**\n * Layer configuration registry\n */\nexport class LayerConfigRegistry {\n constructor() {\n this.registry = {};\n }\n\n /**\n * Get a layer config by id\n */\n get(id) {\n return this.registry[id];\n }\n\n /**\n * Register a new layer config\n */\n register(config) {\n this.registry[config.id] = config;\n }\n\n /**\n * Remove a layer config by id\n */\n remove(id) {\n if (!this.registry[id]) return false;\n delete this.registry[id];\n return true;\n }\n\n /**\n * Detect layer config from tile URL templates (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n */\n detectFromTemplates(templates) {\n if (!templates || (Array.isArray(templates) && templates.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTemplate(templates)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Detect layer config from actual tile URLs (with numeric coordinates)\n * @param {string | string[]} urls - Single tile URL or array of tile URLs\n */\n detectFromTileUrls(urls) {\n if (!urls || (Array.isArray(urls) && urls.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTileUrl(urls)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Get all available layer config ids\n */\n getAvailableIds() {\n return Object.keys(this.registry);\n }\n\n /**\n * Create a new registry with all configs from this registry plus extra configs.\n * @param {LayerConfig[]} extraLayerConfigs - Additional configs to add\n * @returns {LayerConfigRegistry} A new registry with merged configs\n */\n createMergedRegistry(extraLayerConfigs) {\n const registry = new LayerConfigRegistry();\n \n for (const id of this.getAvailableIds()) {\n registry.register(this.get(id));\n }\n \n if (extraLayerConfigs && extraLayerConfigs.length > 0) {\n for (const config of extraLayerConfigs) {\n registry.register(config);\n }\n }\n \n return registry;\n }\n\n /**\n * Parse a tile URL into its components: layer config and coordinates\n * @param {string} url - Tile URL to parse\n * @returns {{ layerConfig: LayerConfig, coords: { z: number, x: number, y: number } } | null}\n */\n parseTileUrl(url) {\n // Check if URL matches any layer config\n const layerConfig = this.detectFromTileUrls([url]);\n if (!layerConfig) return null;\n \n // Extract tile coordinates using the matched config\n const coords = layerConfig.extractCoords(url);\n if (!coords) return null;\n \n return { layerConfig, coords };\n }\n}\n\n// Default registry with built-in configs loaded from JSON\nexport const layerConfigs = new LayerConfigRegistry();\nfor (const configData of configsJson) {\n layerConfigs.register(new LayerConfig(configData));\n}\n\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/configs.json","../src/layerconfig.js","../src/index.js"],"sourcesContent":["[\n {\n \"id\": \"cartodb-dark\",\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_nolabels/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"10\": 2.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(40, 40, 40)\" }\n ]\n },\n {\n \"id\": \"cartodb-light\",\n \"startZoom\": 0,\n \"zoomThreshold\": 5,\n \"tileUrlTemplates\": [\n \"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png\",\n \"https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\",\n \"https://basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.25, \"2\": 0.25, \"3\": 0.5, \"4\": 0.75, \"5\": 1.0 },\n \"lineStyles\": [\n { \"color\": \"rgb(235, 214, 214)\", \"alpha\": 0.2, \"startZoom\": 6, \"widthFraction\": 5 },\n { \"color\": \"rgb(235, 214, 214)\" }\n ]\n },\n {\n \"id\": \"open-topo\",\n \"startZoom\": 4,\n \"zoomThreshold\": 4,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png\",\n \"https://tile.opentopomap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"4\": 0.75, \"5\": 1.0, \"6\": 1.25, \"7\": 1.5, \"8\": 1.75, \"9\": 1.25, \"10\": 1.25, \"13\": 1.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(83, 83, 83)\", \"startZoom\": 7, \"endZoom\": 8, \"alpha\": 0.4, \"widthFraction\": 4 },\n { \"color\": \"rgb(83, 83, 83)\", \"endZoom\": 8 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9, \"widthFraction\": 9, \"alpha\": 0.2 },\n { \"color\": \"rgb(140, 20, 180)\", \"startZoom\": 9 }\n ]\n },\n {\n \"id\": \"osm-carto\",\n \"startZoom\": 1,\n \"zoomThreshold\": 1,\n \"tileUrlTemplates\": [\n \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\",\n \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"1\": 0.5, \"2\": 0.6, \"3\": 0.7, \"4\": 1.0, \"10\": 3.75 },\n \"lineStyles\": [\n { \"color\": \"rgb(200, 180, 200)\" },\n { \"color\": \"rgb(160, 120, 160)\", \"widthFraction\": 0.333, \"dashArray\": [30, 2, 8, 2] }\n ]\n },\n {\n \"id\": \"osm-hot\",\n \"startZoom\": 2,\n \"zoomThreshold\": 2,\n \"tileUrlTemplates\": [\n \"https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png\"\n ],\n \"lineWidthStops\": { \"2\": 3.0, \"3\": 3.0, \"7\": 3.0, \"8\": 3.5, \"9\": 3.5 },\n \"lineStyles\": [\n { \"color\": \"rgb(149, 175, 180)\" },\n { \"color\": \"rgb(89, 117, 123)\", \"widthFraction\": 0.33 }\n ]\n }\n]\n","/**\n * Convert a tile URL template to a regex pattern and capture group names.\n * Supports {z}, {x}, {y}, {s} (Leaflet subdomain), {a-c}/{1-4} (OpenLayers subdomain), and {r} (retina) placeholders.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {{ pattern: RegExp, groups: string[] }}\n */\nfunction templateToRegex(template) {\n const groups = [];\n // Escape regex special chars, then replace placeholders\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n // Don't escape our placeholders\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} etc (OpenLayers style subdomain)\n .replace(/\\{[a-z0-9]-[a-z0-9]\\}/gi, () => {\n groups.push('s');\n return '([a-z0-9]+)';\n })\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n groups.push(lowerName);\n if (lowerName === 's') {\n // Subdomain: single letter or short string\n return '([a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Retina suffix: optional @2x or similar\n return '(@\\\\d+x)?';\n }\n // z, x, y: numeric\n return '(\\\\d+)';\n });\n \n // Allow optional query string at end\n return { pattern: new RegExp('^' + pattern + '(\\\\?.*)?$', 'i'), groups };\n}\n\n/**\n * Convert a tile URL template to a regex that matches the template itself.\n * @param {string} template - URL template like \"https://{s}.tile.example.com/{z}/{x}/{y}.png\"\n * @returns {RegExp}\n */\nfunction templateToTemplateRegex(template) {\n // Escape regex special chars, then replace placeholders with literal match\n let pattern = template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n if (char === '{' || char === '}') return char;\n return '\\\\' + char;\n })\n // Make protocol flexible (http/https)\n .replace(/^https:\\/\\//, 'https?://')\n .replace(/^http:\\/\\//, 'https?://')\n // Handle {a-c} or {1-4} (OpenLayers style subdomain)\n .replace(/\\{([a-z0-9])-([a-z0-9])\\}/gi, (_, start, end) => `(\\\\{${start}-${end}\\\\}|\\\\{s\\\\}|[a-z0-9]+)`)\n .replace(/\\{(z|x|y|s|r)\\}/gi, (_, name) => {\n const lowerName = name.toLowerCase();\n if (lowerName === 's') {\n // Match {s} placeholder, {a-c} style placeholder, or actual subdomain\n return '(\\\\{s\\\\}|\\\\{[a-z0-9]-[a-z0-9]\\\\}|[a-z0-9]+)';\n }\n if (lowerName === 'r') {\n // Match {r} placeholder or actual retina or empty\n return '(\\\\{r\\\\}|@\\\\d+x)?';\n }\n // Match {z}, {x}, {y} placeholders\n return `\\\\{${lowerName}\\\\}`;\n });\n \n // Allow optional query string at end\n return new RegExp('^' + pattern + '(\\\\?.*)?$', 'i');\n}\n\n/**\n * Check if a string is a valid CSS color using the browser's CSS parser.\n * Falls back to a basic regex check in non-browser environments.\n * @param {string} color\n * @returns {boolean}\n */\nfunction isValidColor(color) {\n if (typeof color !== 'string' || !color.trim()) return false;\n \n // Use CSS.supports if available (modern browsers)\n if (typeof CSS !== 'undefined' && CSS.supports) {\n return CSS.supports('color', color);\n }\n \n // Fallback: basic validation for common formats\n const trimmed = color.trim().toLowerCase();\n // Hex colors\n if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(trimmed)) return true;\n // rgb/rgba/hsl/hsla functions\n if (/^(rgb|hsl)a?\\(/.test(trimmed)) return true;\n // Named colors (just check it's alphabetic)\n if (/^[a-z]+$/.test(trimmed)) return true;\n return false;\n}\n\n/**\n * Represents a line style for drawing boundaries.\n */\nexport class LineStyle {\n /**\n * Validate a LineStyle configuration object.\n * @param {Object} obj - The object to validate\n * @param {number} [index] - Optional index for error messages (when validating in an array)\n * @throws {Error} If validation fails\n */\n static validateJSON(obj, index) {\n const prefix = index !== undefined ? `lineStyles[${index}]` : 'LineStyle';\n \n if (!obj || typeof obj !== 'object') {\n throw new Error(`${prefix}: must be an object`);\n }\n \n if (!obj.color || typeof obj.color !== 'string') {\n throw new Error(`${prefix}: color must be a non-empty string`);\n }\n \n if (!isValidColor(obj.color)) {\n throw new Error(`${prefix}: color \"${obj.color}\" is not a valid CSS color`);\n }\n \n if (obj.widthFraction !== undefined && (typeof obj.widthFraction !== 'number' || obj.widthFraction <= 0)) {\n throw new Error(`${prefix}: widthFraction must be a positive number`);\n }\n \n if (obj.dashArray !== undefined && !Array.isArray(obj.dashArray)) {\n throw new Error(`${prefix}: dashArray must be an array`);\n }\n \n if (obj.alpha !== undefined && (typeof obj.alpha !== 'number' || obj.alpha < 0 || obj.alpha > 1)) {\n throw new Error(`${prefix}: alpha must be a number between 0 and 1`);\n }\n \n if (obj.startZoom !== undefined && (typeof obj.startZoom !== 'number' || obj.startZoom < 0)) {\n throw new Error(`${prefix}: startZoom must be a non-negative number`);\n }\n \n if (obj.endZoom !== undefined && (typeof obj.endZoom !== 'number' || obj.endZoom < 0)) {\n throw new Error(`${prefix}: endZoom must be a non-negative number`);\n }\n }\n\n /**\n * @param {Object} options\n * @param {string} options.color - CSS color string\n * @param {number} [options.widthFraction=1.0] - Multiplier for base line width\n * @param {number[]} [options.dashArray] - Dash pattern for dashed lines\n * @param {number} [options.alpha=1.0] - Opacity (0-1)\n * @param {number} [options.startZoom] - Minimum zoom level for this style\n * @param {number} [options.endZoom=Infinity] - Maximum zoom level for this style\n */\n constructor({ color, widthFraction = 1.0, dashArray, alpha = 1.0, startZoom, endZoom = Infinity }) {\n this.color = color;\n this.widthFraction = widthFraction;\n this.dashArray = dashArray;\n this.alpha = alpha;\n this.startZoom = startZoom;\n this.endZoom = endZoom;\n }\n\n /**\n * Check if this style is active at the given zoom level.\n * @param {number} z - Zoom level\n * @returns {boolean}\n */\n isActiveAtZoom(z) {\n return z >= this.startZoom && z <= this.endZoom;\n }\n\n /**\n * Serialize to plain object.\n * @returns {Object}\n */\n toJSON() {\n const obj = { color: this.color };\n if (this.widthFraction !== 1.0) obj.widthFraction = this.widthFraction;\n if (this.dashArray) obj.dashArray = this.dashArray;\n if (this.alpha !== 1.0) obj.alpha = this.alpha;\n if (this.startZoom !== undefined) obj.startZoom = this.startZoom;\n if (this.endZoom !== Infinity) obj.endZoom = this.endZoom;\n return obj;\n }\n\n /**\n * Create from plain object with validation.\n * @param {Object} obj\n * @param {number} [defaultStartZoom=0] - Default startZoom if not specified\n * @param {number} [index] - Optional index for error messages\n * @returns {LineStyle}\n */\n static fromJSON(obj, defaultStartZoom = 0, index) {\n LineStyle.validateJSON(obj, index);\n return new LineStyle({\n ...obj,\n startZoom: obj.startZoom ?? defaultStartZoom,\n });\n }\n}\n\n/**\n * Base class for layer configurations\n * \n * Supports separate styling for NE (Natural Earth) data at low zoom levels\n * and OSM data at higher zoom levels, split by zoomThreshold.\n */\nexport class LayerConfig {\n /**\n * Validate a LayerConfig configuration object.\n * Also validates all lineStyles within the config.\n * @param {Object} obj - The object to validate\n * @throws {Error} If validation fails\n */\n static validateJSON(obj) {\n if (!obj || typeof obj !== 'object') {\n throw new Error('LayerConfig: must be an object');\n }\n \n // Validate id (required)\n if (!obj.id || typeof obj.id !== 'string') {\n throw new Error('LayerConfig: id must be a non-empty string');\n }\n if (obj.id.includes('/')) {\n throw new Error(`LayerConfig: id cannot contain slashes: \"${obj.id}\"`);\n }\n \n const id = obj.id;\n \n // Validate zoom parameters (optional, but if provided must be valid)\n if (obj.startZoom !== undefined && (typeof obj.startZoom !== 'number' || obj.startZoom < 0)) {\n throw new Error(`LayerConfig \"${id}\": startZoom must be a non-negative number`);\n }\n if (obj.zoomThreshold !== undefined && (typeof obj.zoomThreshold !== 'number' || obj.zoomThreshold < 0)) {\n throw new Error(`LayerConfig \"${id}\": zoomThreshold must be a non-negative number`);\n }\n // Check startZoom <= zoomThreshold (using defaults if not provided)\n const startZoom = obj.startZoom ?? 0;\n const zoomThreshold = obj.zoomThreshold ?? 5;\n if (startZoom > zoomThreshold) {\n throw new Error(`LayerConfig \"${id}\": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);\n }\n \n // Validate lineWidthStops (optional, but if provided must be valid)\n if (obj.lineWidthStops !== undefined) {\n if (!obj.lineWidthStops || typeof obj.lineWidthStops !== 'object' || Array.isArray(obj.lineWidthStops)) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must be an object`);\n }\n const stopKeys = Object.keys(obj.lineWidthStops);\n if (stopKeys.length < 2) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops must have at least 2 entries`);\n }\n for (const key of stopKeys) {\n const zoom = Number(key);\n if (!Number.isInteger(zoom) || zoom < 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops keys must be non-negative integers, got \"${key}\"`);\n }\n if (typeof obj.lineWidthStops[key] !== 'number' || obj.lineWidthStops[key] <= 0) {\n throw new Error(`LayerConfig \"${id}\": lineWidthStops values must be positive numbers`);\n }\n }\n }\n \n // Validate lineStyles (optional, but if provided must be valid)\n if (obj.lineStyles !== undefined) {\n if (!Array.isArray(obj.lineStyles) || obj.lineStyles.length === 0) {\n throw new Error(`LayerConfig \"${id}\": lineStyles must be a non-empty array`);\n }\n // Validate each lineStyle\n for (let i = 0; i < obj.lineStyles.length; i++) {\n LineStyle.validateJSON(obj.lineStyles[i], i);\n }\n }\n \n // Validate optional numeric fields\n if (obj.delWidthFactor !== undefined && (typeof obj.delWidthFactor !== 'number' || obj.delWidthFactor <= 0)) {\n throw new Error(`LayerConfig \"${id}\": delWidthFactor must be a positive number`);\n }\n if (obj.lineExtensionFactor !== undefined && (typeof obj.lineExtensionFactor !== 'number' || obj.lineExtensionFactor < 0)) {\n throw new Error(`LayerConfig \"${id}\": lineExtensionFactor must be a non-negative number`);\n }\n }\n\n constructor({\n id,\n startZoom = 0,\n zoomThreshold = 5,\n // Tile URL templates for matching (e.g., \"https://{s}.tile.example.com/{z}/{x}/{y}.png\")\n tileUrlTemplates = [],\n // Line width stops: map of zoom level to line width (at least 2 entries)\n // Note: interpolated/extrapolated line width is capped at a minimum of 0.5\n lineWidthStops = { 1: 0.5, 10: 2.5 },\n // Line styles array - each element describes a line to draw\n // { color: string, widthFraction?: number, dashArray?: number[], startZoom?: number, endZoom?: number }\n // Lines are drawn in array order. startZoom defaults to layerConfig startZoom, endZoom defaults to Infinity\n lineStyles = [{ color: 'green', widthFraction: 1.0 }],\n // Factor to multiply line width for deletion blur (default 1.5)\n // Higher values leave gaps where wiped lines meet existing lines\n // Lower values mean wiped lines show through\n delWidthFactor = 1.5,\n // Factor to extend add lines by (multiplied by deletion line width)\n // Helps cover gaps where deleted lines meet the new boundary\n // Set to 0 to disable extension\n lineExtensionFactor = 0.5,\n }) {\n this.id = id;\n this.startZoom = startZoom;\n this.zoomThreshold = zoomThreshold;\n\n // Normalize to array\n const templates = Array.isArray(tileUrlTemplates) ? tileUrlTemplates : \n (tileUrlTemplates ? [tileUrlTemplates] : []);\n this.tileUrlTemplates = templates;\n \n // Pre-compile regex patterns for matching tile URLs (with actual coords)\n this._compiledPatterns = templates.map(t => templateToRegex(t));\n \n // Pre-compile regex patterns for matching template URLs (with {z}/{x}/{y} placeholders)\n this._templatePatterns = templates.map(t => templateToTemplateRegex(t));\n\n this.lineWidthStops = lineWidthStops;\n \n // Convert to LineStyle instances with defaults\n this.lineStyles = lineStyles.map(style => \n style instanceof LineStyle ? style : new LineStyle({\n ...style,\n startZoom: style.startZoom ?? startZoom,\n })\n );\n \n // Deletion width factor\n this.delWidthFactor = delWidthFactor;\n \n // Line extension factor\n this.lineExtensionFactor = lineExtensionFactor;\n }\n\n /**\n * Get line styles active at a given zoom level\n * @param {number} z - Zoom level\n * @returns {LineStyle[]}\n */\n getLineStylesForZoom(z) {\n return this.lineStyles.filter(style => style.isActiveAtZoom(z));\n }\n\n /**\n * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n * @returns {boolean}\n */\n matchTemplate(templates) {\n if (this._templatePatterns.length === 0) return false;\n \n const urls = Array.isArray(templates) ? templates : [templates];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._templatePatterns.some(pattern => pattern.test(url))\n );\n }\n\n /**\n * Check if this config matches the given tile URLs (with actual coordinates)\n * @param {string | string[]} tiles - Single tile URL or array of tile URLs\n * @returns {boolean}\n */\n matchTileUrl(tiles) {\n if (this._compiledPatterns.length === 0) return false;\n \n const urls = Array.isArray(tiles) ? tiles : [tiles];\n if (urls.length === 0) return false;\n \n return urls.some(url => \n this._compiledPatterns.some(({ pattern }) => pattern.test(url))\n );\n }\n\n /**\n * Extract tile coordinates (z, x, y) from a URL using this config's templates\n * @param {string} url - Tile URL to extract coordinates from\n * @returns {{ z: number, x: number, y: number } | null}\n */\n extractCoords(url) {\n for (const { pattern, groups } of this._compiledPatterns) {\n const match = url.match(pattern);\n if (match) {\n const result = {};\n for (let i = 0; i < groups.length; i++) {\n const name = groups[i];\n const value = match[i + 1];\n if (name === 'z' || name === 'x' || name === 'y') {\n result[name] = parseInt(value, 10);\n }\n }\n if ('z' in result && 'x' in result && 'y' in result) {\n return { z: result.z, x: result.x, y: result.y };\n }\n }\n }\n return null;\n }\n\n /**\n * Serialize the config to a plain object for postMessage\n * @returns {Object}\n */\n toJSON() {\n return {\n id: this.id,\n startZoom: this.startZoom,\n zoomThreshold: this.zoomThreshold,\n tileUrlTemplates: this.tileUrlTemplates,\n lineWidthStops: this.lineWidthStops,\n lineStyles: this.lineStyles.map(s => s.toJSON()),\n delWidthFactor: this.delWidthFactor,\n lineExtensionFactor: this.lineExtensionFactor,\n };\n }\n\n /**\n * Create a LayerConfig from a plain object with validation.\n * @param {Object} obj\n * @returns {LayerConfig}\n * @throws {Error} If validation fails\n */\n static fromJSON(obj) {\n LayerConfig.validateJSON(obj);\n return new LayerConfig(obj);\n }\n}\n\nexport default LayerConfig;\n","import configsJson from './configs.json' with { type: 'json' };\nimport { LayerConfig, LineStyle } from './layerconfig.js';\n\nexport { LayerConfig, LineStyle } from './layerconfig.js';\n\n// Export raw configs for testing/inspection\nexport { configsJson };\n\n/**\n * Layer configuration registry\n */\nexport class LayerConfigRegistry {\n constructor() {\n this.registry = {};\n }\n\n /**\n * Get a layer config by id\n */\n get(id) {\n return this.registry[id];\n }\n\n /**\n * Register a new layer config\n */\n register(config) {\n this.registry[config.id] = config;\n }\n\n /**\n * Remove a layer config by id\n */\n remove(id) {\n if (!this.registry[id]) return false;\n delete this.registry[id];\n return true;\n }\n\n /**\n * Detect layer config from tile URL templates (with {z}/{x}/{y} placeholders)\n * @param {string | string[]} templates - Single template URL or array of template URLs\n */\n detectFromTemplates(templates) {\n if (!templates || (Array.isArray(templates) && templates.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTemplate(templates)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Detect layer config from actual tile URLs (with numeric coordinates)\n * @param {string | string[]} urls - Single tile URL or array of tile URLs\n */\n detectFromTileUrls(urls) {\n if (!urls || (Array.isArray(urls) && urls.length === 0)) return undefined;\n \n for (const config of Object.values(this.registry)) {\n if (config.matchTileUrl(urls)) {\n return config;\n }\n }\n \n return undefined;\n }\n\n /**\n * Get all available layer config ids\n */\n getAvailableIds() {\n return Object.keys(this.registry);\n }\n\n /**\n * Create a new registry with all configs from this registry plus extra configs.\n * @param {LayerConfig[]} extraLayerConfigs - Additional configs to add\n * @returns {LayerConfigRegistry} A new registry with merged configs\n */\n createMergedRegistry(extraLayerConfigs) {\n const registry = new LayerConfigRegistry();\n \n for (const id of this.getAvailableIds()) {\n registry.register(this.get(id));\n }\n \n if (extraLayerConfigs && extraLayerConfigs.length > 0) {\n for (const config of extraLayerConfigs) {\n registry.register(config);\n }\n }\n \n return registry;\n }\n\n /**\n * Parse a tile URL into its components: layer config and coordinates\n * @param {string} url - Tile URL to parse\n * @returns {{ layerConfig: LayerConfig, coords: { z: number, x: number, y: number } } | null}\n */\n parseTileUrl(url) {\n // Check if URL matches any layer config\n const layerConfig = this.detectFromTileUrls([url]);\n if (!layerConfig) return null;\n \n // Extract tile coordinates using the matched config\n const coords = layerConfig.extractCoords(url);\n if (!coords) return null;\n \n return { layerConfig, coords };\n }\n}\n\n// Default registry with built-in configs loaded from JSON\nexport const layerConfigs = new LayerConfigRegistry();\nfor (const configData of configsJson) {\n layerConfigs.register(new LayerConfig(configData));\n}\n\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@india-boundary-corrector/layer-configs",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Pre-built layer configurations for India boundary corrections",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import configsJson from './configs.json' with { type: 'json' };
2
- import { LayerConfig } from './layerconfig.js';
2
+ import { LayerConfig, LineStyle } from './layerconfig.js';
3
3
 
4
- export { LayerConfig } from './layerconfig.js';
4
+ export { LayerConfig, LineStyle } from './layerconfig.js';
5
+
6
+ // Export raw configs for testing/inspection
7
+ export { configsJson };
5
8
 
6
9
  /**
7
10
  * Layer configuration registry
@@ -75,6 +75,134 @@ function templateToTemplateRegex(template) {
75
75
  return new RegExp('^' + pattern + '(\\?.*)?$', 'i');
76
76
  }
77
77
 
78
+ /**
79
+ * Check if a string is a valid CSS color using the browser's CSS parser.
80
+ * Falls back to a basic regex check in non-browser environments.
81
+ * @param {string} color
82
+ * @returns {boolean}
83
+ */
84
+ function isValidColor(color) {
85
+ if (typeof color !== 'string' || !color.trim()) return false;
86
+
87
+ // Use CSS.supports if available (modern browsers)
88
+ if (typeof CSS !== 'undefined' && CSS.supports) {
89
+ return CSS.supports('color', color);
90
+ }
91
+
92
+ // Fallback: basic validation for common formats
93
+ const trimmed = color.trim().toLowerCase();
94
+ // Hex colors
95
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(trimmed)) return true;
96
+ // rgb/rgba/hsl/hsla functions
97
+ if (/^(rgb|hsl)a?\(/.test(trimmed)) return true;
98
+ // Named colors (just check it's alphabetic)
99
+ if (/^[a-z]+$/.test(trimmed)) return true;
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * Represents a line style for drawing boundaries.
105
+ */
106
+ export class LineStyle {
107
+ /**
108
+ * Validate a LineStyle configuration object.
109
+ * @param {Object} obj - The object to validate
110
+ * @param {number} [index] - Optional index for error messages (when validating in an array)
111
+ * @throws {Error} If validation fails
112
+ */
113
+ static validateJSON(obj, index) {
114
+ const prefix = index !== undefined ? `lineStyles[${index}]` : 'LineStyle';
115
+
116
+ if (!obj || typeof obj !== 'object') {
117
+ throw new Error(`${prefix}: must be an object`);
118
+ }
119
+
120
+ if (!obj.color || typeof obj.color !== 'string') {
121
+ throw new Error(`${prefix}: color must be a non-empty string`);
122
+ }
123
+
124
+ if (!isValidColor(obj.color)) {
125
+ throw new Error(`${prefix}: color "${obj.color}" is not a valid CSS color`);
126
+ }
127
+
128
+ if (obj.widthFraction !== undefined && (typeof obj.widthFraction !== 'number' || obj.widthFraction <= 0)) {
129
+ throw new Error(`${prefix}: widthFraction must be a positive number`);
130
+ }
131
+
132
+ if (obj.dashArray !== undefined && !Array.isArray(obj.dashArray)) {
133
+ throw new Error(`${prefix}: dashArray must be an array`);
134
+ }
135
+
136
+ if (obj.alpha !== undefined && (typeof obj.alpha !== 'number' || obj.alpha < 0 || obj.alpha > 1)) {
137
+ throw new Error(`${prefix}: alpha must be a number between 0 and 1`);
138
+ }
139
+
140
+ if (obj.startZoom !== undefined && (typeof obj.startZoom !== 'number' || obj.startZoom < 0)) {
141
+ throw new Error(`${prefix}: startZoom must be a non-negative number`);
142
+ }
143
+
144
+ if (obj.endZoom !== undefined && (typeof obj.endZoom !== 'number' || obj.endZoom < 0)) {
145
+ throw new Error(`${prefix}: endZoom must be a non-negative number`);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * @param {Object} options
151
+ * @param {string} options.color - CSS color string
152
+ * @param {number} [options.widthFraction=1.0] - Multiplier for base line width
153
+ * @param {number[]} [options.dashArray] - Dash pattern for dashed lines
154
+ * @param {number} [options.alpha=1.0] - Opacity (0-1)
155
+ * @param {number} [options.startZoom] - Minimum zoom level for this style
156
+ * @param {number} [options.endZoom=Infinity] - Maximum zoom level for this style
157
+ */
158
+ constructor({ color, widthFraction = 1.0, dashArray, alpha = 1.0, startZoom, endZoom = Infinity }) {
159
+ this.color = color;
160
+ this.widthFraction = widthFraction;
161
+ this.dashArray = dashArray;
162
+ this.alpha = alpha;
163
+ this.startZoom = startZoom;
164
+ this.endZoom = endZoom;
165
+ }
166
+
167
+ /**
168
+ * Check if this style is active at the given zoom level.
169
+ * @param {number} z - Zoom level
170
+ * @returns {boolean}
171
+ */
172
+ isActiveAtZoom(z) {
173
+ return z >= this.startZoom && z <= this.endZoom;
174
+ }
175
+
176
+ /**
177
+ * Serialize to plain object.
178
+ * @returns {Object}
179
+ */
180
+ toJSON() {
181
+ const obj = { color: this.color };
182
+ if (this.widthFraction !== 1.0) obj.widthFraction = this.widthFraction;
183
+ if (this.dashArray) obj.dashArray = this.dashArray;
184
+ if (this.alpha !== 1.0) obj.alpha = this.alpha;
185
+ if (this.startZoom !== undefined) obj.startZoom = this.startZoom;
186
+ if (this.endZoom !== Infinity) obj.endZoom = this.endZoom;
187
+ return obj;
188
+ }
189
+
190
+ /**
191
+ * Create from plain object with validation.
192
+ * @param {Object} obj
193
+ * @param {number} [defaultStartZoom=0] - Default startZoom if not specified
194
+ * @param {number} [index] - Optional index for error messages
195
+ * @returns {LineStyle}
196
+ */
197
+ static fromJSON(obj, defaultStartZoom = 0, index) {
198
+ LineStyle.validateJSON(obj, index);
199
+ return new LineStyle({
200
+ ...obj,
201
+ startZoom: obj.startZoom ?? defaultStartZoom,
202
+ });
203
+ }
204
+ }
205
+
78
206
  /**
79
207
  * Base class for layer configurations
80
208
  *
@@ -82,6 +210,81 @@ function templateToTemplateRegex(template) {
82
210
  * and OSM data at higher zoom levels, split by zoomThreshold.
83
211
  */
84
212
  export class LayerConfig {
213
+ /**
214
+ * Validate a LayerConfig configuration object.
215
+ * Also validates all lineStyles within the config.
216
+ * @param {Object} obj - The object to validate
217
+ * @throws {Error} If validation fails
218
+ */
219
+ static validateJSON(obj) {
220
+ if (!obj || typeof obj !== 'object') {
221
+ throw new Error('LayerConfig: must be an object');
222
+ }
223
+
224
+ // Validate id (required)
225
+ if (!obj.id || typeof obj.id !== 'string') {
226
+ throw new Error('LayerConfig: id must be a non-empty string');
227
+ }
228
+ if (obj.id.includes('/')) {
229
+ throw new Error(`LayerConfig: id cannot contain slashes: "${obj.id}"`);
230
+ }
231
+
232
+ const id = obj.id;
233
+
234
+ // Validate zoom parameters (optional, but if provided must be valid)
235
+ if (obj.startZoom !== undefined && (typeof obj.startZoom !== 'number' || obj.startZoom < 0)) {
236
+ throw new Error(`LayerConfig "${id}": startZoom must be a non-negative number`);
237
+ }
238
+ if (obj.zoomThreshold !== undefined && (typeof obj.zoomThreshold !== 'number' || obj.zoomThreshold < 0)) {
239
+ throw new Error(`LayerConfig "${id}": zoomThreshold must be a non-negative number`);
240
+ }
241
+ // Check startZoom <= zoomThreshold (using defaults if not provided)
242
+ const startZoom = obj.startZoom ?? 0;
243
+ const zoomThreshold = obj.zoomThreshold ?? 5;
244
+ if (startZoom > zoomThreshold) {
245
+ throw new Error(`LayerConfig "${id}": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);
246
+ }
247
+
248
+ // Validate lineWidthStops (optional, but if provided must be valid)
249
+ if (obj.lineWidthStops !== undefined) {
250
+ if (!obj.lineWidthStops || typeof obj.lineWidthStops !== 'object' || Array.isArray(obj.lineWidthStops)) {
251
+ throw new Error(`LayerConfig "${id}": lineWidthStops must be an object`);
252
+ }
253
+ const stopKeys = Object.keys(obj.lineWidthStops);
254
+ if (stopKeys.length < 2) {
255
+ throw new Error(`LayerConfig "${id}": lineWidthStops must have at least 2 entries`);
256
+ }
257
+ for (const key of stopKeys) {
258
+ const zoom = Number(key);
259
+ if (!Number.isInteger(zoom) || zoom < 0) {
260
+ throw new Error(`LayerConfig "${id}": lineWidthStops keys must be non-negative integers, got "${key}"`);
261
+ }
262
+ if (typeof obj.lineWidthStops[key] !== 'number' || obj.lineWidthStops[key] <= 0) {
263
+ throw new Error(`LayerConfig "${id}": lineWidthStops values must be positive numbers`);
264
+ }
265
+ }
266
+ }
267
+
268
+ // Validate lineStyles (optional, but if provided must be valid)
269
+ if (obj.lineStyles !== undefined) {
270
+ if (!Array.isArray(obj.lineStyles) || obj.lineStyles.length === 0) {
271
+ throw new Error(`LayerConfig "${id}": lineStyles must be a non-empty array`);
272
+ }
273
+ // Validate each lineStyle
274
+ for (let i = 0; i < obj.lineStyles.length; i++) {
275
+ LineStyle.validateJSON(obj.lineStyles[i], i);
276
+ }
277
+ }
278
+
279
+ // Validate optional numeric fields
280
+ if (obj.delWidthFactor !== undefined && (typeof obj.delWidthFactor !== 'number' || obj.delWidthFactor <= 0)) {
281
+ throw new Error(`LayerConfig "${id}": delWidthFactor must be a positive number`);
282
+ }
283
+ if (obj.lineExtensionFactor !== undefined && (typeof obj.lineExtensionFactor !== 'number' || obj.lineExtensionFactor < 0)) {
284
+ throw new Error(`LayerConfig "${id}": lineExtensionFactor must be a non-negative number`);
285
+ }
286
+ }
287
+
85
288
  constructor({
86
289
  id,
87
290
  startZoom = 0,
@@ -104,21 +307,10 @@ export class LayerConfig {
104
307
  // Set to 0 to disable extension
105
308
  lineExtensionFactor = 0.5,
106
309
  }) {
107
- if (!id || typeof id !== 'string') {
108
- throw new Error('LayerConfig requires a non-empty string id');
109
- }
110
- if (id.includes('/')) {
111
- throw new Error(`LayerConfig id cannot contain slashes: "${id}"`);
112
- }
113
-
114
310
  this.id = id;
115
311
  this.startZoom = startZoom;
116
312
  this.zoomThreshold = zoomThreshold;
117
313
 
118
- if (startZoom > zoomThreshold) {
119
- throw new Error(`LayerConfig "${id}": startZoom (${startZoom}) must be <= zoomThreshold (${zoomThreshold})`);
120
- }
121
-
122
314
  // Normalize to array
123
315
  const templates = Array.isArray(tileUrlTemplates) ? tileUrlTemplates :
124
316
  (tileUrlTemplates ? [tileUrlTemplates] : []);
@@ -130,45 +322,15 @@ export class LayerConfig {
130
322
  // Pre-compile regex patterns for matching template URLs (with {z}/{x}/{y} placeholders)
131
323
  this._templatePatterns = templates.map(t => templateToTemplateRegex(t));
132
324
 
133
- // Validate lineWidthStops
134
- if (!lineWidthStops || typeof lineWidthStops !== 'object' || Array.isArray(lineWidthStops)) {
135
- throw new Error(`LayerConfig "${id}": lineWidthStops must be an object`);
136
- }
137
- const stopKeys = Object.keys(lineWidthStops);
138
- if (stopKeys.length < 2) {
139
- throw new Error(`LayerConfig "${id}": lineWidthStops must have at least 2 entries`);
140
- }
141
- for (const key of stopKeys) {
142
- const zoom = Number(key);
143
- if (!Number.isInteger(zoom) || zoom < 0) {
144
- throw new Error(`LayerConfig "${id}": lineWidthStops keys must be non-negative integers, got "${key}"`);
145
- }
146
- if (typeof lineWidthStops[key] !== 'number' || lineWidthStops[key] <= 0) {
147
- throw new Error(`LayerConfig "${id}": lineWidthStops values must be positive numbers`);
148
- }
149
- }
150
325
  this.lineWidthStops = lineWidthStops;
151
-
152
- // Validate lineStyles
153
- if (!Array.isArray(lineStyles) || lineStyles.length === 0) {
154
- throw new Error(`LayerConfig "${id}": lineStyles must be a non-empty array`);
155
- }
156
- for (let i = 0; i < lineStyles.length; i++) {
157
- const style = lineStyles[i];
158
- if (!style || typeof style !== 'object') {
159
- throw new Error(`LayerConfig "${id}": lineStyles[${i}] must be an object`);
160
- }
161
- if (!style.color || typeof style.color !== 'string') {
162
- throw new Error(`LayerConfig "${id}": lineStyles[${i}].color must be a non-empty string`);
163
- }
164
- }
165
326
 
166
- // Line styles - normalize startZoom/endZoom defaults
167
- this.lineStyles = lineStyles.map(style => ({
168
- ...style,
169
- startZoom: style.startZoom ?? startZoom,
170
- endZoom: style.endZoom ?? Infinity,
171
- }));
327
+ // Convert to LineStyle instances with defaults
328
+ this.lineStyles = lineStyles.map(style =>
329
+ style instanceof LineStyle ? style : new LineStyle({
330
+ ...style,
331
+ startZoom: style.startZoom ?? startZoom,
332
+ })
333
+ );
172
334
 
173
335
  // Deletion width factor
174
336
  this.delWidthFactor = delWidthFactor;
@@ -180,10 +342,10 @@ export class LayerConfig {
180
342
  /**
181
343
  * Get line styles active at a given zoom level
182
344
  * @param {number} z - Zoom level
183
- * @returns {Array<{color: string, widthFraction?: number, dashArray?: number[]}>}
345
+ * @returns {LineStyle[]}
184
346
  */
185
347
  getLineStylesForZoom(z) {
186
- return this.lineStyles.filter(style => z >= style.startZoom && z <= style.endZoom);
348
+ return this.lineStyles.filter(style => style.isActiveAtZoom(z));
187
349
  }
188
350
 
189
351
  /**
@@ -254,18 +416,20 @@ export class LayerConfig {
254
416
  zoomThreshold: this.zoomThreshold,
255
417
  tileUrlTemplates: this.tileUrlTemplates,
256
418
  lineWidthStops: this.lineWidthStops,
257
- lineStyles: this.lineStyles,
419
+ lineStyles: this.lineStyles.map(s => s.toJSON()),
258
420
  delWidthFactor: this.delWidthFactor,
259
421
  lineExtensionFactor: this.lineExtensionFactor,
260
422
  };
261
423
  }
262
424
 
263
425
  /**
264
- * Create a LayerConfig from a plain object (e.g., from postMessage)
426
+ * Create a LayerConfig from a plain object with validation.
265
427
  * @param {Object} obj
266
428
  * @returns {LayerConfig}
429
+ * @throws {Error} If validation fails
267
430
  */
268
431
  static fromJSON(obj) {
432
+ LayerConfig.validateJSON(obj);
269
433
  return new LayerConfig(obj);
270
434
  }
271
435
  }