@geogdev/styles 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,688 @@
1
+ import type {
2
+ ExpressionSpecification,
3
+ FilterSpecification,
4
+ LayerSpecification,
5
+ LineLayerSpecification,
6
+ } from "@maplibre/maplibre-gl-style-spec";
7
+ import { FILTERS, LINE_WIDTHS, ROAD_CLASSES, ZOOM } from "../constants";
8
+ import type { Style } from "../styles";
9
+
10
+ // ============================================
11
+ // TYPES
12
+ // ============================================
13
+
14
+ type RoadCategory = "other" | "minor" | "link" | "major" | "highway";
15
+ type BrunnelType = "tunnel" | "bridge" | null;
16
+
17
+ interface RoadLayerConfig {
18
+ source: string;
19
+ style: Style;
20
+ brunnel: BrunnelType;
21
+ category: RoadCategory;
22
+ isCasing: boolean;
23
+ minzoom?: number;
24
+ maxzoom?: number;
25
+ }
26
+
27
+ // ============================================
28
+ // HELPER FUNCTIONS
29
+ // ============================================
30
+
31
+ function getRoadClasses(category: RoadCategory): readonly string[] {
32
+ switch (category) {
33
+ case "other":
34
+ return ROAD_CLASSES.OTHER;
35
+ case "minor":
36
+ return ROAD_CLASSES.MINOR;
37
+ case "major":
38
+ return ROAD_CLASSES.MAJOR;
39
+ case "highway":
40
+ return ROAD_CLASSES.HIGHWAY;
41
+ case "link":
42
+ return []; // Link uses ramp filter, not class filter
43
+ }
44
+ }
45
+
46
+ function buildFilter(
47
+ brunnel: BrunnelType,
48
+ category: RoadCategory,
49
+ ): FilterSpecification {
50
+ const conditions: ExpressionSpecification[] = [];
51
+
52
+ // Brunnel condition
53
+ if (brunnel === "tunnel") {
54
+ conditions.push(FILTERS.IS_TUNNEL);
55
+ } else if (brunnel === "bridge") {
56
+ conditions.push(FILTERS.IS_BRIDGE);
57
+ } else {
58
+ conditions.push(FILTERS.NOT_BRUNNEL);
59
+ }
60
+
61
+ // Road class condition
62
+ if (category === "link") {
63
+ conditions.push(FILTERS.IS_RAMP);
64
+ } else {
65
+ const classes = getRoadClasses(category);
66
+ if (classes.length > 0) {
67
+ conditions.push([
68
+ "in",
69
+ ["get", "class"],
70
+ ["literal", classes as unknown as string[]],
71
+ ]);
72
+ }
73
+ // Highway excludes ramps
74
+ if (category === "highway") {
75
+ conditions.push(FILTERS.NOT_RAMP);
76
+ }
77
+ }
78
+
79
+ return conditions.length === 1
80
+ ? conditions[0]
81
+ : (["all", ...conditions] as FilterSpecification);
82
+ }
83
+
84
+ function getLayerId(
85
+ brunnel: BrunnelType,
86
+ category: RoadCategory,
87
+ isCasing: boolean,
88
+ ): string {
89
+ const prefix =
90
+ brunnel === "tunnel"
91
+ ? "roads_tunnels"
92
+ : brunnel === "bridge"
93
+ ? "roads_bridges"
94
+ : "roads";
95
+
96
+ const suffix = isCasing ? "_casing" : "";
97
+ return `${prefix}_${category}${suffix}`;
98
+ }
99
+
100
+ function getCasingColor(
101
+ style: Style,
102
+ brunnel: BrunnelType,
103
+ category: RoadCategory,
104
+ ): string {
105
+ if (brunnel === "tunnel") {
106
+ switch (category) {
107
+ case "other":
108
+ return style.tunnel_other_casing;
109
+ case "minor":
110
+ return style.tunnel_minor_casing;
111
+ case "link":
112
+ return style.tunnel_link_casing;
113
+ case "major":
114
+ return style.tunnel_major_casing;
115
+ case "highway":
116
+ return style.tunnel_highway_casing;
117
+ }
118
+ }
119
+ if (brunnel === "bridge") {
120
+ switch (category) {
121
+ case "other":
122
+ return style.bridges_other_casing;
123
+ case "minor":
124
+ return style.bridges_minor_casing;
125
+ case "link":
126
+ return style.bridges_minor_casing; // Uses minor casing color
127
+ case "major":
128
+ return style.bridges_major_casing;
129
+ case "highway":
130
+ return style.bridges_highway_casing;
131
+ }
132
+ }
133
+ // Regular roads (no brunnel)
134
+ switch (category) {
135
+ case "other":
136
+ return style.minor_casing; // No dedicated other casing
137
+ case "minor":
138
+ return style.minor_casing;
139
+ case "link":
140
+ return style.minor_casing;
141
+ case "major":
142
+ return style.major_casing_late;
143
+ case "highway":
144
+ return style.highway_casing_late;
145
+ }
146
+ }
147
+
148
+ function getFillColor(
149
+ style: Style,
150
+ brunnel: BrunnelType,
151
+ category: RoadCategory,
152
+ ): string | ExpressionSpecification {
153
+ if (brunnel === "tunnel") {
154
+ switch (category) {
155
+ case "other":
156
+ return style.tunnel_other;
157
+ case "minor":
158
+ return style.tunnel_minor;
159
+ case "link":
160
+ return style.tunnel_minor; // Uses minor color
161
+ case "major":
162
+ return style.tunnel_major;
163
+ case "highway":
164
+ return style.tunnel_highway;
165
+ }
166
+ }
167
+ if (brunnel === "bridge") {
168
+ switch (category) {
169
+ case "other":
170
+ return style.bridges_other;
171
+ case "minor":
172
+ return style.bridges_minor;
173
+ case "link":
174
+ return style.bridges_minor; // Uses minor color
175
+ case "major":
176
+ return style.bridges_major;
177
+ case "highway":
178
+ return style.bridges_highway;
179
+ }
180
+ }
181
+ // Regular roads (no brunnel)
182
+ switch (category) {
183
+ case "other":
184
+ return style.other;
185
+ case "minor":
186
+ return [
187
+ "interpolate",
188
+ ["exponential", 1.6],
189
+ ["zoom"],
190
+ 11,
191
+ style.minor_a,
192
+ 16,
193
+ style.minor_b,
194
+ ] as ExpressionSpecification;
195
+ case "link":
196
+ return style.link;
197
+ case "major":
198
+ return style.major;
199
+ case "highway":
200
+ return style.highway;
201
+ }
202
+ }
203
+
204
+ function getLineWidth(category: RoadCategory): ExpressionSpecification {
205
+ switch (category) {
206
+ case "other":
207
+ return LINE_WIDTHS.OTHER;
208
+ case "minor":
209
+ return LINE_WIDTHS.MINOR;
210
+ case "link":
211
+ return LINE_WIDTHS.LINK;
212
+ case "major":
213
+ return LINE_WIDTHS.MAJOR;
214
+ case "highway":
215
+ return LINE_WIDTHS.HIGHWAY;
216
+ }
217
+ }
218
+
219
+ function getCasingGapWidth(category: RoadCategory): ExpressionSpecification {
220
+ switch (category) {
221
+ case "other":
222
+ return LINE_WIDTHS.OTHER;
223
+ case "minor":
224
+ return LINE_WIDTHS.MINOR_CASING_GAP;
225
+ case "link":
226
+ return LINE_WIDTHS.LINK_CASING_GAP;
227
+ case "major":
228
+ return LINE_WIDTHS.MAJOR_CASING_GAP;
229
+ case "highway":
230
+ return LINE_WIDTHS.HIGHWAY_CASING_GAP;
231
+ }
232
+ }
233
+
234
+ function getCasingWidth(category: RoadCategory): ExpressionSpecification {
235
+ switch (category) {
236
+ case "other":
237
+ // Other roads don't have explicit casing width
238
+ return LINE_WIDTHS.CASING_THIN;
239
+ case "minor":
240
+ return LINE_WIDTHS.CASING_THIN;
241
+ case "link":
242
+ return LINE_WIDTHS.CASING_THIN;
243
+ case "major":
244
+ return LINE_WIDTHS.CASING_MEDIUM;
245
+ case "highway":
246
+ return LINE_WIDTHS.CASING_THICK;
247
+ }
248
+ }
249
+
250
+ function getDasharray(
251
+ brunnel: BrunnelType,
252
+ category: RoadCategory,
253
+ isCasing: boolean,
254
+ ): number[] | undefined {
255
+ if (brunnel === "tunnel") {
256
+ if (isCasing) {
257
+ // Tunnel casings have dashed outlines
258
+ if (category === "highway") return [6, 0.5];
259
+ if (category !== "other") return [3, 2];
260
+ } else {
261
+ // Tunnel fills for other roads
262
+ if (category === "other") return [4.5, 0.5];
263
+ }
264
+ } else if (brunnel === "bridge") {
265
+ // Bridge other roads have dash
266
+ if (!isCasing && category === "other") return [2, 1];
267
+ } else {
268
+ // Regular other roads have dash
269
+ if (!isCasing && category === "other") return [3, 1];
270
+ }
271
+ return undefined;
272
+ }
273
+
274
+ // ============================================
275
+ // FACTORY FUNCTION
276
+ // ============================================
277
+
278
+ export function createRoadLayer(
279
+ config: RoadLayerConfig,
280
+ ): LineLayerSpecification {
281
+ const { source, style, brunnel, category, isCasing, minzoom, maxzoom } =
282
+ config;
283
+
284
+ const layer: LineLayerSpecification = {
285
+ id: getLayerId(brunnel, category, isCasing),
286
+ type: "line",
287
+ source,
288
+ "source-layer": "transportation",
289
+ filter: buildFilter(brunnel, category),
290
+ paint: {},
291
+ };
292
+
293
+ if (minzoom !== undefined) layer.minzoom = minzoom;
294
+ if (maxzoom !== undefined) layer.maxzoom = maxzoom;
295
+
296
+ if (isCasing) {
297
+ layer.paint = {
298
+ "line-color": getCasingColor(style, brunnel, category),
299
+ "line-gap-width": getCasingGapWidth(category),
300
+ "line-width": getCasingWidth(category),
301
+ };
302
+ const dasharray = getDasharray(brunnel, category, true);
303
+ if (dasharray) {
304
+ layer.paint["line-dasharray"] = dasharray;
305
+ }
306
+ } else {
307
+ layer.paint = {
308
+ "line-color": getFillColor(style, brunnel, category),
309
+ "line-width": getLineWidth(category),
310
+ };
311
+ const dasharray = getDasharray(brunnel, category, false);
312
+ if (dasharray) {
313
+ layer.paint["line-dasharray"] = dasharray;
314
+ }
315
+ }
316
+
317
+ return layer;
318
+ }
319
+
320
+ // ============================================
321
+ // BATCH CREATION FUNCTIONS
322
+ // ============================================
323
+
324
+ /**
325
+ * Creates all tunnel road layers (casing + fill for each category)
326
+ */
327
+ export function createTunnelLayers(
328
+ source: string,
329
+ style: Style,
330
+ ): LayerSpecification[] {
331
+ const categories: RoadCategory[] = [
332
+ "other",
333
+ "minor",
334
+ "link",
335
+ "major",
336
+ "highway",
337
+ ];
338
+ const layers: LayerSpecification[] = [];
339
+
340
+ // Casing layers first
341
+ for (const category of categories) {
342
+ layers.push(
343
+ createRoadLayer({
344
+ source,
345
+ style,
346
+ brunnel: "tunnel",
347
+ category,
348
+ isCasing: true,
349
+ }),
350
+ );
351
+ }
352
+
353
+ // Fill layers
354
+ for (const category of categories) {
355
+ layers.push(
356
+ createRoadLayer({
357
+ source,
358
+ style,
359
+ brunnel: "tunnel",
360
+ category,
361
+ isCasing: false,
362
+ }),
363
+ );
364
+ }
365
+
366
+ return layers;
367
+ }
368
+
369
+ /**
370
+ * Creates all regular (non-tunnel, non-bridge) road layers
371
+ */
372
+ export function createRoadLayers(
373
+ source: string,
374
+ style: Style,
375
+ ): LayerSpecification[] {
376
+ const layers: LayerSpecification[] = [];
377
+
378
+ // Minor service casing (z16+)
379
+ layers.push({
380
+ id: "roads_minor_service_casing",
381
+ type: "line",
382
+ source,
383
+ "source-layer": "transportation",
384
+ minzoom: ZOOM.ROADS_MINOR_SERVICE_MIN,
385
+ filter: ["all", FILTERS.NOT_BRUNNEL, ["==", ["get", "class"], "service"]],
386
+ paint: {
387
+ "line-color": style.minor_service_casing,
388
+ "line-gap-width": [
389
+ "interpolate",
390
+ ["exponential", 1.6],
391
+ ["zoom"],
392
+ 13,
393
+ 0,
394
+ 18,
395
+ 8,
396
+ ],
397
+ "line-width": [
398
+ "interpolate",
399
+ ["exponential", 1.6],
400
+ ["zoom"],
401
+ 13,
402
+ 0,
403
+ 13.5,
404
+ 0.8,
405
+ ],
406
+ },
407
+ });
408
+
409
+ // Minor casing
410
+ layers.push(
411
+ createRoadLayer({
412
+ source,
413
+ style,
414
+ brunnel: null,
415
+ category: "minor",
416
+ isCasing: true,
417
+ }),
418
+ );
419
+
420
+ // Link casing (z13+)
421
+ layers.push({
422
+ ...createRoadLayer({
423
+ source,
424
+ style,
425
+ brunnel: null,
426
+ category: "link",
427
+ isCasing: true,
428
+ minzoom: ZOOM.ROADS_LINK_MIN,
429
+ }),
430
+ // Override filter to remove NOT_BRUNNEL (link casing shows everywhere)
431
+ filter: FILTERS.IS_RAMP,
432
+ });
433
+
434
+ // Major casing late (z12+)
435
+ layers.push(
436
+ createRoadLayer({
437
+ source,
438
+ style,
439
+ brunnel: null,
440
+ category: "major",
441
+ isCasing: true,
442
+ minzoom: ZOOM.ROADS_MAJOR_CASING_LATE_MIN,
443
+ }),
444
+ );
445
+
446
+ // Highway casing late (z12+)
447
+ layers.push(
448
+ createRoadLayer({
449
+ source,
450
+ style,
451
+ brunnel: null,
452
+ category: "highway",
453
+ isCasing: true,
454
+ minzoom: ZOOM.ROADS_HIGHWAY_CASING_LATE_MIN,
455
+ }),
456
+ );
457
+
458
+ // Other roads fill
459
+ layers.push(
460
+ createRoadLayer({
461
+ source,
462
+ style,
463
+ brunnel: null,
464
+ category: "other",
465
+ isCasing: false,
466
+ }),
467
+ );
468
+
469
+ // Link roads fill
470
+ layers.push({
471
+ ...createRoadLayer({
472
+ source,
473
+ style,
474
+ brunnel: null,
475
+ category: "link",
476
+ isCasing: false,
477
+ }),
478
+ // Override filter to remove NOT_BRUNNEL
479
+ filter: FILTERS.IS_RAMP,
480
+ });
481
+
482
+ // Minor service fill
483
+ layers.push({
484
+ id: "roads_minor_service",
485
+ type: "line",
486
+ source,
487
+ "source-layer": "transportation",
488
+ filter: ["all", FILTERS.NOT_BRUNNEL, ["==", ["get", "class"], "service"]],
489
+ paint: {
490
+ "line-color": style.minor_service,
491
+ "line-width": [
492
+ "interpolate",
493
+ ["exponential", 1.6],
494
+ ["zoom"],
495
+ 13,
496
+ 0,
497
+ 18,
498
+ 8,
499
+ ],
500
+ },
501
+ });
502
+
503
+ // Minor roads fill
504
+ layers.push(
505
+ createRoadLayer({
506
+ source,
507
+ style,
508
+ brunnel: null,
509
+ category: "minor",
510
+ isCasing: false,
511
+ }),
512
+ );
513
+
514
+ // Major casing early (maxzoom 12)
515
+ layers.push({
516
+ id: "roads_major_casing_early",
517
+ type: "line",
518
+ source,
519
+ "source-layer": "transportation",
520
+ maxzoom: ZOOM.ROADS_MAJOR_CASING_EARLY_MAX,
521
+ filter: [
522
+ "all",
523
+ FILTERS.NOT_BRUNNEL,
524
+ ["in", ["get", "class"], ["literal", [...ROAD_CLASSES.MAJOR]]],
525
+ ],
526
+ paint: {
527
+ "line-color": style.major_casing_early,
528
+ "line-gap-width": LINE_WIDTHS.MAJOR_CASING_GAP,
529
+ "line-width": LINE_WIDTHS.CASING_MEDIUM,
530
+ },
531
+ });
532
+
533
+ // Major roads fill
534
+ layers.push(
535
+ createRoadLayer({
536
+ source,
537
+ style,
538
+ brunnel: null,
539
+ category: "major",
540
+ isCasing: false,
541
+ }),
542
+ );
543
+
544
+ // Highway casing early (maxzoom 12)
545
+ layers.push({
546
+ id: "roads_highway_casing_early",
547
+ type: "line",
548
+ source,
549
+ "source-layer": "transportation",
550
+ maxzoom: ZOOM.ROADS_HIGHWAY_CASING_EARLY_MAX,
551
+ filter: [
552
+ "all",
553
+ FILTERS.NOT_BRUNNEL,
554
+ ["in", ["get", "class"], ["literal", [...ROAD_CLASSES.HIGHWAY]]],
555
+ FILTERS.NOT_RAMP,
556
+ ],
557
+ paint: {
558
+ "line-color": style.highway_casing_early,
559
+ "line-gap-width": [
560
+ "interpolate",
561
+ ["exponential", 1.6],
562
+ ["zoom"],
563
+ 3,
564
+ 0,
565
+ 3.5,
566
+ 0.5,
567
+ 18,
568
+ 10,
569
+ ],
570
+ "line-width": [
571
+ "interpolate",
572
+ ["exponential", 1.6],
573
+ ["zoom"],
574
+ 7,
575
+ 0,
576
+ 7.5,
577
+ 1,
578
+ ],
579
+ },
580
+ });
581
+
582
+ // Highway fill
583
+ layers.push(
584
+ createRoadLayer({
585
+ source,
586
+ style,
587
+ brunnel: null,
588
+ category: "highway",
589
+ isCasing: false,
590
+ }),
591
+ );
592
+
593
+ return layers;
594
+ }
595
+
596
+ /**
597
+ * Creates all bridge road layers (casing + fill for each category)
598
+ */
599
+ export function createBridgeLayers(
600
+ source: string,
601
+ style: Style,
602
+ ): LayerSpecification[] {
603
+ const categories: RoadCategory[] = ["other", "link", "minor", "major"];
604
+ const layers: LayerSpecification[] = [];
605
+
606
+ // Casing layers first (all at z12+)
607
+ for (const category of categories) {
608
+ layers.push(
609
+ createRoadLayer({
610
+ source,
611
+ style,
612
+ brunnel: "bridge",
613
+ category,
614
+ isCasing: true,
615
+ minzoom: ZOOM.ROADS_BRIDGES_MIN,
616
+ }),
617
+ );
618
+ }
619
+
620
+ // Fill layers
621
+ for (const category of categories) {
622
+ layers.push(
623
+ createRoadLayer({
624
+ source,
625
+ style,
626
+ brunnel: "bridge",
627
+ category,
628
+ isCasing: false,
629
+ minzoom: ZOOM.ROADS_BRIDGES_MIN,
630
+ }),
631
+ );
632
+ }
633
+
634
+ // Highway casing (z12+)
635
+ layers.push(
636
+ createRoadLayer({
637
+ source,
638
+ style,
639
+ brunnel: "bridge",
640
+ category: "highway",
641
+ isCasing: true,
642
+ minzoom: ZOOM.ROADS_BRIDGES_MIN,
643
+ }),
644
+ );
645
+
646
+ // Highway fill (no minzoom in original)
647
+ layers.push(
648
+ createRoadLayer({
649
+ source,
650
+ style,
651
+ brunnel: "bridge",
652
+ category: "highway",
653
+ isCasing: false,
654
+ }),
655
+ );
656
+
657
+ return layers;
658
+ }
659
+
660
+ /**
661
+ * Creates pier layer (special road type)
662
+ */
663
+ export function createPierLayer(
664
+ source: string,
665
+ style: Style,
666
+ ): LayerSpecification {
667
+ return {
668
+ id: "roads_pier",
669
+ type: "line",
670
+ source,
671
+ "source-layer": "transportation",
672
+ filter: ["==", "subclass", "pier"],
673
+ paint: {
674
+ "line-color": style.pier,
675
+ "line-width": [
676
+ "interpolate",
677
+ ["exponential", 1.6],
678
+ ["zoom"],
679
+ 12,
680
+ 0,
681
+ 12.5,
682
+ 0.5,
683
+ 20,
684
+ 16,
685
+ ],
686
+ },
687
+ };
688
+ }