@mapmap/maps 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1827 @@
1
+ import maplibregl from 'maplibre-gl';
2
+ import { Protocol } from 'pmtiles';
3
+
4
+ // src/map.ts
5
+
6
+ // src/logo.ts
7
+ var LOGO_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="84" height="20" viewBox="0 0 156 37" fill="none" role="img" aria-label="MapMap"><g stroke="#ffffff" stroke-linejoin="round" stroke-linecap="round"><path d="M146.965 19.2363C146.965 17.5281 146.64 16.2469 145.989 15.3927C145.358 14.5386 144.464 14.1115 143.304 14.1115C142.227 14.1115 141.352 14.5589 140.681 15.4537C140.03 16.3282 139.705 17.5382 139.705 19.0838V19.3889C139.705 20.9751 140.03 22.2055 140.681 23.08C141.352 23.9341 142.227 24.3612 143.304 24.3612C144.382 24.3612 145.257 23.924 145.928 23.0495C146.619 22.1547 146.965 20.8836 146.965 19.2363ZM150.778 19.2363C150.778 21.8598 150.188 23.8934 149.009 25.3373C147.85 26.7609 146.324 27.4727 144.433 27.4727C142.481 27.4727 140.925 26.7304 139.766 25.2458H139.705V33.8787H135.739V11.305H139.461L139.522 13.5319H139.583C140.803 11.844 142.42 11 144.433 11C146.406 11 147.951 11.7016 149.07 13.1048C150.209 14.5081 150.778 16.5519 150.778 19.2363Z" fill="#ffffff" stroke-width="3" paint-order="stroke"/><path d="M126.399 11C128.819 11 130.558 11.4779 131.615 12.4337C132.673 13.3692 133.202 14.8945 133.202 17.0095V22.8969C133.202 24.3409 133.354 25.7644 133.659 27.1676H129.907C129.785 26.4965 129.694 25.7136 129.633 24.8188H129.572C129.002 25.6526 128.229 26.3033 127.253 26.7711C126.297 27.2388 125.25 27.4727 124.111 27.4727C122.627 27.4727 121.437 27.066 120.542 26.2525C119.668 25.4187 119.23 24.2697 119.23 22.8054C119.23 21.0361 119.973 19.6227 121.457 18.5652C122.962 17.4874 125.118 16.9485 127.924 16.9485H129.236V16.857C129.236 15.7791 129.023 15.0267 128.596 14.5996C128.168 14.1725 127.406 13.959 126.308 13.959C124.315 13.959 122.433 14.4166 120.664 15.3317L120.085 12.3727C122.017 11.4576 124.121 11 126.399 11ZM122.952 22.5004C122.952 23.1512 123.145 23.6596 123.532 24.0256C123.918 24.3917 124.447 24.5747 125.118 24.5747C126.277 24.5747 127.253 24.1985 128.046 23.446C128.84 22.6936 129.236 21.7479 129.236 20.6091V19.5719H127.924C126.318 19.5719 125.087 19.8464 124.233 20.3955C123.379 20.9243 122.952 21.6259 122.952 22.5004Z" fill="#ffffff" stroke-width="3" paint-order="stroke"/><path d="M96.1152 11.305H99.7758L99.8673 13.4404H99.9284C101.108 11.8135 102.46 11 103.986 11C104.982 11 105.806 11.2034 106.456 11.6101C107.107 12.0168 107.666 12.6778 108.134 13.5929H108.195C109.517 11.8643 111.073 11 112.862 11C114.571 11 115.811 11.4779 116.584 12.4337C117.377 13.3692 117.774 14.9555 117.774 17.1925V27.1676H113.93V18.1077C113.93 16.5214 113.757 15.4537 113.412 14.9046C113.086 14.3555 112.496 14.081 111.642 14.081C110.91 14.081 110.27 14.4674 109.72 15.2402C109.171 16.013 108.897 16.9688 108.897 18.1077V27.1676H105.053V18.1077C105.053 16.5214 104.88 15.4537 104.535 14.9046C104.209 14.3555 103.619 14.081 102.765 14.081C102.033 14.081 101.393 14.4674 100.844 15.2402C100.294 16.013 100.02 16.9688 100.02 18.1077V27.1676H96.1152V11.305Z" fill="#ffffff" stroke-width="3" paint-order="stroke"/><path d="M90.8503 19.2363C90.8503 17.5281 90.525 16.2469 89.8742 15.3927C89.2438 14.5386 88.3489 14.1115 87.1898 14.1115C86.1119 14.1115 85.2374 14.5589 84.5663 15.4537C83.9155 16.3282 83.5902 17.5382 83.5902 19.0838V19.3889C83.5902 20.9751 83.9155 22.2055 84.5663 23.08C85.2374 23.9341 86.1119 24.3612 87.1898 24.3612C88.2676 24.3612 89.1421 23.924 89.8132 23.0495C90.5046 22.1547 90.8503 20.8836 90.8503 19.2363ZM94.6635 19.2363C94.6635 21.8598 94.0737 23.8934 92.8942 25.3373C91.735 26.7609 90.2097 27.4727 88.3184 27.4727C86.3661 27.4727 84.8104 26.7304 83.6512 25.2458H83.5902V33.8787H79.6245V11.305H83.3461L83.4071 13.5319H83.4681C84.6883 11.844 86.3051 11 88.3184 11C90.2911 11 91.8367 11.7016 92.9552 13.1048C94.094 14.5081 94.6635 16.5519 94.6635 19.2363Z" fill="#ffffff" stroke-width="3" paint-order="stroke"/><path d="M70.2844 11C72.7045 11 74.4432 11.4779 75.5008 12.4337C76.5583 13.3692 77.087 14.8945 77.087 17.0095V22.8969C77.087 24.3409 77.2395 25.7644 77.5446 27.1676H73.7925C73.6705 26.4965 73.5789 25.7136 73.5179 24.8188H73.4569C72.8875 25.6526 72.1147 26.3033 71.1385 26.7711C70.1827 27.2388 69.1354 27.4727 67.9965 27.4727C66.5119 27.4727 65.3223 27.066 64.4274 26.2525C63.553 25.4187 63.1157 24.2697 63.1157 22.8054C63.1157 21.0361 63.858 19.6227 65.3426 18.5652C66.8475 17.4874 69.0032 16.9485 71.8096 16.9485H73.1214V16.857C73.1214 15.7791 72.9078 15.0267 72.4808 14.5996C72.0537 14.1725 71.2911 13.959 70.1929 13.959C68.1999 13.959 66.3187 14.4166 64.5495 15.3317L63.9699 12.3727C65.9018 11.4576 68.0067 11 70.2844 11ZM66.8373 22.5004C66.8373 23.1512 67.0305 23.6596 67.4169 24.0256C67.8033 24.3917 68.3321 24.5747 69.0032 24.5747C70.1624 24.5747 71.1385 24.1985 71.9317 23.446C72.7248 22.6936 73.1214 21.7479 73.1214 20.6091V19.5719H71.8096C70.203 19.5719 68.9727 19.8464 68.1185 20.3955C67.2644 20.9243 66.8373 21.6259 66.8373 22.5004Z" fill="#ffffff" stroke-width="3" paint-order="stroke"/><path d="M40 11.3049H43.6606L43.7521 13.4403H43.8131C44.9927 11.8133 46.345 10.9999 47.8703 10.9999C48.8668 10.9999 49.6904 11.2032 50.3412 11.61C50.992 12.0167 51.5512 12.6777 52.019 13.5928H52.08C53.4019 11.8642 54.9576 10.9999 56.7472 10.9999C58.4555 10.9999 59.6961 11.4778 60.4689 12.4336C61.262 13.3691 61.6586 14.9554 61.6586 17.1924V27.1675H57.8149V18.1075C57.8149 16.5213 57.6421 15.4536 57.2963 14.9045C56.9709 14.3554 56.3812 14.0809 55.527 14.0809C54.7949 14.0809 54.1543 14.4673 53.6052 15.2401C53.0561 16.0129 52.7816 16.9687 52.7816 18.1075V27.1675H48.938V18.1075C48.938 16.5213 48.7651 15.4536 48.4194 14.9045C48.094 14.3554 47.5042 14.0809 46.6501 14.0809C45.918 14.0809 45.2774 14.4673 44.7283 15.2401C44.1792 16.0129 43.9046 16.9687 43.9046 18.1075V27.1675H40V11.3049Z" fill="#ffffff" stroke-width="3" paint-order="stroke"/><path d="M27.3847 25.9178C28.8081 23.5868 30.5287 21.3029 30.9562 18.5589C31.9911 11.9172 26.9381 5.71447 19.956 5.30338C17.4904 5.15838 13.6449 5.12507 11.1992 5.30299C10.0552 5.38607 8.94122 5.89121 8.16887 6.70868C7.71694 7.18718 5.28183 11.1346 5.20651 11.6441C5.02777 12.8495 5.83228 13.8426 7.09035 13.9645C9.18143 14.1667 11.8763 13.9253 14.0309 13.9264C15.8964 13.9272 17.9989 13.8097 19.8339 13.9253C25.9341 14.3093 29.7128 20.5446 27.3249 25.9319L27.3847 25.9178Z" stroke-width="4.96"/><path d="M14.0302 13.9258L10.6029 19.6599C9.96327 20.8096 10.6737 22.3059 12.0613 22.4638C14.1002 22.6962 17.0133 22.4493 19.1606 22.4689C21.213 22.4877 23.197 22.146 24.8203 23.6246C26.1443 24.8308 26.5555 26.7091 25.9192 28.3472L27.3849 25.9175C29.7728 20.5302 25.9338 14.3083 19.8336 13.9246C17.9986 13.809 15.8961 13.927 14.0306 13.9258H14.0302Z" stroke-width="4.96"/><path d="M19.1603 22.469L15.6471 28.3947C15.2098 29.3278 15.7183 30.5509 16.7183 30.9134C17.4381 31.174 21.8747 31.1148 22.7354 30.9463C24.1038 30.6786 25.4299 29.6056 25.9188 28.3477C26.5548 26.7096 26.1436 24.8313 24.82 23.6251C23.1967 22.1461 21.1733 22.5542 19.1209 22.5354L19.1603 22.469Z" stroke-width="4.96"/></g><path d="M146.965 19.2363C146.965 17.5281 146.64 16.2469 145.989 15.3927C145.358 14.5386 144.464 14.1115 143.304 14.1115C142.227 14.1115 141.352 14.5589 140.681 15.4537C140.03 16.3282 139.705 17.5382 139.705 19.0838V19.3889C139.705 20.9751 140.03 22.2055 140.681 23.08C141.352 23.9341 142.227 24.3612 143.304 24.3612C144.382 24.3612 145.257 23.924 145.928 23.0495C146.619 22.1547 146.965 20.8836 146.965 19.2363ZM150.778 19.2363C150.778 21.8598 150.188 23.8934 149.009 25.3373C147.85 26.7609 146.324 27.4727 144.433 27.4727C142.481 27.4727 140.925 26.7304 139.766 25.2458H139.705V33.8787H135.739V11.305H139.461L139.522 13.5319H139.583C140.803 11.844 142.42 11 144.433 11C146.406 11 147.951 11.7016 149.07 13.1048C150.209 14.5081 150.778 16.5519 150.778 19.2363Z" fill="#26282A"/><path d="M126.399 11C128.819 11 130.558 11.4779 131.615 12.4337C132.673 13.3692 133.202 14.8945 133.202 17.0095V22.8969C133.202 24.3409 133.354 25.7644 133.659 27.1676H129.907C129.785 26.4965 129.694 25.7136 129.633 24.8188H129.572C129.002 25.6526 128.229 26.3033 127.253 26.7711C126.297 27.2388 125.25 27.4727 124.111 27.4727C122.627 27.4727 121.437 27.066 120.542 26.2525C119.668 25.4187 119.23 24.2697 119.23 22.8054C119.23 21.0361 119.973 19.6227 121.457 18.5652C122.962 17.4874 125.118 16.9485 127.924 16.9485H129.236V16.857C129.236 15.7791 129.023 15.0267 128.596 14.5996C128.168 14.1725 127.406 13.959 126.308 13.959C124.315 13.959 122.433 14.4166 120.664 15.3317L120.085 12.3727C122.017 11.4576 124.121 11 126.399 11ZM122.952 22.5004C122.952 23.1512 123.145 23.6596 123.532 24.0256C123.918 24.3917 124.447 24.5747 125.118 24.5747C126.277 24.5747 127.253 24.1985 128.046 23.446C128.84 22.6936 129.236 21.7479 129.236 20.6091V19.5719H127.924C126.318 19.5719 125.087 19.8464 124.233 20.3955C123.379 20.9243 122.952 21.6259 122.952 22.5004Z" fill="#26282A"/><path d="M96.1152 11.305H99.7758L99.8673 13.4404H99.9284C101.108 11.8135 102.46 11 103.986 11C104.982 11 105.806 11.2034 106.456 11.6101C107.107 12.0168 107.666 12.6778 108.134 13.5929H108.195C109.517 11.8643 111.073 11 112.862 11C114.571 11 115.811 11.4779 116.584 12.4337C117.377 13.3692 117.774 14.9555 117.774 17.1925V27.1676H113.93V18.1077C113.93 16.5214 113.757 15.4537 113.412 14.9046C113.086 14.3555 112.496 14.081 111.642 14.081C110.91 14.081 110.27 14.4674 109.72 15.2402C109.171 16.013 108.897 16.9688 108.897 18.1077V27.1676H105.053V18.1077C105.053 16.5214 104.88 15.4537 104.535 14.9046C104.209 14.3555 103.619 14.081 102.765 14.081C102.033 14.081 101.393 14.4674 100.844 15.2402C100.294 16.013 100.02 16.9688 100.02 18.1077V27.1676H96.1152V11.305Z" fill="#26282A"/><path d="M90.8503 19.2363C90.8503 17.5281 90.525 16.2469 89.8742 15.3927C89.2438 14.5386 88.3489 14.1115 87.1898 14.1115C86.1119 14.1115 85.2374 14.5589 84.5663 15.4537C83.9155 16.3282 83.5902 17.5382 83.5902 19.0838V19.3889C83.5902 20.9751 83.9155 22.2055 84.5663 23.08C85.2374 23.9341 86.1119 24.3612 87.1898 24.3612C88.2676 24.3612 89.1421 23.924 89.8132 23.0495C90.5046 22.1547 90.8503 20.8836 90.8503 19.2363ZM94.6635 19.2363C94.6635 21.8598 94.0737 23.8934 92.8942 25.3373C91.735 26.7609 90.2097 27.4727 88.3184 27.4727C86.3661 27.4727 84.8104 26.7304 83.6512 25.2458H83.5902V33.8787H79.6245V11.305H83.3461L83.4071 13.5319H83.4681C84.6883 11.844 86.3051 11 88.3184 11C90.2911 11 91.8367 11.7016 92.9552 13.1048C94.094 14.5081 94.6635 16.5519 94.6635 19.2363Z" fill="#26282A"/><path d="M70.2844 11C72.7045 11 74.4432 11.4779 75.5008 12.4337C76.5583 13.3692 77.087 14.8945 77.087 17.0095V22.8969C77.087 24.3409 77.2395 25.7644 77.5446 27.1676H73.7925C73.6705 26.4965 73.5789 25.7136 73.5179 24.8188H73.4569C72.8875 25.6526 72.1147 26.3033 71.1385 26.7711C70.1827 27.2388 69.1354 27.4727 67.9965 27.4727C66.5119 27.4727 65.3223 27.066 64.4274 26.2525C63.553 25.4187 63.1157 24.2697 63.1157 22.8054C63.1157 21.0361 63.858 19.6227 65.3426 18.5652C66.8475 17.4874 69.0032 16.9485 71.8096 16.9485H73.1214V16.857C73.1214 15.7791 72.9078 15.0267 72.4808 14.5996C72.0537 14.1725 71.2911 13.959 70.1929 13.959C68.1999 13.959 66.3187 14.4166 64.5495 15.3317L63.9699 12.3727C65.9018 11.4576 68.0067 11 70.2844 11ZM66.8373 22.5004C66.8373 23.1512 67.0305 23.6596 67.4169 24.0256C67.8033 24.3917 68.3321 24.5747 69.0032 24.5747C70.1624 24.5747 71.1385 24.1985 71.9317 23.446C72.7248 22.6936 73.1214 21.7479 73.1214 20.6091V19.5719H71.8096C70.203 19.5719 68.9727 19.8464 68.1185 20.3955C67.2644 20.9243 66.8373 21.6259 66.8373 22.5004Z" fill="#26282A"/><path d="M40 11.3049H43.6606L43.7521 13.4403H43.8131C44.9927 11.8133 46.345 10.9999 47.8703 10.9999C48.8668 10.9999 49.6904 11.2032 50.3412 11.61C50.992 12.0167 51.5512 12.6777 52.019 13.5928H52.08C53.4019 11.8642 54.9576 10.9999 56.7472 10.9999C58.4555 10.9999 59.6961 11.4778 60.4689 12.4336C61.262 13.3691 61.6586 14.9554 61.6586 17.1924V27.1675H57.8149V18.1075C57.8149 16.5213 57.6421 15.4536 57.2963 14.9045C56.9709 14.3554 56.3812 14.0809 55.527 14.0809C54.7949 14.0809 54.1543 14.4673 53.6052 15.2401C53.0561 16.0129 52.7816 16.9687 52.7816 18.1075V27.1675H48.938V18.1075C48.938 16.5213 48.7651 15.4536 48.4194 14.9045C48.094 14.3554 47.5042 14.0809 46.6501 14.0809C45.918 14.0809 45.2774 14.4673 44.7283 15.2401C44.1792 16.0129 43.9046 16.9687 43.9046 18.1075V27.1675H40V11.3049Z" fill="#26282A"/><path d="M27.3847 25.9178C28.8081 23.5868 30.5287 21.3029 30.9562 18.5589C31.9911 11.9172 26.9381 5.71447 19.956 5.30338C17.4904 5.15838 13.6449 5.12507 11.1992 5.30299C10.0552 5.38607 8.94122 5.89121 8.16887 6.70868C7.71694 7.18718 5.28183 11.1346 5.20651 11.6441C5.02777 12.8495 5.83228 13.8426 7.09035 13.9645C9.18143 14.1667 11.8763 13.9253 14.0309 13.9264C15.8964 13.9272 17.9989 13.8097 19.8339 13.9253C25.9341 14.3093 29.7128 20.5446 27.3249 25.9319L27.3847 25.9178Z" stroke="#3A3838" stroke-width="1.95943" stroke-miterlimit="10"/><path d="M14.0302 13.9258L10.6029 19.6599C9.96327 20.8096 10.6737 22.3059 12.0613 22.4638C14.1002 22.6962 17.0133 22.4493 19.1606 22.4689C21.213 22.4877 23.197 22.146 24.8203 23.6246C26.1443 24.8308 26.5555 26.7091 25.9192 28.3472L27.3849 25.9175C29.7728 20.5302 25.9338 14.3083 19.8336 13.9246C17.9986 13.809 15.8961 13.927 14.0306 13.9258H14.0302Z" stroke="#3A3838" stroke-width="1.95943" stroke-miterlimit="10"/><path d="M19.1603 22.469L15.6471 28.3947C15.2098 29.3278 15.7183 30.5509 16.7183 30.9134C17.4381 31.174 21.8747 31.1148 22.7354 30.9463C24.1038 30.6786 25.4299 29.6056 25.9188 28.3477C26.5548 26.7096 26.1436 24.8313 24.82 23.6251C23.1967 22.1461 21.1733 22.5542 19.1209 22.5354L19.1603 22.469Z" stroke="#3A3838" stroke-width="1.95943" stroke-miterlimit="10"/></svg>';
8
+ var DEFAULT_HREF = "https://mapmap.ai";
9
+ var LogoControl = class {
10
+ constructor(options = {}) {
11
+ this.href = options.href ?? DEFAULT_HREF;
12
+ }
13
+ onAdd() {
14
+ const doc = globalThis.document;
15
+ if (!doc) throw new Error("LogoControl requires a DOM environment");
16
+ const link = doc.createElement("a");
17
+ link.className = "maplibregl-ctrl mapmap-logo";
18
+ link.href = this.href;
19
+ link.target = "_blank";
20
+ link.rel = "noopener";
21
+ link.setAttribute("aria-label", "MapMap");
22
+ link.style.cssText = "display:block;line-height:0;margin:0 8px 4px 8px;opacity:0.9;pointer-events:auto";
23
+ const svg = new DOMParser().parseFromString(LOGO_SVG, "image/svg+xml").documentElement;
24
+ link.appendChild(doc.importNode(svg, true));
25
+ this.container = link;
26
+ return link;
27
+ }
28
+ onRemove() {
29
+ this.container?.remove();
30
+ this.container = void 0;
31
+ }
32
+ };
33
+
34
+ // src/nav-design.ts
35
+ var NAV_CAMERA_DEFAULTS = {
36
+ pitch: 60,
37
+ zoom: 17.5,
38
+ speedMps: 12
39
+ };
40
+ function defaultNavDesign() {
41
+ return {
42
+ version: 1,
43
+ route: { color: "#3a86ff", width: 4.5, opacity: 1, casingColor: "#1f438a" },
44
+ puck: { color: "#1f6fff", size: 14, headingArrow: true },
45
+ banner: {
46
+ background: "#101418",
47
+ textColor: "#ffffff",
48
+ fontSize: 16,
49
+ showLanes: true,
50
+ padding: 10,
51
+ cornerRadius: 10,
52
+ maxWidth: 340
53
+ }
54
+ };
55
+ }
56
+ function isRecord(v) {
57
+ return typeof v === "object" && v !== null && !Array.isArray(v);
58
+ }
59
+ function str(v, fallback) {
60
+ return typeof v === "string" && v.trim() !== "" ? v : fallback;
61
+ }
62
+ function num(v, fallback, min, max) {
63
+ return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : fallback;
64
+ }
65
+ function optNum(v, min, max) {
66
+ return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : void 0;
67
+ }
68
+ function bool(v, fallback) {
69
+ return typeof v === "boolean" ? v : fallback;
70
+ }
71
+ function puckImageUrl(v) {
72
+ if (typeof v !== "string") return void 0;
73
+ const t = v.trim();
74
+ return /^(https:|data:)/i.test(t) ? t : void 0;
75
+ }
76
+ function parseNavDesign(value) {
77
+ const d = defaultNavDesign();
78
+ if (!isRecord(value)) return d;
79
+ const route = isRecord(value.route) ? value.route : {};
80
+ const puck = isRecord(value.puck) ? value.puck : {};
81
+ const banner = isRecord(value.banner) ? value.banner : {};
82
+ return {
83
+ version: 1,
84
+ route: {
85
+ color: str(route.color, d.route.color),
86
+ width: num(route.width, d.route.width, 0.5, 20),
87
+ opacity: num(route.opacity, d.route.opacity, 0, 1),
88
+ casingColor: str(route.casingColor, d.route.casingColor)
89
+ },
90
+ puck: {
91
+ color: str(puck.color, d.puck.color),
92
+ size: num(puck.size, d.puck.size, 6, 40),
93
+ headingArrow: bool(puck.headingArrow, d.puck.headingArrow),
94
+ imageUrl: puckImageUrl(puck.imageUrl)
95
+ },
96
+ banner: {
97
+ background: str(banner.background, d.banner.background),
98
+ textColor: str(banner.textColor, d.banner.textColor),
99
+ fontSize: num(banner.fontSize, d.banner.fontSize, 10, 32),
100
+ showLanes: bool(banner.showLanes, d.banner.showLanes),
101
+ padding: num(banner.padding, d.banner.padding ?? 10, 6, 24),
102
+ cornerRadius: num(banner.cornerRadius, d.banner.cornerRadius ?? 10, 0, 24),
103
+ maxWidth: num(banner.maxWidth, d.banner.maxWidth ?? 340, 200, 520),
104
+ height: optNum(banner.height, 40, 120)
105
+ },
106
+ // Camera is optional since v1: absent (or malformed) stays undefined so
107
+ // old themes round-trip byte-identical; a present block clamps per
108
+ // field, each absent field meaning "use the default".
109
+ ...isRecord(value.camera) ? {
110
+ camera: {
111
+ pitch: optNum(value.camera.pitch, 0, 85),
112
+ zoom: optNum(value.camera.zoom, 14, 20),
113
+ speedMps: optNum(value.camera.speedMps, 2, 40)
114
+ }
115
+ } : {}
116
+ };
117
+ }
118
+ function navDesignFromTheme(theme) {
119
+ const extra = theme?.extra;
120
+ if (typeof extra !== "object" || extra === null || !("nav" in extra)) {
121
+ return void 0;
122
+ }
123
+ return parseNavDesign(extra.nav);
124
+ }
125
+ async function navDesignFromThemeUrl(url, fetchImpl) {
126
+ const doFetch = fetchImpl ?? ((input) => globalThis.fetch(input));
127
+ const response = await doFetch(url);
128
+ if (!response.ok) {
129
+ throw new Error(`fetching theme ${url}: HTTP ${response.status}`);
130
+ }
131
+ const theme = await response.json();
132
+ return navDesignFromTheme(theme);
133
+ }
134
+
135
+ // src/style.ts
136
+ var OSM_ATTRIBUTION = "\xA9 OpenStreetMap contributors";
137
+ var OPENMAPTILES_ATTRIBUTION = "\xA9 OpenMapTiles";
138
+ var FULL_ATTRIBUTION = "\xA9 OpenStreetMap contributors \xA9 OpenMapTiles";
139
+ var DEFAULT_GLYPHS_URL = "https://fonts.mapmap.ai/{fontstack}/{range}.pbf";
140
+ var DEFAULT_TERRITORY_TILES_URL = "pmtiles://https://tiles.mapmap.ai/territories/planet.pmtiles";
141
+ var SOURCE_LAYERS = [
142
+ "aerodrome_label",
143
+ "aeroway",
144
+ "boundary",
145
+ "building",
146
+ "housenumber",
147
+ "landcover",
148
+ "landuse",
149
+ "mountain_peak",
150
+ "park",
151
+ "place",
152
+ "poi",
153
+ "transportation",
154
+ "transportation_name",
155
+ "water",
156
+ "water_name",
157
+ "waterway"
158
+ ];
159
+ var PALETTE_SLOTS = [
160
+ ["background", "#f4f2ee", "#12161c"],
161
+ ["water", "#a8c8e8", "#1b2c40"],
162
+ ["waterway", "#a8c8e8", "#1b2c40"],
163
+ ["landcover", "#e3e8dd", "#182029"],
164
+ ["landuse", "#ece8e1", "#171e26"],
165
+ ["park", "#cfe4c8", "#1a2a1f"],
166
+ ["building", "#e2ddd4", "#20272f"],
167
+ ["aeroway", "#dcd9d2", "#232b34"],
168
+ ["road", "#ffffff", "#2b333d"],
169
+ ["roadMajor", "#f6d9a0", "#4a5461"],
170
+ ["path", "#d9d4ca", "#2a323c"],
171
+ ["rail", "#d0cbc2", "#262e37"],
172
+ ["boundary", "#b9a6c9", "#4a3d57"],
173
+ ["boundaryMinor", "#c9bcd6", "#3a3247"],
174
+ ["textPrimary", "#3a3a3a", "#d6dbe1"],
175
+ ["textSecondary", "#6b6b6b", "#9aa3ad"],
176
+ ["textHalo", "#ffffff", "#12161c"]
177
+ ];
178
+ function toPmtilesUrl(url) {
179
+ return url.startsWith("pmtiles://") ? url : `pmtiles://${url}`;
180
+ }
181
+ function isCssColour(value) {
182
+ const v = value.trim();
183
+ if (v.startsWith("#")) {
184
+ const hex = v.slice(1);
185
+ return [3, 4, 6, 8].includes(hex.length) && /^[0-9a-fA-F]+$/.test(hex);
186
+ }
187
+ for (const prefix of ["rgb(", "rgba(", "hsl(", "hsla("]) {
188
+ if (v.startsWith(prefix) && v.endsWith(")")) return true;
189
+ }
190
+ return false;
191
+ }
192
+ function resolvePalette(theme) {
193
+ const base = theme.base ?? "light";
194
+ const palette = {};
195
+ const knownSlots = new Set(PALETTE_SLOTS.map(([slot]) => slot));
196
+ for (const [slot, light, dark] of PALETTE_SLOTS) {
197
+ palette[slot] = base === "dark" ? dark : light;
198
+ }
199
+ for (const [slot, value] of Object.entries(theme.palette ?? {})) {
200
+ if (!knownSlots.has(slot)) {
201
+ const accepted = PALETTE_SLOTS.map(([s]) => s).join(", ");
202
+ throw new Error(
203
+ `unknown palette slot ${JSON.stringify(slot)}; accepted slots: ${accepted}`
204
+ );
205
+ }
206
+ if (!isCssColour(value)) {
207
+ throw new Error(
208
+ `palette slot ${JSON.stringify(slot)} has invalid colour ${JSON.stringify(value)}; use #rgb/#rrggbb/#rrggbbaa or rgb()/rgba()/hsl()/hsla()`
209
+ );
210
+ }
211
+ palette[slot] = value;
212
+ }
213
+ return palette;
214
+ }
215
+ function fill(id, sourceLayer, colour, opacity, minzoom) {
216
+ const layer = {
217
+ id,
218
+ type: "fill",
219
+ source: "territory",
220
+ "source-layer": sourceLayer,
221
+ paint: { "fill-color": colour, "fill-opacity": opacity }
222
+ };
223
+ if (minzoom !== void 0) layer["minzoom"] = minzoom;
224
+ return layer;
225
+ }
226
+ function symbol(id, sourceLayer, textField, font, textSize, colour, halo, minzoom, extraLayout) {
227
+ const layout = {
228
+ "text-field": textField,
229
+ "text-font": font,
230
+ "text-size": textSize,
231
+ ...extraLayout
232
+ };
233
+ const layer = {
234
+ id,
235
+ type: "symbol",
236
+ source: "territory",
237
+ "source-layer": sourceLayer,
238
+ layout,
239
+ paint: {
240
+ "text-color": colour,
241
+ "text-halo-color": halo,
242
+ "text-halo-width": 1.2
243
+ }
244
+ };
245
+ if (minzoom !== void 0) layer["minzoom"] = minzoom;
246
+ return layer;
247
+ }
248
+ function applyOverride(layer, layerOverride) {
249
+ if (layerOverride.paint) mergeObject(layer, "paint", layerOverride.paint);
250
+ if (layerOverride.layout) mergeObject(layer, "layout", layerOverride.layout);
251
+ if (layerOverride.filter !== void 0) layer["filter"] = layerOverride.filter;
252
+ if (layerOverride.minzoom !== void 0) layer["minzoom"] = layerOverride.minzoom;
253
+ if (layerOverride.maxzoom !== void 0) layer["maxzoom"] = layerOverride.maxzoom;
254
+ if (layerOverride.visible === false) {
255
+ const layout = layer["layout"] ??= {};
256
+ layout["visibility"] = "none";
257
+ }
258
+ }
259
+ function mergeObject(layer, key, overrides) {
260
+ const target = layer[key] ??= {};
261
+ for (const [k, v] of Object.entries(overrides)) target[k] = v;
262
+ }
263
+ function validateExtraLayer(index, layer) {
264
+ const invalid = (reason) => new Error(`extra layer ${index} is invalid: ${reason}`);
265
+ if (typeof layer !== "object" || layer === null || Array.isArray(layer)) {
266
+ throw invalid("must be an object");
267
+ }
268
+ const obj = layer;
269
+ const id = obj["id"];
270
+ if (typeof id !== "string") throw invalid('must have a string "id"');
271
+ if (id.length === 0) throw invalid('"id" must be non-empty');
272
+ if (obj["type"] !== "background") {
273
+ if (obj["source"] !== "territory") {
274
+ throw invalid('"source" must be "territory"');
275
+ }
276
+ const sourceLayer = obj["source-layer"];
277
+ if (typeof sourceLayer !== "string") {
278
+ throw invalid('must have a string "source-layer"');
279
+ }
280
+ if (!SOURCE_LAYERS.includes(sourceLayer)) {
281
+ throw invalid(
282
+ `unknown source-layer ${JSON.stringify(sourceLayer)}; accepted: ` + SOURCE_LAYERS.join(", ")
283
+ );
284
+ }
285
+ }
286
+ return obj;
287
+ }
288
+ var THEME_KEYS = [
289
+ "name",
290
+ "base",
291
+ "palette",
292
+ "layers",
293
+ "fonts",
294
+ "glyphs",
295
+ "sprite",
296
+ "extra_layers",
297
+ "extra",
298
+ "buildings_3d"
299
+ ];
300
+ var BUILDINGS_3D_MINZOOM = 15;
301
+ function buildings3dLayer(colour) {
302
+ return {
303
+ id: "building-3d",
304
+ type: "fill-extrusion",
305
+ source: "territory",
306
+ "source-layer": "building",
307
+ minzoom: BUILDINGS_3D_MINZOOM,
308
+ paint: {
309
+ "fill-extrusion-color": colour,
310
+ "fill-extrusion-height": [
311
+ "interpolate",
312
+ ["linear"],
313
+ ["zoom"],
314
+ BUILDINGS_3D_MINZOOM,
315
+ 0,
316
+ BUILDINGS_3D_MINZOOM + 0.5,
317
+ ["coalesce", ["get", "render_height"], 5]
318
+ ],
319
+ "fill-extrusion-base": [
320
+ "interpolate",
321
+ ["linear"],
322
+ ["zoom"],
323
+ BUILDINGS_3D_MINZOOM,
324
+ 0,
325
+ BUILDINGS_3D_MINZOOM + 0.5,
326
+ ["coalesce", ["get", "render_min_height"], 0]
327
+ ],
328
+ "fill-extrusion-opacity": 0.85,
329
+ "fill-extrusion-vertical-gradient": true
330
+ }
331
+ };
332
+ }
333
+ function buildStyle(options = {}) {
334
+ const input = options.theme ?? "light";
335
+ if (typeof input === "object") {
336
+ for (const key of Object.keys(input)) {
337
+ if (!THEME_KEYS.includes(key)) {
338
+ throw new Error(
339
+ `unknown theme field ${JSON.stringify(key)}; accepted fields: ` + THEME_KEYS.join(", ")
340
+ );
341
+ }
342
+ }
343
+ }
344
+ const theme = typeof input === "string" ? { name: input, base: input } : input;
345
+ const palette = resolvePalette(theme);
346
+ const p = (slot) => palette[slot];
347
+ const font = [theme.fonts?.regular ?? "Noto Sans Regular"];
348
+ const nameField = ["coalesce", ["get", "name:en"], ["get", "name"]];
349
+ const tilesUrl = toPmtilesUrl(
350
+ options.territoryTilesUrl ?? DEFAULT_TERRITORY_TILES_URL
351
+ );
352
+ const layers = [
353
+ {
354
+ id: "background",
355
+ type: "background",
356
+ paint: { "background-color": p("background") }
357
+ },
358
+ fill("landcover", "landcover", p("landcover"), 0.6),
359
+ fill("landuse", "landuse", p("landuse"), 0.45),
360
+ fill("park", "park", p("park"), 0.7),
361
+ fill("water", "water", p("water"), 1),
362
+ {
363
+ id: "waterway",
364
+ type: "line",
365
+ source: "territory",
366
+ "source-layer": "waterway",
367
+ paint: {
368
+ "line-color": p("waterway"),
369
+ "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 16, 3]
370
+ }
371
+ },
372
+ {
373
+ id: "aeroway",
374
+ type: "line",
375
+ source: "territory",
376
+ "source-layer": "aeroway",
377
+ minzoom: 10,
378
+ filter: ["in", "class", "runway", "taxiway"],
379
+ paint: {
380
+ "line-color": p("aeroway"),
381
+ "line-width": ["interpolate", ["linear"], ["zoom"], 10, 1, 16, 6]
382
+ }
383
+ },
384
+ fill("building", "building", p("building"), 0.9, 13),
385
+ {
386
+ id: "rail",
387
+ type: "line",
388
+ source: "territory",
389
+ "source-layer": "transportation",
390
+ minzoom: 8,
391
+ filter: ["in", "class", "rail", "transit"],
392
+ paint: {
393
+ "line-color": p("rail"),
394
+ "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.4, 16, 2]
395
+ }
396
+ },
397
+ {
398
+ id: "road-path",
399
+ type: "line",
400
+ source: "territory",
401
+ "source-layer": "transportation",
402
+ minzoom: 13,
403
+ filter: ["in", "class", "path", "track", "pedestrian"],
404
+ paint: {
405
+ "line-color": p("path"),
406
+ "line-width": ["interpolate", ["linear"], ["zoom"], 13, 0.5, 16, 2],
407
+ "line-dasharray": [2, 1]
408
+ }
409
+ },
410
+ {
411
+ id: "road-minor",
412
+ type: "line",
413
+ source: "territory",
414
+ "source-layer": "transportation",
415
+ filter: [
416
+ "!in",
417
+ "class",
418
+ "motorway",
419
+ "trunk",
420
+ "primary",
421
+ "rail",
422
+ "transit",
423
+ "path",
424
+ "track",
425
+ "pedestrian",
426
+ "ferry"
427
+ ],
428
+ paint: {
429
+ "line-color": p("road"),
430
+ "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 16, 4]
431
+ }
432
+ },
433
+ {
434
+ id: "road-major",
435
+ type: "line",
436
+ source: "territory",
437
+ "source-layer": "transportation",
438
+ filter: ["in", "class", "motorway", "trunk", "primary"],
439
+ paint: {
440
+ "line-color": p("roadMajor"),
441
+ "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1, 16, 8]
442
+ }
443
+ },
444
+ {
445
+ id: "boundary-minor",
446
+ type: "line",
447
+ source: "territory",
448
+ "source-layer": "boundary",
449
+ minzoom: 6,
450
+ filter: ["all", [">=", "admin_level", 3], ["<=", "admin_level", 8]],
451
+ paint: {
452
+ "line-color": p("boundaryMinor"),
453
+ "line-width": 0.6,
454
+ "line-dasharray": [2, 2]
455
+ }
456
+ },
457
+ {
458
+ id: "boundary",
459
+ type: "line",
460
+ source: "territory",
461
+ "source-layer": "boundary",
462
+ filter: ["<=", "admin_level", 2],
463
+ paint: {
464
+ "line-color": p("boundary"),
465
+ "line-width": 1,
466
+ "line-dasharray": [3, 2]
467
+ }
468
+ }
469
+ ];
470
+ if (theme.buildings_3d) {
471
+ const flat = layers.find((l) => l["id"] === "building");
472
+ flat["maxzoom"] = BUILDINGS_3D_MINZOOM;
473
+ layers.push(buildings3dLayer(p("building")));
474
+ }
475
+ (theme.extra_layers ?? []).forEach((extra, index) => {
476
+ layers.push(structuredClone(validateExtraLayer(index, extra)));
477
+ });
478
+ layers.push(
479
+ symbol(
480
+ "housenumber",
481
+ "housenumber",
482
+ ["get", "housenumber"],
483
+ font,
484
+ 10,
485
+ p("textSecondary"),
486
+ p("textHalo"),
487
+ 17
488
+ ),
489
+ symbol(
490
+ "road-labels",
491
+ "transportation_name",
492
+ nameField,
493
+ font,
494
+ 11,
495
+ p("textSecondary"),
496
+ p("textHalo"),
497
+ 12,
498
+ { "symbol-placement": "line" }
499
+ ),
500
+ symbol(
501
+ "water-name",
502
+ "water_name",
503
+ nameField,
504
+ font,
505
+ 12,
506
+ p("textSecondary"),
507
+ p("textHalo")
508
+ ),
509
+ symbol(
510
+ "poi-labels",
511
+ "poi",
512
+ nameField,
513
+ font,
514
+ 11,
515
+ p("textSecondary"),
516
+ p("textHalo"),
517
+ 14,
518
+ { "text-anchor": "top" }
519
+ ),
520
+ symbol(
521
+ "mountain-peak-labels",
522
+ "mountain_peak",
523
+ nameField,
524
+ font,
525
+ 11,
526
+ p("textSecondary"),
527
+ p("textHalo")
528
+ ),
529
+ symbol(
530
+ "aerodrome-labels",
531
+ "aerodrome_label",
532
+ nameField,
533
+ font,
534
+ 11,
535
+ p("textSecondary"),
536
+ p("textHalo"),
537
+ 10
538
+ ),
539
+ symbol(
540
+ "place-labels",
541
+ "place",
542
+ nameField,
543
+ font,
544
+ ["interpolate", ["linear"], ["zoom"], 4, 11, 12, 16],
545
+ p("textPrimary"),
546
+ p("textHalo")
547
+ )
548
+ );
549
+ const seenIds = /* @__PURE__ */ new Set();
550
+ for (const layer of layers) {
551
+ const layerId = layer["id"];
552
+ if (seenIds.has(layerId)) {
553
+ throw new Error(`duplicate layer id ${JSON.stringify(layerId)}`);
554
+ }
555
+ seenIds.add(layerId);
556
+ }
557
+ const accepted = layers.map((l) => l["id"]).join(", ");
558
+ for (const [id, layerOverride] of Object.entries(theme.layers ?? {})) {
559
+ const layer = layers.find((l) => l["id"] === id);
560
+ if (!layer) {
561
+ throw new Error(
562
+ `unknown layer id ${JSON.stringify(id)}; accepted ids: ${accepted}`
563
+ );
564
+ }
565
+ applyOverride(layer, layerOverride);
566
+ }
567
+ const style = {
568
+ version: 8,
569
+ name: theme.name ? `MapMap ${theme.name}` : "MapMap",
570
+ glyphs: theme.glyphs ?? DEFAULT_GLYPHS_URL,
571
+ sources: {
572
+ territory: {
573
+ type: "vector",
574
+ url: tilesUrl,
575
+ // Legally required (ODbL + CC-BY) - enforced, not themable. There
576
+ // is no theme field that can remove it.
577
+ attribution: FULL_ATTRIBUTION
578
+ }
579
+ },
580
+ layers
581
+ };
582
+ if (theme.sprite !== void 0) style["sprite"] = theme.sprite;
583
+ return style;
584
+ }
585
+
586
+ // src/map.ts
587
+ var pmtilesRegistered = /* @__PURE__ */ new WeakSet();
588
+ function registerPmtilesProtocol(gl = maplibregl) {
589
+ if (pmtilesRegistered.has(gl)) return;
590
+ const protocol = new Protocol();
591
+ gl.addProtocol("pmtiles", protocol.tile);
592
+ pmtilesRegistered.add(gl);
593
+ }
594
+ var DEFAULT_BASE_URL = "https://api.mapmap.ai";
595
+ var DEFAULT_CENTER = [-1.5, 52.6];
596
+ var DEFAULT_ZOOM = 5;
597
+ var MapMapMap = class {
598
+ constructor(options) {
599
+ registerPmtilesProtocol();
600
+ this.apiKey = options.apiKey;
601
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
602
+ this.navDesign = navDesignFromTheme(themeOf(options.style));
603
+ const style = resolveStyle(options.style, options.territoryTilesUrl);
604
+ this.map = new maplibregl.Map({
605
+ container: options.container,
606
+ style,
607
+ center: options.center ?? DEFAULT_CENTER,
608
+ zoom: options.zoom ?? DEFAULT_ZOOM,
609
+ attributionControl: { compact: false },
610
+ ...options.mapOptions
611
+ });
612
+ if (options.logo !== false) {
613
+ const logoOptions = options.logo === true ? {} : options.logo ?? {};
614
+ this.map.addControl(
615
+ new LogoControl(logoOptions),
616
+ logoOptions.position ?? "bottom-right"
617
+ );
618
+ }
619
+ }
620
+ /**
621
+ * Toggle 3D building extrusions at runtime — the live equivalent of
622
+ * compiling the style with `buildings_3d: true` (see `Theme`): adds or
623
+ * removes the `building-3d` fill-extrusion layer and hands the flat
624
+ * `building` footprint layer off at zoom 15. The extrusion takes its
625
+ * colour from the current `building` fill, so it follows the theme.
626
+ * Idempotent; safe to call before the style has loaded (it applies on
627
+ * load). Requires the MapMap skeleton's `building` layer — on a custom
628
+ * style without one this throws rather than guessing.
629
+ *
630
+ * Mobile note: leave 3D off in native navigation views for now
631
+ * (maplibre-native#4107 — fill-extrusion memory at street zooms).
632
+ */
633
+ setBuildings3d(enabled) {
634
+ if (!this.map.isStyleLoaded()) {
635
+ this.map.once("load", () => this.setBuildings3d(enabled));
636
+ return;
637
+ }
638
+ if (enabled === (this.map.getLayer("building-3d") !== void 0)) return;
639
+ const building = this.map.getLayer("building");
640
+ if (!building) {
641
+ throw new Error(
642
+ 'setBuildings3d needs the MapMap skeleton "building" layer; the current style does not have one'
643
+ );
644
+ }
645
+ const minzoom = building.minzoom ?? 13;
646
+ if (enabled) {
647
+ const colour = this.map.getPaintProperty("building", "fill-color") ?? "#e2ddd4";
648
+ this.map.addLayer(
649
+ buildings3dLayer(colour),
650
+ buildings3dBeforeId(this.map)
651
+ );
652
+ this.map.setLayerZoomRange("building", minzoom, BUILDINGS_3D_MINZOOM);
653
+ } else {
654
+ this.map.removeLayer("building-3d");
655
+ this.map.setLayerZoomRange("building", minzoom, 24);
656
+ }
657
+ }
658
+ /** Resolves once the style and first tiles are loaded. */
659
+ whenReady() {
660
+ return new Promise((resolve) => {
661
+ if (this.map.loaded()) {
662
+ resolve();
663
+ return;
664
+ }
665
+ this.map.once("load", () => resolve());
666
+ });
667
+ }
668
+ /** Remove the map and release its WebGL context. */
669
+ destroy() {
670
+ this.map.remove();
671
+ }
672
+ };
673
+ function createMap(options) {
674
+ return new MapMapMap(options);
675
+ }
676
+ function buildings3dBeforeId(map) {
677
+ const layers = map.getStyle().layers ?? [];
678
+ const boundary = layers.findIndex((l) => l.id === "boundary");
679
+ if (boundary >= 0) return layers[boundary + 1]?.id;
680
+ return layers.find((l) => l.type === "symbol")?.id;
681
+ }
682
+ function themeOf(style) {
683
+ if (typeof style === "object" && style !== null && !("version" in style)) {
684
+ return style;
685
+ }
686
+ return void 0;
687
+ }
688
+ function resolveStyle(style, territoryTilesUrl) {
689
+ if (style === void 0 || style === "light" || style === "dark") {
690
+ return buildStyle({
691
+ theme: style ?? "light",
692
+ ...territoryTilesUrl ? { territoryTilesUrl } : {}
693
+ });
694
+ }
695
+ const theme = themeOf(style);
696
+ if (theme) {
697
+ return buildStyle({
698
+ theme,
699
+ ...territoryTilesUrl ? { territoryTilesUrl } : {}
700
+ });
701
+ }
702
+ return style;
703
+ }
704
+
705
+ // src/coords.ts
706
+ function toLngLat(point) {
707
+ if (Array.isArray(point)) {
708
+ const [lng2, lat2] = point;
709
+ assertFinite(lng2, lat2);
710
+ return [lng2, lat2];
711
+ }
712
+ const lng = "lng" in point ? point.lng : point.lon;
713
+ const lat = point.lat;
714
+ assertFinite(lng, lat);
715
+ return [lng, lat];
716
+ }
717
+ function formatCoord(point) {
718
+ const [lng, lat] = toLngLat(point);
719
+ return `${lng},${lat}`;
720
+ }
721
+ function formatCoords(points) {
722
+ if (points.length < 2) {
723
+ throw new Error("at least two coordinates are required for a route");
724
+ }
725
+ return points.map(formatCoord).join(";");
726
+ }
727
+ function assertFinite(lng, lat) {
728
+ if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
729
+ throw new Error(`invalid coordinate: lng=${lng}, lat=${lat}`);
730
+ }
731
+ if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
732
+ throw new Error(
733
+ `coordinate out of range: lng=${lng} (\xB1180), lat=${lat} (\xB190) - check lng/lat order`
734
+ );
735
+ }
736
+ }
737
+
738
+ // src/osrm.ts
739
+ function buildRouteQuery(truck, guidance) {
740
+ const params = new URLSearchParams({
741
+ geometries: "geojson",
742
+ overview: "full",
743
+ steps: "true"
744
+ });
745
+ if (guidance?.voice) params.set("voice_instructions", "true");
746
+ if (guidance?.banner) params.set("banner_instructions", "true");
747
+ if (guidance?.language) params.set("language", guidance.language);
748
+ if (truck) {
749
+ if (truck.heightM != null) params.set("height", String(truck.heightM));
750
+ if (truck.widthM != null) params.set("width", String(truck.widthM));
751
+ if (truck.lengthM != null) params.set("length", String(truck.lengthM));
752
+ if (truck.weightT != null) params.set("weight", String(truck.weightT));
753
+ if (truck.hazmat != null) params.set("hazmat", String(truck.hazmat));
754
+ if (truck.tunnelCode != null) params.set("tunnel_code", truck.tunnelCode);
755
+ }
756
+ return params.toString();
757
+ }
758
+ function buildRouteUrl(baseUrl, profile, points, truck, guidance) {
759
+ const origin = baseUrl.replace(/\/+$/, "");
760
+ const coords = formatCoords(points);
761
+ const query = buildRouteQuery(truck, guidance);
762
+ return `${origin}/route/v1/${encodeURIComponent(profile)}/${coords}?${query}`;
763
+ }
764
+ function parseOsrmRoute(body) {
765
+ if (typeof body !== "object" || body === null) {
766
+ throw new Error("OSRM response was not an object");
767
+ }
768
+ const obj = body;
769
+ const code = obj["code"];
770
+ if (code !== "Ok") {
771
+ const message = typeof obj["message"] === "string" ? obj["message"] : "";
772
+ throw new Error(
773
+ `OSRM routing failed: ${String(code)}${message ? ` - ${message}` : ""}`
774
+ );
775
+ }
776
+ const routes = obj["routes"];
777
+ if (!Array.isArray(routes) || routes.length === 0) {
778
+ throw new Error("OSRM response contained no routes");
779
+ }
780
+ const route = routes[0];
781
+ const geometry = parseGeometry(route["geometry"]);
782
+ const distanceM = numberOr(route["distance"], 0);
783
+ const durationS = numberOr(route["duration"], 0);
784
+ return { distanceM, durationS, geometry, raw: route };
785
+ }
786
+ function parseGeometry(geometry) {
787
+ if (typeof geometry !== "object" || geometry === null || geometry.type !== "LineString") {
788
+ throw new Error(
789
+ "route geometry was not GeoJSON - request geometries=geojson"
790
+ );
791
+ }
792
+ const coordinates = geometry.coordinates;
793
+ if (!Array.isArray(coordinates)) {
794
+ throw new Error("route geometry had no coordinates");
795
+ }
796
+ const positions = coordinates.map((pos, i) => {
797
+ if (!Array.isArray(pos) || typeof pos[0] !== "number" || typeof pos[1] !== "number") {
798
+ throw new Error(`route geometry position ${i} is not [lng, lat]`);
799
+ }
800
+ return [pos[0], pos[1]];
801
+ });
802
+ return { type: "LineString", coordinates: positions };
803
+ }
804
+ function numberOr(value, fallback) {
805
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
806
+ }
807
+
808
+ // src/route.ts
809
+ var SIGNAL_BLUE = "#3a86ff";
810
+ var CASING_COLOR = "#1f438a";
811
+ var RouteLayer = class {
812
+ constructor(map, options = {}) {
813
+ this.handleStyleLoad = () => {
814
+ if (this.lastRoute) this.install(this.lastRoute);
815
+ };
816
+ if (map instanceof MapMapMap) {
817
+ this.map = map.map;
818
+ this.baseUrl = (options.baseUrl ?? map.baseUrl).replace(/\/+$/, "");
819
+ this.apiKey = options.apiKey ?? map.apiKey;
820
+ this.design = options.design ?? map.navDesign?.route;
821
+ } else {
822
+ this.map = map;
823
+ if (!options.baseUrl) {
824
+ throw new Error("RouteLayer needs a baseUrl when given a raw MapLibre map");
825
+ }
826
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
827
+ this.apiKey = options.apiKey;
828
+ this.design = options.design;
829
+ }
830
+ const id = options.id ?? "mapmap-route";
831
+ this.sourceId = `${id}-src`;
832
+ this.casingLayerId = `${id}-casing`;
833
+ this.lineLayerId = `${id}-line`;
834
+ this.map.on("style.load", this.handleStyleLoad);
835
+ }
836
+ /**
837
+ * Route from `from` to `to` (with optional intermediate `via` points passed
838
+ * through `options` is not supported here - pass a coordinate list to
839
+ * {@link routePath} for that), draw the line, and return the parsed route.
840
+ */
841
+ async route(from, to, options = {}) {
842
+ return this.routePath([from, to], options);
843
+ }
844
+ /** Route through an ordered list of coordinates and draw the result. */
845
+ async routePath(points, options = {}) {
846
+ const url = buildRouteUrl(
847
+ this.baseUrl,
848
+ options.profile ?? "driving",
849
+ points,
850
+ options.truck,
851
+ { voice: options.voice, banner: options.banner, language: options.language }
852
+ );
853
+ const headers = { Accept: "application/json" };
854
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
855
+ const response = await fetch(url, { headers });
856
+ const body = await response.json().catch(() => null);
857
+ if (!response.ok) {
858
+ const obj = typeof body === "object" && body !== null ? body : void 0;
859
+ if (typeof obj?.["code"] !== "string") {
860
+ const context = [obj?.["title"], obj?.["detail"]].filter((v) => typeof v === "string" && v.length > 0).join(" - ");
861
+ throw new Error(
862
+ `route request failed: HTTP ${response.status}${context ? ` (${context})` : ""}`
863
+ );
864
+ }
865
+ }
866
+ const parsed = parseOsrmRoute(body);
867
+ this.draw(parsed);
868
+ return parsed;
869
+ }
870
+ /** The most recently drawn route, if any. */
871
+ get current() {
872
+ return this.lastRoute;
873
+ }
874
+ /**
875
+ * Draw (or update) a parsed route's line on the map. Safe to call before
876
+ * the style has loaded: the route is stored and installed on the next
877
+ * `style.load` instead of throwing MapLibre's "Style is not done loading".
878
+ */
879
+ draw(route) {
880
+ this.lastRoute = route;
881
+ if (!this.map.isStyleLoaded()) return;
882
+ this.install(route);
883
+ }
884
+ /** Add-or-update the source and layers for a route on the current style. */
885
+ install(route) {
886
+ const data = {
887
+ type: "Feature",
888
+ properties: {},
889
+ geometry: route.geometry
890
+ };
891
+ const existing = this.map.getSource(this.sourceId);
892
+ if (existing) {
893
+ existing.setData(data);
894
+ } else {
895
+ this.map.addSource(this.sourceId, { type: "geojson", data });
896
+ }
897
+ if (this.map.getLayer(this.casingLayerId) && this.map.getLayer(this.lineLayerId)) {
898
+ return;
899
+ }
900
+ const casing = {
901
+ id: this.casingLayerId,
902
+ type: "line",
903
+ source: this.sourceId,
904
+ layout: { "line-cap": "round", "line-join": "round" },
905
+ paint: this.design ? {
906
+ "line-color": this.design.casingColor,
907
+ "line-width": this.design.width + 4,
908
+ "line-opacity": this.design.opacity
909
+ } : {
910
+ "line-color": CASING_COLOR,
911
+ "line-width": ["interpolate", ["linear"], ["zoom"], 8, 6, 16, 12]
912
+ }
913
+ };
914
+ const line = {
915
+ id: this.lineLayerId,
916
+ type: "line",
917
+ source: this.sourceId,
918
+ layout: { "line-cap": "round", "line-join": "round" },
919
+ paint: this.design ? {
920
+ "line-color": this.design.color,
921
+ "line-width": this.design.width,
922
+ "line-opacity": this.design.opacity
923
+ } : {
924
+ "line-color": SIGNAL_BLUE,
925
+ "line-width": ["interpolate", ["linear"], ["zoom"], 8, 3, 16, 8]
926
+ }
927
+ };
928
+ if (!this.map.getLayer(this.casingLayerId)) this.map.addLayer(casing);
929
+ if (!this.map.getLayer(this.lineLayerId)) this.map.addLayer(line);
930
+ }
931
+ /** Remove the route's layers and source from the map. */
932
+ clear() {
933
+ for (const layerId of [this.lineLayerId, this.casingLayerId]) {
934
+ if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
935
+ }
936
+ if (this.map.getSource(this.sourceId)) this.map.removeSource(this.sourceId);
937
+ this.lastRoute = void 0;
938
+ }
939
+ /**
940
+ * Remove the route and detach the layer's `style.load` listener. Call
941
+ * when disposing of the layer (the map itself is left untouched).
942
+ */
943
+ destroy() {
944
+ this.map.off("style.load", this.handleStyleLoad);
945
+ this.clear();
946
+ }
947
+ };
948
+ var MAX_PUCK_IMAGE_BYTES = 64 * 1024;
949
+ function dataUriBytes(uri) {
950
+ const comma = uri.indexOf(",");
951
+ const payload = comma === -1 ? "" : uri.slice(comma + 1);
952
+ const meta = comma === -1 ? uri : uri.slice(0, comma);
953
+ return /;base64$/i.test(meta) || /;base64;/i.test(meta) ? Math.floor(payload.length * 3 / 4) : payload.length;
954
+ }
955
+ function effectiveImageUrl(design) {
956
+ const url = design.imageUrl?.trim();
957
+ if (!url) return void 0;
958
+ if (/^data:/i.test(url) && dataUriBytes(url) > MAX_PUCK_IMAGE_BYTES) {
959
+ return void 0;
960
+ }
961
+ return url;
962
+ }
963
+ var PositionPuck = class {
964
+ /**
965
+ * Creates the puck (not yet on the map - it appears on the first
966
+ * {@link setLocation}). The design defaults to the map's
967
+ * `navDesign.puck` when given a `MapMapMap` whose theme carried an
968
+ * `extra.nav` block, then to the built-in blue puck.
969
+ */
970
+ constructor(map, design) {
971
+ this.added = false;
972
+ this.map = map instanceof MapMapMap ? map.map : map;
973
+ this.design = design ?? (map instanceof MapMapMap ? map.navDesign?.puck : void 0) ?? defaultNavDesign().puck;
974
+ this.element = createPuckElement();
975
+ stylePuckElement(this.element, this.design);
976
+ this.marker = new maplibregl.Marker({
977
+ element: this.element,
978
+ rotationAlignment: "map",
979
+ pitchAlignment: "map"
980
+ });
981
+ }
982
+ /**
983
+ * Moves the puck (adding it to the map on the first call). `headingDeg`
984
+ * rotates the whole element - arrow or custom image - clockwise from
985
+ * north; omit it to keep the previous heading.
986
+ */
987
+ setLocation(location, headingDeg) {
988
+ this.marker.setLngLat([location.lon, location.lat]);
989
+ if (headingDeg !== void 0) this.marker.setRotation(headingDeg);
990
+ if (!this.added) {
991
+ this.marker.addTo(this.map);
992
+ this.added = true;
993
+ }
994
+ }
995
+ /** Removes the puck from the map. `setLocation` re-adds it. */
996
+ remove() {
997
+ this.marker.remove();
998
+ this.added = false;
999
+ }
1000
+ };
1001
+ function createPuckElement() {
1002
+ const doc = globalThis.document;
1003
+ if (!doc) throw new Error("PositionPuck requires a DOM environment");
1004
+ const el = doc.createElement("div");
1005
+ el.className = "mapmap-puck";
1006
+ el.style.position = "relative";
1007
+ const arrow = doc.createElement("div");
1008
+ arrow.dataset["part"] = "arrow";
1009
+ const dot = doc.createElement("div");
1010
+ dot.dataset["part"] = "dot";
1011
+ const img = doc.createElement("img");
1012
+ img.dataset["part"] = "image";
1013
+ img.alt = "";
1014
+ img.draggable = false;
1015
+ img.style.display = "none";
1016
+ el.append(arrow, dot, img);
1017
+ return el;
1018
+ }
1019
+ function stylePuckElement(el, puck) {
1020
+ const dot = el.querySelector("[data-part='dot']");
1021
+ const arrow = el.querySelector("[data-part='arrow']");
1022
+ const img = el.querySelector("[data-part='image']");
1023
+ if (!dot || !arrow || !img) return;
1024
+ const s = puck.size;
1025
+ el.style.width = `${s}px`;
1026
+ el.style.height = `${s}px`;
1027
+ const url = effectiveImageUrl(puck) ?? "";
1028
+ if (url !== "" && el.dataset["imageFailed"] !== url) {
1029
+ if (img.getAttribute("src") !== url) {
1030
+ img.onerror = () => {
1031
+ el.dataset["imageFailed"] = url;
1032
+ img.removeAttribute("src");
1033
+ stylePuckElement(el, puck);
1034
+ };
1035
+ img.src = url;
1036
+ }
1037
+ img.style.cssText = `display:block;position:absolute;inset:0;width:${s}px;height:${s}px;object-fit:contain;filter:drop-shadow(0 1px 4px rgba(0,0,0,0.5))`;
1038
+ dot.style.cssText = "display:none";
1039
+ arrow.style.cssText = "display:none";
1040
+ return;
1041
+ }
1042
+ img.onerror = null;
1043
+ if (img.getAttribute("src") !== null) img.removeAttribute("src");
1044
+ img.style.cssText = "display:none";
1045
+ dot.style.cssText = `position:absolute;inset:0;border-radius:50%;background:${puck.color};border:2px solid #ffffff;box-shadow:0 1px 6px rgba(0,0,0,0.45)`;
1046
+ arrow.style.cssText = puck.headingArrow ? `position:absolute;left:50%;top:${(-s * 0.55).toFixed(1)}px;transform:translateX(-50%);width:0;height:0;border-left:${(s * 0.35).toFixed(1)}px solid transparent;border-right:${(s * 0.35).toFixed(1)}px solid transparent;border-bottom:${(s * 0.6).toFixed(1)}px solid ${puck.color}` : "display:none";
1047
+ }
1048
+ var EARTH_RADIUS_M = 63710088e-1;
1049
+ var CLUSTER_TEXT_FONT = "Noto Sans Regular";
1050
+ function placesFromGeoJSON(collection) {
1051
+ const places = [];
1052
+ collection.features.forEach((feature, index) => {
1053
+ if (feature.geometry?.type !== "Point") return;
1054
+ const [lon, lat] = feature.geometry.coordinates;
1055
+ if (typeof lon !== "number" || typeof lat !== "number") {
1056
+ throw new Error(`place feature ${index} has no [lon, lat] coordinates`);
1057
+ }
1058
+ const props = feature.properties ?? {};
1059
+ const rawId = feature.id ?? props["id"];
1060
+ const place = {
1061
+ id: typeof rawId === "string" || typeof rawId === "number" ? String(rawId) : String(index),
1062
+ name: typeof props["name"] === "string" ? props["name"] : "",
1063
+ lat,
1064
+ lon
1065
+ };
1066
+ if (feature.properties) place.properties = feature.properties;
1067
+ places.push(place);
1068
+ });
1069
+ return places;
1070
+ }
1071
+ function haversineDistanceM(a, b) {
1072
+ const rad = (deg) => deg * Math.PI / 180;
1073
+ const dLat = rad(b.lat - a.lat);
1074
+ const dLon = rad(b.lon - a.lon);
1075
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2;
1076
+ return 2 * EARTH_RADIUS_M * Math.asin(Math.sqrt(h));
1077
+ }
1078
+ var PlacesLayer = class {
1079
+ constructor(map, options = {}) {
1080
+ this.places = [];
1081
+ this.fitted = false;
1082
+ this.destroyed = false;
1083
+ this.handleStyleLoad = () => {
1084
+ if (this.data) this.install(this.data);
1085
+ };
1086
+ this.handleMouseEnter = () => {
1087
+ this.map.getCanvas().style.cursor = "pointer";
1088
+ };
1089
+ this.handleMouseLeave = () => {
1090
+ this.map.getCanvas().style.cursor = "";
1091
+ };
1092
+ this.handlePlaceClick = (e) => {
1093
+ const feature = e.features?.[0];
1094
+ if (!feature) return;
1095
+ const index = feature.properties?.["__mapmapIndex"];
1096
+ if (typeof index !== "number") return;
1097
+ const place = this.places[index];
1098
+ if (!place) return;
1099
+ this.onPlaceClick?.(place, { lng: e.lngLat.lng, lat: e.lngLat.lat });
1100
+ if (this.popupFn) this.openPopup(place);
1101
+ };
1102
+ this.handleClusterClick = (e) => {
1103
+ const feature = e.features?.[0];
1104
+ if (!feature) return;
1105
+ const clusterId = feature.properties?.["cluster_id"];
1106
+ if (typeof clusterId !== "number") return;
1107
+ const source = this.map.getSource(this.sourceId);
1108
+ if (!source) return;
1109
+ const geometry = feature.geometry;
1110
+ if (geometry.type !== "Point") return;
1111
+ const [lng, lat] = geometry.coordinates;
1112
+ if (typeof lng !== "number" || typeof lat !== "number") return;
1113
+ void source.getClusterExpansionZoom(clusterId).then((zoom) => {
1114
+ if (!this.destroyed) this.map.easeTo({ center: [lng, lat], zoom });
1115
+ }).catch(() => {
1116
+ });
1117
+ };
1118
+ if (map instanceof MapMapMap) {
1119
+ this.map = map.map;
1120
+ this.baseUrl = (options.baseUrl ?? map.baseUrl).replace(/\/+$/, "");
1121
+ this.apiKey = options.apiKey ?? map.apiKey;
1122
+ } else {
1123
+ this.map = map;
1124
+ this.baseUrl = options.baseUrl?.replace(/\/+$/, "");
1125
+ this.apiKey = options.apiKey;
1126
+ }
1127
+ const id = options.id ?? "mapmap-places";
1128
+ this.sourceId = `${id}-src`;
1129
+ this.clustersLayerId = `${id}-clusters`;
1130
+ this.clusterCountLayerId = `${id}-cluster-count`;
1131
+ this.pointsLayerId = `${id}-points`;
1132
+ this.iconImageId = `${id}-icon`;
1133
+ this.cluster = options.cluster ?? true;
1134
+ this.clusterRadius = options.clusterRadius;
1135
+ this.clusterMaxZoom = options.clusterMaxZoom;
1136
+ this.color = options.color ?? SIGNAL_BLUE;
1137
+ this.icon = options.icon;
1138
+ this.wantFitBounds = options.fitBounds ?? false;
1139
+ this.onPlaceClick = options.onPlaceClick;
1140
+ this.popupFn = options.popup;
1141
+ this.map.on("style.load", this.handleStyleLoad);
1142
+ this.map.on("click", this.pointsLayerId, this.handlePlaceClick);
1143
+ this.map.on("mouseenter", this.pointsLayerId, this.handleMouseEnter);
1144
+ this.map.on("mouseleave", this.pointsLayerId, this.handleMouseLeave);
1145
+ if (this.cluster) {
1146
+ this.map.on("click", this.clustersLayerId, this.handleClusterClick);
1147
+ this.map.on("mouseenter", this.clustersLayerId, this.handleMouseEnter);
1148
+ this.map.on("mouseleave", this.clustersLayerId, this.handleMouseLeave);
1149
+ }
1150
+ if (options.places) this.setPlaces(options.places);
1151
+ }
1152
+ /**
1153
+ * Replace the layer's places - a `Place[]` or a GeoJSON FeatureCollection
1154
+ * of Points. Updates the existing GeoJSON source in place; safe to call
1155
+ * before the style has loaded (installed on the next `style.load`). With
1156
+ * `fitBounds: true` the first non-empty set also fits the map view.
1157
+ */
1158
+ setPlaces(places) {
1159
+ this.places = Array.isArray(places) ? places : placesFromGeoJSON(places);
1160
+ this.data = {
1161
+ type: "FeatureCollection",
1162
+ features: this.places.map((place, index) => ({
1163
+ type: "Feature",
1164
+ geometry: { type: "Point", coordinates: [place.lon, place.lat] },
1165
+ properties: {
1166
+ ...place.properties,
1167
+ id: place.id,
1168
+ name: place.name,
1169
+ __mapmapIndex: index
1170
+ }
1171
+ }))
1172
+ };
1173
+ if (this.wantFitBounds && !this.fitted && this.places.length > 0) {
1174
+ this.fitted = true;
1175
+ this.map.fitBounds(boundsOf(this.places), { padding: 48, maxZoom: 15 });
1176
+ }
1177
+ if (!this.map.isStyleLoaded()) return;
1178
+ this.install(this.data);
1179
+ }
1180
+ /** The layer's current places (normalised to `Place[]`). */
1181
+ get current() {
1182
+ return this.places;
1183
+ }
1184
+ /**
1185
+ * The nearest `n` places (default `1`) to `origin` by straight-line
1186
+ * (haversine) distance, each with `distanceM` attached. Pure and instant -
1187
+ * no network. For road-network answers use {@link nearestByDriveTime}.
1188
+ */
1189
+ nearest(origin, n = 1) {
1190
+ return this.places.map((place) => ({ ...place, distanceM: haversineDistanceM(origin, place) })).sort((a, b) => a.distanceM - b.distanceM).slice(0, Math.max(0, n));
1191
+ }
1192
+ /**
1193
+ * The nearest `n` places (default `1`) to `origin` by drive time, via the
1194
+ * gateway's `POST /matrix` (one source, all places as targets). Resolves
1195
+ * to places sorted by `durationS` ascending, with the driven `distanceM`
1196
+ * attached; unreachable places (`null` duration) are dropped. Needs the
1197
+ * gateway `baseUrl`/`apiKey` - picked up from a `MapMapMap` automatically,
1198
+ * or pass them as layer options.
1199
+ */
1200
+ async nearestByDriveTime(origin, options = {}) {
1201
+ if (!this.baseUrl) {
1202
+ throw new Error(
1203
+ "PlacesLayer needs a baseUrl (or a MapMapMap) for nearestByDriveTime"
1204
+ );
1205
+ }
1206
+ if (this.places.length === 0) return [];
1207
+ const headers = {
1208
+ "Content-Type": "application/json",
1209
+ Accept: "application/json"
1210
+ };
1211
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
1212
+ const body = {
1213
+ sources: [{ lat: origin.lat, lon: origin.lon }],
1214
+ targets: this.places.map((p) => ({ lat: p.lat, lon: p.lon })),
1215
+ costing: options.costing ?? "auto"
1216
+ };
1217
+ if (options.costingOptions) body["costing_options"] = options.costingOptions;
1218
+ const response = await fetch(`${this.baseUrl}/matrix`, {
1219
+ method: "POST",
1220
+ headers,
1221
+ body: JSON.stringify(body)
1222
+ });
1223
+ const payload = await response.json().catch(() => null);
1224
+ if (!response.ok) {
1225
+ const problem = typeof payload === "object" && payload !== null ? payload : void 0;
1226
+ const context = [problem?.["title"], problem?.["detail"]].filter((v) => typeof v === "string" && v.length > 0).join(" - ");
1227
+ throw new Error(
1228
+ `matrix request failed: HTTP ${response.status}${context ? ` (${context})` : ""}`
1229
+ );
1230
+ }
1231
+ const { durations, distances } = matrixRows(payload);
1232
+ const reachable = [];
1233
+ this.places.forEach((place, index) => {
1234
+ const durationS = durations[index];
1235
+ if (typeof durationS !== "number") return;
1236
+ const distanceM = distances?.[index];
1237
+ reachable.push({
1238
+ ...place,
1239
+ durationS,
1240
+ distanceM: typeof distanceM === "number" ? distanceM : null
1241
+ });
1242
+ });
1243
+ reachable.sort((a, b) => a.durationS - b.durationS);
1244
+ return reachable.slice(0, Math.max(0, options.n ?? 1));
1245
+ }
1246
+ /** Add-or-update the source and layers on the current style. */
1247
+ install(data) {
1248
+ const existing = this.map.getSource(this.sourceId);
1249
+ if (existing) {
1250
+ existing.setData(data);
1251
+ } else {
1252
+ this.map.addSource(this.sourceId, {
1253
+ type: "geojson",
1254
+ data,
1255
+ cluster: this.cluster,
1256
+ ...this.cluster && this.clusterRadius !== void 0 ? { clusterRadius: this.clusterRadius } : {},
1257
+ ...this.cluster && this.clusterMaxZoom !== void 0 ? { clusterMaxZoom: this.clusterMaxZoom } : {}
1258
+ });
1259
+ }
1260
+ if (this.cluster) this.installClusterLayers();
1261
+ this.installPointsLayer();
1262
+ }
1263
+ /** Cluster circles (sized by count) and the count badge on top. */
1264
+ installClusterLayers() {
1265
+ if (!this.map.getLayer(this.clustersLayerId)) {
1266
+ const clusters = {
1267
+ id: this.clustersLayerId,
1268
+ type: "circle",
1269
+ source: this.sourceId,
1270
+ filter: ["has", "point_count"],
1271
+ paint: {
1272
+ "circle-color": this.color,
1273
+ "circle-opacity": 0.9,
1274
+ "circle-radius": ["step", ["get", "point_count"], 16, 25, 20, 100, 26],
1275
+ "circle-stroke-color": "#ffffff",
1276
+ "circle-stroke-width": 2
1277
+ }
1278
+ };
1279
+ this.map.addLayer(clusters);
1280
+ }
1281
+ if (!this.map.getLayer(this.clusterCountLayerId)) {
1282
+ const count = {
1283
+ id: this.clusterCountLayerId,
1284
+ type: "symbol",
1285
+ source: this.sourceId,
1286
+ filter: ["has", "point_count"],
1287
+ layout: {
1288
+ "text-field": ["get", "point_count_abbreviated"],
1289
+ "text-font": [CLUSTER_TEXT_FONT],
1290
+ "text-size": 12
1291
+ },
1292
+ paint: { "text-color": "#ffffff" }
1293
+ };
1294
+ this.map.addLayer(count);
1295
+ }
1296
+ }
1297
+ /**
1298
+ * The unclustered-places layer: a circle pin by default, or a symbol
1299
+ * layer once the custom `icon` image has loaded (falling back to the
1300
+ * circle look if it fails, like PositionPuck's image fallback).
1301
+ */
1302
+ installPointsLayer() {
1303
+ if (this.map.getLayer(this.pointsLayerId)) return;
1304
+ if (!this.icon) {
1305
+ this.addCirclePointsLayer();
1306
+ return;
1307
+ }
1308
+ if (this.map.hasImage(this.iconImageId)) {
1309
+ this.addIconPointsLayer();
1310
+ return;
1311
+ }
1312
+ void this.map.loadImage(this.icon.url).then((image) => {
1313
+ if (this.destroyed || this.map.getLayer(this.pointsLayerId)) return;
1314
+ if (!this.map.hasImage(this.iconImageId)) {
1315
+ this.map.addImage(this.iconImageId, image.data);
1316
+ }
1317
+ this.addIconPointsLayer();
1318
+ }).catch(() => {
1319
+ if (this.destroyed || this.map.getLayer(this.pointsLayerId)) return;
1320
+ this.addCirclePointsLayer();
1321
+ });
1322
+ }
1323
+ addCirclePointsLayer() {
1324
+ const points = {
1325
+ id: this.pointsLayerId,
1326
+ type: "circle",
1327
+ source: this.sourceId,
1328
+ ...this.cluster ? { filter: ["!", ["has", "point_count"]] } : {},
1329
+ paint: {
1330
+ "circle-color": this.color,
1331
+ "circle-radius": 7,
1332
+ "circle-stroke-color": "#ffffff",
1333
+ "circle-stroke-width": 2
1334
+ }
1335
+ };
1336
+ this.map.addLayer(points);
1337
+ }
1338
+ addIconPointsLayer() {
1339
+ const points = {
1340
+ id: this.pointsLayerId,
1341
+ type: "symbol",
1342
+ source: this.sourceId,
1343
+ ...this.cluster ? { filter: ["!", ["has", "point_count"]] } : {},
1344
+ layout: {
1345
+ "icon-image": this.iconImageId,
1346
+ "icon-size": this.icon?.size ?? 1,
1347
+ "icon-allow-overlap": true
1348
+ }
1349
+ };
1350
+ this.map.addLayer(points);
1351
+ }
1352
+ /** Open the built-in popup for a place, replacing any previous one. */
1353
+ openPopup(place) {
1354
+ if (!this.popupFn) return;
1355
+ const content = this.popupFn(place);
1356
+ const popup = new maplibregl.Popup({ offset: 12 }).setLngLat([
1357
+ place.lon,
1358
+ place.lat
1359
+ ]);
1360
+ if (typeof content === "string") popup.setHTML(content);
1361
+ else popup.setDOMContent(content);
1362
+ this.popup?.remove();
1363
+ this.popup = popup.addTo(this.map);
1364
+ }
1365
+ /** Remove the places' layers, source, image and popup from the map. */
1366
+ clear() {
1367
+ this.popup?.remove();
1368
+ this.popup = void 0;
1369
+ for (const layerId of [
1370
+ this.pointsLayerId,
1371
+ this.clusterCountLayerId,
1372
+ this.clustersLayerId
1373
+ ]) {
1374
+ if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
1375
+ }
1376
+ if (this.map.getSource(this.sourceId)) this.map.removeSource(this.sourceId);
1377
+ if (this.map.hasImage(this.iconImageId)) {
1378
+ this.map.removeImage(this.iconImageId);
1379
+ }
1380
+ this.places = [];
1381
+ this.data = void 0;
1382
+ }
1383
+ /**
1384
+ * Remove the places and detach every listener (style.load, clicks,
1385
+ * hovers). Call when disposing of the layer (the map is left untouched).
1386
+ */
1387
+ destroy() {
1388
+ this.destroyed = true;
1389
+ this.map.off("style.load", this.handleStyleLoad);
1390
+ this.map.off("click", this.pointsLayerId, this.handlePlaceClick);
1391
+ this.map.off("mouseenter", this.pointsLayerId, this.handleMouseEnter);
1392
+ this.map.off("mouseleave", this.pointsLayerId, this.handleMouseLeave);
1393
+ if (this.cluster) {
1394
+ this.map.off("click", this.clustersLayerId, this.handleClusterClick);
1395
+ this.map.off("mouseenter", this.clustersLayerId, this.handleMouseEnter);
1396
+ this.map.off("mouseleave", this.clustersLayerId, this.handleMouseLeave);
1397
+ }
1398
+ this.clear();
1399
+ }
1400
+ };
1401
+ function boundsOf(places) {
1402
+ let minLng = Infinity;
1403
+ let minLat = Infinity;
1404
+ let maxLng = -Infinity;
1405
+ let maxLat = -Infinity;
1406
+ for (const { lon, lat } of places) {
1407
+ if (lon < minLng) minLng = lon;
1408
+ if (lat < minLat) minLat = lat;
1409
+ if (lon > maxLng) maxLng = lon;
1410
+ if (lat > maxLat) maxLat = lat;
1411
+ }
1412
+ return [
1413
+ [minLng, minLat],
1414
+ [maxLng, maxLat]
1415
+ ];
1416
+ }
1417
+ function matrixRows(payload) {
1418
+ if (typeof payload !== "object" || payload === null) {
1419
+ throw new Error("matrix returned a non-object response");
1420
+ }
1421
+ const obj = payload;
1422
+ const durations = firstRow(obj["durations"]);
1423
+ if (!durations) {
1424
+ throw new Error("matrix response has no durations row");
1425
+ }
1426
+ return { durations, distances: firstRow(obj["distances"]) };
1427
+ }
1428
+ function firstRow(value) {
1429
+ if (!Array.isArray(value)) return void 0;
1430
+ const row = value[0];
1431
+ if (!Array.isArray(row)) return void 0;
1432
+ return row.map(
1433
+ (cell) => typeof cell === "number" && Number.isFinite(cell) ? cell : null
1434
+ );
1435
+ }
1436
+
1437
+ // src/camera.ts
1438
+ var INTERACTION_EVENTS = [
1439
+ "dragstart",
1440
+ "rotatestart",
1441
+ "pitchstart",
1442
+ "zoomstart"
1443
+ ];
1444
+ var DEFAULT_PITCH = 60;
1445
+ var DEFAULT_ZOOM2 = 17;
1446
+ var DEFAULT_ANCHOR_Y = 0.72;
1447
+ var DEFAULT_EASE_MS = 900;
1448
+ var DEFAULT_AUTO_RECENTRE_MS = 6e3;
1449
+ var OVERVIEW_PADDING_PX = 48;
1450
+ function clamp(value, min, max) {
1451
+ return Math.min(max, Math.max(min, value));
1452
+ }
1453
+ var NavigationCamera = class {
1454
+ constructor(map, options = {}) {
1455
+ this.currentMode = "follow";
1456
+ this.destroyed = false;
1457
+ // Bound once so `off` in destroy() removes exactly what `on` added.
1458
+ this.onInteraction = (ev) => {
1459
+ if (!ev?.originalEvent || this.destroyed) return;
1460
+ this.currentMode = "free";
1461
+ this.m.stop();
1462
+ this.scheduleRecentre();
1463
+ };
1464
+ this.m = map instanceof MapMapMap ? map.map : map;
1465
+ const design = map instanceof MapMapMap ? map.navDesign?.camera : void 0;
1466
+ this.pitch = clamp(options.pitch ?? design?.pitch ?? DEFAULT_PITCH, 0, 85);
1467
+ this.zoom = options.zoom ?? design?.zoom ?? DEFAULT_ZOOM2;
1468
+ this.anchorY = clamp(options.anchorY ?? DEFAULT_ANCHOR_Y, 0, 1);
1469
+ this.easeMs = Math.max(0, options.easeMs ?? DEFAULT_EASE_MS);
1470
+ this.autoRecentreMs = Math.max(
1471
+ 0,
1472
+ options.autoRecentreMs ?? DEFAULT_AUTO_RECENTRE_MS
1473
+ );
1474
+ if (this.pitch > this.m.getMaxPitch()) this.m.setMaxPitch(this.pitch);
1475
+ for (const event of INTERACTION_EVENTS) {
1476
+ this.m.on(event, this.onInteraction);
1477
+ }
1478
+ }
1479
+ /**
1480
+ * `false` when the map runs the globe (or vertical-perspective)
1481
+ * projection, whose camera geometry breaks the low-anchor offset maths
1482
+ * and top-down overview framing. Switch to mercator for navigation.
1483
+ */
1484
+ static isSupported(map) {
1485
+ const m = map instanceof MapMapMap ? map.map : map;
1486
+ const projection = m.getProjection?.();
1487
+ return !usesGlobe(projection);
1488
+ }
1489
+ /** The camera's current behaviour: `"follow"`, `"overview"` or `"free"`. */
1490
+ get mode() {
1491
+ return this.currentMode;
1492
+ }
1493
+ /**
1494
+ * Co-drive a {@link PositionPuck}: every {@link follow} call also
1495
+ * `setLocation`s the puck, so camera and puck never diverge.
1496
+ */
1497
+ attachPuck(puck) {
1498
+ this.puck = puck;
1499
+ }
1500
+ /**
1501
+ * Glide the camera to a fix, course-up when `courseDeg` is given
1502
+ * (bearing is left unchanged when it is `undefined`). Each call eases
1503
+ * with linear timing over `easeMs` capped by the observed fix interval,
1504
+ * and interrupts the previous ease (no queue buildup). The fix and an
1505
+ * attached puck are always updated, but the camera only moves in
1506
+ * `"follow"` mode - `"free"` and `"overview"` record the fix for
1507
+ * {@link resume}.
1508
+ */
1509
+ follow(fix, courseDeg) {
1510
+ const now = Date.now();
1511
+ const interval = this.lastFixAt === void 0 ? this.easeMs : Math.max(0, now - this.lastFixAt);
1512
+ this.lastFixAt = now;
1513
+ this.lastFix = fix;
1514
+ if (courseDeg !== void 0) this.lastCourse = courseDeg;
1515
+ this.puck?.setLocation(fix, courseDeg);
1516
+ if (this.currentMode !== "follow") return;
1517
+ this.easeToFix(fix, courseDeg, Math.min(this.easeMs, interval));
1518
+ }
1519
+ /**
1520
+ * Fit the route top-down (pitch 0, north-up). Pass the route geometry on
1521
+ * the first call; later calls reuse the last geometry given. A no-op
1522
+ * when no geometry has ever been provided.
1523
+ */
1524
+ overview(routeGeometry) {
1525
+ if (routeGeometry) this.lastRoute = routeGeometry;
1526
+ const route = this.lastRoute;
1527
+ if (!route || route.coordinates.length === 0) return;
1528
+ this.clearRecentreTimer();
1529
+ this.currentMode = "overview";
1530
+ this.m.fitBounds(boundsOf2(route.coordinates), {
1531
+ padding: OVERVIEW_PADDING_PX,
1532
+ pitch: 0,
1533
+ bearing: 0,
1534
+ duration: this.easeMs,
1535
+ essential: true
1536
+ });
1537
+ }
1538
+ /**
1539
+ * Return to follow mode, gliding back to the last fix (with its last
1540
+ * known course) when one has been recorded.
1541
+ */
1542
+ resume() {
1543
+ this.clearRecentreTimer();
1544
+ this.currentMode = "follow";
1545
+ if (this.lastFix) {
1546
+ this.easeToFix(this.lastFix, this.lastCourse, this.easeMs);
1547
+ }
1548
+ }
1549
+ /** Remove the map listeners and cancel any pending auto-recentre. */
1550
+ destroy() {
1551
+ this.destroyed = true;
1552
+ this.clearRecentreTimer();
1553
+ for (const event of INTERACTION_EVENTS) {
1554
+ this.m.off(event, this.onInteraction);
1555
+ }
1556
+ }
1557
+ easeToFix(fix, courseDeg, duration) {
1558
+ this.m.easeTo({
1559
+ center: [fix.lon, fix.lat],
1560
+ ...courseDeg !== void 0 ? { bearing: courseDeg } : {},
1561
+ pitch: this.pitch,
1562
+ zoom: this.zoom,
1563
+ offset: [0, this.anchorOffsetY()],
1564
+ duration,
1565
+ easing: (t) => t,
1566
+ // Interrupt semantics: `essential` easings run even under
1567
+ // prefers-reduced-motion, and each new easeTo cancels the previous
1568
+ // one, so a fast fix cadence glides instead of queueing.
1569
+ essential: true
1570
+ });
1571
+ }
1572
+ /**
1573
+ * The pixel offset that renders the followed fix at `anchorY` of the
1574
+ * container height: `(anchorY - 0.5) * height` (0.5 is the natural
1575
+ * centre; positive y moves the puck down the screen).
1576
+ */
1577
+ anchorOffsetY() {
1578
+ const height = this.m.getContainer().clientHeight;
1579
+ return (this.anchorY - 0.5) * height;
1580
+ }
1581
+ scheduleRecentre() {
1582
+ this.clearRecentreTimer();
1583
+ if (this.autoRecentreMs === 0) return;
1584
+ this.recentreTimer = setTimeout(() => {
1585
+ this.recentreTimer = void 0;
1586
+ if (this.currentMode === "free") this.resume();
1587
+ }, this.autoRecentreMs);
1588
+ }
1589
+ clearRecentreTimer() {
1590
+ if (this.recentreTimer !== void 0) {
1591
+ clearTimeout(this.recentreTimer);
1592
+ this.recentreTimer = void 0;
1593
+ }
1594
+ }
1595
+ };
1596
+ function boundsOf2(coordinates) {
1597
+ let minLng = Infinity;
1598
+ let minLat = Infinity;
1599
+ let maxLng = -Infinity;
1600
+ let maxLat = -Infinity;
1601
+ for (const [lng, lat] of coordinates) {
1602
+ if (lng < minLng) minLng = lng;
1603
+ if (lng > maxLng) maxLng = lng;
1604
+ if (lat < minLat) minLat = lat;
1605
+ if (lat > maxLat) maxLat = lat;
1606
+ }
1607
+ return [
1608
+ [minLng, minLat],
1609
+ [maxLng, maxLat]
1610
+ ];
1611
+ }
1612
+ function usesGlobe(projection) {
1613
+ if (projection === null || projection === void 0) return false;
1614
+ if (typeof projection === "string") {
1615
+ return projection === "globe" || projection === "vertical-perspective";
1616
+ }
1617
+ if (Array.isArray(projection)) return projection.some(usesGlobe);
1618
+ if (typeof projection === "object") {
1619
+ return Object.values(projection).some(usesGlobe);
1620
+ }
1621
+ return false;
1622
+ }
1623
+
1624
+ // src/adr.ts
1625
+ function buildAdrCheckBody(request) {
1626
+ const dims = request.dimensions ?? {};
1627
+ const adr = {
1628
+ dimensions: {
1629
+ height_m: dims.heightM ?? 4,
1630
+ width_m: dims.widthM ?? 2.55,
1631
+ length_m: dims.lengthM ?? 16.5,
1632
+ gross_weight_t: dims.grossWeightT ?? 40,
1633
+ axle_load_t: dims.axleLoadT ?? null,
1634
+ axle_count: dims.axleCount ?? null
1635
+ },
1636
+ hazmat: request.hazmat
1637
+ };
1638
+ if (request.tunnelCode !== void 0) adr["tunnel_code"] = request.tunnelCode;
1639
+ return { adr, tunnel_category: request.tunnelCategory };
1640
+ }
1641
+ var AdrCheck = class {
1642
+ constructor(options) {
1643
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1644
+ this.apiKey = options.apiKey;
1645
+ }
1646
+ async check(request) {
1647
+ const headers = {
1648
+ "Content-Type": "application/json",
1649
+ Accept: "application/json"
1650
+ };
1651
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
1652
+ const response = await fetch(`${this.baseUrl}/adr/check`, {
1653
+ method: "POST",
1654
+ headers,
1655
+ body: JSON.stringify(buildAdrCheckBody(request))
1656
+ });
1657
+ const body = await response.json().catch(() => null);
1658
+ if (!response.ok) {
1659
+ const problem = typeof body === "object" && body !== null ? body : void 0;
1660
+ const context = [problem?.["title"], problem?.["detail"]].filter((v) => typeof v === "string" && v.length > 0).join(" - ");
1661
+ throw new Error(
1662
+ `ADR check failed: HTTP ${response.status}${context ? ` (${context})` : ""}`
1663
+ );
1664
+ }
1665
+ if (typeof body !== "object" || body === null) {
1666
+ throw new Error("ADR check returned a non-object response");
1667
+ }
1668
+ const obj = body;
1669
+ const result = {
1670
+ status: typeof obj["status"] === "string" ? obj["status"] : "unknown",
1671
+ raw: obj
1672
+ };
1673
+ if (typeof obj["reason"] === "string") result.reason = obj["reason"];
1674
+ return result;
1675
+ }
1676
+ };
1677
+
1678
+ // src/guidance.ts
1679
+ function extractGuidance(route) {
1680
+ const legs = route.raw["legs"] ?? [];
1681
+ const out = [];
1682
+ let index = 0;
1683
+ for (const leg of legs) {
1684
+ const steps = leg["steps"] ?? [];
1685
+ for (const step of steps) {
1686
+ const voice = step["voiceInstructions"] ?? [];
1687
+ const banners = step["bannerInstructions"] ?? [];
1688
+ out.push({
1689
+ stepIndex: index,
1690
+ distanceM: Number(step["distance"] ?? 0),
1691
+ instruction: stepInstruction(step, voice, banners),
1692
+ voice,
1693
+ banners
1694
+ });
1695
+ index += 1;
1696
+ }
1697
+ }
1698
+ return out;
1699
+ }
1700
+ function stepInstruction(step, voice, banners) {
1701
+ const maneuver = step["maneuver"];
1702
+ const fromManeuver = maneuver?.["instruction"];
1703
+ if (typeof fromManeuver === "string" && fromManeuver.length > 0) {
1704
+ return fromManeuver;
1705
+ }
1706
+ const lastVoice = voice[voice.length - 1];
1707
+ if (typeof lastVoice?.announcement === "string" && lastVoice.announcement.length > 0) {
1708
+ return lastVoice.announcement;
1709
+ }
1710
+ const bannerText = banners[0]?.primary?.text;
1711
+ if (typeof bannerText === "string" && bannerText.length > 0) {
1712
+ return bannerText;
1713
+ }
1714
+ return void 0;
1715
+ }
1716
+ var XML_ENTITIES = {
1717
+ "&amp;": "&",
1718
+ "&lt;": "<",
1719
+ "&gt;": ">",
1720
+ "&quot;": '"',
1721
+ "&apos;": "'"
1722
+ };
1723
+ function ssmlToText(ssml) {
1724
+ return ssml.replace(/<[^>]+>/g, "").replace(
1725
+ /&(?:amp|lt|gt|quot|apos);|&#(\d+);/g,
1726
+ (entity, decimal) => decimal !== void 0 ? String.fromCodePoint(Number(decimal)) : XML_ENTITIES[entity] ?? entity
1727
+ ).trim();
1728
+ }
1729
+ function bannerLanes(banner) {
1730
+ const components = banner.sub?.components ?? [];
1731
+ return components.filter((c) => c.type === "lane").map((c) => {
1732
+ const lane = { directions: c.directions ?? [] };
1733
+ if (c.active !== void 0) lane.active = c.active;
1734
+ if (c.active === true) lane.valid = true;
1735
+ return lane;
1736
+ });
1737
+ }
1738
+ function directionArrow(direction) {
1739
+ switch (direction) {
1740
+ case "left":
1741
+ return "\u2190";
1742
+ case "right":
1743
+ return "\u2192";
1744
+ case "straight":
1745
+ case "none":
1746
+ return "\u2191";
1747
+ case "slight left":
1748
+ return "\u2196";
1749
+ case "slight right":
1750
+ return "\u2197";
1751
+ case "sharp left":
1752
+ return "\u2199";
1753
+ case "sharp right":
1754
+ return "\u2198";
1755
+ case "uturn":
1756
+ return "\u21B6";
1757
+ case "merge to left":
1758
+ return "\u2B05";
1759
+ case "merge to right":
1760
+ return "\u27A1";
1761
+ default:
1762
+ return "\u2022";
1763
+ }
1764
+ }
1765
+ function speak(instruction, options = {}) {
1766
+ const synth = globalThis.speechSynthesis;
1767
+ const Utterance = globalThis.SpeechSynthesisUtterance;
1768
+ if (!synth || !Utterance) return false;
1769
+ const text = instruction.ssmlAnnouncement ? ssmlToText(instruction.ssmlAnnouncement) : instruction.announcement;
1770
+ const utterance = new Utterance(text);
1771
+ if (options.lang) utterance.lang = options.lang;
1772
+ if (options.rate) utterance.rate = options.rate;
1773
+ synth.speak(utterance);
1774
+ return true;
1775
+ }
1776
+ var GuidanceBanner = class {
1777
+ constructor(container, design) {
1778
+ const doc = globalThis.document;
1779
+ if (!doc) throw new Error("GuidanceBanner requires a DOM environment");
1780
+ this.design = design;
1781
+ this.element = doc.createElement("div");
1782
+ this.element.className = "mapmap-banner";
1783
+ this.element.style.cssText = design ? `display:none;align-items:center;box-sizing:border-box;gap:${Math.round((design.padding ?? 10) * 1.2)}px;padding:${design.padding ?? 10}px ${Math.round((design.padding ?? 10) * 1.4)}px;background:${design.background};color:${design.textColor};border-radius:${design.cornerRadius ?? 10}px;max-width:${design.maxWidth ?? 340}px;` + (design.height !== void 0 ? `min-height:${design.height}px;` : "") + `font:600 ${design.fontSize}px/1.3 system-ui,sans-serif` : "display:none;gap:12px;align-items:center;padding:10px 14px;background:#101418;color:#fff;border-radius:10px;font:600 16px/1.3 system-ui,sans-serif";
1784
+ container?.appendChild(this.element);
1785
+ }
1786
+ /** Renders a banner instruction (or hides the element with `null`). */
1787
+ update(banner) {
1788
+ if (!banner) {
1789
+ this.element.style.display = "none";
1790
+ this.element.replaceChildren();
1791
+ return;
1792
+ }
1793
+ const doc = this.element.ownerDocument;
1794
+ this.element.style.display = "flex";
1795
+ const arrow = doc.createElement("span");
1796
+ arrow.textContent = directionArrow(banner.primary.modifier ?? "straight");
1797
+ arrow.style.fontSize = this.design ? `${Math.round(this.design.fontSize * 1.4)}px` : "22px";
1798
+ const text = doc.createElement("span");
1799
+ text.textContent = banner.primary.text;
1800
+ const children = [arrow, text];
1801
+ const lanes = this.design?.showLanes === false ? [] : bannerLanes(banner);
1802
+ if (lanes.length > 0) {
1803
+ const laneRow = doc.createElement("span");
1804
+ laneRow.className = "mapmap-banner-lanes";
1805
+ if (this.design) {
1806
+ const spacing = Math.max(2, Math.round((this.design.padding ?? 10) * 0.4));
1807
+ laneRow.style.cssText = `margin-left:auto;letter-spacing:${spacing}px;white-space:nowrap`;
1808
+ for (const lane of lanes) {
1809
+ const glyph = doc.createElement("span");
1810
+ glyph.textContent = directionArrow(lane.directions[0] ?? "none");
1811
+ glyph.style.opacity = lane.active ? "0.95" : "0.35";
1812
+ laneRow.appendChild(glyph);
1813
+ }
1814
+ } else {
1815
+ laneRow.style.cssText = "margin-left:auto;letter-spacing:4px";
1816
+ laneRow.textContent = lanes.map((lane) => directionArrow(lane.directions[0] ?? "none")).join("");
1817
+ laneRow.style.opacity = "0.9";
1818
+ }
1819
+ children.push(laneRow);
1820
+ }
1821
+ this.element.replaceChildren(...children);
1822
+ }
1823
+ };
1824
+
1825
+ export { AdrCheck, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, FULL_ATTRIBUTION, GuidanceBanner, LOGO_SVG, LogoControl, MAX_PUCK_IMAGE_BYTES, MapMapMap, NAV_CAMERA_DEFAULTS, NavigationCamera, OPENMAPTILES_ATTRIBUTION, OSM_ATTRIBUTION, PALETTE_SLOTS, PlacesLayer, PositionPuck, RouteLayer, SIGNAL_BLUE, SOURCE_LAYERS, bannerLanes, buildAdrCheckBody, buildRouteQuery, buildRouteUrl, buildStyle, createMap, defaultNavDesign, directionArrow, extractGuidance, formatCoord, formatCoords, haversineDistanceM, navDesignFromTheme, navDesignFromThemeUrl, parseNavDesign, parseOsrmRoute, placesFromGeoJSON, registerPmtilesProtocol, speak, ssmlToText, toLngLat, toPmtilesUrl };
1826
+ //# sourceMappingURL=index.js.map
1827
+ //# sourceMappingURL=index.js.map