@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,775 @@
1
+ import type {
2
+ DataDrivenPropertyValueSpecification,
3
+ ExpressionSpecification,
4
+ FilterSpecification,
5
+ LayerSpecification,
6
+ SymbolLayerSpecification,
7
+ } from "@maplibre/maplibre-gl-style-spec";
8
+ import { getCountryName, getMultilineName } from "../language";
9
+ import type { Style } from "../styles";
10
+
11
+ // ============================================
12
+ // TYPES
13
+ // ============================================
14
+
15
+ type FontStyle = "regular" | "italic" | "bold" | "medium";
16
+
17
+ interface LabelLayerConfig {
18
+ id: string;
19
+ source: string;
20
+ sourceLayer: string;
21
+ style: Style;
22
+ lang: string;
23
+ script?: string;
24
+ filter?: FilterSpecification;
25
+ minzoom?: number;
26
+ maxzoom?: number;
27
+ fontStyle?: FontStyle;
28
+ textSize?: number | ExpressionSpecification;
29
+ textColor: string;
30
+ haloColor: string;
31
+ haloWidth?: number;
32
+ letterSpacing?: number;
33
+ maxWidth?: number;
34
+ textTransform?: "uppercase" | "lowercase" | "none";
35
+ symbolPlacement?: "point" | "line" | "line-center";
36
+ useCountryName?: boolean;
37
+ }
38
+
39
+ // ============================================
40
+ // HELPER FUNCTIONS
41
+ // ============================================
42
+
43
+ function getFont(style: Style, fontStyle: FontStyle): string {
44
+ switch (fontStyle) {
45
+ case "regular":
46
+ return style.regular || "Noto Sans Regular";
47
+ case "italic":
48
+ return style.italic || "Noto Sans Italic";
49
+ case "bold":
50
+ return style.bold || "Noto Sans Medium";
51
+ case "medium":
52
+ return style.medium || "Noto Sans Medium";
53
+ }
54
+ }
55
+
56
+ // ============================================
57
+ // FACTORY FUNCTION
58
+ // ============================================
59
+
60
+ export function createLabelLayer(
61
+ config: LabelLayerConfig,
62
+ ): SymbolLayerSpecification {
63
+ const {
64
+ id,
65
+ source,
66
+ sourceLayer,
67
+ style,
68
+ lang,
69
+ script,
70
+ filter,
71
+ minzoom,
72
+ maxzoom,
73
+ fontStyle = "regular",
74
+ textSize = 12,
75
+ textColor,
76
+ haloColor,
77
+ haloWidth = 1,
78
+ letterSpacing,
79
+ maxWidth,
80
+ textTransform,
81
+ symbolPlacement = "point",
82
+ useCountryName = false,
83
+ } = config;
84
+
85
+ const font = getFont(style, fontStyle);
86
+ const textField = useCountryName
87
+ ? getCountryName(lang, script)
88
+ : getMultilineName(lang, script, style.regular);
89
+
90
+ const layout: SymbolLayerSpecification["layout"] = {
91
+ "text-font": [font],
92
+ "text-field": textField as DataDrivenPropertyValueSpecification<string>,
93
+ "text-size": textSize,
94
+ };
95
+
96
+ if (symbolPlacement !== "point") {
97
+ layout["symbol-placement"] = symbolPlacement;
98
+ }
99
+
100
+ if (letterSpacing !== undefined) {
101
+ layout["text-letter-spacing"] = letterSpacing;
102
+ }
103
+
104
+ if (maxWidth !== undefined) {
105
+ layout["text-max-width"] = maxWidth;
106
+ }
107
+
108
+ if (textTransform) {
109
+ layout["text-transform"] = textTransform;
110
+ }
111
+
112
+ const layer: SymbolLayerSpecification = {
113
+ id,
114
+ type: "symbol",
115
+ source,
116
+ "source-layer": sourceLayer,
117
+ layout,
118
+ paint: {
119
+ "text-color": textColor,
120
+ "text-halo-color": haloColor,
121
+ "text-halo-width": haloWidth,
122
+ },
123
+ };
124
+
125
+ if (filter) layer.filter = filter;
126
+ if (minzoom !== undefined) layer.minzoom = minzoom;
127
+ if (maxzoom !== undefined) layer.maxzoom = maxzoom;
128
+
129
+ return layer;
130
+ }
131
+
132
+ // ============================================
133
+ // SPECIALIZED LABEL CREATORS
134
+ // ============================================
135
+
136
+ /**
137
+ * Creates water label layers (ocean, lakes, waterways)
138
+ */
139
+ export function createWaterLabels(
140
+ source: string,
141
+ style: Style,
142
+ lang: string,
143
+ script?: string,
144
+ ): LayerSpecification[] {
145
+ return [
146
+ // Waterway labels (rivers, streams)
147
+ createLabelLayer({
148
+ id: "water_waterway_label",
149
+ source,
150
+ sourceLayer: "water_name",
151
+ style,
152
+ lang,
153
+ script,
154
+ minzoom: 13,
155
+ filter: ["in", "class", "river", "stream"],
156
+ fontStyle: "italic",
157
+ textColor: style.ocean_label,
158
+ haloColor: style.water,
159
+ symbolPlacement: "line",
160
+ letterSpacing: 0.2,
161
+ }),
162
+ // Ocean labels
163
+ {
164
+ id: "water_label_ocean",
165
+ type: "symbol",
166
+ source,
167
+ "source-layer": "water_name",
168
+ filter: ["in", ["get", "class"], ["literal", ["sea", "ocean"]]],
169
+ layout: {
170
+ "text-font": [style.italic || "Noto Sans Italic"],
171
+ "text-field": getMultilineName(
172
+ lang,
173
+ script,
174
+ style.regular,
175
+ ) as DataDrivenPropertyValueSpecification<string>,
176
+ "text-size": ["interpolate", ["linear"], ["zoom"], 3, 10, 10, 12],
177
+ "text-letter-spacing": 0.1,
178
+ "text-max-width": 9,
179
+ "text-transform": "uppercase",
180
+ },
181
+ paint: {
182
+ "text-color": style.ocean_label,
183
+ "text-halo-width": 1,
184
+ "text-halo-color": style.water,
185
+ },
186
+ },
187
+ // Lake labels (Point geometry)
188
+ {
189
+ id: "water_label_lakes_point",
190
+ type: "symbol",
191
+ source,
192
+ "source-layer": "water_name",
193
+ filter: [
194
+ "all",
195
+ ["==", ["get", "class"], "lake"],
196
+ ["==", ["geometry-type"], "Point"],
197
+ ],
198
+ layout: {
199
+ "text-font": [style.italic || "Noto Sans Italic"],
200
+ "text-field": getMultilineName(
201
+ lang,
202
+ script,
203
+ style.regular,
204
+ ) as DataDrivenPropertyValueSpecification<string>,
205
+ "text-size": [
206
+ "interpolate",
207
+ ["linear"],
208
+ ["zoom"],
209
+ 3,
210
+ 10,
211
+ 6,
212
+ 12,
213
+ 10,
214
+ 12,
215
+ ],
216
+ "text-letter-spacing": 0.1,
217
+ "text-max-width": 9,
218
+ },
219
+ paint: {
220
+ "text-color": style.ocean_label,
221
+ "text-halo-color": style.water,
222
+ "text-halo-width": 1,
223
+ },
224
+ },
225
+ // Lake labels (LineString geometry)
226
+ {
227
+ id: "water_label_lakes_line",
228
+ type: "symbol",
229
+ source,
230
+ "source-layer": "water_name",
231
+ filter: [
232
+ "all",
233
+ ["==", ["get", "class"], "lake"],
234
+ ["==", ["geometry-type"], "LineString"],
235
+ ],
236
+ layout: {
237
+ "symbol-placement": "line-center",
238
+ "text-rotation-alignment": "viewport",
239
+ "text-font": [style.italic || "Noto Sans Italic"],
240
+ "text-field": getMultilineName(
241
+ lang,
242
+ script,
243
+ style.regular,
244
+ ) as DataDrivenPropertyValueSpecification<string>,
245
+ "text-size": [
246
+ "interpolate",
247
+ ["linear"],
248
+ ["zoom"],
249
+ 3,
250
+ 10,
251
+ 6,
252
+ 12,
253
+ 10,
254
+ 12,
255
+ ],
256
+ "text-letter-spacing": 0.1,
257
+ "text-max-width": 9,
258
+ },
259
+ paint: {
260
+ "text-color": style.ocean_label,
261
+ "text-halo-color": style.water,
262
+ "text-halo-width": 1,
263
+ },
264
+ },
265
+ ];
266
+ }
267
+
268
+ /**
269
+ * Creates road label layers
270
+ */
271
+ export function createRoadLabels(
272
+ source: string,
273
+ style: Style,
274
+ lang: string,
275
+ script?: string,
276
+ ): LayerSpecification[] {
277
+ return [
278
+ // Oneway arrows
279
+ {
280
+ id: "roads_oneway",
281
+ type: "symbol",
282
+ source,
283
+ "source-layer": "transportation",
284
+ minzoom: 16,
285
+ filter: ["==", ["get", "oneway"], "yes"],
286
+ layout: {
287
+ "symbol-placement": "line",
288
+ "icon-image": "arrow_right",
289
+ "icon-rotate": 0,
290
+ "symbol-spacing": 100,
291
+ },
292
+ },
293
+ // Minor road labels
294
+ {
295
+ id: "roads_labels_minor",
296
+ type: "symbol",
297
+ source,
298
+ "source-layer": "transportation_name",
299
+ minzoom: 15,
300
+ filter: [
301
+ "in",
302
+ ["get", "class"],
303
+ [
304
+ "literal",
305
+ ["minor", "residential", "unclassified", "service", "track", "path"],
306
+ ],
307
+ ],
308
+ layout: {
309
+ "symbol-sort-key": ["coalesce", ["get", "min_zoom"], 20],
310
+ "symbol-placement": "line",
311
+ "text-font": [style.regular || "Noto Sans Regular"],
312
+ "text-field": getMultilineName(
313
+ lang,
314
+ script,
315
+ style.regular,
316
+ ) as DataDrivenPropertyValueSpecification<string>,
317
+ "text-size": 12,
318
+ },
319
+ paint: {
320
+ "text-color": style.roads_label_minor,
321
+ "text-halo-color": style.roads_label_minor_halo,
322
+ "text-halo-width": 1,
323
+ },
324
+ },
325
+ // Road shields
326
+ {
327
+ id: "roads_shields",
328
+ type: "symbol",
329
+ source,
330
+ "source-layer": "transportation_name",
331
+ filter: [
332
+ "all",
333
+ [
334
+ "in",
335
+ ["get", "class"],
336
+ [
337
+ "literal",
338
+ ["motorway", "trunk", "primary", "secondary", "tertiary"],
339
+ ],
340
+ ],
341
+ ["has", "ref"],
342
+ ["<=", ["length", ["get", "ref"]], 5],
343
+ ],
344
+ layout: {
345
+ "icon-image": [
346
+ "concat",
347
+ "shield|",
348
+ ["coalesce", ["get", "network"], "default"],
349
+ "=",
350
+ ["get", "ref"],
351
+ "|",
352
+ ["coalesce", ["get", "name"], ""],
353
+ ],
354
+ "text-field": ["get", "ref"],
355
+ "text-font": [style.bold || "Noto Sans Medium"],
356
+ "text-size": 8,
357
+ "icon-size": 0.8,
358
+ "symbol-placement": "line",
359
+ "icon-rotation-alignment": "viewport",
360
+ "text-rotation-alignment": "viewport",
361
+ },
362
+ paint: {
363
+ "text-color": style.roads_label_major,
364
+ "text-opacity": 0,
365
+ },
366
+ },
367
+ // Major road labels
368
+ {
369
+ id: "roads_labels_major",
370
+ type: "symbol",
371
+ source,
372
+ "source-layer": "transportation_name",
373
+ minzoom: 11,
374
+ filter: [
375
+ "in",
376
+ ["get", "class"],
377
+ ["literal", ["motorway", "trunk", "primary", "secondary", "tertiary"]],
378
+ ],
379
+ layout: {
380
+ "symbol-sort-key": ["coalesce", ["get", "min_zoom"], 20],
381
+ "symbol-placement": "line",
382
+ "text-font": [style.regular || "Noto Sans Regular"],
383
+ "text-field": getMultilineName(
384
+ lang,
385
+ script,
386
+ style.regular,
387
+ ) as DataDrivenPropertyValueSpecification<string>,
388
+ "text-size": 12,
389
+ },
390
+ paint: {
391
+ "text-color": style.roads_label_major,
392
+ "text-halo-color": style.roads_label_major_halo,
393
+ "text-halo-width": 1,
394
+ },
395
+ },
396
+ ];
397
+ }
398
+
399
+ /**
400
+ * Creates place label layers (countries, states, cities, subplaces)
401
+ */
402
+ export function createPlaceLabels(
403
+ source: string,
404
+ style: Style,
405
+ lang: string,
406
+ script?: string,
407
+ ): LayerSpecification[] {
408
+ return [
409
+ // Subplace labels (neighbourhood, quarter, suburb)
410
+ {
411
+ id: "places_subplace",
412
+ type: "symbol",
413
+ source,
414
+ "source-layer": "place",
415
+ filter: [
416
+ "in",
417
+ ["get", "class"],
418
+ ["literal", ["neighbourhood", "quarter", "suburb"]],
419
+ ],
420
+ layout: {
421
+ "symbol-sort-key": [
422
+ "case",
423
+ ["has", "sort_key"],
424
+ ["get", "sort_key"],
425
+ ["coalesce", ["get", "min_zoom"], 20],
426
+ ],
427
+ "text-field": getMultilineName(
428
+ lang,
429
+ script,
430
+ style.medium,
431
+ ) as DataDrivenPropertyValueSpecification<string>,
432
+ "text-font": [style.medium || "Noto Sans Medium"],
433
+ "text-max-width": 7,
434
+ "text-letter-spacing": 0.1,
435
+ "text-padding": [
436
+ "interpolate",
437
+ ["linear"],
438
+ ["zoom"],
439
+ 5,
440
+ 2,
441
+ 8,
442
+ 4,
443
+ 12,
444
+ 18,
445
+ 15,
446
+ 20,
447
+ ],
448
+ "text-size": [
449
+ "interpolate",
450
+ ["exponential", 1.2],
451
+ ["zoom"],
452
+ 11,
453
+ 8,
454
+ 14,
455
+ 14,
456
+ 18,
457
+ 24,
458
+ ],
459
+ },
460
+ paint: {
461
+ "text-color": style.subplace_label,
462
+ "text-halo-color": style.subplace_label_halo,
463
+ "text-halo-width": 1,
464
+ },
465
+ },
466
+ // Region/state labels
467
+ {
468
+ id: "places_region",
469
+ type: "symbol",
470
+ source,
471
+ "source-layer": "place",
472
+ filter: ["==", ["get", "class"], "state"],
473
+ layout: {
474
+ "symbol-sort-key": ["get", "sort_key"],
475
+ "text-field": [
476
+ "step",
477
+ ["zoom"],
478
+ ["coalesce", ["get", "ref:en"], ["get", "ref"]],
479
+ 6,
480
+ getMultilineName(
481
+ lang,
482
+ script,
483
+ style.regular,
484
+ ) as ExpressionSpecification,
485
+ ],
486
+ "text-font": [style.regular || "Noto Sans Regular"],
487
+ "text-size": ["interpolate", ["linear"], ["zoom"], 3, 11, 7, 16],
488
+ "text-radial-offset": 0.2,
489
+ "text-anchor": "center",
490
+ },
491
+ paint: {
492
+ "text-color": style.state_label,
493
+ "text-halo-color": style.state_label_halo,
494
+ "text-halo-width": 1,
495
+ },
496
+ },
497
+ // Locality labels (city, town, village, hamlet)
498
+ {
499
+ id: "places_locality",
500
+ type: "symbol",
501
+ source,
502
+ "source-layer": "place",
503
+ filter: [
504
+ "in",
505
+ ["get", "class"],
506
+ ["literal", ["city", "town", "village", "hamlet"]],
507
+ ],
508
+ layout: {
509
+ "icon-image": [
510
+ "step",
511
+ ["zoom"],
512
+ [
513
+ "case",
514
+ // Capital cities get star
515
+ ["==", ["get", "capital"], "yes"],
516
+ "circle-star",
517
+ // Cities get scrubber
518
+ ["==", ["get", "class"], "city"],
519
+ "scrubber",
520
+ // Towns, villages, hamlets get circle
521
+ "circle",
522
+ ],
523
+ 8,
524
+ "",
525
+ ],
526
+ "icon-size": 0.7,
527
+ "text-field": getMultilineName(
528
+ lang,
529
+ script,
530
+ style.medium,
531
+ ) as DataDrivenPropertyValueSpecification<string>,
532
+ "text-font": [
533
+ "case",
534
+ ["<=", ["coalesce", ["get", "min_zoom"], 20], 5],
535
+ ["literal", [style.bold || "Noto Sans Medium"]],
536
+ ["literal", [style.medium || "Noto Sans Medium"]],
537
+ ],
538
+ "symbol-sort-key": [
539
+ "case",
540
+ ["has", "sort_key"],
541
+ ["get", "sort_key"],
542
+ ["coalesce", ["get", "min_zoom"], 20],
543
+ ],
544
+ "text-padding": [
545
+ "interpolate",
546
+ ["linear"],
547
+ ["zoom"],
548
+ 5,
549
+ 3,
550
+ 8,
551
+ 7,
552
+ 12,
553
+ 11,
554
+ ],
555
+ "text-size": [
556
+ "interpolate",
557
+ ["linear"],
558
+ ["zoom"],
559
+ 2,
560
+ [
561
+ "case",
562
+ ["<", ["get", "population_rank"], 13],
563
+ 8,
564
+ [">=", ["get", "population_rank"], 13],
565
+ 13,
566
+ 0,
567
+ ],
568
+ 4,
569
+ [
570
+ "case",
571
+ ["<", ["get", "population_rank"], 13],
572
+ 10,
573
+ [">=", ["get", "population_rank"], 13],
574
+ 15,
575
+ 0,
576
+ ],
577
+ 6,
578
+ [
579
+ "case",
580
+ ["<", ["get", "population_rank"], 12],
581
+ 11,
582
+ [">=", ["get", "population_rank"], 12],
583
+ 17,
584
+ 0,
585
+ ],
586
+ 8,
587
+ [
588
+ "case",
589
+ ["<", ["get", "population_rank"], 11],
590
+ 11,
591
+ [">=", ["get", "population_rank"], 11],
592
+ 18,
593
+ 0,
594
+ ],
595
+ 10,
596
+ [
597
+ "case",
598
+ ["<", ["get", "population_rank"], 9],
599
+ 12,
600
+ [">=", ["get", "population_rank"], 9],
601
+ 20,
602
+ 0,
603
+ ],
604
+ 15,
605
+ [
606
+ "case",
607
+ ["<", ["get", "population_rank"], 8],
608
+ 12,
609
+ [">=", ["get", "population_rank"], 8],
610
+ 22,
611
+ 0,
612
+ ],
613
+ ],
614
+ "icon-padding": [
615
+ "interpolate",
616
+ ["linear"],
617
+ ["zoom"],
618
+ 0,
619
+ 0,
620
+ 8,
621
+ 4,
622
+ 10,
623
+ 8,
624
+ 12,
625
+ 6,
626
+ 22,
627
+ 2,
628
+ ],
629
+ "text-justify": "auto",
630
+ "text-variable-anchor": [
631
+ "step",
632
+ ["zoom"],
633
+ ["literal", ["bottom", "left", "right", "top"]],
634
+ 8,
635
+ ["literal", ["center"]],
636
+ ],
637
+ "text-radial-offset": 0.3,
638
+ },
639
+ paint: {
640
+ "text-color": style.city_label,
641
+ "text-halo-color": style.city_label_halo,
642
+ "text-halo-width": 1,
643
+ },
644
+ },
645
+ // Country labels
646
+ {
647
+ id: "places_country",
648
+ type: "symbol",
649
+ source,
650
+ "source-layer": "place",
651
+ filter: ["==", ["get", "class"], "country"],
652
+ layout: {
653
+ "symbol-sort-key": [
654
+ "case",
655
+ ["has", "sort_key"],
656
+ ["get", "sort_key"],
657
+ ["coalesce", ["get", "min_zoom"], 20],
658
+ ],
659
+ "text-field": getCountryName(
660
+ lang,
661
+ script,
662
+ ) as DataDrivenPropertyValueSpecification<string>,
663
+ "text-font": [style.bold || "Noto Sans Medium"],
664
+ "text-size": [
665
+ "interpolate",
666
+ ["linear"],
667
+ ["zoom"],
668
+ 2,
669
+ [
670
+ "case",
671
+ ["<", ["get", "population_rank"], 10],
672
+ 8,
673
+ [">=", ["get", "population_rank"], 10],
674
+ 12,
675
+ 0,
676
+ ],
677
+ 6,
678
+ [
679
+ "case",
680
+ ["<", ["get", "population_rank"], 8],
681
+ 10,
682
+ [">=", ["get", "population_rank"], 8],
683
+ 18,
684
+ 0,
685
+ ],
686
+ 8,
687
+ [
688
+ "case",
689
+ ["<", ["get", "population_rank"], 7],
690
+ 11,
691
+ [">=", ["get", "population_rank"], 7],
692
+ 20,
693
+ 0,
694
+ ],
695
+ ],
696
+ "icon-padding": [
697
+ "interpolate",
698
+ ["linear"],
699
+ ["zoom"],
700
+ 0,
701
+ 2,
702
+ 14,
703
+ 2,
704
+ 16,
705
+ 20,
706
+ 17,
707
+ 2,
708
+ 22,
709
+ 2,
710
+ ],
711
+ },
712
+ paint: {
713
+ "text-color": style.country_label,
714
+ "text-halo-color": style.earth,
715
+ "text-halo-width": 1,
716
+ },
717
+ },
718
+ ];
719
+ }
720
+
721
+ /**
722
+ * Creates address and island labels
723
+ */
724
+ export function createMiscLabels(
725
+ source: string,
726
+ style: Style,
727
+ lang: string,
728
+ script?: string,
729
+ ): LayerSpecification[] {
730
+ return [
731
+ // Address labels
732
+ {
733
+ id: "address_label",
734
+ type: "symbol",
735
+ source,
736
+ "source-layer": "housenumber",
737
+ minzoom: 18,
738
+ layout: {
739
+ "symbol-placement": "point",
740
+ "text-font": [style.italic || "Noto Sans Italic"],
741
+ "text-field": ["get", "housenumber"],
742
+ "text-size": 12,
743
+ },
744
+ paint: {
745
+ "text-color": style.address_label,
746
+ "text-halo-color": style.address_label_halo,
747
+ "text-halo-width": 1,
748
+ },
749
+ },
750
+ // Island labels
751
+ {
752
+ id: "earth_label_islands",
753
+ type: "symbol",
754
+ source,
755
+ "source-layer": "place",
756
+ filter: ["==", ["get", "class"], "island"],
757
+ layout: {
758
+ "text-font": [style.italic || "Noto Sans Italic"],
759
+ "text-field": getMultilineName(
760
+ lang,
761
+ script,
762
+ style.regular,
763
+ ) as DataDrivenPropertyValueSpecification<string>,
764
+ "text-size": 10,
765
+ "text-letter-spacing": 0.1,
766
+ "text-max-width": 8,
767
+ },
768
+ paint: {
769
+ "text-color": style.subplace_label,
770
+ "text-halo-color": style.subplace_label_halo,
771
+ "text-halo-width": 1,
772
+ },
773
+ },
774
+ ];
775
+ }