@mapmap/maps 0.3.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -778,8 +778,8 @@ var PALETTE_SLOTS = [
778
778
  ["waterway", "#a8c8e8", "#1b2c40"],
779
779
  ["landcover", "#e3e8dd", "#182029"],
780
780
  ["landuse", "#ece8e1", "#171e26"],
781
- ["park", "#cfe4c8", "#1a2a1f"],
782
- ["building", "#e2ddd4", "#20272f"],
781
+ ["park", "#c8e2bf", "#1a2a1f"],
782
+ ["building", "#e0d6c4", "#252d37"],
783
783
  ["aeroway", "#dcd9d2", "#232b34"],
784
784
  ["road", "#ffffff", "#2b333d"],
785
785
  ["roadMajor", "#f6d9a0", "#4a5461"],
@@ -868,6 +868,73 @@ function resolvePalette(theme) {
868
868
  }
869
869
  return palette;
870
870
  }
871
+ function mixColour(a, b, t) {
872
+ const parse = (s) => /^#[0-9a-fA-F]{6}$/.test(s) ? [
873
+ parseInt(s.slice(1, 3), 16),
874
+ parseInt(s.slice(3, 5), 16),
875
+ parseInt(s.slice(5, 7), 16)
876
+ ] : null;
877
+ const ca = parse(a);
878
+ const cb = parse(b);
879
+ if (!ca || !cb) return a;
880
+ const k = Math.min(1, Math.max(0, t));
881
+ const hex = (i) => Math.min(255, Math.max(0, Math.round(ca[i] + (cb[i] - ca[i]) * k))).toString(16).padStart(2, "0");
882
+ return `#${hex(0)}${hex(1)}${hex(2)}`;
883
+ }
884
+ function landuseColour(p) {
885
+ const base = p("landuse");
886
+ return [
887
+ "match",
888
+ ["get", "class"],
889
+ // Civic/medical: leans towards the building tone.
890
+ ["hospital"],
891
+ mixColour(base, p("building"), 0.55),
892
+ // Education campuses read as semi-green grounds.
893
+ ["school", "university", "college", "kindergarten", "library"],
894
+ mixColour(base, p("park"), 0.35),
895
+ ["cemetery"],
896
+ mixColour(base, p("park"), 0.6),
897
+ // Sports/play surfaces borrow the park green outright.
898
+ ["pitch", "playground"],
899
+ mixColour(base, p("park"), 0.75),
900
+ // Restricted land greys off.
901
+ ["military"],
902
+ mixColour(base, p("textSecondary"), 0.3),
903
+ ["retail", "commercial"],
904
+ mixColour(base, p("building"), 0.35),
905
+ ["residential", "suburb", "neighbourhood"],
906
+ mixColour(base, p("building"), 0.18),
907
+ ["railway"],
908
+ mixColour(base, p("textSecondary"), 0.2),
909
+ base
910
+ ];
911
+ }
912
+ function landcoverColour(p) {
913
+ const base = p("landcover");
914
+ return [
915
+ "match",
916
+ ["get", "class"],
917
+ ["wood", "forest"],
918
+ mixColour(base, p("park"), 0.75),
919
+ ["grass"],
920
+ mixColour(base, p("park"), 0.45),
921
+ ["wetland"],
922
+ mixColour(base, p("park"), 0.3),
923
+ ["farmland"],
924
+ mixColour(base, p("landuse"), 0.4),
925
+ ["sand"],
926
+ mixColour(base, p("building"), 0.5),
927
+ ["rock"],
928
+ mixColour(base, p("textSecondary"), 0.25),
929
+ base
930
+ ];
931
+ }
932
+ function notARoadFilter() {
933
+ return ["!in", "class", "rail", "transit", "ferry", "path", "track", "pedestrian", "aerialway"];
934
+ }
935
+ function tunnelOpacity() {
936
+ return ["match", ["get", "brunnel"], "tunnel", 0.55, 1];
937
+ }
871
938
  function fill(id, sourceLayer, colour2, opacity, minzoom) {
872
939
  const layer = {
873
940
  id,
@@ -1026,8 +1093,8 @@ function buildStyle(options = {}) {
1026
1093
  type: "background",
1027
1094
  paint: { "background-color": p("background") }
1028
1095
  },
1029
- fill("landcover", "landcover", p("landcover"), 0.6),
1030
- fill("landuse", "landuse", p("landuse"), 0.45),
1096
+ fill("landcover", "landcover", landcoverColour(p), 0.6),
1097
+ fill("landuse", "landuse", landuseColour(p), 0.45),
1031
1098
  fill("park", "park", p("park"), 0.7),
1032
1099
  fill("water", "water", p("water"), 1),
1033
1100
  {
@@ -1052,7 +1119,32 @@ function buildStyle(options = {}) {
1052
1119
  "line-width": ["interpolate", ["linear"], ["zoom"], 10, 1, 16, 6]
1053
1120
  }
1054
1121
  },
1055
- fill("building", "building", p("building"), 0.9, 13),
1122
+ fill("building", "building", p("building"), 1, 13),
1123
+ // Building footprints sit at barely 1.2:1 against the background, so at
1124
+ // street zooms the fill alone reads as nothing. An outline restores the
1125
+ // block structure without touching the `building` slot default or the
1126
+ // fill opacity, which would alter every saved theme and preset. Starts
1127
+ // at BUILDINGS_3D_MINZOOM so it hands off to the extrusion at exactly
1128
+ // the zoom the flat fill does.
1129
+ {
1130
+ id: "building-outline",
1131
+ type: "line",
1132
+ source: "territory",
1133
+ "source-layer": "building",
1134
+ minzoom: BUILDINGS_3D_MINZOOM,
1135
+ paint: {
1136
+ "line-color": mixColour(p("building"), p("textSecondary"), 0.6),
1137
+ "line-width": [
1138
+ "interpolate",
1139
+ ["exponential", 1.2],
1140
+ ["zoom"],
1141
+ 15,
1142
+ 0.3,
1143
+ 20,
1144
+ 1.6
1145
+ ]
1146
+ }
1147
+ },
1056
1148
  {
1057
1149
  id: "rail",
1058
1150
  type: "line",
@@ -1062,7 +1154,38 @@ function buildStyle(options = {}) {
1062
1154
  filter: ["in", "class", "rail", "transit"],
1063
1155
  paint: {
1064
1156
  "line-color": p("rail"),
1065
- "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.4, 16, 2]
1157
+ "line-width": [
1158
+ "interpolate",
1159
+ ["exponential", 1.2],
1160
+ ["zoom"],
1161
+ 8,
1162
+ 0.4,
1163
+ 14,
1164
+ 1.6,
1165
+ 20,
1166
+ 6
1167
+ ]
1168
+ }
1169
+ },
1170
+ // The `transportation` layer carries POLYGON features for pedestrian
1171
+ // plazas and footway areas (61 in a single Soho tile). Without a fill
1172
+ // they were only ever hairline-dashed as outlines, leaving town centres
1173
+ // empty.
1174
+ {
1175
+ id: "pedestrian-areas",
1176
+ type: "fill",
1177
+ source: "territory",
1178
+ "source-layer": "transportation",
1179
+ minzoom: 14,
1180
+ // A fill layer only ever draws polygonal geometry, so the class match
1181
+ // is the whole filter. An explicit ["==", ["geometry-type"], "Polygon"]
1182
+ // clause looks right but matches NOTHING here: MapLibre reports these
1183
+ // multi-ring plazas as "MultiPolygon" (verified live — the clause
1184
+ // rendered 0 of 73 features).
1185
+ filter: ["match", ["get", "class"], ["path", "pedestrian"], true, false],
1186
+ paint: {
1187
+ "fill-color": p("path"),
1188
+ "fill-opacity": 0.7
1066
1189
  }
1067
1190
  },
1068
1191
  {
@@ -1074,10 +1197,104 @@ function buildStyle(options = {}) {
1074
1197
  filter: ["in", "class", "path", "track", "pedestrian"],
1075
1198
  paint: {
1076
1199
  "line-color": p("path"),
1077
- "line-width": ["interpolate", ["linear"], ["zoom"], 13, 0.5, 16, 2],
1078
- "line-dasharray": [2, 1]
1200
+ "line-width": [
1201
+ "interpolate",
1202
+ ["exponential", 1.2],
1203
+ ["zoom"],
1204
+ 13,
1205
+ 0.8,
1206
+ 16,
1207
+ 2.5,
1208
+ 18,
1209
+ 6,
1210
+ 20,
1211
+ 18
1212
+ ],
1213
+ "line-dasharray": [3, 1.5]
1214
+ }
1215
+ },
1216
+ // Road casings. ALL casings are drawn beneath ALL road fills (rather
1217
+ // than casing/fill pairs) so that at junctions a minor road's casing
1218
+ // never paints over a major road's fill. Colours are derived, not new
1219
+ // palette slots — see `mixColour`.
1220
+ {
1221
+ id: "road-minor-casing",
1222
+ type: "line",
1223
+ source: "territory",
1224
+ "source-layer": "transportation",
1225
+ filter: [
1226
+ "!in",
1227
+ "class",
1228
+ "motorway",
1229
+ "trunk",
1230
+ "primary",
1231
+ "rail",
1232
+ "transit",
1233
+ "path",
1234
+ "track",
1235
+ "pedestrian",
1236
+ "ferry"
1237
+ ],
1238
+ layout: { "line-cap": "round", "line-join": "round" },
1239
+ paint: {
1240
+ "line-color": mixColour(p("road"), p("textSecondary"), 0.45),
1241
+ "line-width": [
1242
+ "interpolate",
1243
+ ["exponential", 1.2],
1244
+ ["zoom"],
1245
+ 12,
1246
+ 0.5,
1247
+ 14,
1248
+ 5,
1249
+ 16,
1250
+ 10,
1251
+ 17,
1252
+ 14,
1253
+ 18,
1254
+ 21,
1255
+ 19,
1256
+ 37,
1257
+ 20,
1258
+ 64
1259
+ ],
1260
+ "line-opacity": tunnelOpacity()
1261
+ }
1262
+ },
1263
+ {
1264
+ id: "road-major-casing",
1265
+ type: "line",
1266
+ source: "territory",
1267
+ "source-layer": "transportation",
1268
+ minzoom: 6,
1269
+ filter: ["in", "class", "motorway", "trunk", "primary"],
1270
+ layout: { "line-cap": "round", "line-join": "round" },
1271
+ paint: {
1272
+ "line-color": mixColour(p("roadMajor"), p("textSecondary"), 0.45),
1273
+ "line-width": [
1274
+ "interpolate",
1275
+ ["exponential", 1.2],
1276
+ ["zoom"],
1277
+ 6,
1278
+ 2,
1279
+ 14,
1280
+ 8,
1281
+ 16,
1282
+ 13,
1283
+ 17,
1284
+ 19,
1285
+ 18,
1286
+ 30,
1287
+ 19,
1288
+ 54,
1289
+ 20,
1290
+ 96
1291
+ ],
1292
+ "line-opacity": tunnelOpacity()
1079
1293
  }
1080
1294
  },
1295
+ // Road widths ramp all the way to z20. They previously ended at z16, so
1296
+ // z17+ rendered frozen at z16 weights (4 px minor) against a 15-30 px
1297
+ // reference — the "schematic lines" bug.
1081
1298
  {
1082
1299
  id: "road-minor",
1083
1300
  type: "line",
@@ -1098,7 +1315,28 @@ function buildStyle(options = {}) {
1098
1315
  ],
1099
1316
  paint: {
1100
1317
  "line-color": p("road"),
1101
- "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 16, 4]
1318
+ "line-width": [
1319
+ "interpolate",
1320
+ ["exponential", 1.2],
1321
+ ["zoom"],
1322
+ 8,
1323
+ 0.5,
1324
+ 13,
1325
+ 1.5,
1326
+ 14,
1327
+ 3,
1328
+ 16,
1329
+ 7,
1330
+ 17,
1331
+ 10,
1332
+ 18,
1333
+ 16,
1334
+ 19,
1335
+ 29,
1336
+ 20,
1337
+ 52
1338
+ ],
1339
+ "line-opacity": tunnelOpacity()
1102
1340
  }
1103
1341
  },
1104
1342
  {
@@ -1109,7 +1347,64 @@ function buildStyle(options = {}) {
1109
1347
  filter: ["in", "class", "motorway", "trunk", "primary"],
1110
1348
  paint: {
1111
1349
  "line-color": p("roadMajor"),
1112
- "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1, 16, 8]
1350
+ "line-width": [
1351
+ "interpolate",
1352
+ ["exponential", 1.2],
1353
+ ["zoom"],
1354
+ 6,
1355
+ 1,
1356
+ 14,
1357
+ 5.5,
1358
+ 16,
1359
+ 10,
1360
+ 17,
1361
+ 15,
1362
+ 18,
1363
+ 24,
1364
+ 19,
1365
+ 44,
1366
+ 20,
1367
+ 80
1368
+ ],
1369
+ "line-opacity": tunnelOpacity()
1370
+ }
1371
+ },
1372
+ // Bridges are REDRAWN on top of the whole flat road network (casing
1373
+ // then fill) so an overpass reads as crossing over what it spans
1374
+ // instead of as a flat junction. This is the proportionate treatment:
1375
+ // true multi-level ordering would need one layer pair per distinct
1376
+ // `layer` value, which is unbounded. The extra-wide casing supplies the
1377
+ // shadow edge that sells the crossing.
1378
+ {
1379
+ id: "road-bridge-casing",
1380
+ type: "line",
1381
+ source: "territory",
1382
+ "source-layer": "transportation",
1383
+ minzoom: 13,
1384
+ filter: ["all", ["==", "brunnel", "bridge"], notARoadFilter()],
1385
+ layout: { "line-cap": "butt", "line-join": "round" },
1386
+ paint: {
1387
+ "line-color": mixColour(p("road"), p("textSecondary"), 0.8),
1388
+ "line-width": ["interpolate", ["exponential", 1.2], ["zoom"], 13, 3, 16, 13, 17, 19, 18, 30, 19, 54, 20, 96]
1389
+ }
1390
+ },
1391
+ {
1392
+ id: "road-bridge",
1393
+ type: "line",
1394
+ source: "territory",
1395
+ "source-layer": "transportation",
1396
+ minzoom: 13,
1397
+ filter: ["all", ["==", "brunnel", "bridge"], notARoadFilter()],
1398
+ layout: { "line-cap": "round", "line-join": "round" },
1399
+ paint: {
1400
+ "line-color": [
1401
+ "match",
1402
+ ["get", "class"],
1403
+ ["motorway", "trunk", "primary"],
1404
+ p("roadMajor"),
1405
+ p("road")
1406
+ ],
1407
+ "line-width": ["interpolate", ["exponential", 1.2], ["zoom"], 13, 1.5, 16, 8, 17, 15, 18, 24, 19, 44, 20, 80]
1113
1408
  }
1114
1409
  },
1115
1410
  {
@@ -1139,14 +1434,77 @@ function buildStyle(options = {}) {
1139
1434
  }
1140
1435
  ];
1141
1436
  if (theme.buildings_3d) {
1142
- const flat = layers.find((l) => l["id"] === "building");
1143
- flat["maxzoom"] = BUILDINGS_3D_MINZOOM;
1437
+ for (const id of ["building", "building-outline"]) {
1438
+ const flat = layers.find((l) => l["id"] === id);
1439
+ flat["maxzoom"] = BUILDINGS_3D_MINZOOM;
1440
+ }
1144
1441
  layers.push(buildings3dLayer(p("building")));
1145
1442
  }
1146
1443
  (theme.extra_layers ?? []).forEach((extra, index) => {
1147
1444
  layers.push(structuredClone(validateExtraLayer(index, extra)));
1148
1445
  });
1149
1446
  layers.push(
1447
+ // One-way arrows, drawn as a TEXT symbol rather than a sprite icon. The
1448
+ // skeleton ships no sprite at all (`sprite` is an optional user-supplied
1449
+ // URL), so an `icon-image` arrow would render nothing for every default
1450
+ // style - a text glyph needs only the glyph server the labels already use.
1451
+ //
1452
+ // The font is PINNED rather than following `theme.fonts`: U+2192 is absent
1453
+ // from 5 of the 15 bundled fontstacks (Barlow, Noto Serif, Nunito, Open
1454
+ // Sans, Rubik - verified by decoding their 8448-8703 glyph ranges), so a
1455
+ // themed font would silently drop every arrow. "Noto Sans Regular" is the
1456
+ // default fontstack and does carry it.
1457
+ //
1458
+ // `text-keep-upright` MUST be false: left at its `true` default MapLibre
1459
+ // flips glyphs on right-to-left line segments to keep them readable, which
1460
+ // would reverse the very thing the arrow encodes.
1461
+ {
1462
+ id: "road-oneway",
1463
+ type: "symbol",
1464
+ source: "territory",
1465
+ "source-layer": "transportation",
1466
+ minzoom: 16,
1467
+ // `oneway` is 1 (forward), -1 (reverse) or 0/absent. The string forms
1468
+ // are matched too: legacy `==` is strictly typed, and a tile build that
1469
+ // encodes the attribute as a string would otherwise match nothing.
1470
+ filter: [
1471
+ "all",
1472
+ [
1473
+ "any",
1474
+ ["==", "oneway", 1],
1475
+ ["==", "oneway", -1],
1476
+ ["==", "oneway", "1"],
1477
+ ["==", "oneway", "-1"]
1478
+ ],
1479
+ notARoadFilter()
1480
+ ],
1481
+ layout: {
1482
+ "symbol-placement": "line",
1483
+ "symbol-spacing": 200,
1484
+ "text-field": "\u2192",
1485
+ "text-font": ["Noto Sans Regular"],
1486
+ "text-size": ["interpolate", ["linear"], ["zoom"], 16, 8, 20, 12],
1487
+ "text-rotation-alignment": "map",
1488
+ "text-pitch-alignment": "map",
1489
+ "text-keep-upright": false,
1490
+ // -1 means the arrow points against the digitisation direction.
1491
+ "text-rotate": [
1492
+ "case",
1493
+ ["any", ["==", ["get", "oneway"], -1], ["==", ["get", "oneway"], "-1"]],
1494
+ 180,
1495
+ 0
1496
+ ],
1497
+ // Arrows are road furniture, not labels: they should not lose
1498
+ // collisions with names, nor push them out.
1499
+ "text-allow-overlap": true,
1500
+ "text-ignore-placement": true
1501
+ },
1502
+ paint: {
1503
+ "text-color": mixColour(p("road"), p("textSecondary"), 0.55),
1504
+ "text-halo-color": p("textHalo"),
1505
+ "text-halo-width": 1
1506
+ }
1507
+ },
1150
1508
  symbol(
1151
1509
  "housenumber",
1152
1510
  "housenumber",
@@ -1162,11 +1520,18 @@ function buildStyle(options = {}) {
1162
1520
  "transportation_name",
1163
1521
  nameField,
1164
1522
  font,
1165
- 11,
1523
+ ["interpolate", ["linear"], ["zoom"], 12, 11, 16, 12.5, 20, 15],
1166
1524
  p("textSecondary"),
1167
1525
  p("textHalo"),
1168
1526
  12,
1169
- { "symbol-placement": "line" }
1527
+ {
1528
+ "symbol-placement": "line",
1529
+ // Street names must be able to REPEAT along a long road: at z18 the
1530
+ // map was drawing 80 POI labels to 11 road labels, reading as a pin
1531
+ // cloud rather than a street network. Spacing tightens as the same
1532
+ // street comes to fill more of the screen.
1533
+ "symbol-spacing": ["interpolate", ["linear"], ["zoom"], 12, 400, 16, 260, 18, 320, 20, 520]
1534
+ }
1170
1535
  ),
1171
1536
  symbol(
1172
1537
  "water-name",
@@ -1177,17 +1542,34 @@ function buildStyle(options = {}) {
1177
1542
  p("textSecondary"),
1178
1543
  p("textHalo")
1179
1544
  ),
1180
- symbol(
1181
- "poi-labels",
1182
- "poi",
1183
- nameField,
1184
- font,
1185
- 11,
1186
- p("textSecondary"),
1187
- p("textHalo"),
1188
- 14,
1189
- { "text-anchor": "top" }
1190
- ),
1545
+ {
1546
+ // POI rebalancing. `poi.rank` is populated on 100% of features (1 = most
1547
+ // prominent), so it can do two jobs: a zoom-stepped ceiling admits only the
1548
+ // headline POIs at mid zooms and opens right up by z18, where the user has
1549
+ // asked for detail; and `symbol-sort-key` makes the survivors compete by
1550
+ // rank, so when labels do collide the landmark wins and the vape shop
1551
+ // loses. Lower sort key = placed first, and lower rank = more important, so
1552
+ // `rank` can be used directly. This rebalances rather than hides.
1553
+ ...symbol(
1554
+ "poi-labels",
1555
+ "poi",
1556
+ nameField,
1557
+ font,
1558
+ 11,
1559
+ p("textSecondary"),
1560
+ p("textHalo"),
1561
+ 14,
1562
+ {
1563
+ "text-anchor": "top",
1564
+ "symbol-sort-key": ["coalesce", ["get", "rank"], 99]
1565
+ }
1566
+ ),
1567
+ filter: [
1568
+ "<=",
1569
+ ["coalesce", ["get", "rank"], 99],
1570
+ ["step", ["zoom"], 6, 15, 10, 16, 15, 17, 22, 18, 40, 19, 120]
1571
+ ]
1572
+ },
1191
1573
  symbol(
1192
1574
  "mountain-peak-labels",
1193
1575
  "mountain_peak",
@@ -1469,7 +1851,7 @@ var MapMapMap = class {
1469
1851
  }
1470
1852
  const minzoom = building.minzoom ?? 13;
1471
1853
  if (enabled) {
1472
- const colour2 = this.map.getPaintProperty("building", "fill-color") ?? "#e2ddd4";
1854
+ const colour2 = this.map.getPaintProperty("building", "fill-color") ?? "#e0d6c4";
1473
1855
  this.map.addLayer(
1474
1856
  buildings3dLayer(colour2),
1475
1857
  buildings3dBeforeId(this.map)