1use serde::{Deserialize, Serialize};
2use serde_json::{json, Value};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Style {
7 pub version: u8,
8 pub name: String,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 pub glyphs: Option<String>,
11 pub sources: Value,
12 pub layers: Vec<Value>,
13}
14
15impl Style {
16 pub fn to_json(&self) -> String {
17 serde_json::to_string_pretty(self).unwrap_or_default()
18 }
19}
20
21pub fn default_style_json(source_url: &str) -> Style {
26 let is_pmtiles = source_url.starts_with("pmtiles://") || source_url.ends_with(".pmtiles");
27
28 let source = if is_pmtiles {
29 json!({
30 "osmic": {
31 "type": "vector",
32 "url": source_url
33 }
34 })
35 } else {
36 json!({
37 "osmic": {
38 "type": "vector",
39 "tiles": [source_url]
40 }
41 })
42 };
43
44 let layers = vec![
45 json!({
47 "id": "background",
48 "type": "background",
49 "paint": { "background-color": "#f8f4f0" }
50 }),
51 json!({
53 "id": "landuse-fill",
54 "type": "fill",
55 "source": "osmic",
56 "source-layer": "landuse",
57 "minzoom": 7,
58 "paint": {
59 "fill-color": [
60 "match", ["get", "class"],
61 "forest", "#add19e",
62 "grass", "#cdebb0",
63 "meadow", "#cdebb0",
64 "farmland", "#d5e29e",
65 "residential", "#e0d6d0",
66 "commercial", "#f2dad9",
67 "industrial", "#ebdbe8",
68 "cemetery", "#aacbaf",
69 "#d5cfc8"
70 ],
71 "fill-opacity": 0.8
72 }
73 }),
74 json!({
76 "id": "natural-fill",
77 "type": "fill",
78 "source": "osmic",
79 "source-layer": "natural",
80 "minzoom": 6,
81 "paint": {
82 "fill-color": [
83 "match", ["get", "class"],
84 "wood", "#add19e",
85 "scrub", "#c8d7ab",
86 "grassland", "#cdebb0",
87 "sand", "#f5e9c6",
88 "beach", "#fff1ba",
89 "glacier", "#ddecec",
90 "#e8e0d8"
91 ],
92 "fill-opacity": 0.6
93 }
94 }),
95 json!({
97 "id": "leisure-fill",
98 "type": "fill",
99 "source": "osmic",
100 "source-layer": "leisure",
101 "minzoom": 8,
102 "paint": {
103 "fill-color": [
104 "match", ["get", "class"],
105 "park", "#c8facc",
106 "garden", "#cdebb0",
107 "golf_course", "#b5e3b5",
108 "nature_reserve", "#cdebb0",
109 "#c8facc"
110 ],
111 "fill-opacity": 0.6
112 }
113 }),
114 json!({
116 "id": "water-fill",
117 "type": "fill",
118 "source": "osmic",
119 "source-layer": "water",
120 "filter": ["in", "class", "lake", "pond", "reservoir", "basin"],
121 "paint": {
122 "fill-color": "#aad3df",
123 "fill-opacity": 0.8
124 }
125 }),
126 json!({
128 "id": "water-line",
129 "type": "line",
130 "source": "osmic",
131 "source-layer": "water",
132 "filter": ["in", "class", "river", "stream", "canal", "drain", "ditch"],
133 "paint": {
134 "line-color": "#aad3df",
135 "line-width": [
136 "match", ["get", "class"],
137 "river", 3,
138 "canal", 2,
139 1
140 ]
141 }
142 }),
143 json!({
145 "id": "building-fill",
146 "type": "fill",
147 "source": "osmic",
148 "source-layer": "building",
149 "minzoom": 13,
150 "paint": {
151 "fill-color": "#dfdbd7",
152 "fill-opacity": 0.8
153 }
154 }),
155 json!({
157 "id": "building-outline",
158 "type": "line",
159 "source": "osmic",
160 "source-layer": "building",
161 "minzoom": 14,
162 "paint": {
163 "line-color": "#c9c0b8",
164 "line-width": 0.5
165 }
166 }),
167 json!({
169 "id": "boundary",
170 "type": "line",
171 "source": "osmic",
172 "source-layer": "boundary",
173 "minzoom": 2,
174 "paint": {
175 "line-color": "#9e9cab",
176 "line-width": 1.5,
177 "line-dasharray": [4, 2]
178 }
179 }),
180 json!({
182 "id": "railway",
183 "type": "line",
184 "source": "osmic",
185 "source-layer": "railway",
186 "minzoom": 8,
187 "paint": {
188 "line-color": "#bfbfbf",
189 "line-width": 1.0
190 }
191 }),
192 json!({
194 "id": "highway-casing",
195 "type": "line",
196 "source": "osmic",
197 "source-layer": "highway",
198 "minzoom": 7,
199 "layout": { "line-cap": "round", "line-join": "round" },
200 "paint": {
201 "line-color": "#c0b8b0",
202 "line-width": [
203 "match", ["get", "class"],
204 "motorway", 8,
205 "trunk", 7,
206 "primary", 6,
207 "secondary", 5,
208 "tertiary", 4,
209 "residential", 3,
210 "service", 2,
211 1.5
212 ]
213 }
214 }),
215 json!({
217 "id": "highway-fill",
218 "type": "line",
219 "source": "osmic",
220 "source-layer": "highway",
221 "minzoom": 4,
222 "layout": { "line-cap": "round", "line-join": "round" },
223 "paint": {
224 "line-color": [
225 "match", ["get", "class"],
226 "motorway", "#e892a2",
227 "motorway_link", "#e892a2",
228 "trunk", "#f9b29c",
229 "trunk_link", "#f9b29c",
230 "primary", "#fcd6a4",
231 "primary_link", "#fcd6a4",
232 "secondary", "#f7fabf",
233 "secondary_link", "#f7fabf",
234 "tertiary", "#ffffff",
235 "tertiary_link", "#ffffff",
236 "#ffffff"
237 ],
238 "line-width": [
239 "match", ["get", "class"],
240 "motorway", 6,
241 "trunk", 5,
242 "primary", 4,
243 "secondary", 3,
244 "tertiary", 2.5,
245 "residential", 1.5,
246 "service", 1,
247 0.75
248 ]
249 }
250 }),
251 json!({
253 "id": "highway-label-major",
254 "type": "symbol",
255 "source": "osmic",
256 "source-layer": "highway",
257 "minzoom": 10,
258 "filter": ["in", "class", "motorway", "trunk", "primary", "secondary"],
259 "layout": {
260 "text-field": ["get", "name"],
261 "text-font": ["Open Sans Regular"],
262 "text-size": [
263 "match", ["get", "class"],
264 "motorway", 13,
265 "trunk", 12,
266 "primary", 11,
267 10
268 ],
269 "symbol-placement": "line",
270 "text-rotation-alignment": "map",
271 "text-max-angle": 30,
272 "text-padding": 20
273 },
274 "paint": {
275 "text-color": "#555",
276 "text-halo-color": "#fff",
277 "text-halo-width": 1.5
278 }
279 }),
280 json!({
281 "id": "highway-label-minor",
282 "type": "symbol",
283 "source": "osmic",
284 "source-layer": "highway",
285 "minzoom": 14,
286 "filter": ["in", "class", "tertiary", "residential", "unclassified", "service", "living_street"],
287 "layout": {
288 "text-field": ["get", "name"],
289 "text-font": ["Open Sans Regular"],
290 "text-size": 10,
291 "symbol-placement": "line",
292 "text-rotation-alignment": "map",
293 "text-max-angle": 30,
294 "text-padding": 10
295 },
296 "paint": {
297 "text-color": "#666",
298 "text-halo-color": "#fff",
299 "text-halo-width": 1.0
300 }
301 }),
302 json!({
304 "id": "water-label",
305 "type": "symbol",
306 "source": "osmic",
307 "source-layer": "water",
308 "minzoom": 10,
309 "filter": ["has", "name"],
310 "layout": {
311 "text-field": ["get", "name"],
312 "text-font": ["Open Sans Regular"],
313 "text-size": 12,
314 "symbol-placement": "line",
315 "text-rotation-alignment": "map",
316 "text-max-angle": 30,
317 "text-padding": 30
318 },
319 "paint": {
320 "text-color": "#6b9daf",
321 "text-halo-color": "#fff",
322 "text-halo-width": 1.0
323 }
324 }),
325 json!({
327 "id": "area-label",
328 "type": "symbol",
329 "source": "osmic",
330 "source-layer": "leisure",
331 "minzoom": 12,
332 "filter": ["has", "name"],
333 "layout": {
334 "text-field": ["get", "name"],
335 "text-font": ["Open Sans Regular"],
336 "text-size": 11,
337 "text-padding": 10
338 },
339 "paint": {
340 "text-color": "#3a7a3a",
341 "text-halo-color": "#fff",
342 "text-halo-width": 1.0
343 }
344 }),
345 json!({
347 "id": "amenity-label",
348 "type": "symbol",
349 "source": "osmic",
350 "source-layer": "amenity",
351 "minzoom": 15,
352 "filter": ["has", "name"],
353 "layout": {
354 "text-field": ["get", "name"],
355 "text-font": ["Open Sans Regular"],
356 "text-size": 10,
357 "text-padding": 5,
358 "icon-allow-overlap": false,
359 "text-allow-overlap": false
360 },
361 "paint": {
362 "text-color": "#734a08",
363 "text-halo-color": "#fff",
364 "text-halo-width": 1.0
365 }
366 }),
367 json!({
369 "id": "shop-label",
370 "type": "symbol",
371 "source": "osmic",
372 "source-layer": "shop",
373 "minzoom": 15,
374 "filter": ["has", "name"],
375 "layout": {
376 "text-field": ["get", "name"],
377 "text-font": ["Open Sans Regular"],
378 "text-size": 10,
379 "text-padding": 5,
380 "text-allow-overlap": false
381 },
382 "paint": {
383 "text-color": "#5b3a0a",
384 "text-halo-color": "#fff",
385 "text-halo-width": 1.0
386 }
387 }),
388 json!({
390 "id": "tourism-label",
391 "type": "symbol",
392 "source": "osmic",
393 "source-layer": "tourism",
394 "minzoom": 14,
395 "filter": ["has", "name"],
396 "layout": {
397 "text-field": ["get", "name"],
398 "text-font": ["Open Sans Regular"],
399 "text-size": 10,
400 "text-padding": 5,
401 "text-allow-overlap": false
402 },
403 "paint": {
404 "text-color": "#0d7377",
405 "text-halo-color": "#fff",
406 "text-halo-width": 1.0
407 }
408 }),
409 json!({
411 "id": "healthcare-label",
412 "type": "symbol",
413 "source": "osmic",
414 "source-layer": "healthcare",
415 "minzoom": 15,
416 "filter": ["has", "name"],
417 "layout": {
418 "text-field": ["get", "name"],
419 "text-font": ["Open Sans Regular"],
420 "text-size": 10,
421 "text-padding": 5,
422 "text-allow-overlap": false
423 },
424 "paint": {
425 "text-color": "#c4281c",
426 "text-halo-color": "#fff",
427 "text-halo-width": 1.0
428 }
429 }),
430 json!({
432 "id": "office-label",
433 "type": "symbol",
434 "source": "osmic",
435 "source-layer": "office",
436 "minzoom": 15,
437 "filter": ["has", "name"],
438 "layout": {
439 "text-field": ["get", "name"],
440 "text-font": ["Open Sans Regular"],
441 "text-size": 10,
442 "text-padding": 5,
443 "text-allow-overlap": false
444 },
445 "paint": {
446 "text-color": "#555",
447 "text-halo-color": "#fff",
448 "text-halo-width": 1.0
449 }
450 }),
451 json!({
453 "id": "craft-label",
454 "type": "symbol",
455 "source": "osmic",
456 "source-layer": "craft",
457 "minzoom": 15,
458 "filter": ["has", "name"],
459 "layout": {
460 "text-field": ["get", "name"],
461 "text-font": ["Open Sans Regular"],
462 "text-size": 10,
463 "text-padding": 5,
464 "text-allow-overlap": false
465 },
466 "paint": {
467 "text-color": "#b5651d",
468 "text-halo-color": "#fff",
469 "text-halo-width": 1.0
470 }
471 }),
472 json!({
474 "id": "historic-label",
475 "type": "symbol",
476 "source": "osmic",
477 "source-layer": "historic",
478 "minzoom": 14,
479 "filter": ["has", "name"],
480 "layout": {
481 "text-field": ["get", "name"],
482 "text-font": ["Open Sans Regular"],
483 "text-size": 10,
484 "text-padding": 5,
485 "text-allow-overlap": false
486 },
487 "paint": {
488 "text-color": "#7b2d8b",
489 "text-halo-color": "#fff",
490 "text-halo-width": 1.0
491 }
492 }),
493 json!({
495 "id": "place-label",
496 "type": "symbol",
497 "source": "osmic",
498 "source-layer": "place",
499 "minzoom": 4,
500 "layout": {
501 "text-field": ["get", "name"],
502 "text-size": [
503 "match", ["get", "class"],
504 "city", 20,
505 "town", 15,
506 "village", 12,
507 10
508 ],
509 "text-font": ["Open Sans Regular"],
510 "text-padding": 5
511 },
512 "paint": {
513 "text-color": "#333",
514 "text-halo-color": "#fff",
515 "text-halo-width": 2
516 }
517 }),
518 ];
519
520 Style {
521 version: 8,
522 name: "Osmic Default".into(),
523 glyphs: Some("https://fonts.openmaptiles.org/{fontstack}/{range}.pbf".into()),
524 sources: source,
525 layers,
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn default_style_pmtiles_source_uses_url_key() {
535 let style = default_style_json("pmtiles://tiles.pmtiles");
536 assert_eq!(style.version, 8);
537 assert_eq!(style.name, "Osmic Default");
538 let osmic_source = &style.sources["osmic"];
539 assert_eq!(osmic_source["type"], "vector");
540 assert_eq!(osmic_source["url"], "pmtiles://tiles.pmtiles");
541 assert!(osmic_source.get("tiles").is_none());
542 }
543
544 #[test]
545 fn default_style_http_source_uses_tiles_array() {
546 let url = "http://localhost:3000/{z}/{x}/{y}.mvt";
547 let style = default_style_json(url);
548 let osmic_source = &style.sources["osmic"];
549 assert_eq!(osmic_source["type"], "vector");
550 assert_eq!(osmic_source["tiles"][0], url);
551 assert!(osmic_source.get("url").is_none());
552 }
553
554 #[test]
555 fn default_style_has_background_and_nonempty_layers() {
556 let style = default_style_json("pmtiles://x.pmtiles");
557 assert!(!style.layers.is_empty());
558 assert_eq!(style.layers[0]["id"], "background");
559 assert_eq!(style.layers[0]["type"], "background");
560 }
561
562 #[test]
563 fn style_roundtrips_through_json() {
564 let style = default_style_json("pmtiles://x.pmtiles");
565 let json = style.to_json();
566 assert!(!json.is_empty());
567 let parsed: Style = serde_json::from_str(&json).expect("roundtrip");
568 assert_eq!(parsed.version, style.version);
569 assert_eq!(parsed.name, style.name);
570 assert_eq!(parsed.layers.len(), style.layers.len());
571 }
572}