@diagrammo/dgmo 0.46.0 → 0.48.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/advanced.d.ts +3 -855
- package/dist/advanced.js +541 -70107
- package/dist/analyzer-UYKBXAKR.js +15 -0
- package/dist/auto.js +1 -1
- package/dist/auto.mjs +1 -1
- package/dist/block.js +83 -69417
- package/dist/calculator-LDYQCDDY.js +11 -0
- package/dist/charts-d3-QOZHKTCT.js +25 -0
- package/dist/chunk-2DXMZRWJ.js +800 -0
- package/dist/chunk-2LACINWN.js +786 -0
- package/dist/chunk-33LCUH4H.js +893 -0
- package/dist/chunk-3CIOJ5YJ.js +35 -0
- package/dist/chunk-3HDBAXF5.js +86 -0
- package/dist/chunk-3JLKYGBQ.js +290 -0
- package/dist/chunk-3MQQNXPD.js +415 -0
- package/dist/chunk-3R5POCTZ.js +967 -0
- package/dist/chunk-3SMGIJDD.js +112 -0
- package/dist/chunk-44CM72SQ.js +409 -0
- package/dist/chunk-4GRI7MEB.js +1604 -0
- package/dist/chunk-4OHACBGX.js +606 -0
- package/dist/chunk-5DH3KYFH.js +21 -0
- package/dist/chunk-5MLYVTYM.js +76 -0
- package/dist/chunk-5QXL4NYS.js +553 -0
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-6EK55ART.js +117 -0
- package/dist/chunk-6FZYCKXB.js +2065 -0
- package/dist/chunk-6HO5ZTXK.js +277 -0
- package/dist/chunk-6KFWC4CO.js +7 -0
- package/dist/chunk-6MTWIQVO.js +121 -0
- package/dist/chunk-6NYWYNBI.js +329 -0
- package/dist/chunk-6SM5NJQH.js +620 -0
- package/dist/chunk-725LDASD.js +422 -0
- package/dist/chunk-76OWZ3BA.js +130 -0
- package/{src/block/parser.ts → dist/chunk-7JEGTRLD.js} +100 -189
- package/dist/chunk-A44USUV7.js +923 -0
- package/dist/chunk-A4GOY3PE.js +25 -0
- package/dist/chunk-A4VFQH5G.js +251 -0
- package/dist/chunk-AELOBFW5.js +375 -0
- package/dist/chunk-AN2MOO5T.js +522 -0
- package/dist/chunk-BMR4CIEO.js +41 -0
- package/dist/chunk-BS7SV7C3.js +51 -0
- package/dist/chunk-BUPDIHML.js +613 -0
- package/dist/chunk-BUVJFRPY.js +887 -0
- package/dist/chunk-C3Z4DX3V.js +112 -0
- package/dist/chunk-CI2G3OEB.js +1818 -0
- package/dist/chunk-CKNVO24D.js +54 -0
- package/dist/chunk-CUVHQHZ7.js +391 -0
- package/dist/chunk-CXK4DZH3.js +114 -0
- package/dist/{editor.cjs → chunk-DKEPS5LH.js} +14 -225
- package/dist/chunk-DU7W62N5.js +487 -0
- package/dist/chunk-DZAQBW35.js +751 -0
- package/dist/chunk-EB6FPQUP.js +316 -0
- package/dist/chunk-EMS2SK26.js +376 -0
- package/dist/chunk-ERLBPDGU.js +510 -0
- package/dist/chunk-EVCVDTXE.js +732 -0
- package/dist/chunk-FCIIHT24.js +384 -0
- package/dist/chunk-FFUN5D66.js +97 -0
- package/dist/chunk-FITOLXSX.js +714 -0
- package/dist/chunk-FMDSF3OV.js +68 -0
- package/dist/chunk-FNB3PNGF.js +410 -0
- package/dist/chunk-G2H74XJZ.js +2104 -0
- package/dist/chunk-GMR5MFHJ.js +339 -0
- package/dist/chunk-H6PI3KJT.js +305 -0
- package/dist/chunk-I46X5VUA.js +59 -0
- package/dist/chunk-I5IGB56C.js +255 -0
- package/dist/chunk-IH6ZMVQ5.js +3950 -0
- package/dist/chunk-IJZFN3HM.js +463 -0
- package/dist/chunk-IPWPWM6R.js +3079 -0
- package/dist/chunk-IYULMRBE.js +23 -0
- package/dist/chunk-IZMRON6C.js +167 -0
- package/dist/chunk-J5FIXQRT.js +813 -0
- package/dist/chunk-JED6ZDEE.js +73 -0
- package/dist/chunk-JEUJOX5X.js +136 -0
- package/dist/chunk-JM4LEPY3.js +436 -0
- package/dist/chunk-L3UHP6TD.js +101 -0
- package/dist/chunk-L66GEFB6.js +524 -0
- package/dist/chunk-LIOHW36Z.js +469 -0
- package/dist/chunk-LP7EC4MI.js +16 -0
- package/dist/chunk-MITI6NOL.js +548 -0
- package/dist/chunk-MO5YHHMQ.js +168 -0
- package/dist/chunk-MZSBFAPJ.js +885 -0
- package/dist/chunk-NNAAJVBU.js +9 -0
- package/dist/chunk-NOTA6TEI.js +47 -0
- package/dist/chunk-NURGB5Z4.js +335 -0
- package/dist/chunk-NX4EQS3U.js +1062 -0
- package/dist/chunk-NXMESY4J.js +1 -0
- package/dist/chunk-OJR3MQYV.js +1458 -0
- package/dist/chunk-OURHMODR.js +454 -0
- package/dist/chunk-P235SVAH.js +1057 -0
- package/dist/chunk-P4RH4OSP.js +1038 -0
- package/dist/chunk-P6T66CCZ.js +226 -0
- package/dist/chunk-PBAHEPLQ.js +223 -0
- package/dist/chunk-PBHE5UG6.js +1530 -0
- package/dist/chunk-PDKUANDZ.js +35 -0
- package/dist/chunk-PWIHZ6NJ.js +296 -0
- package/{src/c4/layout.ts → dist/chunk-QEBUY5NY.js} +384 -1140
- package/dist/chunk-QLCCJEJI.js +253 -0
- package/dist/chunk-QOOUC2J5.js +52 -0
- package/dist/chunk-QYTPPDAO.js +402 -0
- package/dist/chunk-R3XI2VC7.js +801 -0
- package/dist/chunk-RCERWOJR.js +154 -0
- package/dist/chunk-RJE5JTVK.js +362 -0
- package/dist/chunk-RTLBSQ3G.js +1155 -0
- package/dist/chunk-RUNNI6VE.js +448 -0
- package/dist/chunk-RVMRZAPB.js +354 -0
- package/dist/chunk-RYEGFMUO.js +955 -0
- package/dist/chunk-RZCF2VVM.js +528 -0
- package/dist/chunk-SML67VPV.js +1025 -0
- package/dist/chunk-SQ7IY53Q.js +44 -0
- package/dist/chunk-SQAG3UXH.js +161 -0
- package/dist/chunk-SRTU6HMQ.js +1851 -0
- package/dist/chunk-T2OE3YQB.js +550 -0
- package/dist/chunk-T3UBLACL.js +299 -0
- package/dist/chunk-TLVIHFRO.js +296 -0
- package/dist/chunk-TYS6CSS5.js +52 -0
- package/dist/chunk-U6EQI3U6.js +130 -0
- package/dist/chunk-UJ4FJLBF.js +86 -0
- package/dist/chunk-ULOEATBZ.js +347 -0
- package/dist/chunk-UNCCZ3NU.js +127 -0
- package/dist/chunk-USZW2HHU.js +197 -0
- package/dist/chunk-UWN33VNX.js +1610 -0
- package/dist/chunk-UZ7EZW3B.js +40 -0
- package/dist/chunk-VOKGH627.js +37 -0
- package/dist/chunk-WBRZJOEC.js +204 -0
- package/dist/chunk-WCSOJFU3.js +380 -0
- package/dist/chunk-WCT3NYCV.js +160 -0
- package/dist/chunk-WVAJFAIA.js +529 -0
- package/dist/chunk-X5LCYCL5.js +394 -0
- package/dist/chunk-XAKEUMEJ.js +1256 -0
- package/dist/chunk-Y2ZE3MSG.js +590 -0
- package/dist/chunk-Y3J7VR52.js +646 -0
- package/dist/chunk-Y5QGVTVC.js +33 -0
- package/dist/chunk-YAA6IID3.js +548 -0
- package/dist/chunk-YGJTDXVR.js +692 -0
- package/{src/utils/time-ticks.ts → dist/chunk-YRYKTYMQ.js} +45 -112
- package/dist/chunk-YYVTBZZC.js +61 -0
- package/dist/chunk-Z5CGSFJB.js +647 -0
- package/{src/gantt/calculator.ts → dist/chunk-Z72XRXD4.js} +236 -428
- package/dist/chunk-ZGR7ULFJ.js +978 -0
- package/dist/chunk-ZJEL4H2R.js +851 -0
- package/dist/chunk-ZO27FGJS.js +554 -0
- package/dist/collapse-CLPJD2RL.js +11 -0
- package/dist/collapse-SLINJQW6.js +9 -0
- package/dist/collapse-ZWUFFCK2.js +9 -0
- package/dist/compute-G55RG53N.js +8 -0
- package/dist/dgmo-router-DZCIDUO5.js +73 -0
- package/dist/dimensions-J3FHIMJ6.js +22 -0
- package/dist/element.js +1 -1
- package/dist/element.mjs +1 -1
- package/dist/flowchart-parser-ZNSKIBVR.js +21 -0
- package/dist/flowchart-renderer-ZAEOL4DX.js +35 -0
- package/dist/index.d.ts +3 -469
- package/dist/index.js +137 -69685
- package/dist/layout-46N4JJYK.js +17 -0
- package/dist/layout-5RA6QWR6.js +26 -0
- package/dist/layout-5T6RH5M6.js +14 -0
- package/dist/layout-7QXCNOVS.js +13 -0
- package/dist/layout-EE6GE46Y.js +17 -0
- package/dist/layout-G23F7LDH.js +9 -0
- package/dist/layout-HRBZPFCN.js +23 -0
- package/dist/layout-HWDSRX5E.js +20 -0
- package/dist/layout-HYZPWAK5.js +18 -0
- package/dist/layout-N76NR4ZO.js +19 -0
- package/dist/layout-OB3RI63S.js +14 -0
- package/dist/layout-S2QBYSK2.js +15 -0
- package/dist/layout-UM5F4DJB.js +15 -0
- package/dist/layout-search-OYV77HHS.js +1655 -0
- package/dist/load-data-EHDDZ5ZS.js +8 -0
- package/dist/palettes-PRJ2RI6D.js +56 -0
- package/dist/parser-2MFP2H2E.js +11 -0
- package/dist/parser-3STE62GB.js +21 -0
- package/dist/parser-4FL2CMDG.js +18 -0
- package/dist/parser-4OGOBJJ4.js +13 -0
- package/dist/parser-6BTRLKRZ.js +15 -0
- package/dist/parser-AQETEVRW.js +21 -0
- package/dist/parser-CFZD5IBL.js +12 -0
- package/dist/parser-FUSKFGZN.js +16 -0
- package/dist/parser-I6KXEYFK.js +13 -0
- package/dist/parser-IXSZZQHY.js +18 -0
- package/dist/parser-JAL2HD4I.js +13 -0
- package/dist/parser-KNSQSKQO.js +20 -0
- package/dist/parser-N5O4IHUN.js +13 -0
- package/dist/parser-NGU3ETO6.js +16 -0
- package/dist/parser-NRAI6O25.js +19 -0
- package/dist/parser-O5J4VBII.js +11 -0
- package/dist/parser-OV7NOBW6.js +21 -0
- package/dist/parser-PQ3POX45.js +23 -0
- package/dist/parser-TFZFBVWN.js +11 -0
- package/dist/parser-TRTHDFPP.js +18 -0
- package/dist/parser-VQP2UJ6W.js +11 -0
- package/dist/parser-XDVELHRK.js +13 -0
- package/dist/parser-XKRP7QKP.js +11 -0
- package/dist/parser-Y6WTBU76.js +14 -0
- package/dist/parser-ZSDPZYQP.js +12 -0
- package/dist/renderer-2XQQQ6QR.js +28 -0
- package/dist/renderer-5NDVNDAZ.js +26 -0
- package/dist/renderer-6EEZHMH4.js +26 -0
- package/dist/renderer-6W5462RB.js +43 -0
- package/dist/renderer-7MVLWWQD.js +22 -0
- package/dist/renderer-AWWAF6XD.js +33 -0
- package/dist/renderer-BYCNMBF7.js +34 -0
- package/dist/renderer-CLJHQPBJ.js +29 -0
- package/dist/renderer-GHPZMFU2.js +15 -0
- package/dist/renderer-JGBKUCZD.js +16 -0
- package/dist/renderer-KQB7UGDS.js +19 -0
- package/dist/renderer-PGX2TKVS.js +24 -0
- package/dist/renderer-QIWVHWZO.js +15 -0
- package/dist/renderer-RPOA5YPZ.js +35 -0
- package/dist/renderer-SMIBCRVB.js +24 -0
- package/dist/renderer-T7YKC5VP.js +28 -0
- package/dist/renderer-TOCOVLHS.js +37 -0
- package/dist/renderer-UWA5XS74.js +39 -0
- package/dist/renderer-VACVQYML.js +25 -0
- package/dist/renderer-W4M6QLIJ.js +37 -0
- package/dist/renderer-WZRREQ2P.js +23 -0
- package/dist/renderer-YY2OSF6E.js +27 -0
- package/dist/renderer-Z7I3W2PP.js +24 -0
- package/dist/renderer-ZGNTUWC4.js +22 -0
- package/dist/renderer-ZTR3DER3.js +19 -0
- package/dist/renderer-radial-C32GPJQ7.js +21 -0
- package/dist/resolver-NOAA7ID7.js +13 -0
- package/dist/state-collapse-HCGD6IVJ.js +8 -0
- package/dist/state-parser-Z2V23ATR.js +18 -0
- package/dist/state-renderer-RFYHSP4K.js +34 -0
- package/dist/themes-1-CuKpeH.d.ts +908 -0
- package/package.json +19 -70
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -6571
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -626
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
|
@@ -0,0 +1,3079 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAdjacency,
|
|
3
|
+
featureBboxPrimary,
|
|
4
|
+
pointInGeometry
|
|
5
|
+
} from "./chunk-I5IGB56C.js";
|
|
6
|
+
import {
|
|
7
|
+
rectCircleOverlap,
|
|
8
|
+
rectsOverlap,
|
|
9
|
+
segmentRectOverlap,
|
|
10
|
+
segmentsCross
|
|
11
|
+
} from "./chunk-PBAHEPLQ.js";
|
|
12
|
+
import {
|
|
13
|
+
compactNumber,
|
|
14
|
+
computeLegendLayout
|
|
15
|
+
} from "./chunk-YAA6IID3.js";
|
|
16
|
+
import {
|
|
17
|
+
TITLE_FONT_SIZE,
|
|
18
|
+
TITLE_Y
|
|
19
|
+
} from "./chunk-A4GOY3PE.js";
|
|
20
|
+
import {
|
|
21
|
+
measureLegendText
|
|
22
|
+
} from "./chunk-QOOUC2J5.js";
|
|
23
|
+
import {
|
|
24
|
+
contrastRatio,
|
|
25
|
+
mix,
|
|
26
|
+
politicalTints,
|
|
27
|
+
relativeLuminance,
|
|
28
|
+
valueRampColor
|
|
29
|
+
} from "./chunk-P6T66CCZ.js";
|
|
30
|
+
import {
|
|
31
|
+
tagAttrKey
|
|
32
|
+
} from "./chunk-QLCCJEJI.js";
|
|
33
|
+
import {
|
|
34
|
+
resolveColor
|
|
35
|
+
} from "./chunk-TLVIHFRO.js";
|
|
36
|
+
|
|
37
|
+
// src/map/legend-band.ts
|
|
38
|
+
var LEGEND_TOP_GAP = 8;
|
|
39
|
+
var LEGEND_BOTTOM_GAP = 10;
|
|
40
|
+
function mapLegendGroups(legend) {
|
|
41
|
+
const ramp = legend.ramp;
|
|
42
|
+
const scoreGroup = ramp ? {
|
|
43
|
+
name: ramp.metric?.trim() || "Value",
|
|
44
|
+
entries: [],
|
|
45
|
+
gradient: {
|
|
46
|
+
min: ramp.min,
|
|
47
|
+
max: ramp.max,
|
|
48
|
+
low: ramp.low,
|
|
49
|
+
high: ramp.high
|
|
50
|
+
}
|
|
51
|
+
} : null;
|
|
52
|
+
const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
|
|
53
|
+
return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
|
|
54
|
+
}
|
|
55
|
+
function mapLegendConfig(groups, mode) {
|
|
56
|
+
return {
|
|
57
|
+
groups,
|
|
58
|
+
position: { placement: "top-center", titleRelation: "below-title" },
|
|
59
|
+
mode,
|
|
60
|
+
showEmptyGroups: false,
|
|
61
|
+
showInactivePills: true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function mapLegendTop(hasTitle, hasSubtitle) {
|
|
65
|
+
return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP;
|
|
66
|
+
}
|
|
67
|
+
function mapLegendBand(legend, opts) {
|
|
68
|
+
if (!legend) return 0;
|
|
69
|
+
const groups = mapLegendGroups(legend);
|
|
70
|
+
if (groups.length === 0) return 0;
|
|
71
|
+
const config = mapLegendConfig(groups, opts.mode);
|
|
72
|
+
const state = { activeGroup: legend.activeGroup };
|
|
73
|
+
const { height } = computeLegendLayout(config, state, opts.width);
|
|
74
|
+
if (height <= 0) return 0;
|
|
75
|
+
return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP;
|
|
76
|
+
}
|
|
77
|
+
function mapLegendBox(legend, opts) {
|
|
78
|
+
if (!legend) return null;
|
|
79
|
+
const groups = mapLegendGroups(legend);
|
|
80
|
+
if (groups.length === 0) return null;
|
|
81
|
+
const config = mapLegendConfig(groups, opts.mode);
|
|
82
|
+
const state = { activeGroup: legend.activeGroup };
|
|
83
|
+
const { width, height } = computeLegendLayout(config, state, opts.width);
|
|
84
|
+
if (height <= 0 || width <= 0) return null;
|
|
85
|
+
return {
|
|
86
|
+
x: (opts.width - width) / 2,
|
|
87
|
+
y: mapLegendTop(opts.hasTitle, opts.hasSubtitle),
|
|
88
|
+
width,
|
|
89
|
+
height
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/map/layout.ts
|
|
94
|
+
import {
|
|
95
|
+
geoPath,
|
|
96
|
+
geoNaturalEarth1,
|
|
97
|
+
geoEqualEarth,
|
|
98
|
+
geoEquirectangular,
|
|
99
|
+
geoConicEqualArea,
|
|
100
|
+
geoMercator,
|
|
101
|
+
geoBounds,
|
|
102
|
+
geoTransform
|
|
103
|
+
} from "d3-geo";
|
|
104
|
+
import { feature } from "topojson-client";
|
|
105
|
+
|
|
106
|
+
// src/map/colorize.ts
|
|
107
|
+
function assignColors(isos, adjacency) {
|
|
108
|
+
const sorted = [...isos].sort();
|
|
109
|
+
const byIso = /* @__PURE__ */ new Map();
|
|
110
|
+
let maxIndex = -1;
|
|
111
|
+
for (const iso of sorted) {
|
|
112
|
+
const taken = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const n of adjacency.get(iso) ?? []) {
|
|
114
|
+
const c = byIso.get(n);
|
|
115
|
+
if (c !== void 0) taken.add(c);
|
|
116
|
+
}
|
|
117
|
+
let h = 0;
|
|
118
|
+
while (taken.has(h)) h++;
|
|
119
|
+
byIso.set(iso, h);
|
|
120
|
+
if (h > maxIndex) maxIndex = h;
|
|
121
|
+
}
|
|
122
|
+
return { byIso, huesNeeded: maxIndex + 1 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/map/context-labels.ts
|
|
126
|
+
var FONT = 11;
|
|
127
|
+
var LINE_HEIGHT = FONT + 2;
|
|
128
|
+
var PADX = 4;
|
|
129
|
+
var PADY = 3;
|
|
130
|
+
var WATER_LETTER_SPACING = 1.5;
|
|
131
|
+
var CONTEXT_PAD = 4;
|
|
132
|
+
var EDGE_CLAMP_MARGIN = 8;
|
|
133
|
+
var EDGE_CLAMP_OVERSHOOT = 0.35;
|
|
134
|
+
var COUNTRY_FONT_MAX = 22;
|
|
135
|
+
var COUNTRY_SIZE_FRAC_MIN = 0.06;
|
|
136
|
+
var COUNTRY_SIZE_FRAC_MAX = 0.32;
|
|
137
|
+
var MAX_COUNTRY_POSITIONS = 4;
|
|
138
|
+
var COUNTRY_POS_GRID = 5;
|
|
139
|
+
var COUNTRY_POS_TOPN_MARGIN = 3;
|
|
140
|
+
var KIND_ORDER = {
|
|
141
|
+
ocean: 0,
|
|
142
|
+
sea: 1,
|
|
143
|
+
gulf: 2,
|
|
144
|
+
bay: 3,
|
|
145
|
+
strait: 4,
|
|
146
|
+
channel: 5,
|
|
147
|
+
sound: 6
|
|
148
|
+
};
|
|
149
|
+
function tierBand(maxSpanDeg) {
|
|
150
|
+
if (maxSpanDeg >= 90) return "world";
|
|
151
|
+
if (maxSpanDeg >= 20) return "continental";
|
|
152
|
+
if (maxSpanDeg >= 5) return "regional";
|
|
153
|
+
return "local";
|
|
154
|
+
}
|
|
155
|
+
function labelBudget(width, height, band) {
|
|
156
|
+
const bandCap = {
|
|
157
|
+
world: 10,
|
|
158
|
+
continental: 9,
|
|
159
|
+
regional: 7,
|
|
160
|
+
local: 6
|
|
161
|
+
};
|
|
162
|
+
const area = Math.floor(Math.sqrt(Math.max(0, width * height)) / 105);
|
|
163
|
+
return Math.max(0, Math.min(area, bandCap[band]));
|
|
164
|
+
}
|
|
165
|
+
function waterEligible(tier, kind, band) {
|
|
166
|
+
switch (band) {
|
|
167
|
+
case "world":
|
|
168
|
+
return tier <= 1 && (kind === "ocean" || kind === "sea");
|
|
169
|
+
case "continental":
|
|
170
|
+
return tier <= 2;
|
|
171
|
+
case "regional":
|
|
172
|
+
return tier <= 3;
|
|
173
|
+
case "local":
|
|
174
|
+
return tier <= 4;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function insideViewport(p, width, height) {
|
|
178
|
+
return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
|
|
179
|
+
}
|
|
180
|
+
function labelWidth(text, letterSpacing, font = FONT) {
|
|
181
|
+
const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
|
|
182
|
+
return measureLegendText(text, font) + spacing + 2 * PADX;
|
|
183
|
+
}
|
|
184
|
+
function wrapLabel(text, letterSpacing) {
|
|
185
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
186
|
+
if (words.length <= 1) return [text];
|
|
187
|
+
const maxLines = words.length >= 4 ? 3 : 2;
|
|
188
|
+
const n = words.length;
|
|
189
|
+
let best = null;
|
|
190
|
+
for (let mask = 0; mask < 1 << n - 1; mask++) {
|
|
191
|
+
const lines = [];
|
|
192
|
+
let cur = [words[0]];
|
|
193
|
+
for (let i = 1; i < n; i++) {
|
|
194
|
+
if (mask & 1 << i - 1) {
|
|
195
|
+
lines.push(cur.join(" "));
|
|
196
|
+
cur = [words[i]];
|
|
197
|
+
} else cur.push(words[i]);
|
|
198
|
+
}
|
|
199
|
+
lines.push(cur.join(" "));
|
|
200
|
+
if (lines.length > maxLines) continue;
|
|
201
|
+
const cost = Math.round(
|
|
202
|
+
Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
|
|
203
|
+
);
|
|
204
|
+
const head = labelWidth(lines[0], letterSpacing);
|
|
205
|
+
if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
|
|
206
|
+
best = { lines, cost, head };
|
|
207
|
+
}
|
|
208
|
+
return best?.lines ?? [text];
|
|
209
|
+
}
|
|
210
|
+
function rectAround(cx, cy, lines, letterSpacing, font = FONT) {
|
|
211
|
+
const lineHeight = font + 2;
|
|
212
|
+
const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing, font)));
|
|
213
|
+
const h = (lines.length - 1) * lineHeight + font + 2 * PADY;
|
|
214
|
+
return { x: cx - w / 2, y: cy - h / 2, w, h };
|
|
215
|
+
}
|
|
216
|
+
function rectFits(r, width, height) {
|
|
217
|
+
return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
|
|
218
|
+
}
|
|
219
|
+
function overlapsPadded(a, b, pad) {
|
|
220
|
+
return a.x - pad < b.x + b.w && a.x + a.w + pad > b.x && a.y - pad < b.y + b.h && a.y + a.h + pad > b.y;
|
|
221
|
+
}
|
|
222
|
+
function placeContextLabels(args) {
|
|
223
|
+
const {
|
|
224
|
+
projection,
|
|
225
|
+
dLonSpan,
|
|
226
|
+
dLatSpan,
|
|
227
|
+
width,
|
|
228
|
+
height,
|
|
229
|
+
waterBodies,
|
|
230
|
+
countries,
|
|
231
|
+
palette,
|
|
232
|
+
countryTone,
|
|
233
|
+
waterTone,
|
|
234
|
+
project,
|
|
235
|
+
collides,
|
|
236
|
+
overLand
|
|
237
|
+
} = args;
|
|
238
|
+
void projection;
|
|
239
|
+
const band = tierBand(Math.max(dLonSpan, dLatSpan));
|
|
240
|
+
const budget = labelBudget(width, height, band);
|
|
241
|
+
if (budget <= 0) return [];
|
|
242
|
+
const waterColor = waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
|
|
243
|
+
const countryColor = countryTone ?? palette.textMuted;
|
|
244
|
+
const haloColor = palette.bg;
|
|
245
|
+
const candidates = [];
|
|
246
|
+
const center = [width / 2, height / 2];
|
|
247
|
+
for (const e of waterBodies?.entries ?? []) {
|
|
248
|
+
const [lat, lon, name, tier, kind, alt] = e;
|
|
249
|
+
if (!waterEligible(tier, kind, band)) continue;
|
|
250
|
+
const wlines = wrapLabel(name, WATER_LETTER_SPACING);
|
|
251
|
+
const anchorsLngLat = [[lon, lat]];
|
|
252
|
+
for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
|
|
253
|
+
let best = null;
|
|
254
|
+
let bestD = Infinity;
|
|
255
|
+
let nearestProj = null;
|
|
256
|
+
let nearestProjD = Infinity;
|
|
257
|
+
for (const [aLon, aLat] of anchorsLngLat) {
|
|
258
|
+
const p = project(aLon, aLat);
|
|
259
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
260
|
+
const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
|
|
261
|
+
if (d < nearestProjD) {
|
|
262
|
+
nearestProjD = d;
|
|
263
|
+
nearestProj = p;
|
|
264
|
+
}
|
|
265
|
+
if (!insideViewport(p, width, height)) continue;
|
|
266
|
+
if (d < bestD) {
|
|
267
|
+
bestD = d;
|
|
268
|
+
best = p;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!best && tier === 0 && nearestProj) {
|
|
272
|
+
const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
|
|
273
|
+
const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
|
|
274
|
+
if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
|
|
275
|
+
const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
|
|
276
|
+
const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
|
|
277
|
+
const m = EDGE_CLAMP_MARGIN;
|
|
278
|
+
best = [
|
|
279
|
+
Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
|
|
280
|
+
Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!best) continue;
|
|
285
|
+
candidates.push({
|
|
286
|
+
text: name,
|
|
287
|
+
lines: wlines,
|
|
288
|
+
cx: best[0],
|
|
289
|
+
cy: best[1],
|
|
290
|
+
italic: true,
|
|
291
|
+
letterSpacing: WATER_LETTER_SPACING,
|
|
292
|
+
color: waterColor,
|
|
293
|
+
fontSize: FONT,
|
|
294
|
+
// water names keep the base font (no footprint to scale on)
|
|
295
|
+
// Orientation-value bands (lower = earlier): MAJOR water (oceans + major
|
|
296
|
+
// seas, tier ≤ 1) leads at `tier*10+kind` (0..~16); MINOR water (tier ≥ 2:
|
|
297
|
+
// smaller seas, bays, gulfs, straits) drops to band 2 (2000+) — BELOW the
|
|
298
|
+
// country band (1000+), so a big country outranks a minor basin.
|
|
299
|
+
sort: (tier <= 1 ? 0 : 2e3) + tier * 10 + KIND_ORDER[kind]
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const ranked = countries.map((c) => {
|
|
303
|
+
const [x0, y0, x1, y1] = c.bbox;
|
|
304
|
+
const w = x1 - x0;
|
|
305
|
+
const h = y1 - y0;
|
|
306
|
+
return { c, w, h, area: w * h };
|
|
307
|
+
}).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
|
|
308
|
+
const canvasLinear = Math.sqrt(Math.max(1, width * height));
|
|
309
|
+
let ci = 0;
|
|
310
|
+
for (const r of ranked) {
|
|
311
|
+
const { c, w, h, area } = r;
|
|
312
|
+
if (!c.curatedAnchor && w > width * 0.66 && h > height * 0.66) continue;
|
|
313
|
+
if (!insideViewport(c.anchor, width, height)) continue;
|
|
314
|
+
const sizeFrac = Math.sqrt(area) / canvasLinear;
|
|
315
|
+
const t = Math.min(
|
|
316
|
+
1,
|
|
317
|
+
Math.max(
|
|
318
|
+
0,
|
|
319
|
+
(sizeFrac - COUNTRY_SIZE_FRAC_MIN) / (COUNTRY_SIZE_FRAC_MAX - COUNTRY_SIZE_FRAC_MIN)
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
|
|
323
|
+
const color = countryColor;
|
|
324
|
+
const text = c.name;
|
|
325
|
+
const tw = labelWidth(text, 0, fontSize);
|
|
326
|
+
if (tw > w || fontSize + 2 * PADY > h) continue;
|
|
327
|
+
candidates.push({
|
|
328
|
+
text,
|
|
329
|
+
lines: [text],
|
|
330
|
+
cx: c.anchor[0],
|
|
331
|
+
cy: c.anchor[1],
|
|
332
|
+
italic: false,
|
|
333
|
+
letterSpacing: 0,
|
|
334
|
+
color,
|
|
335
|
+
fontSize,
|
|
336
|
+
...c.bordering ? { bordering: true } : {},
|
|
337
|
+
// Multi-position dodging: carry the ordered interior positions through to the
|
|
338
|
+
// commit loop. Invariant anchor === positions[0], so `cx/cy` is positions[0].
|
|
339
|
+
...c.positions ? { positions: c.positions } : {},
|
|
340
|
+
// Band 1 (orientation-value ranking): above MINOR water (band 2, 2000+) but
|
|
341
|
+
// below MAJOR water — oceans + major seas (band 0, ≤~16). So a big country
|
|
342
|
+
// (US, Canada, Russia) outranks a minor sea/bay (Sargasso, Bahía de
|
|
343
|
+
// Campeche) yet never displaces an ocean. Larger area = earlier within the
|
|
344
|
+
// band (`ci` is the area-desc rank index).
|
|
345
|
+
sort: 1e3 + ci++
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
candidates.sort((a, b) => a.sort - b.sort);
|
|
349
|
+
const placed = [];
|
|
350
|
+
const placedRects = [];
|
|
351
|
+
const stateCount = candidates.reduce(
|
|
352
|
+
(n, c) => n + (!c.italic && !c.bordering ? 1 : 0),
|
|
353
|
+
0
|
|
354
|
+
);
|
|
355
|
+
const waterCap = budget - Math.min(2, stateCount);
|
|
356
|
+
let waterPlaced = 0;
|
|
357
|
+
let budgetUsed = 0;
|
|
358
|
+
const gateAt = (cx, cy, cand) => {
|
|
359
|
+
const rect = rectAround(
|
|
360
|
+
cx,
|
|
361
|
+
cy,
|
|
362
|
+
cand.lines,
|
|
363
|
+
cand.letterSpacing,
|
|
364
|
+
cand.fontSize
|
|
365
|
+
);
|
|
366
|
+
if (!rectFits(rect, width, height)) return null;
|
|
367
|
+
if (cand.italic && overLand) {
|
|
368
|
+
const inset = 2;
|
|
369
|
+
const top = cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
|
|
370
|
+
const touchesLand = cand.lines.some((line, li) => {
|
|
371
|
+
const lw = labelWidth(line, cand.letterSpacing);
|
|
372
|
+
const x0 = cx - lw / 2 + inset;
|
|
373
|
+
const x1 = cx + lw / 2 - inset;
|
|
374
|
+
const xs = [x0, (x0 + cx) / 2, cx, (cx + x1) / 2, x1];
|
|
375
|
+
const base = top + li * LINE_HEIGHT;
|
|
376
|
+
return [base, base - FONT * 0.4, base - FONT * 0.8].some(
|
|
377
|
+
(y) => xs.some((x) => overLand(x, y))
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
if (touchesLand) return null;
|
|
381
|
+
}
|
|
382
|
+
if (collides(rect)) return null;
|
|
383
|
+
if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD)))
|
|
384
|
+
return null;
|
|
385
|
+
return rect;
|
|
386
|
+
};
|
|
387
|
+
const tryPlace = (cand) => {
|
|
388
|
+
const positions = cand.positions ?? [[cand.cx, cand.cy]];
|
|
389
|
+
for (const [px, py] of positions) {
|
|
390
|
+
const rect = gateAt(px, py, cand);
|
|
391
|
+
if (!rect) continue;
|
|
392
|
+
placedRects.push(rect);
|
|
393
|
+
placed.push({
|
|
394
|
+
x: px,
|
|
395
|
+
y: py,
|
|
396
|
+
text: cand.text,
|
|
397
|
+
anchor: "middle",
|
|
398
|
+
color: cand.color,
|
|
399
|
+
// No halo: the bg-coloured outline reads as a ghost box behind the text
|
|
400
|
+
// over the tinted water/land. Context labels are muted enough to sit
|
|
401
|
+
// cleanly on the basemap without one.
|
|
402
|
+
halo: false,
|
|
403
|
+
haloColor,
|
|
404
|
+
fontSize: cand.fontSize,
|
|
405
|
+
italic: cand.italic,
|
|
406
|
+
letterSpacing: cand.letterSpacing,
|
|
407
|
+
...cand.lines.length > 1 ? { lines: cand.lines } : {},
|
|
408
|
+
lineNumber: 0
|
|
409
|
+
});
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
};
|
|
414
|
+
for (const cand of candidates) if (cand.bordering) tryPlace(cand);
|
|
415
|
+
for (const cand of candidates) {
|
|
416
|
+
if (cand.bordering) continue;
|
|
417
|
+
if (budgetUsed >= budget) break;
|
|
418
|
+
if (cand.italic && waterPlaced >= waterCap) continue;
|
|
419
|
+
if (tryPlace(cand)) {
|
|
420
|
+
budgetUsed++;
|
|
421
|
+
if (cand.italic) waterPlaced++;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return placed;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/map/layout.ts
|
|
428
|
+
var FIT_PAD = 24;
|
|
429
|
+
var PATH_DIGITS = 1;
|
|
430
|
+
var THIN_TOL = 0.6;
|
|
431
|
+
var ALBERS_SKEW_MAX = 1.25;
|
|
432
|
+
function geoThin() {
|
|
433
|
+
const tol2 = THIN_TOL * THIN_TOL;
|
|
434
|
+
return geoTransform({
|
|
435
|
+
lineStart() {
|
|
436
|
+
const t = this;
|
|
437
|
+
t._has = false;
|
|
438
|
+
t._pending = false;
|
|
439
|
+
t.stream.lineStart();
|
|
440
|
+
},
|
|
441
|
+
point(x, y) {
|
|
442
|
+
const t = this;
|
|
443
|
+
t._lx = x;
|
|
444
|
+
t._ly = y;
|
|
445
|
+
if (t._has) {
|
|
446
|
+
const dx = x - t._ex;
|
|
447
|
+
const dy = y - t._ey;
|
|
448
|
+
if (dx * dx + dy * dy < tol2) {
|
|
449
|
+
t._pending = true;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
t.stream.point(x, y);
|
|
454
|
+
t._ex = x;
|
|
455
|
+
t._ey = y;
|
|
456
|
+
t._has = true;
|
|
457
|
+
t._pending = false;
|
|
458
|
+
},
|
|
459
|
+
lineEnd() {
|
|
460
|
+
const t = this;
|
|
461
|
+
if (t._pending) t.stream.point(t._lx, t._ly);
|
|
462
|
+
t._pending = false;
|
|
463
|
+
t.stream.lineEnd();
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
var RAMP_FLOOR = 15;
|
|
468
|
+
var R_DEFAULT = 6;
|
|
469
|
+
var R_MIN = 4;
|
|
470
|
+
var R_MAX = 22;
|
|
471
|
+
var POI_FILL_OPACITY_MAX = 0.92;
|
|
472
|
+
var POI_FILL_OPACITY_MIN = 0.55;
|
|
473
|
+
var W_MIN = 1.25;
|
|
474
|
+
var W_MAX = 8;
|
|
475
|
+
var FONT2 = 11;
|
|
476
|
+
var WORLD_LABEL_ANCHORS = {
|
|
477
|
+
US: [-98.5, 39.5],
|
|
478
|
+
// CONUS geographic centre (near Lebanon, Kansas)
|
|
479
|
+
// Russia crosses the antimeridian (Chukotka at ~170°W), so on a non-global
|
|
480
|
+
// (e.g. Europe) projection its geometry smears across the whole frame and the
|
|
481
|
+
// area-weighted centroid lands mid-map (over Europe) — useless as a label
|
|
482
|
+
// anchor. Pin it to European Russia (~Volga) so a Europe view labels visible
|
|
483
|
+
// western Russia on its eastern margin; on a world view this still sits over
|
|
484
|
+
// Russian land. (See the curated-anchor smear-gate bypass in context-labels.)
|
|
485
|
+
RU: [45, 58]
|
|
486
|
+
};
|
|
487
|
+
var MAX_CLUSTER_EXTENT_FACTOR = 0.18;
|
|
488
|
+
var MAX_COLUMN_ROWS = 7;
|
|
489
|
+
var REGION_LABEL_HALO_RATIO = 4.5;
|
|
490
|
+
var LAND_TINT_LIGHT = 12;
|
|
491
|
+
var LAND_TINT_DARK = 24;
|
|
492
|
+
var TAG_TINT_LIGHT = 60;
|
|
493
|
+
var TAG_TINT_DARK = 68;
|
|
494
|
+
var WATER_TINT_LIGHT = 24;
|
|
495
|
+
var WATER_TINT_DARK = 24;
|
|
496
|
+
var RIVER_WIDTH = 1.3;
|
|
497
|
+
var COMPACT_WIDTH_PX = 480;
|
|
498
|
+
var RELIEF_MIN_AREA = 12;
|
|
499
|
+
var RELIEF_MIN_DIM = 2;
|
|
500
|
+
var RELIEF_HATCH_SPACING = 1.5;
|
|
501
|
+
var RELIEF_HATCH_WIDTH = 0.2;
|
|
502
|
+
var RELIEF_HATCH_STRENGTH = 26;
|
|
503
|
+
var COASTLINE_RING_COUNT = 5;
|
|
504
|
+
var COASTLINE_D0 = 16e-4;
|
|
505
|
+
var COASTLINE_STEP = 28e-4;
|
|
506
|
+
var COASTLINE_THICKNESS = 14e-4;
|
|
507
|
+
var COASTLINE_OPACITY_NEAR = 0.5;
|
|
508
|
+
var COASTLINE_OPACITY_FAR = 0.1;
|
|
509
|
+
var COASTLINE_MIN_EXTENT = 6e-4;
|
|
510
|
+
var COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
|
|
511
|
+
var COASTLINE_STROKE_MIX = 32;
|
|
512
|
+
var FOREIGN_TINT_LIGHT = 30;
|
|
513
|
+
var FOREIGN_TINT_DARK = 62;
|
|
514
|
+
var MUTED_FOREIGN_LIGHT = 28;
|
|
515
|
+
var MUTED_FOREIGN_DARK = 16;
|
|
516
|
+
var COLO_R = 9;
|
|
517
|
+
var GOLDEN_ANGLE = 2.399963229728653;
|
|
518
|
+
var STACK_OVERLAP = 1;
|
|
519
|
+
var STACK_RING_MAX = 8;
|
|
520
|
+
var STACK_RING_GAP = 4;
|
|
521
|
+
var FAN_STEP = 16;
|
|
522
|
+
var ARC_CURVE_FRAC = 0.18;
|
|
523
|
+
function geomObject(topo) {
|
|
524
|
+
const key = Object.keys(topo.objects)[0];
|
|
525
|
+
return topo.objects[key];
|
|
526
|
+
}
|
|
527
|
+
var decodeCache = /* @__PURE__ */ new WeakMap();
|
|
528
|
+
function mergeFeatures(a, b) {
|
|
529
|
+
const polysOf = (f) => {
|
|
530
|
+
const g = f.geometry;
|
|
531
|
+
if (!g) return null;
|
|
532
|
+
if (g.type === "Polygon") return [g.coordinates];
|
|
533
|
+
if (g.type === "MultiPolygon") return g.coordinates;
|
|
534
|
+
return null;
|
|
535
|
+
};
|
|
536
|
+
const pa = polysOf(a);
|
|
537
|
+
const pb = polysOf(b);
|
|
538
|
+
if (!pa || !pb) return a;
|
|
539
|
+
return {
|
|
540
|
+
...a,
|
|
541
|
+
geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function decodeLayer(topo) {
|
|
545
|
+
const cached = decodeCache.get(topo);
|
|
546
|
+
if (cached) return cached;
|
|
547
|
+
const out = /* @__PURE__ */ new Map();
|
|
548
|
+
for (const g of geomObject(topo).geometries) {
|
|
549
|
+
const f = feature(topo, g);
|
|
550
|
+
if (!f.geometry) continue;
|
|
551
|
+
const tagged = { ...f, id: g.id };
|
|
552
|
+
const existing = out.get(g.id);
|
|
553
|
+
out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
|
|
554
|
+
}
|
|
555
|
+
decodeCache.set(topo, out);
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
function countryLabelPositions(args) {
|
|
559
|
+
const { geometry, bounds, project, width, height, curated } = args;
|
|
560
|
+
const w0 = bounds[0][0];
|
|
561
|
+
const s0 = bounds[0][1];
|
|
562
|
+
const e0 = bounds[1][0];
|
|
563
|
+
const n0 = bounds[1][1];
|
|
564
|
+
if (![w0, s0, e0, n0].every(Number.isFinite) || e0 <= w0 || n0 <= s0) {
|
|
565
|
+
return mkCurated(curated, project);
|
|
566
|
+
}
|
|
567
|
+
const N = COUNTRY_POS_GRID;
|
|
568
|
+
const onLand = [];
|
|
569
|
+
const kept = [];
|
|
570
|
+
for (let i = 0; i < N; i++) {
|
|
571
|
+
onLand[i] = [];
|
|
572
|
+
const lon = w0 + (i + 0.5) / N * (e0 - w0);
|
|
573
|
+
for (let j = 0; j < N; j++) {
|
|
574
|
+
const lat = s0 + (j + 0.5) / N * (n0 - s0);
|
|
575
|
+
const land = pointInGeometry(geometry, lon, lat);
|
|
576
|
+
onLand[i][j] = land;
|
|
577
|
+
if (!land) continue;
|
|
578
|
+
const p = project(lon, lat);
|
|
579
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
580
|
+
if (p[0] < 0 || p[0] > width || p[1] < 0 || p[1] > height) continue;
|
|
581
|
+
kept.push({ i, j, lon, lat, sx: p[0], sy: p[1] });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (!kept.length) return mkCurated(curated, project);
|
|
585
|
+
const cx = kept.reduce((s, c) => s + c.sx, 0) / kept.length;
|
|
586
|
+
const cy = kept.reduce((s, c) => s + c.sy, 0) / kept.length;
|
|
587
|
+
const interiorness = (c) => {
|
|
588
|
+
let n = 0;
|
|
589
|
+
for (let di = -1; di <= 1; di++)
|
|
590
|
+
for (let dj = -1; dj <= 1; dj++) {
|
|
591
|
+
if (di === 0 && dj === 0) continue;
|
|
592
|
+
const ni = c.i + di;
|
|
593
|
+
const nj = c.j + dj;
|
|
594
|
+
if (ni >= 0 && ni < N && nj >= 0 && nj < N && onLand[ni][nj]) n++;
|
|
595
|
+
}
|
|
596
|
+
return n;
|
|
597
|
+
};
|
|
598
|
+
const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
|
|
599
|
+
const centreLon = kept.reduce((s, c) => s + c.lon, 0) / kept.length;
|
|
600
|
+
const centreLat = kept.reduce((s, c) => s + c.lat, 0) / kept.length;
|
|
601
|
+
const centreScreen = project(centreLon, centreLat);
|
|
602
|
+
const lead = pointInGeometry(geometry, centreLon, centreLat) && centreScreen && Number.isFinite(centreScreen[0]) && Number.isFinite(centreScreen[1]) && centreScreen[0] >= 0 && centreScreen[0] <= width && centreScreen[1] >= 0 && centreScreen[1] <= height ? [
|
|
603
|
+
{
|
|
604
|
+
lonLat: [centreLon, centreLat],
|
|
605
|
+
screen: [centreScreen[0], centreScreen[1]]
|
|
606
|
+
}
|
|
607
|
+
] : [];
|
|
608
|
+
const grid = [...kept].sort(
|
|
609
|
+
(a, b) => interiorness(b) - interiorness(a) || dist2ToCentre(a) - dist2ToCentre(b)
|
|
610
|
+
).map((c) => ({
|
|
611
|
+
lonLat: [c.lon, c.lat],
|
|
612
|
+
screen: [c.sx, c.sy]
|
|
613
|
+
}));
|
|
614
|
+
const curatedPos = curated ? mkCurated(curated, project) : [];
|
|
615
|
+
return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
|
|
616
|
+
}
|
|
617
|
+
function mkCurated(curated, project) {
|
|
618
|
+
if (!curated) return [];
|
|
619
|
+
const p = project(curated[0], curated[1]);
|
|
620
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) return [];
|
|
621
|
+
return [{ lonLat: [curated[0], curated[1]], screen: [p[0], p[1]] }];
|
|
622
|
+
}
|
|
623
|
+
var usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
|
|
624
|
+
var alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
|
|
625
|
+
var hawaiiProjection = () => geoMercator();
|
|
626
|
+
function projectionFor(family, extent) {
|
|
627
|
+
switch (family) {
|
|
628
|
+
case "albers-usa":
|
|
629
|
+
return usConusProjection();
|
|
630
|
+
case "conic-equal-area": {
|
|
631
|
+
const s = extent[0][1];
|
|
632
|
+
const n = extent[1][1];
|
|
633
|
+
return geoConicEqualArea().parallels([s + (n - s) / 6, s + (n - s) * 5 / 6]).center([0, (s + n) / 2]);
|
|
634
|
+
}
|
|
635
|
+
case "mercator":
|
|
636
|
+
return geoMercator();
|
|
637
|
+
case "equal-earth":
|
|
638
|
+
return geoEqualEarth();
|
|
639
|
+
case "equirectangular":
|
|
640
|
+
return geoEquirectangular();
|
|
641
|
+
case "natural-earth":
|
|
642
|
+
return geoNaturalEarth1();
|
|
643
|
+
default:
|
|
644
|
+
return geoEquirectangular();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
var INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
|
|
648
|
+
var inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
|
|
649
|
+
var inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
|
|
650
|
+
var FOREIGN_BORDER = {
|
|
651
|
+
CA: [
|
|
652
|
+
"US-AK",
|
|
653
|
+
"US-WA",
|
|
654
|
+
"US-ID",
|
|
655
|
+
"US-MT",
|
|
656
|
+
"US-ND",
|
|
657
|
+
"US-MN",
|
|
658
|
+
"US-MI",
|
|
659
|
+
"US-NY",
|
|
660
|
+
"US-VT",
|
|
661
|
+
"US-NH",
|
|
662
|
+
"US-ME"
|
|
663
|
+
],
|
|
664
|
+
MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
|
|
665
|
+
};
|
|
666
|
+
var US_NON_CONUS = /* @__PURE__ */ new Set([
|
|
667
|
+
"US-AK",
|
|
668
|
+
"US-HI",
|
|
669
|
+
"US-AS",
|
|
670
|
+
"US-GU",
|
|
671
|
+
"US-MP",
|
|
672
|
+
"US-PR",
|
|
673
|
+
"US-VI"
|
|
674
|
+
]);
|
|
675
|
+
function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
|
|
676
|
+
return mix(
|
|
677
|
+
palette.colors.blue,
|
|
678
|
+
palette.bg,
|
|
679
|
+
isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
function mapNeutralLandColor(palette, isDark, _dataActive = false) {
|
|
683
|
+
return mix(
|
|
684
|
+
palette.colors.green,
|
|
685
|
+
palette.bg,
|
|
686
|
+
isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
function buildMapProjection(resolved, data) {
|
|
690
|
+
const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
|
|
691
|
+
const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
|
|
692
|
+
const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
|
|
693
|
+
const worldLayer = new Map(decodeLayer(worldTopo));
|
|
694
|
+
if (usCrisp && data.naLand) {
|
|
695
|
+
const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
|
|
696
|
+
const crisp = decodeLayer(data.naLand);
|
|
697
|
+
for (const [iso, cf] of crisp) {
|
|
698
|
+
const base = worldLayer.get(iso);
|
|
699
|
+
if (!base) continue;
|
|
700
|
+
const [[bw, bs], [be, bn]] = geoBounds(base);
|
|
701
|
+
if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
|
|
702
|
+
worldLayer.set(iso, { ...cf, properties: base.properties });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
|
|
706
|
+
const extentOutline = () => {
|
|
707
|
+
const [[w, s], [e, n]] = resolved.extent;
|
|
708
|
+
const N = 16;
|
|
709
|
+
const coords = [];
|
|
710
|
+
for (let i = 0; i <= N; i++) {
|
|
711
|
+
const t = i / N;
|
|
712
|
+
const lon = w + (e - w) * t;
|
|
713
|
+
const lat = s + (n - s) * t;
|
|
714
|
+
coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
type: "Feature",
|
|
718
|
+
properties: {},
|
|
719
|
+
geometry: { type: "MultiPoint", coordinates: coords }
|
|
720
|
+
};
|
|
721
|
+
};
|
|
722
|
+
let fitFeatures;
|
|
723
|
+
if (resolved.projection === "albers-usa" && usLayer) {
|
|
724
|
+
fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
|
|
725
|
+
const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
|
|
726
|
+
if (neighborPoints.length > 0) {
|
|
727
|
+
fitFeatures.push({
|
|
728
|
+
type: "Feature",
|
|
729
|
+
properties: {},
|
|
730
|
+
geometry: { type: "MultiPoint", coordinates: neighborPoints }
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
for (const r of resolved.regions) {
|
|
734
|
+
if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
|
|
735
|
+
const cf = worldLayer.get(r.iso);
|
|
736
|
+
if (cf) fitFeatures.push(cf);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
fitFeatures = [extentOutline()];
|
|
741
|
+
}
|
|
742
|
+
const fitTarget = { type: "FeatureCollection", features: fitFeatures };
|
|
743
|
+
const projection = projectionFor(resolved.projection, resolved.extent);
|
|
744
|
+
if (resolved.projection !== "albers-usa") {
|
|
745
|
+
let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
|
|
746
|
+
if (centerLon > 180) centerLon -= 360;
|
|
747
|
+
projection.rotate([-centerLon, 0]);
|
|
748
|
+
}
|
|
749
|
+
const fitGB = geoBounds(fitTarget);
|
|
750
|
+
const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
|
|
751
|
+
return {
|
|
752
|
+
projection,
|
|
753
|
+
fitTarget,
|
|
754
|
+
fitIsGlobal,
|
|
755
|
+
worldLayer,
|
|
756
|
+
usLayer,
|
|
757
|
+
usCrisp,
|
|
758
|
+
wantsUsStates,
|
|
759
|
+
worldTopo
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function parsePathRings(d) {
|
|
763
|
+
const rings = [];
|
|
764
|
+
let cur = [];
|
|
765
|
+
const re = /([MLZ])([^MLZ]*)/g;
|
|
766
|
+
let m;
|
|
767
|
+
while (m = re.exec(d)) {
|
|
768
|
+
if (m[1] === "Z") {
|
|
769
|
+
if (cur.length) rings.push(cur);
|
|
770
|
+
cur = [];
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (m[1] === "M" && cur.length) {
|
|
774
|
+
rings.push(cur);
|
|
775
|
+
cur = [];
|
|
776
|
+
}
|
|
777
|
+
const nums = m[2].split(/[ ,]+/).map(Number);
|
|
778
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
779
|
+
const x = nums[i];
|
|
780
|
+
const y = nums[i + 1];
|
|
781
|
+
if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (cur.length) rings.push(cur);
|
|
785
|
+
return rings;
|
|
786
|
+
}
|
|
787
|
+
function dropAntimeridianWrapSlivers(d, width, height) {
|
|
788
|
+
const rings = parsePathRings(d);
|
|
789
|
+
if (rings.length <= 1) return d;
|
|
790
|
+
const eps = 0.75;
|
|
791
|
+
const minArea = 3e-3 * width * height;
|
|
792
|
+
const ringArea = (r) => {
|
|
793
|
+
let s = 0;
|
|
794
|
+
for (let i = 0; i < r.length; i++) {
|
|
795
|
+
const a = r[i];
|
|
796
|
+
const b = r[(i + 1) % r.length];
|
|
797
|
+
s += a[0] * b[1] - b[0] * a[1];
|
|
798
|
+
}
|
|
799
|
+
return Math.abs(s) / 2;
|
|
800
|
+
};
|
|
801
|
+
const areas = rings.map(ringArea);
|
|
802
|
+
const maxArea = Math.max(...areas);
|
|
803
|
+
const onVEdge = (a, b) => Math.abs(a[0]) <= eps && Math.abs(b[0]) <= eps || Math.abs(a[0] - width) <= eps && Math.abs(b[0] - width) <= eps;
|
|
804
|
+
let dropped = false;
|
|
805
|
+
const kept = rings.filter((r, idx) => {
|
|
806
|
+
if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
|
|
807
|
+
const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
|
|
808
|
+
if (touches) {
|
|
809
|
+
dropped = true;
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
return true;
|
|
813
|
+
});
|
|
814
|
+
if (!dropped) return d;
|
|
815
|
+
return kept.map(
|
|
816
|
+
(r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
|
|
817
|
+
).join("");
|
|
818
|
+
}
|
|
819
|
+
function albersSkewFallback(resolved, data, width, height) {
|
|
820
|
+
if (resolved.projection !== "albers-usa") return false;
|
|
821
|
+
if (!resolved.basemaps.subdivisions.includes("us-states")) return false;
|
|
822
|
+
if (!(width > 0) || !(height > 0)) return false;
|
|
823
|
+
const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
|
|
824
|
+
const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
|
|
825
|
+
if (akRef || hiRef) return false;
|
|
826
|
+
const { projection, fitTarget, usLayer } = buildMapProjection(resolved, data);
|
|
827
|
+
if (!usLayer || ![...usLayer.keys()].some((iso) => !US_NON_CONUS.has(iso)))
|
|
828
|
+
return false;
|
|
829
|
+
projection.fitSize([1e3, 1e3], fitTarget);
|
|
830
|
+
const b = geoPath(projection).bounds(fitTarget);
|
|
831
|
+
const conusAspect = (b[1][0] - b[0][0]) / (b[1][1] - b[0][1]);
|
|
832
|
+
if (!(conusAspect > 0)) return false;
|
|
833
|
+
const canvasAspect = width / height;
|
|
834
|
+
const skew = Math.max(canvasAspect / conusAspect, conusAspect / canvasAspect);
|
|
835
|
+
return skew > ALBERS_SKEW_MAX;
|
|
836
|
+
}
|
|
837
|
+
var REGION_GEO_MEMO = /* @__PURE__ */ new WeakMap();
|
|
838
|
+
var REGION_GEO_MAX_FITS = 8;
|
|
839
|
+
function regionGeoBucket(resolved, projKey) {
|
|
840
|
+
let byFit = REGION_GEO_MEMO.get(resolved);
|
|
841
|
+
if (!byFit) {
|
|
842
|
+
byFit = /* @__PURE__ */ new Map();
|
|
843
|
+
REGION_GEO_MEMO.set(resolved, byFit);
|
|
844
|
+
}
|
|
845
|
+
let bucket = byFit.get(projKey);
|
|
846
|
+
if (!bucket) {
|
|
847
|
+
bucket = /* @__PURE__ */ new Map();
|
|
848
|
+
byFit.set(projKey, bucket);
|
|
849
|
+
if (byFit.size > REGION_GEO_MAX_FITS) {
|
|
850
|
+
const oldest = byFit.keys().next().value;
|
|
851
|
+
if (oldest !== void 0) byFit.delete(oldest);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return bucket;
|
|
855
|
+
}
|
|
856
|
+
function layoutMap(resolvedIn, data, size, opts) {
|
|
857
|
+
const { palette, isDark } = opts;
|
|
858
|
+
const { width, height } = size;
|
|
859
|
+
let resolved = resolvedIn;
|
|
860
|
+
if (albersSkewFallback(resolved, data, width, height)) {
|
|
861
|
+
resolved = { ...resolved, projection: "conic-equal-area" };
|
|
862
|
+
}
|
|
863
|
+
const {
|
|
864
|
+
projection,
|
|
865
|
+
fitTarget,
|
|
866
|
+
fitIsGlobal,
|
|
867
|
+
worldLayer,
|
|
868
|
+
usLayer,
|
|
869
|
+
usCrisp,
|
|
870
|
+
worldTopo
|
|
871
|
+
} = buildMapProjection(resolved, data);
|
|
872
|
+
const usContext = usLayer !== null;
|
|
873
|
+
const regionStroke = isDark ? mix(palette.border, palette.text, 65) : mix(palette.text, palette.bg, 78);
|
|
874
|
+
const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
|
|
875
|
+
const rampMin = values.length > 0 ? Math.min(...values) : 0;
|
|
876
|
+
const rampMax = Math.max(...values);
|
|
877
|
+
const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
|
|
878
|
+
const rampLow = resolved.directives.regionMetricLowColor ? resolveColor(resolved.directives.regionMetricLowColor, palette) ?? void 0 : void 0;
|
|
879
|
+
const hasRamp = values.length > 0;
|
|
880
|
+
const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
|
|
881
|
+
const matchColorGroup = (v) => {
|
|
882
|
+
const lv = v.trim().toLowerCase();
|
|
883
|
+
if (lv === "none") return null;
|
|
884
|
+
if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
|
|
885
|
+
const tg = resolved.tagGroups.find((g) => g.name.toLowerCase() === lv);
|
|
886
|
+
return tg ? tg.name : v;
|
|
887
|
+
};
|
|
888
|
+
const fillGroupNames = /* @__PURE__ */ new Set();
|
|
889
|
+
for (const g of resolved.tagGroups) {
|
|
890
|
+
const k = g.name.toLowerCase();
|
|
891
|
+
if (resolved.regions.some((r) => r.tags[k]) || resolved.pois.some((p) => p.tags[k]))
|
|
892
|
+
fillGroupNames.add(g.name);
|
|
893
|
+
}
|
|
894
|
+
const firstFillGroup = resolved.tagGroups.find((g) => fillGroupNames.has(g.name))?.name ?? null;
|
|
895
|
+
const override = opts.activeGroup;
|
|
896
|
+
let activeGroup;
|
|
897
|
+
if (override !== void 0) {
|
|
898
|
+
activeGroup = override === null ? null : matchColorGroup(override);
|
|
899
|
+
} else if (resolved.directives.activeTag !== void 0) {
|
|
900
|
+
activeGroup = matchColorGroup(resolved.directives.activeTag);
|
|
901
|
+
} else {
|
|
902
|
+
activeGroup = VALUE_NAME ?? firstFillGroup ?? resolved.tagGroups[0]?.name ?? null;
|
|
903
|
+
}
|
|
904
|
+
const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
|
|
905
|
+
const mutedBasemap = activeIsScore || activeGroup !== null && fillGroupNames.has(activeGroup);
|
|
906
|
+
const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
|
|
907
|
+
const water = mapBackgroundColor(palette, isDark, mutedBasemap);
|
|
908
|
+
const lakeStroke = mix(regionStroke, water, 45);
|
|
909
|
+
const foreignFill = mix(
|
|
910
|
+
palette.colors.gray,
|
|
911
|
+
palette.bg,
|
|
912
|
+
mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
|
|
913
|
+
);
|
|
914
|
+
const contrastFloorTone = (tone, ...substrates) => {
|
|
915
|
+
const clears = (t) => substrates.every((s) => contrastRatio(t, s) >= REGION_LABEL_HALO_RATIO);
|
|
916
|
+
if (clears(tone)) return tone;
|
|
917
|
+
for (let s = 10; s <= 100; s += 10) {
|
|
918
|
+
const t = mix(palette.text, tone, s);
|
|
919
|
+
if (clears(t)) return t;
|
|
920
|
+
}
|
|
921
|
+
return palette.text;
|
|
922
|
+
};
|
|
923
|
+
const backdropLandTone = contrastFloorTone(
|
|
924
|
+
palette.textMuted,
|
|
925
|
+
neutralFill,
|
|
926
|
+
water
|
|
927
|
+
);
|
|
928
|
+
const backdropWaterTone = contrastFloorTone(
|
|
929
|
+
mix(palette.colors.blue, palette.textMuted, 50),
|
|
930
|
+
water,
|
|
931
|
+
neutralFill
|
|
932
|
+
);
|
|
933
|
+
const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
|
|
934
|
+
const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
|
|
935
|
+
const colorByIso = /* @__PURE__ */ new Map();
|
|
936
|
+
if (colorizeActive) {
|
|
937
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
938
|
+
const addEdges = (src) => {
|
|
939
|
+
for (const [iso, ns] of src) {
|
|
940
|
+
const cur = adjacency.get(iso);
|
|
941
|
+
if (cur) cur.push(...ns);
|
|
942
|
+
else adjacency.set(iso, [...ns]);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
addEdges(buildAdjacency(worldTopo));
|
|
946
|
+
if (usLayer) {
|
|
947
|
+
addEdges(buildAdjacency(data.usStates));
|
|
948
|
+
for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
|
|
949
|
+
const cn = adjacency.get(country);
|
|
950
|
+
if (!cn) continue;
|
|
951
|
+
for (const st of states) {
|
|
952
|
+
const sn = adjacency.get(st);
|
|
953
|
+
if (!sn) continue;
|
|
954
|
+
cn.push(st);
|
|
955
|
+
sn.push(country);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const { byIso, huesNeeded } = assignColors(
|
|
960
|
+
[...adjacency.keys()],
|
|
961
|
+
adjacency
|
|
962
|
+
);
|
|
963
|
+
const tints = politicalTints(palette, huesNeeded, isDark);
|
|
964
|
+
for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
|
|
965
|
+
}
|
|
966
|
+
const colorizeStroke = (fill) => mix(fill, palette.text, 35);
|
|
967
|
+
const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
|
|
968
|
+
const rampLowFloor = mix(rampHue, rampBase, RAMP_FLOOR);
|
|
969
|
+
const fillForValue = (s) => {
|
|
970
|
+
const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
|
|
971
|
+
if (rampLow !== void 0)
|
|
972
|
+
return valueRampColor(rampLow, rampHue, t, { isDark });
|
|
973
|
+
const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
|
|
974
|
+
return mix(rampHue, rampBase, pct);
|
|
975
|
+
};
|
|
976
|
+
const tagFill = (tags, groupName) => {
|
|
977
|
+
if (!groupName) return null;
|
|
978
|
+
const group = resolved.tagGroups.find(
|
|
979
|
+
(g) => g.name.toLowerCase() === groupName.toLowerCase()
|
|
980
|
+
);
|
|
981
|
+
if (!group) return null;
|
|
982
|
+
const val = tags[tagAttrKey(group.name)] ?? group.defaultValue;
|
|
983
|
+
if (!val) return null;
|
|
984
|
+
const entry = group.entries.find(
|
|
985
|
+
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
986
|
+
);
|
|
987
|
+
if (!entry?.color) return null;
|
|
988
|
+
return mix(
|
|
989
|
+
entry.color,
|
|
990
|
+
palette.bg,
|
|
991
|
+
isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
|
|
992
|
+
);
|
|
993
|
+
};
|
|
994
|
+
const directFill = (name) => {
|
|
995
|
+
const hex = name ? resolveColor(name, palette) : null;
|
|
996
|
+
if (!hex) return null;
|
|
997
|
+
return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
|
|
998
|
+
};
|
|
999
|
+
const regionFill = (r) => {
|
|
1000
|
+
const direct = directFill(r.color);
|
|
1001
|
+
if (direct) return direct;
|
|
1002
|
+
if (activeIsScore) {
|
|
1003
|
+
return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
|
|
1004
|
+
}
|
|
1005
|
+
if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
|
|
1006
|
+
return tagFill(r.tags, activeGroup) ?? neutralFill;
|
|
1007
|
+
};
|
|
1008
|
+
const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
|
|
1009
|
+
let legend = null;
|
|
1010
|
+
if (!resolved.directives.noLegend) {
|
|
1011
|
+
const legendTagGroups = resolved.tagGroups.map((g) => ({
|
|
1012
|
+
name: g.name,
|
|
1013
|
+
entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
|
|
1014
|
+
}));
|
|
1015
|
+
if (legendTagGroups.length > 0 || hasRamp) {
|
|
1016
|
+
legend = {
|
|
1017
|
+
tagGroups: legendTagGroups,
|
|
1018
|
+
activeGroup,
|
|
1019
|
+
...hasRamp && {
|
|
1020
|
+
ramp: {
|
|
1021
|
+
...resolved.directives.regionMetric !== void 0 && {
|
|
1022
|
+
metric: resolved.directives.regionMetric
|
|
1023
|
+
},
|
|
1024
|
+
min: rampMin,
|
|
1025
|
+
max: rampMax,
|
|
1026
|
+
low: rampLow ?? rampLowFloor,
|
|
1027
|
+
high: rampHue
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const shownTitle = resolved.directives.noTitle ? null : resolved.title;
|
|
1034
|
+
const TITLE_GAP = 16;
|
|
1035
|
+
let topPad = FIT_PAD;
|
|
1036
|
+
if (shownTitle && resolved.pois.length > 0) {
|
|
1037
|
+
const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
|
|
1038
|
+
topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
|
|
1039
|
+
}
|
|
1040
|
+
const legendBand = mapLegendBand(legend, {
|
|
1041
|
+
width,
|
|
1042
|
+
mode: opts.legendMode ?? "preview",
|
|
1043
|
+
hasTitle: Boolean(shownTitle),
|
|
1044
|
+
hasSubtitle: Boolean(resolved.subtitle)
|
|
1045
|
+
});
|
|
1046
|
+
if (legendBand > topPad) topPad = legendBand;
|
|
1047
|
+
const topReserved = [];
|
|
1048
|
+
if (shownTitle) {
|
|
1049
|
+
const lines = resolved.subtitle ? 2 : 1;
|
|
1050
|
+
const tw = Math.max(
|
|
1051
|
+
measureLegendText(shownTitle, TITLE_FONT_SIZE),
|
|
1052
|
+
resolved.subtitle ? measureLegendText(resolved.subtitle, TITLE_FONT_SIZE) : 0
|
|
1053
|
+
) + 2 * TITLE_FONT_SIZE;
|
|
1054
|
+
topReserved.push({
|
|
1055
|
+
x: width / 2 - tw / 2,
|
|
1056
|
+
y: TITLE_Y - TITLE_FONT_SIZE,
|
|
1057
|
+
w: tw,
|
|
1058
|
+
h: TITLE_FONT_SIZE * (lines + 0.5)
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const legendBox = mapLegendBox(legend, {
|
|
1062
|
+
width,
|
|
1063
|
+
mode: opts.legendMode ?? "preview",
|
|
1064
|
+
hasTitle: Boolean(shownTitle),
|
|
1065
|
+
hasSubtitle: Boolean(resolved.subtitle)
|
|
1066
|
+
});
|
|
1067
|
+
if (legendBox)
|
|
1068
|
+
topReserved.push({
|
|
1069
|
+
x: legendBox.x,
|
|
1070
|
+
y: legendBox.y,
|
|
1071
|
+
w: legendBox.width,
|
|
1072
|
+
h: legendBox.height
|
|
1073
|
+
});
|
|
1074
|
+
const reserve = opts._calloutReserve;
|
|
1075
|
+
const fitLeft = FIT_PAD + (reserve?.left ?? 0);
|
|
1076
|
+
const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
|
|
1077
|
+
const fitTop = topPad + (reserve?.top ?? 0);
|
|
1078
|
+
const fitBottom = height - FIT_PAD - (reserve?.bottom ?? 0);
|
|
1079
|
+
const fitBox = [
|
|
1080
|
+
[fitLeft, fitTop],
|
|
1081
|
+
[Math.max(fitLeft + 1, fitRight), Math.max(fitTop + 1, fitBottom)]
|
|
1082
|
+
];
|
|
1083
|
+
projection.fitExtent(fitBox, fitTarget);
|
|
1084
|
+
if (!fitIsGlobal && resolved.projection !== "albers-usa" && resolved.regions.length > 0) {
|
|
1085
|
+
let yMin = Infinity;
|
|
1086
|
+
let yMax = -Infinity;
|
|
1087
|
+
for (const r of resolved.regions) {
|
|
1088
|
+
const bb = r.iso ? featureBboxPrimary(data.worldCoarse, r.iso) : null;
|
|
1089
|
+
if (!bb) continue;
|
|
1090
|
+
for (const lon of [bb[0][0], bb[1][0]]) {
|
|
1091
|
+
for (const lat of [bb[0][1], bb[1][1]]) {
|
|
1092
|
+
const p = projection([lon, lat]);
|
|
1093
|
+
if (p && Number.isFinite(p[1])) {
|
|
1094
|
+
if (p[1] < yMin) yMin = p[1];
|
|
1095
|
+
if (p[1] > yMax) yMax = p[1];
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (yMin < yMax) {
|
|
1101
|
+
const boxTop = fitTop;
|
|
1102
|
+
const boxBottom = fitBottom;
|
|
1103
|
+
let dy = (boxTop + boxBottom) / 2 - (yMin + yMax) / 2;
|
|
1104
|
+
const minDy = boxTop - yMin;
|
|
1105
|
+
const maxDy = boxBottom - yMax;
|
|
1106
|
+
if (minDy <= maxDy) dy = Math.max(minDy, Math.min(maxDy, dy));
|
|
1107
|
+
const [tx, ty] = projection.translate();
|
|
1108
|
+
projection.translate([tx, ty + dy]);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (!fitIsGlobal && resolved.projection === "albers-usa") {
|
|
1112
|
+
const cb = geoPath(projection).bounds(fitTarget);
|
|
1113
|
+
if (Number.isFinite(cb[0][1])) {
|
|
1114
|
+
const slack = fitBottom - fitTop - (cb[1][1] - cb[0][1]);
|
|
1115
|
+
if (slack > 0) {
|
|
1116
|
+
const currentTopGap = cb[0][1] - fitTop;
|
|
1117
|
+
const dy = slack * 0.3 - currentTopGap;
|
|
1118
|
+
const [tx, ty] = projection.translate();
|
|
1119
|
+
projection.translate([tx, ty + dy]);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
let path;
|
|
1124
|
+
let project;
|
|
1125
|
+
let stretchParams = null;
|
|
1126
|
+
if (fitIsGlobal && !opts.preferContain) {
|
|
1127
|
+
const cb = geoPath(projection).bounds(fitTarget);
|
|
1128
|
+
const bx0 = cb[0][0];
|
|
1129
|
+
const by0 = cb[0][1];
|
|
1130
|
+
const cw = cb[1][0] - bx0;
|
|
1131
|
+
const ch = cb[1][1] - by0;
|
|
1132
|
+
const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
|
|
1133
|
+
const ox = 0;
|
|
1134
|
+
const oy = topReserve;
|
|
1135
|
+
const sx = cw > 0 ? width / cw : 1;
|
|
1136
|
+
const sy = ch > 0 ? (height - topReserve) / ch : 1;
|
|
1137
|
+
stretchParams = { sx, sy, ox, oy, bx0, by0 };
|
|
1138
|
+
const stretch = (x, y) => [
|
|
1139
|
+
ox + (x - bx0) * sx,
|
|
1140
|
+
oy + (y - by0) * sy
|
|
1141
|
+
];
|
|
1142
|
+
const baseProjection = projection;
|
|
1143
|
+
const tx = geoTransform({
|
|
1144
|
+
point(x, y) {
|
|
1145
|
+
const [px, py] = stretch(x, y);
|
|
1146
|
+
this.stream.point(px, py);
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
const thin = geoThin();
|
|
1150
|
+
path = geoPath({
|
|
1151
|
+
stream: (s) => baseProjection.stream(
|
|
1152
|
+
tx.stream(
|
|
1153
|
+
thin.stream(s)
|
|
1154
|
+
)
|
|
1155
|
+
)
|
|
1156
|
+
}).digits(PATH_DIGITS);
|
|
1157
|
+
project = (lon, lat) => {
|
|
1158
|
+
const p = baseProjection([lon, lat]);
|
|
1159
|
+
return p ? stretch(p[0], p[1]) : null;
|
|
1160
|
+
};
|
|
1161
|
+
} else {
|
|
1162
|
+
projection.clipExtent([
|
|
1163
|
+
[0, 0],
|
|
1164
|
+
[width, height]
|
|
1165
|
+
]);
|
|
1166
|
+
const thin = geoThin();
|
|
1167
|
+
path = geoPath({
|
|
1168
|
+
stream: (s) => projection.stream(
|
|
1169
|
+
thin.stream(s)
|
|
1170
|
+
)
|
|
1171
|
+
}).digits(PATH_DIGITS);
|
|
1172
|
+
project = (lon, lat) => projection([lon, lat]) ?? null;
|
|
1173
|
+
}
|
|
1174
|
+
const regionGeo = regionGeoBucket(
|
|
1175
|
+
resolvedIn,
|
|
1176
|
+
JSON.stringify([
|
|
1177
|
+
width,
|
|
1178
|
+
height,
|
|
1179
|
+
[
|
|
1180
|
+
[-100, 40],
|
|
1181
|
+
[-120, 37],
|
|
1182
|
+
[-80, 40],
|
|
1183
|
+
[0, 0],
|
|
1184
|
+
[60, 30],
|
|
1185
|
+
[-60, -30],
|
|
1186
|
+
[150, 60],
|
|
1187
|
+
[-150, -60],
|
|
1188
|
+
[100, -40],
|
|
1189
|
+
[-30, 50]
|
|
1190
|
+
].map(([lo, la]) => project(lo, la))
|
|
1191
|
+
])
|
|
1192
|
+
);
|
|
1193
|
+
const insets = [];
|
|
1194
|
+
const insetRegions = [];
|
|
1195
|
+
const insetLabelSeeds = [];
|
|
1196
|
+
const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
|
|
1197
|
+
const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
|
|
1198
|
+
if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
|
|
1199
|
+
const PAD = 8;
|
|
1200
|
+
const GAP = 12;
|
|
1201
|
+
const yB = height - FIT_PAD;
|
|
1202
|
+
const BW = 8;
|
|
1203
|
+
const coast = /* @__PURE__ */ new Map();
|
|
1204
|
+
const addPt = (lon, lat) => {
|
|
1205
|
+
const p = projection([lon, lat]);
|
|
1206
|
+
if (!p) return;
|
|
1207
|
+
const bi = Math.floor(p[0] / BW);
|
|
1208
|
+
const cur = coast.get(bi);
|
|
1209
|
+
if (cur === void 0 || p[1] > cur) coast.set(bi, p[1]);
|
|
1210
|
+
};
|
|
1211
|
+
const walk = (co) => {
|
|
1212
|
+
if (Array.isArray(co) && typeof co[0] === "number")
|
|
1213
|
+
addPt(co[0], co[1]);
|
|
1214
|
+
else if (Array.isArray(co)) for (const c of co) walk(c);
|
|
1215
|
+
};
|
|
1216
|
+
for (const [iso, f] of usLayer) {
|
|
1217
|
+
if (US_NON_CONUS.has(iso)) continue;
|
|
1218
|
+
walk(f.geometry.coordinates);
|
|
1219
|
+
}
|
|
1220
|
+
const at = (x) => {
|
|
1221
|
+
const bi = Math.floor(x / BW);
|
|
1222
|
+
let y = -Infinity;
|
|
1223
|
+
for (let k = bi - 1; k <= bi + 1; k++) {
|
|
1224
|
+
const v = coast.get(k);
|
|
1225
|
+
if (v !== void 0 && v > y) y = v;
|
|
1226
|
+
}
|
|
1227
|
+
return y;
|
|
1228
|
+
};
|
|
1229
|
+
const coastFloor = (x0, xr) => {
|
|
1230
|
+
const n = 24;
|
|
1231
|
+
let maxY = -Infinity;
|
|
1232
|
+
for (let i = 0; i <= n; i++) {
|
|
1233
|
+
const y = at(x0 + (xr - x0) * i / n);
|
|
1234
|
+
if (y > maxY) maxY = y;
|
|
1235
|
+
}
|
|
1236
|
+
return maxY;
|
|
1237
|
+
};
|
|
1238
|
+
const placeInset = (iso, proj, boxX, iwReq) => {
|
|
1239
|
+
const f = usLayer.get(iso);
|
|
1240
|
+
if (!f) return boxX;
|
|
1241
|
+
const x0 = boxX;
|
|
1242
|
+
const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
|
|
1243
|
+
if (iw < 24) return boxX;
|
|
1244
|
+
const xr = x0 + iw + 2 * PAD;
|
|
1245
|
+
const floor = coastFloor(x0, xr);
|
|
1246
|
+
const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
|
|
1247
|
+
proj.fitWidth(iw, f);
|
|
1248
|
+
const bb = geoPath(proj).bounds(f);
|
|
1249
|
+
const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
|
|
1250
|
+
const needH = sh + 2 * PAD;
|
|
1251
|
+
let topFit = topGuess;
|
|
1252
|
+
const bottom = Math.min(topFit + needH, yB);
|
|
1253
|
+
if (bottom - topFit < needH) topFit = bottom - needH;
|
|
1254
|
+
proj.fitExtent(
|
|
1255
|
+
[
|
|
1256
|
+
[x0 + PAD, topFit + PAD],
|
|
1257
|
+
[xr - PAD, bottom - PAD]
|
|
1258
|
+
],
|
|
1259
|
+
f
|
|
1260
|
+
);
|
|
1261
|
+
const insetPath = geoPath(proj).digits(PATH_DIGITS);
|
|
1262
|
+
const d = insetPath(f) ?? "";
|
|
1263
|
+
if (!d) return xr;
|
|
1264
|
+
let contextLand;
|
|
1265
|
+
if (iso === "US-AK") {
|
|
1266
|
+
const can = worldLayer.get("CA");
|
|
1267
|
+
const cd = can ? insetPath(can) ?? "" : "";
|
|
1268
|
+
if (cd)
|
|
1269
|
+
contextLand = {
|
|
1270
|
+
d: cd,
|
|
1271
|
+
fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
const r = regionById.get(iso);
|
|
1275
|
+
let fill = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
|
|
1276
|
+
let lineNumber = -1;
|
|
1277
|
+
if (r?.layer === "us-state") {
|
|
1278
|
+
fill = regionFill(r);
|
|
1279
|
+
lineNumber = r.lineNumber;
|
|
1280
|
+
}
|
|
1281
|
+
insets.push({
|
|
1282
|
+
x: x0,
|
|
1283
|
+
y: topFit,
|
|
1284
|
+
w: xr - x0,
|
|
1285
|
+
h: bottom - topFit,
|
|
1286
|
+
points: [
|
|
1287
|
+
[x0, topFit],
|
|
1288
|
+
[xr, topFit],
|
|
1289
|
+
[xr, bottom],
|
|
1290
|
+
[x0, bottom]
|
|
1291
|
+
],
|
|
1292
|
+
// The FITTED inset projection (just fit to this box) — captured so the
|
|
1293
|
+
// geo-query can invert pixels inside the frame back to AK/HI coords.
|
|
1294
|
+
projection: proj,
|
|
1295
|
+
...contextLand && { contextLand }
|
|
1296
|
+
});
|
|
1297
|
+
insetRegions.push({
|
|
1298
|
+
id: iso,
|
|
1299
|
+
d,
|
|
1300
|
+
fill,
|
|
1301
|
+
stroke: colorizeActive ? colorizeStroke(fill) : regionStroke,
|
|
1302
|
+
lineNumber,
|
|
1303
|
+
layer: "us-state",
|
|
1304
|
+
...r?.value !== void 0 && { value: r.value },
|
|
1305
|
+
...r && Object.keys(r.tags).length > 0 && { tags: r.tags }
|
|
1306
|
+
});
|
|
1307
|
+
const ctr = geoPath(proj).centroid(f);
|
|
1308
|
+
if (Number.isFinite(ctr[0])) {
|
|
1309
|
+
const name = f.properties?.name ?? iso;
|
|
1310
|
+
insetLabelSeeds.push({ x: ctr[0], y: ctr[1], iso, name, lineNumber });
|
|
1311
|
+
}
|
|
1312
|
+
return xr;
|
|
1313
|
+
};
|
|
1314
|
+
let akRight = FIT_PAD;
|
|
1315
|
+
if (akRef)
|
|
1316
|
+
akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.18);
|
|
1317
|
+
if (hiRef)
|
|
1318
|
+
placeInset(
|
|
1319
|
+
"US-HI",
|
|
1320
|
+
hawaiiProjection(),
|
|
1321
|
+
akRef ? akRight + 24 : FIT_PAD,
|
|
1322
|
+
width * 0.12
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
const conusFit = resolved.projection === "albers-usa" && !!usLayer;
|
|
1326
|
+
const classifyExtent = conusFit ? geoBounds(fitTarget) : resolved.extent;
|
|
1327
|
+
const dLonSpan = classifyExtent[1][0] - classifyExtent[0][0];
|
|
1328
|
+
const dLatSpan = classifyExtent[1][1] - classifyExtent[0][1];
|
|
1329
|
+
const isGlobalView = dLonSpan >= 270 || dLatSpan >= 130;
|
|
1330
|
+
const vLonCenter = (classifyExtent[0][0] + classifyExtent[1][0]) / 2;
|
|
1331
|
+
const normLon = (lon) => {
|
|
1332
|
+
let L = lon;
|
|
1333
|
+
while (L < vLonCenter - 180) L += 360;
|
|
1334
|
+
while (L > vLonCenter + 180) L -= 360;
|
|
1335
|
+
return L;
|
|
1336
|
+
};
|
|
1337
|
+
const ringOverlapsView = (ring) => {
|
|
1338
|
+
let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
|
|
1339
|
+
const lons = [];
|
|
1340
|
+
for (const [rawLon] of ring) {
|
|
1341
|
+
const lon = normLon(rawLon);
|
|
1342
|
+
lons.push(lon);
|
|
1343
|
+
if (lon < loMin) loMin = lon;
|
|
1344
|
+
if (lon > loMax) loMax = lon;
|
|
1345
|
+
if (rawLon < rawMin) rawMin = rawLon;
|
|
1346
|
+
if (rawLon > rawMax) rawMax = rawLon;
|
|
1347
|
+
}
|
|
1348
|
+
lons.sort((a, b) => a - b);
|
|
1349
|
+
let maxGap = 0;
|
|
1350
|
+
for (let i = 1; i < lons.length; i++)
|
|
1351
|
+
maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
|
|
1352
|
+
if (lons.length > 1)
|
|
1353
|
+
maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
|
|
1354
|
+
const occupiedArc = 360 - maxGap;
|
|
1355
|
+
if (occupiedArc > 270) return false;
|
|
1356
|
+
if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
|
|
1357
|
+
let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
|
|
1358
|
+
for (const [lon, lat] of ring) {
|
|
1359
|
+
const p = project(lon, lat);
|
|
1360
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
1361
|
+
anyFinite = true;
|
|
1362
|
+
if (p[0] < px0) px0 = p[0];
|
|
1363
|
+
if (p[0] > px1) px1 = p[0];
|
|
1364
|
+
if (p[1] < py0) py0 = p[1];
|
|
1365
|
+
if (p[1] > py1) py1 = p[1];
|
|
1366
|
+
}
|
|
1367
|
+
if (!anyFinite) return false;
|
|
1368
|
+
return !(px1 < 0 || px0 > width || py1 < 0 || py0 > height);
|
|
1369
|
+
};
|
|
1370
|
+
const cullFeatureToView = (f) => {
|
|
1371
|
+
if (isGlobalView) return f;
|
|
1372
|
+
const g = f.geometry;
|
|
1373
|
+
if (!g) return f;
|
|
1374
|
+
if (g.type === "Polygon") {
|
|
1375
|
+
const ring = g.coordinates[0];
|
|
1376
|
+
return ringOverlapsView(ring) ? f : null;
|
|
1377
|
+
}
|
|
1378
|
+
if (g.type === "MultiPolygon") {
|
|
1379
|
+
const polys = g.coordinates;
|
|
1380
|
+
const keep = polys.filter(
|
|
1381
|
+
(p) => ringOverlapsView(p[0])
|
|
1382
|
+
);
|
|
1383
|
+
if (!keep.length) return null;
|
|
1384
|
+
if (keep.length === polys.length) return f;
|
|
1385
|
+
return { ...f, geometry: { ...g, coordinates: keep } };
|
|
1386
|
+
}
|
|
1387
|
+
return f;
|
|
1388
|
+
};
|
|
1389
|
+
const SEAM_SLIVER_MAX_SPAN = 100;
|
|
1390
|
+
const ringIsFrameFiller = (ring) => {
|
|
1391
|
+
const lons = ring.map(([lon]) => lon).sort((a, b) => a - b);
|
|
1392
|
+
if (lons.length < 2) return false;
|
|
1393
|
+
let maxGap = -1;
|
|
1394
|
+
let gapIdx = 0;
|
|
1395
|
+
for (let i = 1; i < lons.length; i++) {
|
|
1396
|
+
const g = lons[i] - lons[i - 1];
|
|
1397
|
+
if (g > maxGap) {
|
|
1398
|
+
maxGap = g;
|
|
1399
|
+
gapIdx = i;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const wrapGap = lons[0] + 360 - lons[lons.length - 1];
|
|
1403
|
+
if (wrapGap >= maxGap) return false;
|
|
1404
|
+
const span = 360 - maxGap;
|
|
1405
|
+
const east = lons[gapIdx - 1] + 360;
|
|
1406
|
+
return east > 180 && span < SEAM_SLIVER_MAX_SPAN;
|
|
1407
|
+
};
|
|
1408
|
+
const dropFrameFillers = (f) => {
|
|
1409
|
+
const g = f.geometry;
|
|
1410
|
+
if (!g) return f;
|
|
1411
|
+
if (g.type === "Polygon") {
|
|
1412
|
+
const ring = g.coordinates[0];
|
|
1413
|
+
return ringIsFrameFiller(ring) ? null : f;
|
|
1414
|
+
}
|
|
1415
|
+
if (g.type === "MultiPolygon") {
|
|
1416
|
+
const polys = g.coordinates;
|
|
1417
|
+
const keep = polys.filter(
|
|
1418
|
+
(p) => !ringIsFrameFiller(p[0])
|
|
1419
|
+
);
|
|
1420
|
+
if (!keep.length) return null;
|
|
1421
|
+
if (keep.length === polys.length) return f;
|
|
1422
|
+
return { ...f, geometry: { ...g, coordinates: keep } };
|
|
1423
|
+
}
|
|
1424
|
+
return f;
|
|
1425
|
+
};
|
|
1426
|
+
const regions = [];
|
|
1427
|
+
const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
|
|
1428
|
+
for (const [iso, f] of layerFeatures) {
|
|
1429
|
+
if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
|
|
1430
|
+
continue;
|
|
1431
|
+
if (layerKind === "country" && usContext && iso === "US") continue;
|
|
1432
|
+
if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
|
|
1433
|
+
continue;
|
|
1434
|
+
const r = regionById.get(iso);
|
|
1435
|
+
const geoK = layerKind + ":" + iso;
|
|
1436
|
+
let geo = regionGeo.get(geoK);
|
|
1437
|
+
if (geo === void 0) {
|
|
1438
|
+
const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
|
|
1439
|
+
if (!viewF) {
|
|
1440
|
+
regionGeo.set(geoK, null);
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
const raw = path(viewF) ?? "";
|
|
1444
|
+
const d0 = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
|
|
1445
|
+
if (!d0) {
|
|
1446
|
+
regionGeo.set(geoK, null);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const anchor = WORLD_LABEL_ANCHORS[iso];
|
|
1450
|
+
const cc = anchor ? project(anchor[0], anchor[1]) : path.centroid(viewF);
|
|
1451
|
+
const ok = cc != null && Number.isFinite(cc[0]) && Number.isFinite(cc[1]);
|
|
1452
|
+
geo = { d: d0, cx: ok ? cc[0] : void 0, cy: ok ? cc[1] : void 0 };
|
|
1453
|
+
regionGeo.set(geoK, geo);
|
|
1454
|
+
} else if (geo === null) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const d = geo.d;
|
|
1458
|
+
const hasCentroid = geo.cx !== void 0 && geo.cy !== void 0;
|
|
1459
|
+
const isThisLayer = r?.layer === layerKind;
|
|
1460
|
+
const isForeign = layerKind === "country" && usContext && iso !== "US";
|
|
1461
|
+
const baseFill = isForeign ? foreignFill : neutralFill;
|
|
1462
|
+
let fill = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
|
|
1463
|
+
let label;
|
|
1464
|
+
let lineNumber = -1;
|
|
1465
|
+
let layer = "base";
|
|
1466
|
+
if (isThisLayer) {
|
|
1467
|
+
fill = regionFill(r);
|
|
1468
|
+
lineNumber = r.lineNumber;
|
|
1469
|
+
layer = layerKind;
|
|
1470
|
+
label = r.name;
|
|
1471
|
+
} else {
|
|
1472
|
+
label = f.properties?.name;
|
|
1473
|
+
}
|
|
1474
|
+
regions.push({
|
|
1475
|
+
id: iso,
|
|
1476
|
+
d,
|
|
1477
|
+
fill,
|
|
1478
|
+
stroke: colorizeActive ? colorizeStroke(fill) : regionStroke,
|
|
1479
|
+
lineNumber,
|
|
1480
|
+
layer,
|
|
1481
|
+
...label !== void 0 && { label },
|
|
1482
|
+
...hasCentroid && { labelX: geo.cx, labelY: geo.cy },
|
|
1483
|
+
...isThisLayer && r.value !== void 0 && { value: r.value },
|
|
1484
|
+
...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
pushRegionLayer(worldLayer, "country", !isGlobalView);
|
|
1489
|
+
if (usLayer) pushRegionLayer(usLayer, "us-state", !conusFit && !isGlobalView);
|
|
1490
|
+
const lakesTopo = usCrisp && data.naLakes ? data.naLakes : data.lakes;
|
|
1491
|
+
if (lakesTopo) {
|
|
1492
|
+
for (const [, f] of decodeLayer(lakesTopo)) {
|
|
1493
|
+
const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
|
|
1494
|
+
if (!viewF) continue;
|
|
1495
|
+
const d = path(viewF) ?? "";
|
|
1496
|
+
if (!d) continue;
|
|
1497
|
+
regions.push({
|
|
1498
|
+
id: "lake",
|
|
1499
|
+
d,
|
|
1500
|
+
fill: water,
|
|
1501
|
+
stroke: lakeStroke,
|
|
1502
|
+
lineNumber: -1,
|
|
1503
|
+
layer: "base"
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const pointInRings = (px, py, rings) => {
|
|
1508
|
+
let inside = false;
|
|
1509
|
+
for (const ring of rings) {
|
|
1510
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
1511
|
+
const [xi, yi] = ring[i];
|
|
1512
|
+
const [xj, yj] = ring[j];
|
|
1513
|
+
if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
|
|
1514
|
+
inside = !inside;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return inside;
|
|
1518
|
+
};
|
|
1519
|
+
const fillHitTargets = [...regions, ...insetRegions].map((r) => {
|
|
1520
|
+
const rings = parsePathRings(r.d);
|
|
1521
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1522
|
+
for (const ring of rings)
|
|
1523
|
+
for (const p of ring) {
|
|
1524
|
+
if (p[0] < minX) minX = p[0];
|
|
1525
|
+
if (p[0] > maxX) maxX = p[0];
|
|
1526
|
+
if (p[1] < minY) minY = p[1];
|
|
1527
|
+
if (p[1] > maxY) maxY = p[1];
|
|
1528
|
+
}
|
|
1529
|
+
r.bbox = [minX, minY, maxX, maxY];
|
|
1530
|
+
r.rings = rings;
|
|
1531
|
+
return { fill: r.fill, rings, minX, minY, maxX, maxY };
|
|
1532
|
+
});
|
|
1533
|
+
const fillAt = (x, y) => {
|
|
1534
|
+
let hit = water;
|
|
1535
|
+
for (const t of fillHitTargets) {
|
|
1536
|
+
if (x < t.minX || x > t.maxX || y < t.minY || y > t.maxY) continue;
|
|
1537
|
+
if (pointInRings(x, y, t.rings)) hit = t.fill;
|
|
1538
|
+
}
|
|
1539
|
+
return hit;
|
|
1540
|
+
};
|
|
1541
|
+
const labelOnFill = (fill) => {
|
|
1542
|
+
const color = contrastRatio(fill, palette.textOnFillDark) >= contrastRatio(fill, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
|
|
1543
|
+
const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
|
|
1544
|
+
return {
|
|
1545
|
+
color,
|
|
1546
|
+
halo: contrastRatio(fill, color) < REGION_LABEL_HALO_RATIO,
|
|
1547
|
+
haloColor
|
|
1548
|
+
};
|
|
1549
|
+
};
|
|
1550
|
+
const reliefAllowed = resolved.directives.noRelief !== true;
|
|
1551
|
+
const relief = [];
|
|
1552
|
+
let reliefHatch = null;
|
|
1553
|
+
if (reliefAllowed && data.mountainRanges) {
|
|
1554
|
+
for (const [, f] of decodeLayer(data.mountainRanges)) {
|
|
1555
|
+
const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
|
|
1556
|
+
if (!viewF) continue;
|
|
1557
|
+
const area = path.area(viewF);
|
|
1558
|
+
if (!Number.isFinite(area) || area < RELIEF_MIN_AREA) continue;
|
|
1559
|
+
const box = path.bounds(viewF);
|
|
1560
|
+
if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
|
|
1561
|
+
continue;
|
|
1562
|
+
const d = path(viewF) ?? "";
|
|
1563
|
+
if (!d) continue;
|
|
1564
|
+
relief.push({ d });
|
|
1565
|
+
}
|
|
1566
|
+
if (relief.length) {
|
|
1567
|
+
const darkTone = isDark ? palette.bg : palette.text;
|
|
1568
|
+
const lightTone = isDark ? palette.text : palette.bg;
|
|
1569
|
+
const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
|
|
1570
|
+
const landLum = relativeLuminance(reliefLandRef);
|
|
1571
|
+
const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
|
|
1572
|
+
reliefHatch = {
|
|
1573
|
+
color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
|
|
1574
|
+
spacing: RELIEF_HATCH_SPACING,
|
|
1575
|
+
width: RELIEF_HATCH_WIDTH
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
let coastlineStyle = null;
|
|
1580
|
+
if (resolved.directives.noCoastline !== true) {
|
|
1581
|
+
const minDim = Math.min(width, height);
|
|
1582
|
+
coastlineStyle = {
|
|
1583
|
+
color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
|
|
1584
|
+
// N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
|
|
1585
|
+
// fades linearly from NEAR (innermost) to FAR (outermost).
|
|
1586
|
+
lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
|
|
1587
|
+
d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
|
|
1588
|
+
thickness: COASTLINE_THICKNESS * minDim,
|
|
1589
|
+
opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
|
|
1590
|
+
})),
|
|
1591
|
+
minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
const riverColor = mix(palette.colors.blue, water, 32);
|
|
1595
|
+
const rivers = [];
|
|
1596
|
+
if (data.rivers) {
|
|
1597
|
+
for (const [, f] of decodeLayer(data.rivers)) {
|
|
1598
|
+
const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
|
|
1599
|
+
if (!viewF) continue;
|
|
1600
|
+
const d = path(viewF) ?? "";
|
|
1601
|
+
if (!d) continue;
|
|
1602
|
+
rivers.push({ d, color: riverColor, width: RIVER_WIDTH });
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
const sizeVals = resolved.pois.map((p) => Number(p.meta["size"])).filter((n) => Number.isFinite(n) && n > 0);
|
|
1606
|
+
const sizeMin = sizeVals.length ? Math.min(...sizeVals) : 0;
|
|
1607
|
+
const sizeMax = sizeVals.length ? Math.max(...sizeVals) : 0;
|
|
1608
|
+
const radiusFor = (p) => {
|
|
1609
|
+
const v = Number(p.meta["size"]);
|
|
1610
|
+
if (!Number.isFinite(v) || v <= 0 || sizeMax <= 0) return R_DEFAULT;
|
|
1611
|
+
const t = sizeMax > sizeMin ? (Math.sqrt(v) - Math.sqrt(sizeMin)) / (Math.sqrt(sizeMax) - Math.sqrt(sizeMin)) : 1;
|
|
1612
|
+
return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
|
|
1613
|
+
};
|
|
1614
|
+
const fillOpacityFor = (r) => {
|
|
1615
|
+
const t = Math.max(0, Math.min(1, (r - R_MIN) / (R_MAX - R_MIN)));
|
|
1616
|
+
return POI_FILL_OPACITY_MAX - t * (POI_FILL_OPACITY_MAX - POI_FILL_OPACITY_MIN);
|
|
1617
|
+
};
|
|
1618
|
+
const poiFill = (p) => {
|
|
1619
|
+
const directHex = p.color ? resolveColor(p.color, palette) : null;
|
|
1620
|
+
if (directHex)
|
|
1621
|
+
return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
|
|
1622
|
+
for (const group of resolved.tagGroups) {
|
|
1623
|
+
const val = p.tags[tagAttrKey(group.name)];
|
|
1624
|
+
if (!val) continue;
|
|
1625
|
+
const entry = group.entries.find(
|
|
1626
|
+
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
1627
|
+
);
|
|
1628
|
+
const hex = entry?.color;
|
|
1629
|
+
if (hex) return { fill: hex, stroke: mix(hex, palette.text, 18) };
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
fill: palette.colors.orange,
|
|
1633
|
+
stroke: mix(palette.colors.orange, palette.text, 18)
|
|
1634
|
+
};
|
|
1635
|
+
};
|
|
1636
|
+
const lineColor = (tags) => {
|
|
1637
|
+
for (const group of resolved.tagGroups) {
|
|
1638
|
+
const val = tags[tagAttrKey(group.name)];
|
|
1639
|
+
if (!val) continue;
|
|
1640
|
+
const entry = group.entries.find(
|
|
1641
|
+
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
1642
|
+
);
|
|
1643
|
+
if (entry?.color) return entry.color;
|
|
1644
|
+
}
|
|
1645
|
+
return null;
|
|
1646
|
+
};
|
|
1647
|
+
const routeNumberById = /* @__PURE__ */ new Map();
|
|
1648
|
+
const originIds = /* @__PURE__ */ new Set();
|
|
1649
|
+
for (const rt of resolved.routes) {
|
|
1650
|
+
rt.stopIds.forEach((id, i) => {
|
|
1651
|
+
if (i === 0) originIds.add(id);
|
|
1652
|
+
if (!routeNumberById.has(id)) routeNumberById.set(id, i + 1);
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
const poiScreen = /* @__PURE__ */ new Map();
|
|
1656
|
+
const pois = [];
|
|
1657
|
+
const orderedPois = [...resolved.pois].sort(
|
|
1658
|
+
(a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
|
|
1659
|
+
);
|
|
1660
|
+
const projected = [];
|
|
1661
|
+
for (const p of orderedPois) {
|
|
1662
|
+
const xy = project(p.lon, p.lat);
|
|
1663
|
+
if (xy) projected.push({ p, xy });
|
|
1664
|
+
}
|
|
1665
|
+
const placePoi = (e, cx, cy, clusterId) => {
|
|
1666
|
+
const { fill, stroke } = poiFill(e.p);
|
|
1667
|
+
const r = radiusFor(e.p);
|
|
1668
|
+
poiScreen.set(e.p.id, { cx, cy, r });
|
|
1669
|
+
const num = routeNumberById.get(e.p.id);
|
|
1670
|
+
pois.push({
|
|
1671
|
+
id: e.p.id,
|
|
1672
|
+
cx,
|
|
1673
|
+
cy,
|
|
1674
|
+
r,
|
|
1675
|
+
fill,
|
|
1676
|
+
fillOpacity: fillOpacityFor(r),
|
|
1677
|
+
stroke,
|
|
1678
|
+
lineNumber: e.p.lineNumber,
|
|
1679
|
+
implicit: !!e.p.implicit,
|
|
1680
|
+
isOrigin: originIds.has(e.p.id),
|
|
1681
|
+
...num !== void 0 && { routeNumber: num },
|
|
1682
|
+
...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
|
|
1683
|
+
...clusterId !== void 0 && { clusterId }
|
|
1684
|
+
});
|
|
1685
|
+
};
|
|
1686
|
+
const clusters = [];
|
|
1687
|
+
const connected = /* @__PURE__ */ new Set();
|
|
1688
|
+
for (const e of resolved.edges) {
|
|
1689
|
+
connected.add(e.fromId);
|
|
1690
|
+
connected.add(e.toId);
|
|
1691
|
+
}
|
|
1692
|
+
for (const rt of resolved.routes) {
|
|
1693
|
+
rt.stopIds.forEach((id) => connected.add(id));
|
|
1694
|
+
}
|
|
1695
|
+
const radiusOf = (e) => radiusFor(e.p);
|
|
1696
|
+
for (const e of projected) {
|
|
1697
|
+
if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
|
|
1698
|
+
}
|
|
1699
|
+
const groups = [];
|
|
1700
|
+
for (const e of projected) {
|
|
1701
|
+
if (connected.has(e.p.id)) continue;
|
|
1702
|
+
const r = radiusOf(e);
|
|
1703
|
+
const near = groups.find(
|
|
1704
|
+
(g) => g.some(
|
|
1705
|
+
(q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
|
|
1706
|
+
)
|
|
1707
|
+
);
|
|
1708
|
+
if (near) near.push(e);
|
|
1709
|
+
else groups.push([e]);
|
|
1710
|
+
}
|
|
1711
|
+
for (const g of groups) {
|
|
1712
|
+
if (g.length === 1) {
|
|
1713
|
+
placePoi(g[0], g[0].xy[0], g[0].xy[1]);
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
const clusterId = g[0].p.id;
|
|
1717
|
+
const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
|
|
1718
|
+
const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
|
|
1719
|
+
const maxR = Math.max(...g.map(radiusOf));
|
|
1720
|
+
const sep = 2 * maxR + STACK_RING_GAP;
|
|
1721
|
+
const ringR = Math.max(
|
|
1722
|
+
COLO_R,
|
|
1723
|
+
sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
|
|
1724
|
+
);
|
|
1725
|
+
const positions = g.map((e, i) => {
|
|
1726
|
+
if (g.length <= STACK_RING_MAX) {
|
|
1727
|
+
const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
|
|
1728
|
+
return {
|
|
1729
|
+
e,
|
|
1730
|
+
mx: cx0 + Math.cos(ang2) * ringR,
|
|
1731
|
+
my: cy0 + Math.sin(ang2) * ringR
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
const ang = i * GOLDEN_ANGLE;
|
|
1735
|
+
const rr = ringR * Math.sqrt((i + 1) / g.length);
|
|
1736
|
+
return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
|
|
1737
|
+
});
|
|
1738
|
+
let minX = cx0 - maxR;
|
|
1739
|
+
let maxX = cx0 + maxR;
|
|
1740
|
+
let minY = cy0 - maxR;
|
|
1741
|
+
let maxY = cy0 + maxR;
|
|
1742
|
+
for (const { mx, my, e } of positions) {
|
|
1743
|
+
const r = radiusOf(e);
|
|
1744
|
+
minX = Math.min(minX, mx - r);
|
|
1745
|
+
maxX = Math.max(maxX, mx + r);
|
|
1746
|
+
minY = Math.min(minY, my - r);
|
|
1747
|
+
maxY = Math.max(maxY, my + r);
|
|
1748
|
+
}
|
|
1749
|
+
let dx = 0;
|
|
1750
|
+
let dy = 0;
|
|
1751
|
+
if (minX + dx < 2) dx = 2 - minX;
|
|
1752
|
+
if (maxX + dx > width - 2) dx = width - 2 - maxX;
|
|
1753
|
+
if (minY + dy < 2) dy = 2 - minY;
|
|
1754
|
+
if (maxY + dy > height - 2) dy = height - 2 - maxY;
|
|
1755
|
+
const legsOut = [];
|
|
1756
|
+
for (const { e, mx, my } of positions) {
|
|
1757
|
+
const fx = mx + dx;
|
|
1758
|
+
const fy = my + dy;
|
|
1759
|
+
placePoi(e, fx, fy, clusterId);
|
|
1760
|
+
legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
|
|
1761
|
+
}
|
|
1762
|
+
clusters.push({
|
|
1763
|
+
id: clusterId,
|
|
1764
|
+
cx: cx0 + dx,
|
|
1765
|
+
cy: cy0 + dy,
|
|
1766
|
+
count: g.length,
|
|
1767
|
+
hitR: ringR + maxR + 6,
|
|
1768
|
+
legs: legsOut
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
const legs = [];
|
|
1772
|
+
const RIM_GAP = 1.5;
|
|
1773
|
+
const bowMagnitude = (mx, my, nx, ny, offset, len, away) => {
|
|
1774
|
+
if (offset !== 0) return offset;
|
|
1775
|
+
const base = len * ARC_CURVE_FRAC;
|
|
1776
|
+
if (!away) return base;
|
|
1777
|
+
const dot = nx * (mx - away.x) + ny * (my - away.y);
|
|
1778
|
+
return dot < 0 ? -base : base;
|
|
1779
|
+
};
|
|
1780
|
+
const legPath = (a, b, curved, offset, away) => {
|
|
1781
|
+
const mx = (a.cx + b.cx) / 2;
|
|
1782
|
+
const my = (a.cy + b.cy) / 2;
|
|
1783
|
+
const dx = b.cx - a.cx;
|
|
1784
|
+
const dy = b.cy - a.cy;
|
|
1785
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
1786
|
+
const trimA = Math.min(a.r + RIM_GAP, len * 0.45);
|
|
1787
|
+
const trimB = Math.min(b.r + RIM_GAP, len * 0.45);
|
|
1788
|
+
if (!curved && offset === 0) {
|
|
1789
|
+
const ux = dx / len;
|
|
1790
|
+
const uy = dy / len;
|
|
1791
|
+
const ax2 = a.cx + ux * trimA;
|
|
1792
|
+
const ay2 = a.cy + uy * trimA;
|
|
1793
|
+
const bx2 = b.cx - ux * trimB;
|
|
1794
|
+
const by2 = b.cy - uy * trimB;
|
|
1795
|
+
return `M${ax2},${ay2}L${bx2},${by2}`;
|
|
1796
|
+
}
|
|
1797
|
+
const nx = -dy / len;
|
|
1798
|
+
const ny = dx / len;
|
|
1799
|
+
const bow = bowMagnitude(mx, my, nx, ny, offset, len, away);
|
|
1800
|
+
const px = mx + nx * bow;
|
|
1801
|
+
const py = my + ny * bow;
|
|
1802
|
+
const ta = Math.hypot(px - a.cx, py - a.cy) || 1;
|
|
1803
|
+
const tb = Math.hypot(b.cx - px, b.cy - py) || 1;
|
|
1804
|
+
const ax = a.cx + (px - a.cx) / ta * trimA;
|
|
1805
|
+
const ay = a.cy + (py - a.cy) / ta * trimA;
|
|
1806
|
+
const bx = b.cx - (b.cx - px) / tb * trimB;
|
|
1807
|
+
const by = b.cy - (b.cy - py) / tb * trimB;
|
|
1808
|
+
return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
|
|
1809
|
+
};
|
|
1810
|
+
const legLabelPoint = (a, b, curved, offset, away) => {
|
|
1811
|
+
const mx = (a.cx + b.cx) / 2;
|
|
1812
|
+
const my = (a.cy + b.cy) / 2;
|
|
1813
|
+
if (!curved && offset === 0) return { x: mx, y: my - 4 };
|
|
1814
|
+
const dx = b.cx - a.cx;
|
|
1815
|
+
const dy = b.cy - a.cy;
|
|
1816
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
1817
|
+
const nx = -dy / len;
|
|
1818
|
+
const ny = dx / len;
|
|
1819
|
+
const bow = bowMagnitude(mx, my, nx, ny, offset, len, away);
|
|
1820
|
+
const off = bow * 0.5 + Math.sign(bow || 1) * 8;
|
|
1821
|
+
return { x: mx + nx * off, y: my + ny * off };
|
|
1822
|
+
};
|
|
1823
|
+
const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
|
|
1824
|
+
const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
|
|
1825
|
+
const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
|
|
1826
|
+
const routeWidthFor = (v) => {
|
|
1827
|
+
if (!Number.isFinite(v) || v <= 0 || rlMax <= 0) return W_MIN;
|
|
1828
|
+
const t = rlMax > rlMin ? (v - rlMin) / (rlMax - rlMin) : 1;
|
|
1829
|
+
return W_MIN + t * (W_MAX - W_MIN);
|
|
1830
|
+
};
|
|
1831
|
+
for (const rt of resolved.routes) {
|
|
1832
|
+
const stopPts = rt.stopIds.map((id) => poiScreen.get(id)).filter((p) => !!p);
|
|
1833
|
+
const center = stopPts.length >= 3 ? {
|
|
1834
|
+
x: stopPts.reduce((s, p) => s + p.cx, 0) / stopPts.length,
|
|
1835
|
+
y: stopPts.reduce((s, p) => s + p.cy, 0) / stopPts.length
|
|
1836
|
+
} : void 0;
|
|
1837
|
+
for (const leg of rt.legs) {
|
|
1838
|
+
const a = poiScreen.get(leg.fromId);
|
|
1839
|
+
const b = poiScreen.get(leg.toId);
|
|
1840
|
+
if (!a || !b) continue;
|
|
1841
|
+
const lp = legLabelPoint(a, b, leg.style === "arc", 0, center);
|
|
1842
|
+
const bow = {
|
|
1843
|
+
curved: leg.style === "arc",
|
|
1844
|
+
offset: 0,
|
|
1845
|
+
center,
|
|
1846
|
+
labelX: lp.x,
|
|
1847
|
+
labelY: lp.y
|
|
1848
|
+
};
|
|
1849
|
+
const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
|
|
1850
|
+
const routeVal = Number(leg.value);
|
|
1851
|
+
legs.push({
|
|
1852
|
+
d: legPath(a, b, bow.curved, bow.offset, bow.center),
|
|
1853
|
+
width: routeWidthFor(routeVal),
|
|
1854
|
+
color: lineColor(leg.tags) ?? mix(palette.text, palette.bg, 72),
|
|
1855
|
+
arrow: true,
|
|
1856
|
+
fromId: leg.fromId,
|
|
1857
|
+
toId: leg.toId,
|
|
1858
|
+
...Number.isFinite(routeVal) && routeVal > 0 && { value: routeVal },
|
|
1859
|
+
...Object.keys(leg.tags).length > 0 && { tags: leg.tags },
|
|
1860
|
+
lineNumber: leg.lineNumber,
|
|
1861
|
+
...leg.label !== void 0 && {
|
|
1862
|
+
label: leg.label,
|
|
1863
|
+
labelX: bow.labelX,
|
|
1864
|
+
labelY: bow.labelY,
|
|
1865
|
+
labelColor: routeLabelStyle.color,
|
|
1866
|
+
labelHalo: routeLabelStyle.halo,
|
|
1867
|
+
labelHaloColor: routeLabelStyle.haloColor
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const weightVals = resolved.edges.map((e) => Number(e.meta["width"])).filter((n) => Number.isFinite(n) && n > 0);
|
|
1873
|
+
const wMin = weightVals.length ? Math.min(...weightVals) : 0;
|
|
1874
|
+
const wMax = weightVals.length ? Math.max(...weightVals) : 0;
|
|
1875
|
+
const widthFor = (e) => {
|
|
1876
|
+
const v = Number(e.meta["width"]);
|
|
1877
|
+
if (!Number.isFinite(v) || v <= 0 || wMax <= 0) return W_MIN;
|
|
1878
|
+
const t = wMax > wMin ? (v - wMin) / (wMax - wMin) : 1;
|
|
1879
|
+
return W_MIN + t * (W_MAX - W_MIN);
|
|
1880
|
+
};
|
|
1881
|
+
const pairGroups = /* @__PURE__ */ new Map();
|
|
1882
|
+
for (const e of resolved.edges) {
|
|
1883
|
+
const key = [e.fromId, e.toId].sort().join(" ");
|
|
1884
|
+
const arr = pairGroups.get(key);
|
|
1885
|
+
if (arr) arr.push(e);
|
|
1886
|
+
else pairGroups.set(key, [e]);
|
|
1887
|
+
}
|
|
1888
|
+
for (const groupEdges of pairGroups.values()) {
|
|
1889
|
+
const ordered = [...groupEdges].sort((a, b) => a.lineNumber - b.lineNumber);
|
|
1890
|
+
const n = ordered.length;
|
|
1891
|
+
ordered.forEach((e, i) => {
|
|
1892
|
+
const a = poiScreen.get(e.fromId);
|
|
1893
|
+
const b = poiScreen.get(e.toId);
|
|
1894
|
+
if (!a || !b) return;
|
|
1895
|
+
const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
|
|
1896
|
+
const curved = e.style === "arc" || n > 1;
|
|
1897
|
+
const lp = legLabelPoint(a, b, curved, fanOffset);
|
|
1898
|
+
const bow = {
|
|
1899
|
+
curved,
|
|
1900
|
+
offset: fanOffset,
|
|
1901
|
+
labelX: lp.x,
|
|
1902
|
+
labelY: lp.y
|
|
1903
|
+
};
|
|
1904
|
+
const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
|
|
1905
|
+
const edgeVal = Number(e.meta["width"]);
|
|
1906
|
+
legs.push({
|
|
1907
|
+
d: legPath(a, b, bow.curved, bow.offset),
|
|
1908
|
+
width: widthFor(e),
|
|
1909
|
+
color: lineColor(e.tags) ?? mix(palette.text, palette.bg, 66),
|
|
1910
|
+
arrow: e.directed,
|
|
1911
|
+
fromId: e.fromId,
|
|
1912
|
+
toId: e.toId,
|
|
1913
|
+
...Number.isFinite(edgeVal) && edgeVal > 0 && { value: edgeVal },
|
|
1914
|
+
...Object.keys(e.tags).length > 0 && { tags: e.tags },
|
|
1915
|
+
lineNumber: e.lineNumber,
|
|
1916
|
+
...e.label !== void 0 && {
|
|
1917
|
+
label: e.label,
|
|
1918
|
+
labelX: bow.labelX,
|
|
1919
|
+
labelY: bow.labelY,
|
|
1920
|
+
labelColor: edgeLabelStyle.color,
|
|
1921
|
+
labelHalo: edgeLabelStyle.halo,
|
|
1922
|
+
labelHaloColor: edgeLabelStyle.haloColor
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
const labels = [];
|
|
1928
|
+
const obstacles = [...topReserved];
|
|
1929
|
+
const regionLabelGuards = [];
|
|
1930
|
+
const markers = pois.map((p) => ({
|
|
1931
|
+
cx: p.cx,
|
|
1932
|
+
cy: p.cy,
|
|
1933
|
+
r: p.r
|
|
1934
|
+
}));
|
|
1935
|
+
const legSegments = [];
|
|
1936
|
+
for (const leg of legs) {
|
|
1937
|
+
const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
|
|
1938
|
+
leg.d
|
|
1939
|
+
);
|
|
1940
|
+
if (!m) continue;
|
|
1941
|
+
const x0 = +m[1];
|
|
1942
|
+
const y0 = +m[2];
|
|
1943
|
+
if (m[3] !== void 0) {
|
|
1944
|
+
legSegments.push([x0, y0, +m[3], +m[4]]);
|
|
1945
|
+
} else {
|
|
1946
|
+
const cx = +m[5];
|
|
1947
|
+
const cy = +m[6];
|
|
1948
|
+
const ex = +m[7];
|
|
1949
|
+
const ey = +m[8];
|
|
1950
|
+
const N = 8;
|
|
1951
|
+
let px = x0;
|
|
1952
|
+
let py = y0;
|
|
1953
|
+
for (let i = 1; i <= N; i++) {
|
|
1954
|
+
const t = i / N;
|
|
1955
|
+
const u = 1 - t;
|
|
1956
|
+
const qx = u * u * x0 + 2 * u * t * cx + t * t * ex;
|
|
1957
|
+
const qy = u * u * y0 + 2 * u * t * cy + t * t * ey;
|
|
1958
|
+
legSegments.push([px, py, qx, qy]);
|
|
1959
|
+
px = qx;
|
|
1960
|
+
py = qy;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
|
|
1965
|
+
const showRegionLabels = resolved.directives.noRegionLabels !== true;
|
|
1966
|
+
const labeledRegionIds = /* @__PURE__ */ new Set();
|
|
1967
|
+
const showRegionValues = resolved.directives.noRegionValue !== true && activeIsScore;
|
|
1968
|
+
const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
|
|
1969
|
+
const isCompact = width < COMPACT_WIDTH_PX;
|
|
1970
|
+
const usChoroplethZoom = resolved.projection === "mercator" && resolved.basemaps.subdivisions.includes("us-states") && activeIsScore;
|
|
1971
|
+
const LABEL_PADX = 6;
|
|
1972
|
+
const LABEL_PADY = 3;
|
|
1973
|
+
const VALUE_GAP = 1;
|
|
1974
|
+
const labelW = (text, font = FONT2) => measureLegendText(text, font) + 2 * LABEL_PADX;
|
|
1975
|
+
const labelH = FONT2 + 2 * LABEL_PADY;
|
|
1976
|
+
const stackW = (text, valueText, font = FONT2) => Math.max(
|
|
1977
|
+
labelW(text, font),
|
|
1978
|
+
valueText ? measureLegendText(valueText, Math.round(font * 0.82)) + 2 * LABEL_PADX : 0
|
|
1979
|
+
);
|
|
1980
|
+
const stackH = (hasValue, font = FONT2) => {
|
|
1981
|
+
const lh = font + 2 * LABEL_PADY;
|
|
1982
|
+
return hasValue ? lh + VALUE_GAP + Math.round(font * 0.82) : lh;
|
|
1983
|
+
};
|
|
1984
|
+
const REGION_FONT_MAX_ORIENT = 22;
|
|
1985
|
+
const REGION_SIZE_FRAC_MIN = 0.06;
|
|
1986
|
+
const REGION_SIZE_FRAC_MAX = 0.32;
|
|
1987
|
+
const SUBJECT_FONT_MAX = 18;
|
|
1988
|
+
const SUBJECT_MIN_PROMINENCE = 13;
|
|
1989
|
+
const SUBJECT_LEADER_FONT = 15;
|
|
1990
|
+
const canvasLinear = Math.sqrt(Math.max(1, width * height));
|
|
1991
|
+
const sizeT = (boxW, boxH) => {
|
|
1992
|
+
const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
|
|
1993
|
+
return Math.min(
|
|
1994
|
+
1,
|
|
1995
|
+
Math.max(
|
|
1996
|
+
0,
|
|
1997
|
+
(frac - REGION_SIZE_FRAC_MIN) / (REGION_SIZE_FRAC_MAX - REGION_SIZE_FRAC_MIN)
|
|
1998
|
+
)
|
|
1999
|
+
);
|
|
2000
|
+
};
|
|
2001
|
+
const pushRegionLabel = (x, y, text, fill, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
|
|
2002
|
+
const { color: baseColor, haloColor } = labelOnFill(fill);
|
|
2003
|
+
const color = colorOverride ?? baseColor;
|
|
2004
|
+
const vf = Math.round(fontSize * 0.82);
|
|
2005
|
+
const halfW = Math.max(
|
|
2006
|
+
measureLegendText(text, fontSize),
|
|
2007
|
+
valueLine ? measureLegendText(valueLine, vf) : 0
|
|
2008
|
+
) / 2;
|
|
2009
|
+
const overflows = [y - fontSize * 0.55, y - fontSize * 0.1].some(
|
|
2010
|
+
(sy) => fillAt(x - halfW, sy) !== fill || fillAt(x + halfW, sy) !== fill
|
|
2011
|
+
);
|
|
2012
|
+
labels.push({
|
|
2013
|
+
x,
|
|
2014
|
+
y,
|
|
2015
|
+
text,
|
|
2016
|
+
anchor: "middle",
|
|
2017
|
+
color,
|
|
2018
|
+
// A backdrop label (colorOverride) NEVER haloes: the placement gate above
|
|
2019
|
+
// guarantees it sits on a floored-readable substrate (own fill, neutral
|
|
2020
|
+
// land, foreign land, or water), so a halo is pure noise. Data labels keep
|
|
2021
|
+
// the containment-gated halo until the role-tier pass (Phase 3).
|
|
2022
|
+
halo: colorOverride ? false : overflows,
|
|
2023
|
+
haloColor,
|
|
2024
|
+
...fontSize !== FONT2 && { fontSize },
|
|
2025
|
+
...valueLine !== void 0 && { valueLine },
|
|
2026
|
+
lineNumber
|
|
2027
|
+
});
|
|
2028
|
+
};
|
|
2029
|
+
const REGION_LABEL_GAP = 2;
|
|
2030
|
+
const regionLabelRect = (cx, cy, text, valueText, font = FONT2) => {
|
|
2031
|
+
const vf = Math.round(font * 0.82);
|
|
2032
|
+
const w = Math.max(
|
|
2033
|
+
measureLegendText(text, font),
|
|
2034
|
+
valueText ? measureLegendText(valueText, vf) : 0
|
|
2035
|
+
) + 2 * REGION_LABEL_GAP;
|
|
2036
|
+
const h = valueText ? font + VALUE_GAP + vf : font;
|
|
2037
|
+
return { x: cx - w / 2, y: cy - h / 2, w, h };
|
|
2038
|
+
};
|
|
2039
|
+
if (showRegionLabels) {
|
|
2040
|
+
const frameContainers = new Set(resolved.poiFrameContainers);
|
|
2041
|
+
const entries = regions.map((r) => {
|
|
2042
|
+
const isContainer = frameContainers.has(r.id);
|
|
2043
|
+
if (r.layer === "base" && !isContainer || r.label === void 0)
|
|
2044
|
+
return null;
|
|
2045
|
+
const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
|
|
2046
|
+
const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
|
|
2047
|
+
if (!f) return null;
|
|
2048
|
+
const [[x0, y0], [x1, y1]] = path.bounds(f);
|
|
2049
|
+
const boxW = x1 - x0;
|
|
2050
|
+
const boxH = y1 - y0;
|
|
2051
|
+
const abbrev = isUsState && !usChoroplethZoom ? r.id.replace(/^US-/, "") : void 0;
|
|
2052
|
+
const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : isContainer ? [r.label] : [r.label, abbrev] : [r.label];
|
|
2053
|
+
const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
|
|
2054
|
+
const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
|
|
2055
|
+
if (!c || !Number.isFinite(c[0])) return null;
|
|
2056
|
+
return { r, c, boxW, boxH, area: boxW * boxH, candidates };
|
|
2057
|
+
}).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
|
|
2058
|
+
const placedRegionRects = [...topReserved];
|
|
2059
|
+
const placedLeaders = [];
|
|
2060
|
+
const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
|
|
2061
|
+
const POI_LABEL_PAD = 14;
|
|
2062
|
+
const poiObstacles = pois.map((p) => ({
|
|
2063
|
+
x: p.cx - p.r - POI_LABEL_PAD,
|
|
2064
|
+
y: p.cy - p.r - POI_LABEL_PAD,
|
|
2065
|
+
w: 2 * (p.r + POI_LABEL_PAD),
|
|
2066
|
+
h: 2 * (p.r + POI_LABEL_PAD)
|
|
2067
|
+
}));
|
|
2068
|
+
const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
|
|
2069
|
+
const STEP = 4;
|
|
2070
|
+
const HOP_GAP = 9;
|
|
2071
|
+
const CLEAR_STEP = 6;
|
|
2072
|
+
const CLEAR_MAX = SHORT_HOP_MAX * 3;
|
|
2073
|
+
const CLEAR_TOL = CLEAR_STEP;
|
|
2074
|
+
const clearanceAt = (px, py, ox, oy) => {
|
|
2075
|
+
const baseAng = Math.atan2(oy, ox);
|
|
2076
|
+
let minClear = CLEAR_MAX;
|
|
2077
|
+
for (let k = -2; k <= 2; k++) {
|
|
2078
|
+
const a = baseAng + k * Math.PI / 4;
|
|
2079
|
+
const rx = Math.cos(a);
|
|
2080
|
+
const ry = Math.sin(a);
|
|
2081
|
+
let d = CLEAR_STEP;
|
|
2082
|
+
for (; d <= CLEAR_MAX; d += CLEAR_STEP) {
|
|
2083
|
+
const qx = px + rx * d;
|
|
2084
|
+
const qy = py + ry * d;
|
|
2085
|
+
if (qx < FIT_PAD || qy < topPad || qx > width - FIT_PAD || qy > height - FIT_PAD)
|
|
2086
|
+
break;
|
|
2087
|
+
if (fillAt(qx, qy) !== water) break;
|
|
2088
|
+
}
|
|
2089
|
+
if (d < minClear) minClear = d;
|
|
2090
|
+
}
|
|
2091
|
+
return minClear;
|
|
2092
|
+
};
|
|
2093
|
+
const tryShortHopCallout = (cx, cy, fill, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
|
|
2094
|
+
const chipW = stackW(name, value, font);
|
|
2095
|
+
const chipH = stackH(value !== void 0, font);
|
|
2096
|
+
const D = Math.SQRT1_2;
|
|
2097
|
+
const dirs = [
|
|
2098
|
+
[0, -1],
|
|
2099
|
+
// north
|
|
2100
|
+
[0, 1],
|
|
2101
|
+
// south
|
|
2102
|
+
[1, 0],
|
|
2103
|
+
// east
|
|
2104
|
+
[-1, 0],
|
|
2105
|
+
// west
|
|
2106
|
+
[D, -D],
|
|
2107
|
+
// NE
|
|
2108
|
+
[D, D],
|
|
2109
|
+
// SE
|
|
2110
|
+
[-D, -D],
|
|
2111
|
+
// NW
|
|
2112
|
+
[-D, D]
|
|
2113
|
+
// SW
|
|
2114
|
+
];
|
|
2115
|
+
let best = null;
|
|
2116
|
+
for (const [dx, dy] of dirs) {
|
|
2117
|
+
let ex = cx;
|
|
2118
|
+
let ey = cy;
|
|
2119
|
+
let steps = 0;
|
|
2120
|
+
const maxSteps = Math.ceil(maxLen / STEP) + 1;
|
|
2121
|
+
while (steps < maxSteps && fillAt(ex, ey) === fill) {
|
|
2122
|
+
ex += dx * STEP;
|
|
2123
|
+
ey += dy * STEP;
|
|
2124
|
+
steps++;
|
|
2125
|
+
}
|
|
2126
|
+
if (fillAt(ex, ey) === fill) continue;
|
|
2127
|
+
const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
|
|
2128
|
+
const ccx = ex + dx * (gap + halfAlong);
|
|
2129
|
+
const ccy = ey + dy * (gap + halfAlong);
|
|
2130
|
+
const rect = {
|
|
2131
|
+
x: ccx - chipW / 2,
|
|
2132
|
+
y: ccy - chipH / 2,
|
|
2133
|
+
w: chipW,
|
|
2134
|
+
h: chipH
|
|
2135
|
+
};
|
|
2136
|
+
if (rect.x < FIT_PAD || rect.y < topPad || rect.x + rect.w > width - FIT_PAD || rect.y + rect.h > height - FIT_PAD)
|
|
2137
|
+
continue;
|
|
2138
|
+
const sx = [rect.x + 2, ccx, rect.x + rect.w - 2];
|
|
2139
|
+
const sy = [rect.y + 2, ccy, rect.y + rect.h - 2];
|
|
2140
|
+
let onEmpty = true;
|
|
2141
|
+
for (const px of sx)
|
|
2142
|
+
for (const py of sy)
|
|
2143
|
+
if (!isEmptyFill(fillAt(px, py))) {
|
|
2144
|
+
onEmpty = false;
|
|
2145
|
+
break;
|
|
2146
|
+
}
|
|
2147
|
+
if (!onEmpty) continue;
|
|
2148
|
+
if (placedRegionRects.some((p) => rectsOverlap(rect, p))) continue;
|
|
2149
|
+
if (poiObstacles.some((o) => rectsOverlap(rect, o))) continue;
|
|
2150
|
+
if (markers.some((m) => rectCircleOverlap(rect, m))) continue;
|
|
2151
|
+
const innerX = ccx - dx * (chipW / 2);
|
|
2152
|
+
const innerY = ccy - dy * (chipH / 2);
|
|
2153
|
+
const len = Math.hypot(innerX - cx, innerY - cy);
|
|
2154
|
+
if (len > maxLen) continue;
|
|
2155
|
+
if (placedLeaders.some(
|
|
2156
|
+
(l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
|
|
2157
|
+
))
|
|
2158
|
+
continue;
|
|
2159
|
+
if (placedRegionRects.some(
|
|
2160
|
+
(p) => segmentRectOverlap(cx, cy, innerX, innerY, p)
|
|
2161
|
+
))
|
|
2162
|
+
continue;
|
|
2163
|
+
const clear = clearanceAt(ccx, ccy, dx, dy);
|
|
2164
|
+
const better = best === null || clear > best.clear + CLEAR_TOL || clear >= best.clear - CLEAR_TOL && len < best.len;
|
|
2165
|
+
if (better)
|
|
2166
|
+
best = {
|
|
2167
|
+
x: ccx,
|
|
2168
|
+
y: ccy,
|
|
2169
|
+
rect,
|
|
2170
|
+
leader: [cx, cy, innerX, innerY],
|
|
2171
|
+
len,
|
|
2172
|
+
clear
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
return best ? { x: best.x, y: best.y, rect: best.rect, leader: best.leader } : null;
|
|
2176
|
+
};
|
|
2177
|
+
for (const { r, c, boxW, boxH, candidates } of entries) {
|
|
2178
|
+
const valStr = regionValueStr(r.value);
|
|
2179
|
+
const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
|
|
2180
|
+
const fitsPois = (rect) => !poiObstacles.some((o) => rectsOverlap(rect, o));
|
|
2181
|
+
const isSubject = regionById.has(r.id);
|
|
2182
|
+
const extremesOnFill = (ax, ay, text) => {
|
|
2183
|
+
const halfW = measureLegendText(text, FONT2) / 2;
|
|
2184
|
+
return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
|
|
2185
|
+
};
|
|
2186
|
+
if (isSubject && valStr === void 0 && r.label !== void 0) {
|
|
2187
|
+
const name = candidates[0];
|
|
2188
|
+
let best = -1;
|
|
2189
|
+
for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
|
|
2190
|
+
if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
|
|
2191
|
+
continue;
|
|
2192
|
+
const rect = regionLabelRect(c[0], c[1], name, void 0, f);
|
|
2193
|
+
if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
|
|
2194
|
+
const halfW = measureLegendText(name, f) / 2;
|
|
2195
|
+
if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
|
|
2196
|
+
continue;
|
|
2197
|
+
best = f;
|
|
2198
|
+
break;
|
|
2199
|
+
}
|
|
2200
|
+
if (best >= SUBJECT_MIN_PROMINENCE) {
|
|
2201
|
+
const rect = regionLabelRect(c[0], c[1], name, void 0, best);
|
|
2202
|
+
placedRegionRects.push(rect);
|
|
2203
|
+
pushRegionLabel(
|
|
2204
|
+
c[0],
|
|
2205
|
+
c[1],
|
|
2206
|
+
name,
|
|
2207
|
+
r.fill,
|
|
2208
|
+
r.lineNumber,
|
|
2209
|
+
void 0,
|
|
2210
|
+
best
|
|
2211
|
+
);
|
|
2212
|
+
labeledRegionIds.add(r.id);
|
|
2213
|
+
regionLabelGuards.push({ label: labels[labels.length - 1], rect });
|
|
2214
|
+
} else {
|
|
2215
|
+
const hop = tryShortHopCallout(
|
|
2216
|
+
c[0],
|
|
2217
|
+
c[1],
|
|
2218
|
+
r.fill,
|
|
2219
|
+
r.label,
|
|
2220
|
+
void 0,
|
|
2221
|
+
SUBJECT_LEADER_FONT,
|
|
2222
|
+
HOP_GAP + SUBJECT_LEADER_FONT,
|
|
2223
|
+
SHORT_HOP_MAX * 2.2
|
|
2224
|
+
);
|
|
2225
|
+
if (hop) {
|
|
2226
|
+
placedRegionRects.push(hop.rect);
|
|
2227
|
+
placedLeaders.push(hop.leader);
|
|
2228
|
+
const dark = mix(r.fill, palette.text, 60);
|
|
2229
|
+
labels.push({
|
|
2230
|
+
x: hop.x,
|
|
2231
|
+
y: hop.y,
|
|
2232
|
+
text: r.label,
|
|
2233
|
+
anchor: "middle",
|
|
2234
|
+
color: palette.text,
|
|
2235
|
+
halo: false,
|
|
2236
|
+
haloColor: palette.bg,
|
|
2237
|
+
fontSize: SUBJECT_LEADER_FONT,
|
|
2238
|
+
leader: {
|
|
2239
|
+
x1: hop.leader[0],
|
|
2240
|
+
y1: hop.leader[1],
|
|
2241
|
+
x2: hop.leader[2],
|
|
2242
|
+
y2: hop.leader[3]
|
|
2243
|
+
},
|
|
2244
|
+
leaderColor: dark,
|
|
2245
|
+
calloutDot: { x: c[0], y: c[1], color: dark },
|
|
2246
|
+
lineNumber: r.lineNumber
|
|
2247
|
+
});
|
|
2248
|
+
labeledRegionIds.add(r.id);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
const seekAnchors = [
|
|
2254
|
+
{ x: c[0], y: c[1], guard: false }
|
|
2255
|
+
];
|
|
2256
|
+
if (valStr) {
|
|
2257
|
+
seekAnchors.push(
|
|
2258
|
+
{ x: c[0], y: c[1] + boxH * 0.26, guard: true },
|
|
2259
|
+
{ x: c[0], y: c[1] - boxH * 0.26, guard: true },
|
|
2260
|
+
{ x: c[0] + boxW * 0.26, y: c[1], guard: true },
|
|
2261
|
+
{ x: c[0] - boxW * 0.26, y: c[1], guard: true },
|
|
2262
|
+
{ x: c[0] + boxW * 0.22, y: c[1] + boxH * 0.22, guard: true },
|
|
2263
|
+
{ x: c[0] - boxW * 0.22, y: c[1] + boxH * 0.22, guard: true },
|
|
2264
|
+
{ x: c[0] + boxW * 0.22, y: c[1] - boxH * 0.22, guard: true },
|
|
2265
|
+
{ x: c[0] - boxW * 0.22, y: c[1] - boxH * 0.22, guard: true }
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
let chosen;
|
|
2269
|
+
for (const a of seekAnchors) {
|
|
2270
|
+
if (a.guard && fillAt(a.x, a.y) !== r.fill) continue;
|
|
2271
|
+
for (const t of candidates) {
|
|
2272
|
+
const nameRect = regionLabelRect(a.x, a.y, t);
|
|
2273
|
+
if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
|
|
2274
|
+
const stackRect = regionLabelRect(a.x, a.y, t, valStr);
|
|
2275
|
+
if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
|
|
2276
|
+
chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (labelW(t) <= boxW && labelH <= boxH) {
|
|
2281
|
+
if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
|
|
2282
|
+
chosen = { text: t, ax: a.x, ay: a.y };
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
if (chosen) break;
|
|
2288
|
+
}
|
|
2289
|
+
if (chosen === void 0 && r.label !== void 0 && isSubject) {
|
|
2290
|
+
const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
|
|
2291
|
+
if (hop) {
|
|
2292
|
+
placedRegionRects.push(hop.rect);
|
|
2293
|
+
placedLeaders.push(hop.leader);
|
|
2294
|
+
const dark = mix(r.fill, palette.text, 60);
|
|
2295
|
+
labels.push({
|
|
2296
|
+
x: hop.x,
|
|
2297
|
+
y: hop.y,
|
|
2298
|
+
text: r.label,
|
|
2299
|
+
anchor: "middle",
|
|
2300
|
+
color: palette.text,
|
|
2301
|
+
halo: false,
|
|
2302
|
+
haloColor: palette.bg,
|
|
2303
|
+
...valStr !== void 0 && { valueLine: valStr },
|
|
2304
|
+
leader: {
|
|
2305
|
+
x1: hop.leader[0],
|
|
2306
|
+
y1: hop.leader[1],
|
|
2307
|
+
x2: hop.leader[2],
|
|
2308
|
+
y2: hop.leader[3]
|
|
2309
|
+
},
|
|
2310
|
+
leaderColor: dark,
|
|
2311
|
+
calloutDot: { x: c[0], y: c[1], color: dark },
|
|
2312
|
+
lineNumber: r.lineNumber
|
|
2313
|
+
});
|
|
2314
|
+
labeledRegionIds.add(r.id);
|
|
2315
|
+
}
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
if (chosen === void 0) continue;
|
|
2319
|
+
const isOrient = r.value === void 0 && r.layer === "base";
|
|
2320
|
+
let font = FONT2;
|
|
2321
|
+
if (isOrient) {
|
|
2322
|
+
const growT = sizeT(boxW, boxH);
|
|
2323
|
+
const desiredFont = Math.round(
|
|
2324
|
+
FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
|
|
2325
|
+
);
|
|
2326
|
+
const hasVal = chosen.valueLine !== void 0;
|
|
2327
|
+
const extremesClean = (f) => {
|
|
2328
|
+
const halfW = measureLegendText(chosen.text, f) / 2;
|
|
2329
|
+
return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
|
|
2330
|
+
const fAt = fillAt(sx, chosen.ay);
|
|
2331
|
+
return fAt === r.fill || isEmptyFill(fAt);
|
|
2332
|
+
});
|
|
2333
|
+
};
|
|
2334
|
+
let fit = -1;
|
|
2335
|
+
for (let f = desiredFont; f >= FONT2; f--) {
|
|
2336
|
+
if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
|
|
2337
|
+
continue;
|
|
2338
|
+
const gRect = regionLabelRect(
|
|
2339
|
+
chosen.ax,
|
|
2340
|
+
chosen.ay,
|
|
2341
|
+
chosen.text,
|
|
2342
|
+
chosen.valueLine,
|
|
2343
|
+
f
|
|
2344
|
+
);
|
|
2345
|
+
const gName = regionLabelRect(
|
|
2346
|
+
chosen.ax,
|
|
2347
|
+
chosen.ay,
|
|
2348
|
+
chosen.text,
|
|
2349
|
+
void 0,
|
|
2350
|
+
f
|
|
2351
|
+
);
|
|
2352
|
+
if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
|
|
2353
|
+
continue;
|
|
2354
|
+
if (!extremesClean(f)) continue;
|
|
2355
|
+
fit = f;
|
|
2356
|
+
break;
|
|
2357
|
+
}
|
|
2358
|
+
if (fit < 0) {
|
|
2359
|
+
if (!frameContainers.has(r.id)) continue;
|
|
2360
|
+
font = FONT2;
|
|
2361
|
+
} else {
|
|
2362
|
+
font = fit;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
const rRect = regionLabelRect(
|
|
2366
|
+
chosen.ax,
|
|
2367
|
+
chosen.ay,
|
|
2368
|
+
chosen.text,
|
|
2369
|
+
chosen.valueLine,
|
|
2370
|
+
font
|
|
2371
|
+
);
|
|
2372
|
+
placedRegionRects.push(rRect);
|
|
2373
|
+
pushRegionLabel(
|
|
2374
|
+
chosen.ax,
|
|
2375
|
+
chosen.ay,
|
|
2376
|
+
chosen.text,
|
|
2377
|
+
r.fill,
|
|
2378
|
+
r.lineNumber,
|
|
2379
|
+
chosen.valueLine,
|
|
2380
|
+
font,
|
|
2381
|
+
isOrient ? backdropLandTone : void 0
|
|
2382
|
+
);
|
|
2383
|
+
labeledRegionIds.add(r.id);
|
|
2384
|
+
regionLabelGuards.push({
|
|
2385
|
+
label: labels[labels.length - 1],
|
|
2386
|
+
rect: rRect
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
for (const seed of insetLabelSeeds) {
|
|
2390
|
+
const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
|
|
2391
|
+
const src = regionById.get(seed.iso);
|
|
2392
|
+
const valStr = regionValueStr(src?.value);
|
|
2393
|
+
pushRegionLabel(
|
|
2394
|
+
seed.x,
|
|
2395
|
+
seed.y,
|
|
2396
|
+
text,
|
|
2397
|
+
src ? regionFill(src) : neutralFill,
|
|
2398
|
+
seed.lineNumber,
|
|
2399
|
+
valStr
|
|
2400
|
+
);
|
|
2401
|
+
labeledRegionIds.add(seed.iso);
|
|
2402
|
+
regionLabelGuards.push({
|
|
2403
|
+
label: labels[labels.length - 1],
|
|
2404
|
+
rect: regionLabelRect(seed.x, seed.y, text, valStr)
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
if (resolved.directives.noPoiLabels !== true) {
|
|
2409
|
+
const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
|
|
2410
|
+
const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
|
|
2411
|
+
const labelText = (p) => {
|
|
2412
|
+
const src = poiById.get(p.id);
|
|
2413
|
+
return src?.label ?? src?.name ?? p.id;
|
|
2414
|
+
};
|
|
2415
|
+
const poiLabH = FONT2 * 1.25;
|
|
2416
|
+
const labelInfo = (p) => {
|
|
2417
|
+
const text = labelText(p);
|
|
2418
|
+
return { text, w: measureLegendText(text, FONT2) };
|
|
2419
|
+
};
|
|
2420
|
+
const GAP = 3;
|
|
2421
|
+
const POI_EDGE_CLEAR = Math.max(
|
|
2422
|
+
20,
|
|
2423
|
+
Math.round(Math.min(width, height) * 0.03)
|
|
2424
|
+
);
|
|
2425
|
+
const clusterMembersById = /* @__PURE__ */ new Map();
|
|
2426
|
+
for (const p of pois) {
|
|
2427
|
+
if (p.clusterId === void 0) continue;
|
|
2428
|
+
const arr = clusterMembersById.get(p.clusterId);
|
|
2429
|
+
if (arr) arr.push(p);
|
|
2430
|
+
else clusterMembersById.set(p.clusterId, [p]);
|
|
2431
|
+
}
|
|
2432
|
+
const inlineRect = (p, w, side) => {
|
|
2433
|
+
switch (side) {
|
|
2434
|
+
case "right":
|
|
2435
|
+
return { x: p.cx + p.r + GAP, y: p.cy - poiLabH / 2, w, h: poiLabH };
|
|
2436
|
+
case "left":
|
|
2437
|
+
return {
|
|
2438
|
+
x: p.cx - p.r - GAP - w,
|
|
2439
|
+
y: p.cy - poiLabH / 2,
|
|
2440
|
+
w,
|
|
2441
|
+
h: poiLabH
|
|
2442
|
+
};
|
|
2443
|
+
case "above":
|
|
2444
|
+
return {
|
|
2445
|
+
x: p.cx - w / 2,
|
|
2446
|
+
y: p.cy - p.r - GAP - poiLabH,
|
|
2447
|
+
w,
|
|
2448
|
+
h: poiLabH
|
|
2449
|
+
};
|
|
2450
|
+
case "below":
|
|
2451
|
+
return { x: p.cx - w / 2, y: p.cy + p.r + GAP, w, h: poiLabH };
|
|
2452
|
+
// Diagonal corners: offset both axes by (r+GAP)/√2 so the box's near
|
|
2453
|
+
// corner clears the dot rim by the same gap as a cardinal side.
|
|
2454
|
+
case "below-right": {
|
|
2455
|
+
const d = (p.r + GAP) * 0.7071;
|
|
2456
|
+
return { x: p.cx + d, y: p.cy + d, w, h: poiLabH };
|
|
2457
|
+
}
|
|
2458
|
+
case "below-left": {
|
|
2459
|
+
const d = (p.r + GAP) * 0.7071;
|
|
2460
|
+
return { x: p.cx - d - w, y: p.cy + d, w, h: poiLabH };
|
|
2461
|
+
}
|
|
2462
|
+
case "above-right": {
|
|
2463
|
+
const d = (p.r + GAP) * 0.7071;
|
|
2464
|
+
return { x: p.cx + d, y: p.cy - d - poiLabH, w, h: poiLabH };
|
|
2465
|
+
}
|
|
2466
|
+
case "above-left": {
|
|
2467
|
+
const d = (p.r + GAP) * 0.7071;
|
|
2468
|
+
return { x: p.cx - d - w, y: p.cy - d - poiLabH, w, h: poiLabH };
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
const pushInline = (p, text, w, side, clusterId) => {
|
|
2473
|
+
const rect = inlineRect(p, w, side);
|
|
2474
|
+
obstacles.push(rect);
|
|
2475
|
+
const startSide = side === "right" || side === "below-right" || side === "above-right";
|
|
2476
|
+
const endSide = side === "left" || side === "below-left" || side === "above-left";
|
|
2477
|
+
const anchor = startSide ? "start" : endSide ? "end" : "middle";
|
|
2478
|
+
const x = startSide ? rect.x : endSide ? rect.x + w : p.cx;
|
|
2479
|
+
labels.push({
|
|
2480
|
+
x,
|
|
2481
|
+
y: rect.y + poiLabH / 2 + FONT2 / 3,
|
|
2482
|
+
text,
|
|
2483
|
+
anchor,
|
|
2484
|
+
color: palette.text,
|
|
2485
|
+
halo: false,
|
|
2486
|
+
haloColor: palette.bg,
|
|
2487
|
+
poiId: p.id,
|
|
2488
|
+
lineNumber: p.lineNumber,
|
|
2489
|
+
...clusterId !== void 0 && { clusterMember: clusterId }
|
|
2490
|
+
});
|
|
2491
|
+
};
|
|
2492
|
+
const inlineFits = (p, w, side) => {
|
|
2493
|
+
const rect = inlineRect(p, w, side);
|
|
2494
|
+
return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
|
|
2495
|
+
};
|
|
2496
|
+
const GROUP_R = 30;
|
|
2497
|
+
const groups2 = [];
|
|
2498
|
+
for (const p of ordered) {
|
|
2499
|
+
const near = groups2.find(
|
|
2500
|
+
(g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
|
|
2501
|
+
);
|
|
2502
|
+
if (near) near.push(p);
|
|
2503
|
+
else groups2.push([p]);
|
|
2504
|
+
}
|
|
2505
|
+
const ROW_GAP = 3;
|
|
2506
|
+
const step = poiLabH + ROW_GAP;
|
|
2507
|
+
const COL_GAP = 16;
|
|
2508
|
+
const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
|
|
2509
|
+
const columnRows = (items, side) => {
|
|
2510
|
+
const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
|
|
2511
|
+
const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
|
|
2512
|
+
const maxW = Math.max(...items.map((o) => o.w));
|
|
2513
|
+
const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
|
|
2514
|
+
const colX = side === "right" ? Math.min(right + COL_GAP, width - POI_EDGE_CLEAR - maxW) : Math.max(left - COL_GAP, POI_EDGE_CLEAR + maxW);
|
|
2515
|
+
const totalH = items.length * step;
|
|
2516
|
+
let startY = cyMid - totalH / 2;
|
|
2517
|
+
startY = Math.max(
|
|
2518
|
+
POI_EDGE_CLEAR,
|
|
2519
|
+
Math.min(startY, height - totalH - POI_EDGE_CLEAR)
|
|
2520
|
+
);
|
|
2521
|
+
return items.map((o, i) => {
|
|
2522
|
+
const rowCy = startY + i * step + step / 2;
|
|
2523
|
+
return {
|
|
2524
|
+
o,
|
|
2525
|
+
colX,
|
|
2526
|
+
rowCy,
|
|
2527
|
+
rect: {
|
|
2528
|
+
x: side === "right" ? colX : colX - o.w,
|
|
2529
|
+
y: rowCy - poiLabH / 2,
|
|
2530
|
+
w: o.w,
|
|
2531
|
+
h: poiLabH
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
});
|
|
2535
|
+
};
|
|
2536
|
+
const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
|
|
2537
|
+
({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
|
|
2538
|
+
);
|
|
2539
|
+
const WATER_PREF = 1e3;
|
|
2540
|
+
const openness = (rect) => {
|
|
2541
|
+
const xs = [
|
|
2542
|
+
rect.x + rect.w * 0.15,
|
|
2543
|
+
rect.x + rect.w * 0.5,
|
|
2544
|
+
rect.x + rect.w * 0.85
|
|
2545
|
+
];
|
|
2546
|
+
const ys = [rect.y + rect.h * 0.25, rect.y + rect.h * 0.75];
|
|
2547
|
+
let waterHits = 0;
|
|
2548
|
+
for (const x of xs)
|
|
2549
|
+
for (const y of ys) if (fillAt(x, y) === water) waterHits++;
|
|
2550
|
+
const waterFrac = waterHits / (xs.length * ys.length);
|
|
2551
|
+
const edgeClear = Math.max(
|
|
2552
|
+
0,
|
|
2553
|
+
Math.min(
|
|
2554
|
+
rect.x,
|
|
2555
|
+
width - (rect.x + rect.w),
|
|
2556
|
+
rect.y,
|
|
2557
|
+
height - (rect.y + rect.h)
|
|
2558
|
+
)
|
|
2559
|
+
);
|
|
2560
|
+
return WATER_PREF * waterFrac + edgeClear * 0.1;
|
|
2561
|
+
};
|
|
2562
|
+
const columnSideScore = (items, side) => {
|
|
2563
|
+
const rows = columnRows(items, side);
|
|
2564
|
+
if (rows.length === 0) return -Infinity;
|
|
2565
|
+
return rows.reduce((s, { rect }) => s + openness(rect), 0) / rows.length;
|
|
2566
|
+
};
|
|
2567
|
+
const defaultColumnSide = (items) => columnSideScore(items, "right") >= columnSideScore(items, "left") ? "right" : "left";
|
|
2568
|
+
const commitColumn = (items, side, clusterId) => {
|
|
2569
|
+
for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
|
|
2570
|
+
obstacles.push(rect);
|
|
2571
|
+
labels.push({
|
|
2572
|
+
x: colX,
|
|
2573
|
+
y: rowCy + FONT2 / 3,
|
|
2574
|
+
text: o.text,
|
|
2575
|
+
anchor: side === "right" ? "start" : "end",
|
|
2576
|
+
color: palette.text,
|
|
2577
|
+
halo: false,
|
|
2578
|
+
haloColor: palette.bg,
|
|
2579
|
+
leader: {
|
|
2580
|
+
x1: o.p.cx,
|
|
2581
|
+
y1: o.p.cy,
|
|
2582
|
+
x2: side === "right" ? colX - 2 : colX + 2,
|
|
2583
|
+
y2: rowCy
|
|
2584
|
+
},
|
|
2585
|
+
leaderColor: o.p.fill,
|
|
2586
|
+
poiId: o.p.id,
|
|
2587
|
+
lineNumber: o.p.lineNumber,
|
|
2588
|
+
...clusterId !== void 0 && { clusterMember: clusterId }
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
const pushHidden = (p) => {
|
|
2593
|
+
const { text, w } = labelInfo(p);
|
|
2594
|
+
let x = p.cx + p.r + GAP;
|
|
2595
|
+
let anchor = "start";
|
|
2596
|
+
if (x + w > width) {
|
|
2597
|
+
x = p.cx - p.r - GAP - w;
|
|
2598
|
+
anchor = "end";
|
|
2599
|
+
}
|
|
2600
|
+
const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
|
|
2601
|
+
labels.push({
|
|
2602
|
+
x: anchor === "start" ? x : x + w,
|
|
2603
|
+
y: y + poiLabH / 2 + FONT2 / 3,
|
|
2604
|
+
text,
|
|
2605
|
+
anchor,
|
|
2606
|
+
color: palette.text,
|
|
2607
|
+
halo: false,
|
|
2608
|
+
haloColor: palette.bg,
|
|
2609
|
+
poiId: p.id,
|
|
2610
|
+
hidden: true,
|
|
2611
|
+
lineNumber: p.lineNumber
|
|
2612
|
+
});
|
|
2613
|
+
};
|
|
2614
|
+
const STACK_RADIAL_MAX = 4;
|
|
2615
|
+
const radialSide = (p, cx, cy) => {
|
|
2616
|
+
const dx = p.cx - cx;
|
|
2617
|
+
const dy = p.cy - cy;
|
|
2618
|
+
return Math.abs(dy) >= Math.abs(dx) ? dy <= 0 ? "above" : "below" : dx < 0 ? "left" : "right";
|
|
2619
|
+
};
|
|
2620
|
+
const tryStackRadial = (items, clusterId) => {
|
|
2621
|
+
const cluster = clusters.find((c) => c.id === clusterId);
|
|
2622
|
+
if (!cluster || items.length > STACK_RADIAL_MAX) return false;
|
|
2623
|
+
const temp = [];
|
|
2624
|
+
const seated = [];
|
|
2625
|
+
for (const { p, text, w } of items) {
|
|
2626
|
+
const pref = radialSide(p, cluster.cx, cluster.cy);
|
|
2627
|
+
const order = [
|
|
2628
|
+
pref,
|
|
2629
|
+
...["above", "below", "right", "left"].filter(
|
|
2630
|
+
(s) => s !== pref
|
|
2631
|
+
)
|
|
2632
|
+
];
|
|
2633
|
+
const side = order.find((s) => {
|
|
2634
|
+
const rect = inlineRect(p, w, s);
|
|
2635
|
+
return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect) && !temp.some((t) => rectsOverlap(t, rect));
|
|
2636
|
+
});
|
|
2637
|
+
if (side === void 0) return false;
|
|
2638
|
+
temp.push(inlineRect(p, w, side));
|
|
2639
|
+
seated.push({ p, text, w, side });
|
|
2640
|
+
}
|
|
2641
|
+
for (const { p, text, w, side } of seated)
|
|
2642
|
+
pushInline(p, text, w, side, clusterId);
|
|
2643
|
+
return true;
|
|
2644
|
+
};
|
|
2645
|
+
for (const [clusterId, members] of clusterMembersById) {
|
|
2646
|
+
if (members.length === 0) continue;
|
|
2647
|
+
const items = makeItems(members);
|
|
2648
|
+
if (tryStackRadial(items, clusterId)) continue;
|
|
2649
|
+
const cleanR = wouldColumnBeClean(items, "right");
|
|
2650
|
+
const cleanL = wouldColumnBeClean(items, "left");
|
|
2651
|
+
const side = cleanR && cleanL ? defaultColumnSide(items) : cleanR ? "right" : cleanL ? "left" : defaultColumnSide(items);
|
|
2652
|
+
commitColumn(items, side, clusterId);
|
|
2653
|
+
}
|
|
2654
|
+
const CLEAR_CAP = 48;
|
|
2655
|
+
const CLEAR_W = 8;
|
|
2656
|
+
const WATER_W = 0.25;
|
|
2657
|
+
const ptSegDist = (px, py, ax, ay, bx, by) => {
|
|
2658
|
+
const dx = bx - ax;
|
|
2659
|
+
const dy = by - ay;
|
|
2660
|
+
const len2 = dx * dx + dy * dy;
|
|
2661
|
+
let t = len2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0;
|
|
2662
|
+
t = Math.max(0, Math.min(1, t));
|
|
2663
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
2664
|
+
};
|
|
2665
|
+
const clearanceOf = (rect, ownId) => {
|
|
2666
|
+
const cxL = rect.x + rect.w / 2;
|
|
2667
|
+
const cyL = rect.y + rect.h / 2;
|
|
2668
|
+
let d = CLEAR_CAP;
|
|
2669
|
+
for (const q of pois) {
|
|
2670
|
+
if (q.id === ownId) continue;
|
|
2671
|
+
d = Math.min(d, Math.hypot(q.cx - cxL, q.cy - cyL) - q.r);
|
|
2672
|
+
}
|
|
2673
|
+
for (const s of legSegments)
|
|
2674
|
+
d = Math.min(d, ptSegDist(cxL, cyL, s[0], s[1], s[2], s[3]));
|
|
2675
|
+
return Math.max(0, d);
|
|
2676
|
+
};
|
|
2677
|
+
const LEGIBILITY = {
|
|
2678
|
+
right: 160,
|
|
2679
|
+
left: 160,
|
|
2680
|
+
above: 100,
|
|
2681
|
+
below: 100,
|
|
2682
|
+
"below-right": 0,
|
|
2683
|
+
"below-left": 0,
|
|
2684
|
+
"above-right": 0,
|
|
2685
|
+
"above-left": 0
|
|
2686
|
+
};
|
|
2687
|
+
const ALL_SIDES = Object.keys(LEGIBILITY);
|
|
2688
|
+
const placementScore = (p, w, side) => {
|
|
2689
|
+
const rect = inlineRect(p, w, side);
|
|
2690
|
+
return CLEAR_W * clearanceOf(rect, p.id) + WATER_W * openness(rect) + LEGIBILITY[side];
|
|
2691
|
+
};
|
|
2692
|
+
const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
|
|
2693
|
+
const clusterPending = [];
|
|
2694
|
+
for (const g of groups2) {
|
|
2695
|
+
const items = makeItems(g);
|
|
2696
|
+
if (g.length === 1) {
|
|
2697
|
+
const { p, text, w } = items[0];
|
|
2698
|
+
const scored = ALL_SIDES.filter((s) => inlineFits(p, w, s)).map(
|
|
2699
|
+
(s) => ({
|
|
2700
|
+
s,
|
|
2701
|
+
v: placementScore(p, w, s)
|
|
2702
|
+
})
|
|
2703
|
+
);
|
|
2704
|
+
if (scored.length === 0) {
|
|
2705
|
+
commitColumn(items, defaultColumnSide(items));
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
const side = scored.reduce((b, c) => c.v > b.v ? c : b).s;
|
|
2709
|
+
pushInline(p, text, w, side);
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
|
|
2713
|
+
const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
|
|
2714
|
+
const minCy = Math.min(...items.map((o) => o.p.cy));
|
|
2715
|
+
const maxCy = Math.max(...items.map((o) => o.p.cy));
|
|
2716
|
+
const diag = Math.hypot(right - left, maxCy - minCy);
|
|
2717
|
+
if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
|
|
2718
|
+
items.forEach((o) => pushHidden(o.p));
|
|
2719
|
+
} else {
|
|
2720
|
+
clusterPending.push(items);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
for (const items of clusterPending) {
|
|
2724
|
+
const cleanSides = ["right", "left"].filter(
|
|
2725
|
+
(s) => wouldColumnBeClean(items, s)
|
|
2726
|
+
);
|
|
2727
|
+
const side = cleanSides.length > 1 ? defaultColumnSide(items) : cleanSides[0];
|
|
2728
|
+
if (side) commitColumn(items, side);
|
|
2729
|
+
else items.forEach((o) => pushHidden(o.p));
|
|
2730
|
+
}
|
|
2731
|
+
const clearancePass = opts._poiClearancePass ?? 0;
|
|
2732
|
+
const MAX_CLEARANCE_PASSES = 4;
|
|
2733
|
+
if (clearancePass < MAX_CLEARANCE_PASSES && pois.length > 0) {
|
|
2734
|
+
const EDGE_CLEAR = POI_EDGE_CLEAR;
|
|
2735
|
+
const capH = Math.floor(width * 0.3);
|
|
2736
|
+
const capV = Math.floor(height * 0.3);
|
|
2737
|
+
const poiById2 = new Map(pois.map((p) => [p.id, p]));
|
|
2738
|
+
let needLeft = 0;
|
|
2739
|
+
let needRight = 0;
|
|
2740
|
+
let needTop = 0;
|
|
2741
|
+
let needBottom = 0;
|
|
2742
|
+
for (const p of pois) {
|
|
2743
|
+
needLeft = Math.max(needLeft, EDGE_CLEAR - (p.cx - p.r));
|
|
2744
|
+
needRight = Math.max(needRight, p.cx + p.r + EDGE_CLEAR - width);
|
|
2745
|
+
needTop = Math.max(needTop, EDGE_CLEAR - (p.cy - p.r));
|
|
2746
|
+
needBottom = Math.max(needBottom, p.cy + p.r + EDGE_CLEAR - height);
|
|
2747
|
+
}
|
|
2748
|
+
for (const l of labels) {
|
|
2749
|
+
if (l.poiId === void 0) continue;
|
|
2750
|
+
const p = poiById2.get(l.poiId);
|
|
2751
|
+
if (!p) continue;
|
|
2752
|
+
if (l.hidden || l.leader) {
|
|
2753
|
+
const lw = l.hidden ? labelInfo(p).w : measureLegendText(l.text, FONT2);
|
|
2754
|
+
const reach = p.r + COL_GAP + lw + EDGE_CLEAR;
|
|
2755
|
+
if (p.cx >= width / 2)
|
|
2756
|
+
needRight = Math.max(needRight, p.cx + reach - width);
|
|
2757
|
+
else needLeft = Math.max(needLeft, reach - p.cx);
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
const w = measureLegendText(l.text, FONT2);
|
|
2761
|
+
const boxLeft = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
|
|
2762
|
+
const boxTop = l.y - FONT2 / 3 - poiLabH / 2;
|
|
2763
|
+
const boxRight = boxLeft + w;
|
|
2764
|
+
const boxBottom = boxTop + poiLabH;
|
|
2765
|
+
needLeft = Math.max(needLeft, EDGE_CLEAR - boxLeft);
|
|
2766
|
+
needRight = Math.max(needRight, boxRight + EDGE_CLEAR - width);
|
|
2767
|
+
needTop = Math.max(needTop, EDGE_CLEAR - boxTop);
|
|
2768
|
+
needBottom = Math.max(needBottom, boxBottom + EDGE_CLEAR - height);
|
|
2769
|
+
}
|
|
2770
|
+
needLeft = Math.min(Math.max(0, Math.ceil(needLeft)), capH);
|
|
2771
|
+
needRight = Math.min(Math.max(0, Math.ceil(needRight)), capH);
|
|
2772
|
+
needTop = Math.min(Math.max(0, Math.ceil(needTop)), capV);
|
|
2773
|
+
needBottom = Math.min(Math.max(0, Math.ceil(needBottom)), capV);
|
|
2774
|
+
if (needLeft >= 1 || needRight >= 1 || needTop >= 1 || needBottom >= 1) {
|
|
2775
|
+
const prev = opts._calloutReserve;
|
|
2776
|
+
const left = Math.min((prev?.left ?? 0) + needLeft, capH);
|
|
2777
|
+
const right = Math.min((prev?.right ?? 0) + needRight, capH);
|
|
2778
|
+
const top = Math.min((prev?.top ?? 0) + needTop, capV);
|
|
2779
|
+
const bottom = Math.min((prev?.bottom ?? 0) + needBottom, capV);
|
|
2780
|
+
return layoutMap(resolved, data, size, {
|
|
2781
|
+
...opts,
|
|
2782
|
+
_poiClearancePass: clearancePass + 1,
|
|
2783
|
+
_calloutReserve: {
|
|
2784
|
+
...left > 0 && { left },
|
|
2785
|
+
...right > 0 && { right },
|
|
2786
|
+
...top > 0 && { top },
|
|
2787
|
+
...bottom > 0 && { bottom }
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
if (regionLabelGuards.length > 0) {
|
|
2794
|
+
const PAD = 2;
|
|
2795
|
+
const poiRects = labels.filter((l) => l.poiId !== void 0 && l.hidden !== true).map((l) => {
|
|
2796
|
+
const w = measureLegendText(l.text, FONT2);
|
|
2797
|
+
const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
|
|
2798
|
+
return {
|
|
2799
|
+
x: x - PAD,
|
|
2800
|
+
y: l.y - FONT2,
|
|
2801
|
+
w: w + 2 * PAD,
|
|
2802
|
+
h: FONT2 * 1.4 + 2 * PAD
|
|
2803
|
+
};
|
|
2804
|
+
});
|
|
2805
|
+
for (const g of regionLabelGuards) {
|
|
2806
|
+
if (poiRects.some((pr) => rectsOverlap(pr, g.rect))) {
|
|
2807
|
+
const i = labels.indexOf(g.label);
|
|
2808
|
+
if (i >= 0) labels.splice(i, 1);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
if (legs.some((lg) => lg.label !== void 0)) {
|
|
2813
|
+
const committedBoxes = labels.filter((l) => !l.hidden).map((l) => {
|
|
2814
|
+
const w = labelW(l.text);
|
|
2815
|
+
const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
|
|
2816
|
+
return { x, y: l.y - labelH / 2, w, h: labelH };
|
|
2817
|
+
});
|
|
2818
|
+
const placedEdge = [];
|
|
2819
|
+
const T_LIST = [0.5, 0.42, 0.58, 0.34, 0.66, 0.28, 0.72];
|
|
2820
|
+
for (let i = 0; i < legs.length; i++) {
|
|
2821
|
+
const lg = legs[i];
|
|
2822
|
+
if (lg.label === void 0) continue;
|
|
2823
|
+
const a = poiScreen.get(lg.fromId);
|
|
2824
|
+
const b = poiScreen.get(lg.toId);
|
|
2825
|
+
if (!a || !b) continue;
|
|
2826
|
+
const w = labelW(lg.label);
|
|
2827
|
+
const dx = b.cx - a.cx;
|
|
2828
|
+
const dy = b.cy - a.cy;
|
|
2829
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
2830
|
+
const nx = -dy / len;
|
|
2831
|
+
const ny = dx / len;
|
|
2832
|
+
const perp = labelH + 2;
|
|
2833
|
+
const stroke = lg.width ?? W_MIN;
|
|
2834
|
+
const clearW = stroke / 2 + labelH / 2 + 3;
|
|
2835
|
+
const offList = stroke >= 4 ? [clearW, -clearW, clearW + perp, -(clearW + perp), 0] : [0, perp, -perp, 2 * perp, -2 * perp];
|
|
2836
|
+
const pm = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
|
|
2837
|
+
lg.d
|
|
2838
|
+
);
|
|
2839
|
+
const pointAt = (t) => {
|
|
2840
|
+
if (pm?.[5] !== void 0) {
|
|
2841
|
+
const u = 1 - t;
|
|
2842
|
+
return [
|
|
2843
|
+
u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
|
|
2844
|
+
u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
|
|
2845
|
+
];
|
|
2846
|
+
}
|
|
2847
|
+
if (pm?.[3] !== void 0)
|
|
2848
|
+
return [
|
|
2849
|
+
+pm[1] + (+pm[3] - +pm[1]) * t,
|
|
2850
|
+
+pm[2] + (+pm[4] - +pm[2]) * t
|
|
2851
|
+
];
|
|
2852
|
+
return [a.cx + dx * t, a.cy + dy * t];
|
|
2853
|
+
};
|
|
2854
|
+
const candidates = [];
|
|
2855
|
+
for (const off of offList)
|
|
2856
|
+
for (const t of T_LIST) {
|
|
2857
|
+
const [bx, by] = pointAt(t);
|
|
2858
|
+
candidates.push([bx + nx * off, by + ny * off - 4]);
|
|
2859
|
+
}
|
|
2860
|
+
const clean = ([cx2, cy2]) => {
|
|
2861
|
+
const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
|
|
2862
|
+
if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
|
|
2863
|
+
return false;
|
|
2864
|
+
if (markers.some((m) => rectCircleOverlap(rect2, m))) return false;
|
|
2865
|
+
if (committedBoxes.some((o) => rectsOverlap(o, rect2))) return false;
|
|
2866
|
+
if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
|
|
2867
|
+
return true;
|
|
2868
|
+
};
|
|
2869
|
+
const mid = pointAt(0.5);
|
|
2870
|
+
const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
|
|
2871
|
+
const style = labelOnFill(fillAt(cx, cy));
|
|
2872
|
+
legs[i] = {
|
|
2873
|
+
...lg,
|
|
2874
|
+
labelX: cx,
|
|
2875
|
+
labelY: cy,
|
|
2876
|
+
labelColor: style.color,
|
|
2877
|
+
labelHalo: style.halo,
|
|
2878
|
+
labelHaloColor: style.haloColor
|
|
2879
|
+
};
|
|
2880
|
+
const rect = { x: cx - w / 2, y: cy - labelH / 2, w, h: labelH };
|
|
2881
|
+
placedEdge.push(rect);
|
|
2882
|
+
obstacles.push(rect);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
if (resolved.directives.noContextLabels !== true) {
|
|
2886
|
+
for (const l of labels) {
|
|
2887
|
+
if (l.hidden) continue;
|
|
2888
|
+
const w = labelW(l.text);
|
|
2889
|
+
const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
|
|
2890
|
+
obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
|
|
2891
|
+
}
|
|
2892
|
+
for (const box of insets)
|
|
2893
|
+
obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
|
|
2894
|
+
const countryCandidates = [];
|
|
2895
|
+
const rawCountries = [];
|
|
2896
|
+
for (const f of worldLayer.values()) {
|
|
2897
|
+
const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
|
|
2898
|
+
if (!iso || regionById.has(iso) || labeledRegionIds.has(iso)) continue;
|
|
2899
|
+
if (usContext && iso === "US") continue;
|
|
2900
|
+
let hasReferencedSub = false;
|
|
2901
|
+
for (const k of regionById.keys())
|
|
2902
|
+
if (k.startsWith(iso + "-")) {
|
|
2903
|
+
hasReferencedSub = true;
|
|
2904
|
+
break;
|
|
2905
|
+
}
|
|
2906
|
+
if (hasReferencedSub) continue;
|
|
2907
|
+
const b = path.bounds(f);
|
|
2908
|
+
const [x0, y0] = b[0];
|
|
2909
|
+
const [x1, y1] = b[1];
|
|
2910
|
+
if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
|
|
2911
|
+
const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
|
|
2912
|
+
const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
|
|
2913
|
+
rawCountries.push({
|
|
2914
|
+
f,
|
|
2915
|
+
iso,
|
|
2916
|
+
name: f.properties?.name ?? iso,
|
|
2917
|
+
bbox: [x0, y0, x1, y1],
|
|
2918
|
+
anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null,
|
|
2919
|
+
curatedLngLat: anchorLngLat ?? null,
|
|
2920
|
+
area: (x1 - x0) * (y1 - y0)
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
const cBand = tierBand(Math.max(dLonSpan, dLatSpan));
|
|
2924
|
+
const cBudget = labelBudget(width, height, cBand);
|
|
2925
|
+
const nationsUnconditional = cBand !== "world";
|
|
2926
|
+
const topN = cBudget + COUNTRY_POS_TOPN_MARGIN;
|
|
2927
|
+
const rankOrder = rawCountries.map((r, idx) => ({ idx, area: r.area })).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area).slice(0, topN);
|
|
2928
|
+
const genIdx = new Set(rankOrder.map((r) => r.idx));
|
|
2929
|
+
for (let i = 0; i < rawCountries.length; i++) {
|
|
2930
|
+
const r = rawCountries[i];
|
|
2931
|
+
let anchor = r.anchor;
|
|
2932
|
+
let positions;
|
|
2933
|
+
if (genIdx.has(i) && anchor) {
|
|
2934
|
+
const gb = geoBounds(r.f);
|
|
2935
|
+
const gen = countryLabelPositions({
|
|
2936
|
+
geometry: r.f.geometry,
|
|
2937
|
+
bounds: gb,
|
|
2938
|
+
project,
|
|
2939
|
+
width,
|
|
2940
|
+
height,
|
|
2941
|
+
curated: r.curatedLngLat
|
|
2942
|
+
});
|
|
2943
|
+
if (gen.length) {
|
|
2944
|
+
positions = gen.map((p) => p.screen);
|
|
2945
|
+
anchor = positions[0];
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
countryCandidates.push({
|
|
2949
|
+
name: r.name,
|
|
2950
|
+
bbox: r.bbox,
|
|
2951
|
+
anchor,
|
|
2952
|
+
curatedAnchor: !!r.curatedLngLat,
|
|
2953
|
+
bordering: nationsUnconditional,
|
|
2954
|
+
...positions ? { positions } : {}
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
|
|
2958
|
+
(id) => id.startsWith("US-")
|
|
2959
|
+
);
|
|
2960
|
+
if (usLayer && framedStateContainers) {
|
|
2961
|
+
for (const [iso, f] of usLayer) {
|
|
2962
|
+
if (regionById.has(iso) || labeledRegionIds.has(iso)) continue;
|
|
2963
|
+
const viewF = cullFeatureToView(f);
|
|
2964
|
+
if (!viewF) continue;
|
|
2965
|
+
const b = path.bounds(viewF);
|
|
2966
|
+
const [x0, y0] = b[0];
|
|
2967
|
+
const [x1, y1] = b[1];
|
|
2968
|
+
if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
|
|
2969
|
+
const a = path.centroid(viewF);
|
|
2970
|
+
countryCandidates.push({
|
|
2971
|
+
name: f.properties?.name ?? iso,
|
|
2972
|
+
bbox: [x0, y0, x1, y1],
|
|
2973
|
+
anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
const contextLabels = placeContextLabels({
|
|
2978
|
+
projection: resolved.projection,
|
|
2979
|
+
dLonSpan,
|
|
2980
|
+
dLatSpan,
|
|
2981
|
+
width,
|
|
2982
|
+
height,
|
|
2983
|
+
waterBodies: data.waterBodies,
|
|
2984
|
+
countries: countryCandidates,
|
|
2985
|
+
palette,
|
|
2986
|
+
// One muted, contrast-floored tone shared with the orientation backdrop
|
|
2987
|
+
// (region pass) so a context country (Canada) and an orientation region
|
|
2988
|
+
// (Minnesota) read identically — never below the WCAG-AA floor, no fade.
|
|
2989
|
+
countryTone: backdropLandTone,
|
|
2990
|
+
waterTone: backdropWaterTone,
|
|
2991
|
+
project,
|
|
2992
|
+
collides,
|
|
2993
|
+
// Water labels must stay over open water — `fillAt` returns the ocean
|
|
2994
|
+
// backdrop colour off-land and a region fill on-land (lakes/states count
|
|
2995
|
+
// as land here, which is the safe side for an ocean name).
|
|
2996
|
+
overLand: (x, y) => fillAt(x, y) !== water
|
|
2997
|
+
});
|
|
2998
|
+
labels.push(...contextLabels);
|
|
2999
|
+
}
|
|
3000
|
+
const cityDots = [];
|
|
3001
|
+
if (resolved.directives.noCities !== true) {
|
|
3002
|
+
const CITY_DOT_SPACING = 12;
|
|
3003
|
+
const CITY_DOT_CAP = 220;
|
|
3004
|
+
const SPACING_SQ = CITY_DOT_SPACING * CITY_DOT_SPACING;
|
|
3005
|
+
const CITY_DOT_R_MIN = 0.7;
|
|
3006
|
+
const CITY_DOT_R_MAX = 2.6;
|
|
3007
|
+
const CITY_POP_MIN = 5e4;
|
|
3008
|
+
const CITY_POP_MAX = 15e6;
|
|
3009
|
+
const LOG_MIN = Math.log10(CITY_POP_MIN);
|
|
3010
|
+
const LOG_SPAN = Math.log10(CITY_POP_MAX) - LOG_MIN;
|
|
3011
|
+
const cityDotRadius = (pop) => {
|
|
3012
|
+
if (!(pop > CITY_POP_MIN)) return CITY_DOT_R_MIN;
|
|
3013
|
+
const t = Math.min(1, (Math.log10(pop) - LOG_MIN) / LOG_SPAN);
|
|
3014
|
+
return CITY_DOT_R_MIN + t * (CITY_DOT_R_MAX - CITY_DOT_R_MIN);
|
|
3015
|
+
};
|
|
3016
|
+
const placed = pois.map((p) => ({
|
|
3017
|
+
x: p.cx,
|
|
3018
|
+
y: p.cy
|
|
3019
|
+
}));
|
|
3020
|
+
const sorted = [...data.gazetteer.cities].sort((a, b) => b[3] - a[3]);
|
|
3021
|
+
for (const c of sorted) {
|
|
3022
|
+
if (cityDots.length >= CITY_DOT_CAP) break;
|
|
3023
|
+
const lat = c[0];
|
|
3024
|
+
const lon = c[1];
|
|
3025
|
+
const p = project(lon, lat);
|
|
3026
|
+
if (!p) continue;
|
|
3027
|
+
const [px, py] = p;
|
|
3028
|
+
if (px < 0 || px > width || py < 0 || py > height) continue;
|
|
3029
|
+
let tooClose = false;
|
|
3030
|
+
for (const q of placed) {
|
|
3031
|
+
const dx = q.x - px;
|
|
3032
|
+
const dy = q.y - py;
|
|
3033
|
+
if (dx * dx + dy * dy < SPACING_SQ) {
|
|
3034
|
+
tooClose = true;
|
|
3035
|
+
break;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
if (tooClose) continue;
|
|
3039
|
+
placed.push({ x: px, y: py });
|
|
3040
|
+
cityDots.push({ cx: px, cy: py, r: cityDotRadius(c[3]) });
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
return {
|
|
3044
|
+
width,
|
|
3045
|
+
height,
|
|
3046
|
+
background: water,
|
|
3047
|
+
title: shownTitle,
|
|
3048
|
+
...resolved.subtitle !== void 0 && { subtitle: resolved.subtitle },
|
|
3049
|
+
...resolved.caption !== void 0 && { caption: resolved.caption },
|
|
3050
|
+
regions,
|
|
3051
|
+
rivers,
|
|
3052
|
+
relief,
|
|
3053
|
+
reliefHatch,
|
|
3054
|
+
coastlineStyle,
|
|
3055
|
+
legs,
|
|
3056
|
+
pois,
|
|
3057
|
+
cityDots,
|
|
3058
|
+
clusters,
|
|
3059
|
+
labels,
|
|
3060
|
+
legend,
|
|
3061
|
+
insets,
|
|
3062
|
+
insetRegions,
|
|
3063
|
+
projection,
|
|
3064
|
+
stretch: stretchParams,
|
|
3065
|
+
diagnostics: []
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
export {
|
|
3070
|
+
mapLegendGroups,
|
|
3071
|
+
mapLegendConfig,
|
|
3072
|
+
mapBackgroundColor,
|
|
3073
|
+
mapNeutralLandColor,
|
|
3074
|
+
buildMapProjection,
|
|
3075
|
+
parsePathRings,
|
|
3076
|
+
albersSkewFallback,
|
|
3077
|
+
layoutMap
|
|
3078
|
+
};
|
|
3079
|
+
//# sourceMappingURL=chunk-IPWPWM6R.js.map
|