@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,323 @@
1
+ import type { LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
2
+ import { LANDCOVER_SUBCLASSES, LANDUSE_CLASSES, ZOOM } from "../constants";
3
+ import type { Style } from "../styles";
4
+
5
+ /**
6
+ * Creates landcover layers (natural features)
7
+ */
8
+ export function createLandcoverLayers(
9
+ source: string,
10
+ style: Style,
11
+ ): LayerSpecification[] {
12
+ // Return empty array if landcover is not defined in style
13
+ if (!style.landcover) {
14
+ return [];
15
+ }
16
+
17
+ return [
18
+ // Main landcover (fades out at higher zoom)
19
+ {
20
+ id: "landcover",
21
+ type: "fill",
22
+ source,
23
+ "source-layer": "landcover",
24
+ paint: {
25
+ "fill-color": [
26
+ "match",
27
+ ["get", "class"],
28
+ "grass",
29
+ style.landcover.grassland,
30
+ "sand",
31
+ style.landcover.barren,
32
+ "farmland",
33
+ style.landcover.farmland,
34
+ "ice",
35
+ style.landcover.glacier,
36
+ "wood",
37
+ style.landcover.forest,
38
+ "rock",
39
+ style.landcover.rock,
40
+ "wetland",
41
+ style.landcover.wetland,
42
+ style.landcover.forest, // default
43
+ ],
44
+ "fill-opacity": [
45
+ "interpolate",
46
+ ["linear"],
47
+ ["zoom"],
48
+ ZOOM.LANDCOVER_FADE_START,
49
+ 1,
50
+ ZOOM.LANDCOVER_FADE_END,
51
+ 0,
52
+ ],
53
+ },
54
+ },
55
+ // Green areas (parks, gardens, golf courses, etc.)
56
+ {
57
+ id: "landcover_green",
58
+ type: "fill",
59
+ source,
60
+ "source-layer": "landcover",
61
+ filter: [
62
+ "in",
63
+ ["get", "subclass"],
64
+ ["literal", [...LANDCOVER_SUBCLASSES.GREEN]],
65
+ ],
66
+ paint: {
67
+ "fill-color": style.park_b,
68
+ "fill-opacity": [
69
+ "interpolate",
70
+ ["linear"],
71
+ ["zoom"],
72
+ ZOOM.LANDCOVER_GREEN_START,
73
+ 0,
74
+ ZOOM.LANDCOVER_GREEN_END,
75
+ 0.5,
76
+ ],
77
+ },
78
+ },
79
+ // Meadow and grassland
80
+ {
81
+ id: "landcover_meadow",
82
+ type: "fill",
83
+ source,
84
+ "source-layer": "landcover",
85
+ filter: [
86
+ "in",
87
+ ["get", "subclass"],
88
+ ["literal", [...LANDCOVER_SUBCLASSES.MEADOW]],
89
+ ],
90
+ paint: {
91
+ "fill-color": style.landcover.grassland,
92
+ "fill-opacity": [
93
+ "interpolate",
94
+ ["linear"],
95
+ ["zoom"],
96
+ ZOOM.LANDCOVER_GREEN_START,
97
+ 0,
98
+ ZOOM.LANDCOVER_GREEN_END,
99
+ 1,
100
+ ],
101
+ },
102
+ },
103
+ ];
104
+ }
105
+
106
+ /**
107
+ * Creates park layers
108
+ */
109
+ export function createParkLayers(
110
+ source: string,
111
+ style: Style,
112
+ ): LayerSpecification[] {
113
+ return [
114
+ {
115
+ id: "park",
116
+ type: "fill",
117
+ source,
118
+ "source-layer": "park",
119
+ paint: {
120
+ "fill-opacity": [
121
+ "interpolate",
122
+ ["linear"],
123
+ ["zoom"],
124
+ ZOOM.PARK_FADE_START,
125
+ 0,
126
+ ZOOM.PARK_FADE_END,
127
+ 1,
128
+ ],
129
+ "fill-color": [
130
+ "match",
131
+ ["get", "class"],
132
+ "national_park",
133
+ style.park_a,
134
+ "nature_reserve",
135
+ style.park_a,
136
+ "protected_area",
137
+ style.park_a,
138
+ style.park_b, // default
139
+ ],
140
+ },
141
+ },
142
+ ];
143
+ }
144
+
145
+ /**
146
+ * Creates landuse layers (human-made features)
147
+ */
148
+ export function createLanduseLayers(
149
+ source: string,
150
+ style: Style,
151
+ ): LayerSpecification[] {
152
+ return [
153
+ // Green landuse (cemeteries, playgrounds, stadiums, etc.)
154
+ {
155
+ id: "landuse_green",
156
+ type: "fill",
157
+ source,
158
+ "source-layer": "landuse",
159
+ filter: ["in", "class", ...LANDUSE_CLASSES.GREEN],
160
+ paint: {
161
+ "fill-opacity": [
162
+ "interpolate",
163
+ ["linear"],
164
+ ["zoom"],
165
+ ZOOM.LANDUSE_FADE_START,
166
+ 0,
167
+ ZOOM.LANDUSE_FADE_END,
168
+ 1,
169
+ ],
170
+ "fill-color": style.park_b,
171
+ },
172
+ },
173
+ // Hospital
174
+ {
175
+ id: "landuse_hospital",
176
+ type: "fill",
177
+ source,
178
+ "source-layer": "landuse",
179
+ filter: ["==", "class", "hospital"],
180
+ paint: {
181
+ "fill-color": style.hospital,
182
+ },
183
+ },
184
+ // Industrial
185
+ {
186
+ id: "landuse_industrial",
187
+ type: "fill",
188
+ source,
189
+ "source-layer": "landuse",
190
+ filter: ["==", "class", "industrial"],
191
+ paint: {
192
+ "fill-color": style.industrial,
193
+ },
194
+ },
195
+ // Schools and universities
196
+ {
197
+ id: "landuse_school",
198
+ type: "fill",
199
+ source,
200
+ "source-layer": "landuse",
201
+ filter: ["in", "class", ...LANDUSE_CLASSES.EDUCATION],
202
+ paint: {
203
+ "fill-color": style.school,
204
+ },
205
+ },
206
+ // Zoo
207
+ {
208
+ id: "landuse_zoo",
209
+ type: "fill",
210
+ source,
211
+ "source-layer": "landuse",
212
+ filter: ["==", "class", "zoo"],
213
+ paint: {
214
+ "fill-color": style.zoo,
215
+ },
216
+ },
217
+ // Aerodrome
218
+ {
219
+ id: "landuse_aerodrome",
220
+ type: "fill",
221
+ source,
222
+ "source-layer": "landuse",
223
+ filter: ["==", "class", "aerodrome"],
224
+ paint: {
225
+ "fill-color": style.aerodrome,
226
+ },
227
+ },
228
+ // Pedestrian areas
229
+ {
230
+ id: "landuse_pedestrian",
231
+ type: "fill",
232
+ source,
233
+ "source-layer": "landuse",
234
+ filter: ["in", "class", ...LANDUSE_CLASSES.PEDESTRIAN],
235
+ paint: {
236
+ "fill-color": style.pedestrian,
237
+ },
238
+ },
239
+ // Pier
240
+ {
241
+ id: "landuse_pier",
242
+ type: "fill",
243
+ source,
244
+ "source-layer": "landuse",
245
+ filter: ["==", "class", "pier"],
246
+ paint: {
247
+ "fill-color": style.pier,
248
+ },
249
+ },
250
+ ];
251
+ }
252
+
253
+ /**
254
+ * Creates aeroway layers
255
+ */
256
+ export function createAerowayLayers(
257
+ source: string,
258
+ style: Style,
259
+ ): LayerSpecification[] {
260
+ return [
261
+ // Aeroway fill (aprons, helipads)
262
+ {
263
+ id: "aeroway_fill",
264
+ type: "fill",
265
+ source,
266
+ "source-layer": "aeroway",
267
+ filter: [
268
+ "all",
269
+ ["==", "$type", "Polygon"],
270
+ ["in", "class", "apron", "helipad"],
271
+ ],
272
+ paint: {
273
+ "fill-color": style.runway,
274
+ },
275
+ },
276
+ // Runway
277
+ {
278
+ id: "aeroway_runway",
279
+ type: "line",
280
+ source,
281
+ "source-layer": "aeroway",
282
+ minzoom: ZOOM.AEROWAY_MIN,
283
+ filter: ["all", ["==", "$type", "LineString"], ["==", "class", "runway"]],
284
+ paint: {
285
+ "line-color": style.runway,
286
+ "line-width": [
287
+ "interpolate",
288
+ ["exponential", 1.2],
289
+ ["zoom"],
290
+ 11,
291
+ 3,
292
+ 20,
293
+ 48,
294
+ ],
295
+ },
296
+ },
297
+ // Taxiway
298
+ {
299
+ id: "aeroway_taxiway",
300
+ type: "line",
301
+ source,
302
+ "source-layer": "aeroway",
303
+ minzoom: ZOOM.AEROWAY_MIN,
304
+ filter: [
305
+ "all",
306
+ ["==", "$type", "LineString"],
307
+ ["==", "class", "taxiway"],
308
+ ],
309
+ paint: {
310
+ "line-color": style.runway,
311
+ "line-width": [
312
+ "interpolate",
313
+ ["exponential", 1.2],
314
+ ["zoom"],
315
+ 11,
316
+ 1,
317
+ 20,
318
+ 24,
319
+ ],
320
+ },
321
+ },
322
+ ];
323
+ }
@@ -0,0 +1,165 @@
1
+ import type {
2
+ DataDrivenPropertyValueSpecification,
3
+ LayerSpecification,
4
+ } from "@maplibre/maplibre-gl-style-spec";
5
+ import { POI_CATEGORIES, POI_TIERS, ZOOM } from "../constants";
6
+ import { getMultilineName } from "../language";
7
+ import type { Style } from "../styles";
8
+
9
+ /**
10
+ * Creates POI (points of interest) layers
11
+ * Uses tiered zoom levels for progressive disclosure
12
+ */
13
+ export function createPoiLayers(
14
+ source: string,
15
+ style: Style,
16
+ lang: string,
17
+ script?: string,
18
+ ): LayerSpecification[] {
19
+ // Return empty array if POIs are not defined in style
20
+ if (!style.pois) {
21
+ return [];
22
+ }
23
+
24
+ return [
25
+ {
26
+ id: "pois",
27
+ type: "symbol",
28
+ source,
29
+ "source-layer": "poi",
30
+ minzoom: ZOOM.LABELS_POI_MIN,
31
+ filter: [
32
+ "any",
33
+ // Tier 1: z10+ - Airports
34
+ ["all", [">=", ["zoom"], 10], ["==", ["get", "subclass"], "aerodrome"]],
35
+ // Tier 2: z12+ - Major infrastructure
36
+ [
37
+ "all",
38
+ [">=", ["zoom"], 12],
39
+ ["in", ["get", "subclass"], ["literal", [...POI_TIERS.TIER_2]]],
40
+ ],
41
+ // Tier 3: z14+ - Standard POIs (civic, tourism, religion)
42
+ [
43
+ "all",
44
+ [">=", ["zoom"], 14],
45
+ ["in", ["get", "subclass"], ["literal", [...POI_TIERS.TIER_3]]],
46
+ // Rank filter for density control
47
+ [
48
+ "<=",
49
+ ["coalesce", ["get", "rank"], 999],
50
+ [
51
+ "interpolate",
52
+ ["linear"],
53
+ ["zoom"],
54
+ 14,
55
+ 15,
56
+ 15,
57
+ 50,
58
+ 16,
59
+ 100,
60
+ 17,
61
+ 200,
62
+ ],
63
+ ],
64
+ ],
65
+ // Tier 4: z15+ - Food, drink, retail, and fuel
66
+ [
67
+ "all",
68
+ [">=", ["zoom"], 15],
69
+ ["in", ["get", "subclass"], ["literal", [...POI_TIERS.TIER_4]]],
70
+ // Rank filter
71
+ [
72
+ "<=",
73
+ ["coalesce", ["get", "rank"], 999],
74
+ ["interpolate", ["linear"], ["zoom"], 15, 30, 16, 75, 17, 150],
75
+ ],
76
+ ],
77
+ // Tier 5: z17+ - Minor POIs
78
+ [
79
+ "all",
80
+ [">=", ["zoom"], 17],
81
+ ["in", ["get", "subclass"], ["literal", [...POI_TIERS.TIER_5]]],
82
+ ],
83
+ // Tier 5b: z17+ - Public access only (pools, gardens)
84
+ [
85
+ "all",
86
+ [">=", ["zoom"], 17],
87
+ [
88
+ "in",
89
+ ["get", "subclass"],
90
+ ["literal", [...POI_TIERS.TIER_5B_ACCESS]],
91
+ ],
92
+ // Only show publicly accessible (not private/customers)
93
+ [
94
+ "!",
95
+ [
96
+ "in",
97
+ ["coalesce", ["get", "access"], "yes"],
98
+ ["literal", ["private", "customers", "no"]],
99
+ ],
100
+ ],
101
+ ],
102
+ ],
103
+ layout: {
104
+ "icon-image": [
105
+ "match",
106
+ ["get", "subclass"],
107
+ // Map subclass names to sprite names where they differ
108
+ "station",
109
+ "train_station",
110
+ "gallery",
111
+ "art_gallery",
112
+ // Default: use subclass name directly
113
+ ["get", "subclass"],
114
+ ],
115
+ "icon-size": 0.5, // Scale 48px Phosphor icons to ~24px
116
+ "text-font": [style.medium || "Noto Sans Medium"],
117
+ "text-justify": "auto",
118
+ "text-field": getMultilineName(
119
+ lang,
120
+ script,
121
+ style.medium,
122
+ ) as DataDrivenPropertyValueSpecification<string>,
123
+ "text-size": ["interpolate", ["linear"], ["zoom"], 17, 11, 19, 18],
124
+ "text-max-width": 8,
125
+ "text-offset": [1.1, 0],
126
+ "text-variable-anchor": ["left", "right"],
127
+ },
128
+ paint: {
129
+ "text-color": [
130
+ "case",
131
+ // Nature/Parks - green
132
+ ["in", ["get", "subclass"], ["literal", [...POI_CATEGORIES.GREEN]]],
133
+ style.pois.green,
134
+ // Transport - lapis
135
+ ["in", ["get", "subclass"], ["literal", [...POI_CATEGORIES.LAPIS]]],
136
+ style.pois.lapis,
137
+ // Civic/Services - slategray
138
+ [
139
+ "in",
140
+ ["get", "subclass"],
141
+ ["literal", [...POI_CATEGORIES.SLATEGRAY]],
142
+ ],
143
+ style.pois.slategray,
144
+ // Shopping - blue
145
+ ["in", ["get", "subclass"], ["literal", [...POI_CATEGORIES.BLUE]]],
146
+ style.pois.blue,
147
+ // Food & Drink - tangerine
148
+ [
149
+ "in",
150
+ ["get", "subclass"],
151
+ ["literal", [...POI_CATEGORIES.TANGERINE]],
152
+ ],
153
+ style.pois.tangerine,
154
+ // Tourism/Culture - pink
155
+ ["in", ["get", "subclass"], ["literal", [...POI_CATEGORIES.PINK]]],
156
+ style.pois.pink,
157
+ // Default
158
+ style.earth,
159
+ ],
160
+ "text-halo-color": style.earth,
161
+ "text-halo-width": 1,
162
+ },
163
+ },
164
+ ];
165
+ }