@k2b/ui 0.30.0 → 0.32.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.
Files changed (186) hide show
  1. package/README.md +1 -1
  2. package/dist/base.css +2 -0
  3. package/dist/browser/actions/Toolbar.js +2 -0
  4. package/dist/browser/actions/Toolbar.js.map +1 -1
  5. package/dist/browser/actions/ViewTabs.js +8 -0
  6. package/dist/browser/actions/ViewTabs.js.map +1 -1
  7. package/dist/browser/chat/ChatDecision.js +28 -2
  8. package/dist/browser/chat/ChatDecision.js.map +1 -1
  9. package/dist/browser/content/Chart.js +104 -55
  10. package/dist/browser/content/Chart.js.map +1 -1
  11. package/dist/browser/content/ChartExplorer.js +5 -3
  12. package/dist/browser/content/ChartExplorer.js.map +1 -1
  13. package/dist/browser/content/MapAttribution.js +27 -0
  14. package/dist/browser/content/MapAttribution.js.map +1 -0
  15. package/dist/browser/content/MapView.js +553 -0
  16. package/dist/browser/content/MapView.js.map +1 -0
  17. package/dist/browser/content/MapViewFrame.js +395 -0
  18. package/dist/browser/content/MapViewFrame.js.map +1 -0
  19. package/dist/browser/content/chart-inspection-core.js +4 -1
  20. package/dist/browser/content/chart-inspection-core.js.map +1 -1
  21. package/dist/browser/content/code-highlight.js +66 -0
  22. package/dist/browser/content/code-highlight.js.map +1 -1
  23. package/dist/browser/content/index.js +6 -0
  24. package/dist/browser/content/index.js.map +1 -1
  25. package/dist/browser/content/map-palette.js +39 -0
  26. package/dist/browser/content/map-palette.js.map +1 -0
  27. package/dist/browser/content/map-style.js +276 -0
  28. package/dist/browser/content/map-style.js.map +1 -0
  29. package/dist/browser/content/map-view-geometry.js +269 -0
  30. package/dist/browser/content/map-view-geometry.js.map +1 -0
  31. package/dist/browser/content/map-view-live.js +65 -0
  32. package/dist/browser/content/map-view-live.js.map +1 -0
  33. package/dist/browser/content/map-view-messages.js +53 -0
  34. package/dist/browser/content/map-view-messages.js.map +1 -0
  35. package/dist/browser/content/map-view-types.js +2 -0
  36. package/dist/browser/content/map-view-types.js.map +1 -0
  37. package/dist/browser/content/map-view-worker.js +10 -0
  38. package/dist/browser/feedback/dialog-core.js +19 -0
  39. package/dist/browser/feedback/dialog-core.js.map +1 -1
  40. package/dist/browser/inputs/AutocompleteEditor.js +167 -104
  41. package/dist/browser/inputs/AutocompleteEditor.js.map +1 -1
  42. package/dist/browser/inputs/RichSelect.js +1474 -0
  43. package/dist/browser/inputs/RichSelect.js.map +1 -0
  44. package/dist/browser/inputs/TextInput.js +95 -60
  45. package/dist/browser/inputs/TextInput.js.map +1 -1
  46. package/dist/browser/inputs/choice.js +19 -8
  47. package/dist/browser/inputs/choice.js.map +1 -1
  48. package/dist/browser/inputs/index.js +1 -0
  49. package/dist/browser/inputs/index.js.map +1 -1
  50. package/dist/browser/inputs/rich-select-messages.js +46 -0
  51. package/dist/browser/inputs/rich-select-messages.js.map +1 -0
  52. package/dist/browser/layout/AppWorkspace.js +194 -144
  53. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  54. package/dist/browser/layout/DetailPanel.js +10 -1
  55. package/dist/browser/layout/DetailPanel.js.map +1 -1
  56. package/dist/browser/layout/Navigation.js +14 -5
  57. package/dist/browser/layout/Navigation.js.map +1 -1
  58. package/dist/browser/layout/PageTitle.js +9 -3
  59. package/dist/browser/layout/PageTitle.js.map +1 -1
  60. package/dist/browser/layout/ResizeSeparator.js +8 -2
  61. package/dist/browser/layout/ResizeSeparator.js.map +1 -1
  62. package/dist/browser/layout/app-workspace-controller.js +301 -34
  63. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  64. package/dist/browser/layout/app-workspace-width.js +26 -0
  65. package/dist/browser/layout/app-workspace-width.js.map +1 -0
  66. package/dist/browser/layout/index.js +1 -1
  67. package/dist/browser/layout/index.js.map +1 -1
  68. package/dist/browser/layout/navigation-model.js.map +1 -1
  69. package/dist/browser/layout/resize-separator.js +25 -1
  70. package/dist/browser/layout/resize-separator.js.map +1 -1
  71. package/dist/browser/surfaces/Skeleton.js +14 -0
  72. package/dist/browser/surfaces/Skeleton.js.map +1 -0
  73. package/dist/browser/surfaces/index.js +1 -0
  74. package/dist/browser/surfaces/index.js.map +1 -1
  75. package/dist/licenses/maplibre-gl.txt +116 -0
  76. package/dist/ssr/actions/Toolbar.js +2 -0
  77. package/dist/ssr/actions/Toolbar.js.map +1 -1
  78. package/dist/ssr/actions/ViewTabs.js +8 -0
  79. package/dist/ssr/actions/ViewTabs.js.map +1 -1
  80. package/dist/ssr/chat/ChatDecision.js +11 -0
  81. package/dist/ssr/chat/ChatDecision.js.map +1 -1
  82. package/dist/ssr/content/Chart.js +27 -7
  83. package/dist/ssr/content/Chart.js.map +1 -1
  84. package/dist/ssr/content/ChartExplorer.js +5 -3
  85. package/dist/ssr/content/ChartExplorer.js.map +1 -1
  86. package/dist/ssr/content/MapAttribution.js +19 -0
  87. package/dist/ssr/content/MapAttribution.js.map +1 -0
  88. package/dist/ssr/content/MapView.js +474 -0
  89. package/dist/ssr/content/MapView.js.map +1 -0
  90. package/dist/ssr/content/MapViewFrame.js +193 -0
  91. package/dist/ssr/content/MapViewFrame.js.map +1 -0
  92. package/dist/ssr/content/chart-inspection-core.js +4 -1
  93. package/dist/ssr/content/chart-inspection-core.js.map +1 -1
  94. package/dist/ssr/content/code-highlight.js +66 -0
  95. package/dist/ssr/content/code-highlight.js.map +1 -1
  96. package/dist/ssr/content/index.js +6 -0
  97. package/dist/ssr/content/index.js.map +1 -1
  98. package/dist/ssr/content/map-palette.js +39 -0
  99. package/dist/ssr/content/map-palette.js.map +1 -0
  100. package/dist/ssr/content/map-style.js +276 -0
  101. package/dist/ssr/content/map-style.js.map +1 -0
  102. package/dist/ssr/content/map-view-geometry.js +269 -0
  103. package/dist/ssr/content/map-view-geometry.js.map +1 -0
  104. package/dist/ssr/content/map-view-live.js +1 -0
  105. package/dist/ssr/content/map-view-messages.js +53 -0
  106. package/dist/ssr/content/map-view-messages.js.map +1 -0
  107. package/dist/ssr/content/map-view-types.js +2 -0
  108. package/dist/ssr/content/map-view-types.js.map +1 -0
  109. package/dist/ssr/feedback/dialog-core.js +19 -0
  110. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  111. package/dist/ssr/inputs/AutocompleteEditor.js +64 -24
  112. package/dist/ssr/inputs/AutocompleteEditor.js.map +1 -1
  113. package/dist/ssr/inputs/RichSelect.js +950 -0
  114. package/dist/ssr/inputs/RichSelect.js.map +1 -0
  115. package/dist/ssr/inputs/TextInput.js +40 -16
  116. package/dist/ssr/inputs/TextInput.js.map +1 -1
  117. package/dist/ssr/inputs/choice.js +19 -8
  118. package/dist/ssr/inputs/choice.js.map +1 -1
  119. package/dist/ssr/inputs/index.js +1 -0
  120. package/dist/ssr/inputs/index.js.map +1 -1
  121. package/dist/ssr/inputs/rich-select-messages.js +46 -0
  122. package/dist/ssr/inputs/rich-select-messages.js.map +1 -0
  123. package/dist/ssr/layout/AppWorkspace.js +53 -16
  124. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  125. package/dist/ssr/layout/DetailPanel.js +1 -1
  126. package/dist/ssr/layout/DetailPanel.js.map +1 -1
  127. package/dist/ssr/layout/Navigation.js +1 -1
  128. package/dist/ssr/layout/Navigation.js.map +1 -1
  129. package/dist/ssr/layout/PageTitle.js +9 -3
  130. package/dist/ssr/layout/PageTitle.js.map +1 -1
  131. package/dist/ssr/layout/ResizeSeparator.js +4 -1
  132. package/dist/ssr/layout/ResizeSeparator.js.map +1 -1
  133. package/dist/ssr/layout/app-workspace-controller.js +301 -34
  134. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  135. package/dist/ssr/layout/app-workspace-width.js +26 -0
  136. package/dist/ssr/layout/app-workspace-width.js.map +1 -0
  137. package/dist/ssr/layout/index.js +1 -1
  138. package/dist/ssr/layout/index.js.map +1 -1
  139. package/dist/ssr/layout/navigation-model.js.map +1 -1
  140. package/dist/ssr/layout/resize-separator.js +25 -1
  141. package/dist/ssr/layout/resize-separator.js.map +1 -1
  142. package/dist/ssr/surfaces/Skeleton.js +8 -0
  143. package/dist/ssr/surfaces/Skeleton.js.map +1 -0
  144. package/dist/ssr/surfaces/index.js +1 -0
  145. package/dist/ssr/surfaces/index.js.map +1 -1
  146. package/dist/styles.css +1 -1
  147. package/dist/types/content/Chart.d.ts +4 -2
  148. package/dist/types/content/ChartExplorer.d.ts +0 -1
  149. package/dist/types/content/MapAttribution.d.ts +6 -0
  150. package/dist/types/content/MapView.d.ts +5 -0
  151. package/dist/types/content/MapViewFrame.d.ts +31 -0
  152. package/dist/types/content/code-highlight.d.ts +3 -1
  153. package/dist/types/content/index.d.ts +7 -0
  154. package/dist/types/content/map-palette.d.ts +58 -0
  155. package/dist/types/content/map-style.d.ts +29 -0
  156. package/dist/types/content/map-view-geometry.d.ts +55 -0
  157. package/dist/types/content/map-view-live.d.ts +2 -0
  158. package/dist/types/content/map-view-messages.d.ts +21 -0
  159. package/dist/types/content/map-view-types.d.ts +63 -0
  160. package/dist/types/feedback/dialog-core.d.ts +1 -0
  161. package/dist/types/inputs/AutocompleteEditor.d.ts +6 -1
  162. package/dist/types/inputs/RichSelect.d.ts +93 -0
  163. package/dist/types/inputs/TextInput.d.ts +5 -2
  164. package/dist/types/inputs/choice.d.ts +24 -4
  165. package/dist/types/inputs/index.d.ts +2 -0
  166. package/dist/types/inputs/rich-select-messages.d.ts +21 -0
  167. package/dist/types/layout/AppWorkspace.d.ts +14 -3
  168. package/dist/types/layout/DetailPanel.d.ts +2 -0
  169. package/dist/types/layout/app-workspace-controller.d.ts +2 -0
  170. package/dist/types/layout/app-workspace-width.d.ts +13 -0
  171. package/dist/types/layout/index.d.ts +2 -2
  172. package/dist/types/layout/navigation-model.d.ts +2 -0
  173. package/dist/types/layout/resize-separator.d.ts +1 -0
  174. package/dist/types/surfaces/Skeleton.d.ts +6 -0
  175. package/dist/types/surfaces/index.d.ts +1 -0
  176. package/map-glyphs/IBM Plex Sans Medium/0-255.pbf +0 -0
  177. package/map-glyphs/IBM Plex Sans Medium/1024-1279.pbf +0 -0
  178. package/map-glyphs/IBM Plex Sans Medium/256-511.pbf +0 -0
  179. package/map-glyphs/IBM Plex Sans Medium/512-767.pbf +0 -0
  180. package/map-glyphs/IBM Plex Sans Medium/768-1023.pbf +0 -0
  181. package/map-glyphs/IBM Plex Sans Medium/7680-7935.pbf +0 -0
  182. package/map-glyphs/IBM Plex Sans Medium/8192-8447.pbf +0 -0
  183. package/map-glyphs/IBM Plex Sans Medium/8448-8703.pbf +0 -0
  184. package/map-glyphs/IBM Plex Sans Medium/8704-8959.pbf +0 -0
  185. package/map-glyphs/OFL.txt +93 -0
  186. package/package.json +11 -6
@@ -0,0 +1,276 @@
1
+ import { mapPalette } from "./map-palette";
2
+ /** Attribution for OpenMapTiles-schema maps with OpenStreetMap data from Geoapify, in display order. */
3
+ export const mapAttribution = [{
4
+ label: "© OpenStreetMap",
5
+ href: "https://www.openstreetmap.org/copyright"
6
+ }, {
7
+ label: "© OpenMapTiles",
8
+ href: "https://openmaptiles.org/"
9
+ }, {
10
+ label: "Geoapify",
11
+ href: "https://www.geoapify.com/"
12
+ }];
13
+ const source = "openmaptiles";
14
+ /** One weight keeps the glyph assets small; size and ink carry the label hierarchy. */
15
+ const font = ["IBM Plex Sans Medium"];
16
+ const zoomed = (stops, base = 1.5) => ["interpolate", ["exponential", base], ["zoom"], ...stops.flat()];
17
+ const linear = stops => ["interpolate", ["linear"], ["zoom"], ...stops.flat()];
18
+ /** A casing is the fill plus a fixed edge at every stop, because zoom must stay the outer expression. */
19
+ const widened = (stops, edge) => stops.map(([zoom, width]) => [zoom, width + edge]);
20
+ const classIn = (...classes) => ["in", ["get", "class"], ["literal", classes]];
21
+ const notTunnel = ["!=", ["get", "brunnel"], "tunnel"];
22
+ const tunnelOpacity = ["match", ["get", "brunnel"], "tunnel", 0.5, 1];
23
+ const motorwayWidth = [[5, 0.5], [10, 1.5], [14, 4], [18, 16]];
24
+ const majorWidth = [[8, 0.5], [12, 1.5], [14, 3.5], [18, 14]];
25
+ const secondaryWidth = [[10, 0.5], [13, 1.5], [14, 2.5], [18, 12]];
26
+ const minorWidth = [[13, 0.75], [15, 2.5], [18, 9]];
27
+ // OpenMapTiles files pedestrian streets as paths; they read as streets, so they get the minor road look.
28
+ // Pedestrian squares arrive as polygons and stay plain land.
29
+ const minorFilter = ["any", classIn("minor", "service"), ["all", ["==", ["get", "class"], "path"], ["==", ["get", "subclass"], "pedestrian"], ["==", ["geometry-type"], "LineString"]]];
30
+
31
+ /** `name:<language>`, then the Latin form, then the local name, so a bounded set of glyph ranges covers every label. */
32
+ const labelText = locale => {
33
+ const language = locale.trim().toLowerCase().split(/[-_]/)[0] ?? "";
34
+ const names = [["get", "name:latin"], ["get", "name"]];
35
+ if (/^[a-z]{2,3}$/.test(language)) names.unshift(["get", `name:${language}`]);
36
+ return ["coalesce", ...names];
37
+ };
38
+ const roads = (theme, colors) => {
39
+ const casing = (id, filter, stops, minzoom, color) => ({
40
+ id: `${id}-casing`,
41
+ type: "line",
42
+ source,
43
+ "source-layer": "transportation",
44
+ minzoom,
45
+ filter,
46
+ layout: {
47
+ "line-cap": "round",
48
+ "line-join": "round"
49
+ },
50
+ paint: {
51
+ "line-color": color,
52
+ "line-width": zoomed(widened(stops, 1.5)),
53
+ "line-opacity": tunnelOpacity
54
+ }
55
+ });
56
+ const fill = (id, filter, stops, minzoom, color) => ({
57
+ id,
58
+ type: "line",
59
+ source,
60
+ "source-layer": "transportation",
61
+ minzoom,
62
+ filter,
63
+ layout: {
64
+ "line-cap": "round",
65
+ "line-join": "round"
66
+ },
67
+ paint: {
68
+ "line-color": color,
69
+ "line-width": zoomed(stops),
70
+ "line-opacity": tunnelOpacity
71
+ }
72
+ });
73
+ // Far out, roads take whichever tone stands out more from land: on light land a white road disappears, so it
74
+ // starts in the casing tone; on dark land the casing tone is the weaker one. Edges appear as roads widen.
75
+ const far = theme === "light" ? colors.roadCasing : colors.road;
76
+ const road = ["interpolate", ["linear"], ["zoom"], 11, far, 12.5, colors.road];
77
+ return [{
78
+ id: "path",
79
+ type: "line",
80
+ source,
81
+ "source-layer": "transportation",
82
+ minzoom: 15,
83
+ filter: ["all", classIn("path", "track"), ["!=", ["get", "subclass"], "pedestrian"], notTunnel],
84
+ paint: {
85
+ "line-color": colors.rail,
86
+ "line-width": zoomed([[15, 0.75], [18, 1.5]]),
87
+ "line-dasharray": [2, 1.5]
88
+ }
89
+ }, {
90
+ id: "rail",
91
+ type: "line",
92
+ source,
93
+ "source-layer": "transportation",
94
+ minzoom: 11,
95
+ filter: ["all", classIn("rail", "transit"), notTunnel],
96
+ paint: {
97
+ "line-color": colors.rail,
98
+ "line-width": zoomed([[11, 0.75], [18, 2]])
99
+ }
100
+ }, casing("road-minor", minorFilter, minorWidth, 14, colors.roadCasing), casing("road-secondary", classIn("secondary", "tertiary"), secondaryWidth, 12, colors.roadCasing), casing("road-major", classIn("trunk", "primary"), majorWidth, 12, colors.roadCasing), casing("road-motorway", classIn("motorway"), motorwayWidth, 9, colors.motorwayCasing), fill("road-minor", minorFilter, minorWidth, 13, road), fill("road-secondary", classIn("secondary", "tertiary"), secondaryWidth, 10, road), fill("road-major", classIn("trunk", "primary"), majorWidth, 8, road), fill("road-motorway", classIn("motorway"), motorwayWidth, 5, colors.motorway)];
101
+ };
102
+ const labels = (colors, text) => {
103
+ const paint = (color, halo = colors.land) => ({
104
+ "text-color": color,
105
+ "text-halo-color": halo,
106
+ "text-halo-width": 1.5
107
+ });
108
+ const place = (id, classes, minzoom, maxzoom, size, color) => ({
109
+ id: `place-${id}`,
110
+ type: "symbol",
111
+ source,
112
+ "source-layer": "place",
113
+ minzoom,
114
+ maxzoom,
115
+ filter: classIn(...classes),
116
+ layout: {
117
+ "text-field": text,
118
+ "text-font": font,
119
+ "text-size": linear(size),
120
+ "text-max-width": 8,
121
+ "symbol-sort-key": ["coalesce", ["get", "rank"], 99]
122
+ },
123
+ paint: paint(color)
124
+ });
125
+ return [{
126
+ id: "waterway-label",
127
+ type: "symbol",
128
+ source,
129
+ "source-layer": "waterway",
130
+ minzoom: 13,
131
+ filter: classIn("river", "canal"),
132
+ layout: {
133
+ "text-field": text,
134
+ "text-font": font,
135
+ "text-size": 11,
136
+ "symbol-placement": "line",
137
+ "text-letter-spacing": 0.05
138
+ },
139
+ paint: paint(colors.waterLabel, colors.water)
140
+ }, {
141
+ id: "water-name",
142
+ type: "symbol",
143
+ source,
144
+ "source-layer": "water_name",
145
+ filter: ["==", ["geometry-type"], "Point"],
146
+ layout: {
147
+ "text-field": text,
148
+ "text-font": font,
149
+ "text-size": linear([[4, 11], [12, 13]]),
150
+ "text-max-width": 7
151
+ },
152
+ paint: paint(colors.waterLabel, colors.water)
153
+ }, {
154
+ id: "street-label",
155
+ type: "symbol",
156
+ source,
157
+ "source-layer": "transportation_name",
158
+ minzoom: 13,
159
+ filter: classIn("motorway", "trunk", "primary", "secondary", "tertiary", "minor", "service"),
160
+ layout: {
161
+ "text-field": text,
162
+ "text-font": font,
163
+ "text-size": linear([[13, 11], [18, 13]]),
164
+ "symbol-placement": "line",
165
+ "text-max-angle": 30
166
+ },
167
+ paint: paint(colors.label, colors.road)
168
+ }, place("minor", ["suburb", "quarter", "neighbourhood", "hamlet", "locality", "isolated_dwelling"], 12, 18, [[12, 11], [16, 12.5]], colors.label), place("village", ["village"], 10, 17, [[10, 11.5], [14, 13]], colors.label), place("town", ["town"], 7, 16, [[7, 11.5], [10, 13], [14, 15]], colors.labelStrong), place("city", ["city"], 4, 15, [[4, 12], [8, 15], [12, 18]], colors.labelStrong), place("state", ["state", "province"], 4, 8, [[4, 11], [7, 12]], colors.label), place("country", ["country"], 1, 7, [[1, 11], [6, 15]], colors.labelStrong)];
169
+ };
170
+
171
+ /**
172
+ * A flat Cloud map style for OpenMapTiles-schema vector tiles. The host passes the tile and glyph
173
+ * templates; there are no network defaults, sprites, extrusions, or relief.
174
+ */
175
+ export function mapStyle(options) {
176
+ const {
177
+ theme,
178
+ locale,
179
+ tiles,
180
+ glyphs,
181
+ maxzoom = 14
182
+ } = options;
183
+ if (!["{z}", "{x}", "{y}"].every(token => tiles.includes(token))) throw new TypeError("mapStyle: tiles needs {z}, {x}, and {y}");
184
+ if (!["{fontstack}", "{range}"].every(token => glyphs.includes(token))) throw new TypeError("mapStyle: glyphs needs {fontstack} and {range}");
185
+ if (!Number.isInteger(maxzoom) || maxzoom < 0 || maxzoom > 24) throw new RangeError("mapStyle: maxzoom must be an integer from 0 to 24");
186
+ const colors = mapPalette[theme];
187
+ const text = labelText(locale);
188
+ // Protected areas (the park layer) cover whole regions, so only woods, grass, and city green are tinted.
189
+ const green = (id, sourceLayer, filter) => ({
190
+ id,
191
+ type: "fill",
192
+ source,
193
+ "source-layer": sourceLayer,
194
+ filter,
195
+ paint: {
196
+ "fill-color": colors.green
197
+ }
198
+ });
199
+ return {
200
+ version: 8,
201
+ name: `Cloud ${theme}`,
202
+ glyphs,
203
+ sources: {
204
+ [source]: {
205
+ type: "vector",
206
+ tiles: [tiles],
207
+ minzoom: 0,
208
+ maxzoom
209
+ }
210
+ },
211
+ layers: [{
212
+ id: "land",
213
+ type: "background",
214
+ paint: {
215
+ "background-color": colors.land
216
+ }
217
+ }, green("landcover-green", "landcover", classIn("wood", "grass")), green("landuse-green", "landuse", classIn("cemetery", "pitch", "playground")), {
218
+ id: "water",
219
+ type: "fill",
220
+ source,
221
+ "source-layer": "water",
222
+ filter: notTunnel,
223
+ paint: {
224
+ "fill-color": colors.water
225
+ }
226
+ }, {
227
+ id: "waterway",
228
+ type: "line",
229
+ source,
230
+ "source-layer": "waterway",
231
+ minzoom: 8,
232
+ filter: ["all", notTunnel, ["!=", ["get", "intermittent"], 1]],
233
+ layout: {
234
+ "line-cap": "round",
235
+ "line-join": "round"
236
+ },
237
+ paint: {
238
+ "line-color": colors.water,
239
+ "line-width": ["interpolate", ["exponential", 1.4], ["zoom"], 8, ["match", ["get", "class"], "river", 0.75, 0], 13, ["match", ["get", "class"], "river", 2.5, "canal", 1.5, 0.75], 18, ["match", ["get", "class"], "river", 8, "canal", 5, 2.5]]
240
+ }
241
+ }, {
242
+ id: "building",
243
+ type: "fill",
244
+ source,
245
+ "source-layer": "building",
246
+ minzoom: 13,
247
+ paint: {
248
+ "fill-color": colors.building,
249
+ "fill-opacity": linear([[13, 0], [14, 1]])
250
+ }
251
+ }, {
252
+ id: "boundary-state",
253
+ type: "line",
254
+ source,
255
+ "source-layer": "boundary",
256
+ minzoom: 4,
257
+ filter: ["all", ["==", ["get", "admin_level"], 4], ["!=", ["get", "maritime"], 1]],
258
+ paint: {
259
+ "line-color": colors.boundary,
260
+ "line-width": 0.75,
261
+ "line-dasharray": [3, 2]
262
+ }
263
+ }, {
264
+ id: "boundary-country",
265
+ type: "line",
266
+ source,
267
+ "source-layer": "boundary",
268
+ filter: ["all", ["==", ["get", "admin_level"], 2], ["!=", ["get", "maritime"], 1]],
269
+ paint: {
270
+ "line-color": colors.boundary,
271
+ "line-width": zoomed([[2, 0.75], [10, 1.5]])
272
+ }
273
+ }, ...roads(theme, colors), ...labels(colors, text)]
274
+ };
275
+ }
276
+ //# sourceMappingURL=map-style.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sources":["content/map-style.ts"],"sourcesContent":["import type {\n DataDrivenPropertyValueSpecification,\n ExpressionSpecification,\n FilterSpecification,\n LayerSpecification,\n StyleSpecification,\n} from \"maplibre-gl\";\nimport { type MapPaletteColors, type MapTheme, mapPalette } from \"./map-palette\";\n\nexport type MapStyleOptions = {\n theme: MapTheme;\n /** Request locale such as `de` or `de-DE`; labels prefer `name:<language>`. */\n locale: string;\n /** Vector tile template with `{z}`, `{x}`, and `{y}`, for example `/_geo/tiles/{z}/{x}/{y}.pbf`. */\n tiles: string;\n /** Glyph template with `{fontstack}` and `{range}`, served from `@k2b/ui/map-glyphs/*`. */\n glyphs: string;\n /** Highest zoom the tiles carry; the map scales the last level up beyond it. Defaults to 14. */\n maxzoom?: number;\n};\n\n/** Attribution for OpenMapTiles-schema maps with OpenStreetMap data from Geoapify, in display order. */\nexport const mapAttribution = [\n { label: \"© OpenStreetMap\", href: \"https://www.openstreetmap.org/copyright\" },\n { label: \"© OpenMapTiles\", href: \"https://openmaptiles.org/\" },\n { label: \"Geoapify\", href: \"https://www.geoapify.com/\" },\n] as const;\n\nconst source = \"openmaptiles\";\n/** One weight keeps the glyph assets small; size and ink carry the label hierarchy. */\nconst font = [\"IBM Plex Sans Medium\"];\ntype Stops = readonly (readonly [zoom: number, value: number])[];\n\nconst zoomed = (stops: Stops, base = 1.5): ExpressionSpecification =>\n [\"interpolate\", [\"exponential\", base], [\"zoom\"], ...stops.flat()] as ExpressionSpecification;\nconst linear = (stops: Stops): ExpressionSpecification => [\"interpolate\", [\"linear\"], [\"zoom\"], ...stops.flat()] as ExpressionSpecification;\n/** A casing is the fill plus a fixed edge at every stop, because zoom must stay the outer expression. */\nconst widened = (stops: Stops, edge: number): Stops => stops.map(([zoom, width]) => [zoom, width + edge] as const);\nconst classIn = (...classes: string[]): ExpressionSpecification => [\"in\", [\"get\", \"class\"], [\"literal\", classes]];\nconst notTunnel: ExpressionSpecification = [\"!=\", [\"get\", \"brunnel\"], \"tunnel\"];\nconst tunnelOpacity: DataDrivenPropertyValueSpecification<number> = [\"match\", [\"get\", \"brunnel\"], \"tunnel\", 0.5, 1];\n\nconst motorwayWidth: Stops = [\n [5, 0.5],\n [10, 1.5],\n [14, 4],\n [18, 16],\n];\nconst majorWidth: Stops = [\n [8, 0.5],\n [12, 1.5],\n [14, 3.5],\n [18, 14],\n];\nconst secondaryWidth: Stops = [\n [10, 0.5],\n [13, 1.5],\n [14, 2.5],\n [18, 12],\n];\nconst minorWidth: Stops = [\n [13, 0.75],\n [15, 2.5],\n [18, 9],\n];\n// OpenMapTiles files pedestrian streets as paths; they read as streets, so they get the minor road look.\n// Pedestrian squares arrive as polygons and stay plain land.\nconst minorFilter: FilterSpecification = [\n \"any\",\n classIn(\"minor\", \"service\"),\n [\"all\", [\"==\", [\"get\", \"class\"], \"path\"], [\"==\", [\"get\", \"subclass\"], \"pedestrian\"], [\"==\", [\"geometry-type\"], \"LineString\"]],\n];\n\n/** `name:<language>`, then the Latin form, then the local name, so a bounded set of glyph ranges covers every label. */\nconst labelText = (locale: string): ExpressionSpecification => {\n const language = locale.trim().toLowerCase().split(/[-_]/)[0] ?? \"\";\n const names: ExpressionSpecification[] = [\n [\"get\", \"name:latin\"],\n [\"get\", \"name\"],\n ];\n if (/^[a-z]{2,3}$/.test(language)) names.unshift([\"get\", `name:${language}`]);\n return [\"coalesce\", ...names];\n};\n\nconst roads = (theme: MapTheme, colors: MapPaletteColors): LayerSpecification[] => {\n const casing = (id: string, filter: FilterSpecification, stops: Stops, minzoom: number, color: string): LayerSpecification => ({\n id: `${id}-casing`,\n type: \"line\",\n source,\n \"source-layer\": \"transportation\",\n minzoom,\n filter,\n layout: { \"line-cap\": \"round\", \"line-join\": \"round\" },\n paint: { \"line-color\": color, \"line-width\": zoomed(widened(stops, 1.5)), \"line-opacity\": tunnelOpacity },\n });\n const fill = (\n id: string,\n filter: FilterSpecification,\n stops: Stops,\n minzoom: number,\n color: string | ExpressionSpecification,\n ): LayerSpecification => ({\n id,\n type: \"line\",\n source,\n \"source-layer\": \"transportation\",\n minzoom,\n filter,\n layout: { \"line-cap\": \"round\", \"line-join\": \"round\" },\n paint: { \"line-color\": color, \"line-width\": zoomed(stops), \"line-opacity\": tunnelOpacity },\n });\n // Far out, roads take whichever tone stands out more from land: on light land a white road disappears, so it\n // starts in the casing tone; on dark land the casing tone is the weaker one. Edges appear as roads widen.\n const far = theme === \"light\" ? colors.roadCasing : colors.road;\n const road: ExpressionSpecification = [\"interpolate\", [\"linear\"], [\"zoom\"], 11, far, 12.5, colors.road];\n return [\n {\n id: \"path\",\n type: \"line\",\n source,\n \"source-layer\": \"transportation\",\n minzoom: 15,\n filter: [\"all\", classIn(\"path\", \"track\"), [\"!=\", [\"get\", \"subclass\"], \"pedestrian\"], notTunnel],\n paint: {\n \"line-color\": colors.rail,\n \"line-width\": zoomed([\n [15, 0.75],\n [18, 1.5],\n ]),\n \"line-dasharray\": [2, 1.5],\n },\n },\n {\n id: \"rail\",\n type: \"line\",\n source,\n \"source-layer\": \"transportation\",\n minzoom: 11,\n filter: [\"all\", classIn(\"rail\", \"transit\"), notTunnel],\n paint: {\n \"line-color\": colors.rail,\n \"line-width\": zoomed([\n [11, 0.75],\n [18, 2],\n ]),\n },\n },\n casing(\"road-minor\", minorFilter, minorWidth, 14, colors.roadCasing),\n casing(\"road-secondary\", classIn(\"secondary\", \"tertiary\"), secondaryWidth, 12, colors.roadCasing),\n casing(\"road-major\", classIn(\"trunk\", \"primary\"), majorWidth, 12, colors.roadCasing),\n casing(\"road-motorway\", classIn(\"motorway\"), motorwayWidth, 9, colors.motorwayCasing),\n fill(\"road-minor\", minorFilter, minorWidth, 13, road),\n fill(\"road-secondary\", classIn(\"secondary\", \"tertiary\"), secondaryWidth, 10, road),\n fill(\"road-major\", classIn(\"trunk\", \"primary\"), majorWidth, 8, road),\n fill(\"road-motorway\", classIn(\"motorway\"), motorwayWidth, 5, colors.motorway),\n ];\n};\n\nconst labels = (colors: MapPaletteColors, text: ExpressionSpecification): LayerSpecification[] => {\n const paint = (color: string, halo = colors.land) => ({\n \"text-color\": color,\n \"text-halo-color\": halo,\n \"text-halo-width\": 1.5,\n });\n const place = (id: string, classes: string[], minzoom: number, maxzoom: number, size: Stops, color: string): LayerSpecification => ({\n id: `place-${id}`,\n type: \"symbol\",\n source,\n \"source-layer\": \"place\",\n minzoom,\n maxzoom,\n filter: classIn(...classes),\n layout: {\n \"text-field\": text,\n \"text-font\": font,\n \"text-size\": linear(size),\n \"text-max-width\": 8,\n \"symbol-sort-key\": [\"coalesce\", [\"get\", \"rank\"], 99],\n },\n paint: paint(color),\n });\n return [\n {\n id: \"waterway-label\",\n type: \"symbol\",\n source,\n \"source-layer\": \"waterway\",\n minzoom: 13,\n filter: classIn(\"river\", \"canal\"),\n layout: {\n \"text-field\": text,\n \"text-font\": font,\n \"text-size\": 11,\n \"symbol-placement\": \"line\",\n \"text-letter-spacing\": 0.05,\n },\n paint: paint(colors.waterLabel, colors.water),\n },\n {\n id: \"water-name\",\n type: \"symbol\",\n source,\n \"source-layer\": \"water_name\",\n filter: [\"==\", [\"geometry-type\"], \"Point\"],\n layout: {\n \"text-field\": text,\n \"text-font\": font,\n \"text-size\": linear([\n [4, 11],\n [12, 13],\n ]),\n \"text-max-width\": 7,\n },\n paint: paint(colors.waterLabel, colors.water),\n },\n {\n id: \"street-label\",\n type: \"symbol\",\n source,\n \"source-layer\": \"transportation_name\",\n minzoom: 13,\n filter: classIn(\"motorway\", \"trunk\", \"primary\", \"secondary\", \"tertiary\", \"minor\", \"service\"),\n layout: {\n \"text-field\": text,\n \"text-font\": font,\n \"text-size\": linear([\n [13, 11],\n [18, 13],\n ]),\n \"symbol-placement\": \"line\",\n \"text-max-angle\": 30,\n },\n paint: paint(colors.label, colors.road),\n },\n place(\n \"minor\",\n [\"suburb\", \"quarter\", \"neighbourhood\", \"hamlet\", \"locality\", \"isolated_dwelling\"],\n 12,\n 18,\n [\n [12, 11],\n [16, 12.5],\n ],\n colors.label,\n ),\n place(\n \"village\",\n [\"village\"],\n 10,\n 17,\n [\n [10, 11.5],\n [14, 13],\n ],\n colors.label,\n ),\n place(\n \"town\",\n [\"town\"],\n 7,\n 16,\n [\n [7, 11.5],\n [10, 13],\n [14, 15],\n ],\n colors.labelStrong,\n ),\n place(\n \"city\",\n [\"city\"],\n 4,\n 15,\n [\n [4, 12],\n [8, 15],\n [12, 18],\n ],\n colors.labelStrong,\n ),\n place(\n \"state\",\n [\"state\", \"province\"],\n 4,\n 8,\n [\n [4, 11],\n [7, 12],\n ],\n colors.label,\n ),\n place(\n \"country\",\n [\"country\"],\n 1,\n 7,\n [\n [1, 11],\n [6, 15],\n ],\n colors.labelStrong,\n ),\n ];\n};\n\n/**\n * A flat Cloud map style for OpenMapTiles-schema vector tiles. The host passes the tile and glyph\n * templates; there are no network defaults, sprites, extrusions, or relief.\n */\nexport function mapStyle(options: MapStyleOptions): StyleSpecification {\n const { theme, locale, tiles, glyphs, maxzoom = 14 } = options;\n if (![\"{z}\", \"{x}\", \"{y}\"].every((token) => tiles.includes(token))) throw new TypeError(\"mapStyle: tiles needs {z}, {x}, and {y}\");\n if (![\"{fontstack}\", \"{range}\"].every((token) => glyphs.includes(token)))\n throw new TypeError(\"mapStyle: glyphs needs {fontstack} and {range}\");\n if (!Number.isInteger(maxzoom) || maxzoom < 0 || maxzoom > 24) throw new RangeError(\"mapStyle: maxzoom must be an integer from 0 to 24\");\n const colors = mapPalette[theme];\n const text = labelText(locale);\n // Protected areas (the park layer) cover whole regions, so only woods, grass, and city green are tinted.\n const green = (id: string, sourceLayer: string, filter: FilterSpecification): LayerSpecification => ({\n id,\n type: \"fill\",\n source,\n \"source-layer\": sourceLayer,\n filter,\n paint: { \"fill-color\": colors.green },\n });\n return {\n version: 8,\n name: `Cloud ${theme}`,\n glyphs,\n sources: { [source]: { type: \"vector\", tiles: [tiles], minzoom: 0, maxzoom } },\n layers: [\n { id: \"land\", type: \"background\", paint: { \"background-color\": colors.land } },\n green(\"landcover-green\", \"landcover\", classIn(\"wood\", \"grass\")),\n green(\"landuse-green\", \"landuse\", classIn(\"cemetery\", \"pitch\", \"playground\")),\n {\n id: \"water\",\n type: \"fill\",\n source,\n \"source-layer\": \"water\",\n filter: notTunnel,\n paint: { \"fill-color\": colors.water },\n },\n {\n id: \"waterway\",\n type: \"line\",\n source,\n \"source-layer\": \"waterway\",\n minzoom: 8,\n filter: [\"all\", notTunnel, [\"!=\", [\"get\", \"intermittent\"], 1]],\n layout: { \"line-cap\": \"round\", \"line-join\": \"round\" },\n paint: {\n \"line-color\": colors.water,\n \"line-width\": [\n \"interpolate\",\n [\"exponential\", 1.4],\n [\"zoom\"],\n 8,\n [\"match\", [\"get\", \"class\"], \"river\", 0.75, 0],\n 13,\n [\"match\", [\"get\", \"class\"], \"river\", 2.5, \"canal\", 1.5, 0.75],\n 18,\n [\"match\", [\"get\", \"class\"], \"river\", 8, \"canal\", 5, 2.5],\n ],\n },\n },\n {\n id: \"building\",\n type: \"fill\",\n source,\n \"source-layer\": \"building\",\n minzoom: 13,\n paint: {\n \"fill-color\": colors.building,\n \"fill-opacity\": linear([\n [13, 0],\n [14, 1],\n ]),\n },\n },\n {\n id: \"boundary-state\",\n type: \"line\",\n source,\n \"source-layer\": \"boundary\",\n minzoom: 4,\n filter: [\"all\", [\"==\", [\"get\", \"admin_level\"], 4], [\"!=\", [\"get\", \"maritime\"], 1]],\n paint: { \"line-color\": colors.boundary, \"line-width\": 0.75, \"line-dasharray\": [3, 2] },\n },\n {\n id: \"boundary-country\",\n type: \"line\",\n source,\n \"source-layer\": \"boundary\",\n filter: [\"all\", [\"==\", [\"get\", \"admin_level\"], 2], [\"!=\", [\"get\", \"maritime\"], 1]],\n paint: {\n \"line-color\": colors.boundary,\n \"line-width\": zoomed([\n [2, 0.75],\n [10, 1.5],\n ]),\n },\n },\n ...roads(theme, colors),\n ...labels(colors, text),\n ],\n };\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,269 @@
1
+ export const MIN_SPAN = 256 / (512 * 2 ** 16);
2
+ export const EARTH_CIRCUMFERENCE = 40075016.686;
3
+ /** Room around the fitted geometry: clears the floating controls, the notice, the scale bar and the attribution. */
4
+ export const DEFAULT_PADDING = 64;
5
+ /** Phones are first-class: pin separation is decided from the narrowest common screen up, so it holds at every width. */
6
+ const PHONE_WIDTH = 320;
7
+ /**
8
+ * `referenceWidth` is the fitted width when the container is wide enough; `minWidth` keeps the whole world at least as
9
+ * tall as the map, the smallest zoom MapLibre allows, so a world-scale fit lands where MapLibre puts it.
10
+ */
11
+
12
+ export function decodePolyline6(encoded) {
13
+ let index = 0;
14
+ let lat = 0;
15
+ let lon = 0;
16
+ const next = () => {
17
+ let value = 0;
18
+ let shift = 0;
19
+ let byte;
20
+ do {
21
+ if (index >= encoded.length || shift > 50) throw new RangeError("Invalid polyline6");
22
+ byte = encoded.charCodeAt(index++) - 63;
23
+ if (byte < 0 || byte > 63) throw new RangeError("Invalid polyline6");
24
+ value += (byte & 31) * 2 ** shift;
25
+ shift += 5;
26
+ } while (byte >= 32);
27
+ return value % 2 ? -(value + 1) / 2 : value / 2;
28
+ };
29
+ const points = [];
30
+ while (index < encoded.length) {
31
+ lat += next();
32
+ lon += next();
33
+ points.push({
34
+ lat: lat / 1e6,
35
+ lon: lon / 1e6
36
+ });
37
+ }
38
+ return points;
39
+ }
40
+ export const linePoints = line => "points" in line ? line.points : decodePolyline6(line.polyline6);
41
+ export function projectPoint(point) {
42
+ const lat = Math.max(-85.051129, Math.min(85.051129, point.lat)) * Math.PI / 180;
43
+ return {
44
+ x: (point.lon + 180) / 360,
45
+ y: (1 - Math.log(Math.tan(Math.PI / 4 + lat / 2)) / Math.PI) / 2
46
+ };
47
+ }
48
+ export function unprojectPoint(x, y) {
49
+ return {
50
+ lon: x * 360 - 180,
51
+ lat: Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * 180 / Math.PI
52
+ };
53
+ }
54
+ export function mapFrame(geometry, height, padding = DEFAULT_PADDING) {
55
+ const points = (geometry.markers ?? []).map(marker => marker.point);
56
+ if (geometry.fit !== "markers" || !points.length) {
57
+ for (const line of geometry.lines ?? []) for (const point of linePoints(line)) points.push(point);
58
+ for (const area of geometry.areas ?? []) for (const point of area.polygon) points.push(point);
59
+ if (geometry.ownLocation) points.push(geometry.ownLocation.point);
60
+ }
61
+ if (!points.length) return null;
62
+ let minX = Infinity;
63
+ let maxX = -Infinity;
64
+ let minY = Infinity;
65
+ let maxY = -Infinity;
66
+ for (const point of points) {
67
+ const {
68
+ x,
69
+ y
70
+ } = projectPoint(point);
71
+ minX = Math.min(minX, x);
72
+ maxX = Math.max(maxX, x);
73
+ minY = Math.min(minY, y);
74
+ maxY = Math.max(maxY, y);
75
+ }
76
+ const spanX = Math.max(MIN_SPAN, maxX - minX);
77
+ const spanY = Math.max(MIN_SPAN, maxY - minY);
78
+ minX -= (spanX - (maxX - minX)) / 2;
79
+ minY -= (spanY - (maxY - minY)) / 2;
80
+ const aspect = spanX / spanY;
81
+ const minWidth = spanX * height;
82
+ const referenceWidth = Math.max((height - 2 * padding) * aspect, minWidth);
83
+ const overlapWidth = Math.max(minWidth, Math.min(referenceWidth, PHONE_WIDTH - 2 * padding));
84
+ return {
85
+ minX,
86
+ minY,
87
+ spanX,
88
+ spanY,
89
+ aspect,
90
+ referenceWidth,
91
+ minWidth,
92
+ overlapWidth
93
+ };
94
+ }
95
+ export function fitCamera(frame, width, padding = DEFAULT_PADDING) {
96
+ const w = Math.max(Number.EPSILON, frame.minWidth, Math.min(width - 2 * padding, frame.referenceWidth));
97
+ const center = unprojectPoint(frame.minX + frame.spanX / 2, frame.minY + frame.spanY / 2);
98
+ return {
99
+ center: [center.lon, center.lat],
100
+ zoom: Math.log2(w / (frame.spanX * 512))
101
+ };
102
+ }
103
+ export const isStop = marker => typeof marker.mark === "string" && /^[a-z+]$/i.test(marker.mark);
104
+ export function sortMarkers(markers) {
105
+ const rank = marker => {
106
+ if (typeof marker.mark === "number") return 0;
107
+ if (typeof marker.mark === "string" && /^[a-z]$/i.test(marker.mark)) return 1;
108
+ if (marker.mark === "+") return 2;
109
+ return 3;
110
+ };
111
+ return [...markers].sort((a, b) => {
112
+ const group = rank(a) - rank(b);
113
+ if (group) return group;
114
+ if (typeof a.mark === "number" && typeof b.mark === "number") return a.mark - b.mark;
115
+ if (rank(a) === 1) return String(a.mark).toUpperCase().localeCompare(String(b.mark).toUpperCase(), "en");
116
+ return 0;
117
+ });
118
+ }
119
+ const PIN_SPACING = 28;
120
+ // An attached hit may touch its stop's halo, as in the approved mockup, but never cover the stop's mark.
121
+ const ATTACHED_SPACING = 20;
122
+
123
+ /**
124
+ * The smallest distance between two drawn pins at every width the map can have: the geographic gap `g` (px at the
125
+ * phone fit) grows with the fitted width by r in [1, rMax], the offsets `o` stay in px. |g·r + o| is smallest at one r.
126
+ */
127
+ const closest = (gx, gy, ox, oy, rMax) => {
128
+ const gg = gx * gx + gy * gy;
129
+ const r = gg === 0 ? 1 : Math.min(rMax, Math.max(1, -(gx * ox + gy * oy) / gg));
130
+ return Math.hypot(gx * r + ox, gy * r + oy);
131
+ };
132
+
133
+ /** Deterministic places around a point: the mockup's two rings first, then wider rings for dense clusters. */
134
+ const candidateOffsets = count => {
135
+ const angles = [-45, 45, 135, -135, 0, 90, 180, -90];
136
+ // Ring k holds 8k places, so this many rings fit every pin of a cluster at one point with room to spare.
137
+ const rings = Math.ceil(Math.sqrt(count / 4)) + 2;
138
+ const result = [];
139
+ for (let ring = 1; ring <= rings; ring++) {
140
+ const steps = ring <= 2 ? angles : Array.from({
141
+ length: 8 * ring
142
+ }, (_, index) => -45 + 360 * index / (8 * ring));
143
+ for (const angle of steps) result.push({
144
+ dx: PIN_SPACING * ring * Math.cos(angle * Math.PI / 180),
145
+ dy: PIN_SPACING * ring * Math.sin(angle * Math.PI / 180)
146
+ });
147
+ }
148
+ return result;
149
+ };
150
+ const pinPlaces = (sortedMarkers, frame, zoomFactor) => {
151
+ const scale = frame.overlapWidth / frame.spanX * zoomFactor;
152
+ return marker => {
153
+ const p = projectPoint(marker.point);
154
+ return {
155
+ x: (p.x - frame.minX) * scale,
156
+ y: (p.y - frame.minY) * scale
157
+ };
158
+ };
159
+ };
160
+ /**
161
+ * A drop pin's mark sits 14 px above its point (its tip is on the point). That lift is a fixed px shift, so it belongs
162
+ * with the offsets, never with the geographic gap that grows with the map's width.
163
+ */
164
+ const lift = marker => marker.mark === undefined ? 14 : 0;
165
+
166
+ /**
167
+ * Pin offsets in px, decided from the data alone: at the phone fit and checked for every wider fit up to the widest
168
+ * one, so they hold at any width. `zoomFactor` is 2^(live zoom - fitted zoom): 1 for the server frame, the first client
169
+ * frame and the untouched live map, so all three agree; when the person zooms, separation is decided for the new scale.
170
+ */
171
+ export function pinOffsets(sortedMarkers, frame, zoomFactor = 1, ownLocation) {
172
+ const position = pinPlaces(sortedMarkers, frame, zoomFactor);
173
+ const location = ownLocation ? projectPoint(ownLocation) : undefined;
174
+ const scale = frame.overlapWidth / frame.spanX * zoomFactor;
175
+ const rMax = Math.max(1, frame.referenceWidth / frame.overlapWidth);
176
+ const candidates = candidateOffsets(sortedMarkers.length);
177
+ const reachOf = offset => Math.hypot(offset.dx, offset.dy);
178
+ const furthest = reachOf(candidates[candidates.length - 1]);
179
+ const placed = [];
180
+ const offsets = new Map();
181
+ // Pins at one point search on from where the previous one found its place, so a dense cluster costs one pass.
182
+ const searchFrom = new Map();
183
+ // Offsets here are those of the visible mark; a drop pin's own offset adds its lift back below.
184
+ const place = (marker, preferred, anchorId) => {
185
+ const p = position(marker);
186
+ // The location is fixed; route stops keep their anchors even when they coincide with it.
187
+ const obstacle = !isStop(marker) && location;
188
+ // Two pins whose points are further apart than their offsets plus one pin never meet at a wider fit
189
+ // (|g·r + o| ≥ |g| − |o| for r ≥ 1), so only the pins near this one are checked, once per pin.
190
+ const near = placed.filter(other => Math.hypot(p.x - other.x, p.y - other.y) < furthest + other.reach + PIN_SPACING);
191
+ // Leave 22 px around the fixed dot: 12 px pin radius + 8.5 px dot radius + 1.5 px gap.
192
+ const clear = (offset, anchor) => (!obstacle || closest(p.x - (obstacle.x - frame.minX) * scale, p.y - (obstacle.y - frame.minY) * scale, offset.dx, offset.dy, rMax) >= 22 - 1e-9) && near.every(other => closest(p.x - other.x, p.y - other.y, offset.dx - other.dx, offset.dy - other.dy, rMax) >= (other.id === anchor ? ATTACHED_SPACING : PIN_SPACING) - 1e-9);
193
+ let offset = preferred;
194
+ if (!clear(preferred, anchorId)) {
195
+ const key = `${Math.round(p.x)},${Math.round(p.y)}`;
196
+ const first = searchFrom.get(key) ?? 0;
197
+ const found = candidates.findIndex((candidate, index) => index >= first && clear(candidate));
198
+ offset = candidates[found] ?? candidates[candidates.length - 1];
199
+ searchFrom.set(key, found < 0 ? candidates.length : found + 1);
200
+ }
201
+ offsets.set(marker.id, {
202
+ dx: offset.dx,
203
+ dy: offset.dy + lift(marker)
204
+ });
205
+ const pin = {
206
+ id: marker.id,
207
+ ...p,
208
+ ...offset,
209
+ reach: reachOf(offset)
210
+ };
211
+ placed.push(pin);
212
+ return pin;
213
+ };
214
+ // Stops come first in mark order; a later stop that would cover an earlier one (such as "+" beside C) moves aside.
215
+ const stops = sortedMarkers.filter(isStop).map(stop => place(stop, {
216
+ dx: 0,
217
+ dy: -lift(stop)
218
+ }));
219
+ for (const marker of sortedMarkers.filter(marker => !isStop(marker))) {
220
+ const p = position(marker);
221
+ // The attachment sits top right of where the stop is drawn, wherever the hit lies around it.
222
+ const own = {
223
+ dx: 0,
224
+ dy: -lift(marker)
225
+ };
226
+ const anchor = stops.find(stop => Math.hypot(p.x + own.dx - stop.x - stop.dx, p.y + own.dy - stop.y - stop.dy) < PIN_SPACING);
227
+ place(marker, anchor ? {
228
+ dx: anchor.x + anchor.dx + 17 - p.x,
229
+ dy: anchor.y + anchor.dy - 17 - p.y
230
+ } : own, anchor?.id);
231
+ }
232
+ return offsets;
233
+ }
234
+ /**
235
+ * The touch hit radius of each pin in px: up to 22 px (a 44 px target), but never into a neighbour's visible mark
236
+ * (radius up to 14 px) at any width, so a tap on a visible pin always reaches it.
237
+ */
238
+ export function pinHitRadii(sortedMarkers, frame, offsets, zoomFactor = 1) {
239
+ const position = pinPlaces(sortedMarkers, frame, zoomFactor);
240
+ const rMax = Math.max(1, frame.referenceWidth / frame.overlapWidth);
241
+ const places = sortedMarkers.map(marker => {
242
+ const offset = offsets.get(marker.id) ?? {
243
+ dx: 0,
244
+ dy: 0
245
+ };
246
+ return {
247
+ id: marker.id,
248
+ ...position(marker),
249
+ dx: offset.dx,
250
+ dy: offset.dy - lift(marker)
251
+ };
252
+ });
253
+ return new Map(places.map(place => {
254
+ let nearest = Infinity;
255
+ for (const other of places) if (other !== place) nearest = Math.min(nearest, closest(place.x - other.x, place.y - other.y, place.dx - other.dx, place.dy - other.dy, rMax));
256
+ return [place.id, Math.max(12, Math.min(22, nearest - 14))];
257
+ }));
258
+ }
259
+ export const metersPerPixel = (lat, zoom) => EARTH_CIRCUMFERENCE * Math.cos(lat * Math.PI / 180) / (512 * 2 ** zoom);
260
+ export function scaleBar(mpp, maxPx = 96) {
261
+ const maximum = mpp * maxPx;
262
+ const magnitude = 10 ** Math.floor(Math.log10(maximum));
263
+ const meters = ([5, 2, 1].find(value => value * magnitude <= maximum) ?? 1) * magnitude;
264
+ return {
265
+ meters,
266
+ label: meters >= 1000 ? `${meters / 1000} km` : `${Number(meters.toPrecision(12))} m`
267
+ };
268
+ }
269
+ //# sourceMappingURL=map-view-geometry.js.map