@elabs-ai/components-maps 4.2.0 → 5.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.
Files changed (291) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-2FPB53NG.js +36 -0
  3. package/dist/chunk-2FPB53NG.js.map +1 -0
  4. package/dist/chunk-2VVJRT66.js +37 -0
  5. package/dist/chunk-2VVJRT66.js.map +1 -0
  6. package/dist/chunk-3HXFONRP.js +2 -0
  7. package/dist/chunk-3HXFONRP.js.map +1 -0
  8. package/dist/chunk-3SJN3IYT.js +2 -0
  9. package/dist/chunk-3SJN3IYT.js.map +1 -0
  10. package/dist/chunk-4DANBD5B.js +55 -0
  11. package/dist/chunk-4DANBD5B.js.map +1 -0
  12. package/dist/chunk-5PDVG53B.js +2 -0
  13. package/dist/chunk-5PDVG53B.js.map +1 -0
  14. package/dist/chunk-643R7IJC.js +2 -0
  15. package/dist/chunk-643R7IJC.js.map +1 -0
  16. package/dist/chunk-B3ZDBQ6I.js +164 -0
  17. package/dist/chunk-B3ZDBQ6I.js.map +1 -0
  18. package/dist/chunk-BGCE3RR5.js +35 -0
  19. package/dist/chunk-BGCE3RR5.js.map +1 -0
  20. package/dist/chunk-C4VI37TD.js +485 -0
  21. package/dist/chunk-C4VI37TD.js.map +1 -0
  22. package/dist/chunk-CJJT6AEI.js +179 -0
  23. package/dist/chunk-CJJT6AEI.js.map +1 -0
  24. package/dist/chunk-D2RCPTRO.js +2 -0
  25. package/dist/chunk-D2RCPTRO.js.map +1 -0
  26. package/dist/chunk-DTV7JZVN.js +2 -0
  27. package/dist/chunk-DTV7JZVN.js.map +1 -0
  28. package/dist/chunk-E65Z3MSV.js +80 -0
  29. package/dist/chunk-E65Z3MSV.js.map +1 -0
  30. package/dist/chunk-E7VPNINZ.js +70 -0
  31. package/dist/chunk-E7VPNINZ.js.map +1 -0
  32. package/dist/chunk-FHV5IA6N.js +100 -0
  33. package/dist/chunk-FHV5IA6N.js.map +1 -0
  34. package/dist/chunk-FQ6V7BFZ.js +347 -0
  35. package/dist/chunk-FQ6V7BFZ.js.map +1 -0
  36. package/dist/chunk-G7JDXDJN.js +19 -0
  37. package/dist/chunk-G7JDXDJN.js.map +1 -0
  38. package/dist/chunk-GWULZOMW.js +21 -0
  39. package/dist/chunk-GWULZOMW.js.map +1 -0
  40. package/dist/chunk-GYDTTAVD.js +120 -0
  41. package/dist/chunk-GYDTTAVD.js.map +1 -0
  42. package/dist/chunk-HCOBWIM4.js +48 -0
  43. package/dist/chunk-HCOBWIM4.js.map +1 -0
  44. package/dist/chunk-IAHK5MVT.js +2 -0
  45. package/dist/chunk-IAHK5MVT.js.map +1 -0
  46. package/dist/chunk-JGJ6442I.js +235 -0
  47. package/dist/chunk-JGJ6442I.js.map +1 -0
  48. package/dist/chunk-JYFERXZT.js +18 -0
  49. package/dist/chunk-JYFERXZT.js.map +1 -0
  50. package/dist/chunk-LU2UEKFM.js +53 -0
  51. package/dist/chunk-LU2UEKFM.js.map +1 -0
  52. package/dist/chunk-LVYM6C5C.js +2 -0
  53. package/dist/chunk-LVYM6C5C.js.map +1 -0
  54. package/dist/chunk-MIMDP3EU.js +104 -0
  55. package/dist/chunk-MIMDP3EU.js.map +1 -0
  56. package/dist/chunk-NNBURJHY.js +2 -0
  57. package/dist/chunk-NNBURJHY.js.map +1 -0
  58. package/dist/chunk-NPUC3V37.js +2 -0
  59. package/dist/chunk-NPUC3V37.js.map +1 -0
  60. package/dist/chunk-OEVVNPN3.js +201 -0
  61. package/dist/chunk-OEVVNPN3.js.map +1 -0
  62. package/dist/chunk-PF3R2BXM.js +200 -0
  63. package/dist/chunk-PF3R2BXM.js.map +1 -0
  64. package/dist/chunk-Q2I5YODW.js +2 -0
  65. package/dist/chunk-Q2I5YODW.js.map +1 -0
  66. package/dist/chunk-QDWWP3N7.js +247 -0
  67. package/dist/chunk-QDWWP3N7.js.map +1 -0
  68. package/dist/chunk-QU33KPMP.js +2 -0
  69. package/dist/chunk-QU33KPMP.js.map +1 -0
  70. package/dist/chunk-RK4BUMHS.js +2 -0
  71. package/dist/chunk-RK4BUMHS.js.map +1 -0
  72. package/dist/chunk-RNSLG64F.js +26 -0
  73. package/dist/chunk-RNSLG64F.js.map +1 -0
  74. package/dist/chunk-RV673TB4.js +68 -0
  75. package/dist/chunk-RV673TB4.js.map +1 -0
  76. package/dist/chunk-S7N2WMVT.js +180 -0
  77. package/dist/chunk-S7N2WMVT.js.map +1 -0
  78. package/dist/chunk-T3Z6TCDD.js +2 -0
  79. package/dist/chunk-T3Z6TCDD.js.map +1 -0
  80. package/dist/chunk-T4TJTVVA.js +105 -0
  81. package/dist/chunk-T4TJTVVA.js.map +1 -0
  82. package/dist/chunk-UMOYRCDX.js +103 -0
  83. package/dist/chunk-UMOYRCDX.js.map +1 -0
  84. package/dist/chunk-V2REM6UT.js +185 -0
  85. package/dist/chunk-V2REM6UT.js.map +1 -0
  86. package/dist/chunk-VFBRFGID.js +89 -0
  87. package/dist/chunk-VFBRFGID.js.map +1 -0
  88. package/dist/chunk-W76NLFER.js +103 -0
  89. package/dist/chunk-W76NLFER.js.map +1 -0
  90. package/dist/chunk-WNU4IOGL.js +116 -0
  91. package/dist/chunk-WNU4IOGL.js.map +1 -0
  92. package/dist/chunk-WS4Z7ENS.js +494 -0
  93. package/dist/chunk-WS4Z7ENS.js.map +1 -0
  94. package/dist/chunk-XQT7YLJY.js +497 -0
  95. package/dist/chunk-XQT7YLJY.js.map +1 -0
  96. package/dist/chunk-Y44MEKJV.js +21 -0
  97. package/dist/chunk-Y44MEKJV.js.map +1 -0
  98. package/dist/chunk-Z4EPSOB4.js +53 -0
  99. package/dist/chunk-Z4EPSOB4.js.map +1 -0
  100. package/dist/chunk-Z5OPDBDC.js +2 -0
  101. package/dist/chunk-Z5OPDBDC.js.map +1 -0
  102. package/dist/index.d.ts +1241 -46
  103. package/dist/index.js +189 -1471
  104. package/dist/index.js.map +1 -1
  105. package/dist/lib/arc-math.js +8 -0
  106. package/dist/lib/arc-math.js.map +1 -0
  107. package/dist/lib/map-position.js +11 -0
  108. package/dist/lib/map-position.js.map +1 -0
  109. package/dist/lib/merge-hover-paint.js +10 -0
  110. package/dist/lib/merge-hover-paint.js.map +1 -0
  111. package/dist/lib/plan-crs.js +14 -0
  112. package/dist/lib/plan-crs.js.map +1 -0
  113. package/dist/lib/plan-patterns.js +14 -0
  114. package/dist/lib/plan-patterns.js.map +1 -0
  115. package/dist/lib/plan-regions.js +18 -0
  116. package/dist/lib/plan-regions.js.map +1 -0
  117. package/dist/lib/plan-status.js +14 -0
  118. package/dist/lib/plan-status.js.map +1 -0
  119. package/dist/lib/route-arrow.js +12 -0
  120. package/dist/lib/route-arrow.js.map +1 -0
  121. package/dist/lib/use-map-breakpoint.js +33 -0
  122. package/dist/lib/use-map-breakpoint.js.map +1 -0
  123. package/dist/lib/use-plan-projection.js +9 -0
  124. package/dist/lib/use-plan-projection.js.map +1 -0
  125. package/dist/lib/use-token-color.js +10 -0
  126. package/dist/lib/use-token-color.js.map +1 -0
  127. package/dist/lib/warn-once.js +10 -0
  128. package/dist/lib/warn-once.js.map +1 -0
  129. package/dist/map-annotation/anchor.js +14 -0
  130. package/dist/map-annotation/anchor.js.map +1 -0
  131. package/dist/map-annotation/index.js +21 -0
  132. package/dist/map-annotation/index.js.map +1 -0
  133. package/dist/map-annotation/map-annotation.js +18 -0
  134. package/dist/map-annotation/map-annotation.js.map +1 -0
  135. package/dist/map-arc/index.js +16 -0
  136. package/dist/map-arc/index.js.map +1 -0
  137. package/dist/map-arc/map-arc.js +13 -0
  138. package/dist/map-arc/map-arc.js.map +1 -0
  139. package/dist/map-canvas/index.css +11 -0
  140. package/dist/map-canvas/index.css.map +1 -0
  141. package/dist/map-canvas/index.js +20 -0
  142. package/dist/map-canvas/index.js.map +1 -0
  143. package/dist/map-canvas/map-canvas.css +11 -0
  144. package/dist/map-canvas/map-canvas.css.map +1 -0
  145. package/dist/map-canvas/map-canvas.js +17 -0
  146. package/dist/map-canvas/map-canvas.js.map +1 -0
  147. package/dist/map-canvas/map-context.js +11 -0
  148. package/dist/map-canvas/map-context.js.map +1 -0
  149. package/dist/map-canvas/use-resolved-basemap-theme.js +9 -0
  150. package/dist/map-canvas/use-resolved-basemap-theme.js.map +1 -0
  151. package/dist/map-cluster-layer/index.js +12 -0
  152. package/dist/map-cluster-layer/index.js.map +1 -0
  153. package/dist/map-cluster-layer/map-cluster-layer.js +12 -0
  154. package/dist/map-cluster-layer/map-cluster-layer.js.map +1 -0
  155. package/dist/map-controls/index.js +11 -0
  156. package/dist/map-controls/index.js.map +1 -0
  157. package/dist/map-controls/map-controls.js +11 -0
  158. package/dist/map-controls/map-controls.js.map +1 -0
  159. package/dist/map-geojson/index.js +13 -0
  160. package/dist/map-geojson/index.js.map +1 -0
  161. package/dist/map-geojson/map-geojson.js +13 -0
  162. package/dist/map-geojson/map-geojson.js.map +1 -0
  163. package/dist/map-inset/index.css +11 -0
  164. package/dist/map-inset/index.css.map +1 -0
  165. package/dist/map-inset/index.js +20 -0
  166. package/dist/map-inset/index.js.map +1 -0
  167. package/dist/map-inset/map-inset.css +11 -0
  168. package/dist/map-inset/map-inset.css.map +1 -0
  169. package/dist/map-inset/map-inset.js +20 -0
  170. package/dist/map-inset/map-inset.js.map +1 -0
  171. package/dist/map-legend/index.js +14 -0
  172. package/dist/map-legend/index.js.map +1 -0
  173. package/dist/map-legend/map-corner.js +10 -0
  174. package/dist/map-legend/map-corner.js.map +1 -0
  175. package/dist/map-legend/map-legend.js +11 -0
  176. package/dist/map-legend/map-legend.js.map +1 -0
  177. package/dist/map-marker/index.js +22 -0
  178. package/dist/map-marker/index.js.map +1 -0
  179. package/dist/map-marker/map-marker.js +22 -0
  180. package/dist/map-marker/map-marker.js.map +1 -0
  181. package/dist/map-north-arrow/index.js +11 -0
  182. package/dist/map-north-arrow/index.js.map +1 -0
  183. package/dist/map-north-arrow/map-north-arrow.js +11 -0
  184. package/dist/map-north-arrow/map-north-arrow.js.map +1 -0
  185. package/dist/map-plan/index.js +30 -0
  186. package/dist/map-plan/index.js.map +1 -0
  187. package/dist/map-plan/map-plan-legend.js +10 -0
  188. package/dist/map-plan/map-plan-legend.js.map +1 -0
  189. package/dist/map-plan/map-plan-overlay.js +13 -0
  190. package/dist/map-plan/map-plan-overlay.js.map +1 -0
  191. package/dist/map-plan/map-plan-status.js +9 -0
  192. package/dist/map-plan/map-plan-status.js.map +1 -0
  193. package/dist/map-plan/map-plan-table.js +10 -0
  194. package/dist/map-plan/map-plan-table.js.map +1 -0
  195. package/dist/map-plan/use-plan-selection.js +9 -0
  196. package/dist/map-plan/use-plan-selection.js.map +1 -0
  197. package/dist/map-plan-image/index.js +11 -0
  198. package/dist/map-plan-image/index.js.map +1 -0
  199. package/dist/map-plan-image/map-plan-image.js +11 -0
  200. package/dist/map-plan-image/map-plan-image.js.map +1 -0
  201. package/dist/map-popup/index.js +12 -0
  202. package/dist/map-popup/index.js.map +1 -0
  203. package/dist/map-popup/map-popup.js +12 -0
  204. package/dist/map-popup/map-popup.js.map +1 -0
  205. package/dist/map-route/index.js +12 -0
  206. package/dist/map-route/index.js.map +1 -0
  207. package/dist/map-route/map-route.js +12 -0
  208. package/dist/map-route/map-route.js.map +1 -0
  209. package/dist/map-scale-bar/index.js +18 -0
  210. package/dist/map-scale-bar/index.js.map +1 -0
  211. package/dist/map-scale-bar/map-scale-bar.js +18 -0
  212. package/dist/map-scale-bar/map-scale-bar.js.map +1 -0
  213. package/package.json +5 -5
  214. package/src/index.ts +83 -0
  215. package/src/lib/arc-math.test.ts +19 -0
  216. package/src/lib/arc-math.ts +6 -1
  217. package/src/lib/map-position.ts +77 -0
  218. package/src/lib/merge-hover-paint.ts +44 -13
  219. package/src/lib/plan-crs.parity.test.ts +39 -0
  220. package/src/lib/plan-crs.test.ts +248 -0
  221. package/src/lib/plan-crs.ts +252 -0
  222. package/src/lib/plan-patterns.test.ts +255 -0
  223. package/src/lib/plan-patterns.ts +160 -0
  224. package/src/lib/plan-regions.test.ts +186 -0
  225. package/src/lib/plan-regions.ts +209 -0
  226. package/src/lib/plan-status.test.ts +87 -0
  227. package/src/lib/plan-status.ts +147 -0
  228. package/src/lib/route-arrow.test.ts +88 -0
  229. package/src/lib/route-arrow.ts +59 -0
  230. package/src/lib/use-map-breakpoint.test.ts +83 -0
  231. package/src/lib/use-map-breakpoint.ts +238 -0
  232. package/src/lib/use-plan-projection.ts +157 -0
  233. package/src/lib/warn-once.ts +21 -0
  234. package/src/map-annotation/anchor.ts +92 -0
  235. package/src/map-annotation/index.ts +15 -0
  236. package/src/map-annotation/map-annotation.stories.tsx +113 -0
  237. package/src/map-annotation/map-annotation.test.tsx +106 -0
  238. package/src/map-annotation/map-annotation.tsx +356 -0
  239. package/src/map-arc/map-arc.tsx +26 -5
  240. package/src/map-canvas/map-canvas.stories.tsx +211 -0
  241. package/src/map-canvas/map-canvas.test.tsx +389 -0
  242. package/src/map-canvas/map-canvas.tsx +457 -17
  243. package/src/map-canvas/map-context.ts +11 -0
  244. package/src/map-cluster-layer/map-cluster-layer.tsx +14 -1
  245. package/src/map-controls/map-controls.stories.tsx +16 -0
  246. package/src/map-controls/map-controls.test.tsx +145 -1
  247. package/src/map-controls/map-controls.tsx +57 -12
  248. package/src/map-geojson/map-geojson.stories.tsx +153 -3
  249. package/src/map-geojson/map-geojson.test.tsx +453 -0
  250. package/src/map-geojson/map-geojson.tsx +591 -51
  251. package/src/map-inset/index.ts +1 -0
  252. package/src/map-inset/map-inset.stories.tsx +61 -0
  253. package/src/map-inset/map-inset.test.tsx +117 -0
  254. package/src/map-inset/map-inset.tsx +264 -0
  255. package/src/map-legend/index.ts +10 -0
  256. package/src/map-legend/map-corner.ts +23 -0
  257. package/src/map-legend/map-legend.stories.tsx +171 -0
  258. package/src/map-legend/map-legend.test.tsx +136 -0
  259. package/src/map-legend/map-legend.tsx +264 -0
  260. package/src/map-marker/map-marker.stories.tsx +136 -4
  261. package/src/map-marker/map-marker.test.tsx +328 -3
  262. package/src/map-marker/map-marker.tsx +370 -33
  263. package/src/map-north-arrow/index.ts +1 -0
  264. package/src/map-north-arrow/map-north-arrow.stories.tsx +65 -0
  265. package/src/map-north-arrow/map-north-arrow.test.tsx +39 -0
  266. package/src/map-north-arrow/map-north-arrow.tsx +82 -0
  267. package/src/map-plan/index.ts +13 -0
  268. package/src/map-plan/map-plan-legend.test.tsx +61 -0
  269. package/src/map-plan/map-plan-legend.tsx +83 -0
  270. package/src/map-plan/map-plan-overlay.stories.tsx +260 -0
  271. package/src/map-plan/map-plan-overlay.test.tsx +302 -0
  272. package/src/map-plan/map-plan-overlay.tsx +531 -0
  273. package/src/map-plan/map-plan-status.test.tsx +71 -0
  274. package/src/map-plan/map-plan-status.tsx +77 -0
  275. package/src/map-plan/map-plan-table.test.tsx +86 -0
  276. package/src/map-plan/map-plan-table.tsx +153 -0
  277. package/src/map-plan/use-plan-selection.ts +78 -0
  278. package/src/map-plan-image/index.ts +1 -0
  279. package/src/map-plan-image/map-plan-image.stories.tsx +301 -0
  280. package/src/map-plan-image/map-plan-image.test.tsx +176 -0
  281. package/src/map-plan-image/map-plan-image.tsx +279 -0
  282. package/src/map-popup/map-popup.tsx +15 -10
  283. package/src/map-route/index.ts +1 -1
  284. package/src/map-route/map-route.test.tsx +216 -0
  285. package/src/map-route/map-route.tsx +105 -6
  286. package/src/map-scale-bar/index.ts +9 -0
  287. package/src/map-scale-bar/map-scale-bar.stories.tsx +63 -0
  288. package/src/map-scale-bar/map-scale-bar.test.tsx +65 -0
  289. package/src/map-scale-bar/map-scale-bar.tsx +145 -0
  290. package/src/test-utils/locator-fixture.ts +75 -0
  291. package/src/test-utils/maplibre-mock.ts +256 -19
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import "../chunk-3HXFONRP.js";
3
+ import {
4
+ MapScaleBar,
5
+ computeMapScale,
6
+ metresPerPixel,
7
+ niceDistance
8
+ } from "../chunk-T4TJTVVA.js";
9
+ import "../chunk-GWULZOMW.js";
10
+ import "../chunk-W76NLFER.js";
11
+ import "../chunk-JYFERXZT.js";
12
+ export {
13
+ MapScaleBar,
14
+ computeMapScale,
15
+ metresPerPixel,
16
+ niceDistance
17
+ };
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ MapScaleBar,
5
+ computeMapScale,
6
+ metresPerPixel,
7
+ niceDistance
8
+ } from "../chunk-T4TJTVVA.js";
9
+ import "../chunk-GWULZOMW.js";
10
+ import "../chunk-W76NLFER.js";
11
+ import "../chunk-JYFERXZT.js";
12
+ export {
13
+ MapScaleBar,
14
+ computeMapScale,
15
+ metresPerPixel,
16
+ niceDistance
17
+ };
18
+ //# sourceMappingURL=map-scale-bar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elabs-ai/components-maps",
3
- "version": "4.2.0",
3
+ "version": "5.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "maplibre-gl": "^5.15.0",
33
33
  "react": "^18.2.0 || ^19.0.0",
34
34
  "react-dom": "^18.2.0 || ^19.0.0",
35
- "@elabs-ai/components-tokens": "4.2.0",
36
- "@elabs-ai/components-ui": "4.2.0"
35
+ "@elabs-ai/components-tokens": "^5.1.0",
36
+ "@elabs-ai/components-ui": "^5.1.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@testing-library/jest-dom": "^6.6.3",
@@ -51,9 +51,9 @@
51
51
  "typescript": "^5.7.3",
52
52
  "vitest": "^3.0.2",
53
53
  "@elabs-ai/components-eslint-config": "0.1.0",
54
- "@elabs-ai/components-tokens": "4.2.0",
54
+ "@elabs-ai/components-tokens": "5.1.0",
55
55
  "@elabs-ai/components-typescript-config": "0.1.0",
56
- "@elabs-ai/components-ui": "4.2.0"
56
+ "@elabs-ai/components-ui": "5.1.0"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsup && node ../../scripts/link-dist-css.mjs",
package/src/index.ts CHANGED
@@ -23,10 +23,93 @@ export * from "./map-route";
23
23
  export * from "./map-arc";
24
24
  export * from "./map-geojson";
25
25
  export * from "./map-cluster-layer";
26
+ export * from "./map-plan-image";
27
+ export * from "./map-plan";
28
+
29
+ // Custom (non-geographic) plans: a floor plan, a factory layout, a carriage as
30
+ // the map itself. MapLibre is Web Mercator only, so `createPlanCrs` synthesizes
31
+ // the plan's own coordinate system on top of it — declare it once with
32
+ // `<MapCanvas plan>` and every layer then speaks plan units.
33
+ export {
34
+ createPlanCrs,
35
+ PLAN_MERCATOR_SPAN,
36
+ type PlanCrs,
37
+ type PlanExtent,
38
+ type PlanImageCoordinates,
39
+ type PlanPoint,
40
+ } from "./lib/plan-crs";
41
+ export {
42
+ planBoundsOfGeometry,
43
+ planBoundsUnion,
44
+ planGroupId,
45
+ planGroupsFromRegions,
46
+ planRegionCentre,
47
+ planRegionsFromGeoJSON,
48
+ type MapPlanGroup,
49
+ type MapPlanRegion,
50
+ type PlanRegionBounds,
51
+ type PlanRegionField,
52
+ type PlanRegionFields,
53
+ } from "./lib/plan-regions";
54
+ export {
55
+ usePlanProjection,
56
+ type PlanScreenBox,
57
+ type UsePlanProjectionOptions,
58
+ } from "./lib/use-plan-projection";
59
+ export {
60
+ PLAN_FILL_OPACITY,
61
+ PLAN_STATUS_ENCODING,
62
+ PLAN_STATUSES,
63
+ planStatusMatch,
64
+ type PlanDash,
65
+ type PlanPatternKind,
66
+ type PlanStatus,
67
+ type PlanStatusEncoding,
68
+ } from "./lib/plan-status";
69
+ export { createPlanPatternTile, planPatternImageId, usePlanPatterns } from "./lib/plan-patterns";
70
+ // A plan paints its own shapes, so a consumer needs the same token seam the package
71
+ // uses internally: WebGL cannot read CSS variables, and a paint must re-resolve when
72
+ // the brand theme changes. Call it inside `<MapCanvas>`.
73
+ export { useTokenColor } from "./lib/use-token-color";
74
+ export {
75
+ createRouteArrowImage,
76
+ routeArrowImageId,
77
+ ROUTE_ARROW_SIZE,
78
+ type RouteArrowImage,
79
+ } from "./lib/route-arrow";
80
+ export type { MapGeoPosition, MapPlanPosition, MapPosition } from "./lib/map-position";
81
+
82
+ // Locator furniture — RM-125: responsive tiers (a documented copy of the
83
+ // `charts` helper), legend, scale bar, north arrow, inset, annotations.
84
+ export type { MapProjectionOption } from "./map-canvas/map-canvas";
85
+ export type { MapMarkerLabelSpec } from "./map-marker/map-marker";
86
+ export type { MapGeoJSONPattern, MapGeoJSONVignette } from "./map-geojson/map-geojson";
87
+ export {
88
+ DEFAULT_MAP_HEIGHT,
89
+ MAP_BREAKPOINTS,
90
+ MAP_BREAKPOINT_THRESHOLDS,
91
+ isMapResponsiveByBreakpoint,
92
+ mapBreakpointForWidth,
93
+ resolveMapResponsive,
94
+ useMapBreakpoint,
95
+ useMapResponsive,
96
+ type MapBreakpoint,
97
+ type MapHeight,
98
+ type MapResponsive,
99
+ type MapResponsiveByBreakpoint,
100
+ } from "./lib/use-map-breakpoint";
101
+ export * from "./map-legend";
102
+ export * from "./map-scale-bar";
103
+ export * from "./map-north-arrow";
104
+ export * from "./map-inset";
105
+ export * from "./map-annotation";
26
106
 
27
107
  // Convenience re-exports so consumers can type map work without a direct
28
108
  // maplibre-gl dependency.
29
109
  export type {
110
+ Coordinates,
111
+ FitBoundsOptions,
112
+ ImageSourceSpecification,
30
113
  LngLatLike,
31
114
  LngLatBoundsLike,
32
115
  StyleSpecification,
@@ -33,6 +33,25 @@ describe("buildArcCoordinates", () => {
33
33
  expect(points.at(-1)![0]).toBeCloseTo(237.6, 5);
34
34
  });
35
35
 
36
+ it("keeps a plan coordinate where it was put when wrapping is off", () => {
37
+ // A 1600-unit-wide hall: x = 900 is a place on the floor, not a longitude.
38
+ // Unwrapping it to 540 would move the arc's end 360 units to the left.
39
+ const wrapped = buildArcCoordinates([100, 50], [900, 50], 0, 8);
40
+ const plan = buildArcCoordinates([100, 50], [900, 50], 0, 8, false);
41
+
42
+ expect(wrapped.at(-1)![0]).toBe(540);
43
+ expect(plan.at(-1)![0]).toBe(900);
44
+ });
45
+
46
+ it("still curves in plan space, symmetrically about the straight line", () => {
47
+ const points = buildArcCoordinates([0, 0], [800, 0], 0.2, 16, false);
48
+
49
+ expect(points).toHaveLength(17);
50
+ expect(points[0]).toEqual([0, 0]);
51
+ expect(points.at(-1)).toEqual([800, 0]);
52
+ expect(Math.abs(points[8]![1])).toBeGreaterThan(0);
53
+ });
54
+
36
55
  it("bends to the opposite side with negative curvature", () => {
37
56
  const up = buildArcCoordinates([0, 0], [20, 0], 0.2, 8);
38
57
  const down = buildArcCoordinates([0, 0], [20, 0], -0.2, 8);
@@ -7,17 +7,22 @@
7
7
  * relative to the origin so arcs cross the antimeridian via the shorter
8
8
  * great-circle direction — resulting longitudes may fall outside [-180, 180],
9
9
  * which MapLibre renders correctly on the globe projection.
10
+ *
11
+ * `wrapLongitude: false` turns that unwrap off, for coordinates that are not
12
+ * longitudes at all: on a plan map, an x of 900 in a 1600-unit-wide hall is a
13
+ * position, and shifting it by 360 would move the arc's end across the floor.
10
14
  */
11
15
  export function buildArcCoordinates(
12
16
  from: [number, number],
13
17
  to: [number, number],
14
18
  curvature: number,
15
19
  samples: number,
20
+ wrapLongitude = true,
16
21
  ): [number, number][] {
17
22
  const [x0, y0] = from;
18
23
  const [xTo, y2] = to;
19
24
  const rawDx = xTo - x0;
20
- const x2 = rawDx > 180 ? xTo - 360 : rawDx < -180 ? xTo + 360 : xTo;
25
+ const x2 = !wrapLongitude ? xTo : rawDx > 180 ? xTo - 360 : rawDx < -180 ? xTo + 360 : xTo;
21
26
  const dx = x2 - x0;
22
27
  const dy = y2 - y0;
23
28
  const distance = Math.hypot(dx, dy);
@@ -0,0 +1,77 @@
1
+ import type { PlanCrs, PlanPoint } from "./plan-crs";
2
+ import { warnMapOnce } from "./warn-once";
3
+
4
+ /** A geographic position — the only kind an ordinary basemap understands. */
5
+ export type MapGeoPosition = {
6
+ /** Longitude coordinate. */
7
+ longitude: number;
8
+ /** Latitude coordinate. */
9
+ latitude: number;
10
+ x?: never;
11
+ y?: never;
12
+ };
13
+
14
+ /**
15
+ * A position in PLAN units, for a canvas that declares a `plan` extent: metres
16
+ * along the hall, millimetres across the carriage, pixels into the drawing.
17
+ */
18
+ export type MapPlanPosition = {
19
+ /** Horizontal position in plan units. */
20
+ x: number;
21
+ /** Vertical position in plan units, from the plan's own origin. */
22
+ y: number;
23
+ longitude?: never;
24
+ latitude?: never;
25
+ };
26
+
27
+ /**
28
+ * Either a geographic position or a plan one — never a mix. The `never`-typed
29
+ * twins make `{ longitude, x }` a type error rather than a silent winner.
30
+ */
31
+ export type MapPosition = MapGeoPosition | MapPlanPosition;
32
+
33
+ /**
34
+ * Reduce a position prop to the two scalars MapLibre wants, so every downstream
35
+ * effect keeps primitive dependencies.
36
+ */
37
+ export function resolveMapPosition(
38
+ position: {
39
+ longitude?: number;
40
+ latitude?: number;
41
+ x?: number;
42
+ y?: number;
43
+ },
44
+ plan: PlanCrs | null,
45
+ component: string,
46
+ ): [number, number] {
47
+ const { longitude, latitude, x, y } = position;
48
+
49
+ if (x !== undefined && y !== undefined) {
50
+ if (!plan) {
51
+ warnMapOnce(
52
+ `${component}-plan-position`,
53
+ `<${component} x y> needs a plan coordinate system. Set \`plan\` on <MapCanvas>, or pass \`longitude\`/\`latitude\`.`,
54
+ );
55
+ return [0, 0];
56
+ }
57
+ return plan.toLngLat({ x, y });
58
+ }
59
+
60
+ if (longitude === undefined || latitude === undefined) {
61
+ warnMapOnce(
62
+ `${component}-missing-position`,
63
+ `<${component}> needs a position: \`longitude\` and \`latitude\`, or \`x\` and \`y\` on a plan.`,
64
+ );
65
+ return [0, 0];
66
+ }
67
+
68
+ return [longitude, latitude];
69
+ }
70
+
71
+ /** The plan point for a lng/lat, or `null` on an ordinary geographic canvas. */
72
+ export function toPlanPoint(
73
+ plan: PlanCrs | null,
74
+ lngLat: { lng: number; lat: number },
75
+ ): PlanPoint | null {
76
+ return plan ? plan.toPlan([lngLat.lng, lngLat.lat]) : null;
77
+ }
@@ -1,21 +1,52 @@
1
+ /** Per-feature paint states a layer can express through MapLibre feature-state. */
2
+ export interface FeatureStatePaint<T> {
3
+ /** Applied to the feature under the cursor or keyboard focus. */
4
+ hover?: T;
5
+ /** Applied to the selected feature(s); wins over `hover`. */
6
+ selected?: T;
7
+ }
8
+
1
9
  /**
2
- * Merge hover paint into base paint as MapLibre `case` expressions keyed on
3
- * the per-feature `hover` feature-state, so only the hovered feature changes
4
- * appearance.
10
+ * Merge hover/selected paint into base paint as MapLibre `case` expressions
11
+ * keyed on per-feature feature-state, so only the affected features change
12
+ * appearance. `selected` is tested first, so a selected feature keeps looking
13
+ * selected while the cursor is over it.
14
+ *
15
+ * Only paint properties that accept feature-state belong here (`fill-color`,
16
+ * `fill-opacity`, `line-color`, `line-width`, …). `fill-pattern` and
17
+ * `line-dasharray` do NOT: they read feature PROPERTIES, which is why status
18
+ * texture and selection are separate channels.
5
19
  */
6
- export function mergeHoverPaint<T extends Record<string, unknown>>(
20
+ export function mergeFeatureStatePaint<T extends Record<string, unknown>>(
7
21
  paint: T,
8
- hoverPaint: T | undefined,
22
+ states: FeatureStatePaint<T>,
9
23
  ): T {
10
- if (!hoverPaint) return paint;
24
+ // Identity preserved when there is nothing to merge: callers memoize on it.
25
+ if (!states.hover && !states.selected) return paint;
26
+
11
27
  const merged: Record<string, unknown> = { ...paint };
12
- for (const [key, hoverValue] of Object.entries(hoverPaint)) {
13
- if (hoverValue === undefined) continue;
14
- const baseValue = merged[key];
15
- merged[key] =
16
- baseValue === undefined
17
- ? hoverValue
18
- : ["case", ["boolean", ["feature-state", "hover"], false], hoverValue, baseValue];
28
+
29
+ // Applied hover-first so that `selected` ends up as the OUTER case.
30
+ for (const state of ["hover", "selected"] as const) {
31
+ const statePaint = states[state];
32
+ if (!statePaint) continue;
33
+ for (const [key, stateValue] of Object.entries(statePaint)) {
34
+ if (stateValue === undefined) continue;
35
+ const baseValue = merged[key];
36
+ merged[key] =
37
+ baseValue === undefined
38
+ ? stateValue
39
+ : ["case", ["boolean", ["feature-state", state], false], stateValue, baseValue];
40
+ }
19
41
  }
42
+
20
43
  return merged as T;
21
44
  }
45
+
46
+ /** Hover-only shorthand for {@link mergeFeatureStatePaint}. */
47
+ export function mergeHoverPaint<T extends Record<string, unknown>>(
48
+ paint: T,
49
+ hoverPaint: T | undefined,
50
+ ): T {
51
+ return mergeFeatureStatePaint(paint, { hover: hoverPaint });
52
+ }
@@ -0,0 +1,39 @@
1
+ // @vitest-environment node
2
+ //
3
+ // Pins the local Web Mercator formulas to MapLibre's own `MercatorCoordinate`.
4
+ // The engine is imported for real — no mock — which is why this runs in the node
5
+ // environment: `maplibre-gl`'s module body calls `window.URL.createObjectURL` to
6
+ // set up its worker, which jsdom does not implement.
7
+ import MapLibreGL from "maplibre-gl";
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import { createPlanCrs, lngLatFromMercator, mercatorFromLngLat } from "./plan-crs";
11
+
12
+ describe("Mercator parity with MapLibre", () => {
13
+ it("agrees with MercatorCoordinate over a lng/lat grid", () => {
14
+ for (let lng = -180; lng <= 180; lng += 30) {
15
+ for (let lat = -80; lat <= 80; lat += 20) {
16
+ const engine = MapLibreGL.MercatorCoordinate.fromLngLat({ lng, lat });
17
+ const [x, y] = mercatorFromLngLat(lng, lat);
18
+
19
+ expect(x).toBeCloseTo(engine.x, 12);
20
+ expect(y).toBeCloseTo(engine.y, 12);
21
+
22
+ const back = lngLatFromMercator(engine.x, engine.y);
23
+ expect(back[0]).toBeCloseTo(lng, 12);
24
+ expect(back[1]).toBeCloseTo(lat, 12);
25
+ }
26
+ }
27
+ });
28
+
29
+ it("agrees on the plan corners a plan map actually uses", () => {
30
+ const crs = createPlanCrs({ width: 1600, height: 900 });
31
+
32
+ for (const corner of crs.imageCoordinates) {
33
+ const engine = MapLibreGL.MercatorCoordinate.fromLngLat({ lng: corner[0], lat: corner[1] });
34
+
35
+ expect(engine.toLngLat().lng).toBeCloseTo(corner[0], 10);
36
+ expect(engine.toLngLat().lat).toBeCloseTo(corner[1], 10);
37
+ }
38
+ });
39
+ });
@@ -0,0 +1,248 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { createPlanCrs, mercatorFromLngLat, PLAN_MERCATOR_SPAN } from "./plan-crs";
4
+
5
+ /** Deterministic pseudo-random points — never `Math.random` in a test fixture. */
6
+ function seededPoints(count: number, width: number, height: number) {
7
+ let seed = 1337;
8
+ const next = () => {
9
+ seed = (seed * 1664525 + 1013904223) % 4294967296;
10
+ return seed / 4294967296;
11
+ };
12
+ return Array.from({ length: count }, () => ({ x: next() * width, y: next() * height }));
13
+ }
14
+
15
+ const EXTENTS = [
16
+ { width: 1000, height: 1000 },
17
+ { width: 1600, height: 900 },
18
+ { width: 26400, height: 2900 },
19
+ { width: 300, height: 4000 },
20
+ { width: 1, height: 1 },
21
+ ];
22
+
23
+ describe("createPlanCrs", () => {
24
+ it("puts the plan centre on lng/lat [0, 0] and its corners where they belong", () => {
25
+ const crs = createPlanCrs({ width: 1600, height: 900 });
26
+
27
+ // Longitudes are exact; latitudes come through `atan`/`exp`, so they are
28
+ // pinned to 9 decimal places rather than to the last bit.
29
+ expect(crs.toLngLat({ x: 800, y: 450 })).toEqual([0, 0]);
30
+
31
+ const corners = [
32
+ { point: { x: 0, y: 0 }, lng: -90, lat: 45.089035564831 },
33
+ { point: { x: 1600, y: 0 }, lng: 90, lat: 45.089035564831 },
34
+ { point: { x: 1600, y: 900 }, lng: 90, lat: -45.089035564831 },
35
+ { point: { x: 0, y: 900 }, lng: -90, lat: -45.089035564831 },
36
+ ];
37
+ for (const { point, lng, lat } of corners) {
38
+ const [actualLng, actualLat] = crs.toLngLat(point);
39
+ expect(actualLng).toBe(lng);
40
+ expect(actualLat).toBeCloseTo(lat, 9);
41
+ }
42
+ });
43
+
44
+ it("reads plan y downward, like an image and like Leaflet's CRS.Simple", () => {
45
+ const crs = createPlanCrs({ width: 1000, height: 1000 });
46
+ const top = crs.toLngLat({ x: 500, y: 100 });
47
+ const bottom = crs.toLngLat({ x: 500, y: 900 });
48
+
49
+ expect(top[1]).toBeGreaterThan(bottom[1]);
50
+ });
51
+
52
+ it("round-trips every point back to the plan, at every aspect ratio", () => {
53
+ for (const extent of EXTENTS) {
54
+ const crs = createPlanCrs(extent);
55
+ for (const point of seededPoints(100, extent.width, extent.height)) {
56
+ const back = crs.toPlan(crs.toLngLat(point));
57
+ expect(back.x).toBeCloseTo(point.x, 9);
58
+ expect(back.y).toBeCloseTo(point.y, 9);
59
+ }
60
+ }
61
+ });
62
+
63
+ it("accepts a tuple as well as a point, and a LngLat object coming back", () => {
64
+ const crs = createPlanCrs({ width: 1600, height: 900 });
65
+
66
+ expect(crs.toLngLat([400, 300])).toEqual(crs.toLngLat({ x: 400, y: 300 }));
67
+
68
+ const [lng, lat] = crs.toLngLat({ x: 400, y: 300 });
69
+ expect(crs.toPlan({ lng, lat }).x).toBeCloseTo(400, 9);
70
+ });
71
+
72
+ it("preserves the plan's aspect ratio exactly, in Mercator space", () => {
73
+ for (const { width, height } of EXTENTS) {
74
+ const crs = createPlanCrs({ width, height });
75
+ const [x0, y0] = mercatorFromLngLat(...crs.toLngLat({ x: 0, y: 0 }));
76
+ const [x1, y1] = mercatorFromLngLat(...crs.toLngLat({ x: width, y: height }));
77
+
78
+ expect(Math.abs(x1 - x0) / Math.abs(y1 - y0)).toBeCloseTo(width / height, 9);
79
+ }
80
+ });
81
+
82
+ it("would NOT preserve it if the plan were mapped linearly into degrees", () => {
83
+ // The regression test for the design decision: this is what Qlik Sense's
84
+ // "undefined degrees" projection does, and why this module does not.
85
+ const crs = createPlanCrs({ width: 1600, height: 900 });
86
+ const [, north] = crs.toLngLat({ x: 0, y: 0 });
87
+ const [, south] = crs.toLngLat({ x: 0, y: 900 });
88
+ const [west] = crs.toLngLat({ x: 0, y: 0 });
89
+ const [east] = crs.toLngLat({ x: 1600, y: 0 });
90
+
91
+ const degreeAspect = (east - west) / (north - south);
92
+ expect(degreeAspect).not.toBeCloseTo(1600 / 900, 2);
93
+ expect(degreeAspect).toBeCloseTo(1.9960506777882618, 9);
94
+ });
95
+
96
+ it("spans exactly PLAN_MERCATOR_SPAN on the long side, whatever the aspect", () => {
97
+ for (const { width, height } of EXTENTS) {
98
+ const crs = createPlanCrs({ width, height });
99
+ const [x0, y0] = mercatorFromLngLat(...crs.toLngLat({ x: 0, y: 0 }));
100
+ const [x1, y1] = mercatorFromLngLat(...crs.toLngLat({ x: width, y: height }));
101
+
102
+ expect(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0))).toBeCloseTo(PLAN_MERCATOR_SPAN, 9);
103
+ // ...and stays centred on the Mercator origin.
104
+ expect((x0 + x1) / 2).toBeCloseTo(0.5, 9);
105
+ expect((y0 + y1) / 2).toBeCloseTo(0.5, 9);
106
+ }
107
+ });
108
+
109
+ it("stays well inside Mercator's usable latitude range, even for a square plan", () => {
110
+ const crs = createPlanCrs({ width: 1000, height: 1000 });
111
+ const [, north] = crs.toLngLat({ x: 0, y: 0 });
112
+
113
+ expect(north).toBeLessThan(85.051);
114
+ expect(north).toBeCloseTo(66.51326044311186, 6);
115
+ });
116
+
117
+ it("reports bounds as [[west, south], [east, north]]", () => {
118
+ const crs = createPlanCrs({ width: 1600, height: 900 });
119
+ const [[west, south], [east, north]] = crs.bounds;
120
+
121
+ expect(west).toBeLessThan(east);
122
+ expect(south).toBeLessThan(north);
123
+ expect([west, north]).toEqual(crs.toLngLat({ x: 0, y: 0 }));
124
+ expect([east, south]).toEqual(crs.toLngLat({ x: 1600, y: 900 }));
125
+ });
126
+
127
+ it("lists image corners clockwise from the top left", () => {
128
+ const crs = createPlanCrs({ width: 1600, height: 900 });
129
+
130
+ expect(crs.imageCoordinates).toEqual([
131
+ crs.toLngLat({ x: 0, y: 0 }),
132
+ crs.toLngLat({ x: 1600, y: 0 }),
133
+ crs.toLngLat({ x: 1600, y: 900 }),
134
+ crs.toLngLat({ x: 0, y: 900 }),
135
+ ]);
136
+ });
137
+
138
+ it("grows maxBounds strictly outside the plan on all four sides", () => {
139
+ const crs = createPlanCrs({ width: 1600, height: 900 });
140
+ const [[west, south], [east, north]] = crs.bounds;
141
+ const [[padWest, padSouth], [padEast, padNorth]] = crs.maxBounds();
142
+
143
+ expect(padWest).toBeLessThan(west);
144
+ expect(padSouth).toBeLessThan(south);
145
+ expect(padEast).toBeGreaterThan(east);
146
+ expect(padNorth).toBeGreaterThan(north);
147
+ });
148
+
149
+ it("derives zoom limits from the coordinate system, not from the plan's size", () => {
150
+ const small = createPlanCrs({ width: 16, height: 9 });
151
+ const large = createPlanCrs({ width: 26400, height: 2900 });
152
+
153
+ expect(small.zoomForLongSidePixels(256)).toBe(0);
154
+ expect(small.zoomForLongSidePixels(512)).toBe(1);
155
+ expect(small.minZoom).toBe(-1);
156
+ expect(small.maxZoom).toBe(7);
157
+ expect(large.minZoom).toBe(small.minZoom);
158
+ expect(large.maxZoom).toBe(small.maxZoom);
159
+ });
160
+
161
+ it("supports a y-up plan origin for CAD exports", () => {
162
+ const crs = createPlanCrs({ width: 1000, height: 500, origin: "bottom-left" });
163
+ const [, atOrigin] = crs.toLngLat({ x: 0, y: 0 });
164
+ const [, atTop] = crs.toLngLat({ x: 0, y: 500 });
165
+
166
+ expect(atOrigin).toBeLessThan(atTop);
167
+ expect(crs.toPlan(crs.toLngLat({ x: 250, y: 125 })).y).toBeCloseTo(125, 9);
168
+ });
169
+
170
+ it("measures distance in plan units", () => {
171
+ const crs = createPlanCrs({ width: 1000, height: 1000, unit: "mm" });
172
+
173
+ expect(crs.distance({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5);
174
+ expect(crs.extent.unit).toBe("mm");
175
+ });
176
+
177
+ it("rejects an extent that cannot describe a plan", () => {
178
+ expect(() => createPlanCrs({ width: 0, height: 100 })).toThrow(/finite positive/);
179
+ expect(() => createPlanCrs({ width: -1, height: 100 })).toThrow(/finite positive/);
180
+ expect(() => createPlanCrs({ width: Number.NaN, height: 100 })).toThrow(/finite positive/);
181
+ });
182
+ });
183
+
184
+ describe("createPlanCrs().toGeoJSON", () => {
185
+ const crs = createPlanCrs({ width: 1000, height: 1000 });
186
+ const expected = crs.toLngLat({ x: 100, y: 200 });
187
+
188
+ it("converts every geometry type, nested to any depth", () => {
189
+ const converted = crs.toGeoJSON({
190
+ type: "GeometryCollection",
191
+ geometries: [
192
+ { type: "Point", coordinates: [100, 200] },
193
+ { type: "MultiPoint", coordinates: [[100, 200]] },
194
+ { type: "LineString", coordinates: [[100, 200]] },
195
+ { type: "MultiLineString", coordinates: [[[100, 200]]] },
196
+ { type: "Polygon", coordinates: [[[100, 200]]] },
197
+ { type: "MultiPolygon", coordinates: [[[[100, 200]]]] },
198
+ ],
199
+ } as never) as {
200
+ geometries: { coordinates: unknown }[];
201
+ };
202
+
203
+ expect(converted.geometries.map((geometry) => geometry.coordinates)).toEqual([
204
+ expected,
205
+ [expected],
206
+ [expected],
207
+ [[expected]],
208
+ [[expected]],
209
+ [[[expected]]],
210
+ ]);
211
+ });
212
+
213
+ it("keeps feature ids and properties, and drops a plan-unit bbox", () => {
214
+ const converted = crs.toGeoJSON({
215
+ type: "FeatureCollection",
216
+ bbox: [0, 0, 1000, 1000],
217
+ features: [
218
+ {
219
+ type: "Feature",
220
+ id: "room-1",
221
+ bbox: [0, 0, 100, 100],
222
+ properties: { name: "Studio", seats: 8 },
223
+ geometry: { type: "Point", coordinates: [100, 200] },
224
+ },
225
+ ],
226
+ } as never) as {
227
+ bbox?: unknown;
228
+ features: { id: string; bbox?: unknown; properties: unknown; geometry: unknown }[];
229
+ };
230
+
231
+ expect(converted.bbox).toBeUndefined();
232
+ expect(converted.features).toEqual([
233
+ {
234
+ type: "Feature",
235
+ id: "room-1",
236
+ properties: { name: "Studio", seats: 8 },
237
+ geometry: { type: "Point", coordinates: expected },
238
+ },
239
+ ]);
240
+ });
241
+
242
+ it("leaves the caller's data untouched", () => {
243
+ const input = { type: "Point", coordinates: [100, 200] };
244
+ crs.toGeoJSON(input as never);
245
+
246
+ expect(input.coordinates).toEqual([100, 200]);
247
+ });
248
+ });