@india-boundary-corrector/layer-configs 0.2.0 → 0.2.2

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/src/index.d.ts CHANGED
@@ -9,14 +9,24 @@ export const INFINITY: -1;
9
9
  */
10
10
  export const MIN_LINE_WIDTH: number;
11
11
 
12
+ /**
13
+ * Interpolate or extrapolate line width for a given zoom level.
14
+ * @param zoom - Zoom level
15
+ * @param lineWidthStops - Map of zoom level to line width (at least 2 entries)
16
+ * @returns Interpolated/extrapolated line width (minimum MIN_LINE_WIDTH)
17
+ */
18
+ export function interpolateLineWidth(zoom: number, lineWidthStops: Record<number, number>): number;
19
+
12
20
  /**
13
21
  * Line style definition for drawing boundary lines
14
22
  */
15
- export interface LineStyle {
23
+ export interface LineStyleOptions {
16
24
  /** Line color (CSS color string) */
17
25
  color: string;
18
26
  /** Layer suffix (e.g., 'osm', 'ne', 'osm-disp') - determines PMTiles layer */
19
27
  layerSuffix: string;
28
+ /** Line width stops: map of zoom level to line width (required) */
29
+ lineWidthStops: Record<number, number>;
20
30
  /** Width as fraction of base line width (default: 1.0) */
21
31
  widthFraction?: number;
22
32
  /** Dash pattern array (omit for solid line) */
@@ -34,7 +44,59 @@ export interface LineStyle {
34
44
  }
35
45
 
36
46
  /**
37
- * Configuration options for LayerConfig
47
+ * Line style class for drawing boundary lines
48
+ */
49
+ export class LineStyle {
50
+ readonly color: string;
51
+ readonly layerSuffix: string;
52
+ readonly lineWidthStops: Record<number, number>;
53
+ readonly widthFraction: number;
54
+ readonly dashArray?: number[];
55
+ readonly alpha: number;
56
+ readonly startZoom: number;
57
+ readonly endZoom: number;
58
+ readonly lineExtensionFactor: number;
59
+ readonly delWidthFactor: number;
60
+
61
+ constructor(options: LineStyleOptions);
62
+
63
+ /**
64
+ * Get base line width for this style at a given zoom level.
65
+ * @param zoom - Zoom level
66
+ */
67
+ getLineWidth(zoom: number): number;
68
+
69
+ /**
70
+ * Check if this style is active at the given zoom level.
71
+ * @param z - Zoom level
72
+ */
73
+ isActiveAtZoom(z: number): boolean;
74
+
75
+ /**
76
+ * Serialize to plain object.
77
+ */
78
+ toJSON(): LineStyleOptions;
79
+
80
+ /**
81
+ * Create from plain object with validation.
82
+ */
83
+ static fromJSON(obj: LineStyleOptions, index?: number): LineStyle;
84
+
85
+ /**
86
+ * Validate a LineStyle configuration object.
87
+ */
88
+ static validateJSON(obj: unknown, index?: number, requireLineWidthStops?: boolean): void;
89
+ }
90
+
91
+ /**
92
+ * Line style input for LayerConfig (lineWidthStops is optional, inherited from config if not provided)
93
+ */
94
+ export type LineStyleInput = Omit<LineStyleOptions, 'lineWidthStops'> & {
95
+ lineWidthStops?: Record<number, number>;
96
+ };
97
+
98
+ /**
99
+ * Configuration options for LayerConfig input (constructor)
38
100
  */
39
101
  export interface LayerConfigOptions {
40
102
  /** Unique identifier for this config */
@@ -43,8 +105,18 @@ export interface LayerConfigOptions {
43
105
  tileUrlTemplates?: string | string[];
44
106
  /** Line width stops: map of zoom level to line width (at least 2 entries) */
45
107
  lineWidthStops?: Record<number, number>;
46
- /** Line styles array - lines are drawn in order (required) */
47
- lineStyles: LineStyle[];
108
+ /** Line styles array - lines are drawn in order (required). lineWidthStops is optional per style (inherited from config if not provided) */
109
+ lineStyles: LineStyleInput[];
110
+ }
111
+
112
+ /**
113
+ * Serialized LayerConfig (from toJSON)
114
+ */
115
+ export interface LayerConfigJSON {
116
+ id: string;
117
+ tileUrlTemplates: string[];
118
+ lineWidthStops: Record<number, number>;
119
+ lineStyles: LineStyleOptions[];
48
120
  }
49
121
 
50
122
  /**
@@ -70,14 +142,6 @@ export class LayerConfig {
70
142
  */
71
143
  getLayerSuffixesForZoom(z: number): string[];
72
144
 
73
- /**
74
- * Interpolate or extrapolate line width for a given zoom level.
75
- * Uses the lineWidthStops map to calculate the appropriate width.
76
- * @param zoom - Zoom level
77
- * @returns Interpolated/extrapolated line width (minimum MIN_LINE_WIDTH)
78
- */
79
- getLineWidth(zoom: number): number;
80
-
81
145
  /**
82
146
  * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)
83
147
  * @param templates - Single template URL or array of template URLs
@@ -100,12 +164,12 @@ export class LayerConfig {
100
164
  /**
101
165
  * Serialize the config to a plain object for postMessage
102
166
  */
103
- toJSON(): LayerConfigOptions;
167
+ toJSON(): LayerConfigJSON;
104
168
 
105
169
  /**
106
170
  * Create a LayerConfig from a plain object (e.g., from postMessage)
107
171
  */
108
- static fromJSON(obj: LayerConfigOptions): LayerConfig;
172
+ static fromJSON(obj: LayerConfigOptions | LayerConfigJSON): LayerConfig;
109
173
  }
110
174
 
111
175
  /**
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import configsJson from './configs.json' with { type: 'json' };
2
- import { LayerConfig, LineStyle, INFINITY, MIN_LINE_WIDTH } from './layerconfig.js';
2
+ import { LayerConfig, LineStyle, INFINITY, MIN_LINE_WIDTH, interpolateLineWidth } from './layerconfig.js';
3
3
 
4
- export { LayerConfig, LineStyle, INFINITY, MIN_LINE_WIDTH } from './layerconfig.js';
4
+ export { LayerConfig, LineStyle, INFINITY, MIN_LINE_WIDTH, interpolateLineWidth } from './layerconfig.js';
5
5
 
6
6
  // Export raw configs for testing/inspection
7
7
  export { configsJson };
@@ -14,6 +14,56 @@ export const MIN_LINE_WIDTH = 0.1;
14
14
  */
15
15
  const DEFAULT_LINE_WIDTH = 1;
16
16
 
17
+ /**
18
+ * Interpolate or extrapolate line width for a given zoom level.
19
+ * Uses a lineWidthStops map to calculate the appropriate width.
20
+ * @param {number} zoom - Zoom level
21
+ * @param {Object<number, number>} lineWidthStops - Map of zoom level to line width (at least 2 entries)
22
+ * @returns {number}
23
+ */
24
+ export function interpolateLineWidth(zoom, lineWidthStops) {
25
+ const zooms = Object.keys(lineWidthStops).map(Number).sort((a, b) => a - b);
26
+
27
+ // Exact match
28
+ if (lineWidthStops[zoom] !== undefined) {
29
+ return lineWidthStops[zoom];
30
+ }
31
+
32
+ // Below lowest zoom - extrapolate
33
+ if (zoom < zooms[0]) {
34
+ const z1 = zooms[0];
35
+ const z2 = zooms[1];
36
+ const w1 = lineWidthStops[z1];
37
+ const w2 = lineWidthStops[z2];
38
+ const slope = (w2 - w1) / (z2 - z1);
39
+ return Math.max(MIN_LINE_WIDTH, w1 + slope * (zoom - z1));
40
+ }
41
+
42
+ // Above highest zoom - extrapolate
43
+ if (zoom > zooms[zooms.length - 1]) {
44
+ const z1 = zooms[zooms.length - 2];
45
+ const z2 = zooms[zooms.length - 1];
46
+ const w1 = lineWidthStops[z1];
47
+ const w2 = lineWidthStops[z2];
48
+ const slope = (w2 - w1) / (z2 - z1);
49
+ return Math.max(MIN_LINE_WIDTH, w2 + slope * (zoom - z2));
50
+ }
51
+
52
+ // Interpolate between two stops
53
+ for (let i = 0; i < zooms.length - 1; i++) {
54
+ if (zoom > zooms[i] && zoom < zooms[i + 1]) {
55
+ const z1 = zooms[i];
56
+ const z2 = zooms[i + 1];
57
+ const w1 = lineWidthStops[z1];
58
+ const w2 = lineWidthStops[z2];
59
+ const t = (zoom - z1) / (z2 - z1);
60
+ return w1 + t * (w2 - w1);
61
+ }
62
+ }
63
+
64
+ return DEFAULT_LINE_WIDTH; // fallback
65
+ }
66
+
17
67
  /**
18
68
  * Convert a tile URL template to a regex pattern and capture group names.
19
69
  * Supports {z}, {x}, {y}, {s} (Leaflet subdomain), {a-c}/{1-4} (OpenLayers subdomain), and {r} (retina) placeholders.
@@ -144,6 +194,31 @@ function isValidColor(color) {
144
194
  return false;
145
195
  }
146
196
 
197
+ /**
198
+ * Validate lineWidthStops object.
199
+ * @param {Object} lineWidthStops - The lineWidthStops to validate
200
+ * @param {string} prefix - Error message prefix
201
+ * @throws {Error} If validation fails
202
+ */
203
+ function validateLineWidthStops(lineWidthStops, prefix) {
204
+ if (!lineWidthStops || typeof lineWidthStops !== 'object' || Array.isArray(lineWidthStops)) {
205
+ throw new Error(`${prefix}: lineWidthStops must be an object`);
206
+ }
207
+ const stopKeys = Object.keys(lineWidthStops);
208
+ if (stopKeys.length < 2) {
209
+ throw new Error(`${prefix}: lineWidthStops must have at least 2 entries`);
210
+ }
211
+ for (const key of stopKeys) {
212
+ const zoom = Number(key);
213
+ if (!Number.isInteger(zoom) || zoom < 0) {
214
+ throw new Error(`${prefix}: lineWidthStops keys must be non-negative integers, got "${key}"`);
215
+ }
216
+ if (typeof lineWidthStops[key] !== 'number' || lineWidthStops[key] <= 0) {
217
+ throw new Error(`${prefix}: lineWidthStops values must be positive numbers`);
218
+ }
219
+ }
220
+ }
221
+
147
222
  /**
148
223
  * Represents a line style for drawing boundaries.
149
224
  */
@@ -152,9 +227,10 @@ export class LineStyle {
152
227
  * Validate a LineStyle configuration object.
153
228
  * @param {Object} obj - The object to validate
154
229
  * @param {number} [index] - Optional index for error messages (when validating in an array)
230
+ * @param {boolean} [requireLineWidthStops=false] - Whether lineWidthStops is required
155
231
  * @throws {Error} If validation fails
156
232
  */
157
- static validateJSON(obj, index) {
233
+ static validateJSON(obj, index, requireLineWidthStops = false) {
158
234
  const prefix = index !== undefined ? `lineStyles[${index}]` : 'LineStyle';
159
235
 
160
236
  if (!obj || typeof obj !== 'object') {
@@ -200,23 +276,33 @@ export class LineStyle {
200
276
  if (obj.delWidthFactor !== undefined && (typeof obj.delWidthFactor !== 'number' || obj.delWidthFactor < 0)) {
201
277
  throw new Error(`${prefix}: delWidthFactor must be a non-negative number`);
202
278
  }
279
+
280
+ if (requireLineWidthStops && obj.lineWidthStops === undefined) {
281
+ throw new Error(`${prefix}: lineWidthStops is required`);
282
+ }
283
+
284
+ if (obj.lineWidthStops !== undefined) {
285
+ validateLineWidthStops(obj.lineWidthStops, prefix);
286
+ }
203
287
  }
204
288
 
205
289
  /**
206
290
  * @param {Object} options
207
291
  * @param {string} options.color - CSS color string
208
292
  * @param {string} options.layerSuffix - Layer suffix (e.g., 'osm', 'ne', 'osm-disp')
293
+ * @param {Object<number, number>} options.lineWidthStops - Line width stops for this style
209
294
  * @param {number} [options.widthFraction=1.0] - Multiplier for base line width
210
295
  * @param {number[]} [options.dashArray] - Dash pattern for dashed lines
211
296
  * @param {number} [options.alpha=1.0] - Opacity (0-1)
212
297
  * @param {number} [options.startZoom=0] - Minimum zoom level for this style
213
298
  * @param {number} [options.endZoom=INFINITY] - Maximum zoom level for this style (INFINITY means no limit)
214
- * @param {number} [options.lineExtensionFactor=0.5] - Factor to extend lines by (multiplied by deletion line width)
299
+ * @param {number} [options.lineExtensionFactor=0.0] - Factor to extend lines by (multiplied by deletion line width)
215
300
  * @param {number} [options.delWidthFactor=1.5] - Factor to multiply line width for deletion blur
216
301
  */
217
- constructor({ color, layerSuffix, widthFraction = 1.0, dashArray, alpha = 1.0, startZoom = 0, endZoom = INFINITY, lineExtensionFactor = 0.0, delWidthFactor = 1.5 }) {
302
+ constructor({ color, layerSuffix, lineWidthStops, widthFraction = 1.0, dashArray, alpha = 1.0, startZoom = 0, endZoom = INFINITY, lineExtensionFactor = 0.0, delWidthFactor = 1.5 }) {
218
303
  this.color = color;
219
304
  this.layerSuffix = layerSuffix;
305
+ this.lineWidthStops = lineWidthStops;
220
306
  this.widthFraction = widthFraction;
221
307
  this.dashArray = dashArray;
222
308
  this.alpha = alpha;
@@ -226,6 +312,15 @@ export class LineStyle {
226
312
  this.delWidthFactor = delWidthFactor;
227
313
  }
228
314
 
315
+ /**
316
+ * Get base line width for this style at a given zoom level.
317
+ * @param {number} zoom - Zoom level
318
+ * @returns {number}
319
+ */
320
+ getLineWidth(zoom) {
321
+ return interpolateLineWidth(zoom, this.lineWidthStops);
322
+ }
323
+
229
324
  /**
230
325
  * Check if this style is active at the given zoom level.
231
326
  * @param {number} z - Zoom level
@@ -243,6 +338,7 @@ export class LineStyle {
243
338
  return {
244
339
  color: this.color,
245
340
  layerSuffix: this.layerSuffix,
341
+ lineWidthStops: this.lineWidthStops,
246
342
  widthFraction: this.widthFraction,
247
343
  dashArray: this.dashArray,
248
344
  alpha: this.alpha,
@@ -260,7 +356,7 @@ export class LineStyle {
260
356
  * @returns {LineStyle}
261
357
  */
262
358
  static fromJSON(obj, index) {
263
- LineStyle.validateJSON(obj, index);
359
+ LineStyle.validateJSON(obj, index, true); // require lineWidthStops
264
360
  return new LineStyle(obj);
265
361
  }
266
362
  }
@@ -351,10 +447,15 @@ export class LayerConfig {
351
447
 
352
448
  this.lineWidthStops = lineWidthStops;
353
449
 
354
- // Convert to LineStyle instances
355
- this.lineStyles = lineStyles.map(style =>
356
- style instanceof LineStyle ? style : new LineStyle(style)
357
- );
450
+ // Convert to LineStyle instances, inheriting lineWidthStops from config if not specified
451
+ this.lineStyles = lineStyles.map(style => {
452
+ if (style instanceof LineStyle) {
453
+ return style;
454
+ }
455
+ // If style doesn't have lineWidthStops, use the config's lineWidthStops
456
+ const styleWithStops = style.lineWidthStops ? style : { ...style, lineWidthStops };
457
+ return new LineStyle(styleWithStops);
458
+ });
358
459
  }
359
460
 
360
461
  /**
@@ -376,55 +477,6 @@ export class LayerConfig {
376
477
  return [...new Set(activeStyles.map(s => s.layerSuffix))];
377
478
  }
378
479
 
379
- /**
380
- * Interpolate or extrapolate line width for a given zoom level.
381
- * Uses the lineWidthStops map to calculate the appropriate width.
382
- * @param {number} zoom - Zoom level
383
- * @returns {number}
384
- */
385
- getLineWidth(zoom) {
386
- const zooms = Object.keys(this.lineWidthStops).map(Number).sort((a, b) => a - b);
387
-
388
- // Exact match
389
- if (this.lineWidthStops[zoom] !== undefined) {
390
- return this.lineWidthStops[zoom];
391
- }
392
-
393
- // Below lowest zoom - extrapolate
394
- if (zoom < zooms[0]) {
395
- const z1 = zooms[0];
396
- const z2 = zooms[1];
397
- const w1 = this.lineWidthStops[z1];
398
- const w2 = this.lineWidthStops[z2];
399
- const slope = (w2 - w1) / (z2 - z1);
400
- return Math.max(MIN_LINE_WIDTH, w1 + slope * (zoom - z1));
401
- }
402
-
403
- // Above highest zoom - extrapolate
404
- if (zoom > zooms[zooms.length - 1]) {
405
- const z1 = zooms[zooms.length - 2];
406
- const z2 = zooms[zooms.length - 1];
407
- const w1 = this.lineWidthStops[z1];
408
- const w2 = this.lineWidthStops[z2];
409
- const slope = (w2 - w1) / (z2 - z1);
410
- return Math.max(MIN_LINE_WIDTH, w2 + slope * (zoom - z2));
411
- }
412
-
413
- // Interpolate between two stops
414
- for (let i = 0; i < zooms.length - 1; i++) {
415
- if (zoom > zooms[i] && zoom < zooms[i + 1]) {
416
- const z1 = zooms[i];
417
- const z2 = zooms[i + 1];
418
- const w1 = this.lineWidthStops[z1];
419
- const w2 = this.lineWidthStops[z2];
420
- const t = (zoom - z1) / (z2 - z1);
421
- return w1 + t * (w2 - w1);
422
- }
423
- }
424
-
425
- return DEFAULT_LINE_WIDTH; // fallback
426
- }
427
-
428
480
  /**
429
481
  * Check if this config matches the given template URLs (with {z}/{x}/{y} placeholders)
430
482
  * @param {string | string[]} templates - Single template URL or array of template URLs