@diagrammo/dgmo 0.60.0 → 0.62.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/README.md +1 -1
- package/dist/advanced.d.ts +382 -396
- package/dist/advanced.js +217 -105
- package/dist/auto.js +142 -141
- package/dist/block.js +51 -106
- package/dist/{calculator-ORAKSL5D.js → calculator-M326KQFW.js} +3 -3
- package/dist/{chart-meta-C6UaqhTP.d.ts → chart-meta-D98NbYDp.d.ts} +9 -3
- package/dist/chart-meta.d.ts +2 -2
- package/dist/chart-meta.js +44 -46
- package/dist/{charts-d3-FSNSASP7.js → charts-d3-3WMDA5Z5.js} +6 -6
- package/dist/{chunk-ON4MEUC4.js → chunk-23RWHHI5.js} +11 -2
- package/dist/{chunk-SLGHGT76.js → chunk-2CMM36O7.js} +2 -2
- package/dist/{chunk-XVOGT46C.js → chunk-2PTK46E2.js} +2 -2
- package/dist/{chunk-DXSRQLD4.js → chunk-37EHGTQ2.js} +2205 -2019
- package/dist/{chunk-4IFPX4J6.js → chunk-3A2HZBFD.js} +2 -2
- package/dist/{chunk-OJD4FU6H.js → chunk-3A7KMF33.js} +2 -2
- package/dist/{chunk-DKCEG3N6.js → chunk-3HYRJAXO.js} +4 -4
- package/dist/{chunk-V5OBLPLA.js → chunk-3OLO2MZ6.js} +4 -4
- package/dist/{chunk-P3343TTF.js → chunk-3PVUGECM.js} +95 -117
- package/dist/{chunk-Y5KXCKI5.js → chunk-4BEAHH6G.js} +5 -5
- package/dist/{chunk-ZPUVAJTW.js → chunk-4C3BTTOL.js} +4 -4
- package/dist/{chunk-EKKS325Q.js → chunk-4GQ6I46C.js} +2 -2
- package/dist/{chunk-4B5YISOR.js → chunk-536EBUPG.js} +16 -1
- package/dist/{chunk-DKG2ADEN.js → chunk-63WPPHOM.js} +2 -2
- package/dist/{chunk-I53E3L5M.js → chunk-65QX6LYT.js} +3 -3
- package/dist/{chunk-243KZTTS.js → chunk-6BVIZW5N.js} +7 -7
- package/dist/{chunk-6RR4XUOB.js → chunk-7BJQUII6.js} +6 -6
- package/dist/{chunk-X5CJAFZF.js → chunk-7FTGNJQ2.js} +3 -3
- package/dist/{chunk-PTYUSRGT.js → chunk-7QCYHZVO.js} +5 -5
- package/dist/{chunk-ZU2EXJEL.js → chunk-A34FVVVI.js} +2 -2
- package/dist/{chunk-BGBGWFUI.js → chunk-A5FNLWKP.js} +3 -3
- package/dist/{chunk-BG23Y7JD.js → chunk-AFCVHK3G.js} +3 -3
- package/dist/{chunk-QMKFLQL4.js → chunk-AKD3YZ54.js} +3 -3
- package/dist/{chunk-BVPRRSMX.js → chunk-B25BEH3F.js} +3 -3
- package/dist/{chunk-GCZQAT4Z.js → chunk-CHOKOYSC.js} +3 -3
- package/dist/{chunk-2NSR6INI.js → chunk-CSUF6PNF.js} +3 -3
- package/dist/{chunk-ZTSBP3AH.js → chunk-CUYDE77F.js} +3 -3
- package/dist/{chunk-VHO3YBTB.js → chunk-CVVICCSB.js} +24 -2
- package/dist/{chunk-K5L3LXT3.js → chunk-DHIBVN2D.js} +104 -93
- package/dist/{chunk-ZBOSGLES.js → chunk-DJC5M7VC.js} +3 -3
- package/dist/{chunk-6XWZKCVS.js → chunk-DPFCFU2K.js} +3 -3
- package/dist/{chunk-CDIBHGPW.js → chunk-DY4VJYG7.js} +3 -3
- package/dist/{chunk-32G7X6BX.js → chunk-DZZWR6GT.js} +6 -6
- package/dist/{chunk-6YY56KU2.js → chunk-E2GEV3BI.js} +3 -3
- package/dist/{chunk-PSWWCLLI.js → chunk-EK4QX7NQ.js} +3 -3
- package/dist/{chunk-N4NFZIXE.js → chunk-FVYHDZ6Q.js} +2 -2
- package/dist/{chunk-DC3EBYIG.js → chunk-FXLGBUIQ.js} +5 -5
- package/dist/{chunk-VVCYOF3P.js → chunk-HBSKE3ML.js} +3 -3
- package/dist/{chunk-IO436ODZ.js → chunk-HJ6TMHQG.js} +4 -4
- package/dist/{chunk-6F74K4JL.js → chunk-I7IQ2NEJ.js} +3 -3
- package/dist/{chunk-VGFUVFJA.js → chunk-IL5UVUMK.js} +3 -3
- package/dist/{chunk-S5OQWV54.js → chunk-INLEOJIX.js} +2 -2
- package/dist/{chunk-35SOKGIP.js → chunk-JU2VMFYC.js} +2 -2
- package/dist/{chunk-DDQ47BMP.js → chunk-JWJBWNQL.js} +3 -3
- package/dist/{chunk-ZJR2YBVN.js → chunk-KOHIF4LD.js} +2 -2
- package/dist/{chunk-CWOKQSBQ.js → chunk-KRTO7NRK.js} +2 -2
- package/dist/{chunk-TL6WZWSZ.js → chunk-LALFMQ7Q.js} +2 -2
- package/dist/{chunk-MROSEZHE.js → chunk-LFAQ4BLZ.js} +3 -3
- package/dist/{chunk-EYP34TFP.js → chunk-LQR46XGR.js} +2 -2
- package/dist/{chunk-QABZKAKF.js → chunk-LRKGF7RN.js} +6 -6
- package/dist/{chunk-LLA5C2TM.js → chunk-MDI5FMWO.js} +2 -2
- package/dist/{chunk-GZAKQDX7.js → chunk-MRD4QWFK.js} +3 -3
- package/dist/{chunk-EZGWUS5G.js → chunk-NEV5IW33.js} +5 -5
- package/dist/{chunk-HQI7S72D.js → chunk-NKRVFQGO.js} +4 -4
- package/dist/{chunk-V5PHEH2M.js → chunk-NMIWEUIG.js} +2 -2
- package/dist/{chunk-OSXJY6SY.js → chunk-NSJK7F2V.js} +3 -3
- package/dist/{chunk-6KWE5553.js → chunk-O42EAZUV.js} +3 -3
- package/dist/{chunk-LXUBC5H4.js → chunk-OIK6Y2EJ.js} +5 -5
- package/dist/{chunk-EQGLGTU2.js → chunk-OLPWKS5L.js} +3 -3
- package/dist/{chunk-IXY2IPOW.js → chunk-P2PUWV3Y.js} +233 -58
- package/dist/{chunk-4VFQPEC6.js → chunk-PFGJ7SCM.js} +6 -6
- package/dist/{chunk-DNSEG6P2.js → chunk-PJDX4IKR.js} +2 -2
- package/dist/{chunk-JAHV7RFV.js → chunk-PTKCX5ZE.js} +4 -4
- package/dist/{chunk-U5UKFXHB.js → chunk-Q3XVMFVF.js} +2 -2
- package/dist/{chunk-SJT4W23S.js → chunk-QE4GTFZS.js} +2 -2
- package/dist/{chunk-TR7WNZFJ.js → chunk-QFNRHY62.js} +3 -3
- package/dist/{chunk-R53THUTP.js → chunk-QMYJO4EL.js} +4 -4
- package/dist/{chunk-IH3GHOHU.js → chunk-QQADQBKG.js} +4 -4
- package/dist/{chunk-TDZRLG7I.js → chunk-RF5YDIYT.js} +2 -2
- package/dist/{chunk-HFOSP6E2.js → chunk-RKTIIU7Z.js} +3 -3
- package/dist/{chunk-SEDLXFTX.js → chunk-SBOG6R6A.js} +4 -4
- package/dist/{chunk-T7FMXKIG.js → chunk-SF3JAWMY.js} +3 -3
- package/dist/{chunk-GSSCUREO.js → chunk-SMHTYVMK.js} +6 -6
- package/dist/{chunk-SQLVM3G7.js → chunk-SR7NGZNM.js} +4 -4
- package/dist/{chunk-VGCL263O.js → chunk-SSR6JAR3.js} +2 -2
- package/dist/{chunk-HPNOT5GQ.js → chunk-TJCHULP5.js} +2 -2
- package/dist/{chunk-PLDCXGLQ.js → chunk-TRP7PC5Z.js} +5 -5
- package/dist/{chunk-3O3W2JS2.js → chunk-U42ZEK7L.js} +2 -2
- package/dist/{chunk-UYY7MVJK.js → chunk-U7CXUGQS.js} +3 -3
- package/dist/{chunk-IOUUQLDE.js → chunk-UKZ7K5GS.js} +2 -2
- package/dist/{chunk-X5AIFXB7.js → chunk-USHDV2AF.js} +4 -4
- package/dist/{chunk-TJV3J74Y.js → chunk-UTOELEK2.js} +3 -3
- package/dist/{chunk-E5G55SYS.js → chunk-UZ3JPYD3.js} +3 -3
- package/dist/{chunk-ZP2FV4WO.js → chunk-VWCGN2ST.js} +3 -3
- package/dist/{chunk-W2Z4KS72.js → chunk-VXBEG3IB.js} +3 -3
- package/dist/{chunk-Z4RFILRE.js → chunk-W32HMBPW.js} +3 -3
- package/dist/{chunk-KZFAEMQQ.js → chunk-X2X7MTKH.js} +3 -3
- package/dist/{chunk-5USRKIR3.js → chunk-X4FCKGK2.js} +4 -4
- package/dist/{chunk-MM4Z32EH.js → chunk-XC7BUB3P.js} +2 -2
- package/dist/{chunk-XS7CL7NX.js → chunk-XHFXA6RP.js} +2 -2
- package/dist/{chunk-JQDZHYGU.js → chunk-XJZCXNWR.js} +3 -3
- package/dist/{chunk-XZXXSZ7E.js → chunk-YJCILMEA.js} +4 -4
- package/dist/{chunk-GSVHOF3J.js → chunk-YXI6Y7YY.js} +2 -2
- package/dist/cloud-reference.d.ts +24 -4
- package/dist/cloud-reference.js +5 -1
- package/dist/completion-registry.js +5 -7
- package/dist/{registry-C0HvobTb.d.ts → dgmo-router-Dd5PTjxl.d.ts} +271 -98
- package/dist/dgmo-router-Z5N6QLIC.js +89 -0
- package/dist/{dimensions-PC6WLKCC.js → dimensions-ES6O2T4H.js} +6 -6
- package/dist/editor.d.ts +9 -2
- package/dist/editor.js +229 -57
- package/dist/element.js +143 -142
- package/dist/{flowchart-parser-PZQY2UC4.js → flowchart-parser-T4XZRSA7.js} +4 -4
- package/dist/{flowchart-renderer-RS6IRRE7.js → flowchart-renderer-XFDCX7MZ.js} +5 -5
- package/dist/highlight.js +229 -57
- package/dist/index.d.ts +21 -10
- package/dist/index.js +55 -66
- package/dist/{layout-GKWT37FH.js → layout-H4H4OFE4.js} +3 -3
- package/dist/{layout-W423CCED.js → layout-OH4VQ3OI.js} +4 -4
- package/dist/{layout-LH6LNIUV.js → layout-QTZO6CW5.js} +3 -3
- package/dist/{layout-AUAW5U74.js → layout-SKQMIXTG.js} +4 -4
- package/dist/{layout-GU6POUGD.js → layout-UU4VJTOP.js} +4 -4
- package/dist/{layout-MNQZ3Q45.js → layout-WCOEZPOI.js} +4 -4
- package/dist/live-link-resolve.js +1 -1
- package/dist/{parser-NJGZF5MK.js → parser-2BGDJJ2L.js} +5 -5
- package/dist/{parser-3QHLZK6F.js → parser-2K4Z7RNT.js} +3 -3
- package/dist/{parser-6HT34DZ3.js → parser-3KFNCRLD.js} +4 -4
- package/dist/{parser-WXZU47Z4.js → parser-4CMMKTRS.js} +3 -3
- package/dist/{parser-BNGLB2VS.js → parser-56746U6W.js} +4 -4
- package/dist/{parser-56HMQG5C.js → parser-5JS6VM6L.js} +5 -5
- package/dist/{parser-2OJSWKXO.js → parser-5ZUEF4WE.js} +4 -4
- package/dist/{parser-7C5GZCAU.js → parser-63BQDUWB.js} +4 -4
- package/dist/{parser-TPCKCQ4Z.js → parser-6VNVZO5F.js} +4 -4
- package/dist/{parser-6S65S3DX.js → parser-AZZHRVJF.js} +4 -4
- package/dist/{parser-WI2B7IL3.js → parser-BGX5ODHF.js} +3 -3
- package/dist/{parser-TABGTEHH.js → parser-DLUOCPMD.js} +3 -3
- package/dist/{parser-CXLR6MAY.js → parser-DSNINT2K.js} +3 -3
- package/dist/{parser-RMUGOZO3.js → parser-FTC7FZ6Z.js} +4 -4
- package/dist/{parser-5I7W54EA.js → parser-GHLWPCIX.js} +4 -4
- package/dist/{parser-BVC642CN.js → parser-J75RSQFI.js} +4 -4
- package/dist/{parser-IWQQY5VU.js → parser-JZPSIWRC.js} +4 -4
- package/dist/{parser-QCSDU4FU.js → parser-KN7H2RQC.js} +3 -3
- package/dist/{parser-4QYDFXV2.js → parser-KRF4S5RW.js} +4 -4
- package/dist/{parser-4OU5P3BM.js → parser-KSIYWKYF.js} +4 -4
- package/dist/{parser-UB6XGLTC.js → parser-MCUKPCYE.js} +4 -4
- package/dist/{parser-G7KP6LZH.js → parser-MUYBWGUB.js} +3 -3
- package/dist/{parser-QCYEJOLZ.js → parser-NWERESX3.js} +4 -4
- package/dist/{parser-63LK7YBR.js → parser-OEBFJHT4.js} +5 -5
- package/dist/{parser-DROHJTBZ.js → parser-P7T2BGAI.js} +4 -4
- package/dist/{parser-7W7DPIGR.js → parser-RAKD267X.js} +4 -4
- package/dist/{parser-AD5OAVX6.js → parser-RBY2UFTE.js} +3 -3
- package/dist/{parser-5BHZMV3O.js → parser-SRUPRH77.js} +4 -4
- package/dist/{parser-QJ7XK7AB.js → parser-SSUMVOJA.js} +4 -4
- package/dist/{parser-BLCDJB7N.js → parser-UKQC7N6K.js} +4 -4
- package/dist/{parser-AWO3N6YH.js → parser-VMGTNHP2.js} +3 -3
- package/dist/{parser-55CSWK3N.js → parser-WHDC7OV2.js} +4 -4
- package/dist/{parser-453DEZ4P.js → parser-YO2JBE52.js} +4 -4
- package/dist/{renderer-G24W6J6H.js → renderer-2DKA5QXA.js} +4 -4
- package/dist/{renderer-66YB63F4.js → renderer-2PGIIOFN.js} +7 -7
- package/dist/{renderer-RH5OJ6Q6.js → renderer-35F3XMZH.js} +5 -5
- package/dist/{renderer-3Z5MSBWR.js → renderer-4RQ5PGHY.js} +6 -6
- package/dist/{renderer-LGSSP3JI.js → renderer-62FFDWMY.js} +6 -6
- package/dist/{renderer-SHMFNFE4.js → renderer-63MLWS6L.js} +7 -7
- package/dist/{renderer-GFZT6Q5F.js → renderer-6QW5OAJG.js} +5 -5
- package/dist/renderer-A5GFW2XH.js +76 -0
- package/dist/{renderer-H5C2JN62.js → renderer-AMCTCBV3.js} +5 -5
- package/dist/{renderer-PGMTAQIE.js → renderer-AS6LLDZM.js} +5 -5
- package/dist/{renderer-YYG4LYKS.js → renderer-AVR3LBTS.js} +6 -6
- package/dist/{renderer-LPQUBMXZ.js → renderer-BBBN7QNX.js} +4 -4
- package/dist/{renderer-DQLSUTSU.js → renderer-CHHYNEK7.js} +5 -5
- package/dist/{renderer-J76L474X.js → renderer-CU6BEHHV.js} +3 -3
- package/dist/{renderer-NE2GQBAU.js → renderer-CXRSD26Q.js} +7 -7
- package/dist/{renderer-JO27QMZB.js → renderer-DJ5DP3RH.js} +5 -5
- package/dist/{renderer-ZMAPGMF2.js → renderer-DRLZELXM.js} +5 -5
- package/dist/{renderer-BTFZWWIG.js → renderer-GESKWINV.js} +7 -7
- package/dist/{renderer-RB4FJJYN.js → renderer-IG5PQXEM.js} +7 -7
- package/dist/{renderer-PTLCAUC6.js → renderer-JBF2PAVP.js} +5 -5
- package/dist/{renderer-RRIXAQSF.js → renderer-JL7L5Y5M.js} +5 -5
- package/dist/{renderer-A3QD5R2C.js → renderer-JSFWNV37.js} +6 -6
- package/dist/{renderer-U55GLRDJ.js → renderer-M7YOXAGC.js} +3 -3
- package/dist/{renderer-5INUWSXC.js → renderer-Q2YHQQSN.js} +8 -8
- package/dist/{renderer-2HFRMVHO.js → renderer-RN5ZFIEK.js} +6 -6
- package/dist/{renderer-CQWF3JE3.js → renderer-TDFRMG3O.js} +3 -3
- package/dist/{renderer-MFD57VGM.js → renderer-UTXVIAS7.js} +5 -5
- package/dist/{renderer-S7ZTD4XM.js → renderer-VYMTBSF4.js} +7 -7
- package/dist/{renderer-NVT3RP5Q.js → renderer-XSHOMUJQ.js} +7 -7
- package/dist/{renderer-T4DY572A.js → renderer-ZHBFOSK5.js} +5 -5
- package/dist/{renderer-radial-2DDUIUWA.js → renderer-radial-4J2KO4GZ.js} +6 -6
- package/dist/{resolver-W3WKLWDP.js → resolver-ZLYSMILY.js} +3 -3
- package/dist/{state-parser-PNC2IHIA.js → state-parser-3U4232LJ.js} +5 -5
- package/dist/{state-renderer-QGPK55TI.js → state-renderer-GBHDOEXR.js} +7 -7
- package/dist/{themes-l406Vj2z.d.ts → themes-DiUAG4Bf.d.ts} +37 -2
- package/docs/ai-integration.md +5 -5
- package/docs/language-reference.md +1 -1
- package/package.json +2 -21
- package/.claude/commands/dgmo-codebase-report.md +0 -172
- package/.claude/commands/dgmo-diagram-this.md +0 -60
- package/.claude/commands/dgmo-document-project.md +0 -128
- package/.claude/commands/dgmo.md +0 -313
- package/.cursorrules +0 -587
- package/.github/copilot-instructions.md +0 -597
- package/.windsurfrules +0 -587
- package/SKILL.md +0 -573
- package/dist/auto.d.ts +0 -60
- package/dist/auto.mjs +0 -88591
- package/dist/chunk-6MTWIQVO.js +0 -121
- package/dist/chunk-ZEIYC7XO.js +0 -229
- package/dist/cli.cjs +0 -489
- package/dist/dgmo-router-JD7THHB7.js +0 -90
- package/dist/element.d.ts +0 -68
- package/dist/element.mjs +0 -88457
- package/dist/load-data-EHDDZ5ZS.js +0 -8
- package/dist/renderer-MDCH52MC.js +0 -77
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
pointInGeometry
|
|
5
5
|
} from "./chunk-4QV26TEG.js";
|
|
6
6
|
import {
|
|
7
|
+
createLabelPlacement,
|
|
7
8
|
rectCircleOverlap,
|
|
8
9
|
rectsOverlap,
|
|
9
|
-
segmentRectOverlap
|
|
10
|
-
|
|
11
|
-
} from "./chunk-VHO3YBTB.js";
|
|
10
|
+
segmentRectOverlap
|
|
11
|
+
} from "./chunk-CVVICCSB.js";
|
|
12
12
|
import {
|
|
13
13
|
TITLE_FONT_SIZE,
|
|
14
14
|
TITLE_Y
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "./chunk-SCJ7CAJF.js";
|
|
23
23
|
import {
|
|
24
24
|
tagAttrKey
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-MDI5FMWO.js";
|
|
26
26
|
import {
|
|
27
27
|
contrastRatio,
|
|
28
28
|
mix,
|
|
@@ -92,7 +92,7 @@ function mapLegendBox(legend, opts) {
|
|
|
92
92
|
|
|
93
93
|
// src/map/layout.ts
|
|
94
94
|
import {
|
|
95
|
-
geoPath,
|
|
95
|
+
geoPath as geoPath2,
|
|
96
96
|
geoNaturalEarth1,
|
|
97
97
|
geoEqualEarth,
|
|
98
98
|
geoEquirectangular,
|
|
@@ -424,1607 +424,2249 @@ function placeContextLabels(args) {
|
|
|
424
424
|
return placed;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
// src/map/
|
|
428
|
-
var
|
|
429
|
-
var
|
|
430
|
-
var
|
|
431
|
-
var
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
427
|
+
// src/map/city-dots.ts
|
|
428
|
+
var CITY_DOT_SPACING = 12;
|
|
429
|
+
var CITY_DOT_CAP = 220;
|
|
430
|
+
var SPACING_SQ = CITY_DOT_SPACING * CITY_DOT_SPACING;
|
|
431
|
+
var CITY_DOT_R_MIN = 0.7;
|
|
432
|
+
var CITY_DOT_R_MAX = 2.6;
|
|
433
|
+
var CITY_POP_MIN = 5e4;
|
|
434
|
+
var CITY_POP_MAX = 15e6;
|
|
435
|
+
var LOG_MIN = Math.log10(CITY_POP_MIN);
|
|
436
|
+
var LOG_SPAN = Math.log10(CITY_POP_MAX) - LOG_MIN;
|
|
437
|
+
function cityDotRadius(pop) {
|
|
438
|
+
if (!(pop > CITY_POP_MIN)) return CITY_DOT_R_MIN;
|
|
439
|
+
const t = Math.min(1, (Math.log10(pop) - LOG_MIN) / LOG_SPAN);
|
|
440
|
+
return CITY_DOT_R_MIN + t * (CITY_DOT_R_MAX - CITY_DOT_R_MIN);
|
|
441
|
+
}
|
|
442
|
+
function layoutCityDots(args) {
|
|
443
|
+
const { cities, occupied, project, width, height } = args;
|
|
444
|
+
const cityDots = [];
|
|
445
|
+
const placed = occupied.map((p) => ({
|
|
446
|
+
x: p.x,
|
|
447
|
+
y: p.y
|
|
448
|
+
}));
|
|
449
|
+
const sorted = [...cities].sort((a, b) => b[3] - a[3]);
|
|
450
|
+
for (const c of sorted) {
|
|
451
|
+
if (cityDots.length >= CITY_DOT_CAP) break;
|
|
452
|
+
const lat = c[0];
|
|
453
|
+
const lon = c[1];
|
|
454
|
+
const p = project(lon, lat);
|
|
455
|
+
if (!p) continue;
|
|
456
|
+
const [px, py] = p;
|
|
457
|
+
if (px < 0 || px > width || py < 0 || py > height) continue;
|
|
458
|
+
let tooClose = false;
|
|
459
|
+
for (const q of placed) {
|
|
460
|
+
const dx = q.x - px;
|
|
461
|
+
const dy = q.y - py;
|
|
462
|
+
if (dx * dx + dy * dy < SPACING_SQ) {
|
|
463
|
+
tooClose = true;
|
|
464
|
+
break;
|
|
452
465
|
}
|
|
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
466
|
}
|
|
467
|
+
if (tooClose) continue;
|
|
468
|
+
placed.push({ x: px, y: py });
|
|
469
|
+
cityDots.push({ cx: px, cy: py, r: cityDotRadius(c[3]) });
|
|
470
|
+
}
|
|
471
|
+
return cityDots;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/map/edge-labels.ts
|
|
475
|
+
var T_LIST = [0.5, 0.42, 0.58, 0.34, 0.66, 0.28, 0.72];
|
|
476
|
+
var LEG_PATH_RE = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/;
|
|
477
|
+
function layoutEdgeLabels(args) {
|
|
478
|
+
const {
|
|
479
|
+
legs,
|
|
480
|
+
committed,
|
|
481
|
+
poiScreen,
|
|
482
|
+
markers,
|
|
483
|
+
labelW,
|
|
484
|
+
labelH,
|
|
485
|
+
minStroke,
|
|
486
|
+
fillAt,
|
|
487
|
+
labelOnFill,
|
|
488
|
+
width,
|
|
489
|
+
height
|
|
490
|
+
} = args;
|
|
491
|
+
const chosen = [];
|
|
492
|
+
if (!legs.some((lg) => lg.label !== void 0)) return chosen;
|
|
493
|
+
const committedBoxes = committed.filter((l) => !l.hidden).map((l) => {
|
|
494
|
+
const w = labelW(l.text);
|
|
495
|
+
const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
|
|
496
|
+
return { x, y: l.y - labelH / 2, w, h: labelH };
|
|
465
497
|
});
|
|
498
|
+
const placedEdge = [];
|
|
499
|
+
for (let i = 0; i < legs.length; i++) {
|
|
500
|
+
const lg = legs[i];
|
|
501
|
+
if (lg.label === void 0) continue;
|
|
502
|
+
const a = poiScreen.get(lg.fromId);
|
|
503
|
+
const b = poiScreen.get(lg.toId);
|
|
504
|
+
if (!a || !b) continue;
|
|
505
|
+
const w = labelW(lg.label);
|
|
506
|
+
const dx = b.cx - a.cx;
|
|
507
|
+
const dy = b.cy - a.cy;
|
|
508
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
509
|
+
const nx = -dy / len;
|
|
510
|
+
const ny = dx / len;
|
|
511
|
+
const perp = labelH + 2;
|
|
512
|
+
const stroke = lg.width ?? minStroke;
|
|
513
|
+
const clearW = stroke / 2 + labelH / 2 + 3;
|
|
514
|
+
const offList = stroke >= 4 ? [clearW, -clearW, clearW + perp, -(clearW + perp), 0] : [0, perp, -perp, 2 * perp, -2 * perp];
|
|
515
|
+
const pm = LEG_PATH_RE.exec(lg.d);
|
|
516
|
+
const pointAt = (t) => {
|
|
517
|
+
if (pm?.[5] !== void 0) {
|
|
518
|
+
const u = 1 - t;
|
|
519
|
+
return [
|
|
520
|
+
u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
|
|
521
|
+
u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
|
|
522
|
+
];
|
|
523
|
+
}
|
|
524
|
+
if (pm?.[3] !== void 0)
|
|
525
|
+
return [
|
|
526
|
+
+pm[1] + (+pm[3] - +pm[1]) * t,
|
|
527
|
+
+pm[2] + (+pm[4] - +pm[2]) * t
|
|
528
|
+
];
|
|
529
|
+
return [a.cx + dx * t, a.cy + dy * t];
|
|
530
|
+
};
|
|
531
|
+
const candidates = [];
|
|
532
|
+
for (const off of offList)
|
|
533
|
+
for (const t of T_LIST) {
|
|
534
|
+
const [bx, by] = pointAt(t);
|
|
535
|
+
candidates.push([bx + nx * off, by + ny * off - 4]);
|
|
536
|
+
}
|
|
537
|
+
const clean = ([cx2, cy2]) => {
|
|
538
|
+
const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
|
|
539
|
+
if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
|
|
540
|
+
return false;
|
|
541
|
+
if (markers.some((m) => rectCircleOverlap(rect2, m))) return false;
|
|
542
|
+
if (committedBoxes.some((o) => rectsOverlap(o, rect2))) return false;
|
|
543
|
+
if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
|
|
544
|
+
return true;
|
|
545
|
+
};
|
|
546
|
+
const mid = pointAt(0.5);
|
|
547
|
+
const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
|
|
548
|
+
const style = labelOnFill(fillAt(cx, cy));
|
|
549
|
+
legs[i] = {
|
|
550
|
+
...lg,
|
|
551
|
+
labelX: cx,
|
|
552
|
+
labelY: cy,
|
|
553
|
+
labelColor: style.color,
|
|
554
|
+
labelHalo: style.halo,
|
|
555
|
+
labelHaloColor: style.haloColor
|
|
556
|
+
};
|
|
557
|
+
const rect = { x: cx - w / 2, y: cy - labelH / 2, w, h: labelH };
|
|
558
|
+
placedEdge.push(rect);
|
|
559
|
+
chosen.push(rect);
|
|
560
|
+
}
|
|
561
|
+
return chosen;
|
|
466
562
|
}
|
|
467
|
-
|
|
468
|
-
|
|
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;
|
|
563
|
+
|
|
564
|
+
// src/map/connectors.ts
|
|
473
565
|
var W_MIN = 1.25;
|
|
474
566
|
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
567
|
var FAN_STEP = 16;
|
|
522
568
|
var ARC_CURVE_FRAC = 0.18;
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
var
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
};
|
|
569
|
+
var RIM_GAP = 1.5;
|
|
570
|
+
var LEG_SAMPLES = 25;
|
|
571
|
+
var POI_CLEARANCE = 10;
|
|
572
|
+
var LEG_CLEARANCE = 7;
|
|
573
|
+
var SHARED_END_RADIUS = 36;
|
|
574
|
+
function bowMagnitude(mx, my, nx, ny, offset, len, away) {
|
|
575
|
+
if (offset !== 0) return offset;
|
|
576
|
+
const base = len * ARC_CURVE_FRAC;
|
|
577
|
+
if (!away) return base;
|
|
578
|
+
const dot = nx * (mx - away.x) + ny * (my - away.y);
|
|
579
|
+
return dot < 0 ? -base : base;
|
|
543
580
|
}
|
|
544
|
-
function
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
581
|
+
function legPath(a, b, curved, offset, away) {
|
|
582
|
+
const mx = (a.cx + b.cx) / 2;
|
|
583
|
+
const my = (a.cy + b.cy) / 2;
|
|
584
|
+
const dx = b.cx - a.cx;
|
|
585
|
+
const dy = b.cy - a.cy;
|
|
586
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
587
|
+
const trimA = Math.min(a.r + RIM_GAP, len * 0.45);
|
|
588
|
+
const trimB = Math.min(b.r + RIM_GAP, len * 0.45);
|
|
589
|
+
if (!curved && offset === 0) {
|
|
590
|
+
const ux = dx / len;
|
|
591
|
+
const uy = dy / len;
|
|
592
|
+
const ax2 = a.cx + ux * trimA;
|
|
593
|
+
const ay2 = a.cy + uy * trimA;
|
|
594
|
+
const bx2 = b.cx - ux * trimB;
|
|
595
|
+
const by2 = b.cy - uy * trimB;
|
|
596
|
+
return `M${ax2},${ay2}L${bx2},${by2}`;
|
|
554
597
|
}
|
|
555
|
-
|
|
556
|
-
|
|
598
|
+
const nx = -dy / len;
|
|
599
|
+
const ny = dx / len;
|
|
600
|
+
const bow = bowMagnitude(mx, my, nx, ny, offset, len, away);
|
|
601
|
+
const px = mx + nx * bow;
|
|
602
|
+
const py = my + ny * bow;
|
|
603
|
+
const ta = Math.hypot(px - a.cx, py - a.cy) || 1;
|
|
604
|
+
const tb = Math.hypot(b.cx - px, b.cy - py) || 1;
|
|
605
|
+
const ax = a.cx + (px - a.cx) / ta * trimA;
|
|
606
|
+
const ay = a.cy + (py - a.cy) / ta * trimA;
|
|
607
|
+
const bx = b.cx - (b.cx - px) / tb * trimB;
|
|
608
|
+
const by = b.cy - (b.cy - py) / tb * trimB;
|
|
609
|
+
return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
|
|
557
610
|
}
|
|
558
|
-
function
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (!
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
611
|
+
function legLabelPoint(a, b, curved, offset, away) {
|
|
612
|
+
const mx = (a.cx + b.cx) / 2;
|
|
613
|
+
const my = (a.cy + b.cy) / 2;
|
|
614
|
+
if (!curved && offset === 0) return { x: mx, y: my - 4 };
|
|
615
|
+
const dx = b.cx - a.cx;
|
|
616
|
+
const dy = b.cy - a.cy;
|
|
617
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
618
|
+
const nx = -dy / len;
|
|
619
|
+
const ny = dx / len;
|
|
620
|
+
const bow = bowMagnitude(mx, my, nx, ny, offset, len, away);
|
|
621
|
+
const off = bow * 0.5 + Math.sign(bow || 1) * 8;
|
|
622
|
+
return { x: mx + nx * off, y: my + ny * off };
|
|
623
|
+
}
|
|
624
|
+
function buildConnectorLegs(args) {
|
|
625
|
+
const { resolved, poiScreen, palette, width, height, fillAt, labelOnFill } = args;
|
|
626
|
+
const legs = [];
|
|
627
|
+
const lineColor = (tags) => {
|
|
628
|
+
for (const group of resolved.tagGroups) {
|
|
629
|
+
const val = tags[tagAttrKey(group.name)];
|
|
630
|
+
if (!val) continue;
|
|
631
|
+
const entry = group.entries.find(
|
|
632
|
+
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
633
|
+
);
|
|
634
|
+
if (entry?.color) return entry.color;
|
|
582
635
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
636
|
+
return null;
|
|
637
|
+
};
|
|
638
|
+
const placedLegSamples = [];
|
|
639
|
+
const sampleLeg = (a, b, curved, bow) => {
|
|
640
|
+
const pts = [];
|
|
641
|
+
const dx = b.cx - a.cx;
|
|
642
|
+
const dy = b.cy - a.cy;
|
|
643
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
644
|
+
const px = (a.cx + b.cx) / 2 + -dy / len * bow;
|
|
645
|
+
const py = (a.cy + b.cy) / 2 + dx / len * bow;
|
|
646
|
+
for (let i = 0; i < LEG_SAMPLES; i++) {
|
|
647
|
+
const t = i / (LEG_SAMPLES - 1);
|
|
648
|
+
if (!curved && bow === 0) {
|
|
649
|
+
pts.push({ x: a.cx + dx * t, y: a.cy + dy * t });
|
|
650
|
+
} else {
|
|
651
|
+
const u = 1 - t;
|
|
652
|
+
pts.push({
|
|
653
|
+
x: u * u * a.cx + 2 * u * t * px + t * t * b.cx,
|
|
654
|
+
y: u * u * a.cy + 2 * u * t * py + t * t * b.cy
|
|
655
|
+
});
|
|
595
656
|
}
|
|
596
|
-
|
|
657
|
+
}
|
|
658
|
+
return pts;
|
|
597
659
|
};
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
660
|
+
const legPenalty = (pts, fromId, toId) => {
|
|
661
|
+
let pen = 0;
|
|
662
|
+
for (const [id, p] of poiScreen) {
|
|
663
|
+
if (id === fromId || id === toId) continue;
|
|
664
|
+
let dmin = Infinity;
|
|
665
|
+
for (const s of pts) {
|
|
666
|
+
const d = Math.hypot(s.x - p.cx, s.y - p.cy);
|
|
667
|
+
if (d < dmin) dmin = d;
|
|
668
|
+
}
|
|
669
|
+
const clear = p.r + POI_CLEARANCE;
|
|
670
|
+
if (dmin < clear) pen += (clear - dmin) * 12;
|
|
606
671
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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 });
|
|
672
|
+
for (const other of placedLegSamples) {
|
|
673
|
+
const shared = [fromId, toId].filter((id) => id === other.fromId || id === other.toId).map((id) => poiScreen.get(id)).filter((p) => !!p);
|
|
674
|
+
for (const s of pts) {
|
|
675
|
+
if (shared.some(
|
|
676
|
+
(sp) => Math.hypot(s.x - sp.cx, s.y - sp.cy) < SHARED_END_RADIUS
|
|
677
|
+
)) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
let dmin = Infinity;
|
|
681
|
+
for (const o of other.pts) {
|
|
682
|
+
const d = Math.hypot(s.x - o.x, s.y - o.y);
|
|
683
|
+
if (d < dmin) dmin = d;
|
|
684
|
+
}
|
|
685
|
+
if (dmin < LEG_CLEARANCE) pen += 6;
|
|
686
|
+
}
|
|
703
687
|
}
|
|
704
|
-
|
|
705
|
-
|
|
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]);
|
|
688
|
+
for (const s of pts) {
|
|
689
|
+
if (s.x < 0 || s.x > width || s.y < 0 || s.y > height) pen += 20;
|
|
715
690
|
}
|
|
716
|
-
return
|
|
717
|
-
type: "Feature",
|
|
718
|
-
properties: {},
|
|
719
|
-
geometry: { type: "MultiPoint", coordinates: coords }
|
|
720
|
-
};
|
|
691
|
+
return pen;
|
|
721
692
|
};
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
693
|
+
const chooseArcBow = (a, b, fromId, toId, away) => {
|
|
694
|
+
const mx = (a.cx + b.cx) / 2;
|
|
695
|
+
const my = (a.cy + b.cy) / 2;
|
|
696
|
+
const dx = b.cx - a.cx;
|
|
697
|
+
const dy = b.cy - a.cy;
|
|
698
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
699
|
+
const nx = -dy / len;
|
|
700
|
+
const ny = dx / len;
|
|
701
|
+
const def = bowMagnitude(mx, my, nx, ny, 0, len, away);
|
|
702
|
+
const candidates = [
|
|
703
|
+
def,
|
|
704
|
+
-def,
|
|
705
|
+
def * 1.6,
|
|
706
|
+
-def * 1.6,
|
|
707
|
+
def * 2.2,
|
|
708
|
+
-def * 2.2
|
|
709
|
+
];
|
|
710
|
+
let best = def;
|
|
711
|
+
let bestPen = Infinity;
|
|
712
|
+
for (const c of candidates) {
|
|
713
|
+
const pen = legPenalty(sampleLeg(a, b, true, c), fromId, toId);
|
|
714
|
+
if (pen < bestPen - 1e-6) {
|
|
715
|
+
best = c;
|
|
716
|
+
bestPen = pen;
|
|
737
717
|
}
|
|
718
|
+
if (bestPen === 0) break;
|
|
738
719
|
}
|
|
739
|
-
|
|
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
|
|
720
|
+
return best;
|
|
760
721
|
};
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
const
|
|
781
|
-
|
|
722
|
+
const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
|
|
723
|
+
const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
|
|
724
|
+
const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
|
|
725
|
+
const routeWidthFor = (v) => {
|
|
726
|
+
if (!Number.isFinite(v) || v <= 0 || rlMax <= 0) return W_MIN;
|
|
727
|
+
const t = rlMax > rlMin ? (v - rlMin) / (rlMax - rlMin) : 1;
|
|
728
|
+
return W_MIN + t * (W_MAX - W_MIN);
|
|
729
|
+
};
|
|
730
|
+
for (const rt of resolved.routes) {
|
|
731
|
+
const stopPts = rt.stopIds.map((id) => poiScreen.get(id)).filter((p) => !!p);
|
|
732
|
+
const center = stopPts.length >= 3 ? {
|
|
733
|
+
x: stopPts.reduce((s, p) => s + p.cx, 0) / stopPts.length,
|
|
734
|
+
y: stopPts.reduce((s, p) => s + p.cy, 0) / stopPts.length
|
|
735
|
+
} : void 0;
|
|
736
|
+
for (const leg of rt.legs) {
|
|
737
|
+
const a = poiScreen.get(leg.fromId);
|
|
738
|
+
const b = poiScreen.get(leg.toId);
|
|
739
|
+
if (!a || !b) continue;
|
|
740
|
+
const curvedLeg = leg.style === "arc";
|
|
741
|
+
const chosenBow = curvedLeg ? chooseArcBow(a, b, leg.fromId, leg.toId, center) : 0;
|
|
742
|
+
const lp = legLabelPoint(a, b, curvedLeg, chosenBow, center);
|
|
743
|
+
const bow = {
|
|
744
|
+
curved: curvedLeg,
|
|
745
|
+
offset: chosenBow,
|
|
746
|
+
center,
|
|
747
|
+
labelX: lp.x,
|
|
748
|
+
labelY: lp.y
|
|
749
|
+
};
|
|
750
|
+
placedLegSamples.push({
|
|
751
|
+
fromId: leg.fromId,
|
|
752
|
+
toId: leg.toId,
|
|
753
|
+
pts: sampleLeg(a, b, curvedLeg, chosenBow)
|
|
754
|
+
});
|
|
755
|
+
const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
|
|
756
|
+
const routeVal = Number(leg.value);
|
|
757
|
+
legs.push({
|
|
758
|
+
d: legPath(a, b, bow.curved, bow.offset, bow.center),
|
|
759
|
+
width: routeWidthFor(routeVal),
|
|
760
|
+
color: lineColor(leg.tags) ?? mix(palette.text, palette.bg, 72),
|
|
761
|
+
arrow: true,
|
|
762
|
+
fromId: leg.fromId,
|
|
763
|
+
toId: leg.toId,
|
|
764
|
+
...Number.isFinite(routeVal) && routeVal > 0 && { value: routeVal },
|
|
765
|
+
...Object.keys(leg.tags).length > 0 && { tags: leg.tags },
|
|
766
|
+
lineNumber: leg.lineNumber,
|
|
767
|
+
...leg.label !== void 0 && {
|
|
768
|
+
label: leg.label,
|
|
769
|
+
labelX: bow.labelX,
|
|
770
|
+
labelY: bow.labelY,
|
|
771
|
+
labelColor: routeLabelStyle.color,
|
|
772
|
+
labelHalo: routeLabelStyle.halo,
|
|
773
|
+
labelHaloColor: routeLabelStyle.haloColor
|
|
774
|
+
}
|
|
775
|
+
});
|
|
782
776
|
}
|
|
783
777
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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;
|
|
778
|
+
const weightVals = resolved.edges.map((e) => Number(e.meta["width"])).filter((n) => Number.isFinite(n) && n > 0);
|
|
779
|
+
const wMin = weightVals.length ? Math.min(...weightVals) : 0;
|
|
780
|
+
const wMax = weightVals.length ? Math.max(...weightVals) : 0;
|
|
781
|
+
const widthFor = (e) => {
|
|
782
|
+
const v = Number(e.meta["width"]);
|
|
783
|
+
if (!Number.isFinite(v) || v <= 0 || wMax <= 0) return W_MIN;
|
|
784
|
+
const t = wMax > wMin ? (v - wMin) / (wMax - wMin) : 1;
|
|
785
|
+
return W_MIN + t * (W_MAX - W_MIN);
|
|
800
786
|
};
|
|
801
|
-
const
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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);
|
|
787
|
+
const pairGroups = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const e of resolved.edges) {
|
|
789
|
+
const key = [e.fromId, e.toId].sort().join(" ");
|
|
790
|
+
const arr = pairGroups.get(key);
|
|
791
|
+
if (arr) arr.push(e);
|
|
792
|
+
else pairGroups.set(key, [e]);
|
|
844
793
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
if (
|
|
852
|
-
|
|
794
|
+
for (const groupEdges of pairGroups.values()) {
|
|
795
|
+
const ordered = [...groupEdges].sort((a, b) => a.lineNumber - b.lineNumber);
|
|
796
|
+
const n = ordered.length;
|
|
797
|
+
ordered.forEach((e, i) => {
|
|
798
|
+
const a = poiScreen.get(e.fromId);
|
|
799
|
+
const b = poiScreen.get(e.toId);
|
|
800
|
+
if (!a || !b) return;
|
|
801
|
+
const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
|
|
802
|
+
const curved = e.style === "arc" || n > 1;
|
|
803
|
+
const edgeBow = curved && n === 1 ? chooseArcBow(a, b, e.fromId, e.toId) : fanOffset;
|
|
804
|
+
const lp = legLabelPoint(a, b, curved, edgeBow);
|
|
805
|
+
const bow = {
|
|
806
|
+
curved,
|
|
807
|
+
offset: edgeBow,
|
|
808
|
+
labelX: lp.x,
|
|
809
|
+
labelY: lp.y
|
|
810
|
+
};
|
|
811
|
+
placedLegSamples.push({
|
|
812
|
+
fromId: e.fromId,
|
|
813
|
+
toId: e.toId,
|
|
814
|
+
pts: sampleLeg(a, b, curved, edgeBow)
|
|
815
|
+
});
|
|
816
|
+
const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
|
|
817
|
+
const edgeVal = Number(e.meta["width"]);
|
|
818
|
+
legs.push({
|
|
819
|
+
d: legPath(a, b, bow.curved, bow.offset),
|
|
820
|
+
width: widthFor(e),
|
|
821
|
+
color: lineColor(e.tags) ?? mix(palette.text, palette.bg, 66),
|
|
822
|
+
arrow: e.directed,
|
|
823
|
+
fromId: e.fromId,
|
|
824
|
+
toId: e.toId,
|
|
825
|
+
...Number.isFinite(edgeVal) && edgeVal > 0 && { value: edgeVal },
|
|
826
|
+
...Object.keys(e.tags).length > 0 && { tags: e.tags },
|
|
827
|
+
lineNumber: e.lineNumber,
|
|
828
|
+
...e.label !== void 0 && {
|
|
829
|
+
label: e.label,
|
|
830
|
+
labelX: bow.labelX,
|
|
831
|
+
labelY: bow.labelY,
|
|
832
|
+
labelColor: edgeLabelStyle.color,
|
|
833
|
+
labelHalo: edgeLabelStyle.halo,
|
|
834
|
+
labelHaloColor: edgeLabelStyle.haloColor
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
});
|
|
853
838
|
}
|
|
854
|
-
return
|
|
839
|
+
return legs;
|
|
855
840
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
841
|
+
|
|
842
|
+
// src/map/insets.ts
|
|
843
|
+
import { geoPath } from "d3-geo";
|
|
844
|
+
var PAD = 8;
|
|
845
|
+
var GAP = 12;
|
|
846
|
+
var BW = 8;
|
|
847
|
+
function layoutInsets(args) {
|
|
863
848
|
const {
|
|
864
849
|
projection,
|
|
865
|
-
fitTarget,
|
|
866
|
-
fitIsGlobal,
|
|
867
|
-
worldLayer,
|
|
868
850
|
usLayer,
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
851
|
+
worldLayer,
|
|
852
|
+
regionById,
|
|
853
|
+
width,
|
|
854
|
+
height,
|
|
855
|
+
fitPad,
|
|
856
|
+
pathDigits,
|
|
857
|
+
nonConus,
|
|
858
|
+
colorizeActive,
|
|
859
|
+
colorByIso,
|
|
860
|
+
neutralFill,
|
|
861
|
+
foreignFill,
|
|
862
|
+
regionStroke,
|
|
863
|
+
colorizeStroke,
|
|
864
|
+
regionFill,
|
|
865
|
+
alaskaProjection: alaskaProjection2,
|
|
866
|
+
hawaiiProjection: hawaiiProjection2,
|
|
867
|
+
akRef,
|
|
868
|
+
hiRef
|
|
869
|
+
} = args;
|
|
870
|
+
const insets = [];
|
|
871
|
+
const insetRegions = [];
|
|
872
|
+
const insetLabelSeeds = [];
|
|
873
|
+
const yB = height - fitPad;
|
|
874
|
+
const coast = /* @__PURE__ */ new Map();
|
|
875
|
+
const addPt = (lon, lat) => {
|
|
876
|
+
const p = projection([lon, lat]);
|
|
877
|
+
if (!p) return;
|
|
878
|
+
const bi = Math.floor(p[0] / BW);
|
|
879
|
+
const cur = coast.get(bi);
|
|
880
|
+
if (cur === void 0 || p[1] > cur) coast.set(bi, p[1]);
|
|
887
881
|
};
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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;
|
|
882
|
+
const walk = (co) => {
|
|
883
|
+
if (Array.isArray(co) && typeof co[0] === "number")
|
|
884
|
+
addPt(co[0], co[1]);
|
|
885
|
+
else if (Array.isArray(co)) for (const c of co) walk(c);
|
|
886
|
+
};
|
|
887
|
+
for (const [iso, f] of usLayer) {
|
|
888
|
+
if (nonConus.has(iso)) continue;
|
|
889
|
+
walk(f.geometry.coordinates);
|
|
903
890
|
}
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
)
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
891
|
+
const at = (x) => {
|
|
892
|
+
const bi = Math.floor(x / BW);
|
|
893
|
+
let y = -Infinity;
|
|
894
|
+
for (let k = bi - 1; k <= bi + 1; k++) {
|
|
895
|
+
const v = coast.get(k);
|
|
896
|
+
if (v !== void 0 && v > y) y = v;
|
|
897
|
+
}
|
|
898
|
+
return y;
|
|
899
|
+
};
|
|
900
|
+
const coastFloor = (x0, xr) => {
|
|
901
|
+
const n = 24;
|
|
902
|
+
let maxY = -Infinity;
|
|
903
|
+
for (let i = 0; i <= n; i++) {
|
|
904
|
+
const y = at(x0 + (xr - x0) * i / n);
|
|
905
|
+
if (y > maxY) maxY = y;
|
|
906
|
+
}
|
|
907
|
+
return maxY;
|
|
908
|
+
};
|
|
909
|
+
const placeInset = (iso, proj, boxX, iwReq) => {
|
|
910
|
+
const f = usLayer.get(iso);
|
|
911
|
+
if (!f) return boxX;
|
|
912
|
+
const x0 = boxX;
|
|
913
|
+
const iw = Math.min(iwReq, width - fitPad - x0 - 2 * PAD);
|
|
914
|
+
if (iw < 24) return boxX;
|
|
915
|
+
const xr = x0 + iw + 2 * PAD;
|
|
916
|
+
const floor = coastFloor(x0, xr);
|
|
917
|
+
const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
|
|
918
|
+
proj.fitWidth(iw, f);
|
|
919
|
+
const bb = geoPath(proj).bounds(f);
|
|
920
|
+
const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
|
|
921
|
+
const needH = sh + 2 * PAD;
|
|
922
|
+
let topFit = topGuess;
|
|
923
|
+
const bottom = Math.min(topFit + needH, yB);
|
|
924
|
+
if (bottom - topFit < needH) topFit = bottom - needH;
|
|
925
|
+
proj.fitExtent(
|
|
926
|
+
[
|
|
927
|
+
[x0 + PAD, topFit + PAD],
|
|
928
|
+
[xr - PAD, bottom - PAD]
|
|
929
|
+
],
|
|
930
|
+
f
|
|
931
|
+
);
|
|
932
|
+
const insetPath = geoPath(proj).digits(pathDigits);
|
|
933
|
+
const d = insetPath(f) ?? "";
|
|
934
|
+
if (!d) return xr;
|
|
935
|
+
let contextLand;
|
|
936
|
+
if (iso === "US-AK") {
|
|
937
|
+
const can = worldLayer.get("CA");
|
|
938
|
+
const cd = can ? insetPath(can) ?? "" : "";
|
|
939
|
+
if (cd)
|
|
940
|
+
contextLand = {
|
|
941
|
+
d: cd,
|
|
942
|
+
fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
|
|
943
|
+
};
|
|
920
944
|
}
|
|
921
|
-
|
|
945
|
+
const r = regionById.get(iso);
|
|
946
|
+
let fill = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
|
|
947
|
+
let lineNumber = -1;
|
|
948
|
+
if (r?.layer === "us-state") {
|
|
949
|
+
fill = regionFill(r);
|
|
950
|
+
lineNumber = r.lineNumber;
|
|
951
|
+
}
|
|
952
|
+
insets.push({
|
|
953
|
+
x: x0,
|
|
954
|
+
y: topFit,
|
|
955
|
+
w: xr - x0,
|
|
956
|
+
h: bottom - topFit,
|
|
957
|
+
points: [
|
|
958
|
+
[x0, topFit],
|
|
959
|
+
[xr, topFit],
|
|
960
|
+
[xr, bottom],
|
|
961
|
+
[x0, bottom]
|
|
962
|
+
],
|
|
963
|
+
// The FITTED inset projection (just fit to this box) — captured so the
|
|
964
|
+
// geo-query can invert pixels inside the frame back to AK/HI coords.
|
|
965
|
+
projection: proj,
|
|
966
|
+
...contextLand && { contextLand }
|
|
967
|
+
});
|
|
968
|
+
insetRegions.push({
|
|
969
|
+
id: iso,
|
|
970
|
+
d,
|
|
971
|
+
fill,
|
|
972
|
+
stroke: colorizeActive ? colorizeStroke(fill) : regionStroke,
|
|
973
|
+
lineNumber,
|
|
974
|
+
layer: "us-state",
|
|
975
|
+
...r?.value !== void 0 && { value: r.value },
|
|
976
|
+
...r && Object.keys(r.tags).length > 0 && { tags: r.tags }
|
|
977
|
+
});
|
|
978
|
+
const ctr = geoPath(proj).centroid(f);
|
|
979
|
+
if (Number.isFinite(ctr[0])) {
|
|
980
|
+
const name = f.properties?.name ?? iso;
|
|
981
|
+
insetLabelSeeds.push({ x: ctr[0], y: ctr[1], iso, name, lineNumber });
|
|
982
|
+
}
|
|
983
|
+
return xr;
|
|
922
984
|
};
|
|
923
|
-
|
|
924
|
-
|
|
985
|
+
let akRight = fitPad;
|
|
986
|
+
if (akRef)
|
|
987
|
+
akRight = placeInset("US-AK", alaskaProjection2(), fitPad, width * 0.18);
|
|
988
|
+
if (hiRef)
|
|
989
|
+
placeInset(
|
|
990
|
+
"US-HI",
|
|
991
|
+
hawaiiProjection2(),
|
|
992
|
+
akRef ? akRight + 24 : fitPad,
|
|
993
|
+
width * 0.12
|
|
994
|
+
);
|
|
995
|
+
return { insets, insetRegions, insetLabelSeeds };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/map/label-metrics.ts
|
|
999
|
+
var FONT2 = 11;
|
|
1000
|
+
var LABEL_PADX = 6;
|
|
1001
|
+
var LABEL_PADY = 3;
|
|
1002
|
+
var VALUE_GAP = 1;
|
|
1003
|
+
var VALUE_FONT_FRAC = 0.82;
|
|
1004
|
+
var REGION_FONT_MAX_ORIENT = 22;
|
|
1005
|
+
var REGION_SIZE_FRAC_MIN = 0.06;
|
|
1006
|
+
var REGION_SIZE_FRAC_MAX = 0.32;
|
|
1007
|
+
var SUBJECT_FONT_MAX = 18;
|
|
1008
|
+
var SUBJECT_MIN_PROMINENCE = 13;
|
|
1009
|
+
var SUBJECT_LEADER_FONT = 15;
|
|
1010
|
+
function createLabelMetrics(size) {
|
|
1011
|
+
const canvasLinear = Math.sqrt(Math.max(1, size.width * size.height));
|
|
1012
|
+
const labelW = (text, font = FONT2) => measureLegendText(text, font) + 2 * LABEL_PADX;
|
|
1013
|
+
return {
|
|
1014
|
+
labelW,
|
|
1015
|
+
labelH: FONT2 + 2 * LABEL_PADY,
|
|
1016
|
+
// `font` defaults to the base size (every existing call is byte-identical);
|
|
1017
|
+
// the post-placement growth pass passes a larger size to test an upscaled fit.
|
|
1018
|
+
stackW: (text, valueText, font = FONT2) => Math.max(
|
|
1019
|
+
labelW(text, font),
|
|
1020
|
+
valueText ? measureLegendText(valueText, Math.round(font * VALUE_FONT_FRAC)) + 2 * LABEL_PADX : 0
|
|
1021
|
+
),
|
|
1022
|
+
stackH: (hasValue, font = FONT2) => {
|
|
1023
|
+
const lh = font + 2 * LABEL_PADY;
|
|
1024
|
+
return hasValue ? lh + VALUE_GAP + Math.round(font * VALUE_FONT_FRAC) : lh;
|
|
1025
|
+
},
|
|
1026
|
+
sizeT: (boxW, boxH) => {
|
|
1027
|
+
const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
|
|
1028
|
+
return Math.min(
|
|
1029
|
+
1,
|
|
1030
|
+
Math.max(
|
|
1031
|
+
0,
|
|
1032
|
+
(frac - REGION_SIZE_FRAC_MIN) / (REGION_SIZE_FRAC_MAX - REGION_SIZE_FRAC_MIN)
|
|
1033
|
+
)
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/map/region-labels.ts
|
|
1040
|
+
function placeRegionLabels(args) {
|
|
1041
|
+
const {
|
|
1042
|
+
resolved,
|
|
1043
|
+
palette,
|
|
1044
|
+
width,
|
|
1045
|
+
height,
|
|
1046
|
+
path,
|
|
1047
|
+
project,
|
|
1048
|
+
worldLayer,
|
|
1049
|
+
usLayer,
|
|
1050
|
+
regionById,
|
|
1051
|
+
regions,
|
|
1052
|
+
pois,
|
|
1053
|
+
insetLabelSeeds,
|
|
1054
|
+
topReserved,
|
|
1055
|
+
topPad,
|
|
1056
|
+
markers,
|
|
1057
|
+
collides,
|
|
925
1058
|
neutralFill,
|
|
926
|
-
water
|
|
927
|
-
);
|
|
928
|
-
const backdropWaterTone = contrastFloorTone(
|
|
929
|
-
mix(palette.colors.blue, palette.textMuted, 50),
|
|
930
1059
|
water,
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1060
|
+
foreignFill,
|
|
1061
|
+
backdropLandTone,
|
|
1062
|
+
regionFill,
|
|
1063
|
+
fillAt,
|
|
1064
|
+
regionValueStr,
|
|
1065
|
+
isCompact,
|
|
1066
|
+
usChoroplethZoom,
|
|
1067
|
+
metrics,
|
|
1068
|
+
pushRegionLabel,
|
|
1069
|
+
regionLabelRect,
|
|
1070
|
+
labels,
|
|
1071
|
+
labeledRegionIds,
|
|
1072
|
+
regionLabelGuards
|
|
1073
|
+
} = args;
|
|
1074
|
+
const { labelW, labelH, stackW, stackH, sizeT } = metrics;
|
|
1075
|
+
const frameContainers = new Set(resolved.poiFrameContainers);
|
|
1076
|
+
const entries = regions.map((r) => {
|
|
1077
|
+
const isContainer = frameContainers.has(r.id);
|
|
1078
|
+
if (r.layer === "base" && !isContainer || r.label === void 0)
|
|
1079
|
+
return null;
|
|
1080
|
+
const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
|
|
1081
|
+
const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
|
|
1082
|
+
if (!f) return null;
|
|
1083
|
+
const [[x0, y0], [x1, y1]] = path.bounds(f);
|
|
1084
|
+
const boxW = x1 - x0;
|
|
1085
|
+
const boxH = y1 - y0;
|
|
1086
|
+
const abbrev = isUsState && !usChoroplethZoom ? r.id.replace(/^US-/, "") : void 0;
|
|
1087
|
+
const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : isContainer ? [r.label] : [r.label, abbrev] : [r.label];
|
|
1088
|
+
const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
|
|
1089
|
+
const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
|
|
1090
|
+
if (!c || !Number.isFinite(c[0])) return null;
|
|
1091
|
+
return { r, c, boxW, boxH, area: boxW * boxH, candidates };
|
|
1092
|
+
}).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
|
|
1093
|
+
const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
|
|
1094
|
+
const POI_LABEL_PAD = 14;
|
|
1095
|
+
const poiObstacles = pois.map((p) => ({
|
|
1096
|
+
x: p.cx - p.r - POI_LABEL_PAD,
|
|
1097
|
+
y: p.cy - p.r - POI_LABEL_PAD,
|
|
1098
|
+
w: 2 * (p.r + POI_LABEL_PAD),
|
|
1099
|
+
h: 2 * (p.r + POI_LABEL_PAD)
|
|
1100
|
+
}));
|
|
1101
|
+
const placement = createLabelPlacement({
|
|
1102
|
+
reserved: topReserved,
|
|
1103
|
+
obstacles: poiObstacles,
|
|
1104
|
+
markers
|
|
1105
|
+
});
|
|
1106
|
+
const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
|
|
1107
|
+
const STEP = 4;
|
|
1108
|
+
const HOP_GAP = 9;
|
|
1109
|
+
const CLEAR_STEP = 6;
|
|
1110
|
+
const CLEAR_MAX = SHORT_HOP_MAX * 3;
|
|
1111
|
+
const CLEAR_TOL = CLEAR_STEP;
|
|
1112
|
+
const clearanceAt = (px, py, ox, oy) => {
|
|
1113
|
+
const baseAng = Math.atan2(oy, ox);
|
|
1114
|
+
let minClear = CLEAR_MAX;
|
|
1115
|
+
for (let k = -2; k <= 2; k++) {
|
|
1116
|
+
const a = baseAng + k * Math.PI / 4;
|
|
1117
|
+
const rx = Math.cos(a);
|
|
1118
|
+
const ry = Math.sin(a);
|
|
1119
|
+
let d = CLEAR_STEP;
|
|
1120
|
+
for (; d <= CLEAR_MAX; d += CLEAR_STEP) {
|
|
1121
|
+
const qx = px + rx * d;
|
|
1122
|
+
const qy = py + ry * d;
|
|
1123
|
+
if (qx < FIT_PAD || qy < topPad || qx > width - FIT_PAD || qy > height - FIT_PAD)
|
|
1124
|
+
break;
|
|
1125
|
+
if (fillAt(qx, qy) !== water) break;
|
|
957
1126
|
}
|
|
1127
|
+
if (d < minClear) minClear = d;
|
|
958
1128
|
}
|
|
959
|
-
|
|
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);
|
|
1129
|
+
return minClear;
|
|
998
1130
|
};
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1131
|
+
const tryShortHopCallout = (cx, cy, fill, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
|
|
1132
|
+
const chipW = stackW(name, value, font);
|
|
1133
|
+
const chipH = stackH(value !== void 0, font);
|
|
1134
|
+
const D = Math.SQRT1_2;
|
|
1135
|
+
const dirs = [
|
|
1136
|
+
[0, -1],
|
|
1137
|
+
// north
|
|
1138
|
+
[0, 1],
|
|
1139
|
+
// south
|
|
1140
|
+
[1, 0],
|
|
1141
|
+
// east
|
|
1142
|
+
[-1, 0],
|
|
1143
|
+
// west
|
|
1144
|
+
[D, -D],
|
|
1145
|
+
// NE
|
|
1146
|
+
[D, D],
|
|
1147
|
+
// SE
|
|
1148
|
+
[-D, -D],
|
|
1149
|
+
// NW
|
|
1150
|
+
[-D, D]
|
|
1151
|
+
// SW
|
|
1152
|
+
];
|
|
1153
|
+
let best = null;
|
|
1154
|
+
for (const [dx, dy] of dirs) {
|
|
1155
|
+
let ex = cx;
|
|
1156
|
+
let ey = cy;
|
|
1157
|
+
let steps = 0;
|
|
1158
|
+
const maxSteps = Math.ceil(maxLen / STEP) + 1;
|
|
1159
|
+
while (steps < maxSteps && fillAt(ex, ey) === fill) {
|
|
1160
|
+
ex += dx * STEP;
|
|
1161
|
+
ey += dy * STEP;
|
|
1162
|
+
steps++;
|
|
1163
|
+
}
|
|
1164
|
+
if (fillAt(ex, ey) === fill) continue;
|
|
1165
|
+
const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
|
|
1166
|
+
const ccx = ex + dx * (gap + halfAlong);
|
|
1167
|
+
const ccy = ey + dy * (gap + halfAlong);
|
|
1168
|
+
const rect = {
|
|
1169
|
+
x: ccx - chipW / 2,
|
|
1170
|
+
y: ccy - chipH / 2,
|
|
1171
|
+
w: chipW,
|
|
1172
|
+
h: chipH
|
|
1173
|
+
};
|
|
1174
|
+
if (rect.x < FIT_PAD || rect.y < topPad || rect.x + rect.w > width - FIT_PAD || rect.y + rect.h > height - FIT_PAD)
|
|
1175
|
+
continue;
|
|
1176
|
+
const sx = [rect.x + 2, ccx, rect.x + rect.w - 2];
|
|
1177
|
+
const sy = [rect.y + 2, ccy, rect.y + rect.h - 2];
|
|
1178
|
+
let onEmpty = true;
|
|
1179
|
+
for (const px of sx)
|
|
1180
|
+
for (const py of sy)
|
|
1181
|
+
if (!isEmptyFill(fillAt(px, py))) {
|
|
1182
|
+
onEmpty = false;
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
if (!onEmpty) continue;
|
|
1186
|
+
if (placement.hitsPlaced(rect)) continue;
|
|
1187
|
+
if (placement.hitsObstacle(rect)) continue;
|
|
1188
|
+
if (placement.hitsMarker(rect)) continue;
|
|
1189
|
+
const innerX = ccx - dx * (chipW / 2);
|
|
1190
|
+
const innerY = ccy - dy * (chipH / 2);
|
|
1191
|
+
const len = Math.hypot(innerX - cx, innerY - cy);
|
|
1192
|
+
if (len > maxLen) continue;
|
|
1193
|
+
const leader = [cx, cy, innerX, innerY];
|
|
1194
|
+
if (placement.leaderCrosses(leader)) continue;
|
|
1195
|
+
if (placement.leaderHitsPlaced(leader)) continue;
|
|
1196
|
+
const clear = clearanceAt(ccx, ccy, dx, dy);
|
|
1197
|
+
const better = best === null || clear > best.clear + CLEAR_TOL || clear >= best.clear - CLEAR_TOL && len < best.len;
|
|
1198
|
+
if (better)
|
|
1199
|
+
best = {
|
|
1200
|
+
x: ccx,
|
|
1201
|
+
y: ccy,
|
|
1202
|
+
rect,
|
|
1203
|
+
leader,
|
|
1204
|
+
len,
|
|
1205
|
+
clear
|
|
1206
|
+
};
|
|
1004
1207
|
}
|
|
1005
|
-
|
|
1006
|
-
return tagFill(r.tags, activeGroup) ?? neutralFill;
|
|
1208
|
+
return best ? { x: best.x, y: best.y, rect: best.rect, leader: best.leader } : null;
|
|
1007
1209
|
};
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1210
|
+
for (const { r, c, boxW, boxH, candidates } of entries) {
|
|
1211
|
+
const valStr = regionValueStr(r.value);
|
|
1212
|
+
const fitsRegions = (rect) => !placement.hitsPlaced(rect);
|
|
1213
|
+
const fitsPois = (rect) => !placement.hitsObstacle(rect);
|
|
1214
|
+
const isSubject = regionById.has(r.id);
|
|
1215
|
+
const extremesOnFill = (ax, ay, text) => {
|
|
1216
|
+
const halfW = measureLegendText(text, FONT2) / 2;
|
|
1217
|
+
return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
|
|
1218
|
+
};
|
|
1219
|
+
if (isSubject && valStr === void 0 && r.label !== void 0) {
|
|
1220
|
+
const name = candidates[0];
|
|
1221
|
+
let best = -1;
|
|
1222
|
+
for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
|
|
1223
|
+
if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
|
|
1224
|
+
continue;
|
|
1225
|
+
const rect = regionLabelRect(c[0], c[1], name, void 0, f);
|
|
1226
|
+
if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
|
|
1227
|
+
const halfW = measureLegendText(name, f) / 2;
|
|
1228
|
+
if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
|
|
1229
|
+
continue;
|
|
1230
|
+
best = f;
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
if (best >= SUBJECT_MIN_PROMINENCE) {
|
|
1234
|
+
const rect = regionLabelRect(c[0], c[1], name, void 0, best);
|
|
1235
|
+
placement.commit(rect);
|
|
1236
|
+
pushRegionLabel(
|
|
1237
|
+
c[0],
|
|
1238
|
+
c[1],
|
|
1239
|
+
name,
|
|
1240
|
+
r.fill,
|
|
1241
|
+
r.lineNumber,
|
|
1242
|
+
void 0,
|
|
1243
|
+
best
|
|
1244
|
+
);
|
|
1245
|
+
labeledRegionIds.add(r.id);
|
|
1246
|
+
regionLabelGuards.push({ label: labels[labels.length - 1], rect });
|
|
1247
|
+
} else {
|
|
1248
|
+
const hop = tryShortHopCallout(
|
|
1249
|
+
c[0],
|
|
1250
|
+
c[1],
|
|
1251
|
+
r.fill,
|
|
1252
|
+
r.label,
|
|
1253
|
+
void 0,
|
|
1254
|
+
SUBJECT_LEADER_FONT,
|
|
1255
|
+
HOP_GAP + SUBJECT_LEADER_FONT,
|
|
1256
|
+
SHORT_HOP_MAX * 2.2
|
|
1257
|
+
);
|
|
1258
|
+
if (hop) {
|
|
1259
|
+
placement.commit(hop.rect, hop.leader);
|
|
1260
|
+
const dark = mix(r.fill, palette.text, 60);
|
|
1261
|
+
labels.push({
|
|
1262
|
+
x: hop.x,
|
|
1263
|
+
y: hop.y,
|
|
1264
|
+
text: r.label,
|
|
1265
|
+
anchor: "middle",
|
|
1266
|
+
color: palette.text,
|
|
1267
|
+
halo: false,
|
|
1268
|
+
haloColor: palette.bg,
|
|
1269
|
+
fontSize: SUBJECT_LEADER_FONT,
|
|
1270
|
+
leader: {
|
|
1271
|
+
x1: hop.leader[0],
|
|
1272
|
+
y1: hop.leader[1],
|
|
1273
|
+
x2: hop.leader[2],
|
|
1274
|
+
y2: hop.leader[3]
|
|
1023
1275
|
},
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1276
|
+
leaderColor: dark,
|
|
1277
|
+
calloutDot: { x: c[0], y: c[1], color: dark },
|
|
1278
|
+
lineNumber: r.lineNumber
|
|
1279
|
+
});
|
|
1280
|
+
labeledRegionIds.add(r.id);
|
|
1029
1281
|
}
|
|
1030
|
-
}
|
|
1282
|
+
}
|
|
1283
|
+
continue;
|
|
1031
1284
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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];
|
|
1285
|
+
const seekAnchors = [
|
|
1286
|
+
{ x: c[0], y: c[1], guard: false }
|
|
1287
|
+
];
|
|
1288
|
+
if (valStr) {
|
|
1289
|
+
seekAnchors.push(
|
|
1290
|
+
{ x: c[0], y: c[1] + boxH * 0.26, guard: true },
|
|
1291
|
+
{ x: c[0], y: c[1] - boxH * 0.26, guard: true },
|
|
1292
|
+
{ x: c[0] + boxW * 0.26, y: c[1], guard: true },
|
|
1293
|
+
{ x: c[0] - boxW * 0.26, y: c[1], guard: true },
|
|
1294
|
+
{ x: c[0] + boxW * 0.22, y: c[1] + boxH * 0.22, guard: true },
|
|
1295
|
+
{ x: c[0] - boxW * 0.22, y: c[1] + boxH * 0.22, guard: true },
|
|
1296
|
+
{ x: c[0] + boxW * 0.22, y: c[1] - boxH * 0.22, guard: true },
|
|
1297
|
+
{ x: c[0] - boxW * 0.22, y: c[1] - boxH * 0.22, guard: true }
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
let chosen;
|
|
1301
|
+
for (const a of seekAnchors) {
|
|
1302
|
+
if (a.guard && fillAt(a.x, a.y) !== r.fill) continue;
|
|
1303
|
+
for (const t of candidates) {
|
|
1304
|
+
const nameRect = regionLabelRect(a.x, a.y, t);
|
|
1305
|
+
if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
|
|
1306
|
+
const stackRect = regionLabelRect(a.x, a.y, t, valStr);
|
|
1307
|
+
if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
|
|
1308
|
+
chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (labelW(t) <= boxW && labelH <= boxH) {
|
|
1313
|
+
if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
|
|
1314
|
+
chosen = { text: t, ax: a.x, ay: a.y };
|
|
1315
|
+
break;
|
|
1096
1316
|
}
|
|
1097
1317
|
}
|
|
1098
1318
|
}
|
|
1319
|
+
if (chosen) break;
|
|
1099
1320
|
}
|
|
1100
|
-
if (
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1321
|
+
if (chosen === void 0 && r.label !== void 0 && isSubject) {
|
|
1322
|
+
const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
|
|
1323
|
+
if (hop) {
|
|
1324
|
+
placement.commit(hop.rect, hop.leader);
|
|
1325
|
+
const dark = mix(r.fill, palette.text, 60);
|
|
1326
|
+
labels.push({
|
|
1327
|
+
x: hop.x,
|
|
1328
|
+
y: hop.y,
|
|
1329
|
+
text: r.label,
|
|
1330
|
+
anchor: "middle",
|
|
1331
|
+
color: palette.text,
|
|
1332
|
+
halo: false,
|
|
1333
|
+
haloColor: palette.bg,
|
|
1334
|
+
...valStr !== void 0 && { valueLine: valStr },
|
|
1335
|
+
leader: {
|
|
1336
|
+
x1: hop.leader[0],
|
|
1337
|
+
y1: hop.leader[1],
|
|
1338
|
+
x2: hop.leader[2],
|
|
1339
|
+
y2: hop.leader[3]
|
|
1340
|
+
},
|
|
1341
|
+
leaderColor: dark,
|
|
1342
|
+
calloutDot: { x: c[0], y: c[1], color: dark },
|
|
1343
|
+
lineNumber: r.lineNumber
|
|
1344
|
+
});
|
|
1345
|
+
labeledRegionIds.add(r.id);
|
|
1346
|
+
}
|
|
1347
|
+
continue;
|
|
1109
1348
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1349
|
+
if (chosen === void 0) continue;
|
|
1350
|
+
const isOrient = r.value === void 0 && r.layer === "base";
|
|
1351
|
+
let font = FONT2;
|
|
1352
|
+
if (isOrient) {
|
|
1353
|
+
const growT = sizeT(boxW, boxH);
|
|
1354
|
+
const desiredFont = Math.round(
|
|
1355
|
+
FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
|
|
1356
|
+
);
|
|
1357
|
+
const hasVal = chosen.valueLine !== void 0;
|
|
1358
|
+
const extremesClean = (f) => {
|
|
1359
|
+
const halfW = measureLegendText(chosen.text, f) / 2;
|
|
1360
|
+
return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
|
|
1361
|
+
const fAt = fillAt(sx, chosen.ay);
|
|
1362
|
+
return fAt === r.fill || isEmptyFill(fAt);
|
|
1363
|
+
});
|
|
1364
|
+
};
|
|
1365
|
+
let fit = -1;
|
|
1366
|
+
for (let f = desiredFont; f >= FONT2; f--) {
|
|
1367
|
+
if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
|
|
1368
|
+
continue;
|
|
1369
|
+
const gRect = regionLabelRect(
|
|
1370
|
+
chosen.ax,
|
|
1371
|
+
chosen.ay,
|
|
1372
|
+
chosen.text,
|
|
1373
|
+
chosen.valueLine,
|
|
1374
|
+
f
|
|
1375
|
+
);
|
|
1376
|
+
const gName = regionLabelRect(
|
|
1377
|
+
chosen.ax,
|
|
1378
|
+
chosen.ay,
|
|
1379
|
+
chosen.text,
|
|
1380
|
+
void 0,
|
|
1381
|
+
f
|
|
1382
|
+
);
|
|
1383
|
+
if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
|
|
1384
|
+
continue;
|
|
1385
|
+
if (!extremesClean(f)) continue;
|
|
1386
|
+
fit = f;
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
if (fit < 0) {
|
|
1390
|
+
if (!frameContainers.has(r.id)) continue;
|
|
1391
|
+
font = FONT2;
|
|
1392
|
+
} else {
|
|
1393
|
+
font = fit;
|
|
1120
1394
|
}
|
|
1121
1395
|
}
|
|
1396
|
+
const rRect = regionLabelRect(
|
|
1397
|
+
chosen.ax,
|
|
1398
|
+
chosen.ay,
|
|
1399
|
+
chosen.text,
|
|
1400
|
+
chosen.valueLine,
|
|
1401
|
+
font
|
|
1402
|
+
);
|
|
1403
|
+
placement.commit(rRect);
|
|
1404
|
+
pushRegionLabel(
|
|
1405
|
+
chosen.ax,
|
|
1406
|
+
chosen.ay,
|
|
1407
|
+
chosen.text,
|
|
1408
|
+
r.fill,
|
|
1409
|
+
r.lineNumber,
|
|
1410
|
+
chosen.valueLine,
|
|
1411
|
+
font,
|
|
1412
|
+
isOrient ? backdropLandTone : void 0
|
|
1413
|
+
);
|
|
1414
|
+
labeledRegionIds.add(r.id);
|
|
1415
|
+
regionLabelGuards.push({
|
|
1416
|
+
label: labels[labels.length - 1],
|
|
1417
|
+
rect: rRect
|
|
1418
|
+
});
|
|
1122
1419
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
}
|
|
1420
|
+
for (const seed of insetLabelSeeds) {
|
|
1421
|
+
const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
|
|
1422
|
+
const src = regionById.get(seed.iso);
|
|
1423
|
+
const valStr = regionValueStr(src?.value);
|
|
1424
|
+
pushRegionLabel(
|
|
1425
|
+
seed.x,
|
|
1426
|
+
seed.y,
|
|
1427
|
+
text,
|
|
1428
|
+
src ? regionFill(src) : neutralFill,
|
|
1429
|
+
seed.lineNumber,
|
|
1430
|
+
valStr
|
|
1431
|
+
);
|
|
1432
|
+
labeledRegionIds.add(seed.iso);
|
|
1433
|
+
regionLabelGuards.push({
|
|
1434
|
+
label: labels[labels.length - 1],
|
|
1435
|
+
rect: regionLabelRect(seed.x, seed.y, text, valStr)
|
|
1148
1436
|
});
|
|
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
1437
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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 });
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/map/layout.ts
|
|
1441
|
+
var FIT_PAD = 24;
|
|
1442
|
+
var PATH_DIGITS = 1;
|
|
1443
|
+
var THIN_TOL = 0.6;
|
|
1444
|
+
var ALBERS_SKEW_MAX = 1.25;
|
|
1445
|
+
function geoThin() {
|
|
1446
|
+
const tol2 = THIN_TOL * THIN_TOL;
|
|
1447
|
+
return geoTransform({
|
|
1448
|
+
lineStart() {
|
|
1449
|
+
const t = this;
|
|
1450
|
+
t._has = false;
|
|
1451
|
+
t._pending = false;
|
|
1452
|
+
t.stream.lineStart();
|
|
1453
|
+
},
|
|
1454
|
+
point(x, y) {
|
|
1455
|
+
const t = this;
|
|
1456
|
+
t._lx = x;
|
|
1457
|
+
t._ly = y;
|
|
1458
|
+
if (t._has) {
|
|
1459
|
+
const dx = x - t._ex;
|
|
1460
|
+
const dy = y - t._ey;
|
|
1461
|
+
if (dx * dx + dy * dy < tol2) {
|
|
1462
|
+
t._pending = true;
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1311
1465
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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;
|
|
1466
|
+
t.stream.point(x, y);
|
|
1467
|
+
t._ex = x;
|
|
1468
|
+
t._ey = y;
|
|
1469
|
+
t._has = true;
|
|
1470
|
+
t._pending = false;
|
|
1471
|
+
},
|
|
1472
|
+
lineEnd() {
|
|
1473
|
+
const t = this;
|
|
1474
|
+
if (t._pending) t.stream.point(t._lx, t._ly);
|
|
1475
|
+
t._pending = false;
|
|
1476
|
+
t.stream.lineEnd();
|
|
1347
1477
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
var RAMP_FLOOR = 15;
|
|
1481
|
+
var R_DEFAULT = 6;
|
|
1482
|
+
var R_MIN = 4;
|
|
1483
|
+
var R_MAX = 22;
|
|
1484
|
+
var POI_FILL_OPACITY_MAX = 0.92;
|
|
1485
|
+
var POI_FILL_OPACITY_MIN = 0.55;
|
|
1486
|
+
var WORLD_LABEL_ANCHORS = {
|
|
1487
|
+
US: [-98.5, 39.5],
|
|
1488
|
+
// CONUS geographic centre (near Lebanon, Kansas)
|
|
1489
|
+
// Russia crosses the antimeridian (Chukotka at ~170°W), so on a non-global
|
|
1490
|
+
// (e.g. Europe) projection its geometry smears across the whole frame and the
|
|
1491
|
+
// area-weighted centroid lands mid-map (over Europe) — useless as a label
|
|
1492
|
+
// anchor. Pin it to European Russia (~Volga) so a Europe view labels visible
|
|
1493
|
+
// western Russia on its eastern margin; on a world view this still sits over
|
|
1494
|
+
// Russian land. (See the curated-anchor smear-gate bypass in context-labels.)
|
|
1495
|
+
RU: [45, 58]
|
|
1496
|
+
};
|
|
1497
|
+
var MAX_CLUSTER_EXTENT_FACTOR = 0.18;
|
|
1498
|
+
var MAX_COLUMN_ROWS = 7;
|
|
1499
|
+
var REGION_LABEL_HALO_RATIO = 4.5;
|
|
1500
|
+
var LAND_TINT_LIGHT = 12;
|
|
1501
|
+
var LAND_TINT_DARK = 24;
|
|
1502
|
+
var TAG_TINT_LIGHT = 60;
|
|
1503
|
+
var TAG_TINT_DARK = 68;
|
|
1504
|
+
var WATER_TINT_LIGHT = 24;
|
|
1505
|
+
var WATER_TINT_DARK = 24;
|
|
1506
|
+
var RIVER_WIDTH = 1.3;
|
|
1507
|
+
var COMPACT_WIDTH_PX = 480;
|
|
1508
|
+
var RELIEF_MIN_AREA = 12;
|
|
1509
|
+
var RELIEF_MIN_DIM = 2;
|
|
1510
|
+
var RELIEF_HATCH_SPACING = 1.5;
|
|
1511
|
+
var RELIEF_HATCH_WIDTH = 0.2;
|
|
1512
|
+
var RELIEF_HATCH_STRENGTH = 26;
|
|
1513
|
+
var COASTLINE_RING_COUNT = 5;
|
|
1514
|
+
var COASTLINE_D0 = 16e-4;
|
|
1515
|
+
var COASTLINE_STEP = 28e-4;
|
|
1516
|
+
var COASTLINE_THICKNESS = 14e-4;
|
|
1517
|
+
var COASTLINE_OPACITY_NEAR = 0.5;
|
|
1518
|
+
var COASTLINE_OPACITY_FAR = 0.1;
|
|
1519
|
+
var COASTLINE_MIN_EXTENT = 6e-4;
|
|
1520
|
+
var COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
|
|
1521
|
+
var COASTLINE_STROKE_MIX = 32;
|
|
1522
|
+
var FOREIGN_TINT_LIGHT = 30;
|
|
1523
|
+
var FOREIGN_TINT_DARK = 62;
|
|
1524
|
+
var MUTED_FOREIGN_LIGHT = 28;
|
|
1525
|
+
var MUTED_FOREIGN_DARK = 16;
|
|
1526
|
+
var COLO_R = 9;
|
|
1527
|
+
var GOLDEN_ANGLE = 2.399963229728653;
|
|
1528
|
+
var STACK_OVERLAP = 1;
|
|
1529
|
+
var STACK_RING_MAX = 8;
|
|
1530
|
+
var STACK_RING_GAP = 4;
|
|
1531
|
+
function geomObject(topo) {
|
|
1532
|
+
const key = Object.keys(topo.objects)[0];
|
|
1533
|
+
return topo.objects[key];
|
|
1534
|
+
}
|
|
1535
|
+
var decodeCache = /* @__PURE__ */ new WeakMap();
|
|
1536
|
+
function mergeFeatures(a, b) {
|
|
1537
|
+
const polysOf = (f) => {
|
|
1538
|
+
const g = f.geometry;
|
|
1539
|
+
if (!g) return null;
|
|
1540
|
+
if (g.type === "Polygon") return [g.coordinates];
|
|
1541
|
+
if (g.type === "MultiPolygon") return g.coordinates;
|
|
1542
|
+
return null;
|
|
1543
|
+
};
|
|
1544
|
+
const pa = polysOf(a);
|
|
1545
|
+
const pb = polysOf(b);
|
|
1546
|
+
if (!pa || !pb) return a;
|
|
1547
|
+
return {
|
|
1548
|
+
...a,
|
|
1549
|
+
geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function decodeLayer(topo) {
|
|
1553
|
+
const cached = decodeCache.get(topo);
|
|
1554
|
+
if (cached) return cached;
|
|
1555
|
+
const out = /* @__PURE__ */ new Map();
|
|
1556
|
+
for (const g of geomObject(topo).geometries) {
|
|
1557
|
+
const f = feature(topo, g);
|
|
1558
|
+
if (!f.geometry) continue;
|
|
1559
|
+
const tagged = { ...f, id: g.id };
|
|
1560
|
+
const existing = out.get(g.id);
|
|
1561
|
+
out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
|
|
1562
|
+
}
|
|
1563
|
+
decodeCache.set(topo, out);
|
|
1564
|
+
return out;
|
|
1565
|
+
}
|
|
1566
|
+
function countryLabelPositions(args) {
|
|
1567
|
+
const { geometry, bounds, project, width, height, curated } = args;
|
|
1568
|
+
const w0 = bounds[0][0];
|
|
1569
|
+
const s0 = bounds[0][1];
|
|
1570
|
+
const e0 = bounds[1][0];
|
|
1571
|
+
const n0 = bounds[1][1];
|
|
1572
|
+
if (![w0, s0, e0, n0].every(Number.isFinite) || e0 <= w0 || n0 <= s0) {
|
|
1573
|
+
return mkCurated(curated, project);
|
|
1574
|
+
}
|
|
1575
|
+
const N = COUNTRY_POS_GRID;
|
|
1576
|
+
const onLand = [];
|
|
1577
|
+
const kept = [];
|
|
1578
|
+
for (let i = 0; i < N; i++) {
|
|
1579
|
+
onLand[i] = [];
|
|
1580
|
+
const lon = w0 + (i + 0.5) / N * (e0 - w0);
|
|
1581
|
+
for (let j = 0; j < N; j++) {
|
|
1582
|
+
const lat = s0 + (j + 0.5) / N * (n0 - s0);
|
|
1583
|
+
const land = pointInGeometry(geometry, lon, lat);
|
|
1584
|
+
onLand[i][j] = land;
|
|
1585
|
+
if (!land) continue;
|
|
1359
1586
|
const p = project(lon, lat);
|
|
1360
1587
|
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (p[0] > px1) px1 = p[0];
|
|
1364
|
-
if (p[1] < py0) py0 = p[1];
|
|
1365
|
-
if (p[1] > py1) py1 = p[1];
|
|
1588
|
+
if (p[0] < 0 || p[0] > width || p[1] < 0 || p[1] > height) continue;
|
|
1589
|
+
kept.push({ i, j, lon, lat, sx: p[0], sy: p[1] });
|
|
1366
1590
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1591
|
+
}
|
|
1592
|
+
if (!kept.length) return mkCurated(curated, project);
|
|
1593
|
+
const cx = kept.reduce((s, c) => s + c.sx, 0) / kept.length;
|
|
1594
|
+
const cy = kept.reduce((s, c) => s + c.sy, 0) / kept.length;
|
|
1595
|
+
const interiorness = (c) => {
|
|
1596
|
+
let n = 0;
|
|
1597
|
+
for (let di = -1; di <= 1; di++)
|
|
1598
|
+
for (let dj = -1; dj <= 1; dj++) {
|
|
1599
|
+
if (di === 0 && dj === 0) continue;
|
|
1600
|
+
const ni = c.i + di;
|
|
1601
|
+
const nj = c.j + dj;
|
|
1602
|
+
if (ni >= 0 && ni < N && nj >= 0 && nj < N && onLand[ni][nj]) n++;
|
|
1603
|
+
}
|
|
1604
|
+
return n;
|
|
1369
1605
|
};
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1606
|
+
const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
|
|
1607
|
+
const centreLon = kept.reduce((s, c) => s + c.lon, 0) / kept.length;
|
|
1608
|
+
const centreLat = kept.reduce((s, c) => s + c.lat, 0) / kept.length;
|
|
1609
|
+
const centreScreen = project(centreLon, centreLat);
|
|
1610
|
+
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 ? [
|
|
1611
|
+
{
|
|
1612
|
+
lonLat: [centreLon, centreLat],
|
|
1613
|
+
screen: [centreScreen[0], centreScreen[1]]
|
|
1377
1614
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1615
|
+
] : [];
|
|
1616
|
+
const grid = [...kept].sort(
|
|
1617
|
+
(a, b) => interiorness(b) - interiorness(a) || dist2ToCentre(a) - dist2ToCentre(b)
|
|
1618
|
+
).map((c) => ({
|
|
1619
|
+
lonLat: [c.lon, c.lat],
|
|
1620
|
+
screen: [c.sx, c.sy]
|
|
1621
|
+
}));
|
|
1622
|
+
const curatedPos = curated ? mkCurated(curated, project) : [];
|
|
1623
|
+
return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
|
|
1624
|
+
}
|
|
1625
|
+
function mkCurated(curated, project) {
|
|
1626
|
+
if (!curated) return [];
|
|
1627
|
+
const p = project(curated[0], curated[1]);
|
|
1628
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) return [];
|
|
1629
|
+
return [{ lonLat: [curated[0], curated[1]], screen: [p[0], p[1]] }];
|
|
1630
|
+
}
|
|
1631
|
+
var usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
|
|
1632
|
+
var alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
|
|
1633
|
+
var hawaiiProjection = () => geoMercator();
|
|
1634
|
+
function projectionFor(family, extent) {
|
|
1635
|
+
switch (family) {
|
|
1636
|
+
case "albers-usa":
|
|
1637
|
+
return usConusProjection();
|
|
1638
|
+
case "conic-equal-area": {
|
|
1639
|
+
const s = extent[0][1];
|
|
1640
|
+
const n = extent[1][1];
|
|
1641
|
+
return geoConicEqualArea().parallels([s + (n - s) / 6, s + (n - s) * 5 / 6]).center([0, (s + n) / 2]);
|
|
1642
|
+
}
|
|
1643
|
+
case "mercator":
|
|
1644
|
+
return geoMercator();
|
|
1645
|
+
case "equal-earth":
|
|
1646
|
+
return geoEqualEarth();
|
|
1647
|
+
case "equirectangular":
|
|
1648
|
+
return geoEquirectangular();
|
|
1649
|
+
case "natural-earth":
|
|
1650
|
+
return geoNaturalEarth1();
|
|
1651
|
+
default:
|
|
1652
|
+
return geoEquirectangular();
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
var INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
|
|
1656
|
+
var inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
|
|
1657
|
+
var inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
|
|
1658
|
+
var FOREIGN_BORDER = {
|
|
1659
|
+
CA: [
|
|
1660
|
+
"US-AK",
|
|
1661
|
+
"US-WA",
|
|
1662
|
+
"US-ID",
|
|
1663
|
+
"US-MT",
|
|
1664
|
+
"US-ND",
|
|
1665
|
+
"US-MN",
|
|
1666
|
+
"US-MI",
|
|
1667
|
+
"US-NY",
|
|
1668
|
+
"US-VT",
|
|
1669
|
+
"US-NH",
|
|
1670
|
+
"US-ME"
|
|
1671
|
+
],
|
|
1672
|
+
MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
|
|
1673
|
+
};
|
|
1674
|
+
var US_NON_CONUS = /* @__PURE__ */ new Set([
|
|
1675
|
+
"US-AK",
|
|
1676
|
+
"US-HI",
|
|
1677
|
+
"US-AS",
|
|
1678
|
+
"US-GU",
|
|
1679
|
+
"US-MP",
|
|
1680
|
+
"US-PR",
|
|
1681
|
+
"US-VI"
|
|
1682
|
+
]);
|
|
1683
|
+
function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
|
|
1684
|
+
return mix(
|
|
1685
|
+
palette.colors.blue,
|
|
1686
|
+
palette.bg,
|
|
1687
|
+
isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
function mapNeutralLandColor(palette, isDark, _dataActive = false) {
|
|
1691
|
+
return mix(
|
|
1692
|
+
palette.colors.green,
|
|
1693
|
+
palette.bg,
|
|
1694
|
+
isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
function buildMapProjection(resolved, data) {
|
|
1698
|
+
const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
|
|
1699
|
+
const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
|
|
1700
|
+
const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
|
|
1701
|
+
const worldLayer = new Map(decodeLayer(worldTopo));
|
|
1702
|
+
if (usCrisp && data.naLand) {
|
|
1703
|
+
const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
|
|
1704
|
+
const crisp = decodeLayer(data.naLand);
|
|
1705
|
+
for (const [iso, cf] of crisp) {
|
|
1706
|
+
const base = worldLayer.get(iso);
|
|
1707
|
+
if (!base) continue;
|
|
1708
|
+
const [[bw, bs], [be, bn]] = geoBounds(base);
|
|
1709
|
+
if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
|
|
1710
|
+
worldLayer.set(iso, { ...cf, properties: base.properties });
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
|
|
1714
|
+
const extentOutline = () => {
|
|
1715
|
+
const [[w, s], [e, n]] = resolved.extent;
|
|
1716
|
+
const N = 16;
|
|
1717
|
+
const coords = [];
|
|
1718
|
+
for (let i = 0; i <= N; i++) {
|
|
1719
|
+
const t = i / N;
|
|
1720
|
+
const lon = w + (e - w) * t;
|
|
1721
|
+
const lat = s + (n - s) * t;
|
|
1722
|
+
coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
|
|
1386
1723
|
}
|
|
1387
|
-
return
|
|
1724
|
+
return {
|
|
1725
|
+
type: "Feature",
|
|
1726
|
+
properties: {},
|
|
1727
|
+
geometry: { type: "MultiPoint", coordinates: coords }
|
|
1728
|
+
};
|
|
1388
1729
|
};
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1730
|
+
let fitFeatures;
|
|
1731
|
+
if (resolved.projection === "albers-usa" && usLayer) {
|
|
1732
|
+
fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
|
|
1733
|
+
const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
|
|
1734
|
+
if (neighborPoints.length > 0) {
|
|
1735
|
+
fitFeatures.push({
|
|
1736
|
+
type: "Feature",
|
|
1737
|
+
properties: {},
|
|
1738
|
+
geometry: { type: "MultiPoint", coordinates: neighborPoints }
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
for (const r of resolved.regions) {
|
|
1742
|
+
if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
|
|
1743
|
+
const cf = worldLayer.get(r.iso);
|
|
1744
|
+
if (cf) fitFeatures.push(cf);
|
|
1400
1745
|
}
|
|
1401
1746
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1747
|
+
} else {
|
|
1748
|
+
fitFeatures = [extentOutline()];
|
|
1749
|
+
}
|
|
1750
|
+
const fitTarget = { type: "FeatureCollection", features: fitFeatures };
|
|
1751
|
+
const projection = projectionFor(resolved.projection, resolved.extent);
|
|
1752
|
+
if (resolved.projection !== "albers-usa") {
|
|
1753
|
+
let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
|
|
1754
|
+
if (centerLon > 180) centerLon -= 360;
|
|
1755
|
+
projection.rotate([-centerLon, 0]);
|
|
1756
|
+
}
|
|
1757
|
+
const fitGB = geoBounds(fitTarget);
|
|
1758
|
+
const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
|
|
1759
|
+
return {
|
|
1760
|
+
projection,
|
|
1761
|
+
fitTarget,
|
|
1762
|
+
fitIsGlobal,
|
|
1763
|
+
worldLayer,
|
|
1764
|
+
usLayer,
|
|
1765
|
+
usCrisp,
|
|
1766
|
+
wantsUsStates,
|
|
1767
|
+
worldTopo
|
|
1407
1768
|
};
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1769
|
+
}
|
|
1770
|
+
function parsePathRings(d) {
|
|
1771
|
+
const rings = [];
|
|
1772
|
+
let cur = [];
|
|
1773
|
+
const re = /([MLZ])([^MLZ]*)/g;
|
|
1774
|
+
let m;
|
|
1775
|
+
while (m = re.exec(d)) {
|
|
1776
|
+
if (m[1] === "Z") {
|
|
1777
|
+
if (cur.length) rings.push(cur);
|
|
1778
|
+
cur = [];
|
|
1779
|
+
continue;
|
|
1414
1780
|
}
|
|
1415
|
-
if (
|
|
1416
|
-
|
|
1417
|
-
|
|
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 } };
|
|
1781
|
+
if (m[1] === "M" && cur.length) {
|
|
1782
|
+
rings.push(cur);
|
|
1783
|
+
cur = [];
|
|
1423
1784
|
}
|
|
1424
|
-
|
|
1785
|
+
const nums = m[2].split(/[ ,]+/).map(Number);
|
|
1786
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
1787
|
+
const x = nums[i];
|
|
1788
|
+
const y = nums[i + 1];
|
|
1789
|
+
if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
if (cur.length) rings.push(cur);
|
|
1793
|
+
return rings;
|
|
1794
|
+
}
|
|
1795
|
+
function dropAntimeridianWrapSlivers(d, width, height) {
|
|
1796
|
+
const rings = parsePathRings(d);
|
|
1797
|
+
if (rings.length <= 1) return d;
|
|
1798
|
+
const eps = 0.75;
|
|
1799
|
+
const minArea = 3e-3 * width * height;
|
|
1800
|
+
const ringArea = (r) => {
|
|
1801
|
+
let s = 0;
|
|
1802
|
+
for (let i = 0; i < r.length; i++) {
|
|
1803
|
+
const a = r[i];
|
|
1804
|
+
const b = r[(i + 1) % r.length];
|
|
1805
|
+
s += a[0] * b[1] - b[0] * a[1];
|
|
1806
|
+
}
|
|
1807
|
+
return Math.abs(s) / 2;
|
|
1425
1808
|
};
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
});
|
|
1809
|
+
const areas = rings.map(ringArea);
|
|
1810
|
+
const maxArea = Math.max(...areas);
|
|
1811
|
+
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;
|
|
1812
|
+
let dropped = false;
|
|
1813
|
+
const kept = rings.filter((r, idx) => {
|
|
1814
|
+
if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
|
|
1815
|
+
const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
|
|
1816
|
+
if (touches) {
|
|
1817
|
+
dropped = true;
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
return true;
|
|
1821
|
+
});
|
|
1822
|
+
if (!dropped) return d;
|
|
1823
|
+
return kept.map(
|
|
1824
|
+
(r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
|
|
1825
|
+
).join("");
|
|
1826
|
+
}
|
|
1827
|
+
function albersSkewFallback(resolved, data, width, height) {
|
|
1828
|
+
if (resolved.projection !== "albers-usa") return false;
|
|
1829
|
+
if (!resolved.basemaps.subdivisions.includes("us-states")) return false;
|
|
1830
|
+
if (!(width > 0) || !(height > 0)) return false;
|
|
1831
|
+
const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
|
|
1832
|
+
const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
|
|
1833
|
+
if (akRef || hiRef) return false;
|
|
1834
|
+
const { projection, fitTarget, usLayer } = buildMapProjection(resolved, data);
|
|
1835
|
+
if (!usLayer || ![...usLayer.keys()].some((iso) => !US_NON_CONUS.has(iso)))
|
|
1836
|
+
return false;
|
|
1837
|
+
projection.fitSize([1e3, 1e3], fitTarget);
|
|
1838
|
+
const b = geoPath2(projection).bounds(fitTarget);
|
|
1839
|
+
const conusAspect = (b[1][0] - b[0][0]) / (b[1][1] - b[0][1]);
|
|
1840
|
+
if (!(conusAspect > 0)) return false;
|
|
1841
|
+
const canvasAspect = width / height;
|
|
1842
|
+
const skew = Math.max(canvasAspect / conusAspect, conusAspect / canvasAspect);
|
|
1843
|
+
return skew > ALBERS_SKEW_MAX;
|
|
1844
|
+
}
|
|
1845
|
+
var REGION_GEO_MEMO = /* @__PURE__ */ new WeakMap();
|
|
1846
|
+
var REGION_GEO_MAX_FITS = 8;
|
|
1847
|
+
function regionGeoBucket(resolved, projKey) {
|
|
1848
|
+
let byFit = REGION_GEO_MEMO.get(resolved);
|
|
1849
|
+
if (!byFit) {
|
|
1850
|
+
byFit = /* @__PURE__ */ new Map();
|
|
1851
|
+
REGION_GEO_MEMO.set(resolved, byFit);
|
|
1852
|
+
}
|
|
1853
|
+
let bucket = byFit.get(projKey);
|
|
1854
|
+
if (!bucket) {
|
|
1855
|
+
bucket = /* @__PURE__ */ new Map();
|
|
1856
|
+
byFit.set(projKey, bucket);
|
|
1857
|
+
if (byFit.size > REGION_GEO_MAX_FITS) {
|
|
1858
|
+
const oldest = byFit.keys().next().value;
|
|
1859
|
+
if (oldest !== void 0) byFit.delete(oldest);
|
|
1486
1860
|
}
|
|
1861
|
+
}
|
|
1862
|
+
return bucket;
|
|
1863
|
+
}
|
|
1864
|
+
function layoutMap(resolvedIn, data, size, opts) {
|
|
1865
|
+
const { palette, isDark } = opts;
|
|
1866
|
+
const { width, height } = size;
|
|
1867
|
+
let resolved = resolvedIn;
|
|
1868
|
+
if (albersSkewFallback(resolved, data, width, height)) {
|
|
1869
|
+
resolved = { ...resolved, projection: "conic-equal-area" };
|
|
1870
|
+
}
|
|
1871
|
+
const {
|
|
1872
|
+
projection,
|
|
1873
|
+
fitTarget,
|
|
1874
|
+
fitIsGlobal,
|
|
1875
|
+
worldLayer,
|
|
1876
|
+
usLayer,
|
|
1877
|
+
usCrisp,
|
|
1878
|
+
worldTopo
|
|
1879
|
+
} = buildMapProjection(resolved, data);
|
|
1880
|
+
const usContext = usLayer !== null;
|
|
1881
|
+
const regionStroke = isDark ? mix(palette.border, palette.text, 65) : mix(palette.text, palette.bg, 78);
|
|
1882
|
+
const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
|
|
1883
|
+
const rampMin = values.length > 0 ? Math.min(...values) : 0;
|
|
1884
|
+
const rampMax = Math.max(...values);
|
|
1885
|
+
const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
|
|
1886
|
+
const rampLow = resolved.directives.regionMetricLowColor ? resolveColor(resolved.directives.regionMetricLowColor, palette) ?? void 0 : void 0;
|
|
1887
|
+
const hasRamp = values.length > 0;
|
|
1888
|
+
const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
|
|
1889
|
+
const matchColorGroup = (v) => {
|
|
1890
|
+
const lv = v.trim().toLowerCase();
|
|
1891
|
+
if (lv === "none") return null;
|
|
1892
|
+
if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
|
|
1893
|
+
const tg = resolved.tagGroups.find((g) => g.name.toLowerCase() === lv);
|
|
1894
|
+
return tg ? tg.name : v;
|
|
1487
1895
|
};
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
}
|
|
1896
|
+
const fillGroupNames = /* @__PURE__ */ new Set();
|
|
1897
|
+
for (const g of resolved.tagGroups) {
|
|
1898
|
+
const k = g.name.toLowerCase();
|
|
1899
|
+
if (resolved.regions.some((r) => r.tags[k]) || resolved.pois.some((p) => p.tags[k]))
|
|
1900
|
+
fillGroupNames.add(g.name);
|
|
1506
1901
|
}
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1902
|
+
const firstFillGroup = resolved.tagGroups.find((g) => fillGroupNames.has(g.name))?.name ?? null;
|
|
1903
|
+
const override = opts.activeGroup;
|
|
1904
|
+
let activeGroup;
|
|
1905
|
+
if (override !== void 0) {
|
|
1906
|
+
activeGroup = override === null ? null : matchColorGroup(override);
|
|
1907
|
+
} else if (resolved.directives.activeTag !== void 0) {
|
|
1908
|
+
activeGroup = matchColorGroup(resolved.directives.activeTag);
|
|
1909
|
+
} else {
|
|
1910
|
+
activeGroup = VALUE_NAME ?? firstFillGroup ?? resolved.tagGroups[0]?.name ?? null;
|
|
1911
|
+
}
|
|
1912
|
+
const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
|
|
1913
|
+
const mutedBasemap = activeIsScore || activeGroup !== null && fillGroupNames.has(activeGroup);
|
|
1914
|
+
const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
|
|
1915
|
+
const water = mapBackgroundColor(palette, isDark, mutedBasemap);
|
|
1916
|
+
const lakeStroke = mix(regionStroke, water, 45);
|
|
1917
|
+
const foreignFill = mix(
|
|
1918
|
+
palette.colors.gray,
|
|
1919
|
+
palette.bg,
|
|
1920
|
+
mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
|
|
1921
|
+
);
|
|
1922
|
+
const contrastFloorTone = (tone, ...substrates) => {
|
|
1923
|
+
const clears = (t) => substrates.every((s) => contrastRatio(t, s) >= REGION_LABEL_HALO_RATIO);
|
|
1924
|
+
if (clears(tone)) return tone;
|
|
1925
|
+
for (let s = 10; s <= 100; s += 10) {
|
|
1926
|
+
const t = mix(palette.text, tone, s);
|
|
1927
|
+
if (clears(t)) return t;
|
|
1516
1928
|
}
|
|
1517
|
-
return
|
|
1929
|
+
return palette.text;
|
|
1518
1930
|
};
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1931
|
+
const backdropLandTone = contrastFloorTone(
|
|
1932
|
+
palette.textMuted,
|
|
1933
|
+
neutralFill,
|
|
1934
|
+
water
|
|
1935
|
+
);
|
|
1936
|
+
const backdropWaterTone = contrastFloorTone(
|
|
1937
|
+
mix(palette.colors.blue, palette.textMuted, 50),
|
|
1938
|
+
water,
|
|
1939
|
+
neutralFill
|
|
1940
|
+
);
|
|
1941
|
+
const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
|
|
1942
|
+
const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
|
|
1943
|
+
const colorByIso = /* @__PURE__ */ new Map();
|
|
1944
|
+
if (colorizeActive) {
|
|
1945
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1946
|
+
const addEdges = (src) => {
|
|
1947
|
+
for (const [iso, ns] of src) {
|
|
1948
|
+
const cur = adjacency.get(iso);
|
|
1949
|
+
if (cur) cur.push(...ns);
|
|
1950
|
+
else adjacency.set(iso, [...ns]);
|
|
1528
1951
|
}
|
|
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
1952
|
};
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1953
|
+
addEdges(buildAdjacency(worldTopo));
|
|
1954
|
+
if (usLayer) {
|
|
1955
|
+
addEdges(buildAdjacency(data.usStates));
|
|
1956
|
+
for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
|
|
1957
|
+
const cn = adjacency.get(country);
|
|
1958
|
+
if (!cn) continue;
|
|
1959
|
+
for (const st of states) {
|
|
1960
|
+
const sn = adjacency.get(st);
|
|
1961
|
+
if (!sn) continue;
|
|
1962
|
+
cn.push(st);
|
|
1963
|
+
sn.push(country);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1603
1966
|
}
|
|
1967
|
+
const { byIso, huesNeeded } = assignColors(
|
|
1968
|
+
[...adjacency.keys()],
|
|
1969
|
+
adjacency
|
|
1970
|
+
);
|
|
1971
|
+
const tints = politicalTints(palette, huesNeeded, isDark);
|
|
1972
|
+
for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
|
|
1604
1973
|
}
|
|
1605
|
-
const
|
|
1606
|
-
const
|
|
1607
|
-
const
|
|
1608
|
-
const
|
|
1609
|
-
const
|
|
1610
|
-
if (
|
|
1611
|
-
|
|
1612
|
-
|
|
1974
|
+
const colorizeStroke = (fill) => mix(fill, palette.text, 35);
|
|
1975
|
+
const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
|
|
1976
|
+
const rampLowFloor = mix(rampHue, rampBase, RAMP_FLOOR);
|
|
1977
|
+
const fillForValue = (s) => {
|
|
1978
|
+
const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
|
|
1979
|
+
if (rampLow !== void 0)
|
|
1980
|
+
return valueRampColor(rampLow, rampHue, t, { isDark });
|
|
1981
|
+
const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
|
|
1982
|
+
return mix(rampHue, rampBase, pct);
|
|
1613
1983
|
};
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1984
|
+
const tagFill = (tags, groupName) => {
|
|
1985
|
+
if (!groupName) return null;
|
|
1986
|
+
const group = resolved.tagGroups.find(
|
|
1987
|
+
(g) => g.name.toLowerCase() === groupName.toLowerCase()
|
|
1988
|
+
);
|
|
1989
|
+
if (!group) return null;
|
|
1990
|
+
const val = tags[tagAttrKey(group.name)] ?? group.defaultValue;
|
|
1991
|
+
if (!val) return null;
|
|
1992
|
+
const entry = group.entries.find(
|
|
1993
|
+
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
1994
|
+
);
|
|
1995
|
+
if (!entry?.color) return null;
|
|
1996
|
+
return mix(
|
|
1997
|
+
entry.color,
|
|
1998
|
+
palette.bg,
|
|
1999
|
+
isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
|
|
2000
|
+
);
|
|
1617
2001
|
};
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1620
|
-
if (
|
|
1621
|
-
|
|
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
|
-
};
|
|
2002
|
+
const directFill = (name) => {
|
|
2003
|
+
const hex = name ? resolveColor(name, palette) : null;
|
|
2004
|
+
if (!hex) return null;
|
|
2005
|
+
return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
|
|
1635
2006
|
};
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
1642
|
-
);
|
|
1643
|
-
if (entry?.color) return entry.color;
|
|
2007
|
+
const regionFill = (r) => {
|
|
2008
|
+
const direct = directFill(r.color);
|
|
2009
|
+
if (direct) return direct;
|
|
2010
|
+
if (activeIsScore) {
|
|
2011
|
+
return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
|
|
1644
2012
|
}
|
|
1645
|
-
return
|
|
2013
|
+
if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
|
|
2014
|
+
return tagFill(r.tags, activeGroup) ?? neutralFill;
|
|
1646
2015
|
};
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
});
|
|
2016
|
+
const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
|
|
2017
|
+
let legend = null;
|
|
2018
|
+
if (!resolved.directives.noLegend) {
|
|
2019
|
+
const legendTagGroups = resolved.tagGroups.map((g) => ({
|
|
2020
|
+
name: g.name,
|
|
2021
|
+
entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
|
|
2022
|
+
}));
|
|
2023
|
+
if (legendTagGroups.length > 0 || hasRamp) {
|
|
2024
|
+
legend = {
|
|
2025
|
+
tagGroups: legendTagGroups,
|
|
2026
|
+
activeGroup,
|
|
2027
|
+
...hasRamp && {
|
|
2028
|
+
ramp: {
|
|
2029
|
+
...resolved.directives.regionMetric !== void 0 && {
|
|
2030
|
+
metric: resolved.directives.regionMetric
|
|
2031
|
+
},
|
|
2032
|
+
min: rampMin,
|
|
2033
|
+
max: rampMax,
|
|
2034
|
+
low: rampLow ?? rampLowFloor,
|
|
2035
|
+
high: rampHue
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
1654
2040
|
}
|
|
1655
|
-
const
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
for (const p of orderedPois) {
|
|
1662
|
-
const xy = project(p.lon, p.lat);
|
|
1663
|
-
if (xy) projected.push({ p, xy });
|
|
2041
|
+
const shownTitle = resolved.directives.noTitle ? null : resolved.title;
|
|
2042
|
+
const TITLE_GAP = 16;
|
|
2043
|
+
let topPad = FIT_PAD;
|
|
2044
|
+
if (shownTitle && resolved.pois.length > 0) {
|
|
2045
|
+
const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
|
|
2046
|
+
topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
|
|
1664
2047
|
}
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2048
|
+
const legendBand = mapLegendBand(legend, {
|
|
2049
|
+
width,
|
|
2050
|
+
mode: opts.legendMode ?? "preview",
|
|
2051
|
+
hasTitle: Boolean(shownTitle),
|
|
2052
|
+
hasSubtitle: Boolean(resolved.subtitle)
|
|
2053
|
+
});
|
|
2054
|
+
if (legendBand > topPad) topPad = legendBand;
|
|
2055
|
+
const topReserved = [];
|
|
2056
|
+
if (shownTitle) {
|
|
2057
|
+
const lines = resolved.subtitle ? 2 : 1;
|
|
2058
|
+
const tw = Math.max(
|
|
2059
|
+
measureLegendText(shownTitle, TITLE_FONT_SIZE),
|
|
2060
|
+
resolved.subtitle ? measureLegendText(resolved.subtitle, TITLE_FONT_SIZE) : 0
|
|
2061
|
+
) + 2 * TITLE_FONT_SIZE;
|
|
2062
|
+
topReserved.push({
|
|
2063
|
+
x: width / 2 - tw / 2,
|
|
2064
|
+
y: TITLE_Y - TITLE_FONT_SIZE,
|
|
2065
|
+
w: tw,
|
|
2066
|
+
h: TITLE_FONT_SIZE * (lines + 0.5)
|
|
1684
2067
|
});
|
|
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
2068
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
2069
|
+
const legendBox = mapLegendBox(legend, {
|
|
2070
|
+
width,
|
|
2071
|
+
mode: opts.legendMode ?? "preview",
|
|
2072
|
+
hasTitle: Boolean(shownTitle),
|
|
2073
|
+
hasSubtitle: Boolean(resolved.subtitle)
|
|
2074
|
+
});
|
|
2075
|
+
if (legendBox)
|
|
2076
|
+
topReserved.push({
|
|
2077
|
+
x: legendBox.x,
|
|
2078
|
+
y: legendBox.y,
|
|
2079
|
+
w: legendBox.width,
|
|
2080
|
+
h: legendBox.height
|
|
2081
|
+
});
|
|
2082
|
+
const reserve = opts._calloutReserve;
|
|
2083
|
+
const fitLeft = FIT_PAD + (reserve?.left ?? 0);
|
|
2084
|
+
const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
|
|
2085
|
+
const fitTop = topPad + (reserve?.top ?? 0);
|
|
2086
|
+
const fitBottom = height - FIT_PAD - (reserve?.bottom ?? 0);
|
|
2087
|
+
const fitBox = [
|
|
2088
|
+
[fitLeft, fitTop],
|
|
2089
|
+
[Math.max(fitLeft + 1, fitRight), Math.max(fitTop + 1, fitBottom)]
|
|
2090
|
+
];
|
|
2091
|
+
projection.fitExtent(fitBox, fitTarget);
|
|
2092
|
+
if (!fitIsGlobal && resolved.projection !== "albers-usa" && resolved.regions.length > 0) {
|
|
2093
|
+
let yMin = Infinity;
|
|
2094
|
+
let yMax = -Infinity;
|
|
2095
|
+
for (const r of resolved.regions) {
|
|
2096
|
+
const bb = r.iso ? featureBboxPrimary(data.worldCoarse, r.iso) : null;
|
|
2097
|
+
if (!bb) continue;
|
|
2098
|
+
for (const lon of [bb[0][0], bb[1][0]]) {
|
|
2099
|
+
for (const lat of [bb[0][1], bb[1][1]]) {
|
|
2100
|
+
const p = projection([lon, lat]);
|
|
2101
|
+
if (p && Number.isFinite(p[1])) {
|
|
2102
|
+
if (p[1] < yMin) yMin = p[1];
|
|
2103
|
+
if (p[1] > yMax) yMax = p[1];
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (yMin < yMax) {
|
|
2109
|
+
const boxTop = fitTop;
|
|
2110
|
+
const boxBottom = fitBottom;
|
|
2111
|
+
let dy = (boxTop + boxBottom) / 2 - (yMin + yMax) / 2;
|
|
2112
|
+
const minDy = boxTop - yMin;
|
|
2113
|
+
const maxDy = boxBottom - yMax;
|
|
2114
|
+
if (minDy <= maxDy) dy = Math.max(minDy, Math.min(maxDy, dy));
|
|
2115
|
+
const [tx, ty] = projection.translate();
|
|
2116
|
+
projection.translate([tx, ty + dy]);
|
|
2117
|
+
}
|
|
1694
2118
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
if (
|
|
2119
|
+
if (!fitIsGlobal && resolved.projection === "albers-usa") {
|
|
2120
|
+
const cb = geoPath2(projection).bounds(fitTarget);
|
|
2121
|
+
if (Number.isFinite(cb[0][1])) {
|
|
2122
|
+
const slack = fitBottom - fitTop - (cb[1][1] - cb[0][1]);
|
|
2123
|
+
if (slack > 0) {
|
|
2124
|
+
const currentTopGap = cb[0][1] - fitTop;
|
|
2125
|
+
const dy = slack * 0.3 - currentTopGap;
|
|
2126
|
+
const [tx, ty] = projection.translate();
|
|
2127
|
+
projection.translate([tx, ty + dy]);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
1698
2130
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
|
|
2131
|
+
let path;
|
|
2132
|
+
let project;
|
|
2133
|
+
let stretchParams = null;
|
|
2134
|
+
if (fitIsGlobal && !opts.preferContain) {
|
|
2135
|
+
const cb = geoPath2(projection).bounds(fitTarget);
|
|
2136
|
+
const bx0 = cb[0][0];
|
|
2137
|
+
const by0 = cb[0][1];
|
|
2138
|
+
const cw = cb[1][0] - bx0;
|
|
2139
|
+
const ch = cb[1][1] - by0;
|
|
2140
|
+
const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
|
|
2141
|
+
const ox = 0;
|
|
2142
|
+
const oy = topReserve;
|
|
2143
|
+
const sx = cw > 0 ? width / cw : 1;
|
|
2144
|
+
const sy = ch > 0 ? (height - topReserve) / ch : 1;
|
|
2145
|
+
stretchParams = { sx, sy, ox, oy, bx0, by0 };
|
|
2146
|
+
const stretch = (x, y) => [
|
|
2147
|
+
ox + (x - bx0) * sx,
|
|
2148
|
+
oy + (y - by0) * sy
|
|
2149
|
+
];
|
|
2150
|
+
const baseProjection = projection;
|
|
2151
|
+
const tx = geoTransform({
|
|
2152
|
+
point(x, y) {
|
|
2153
|
+
const [px, py] = stretch(x, y);
|
|
2154
|
+
this.stream.point(px, py);
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
const thin = geoThin();
|
|
2158
|
+
path = geoPath2({
|
|
2159
|
+
stream: (s) => baseProjection.stream(
|
|
2160
|
+
tx.stream(
|
|
2161
|
+
thin.stream(s)
|
|
2162
|
+
)
|
|
1706
2163
|
)
|
|
1707
|
-
);
|
|
1708
|
-
|
|
1709
|
-
|
|
2164
|
+
}).digits(PATH_DIGITS);
|
|
2165
|
+
project = (lon, lat) => {
|
|
2166
|
+
const p = baseProjection([lon, lat]);
|
|
2167
|
+
return p ? stretch(p[0], p[1]) : null;
|
|
2168
|
+
};
|
|
2169
|
+
} else {
|
|
2170
|
+
projection.clipExtent([
|
|
2171
|
+
[0, 0],
|
|
2172
|
+
[width, height]
|
|
2173
|
+
]);
|
|
2174
|
+
const thin = geoThin();
|
|
2175
|
+
path = geoPath2({
|
|
2176
|
+
stream: (s) => projection.stream(
|
|
2177
|
+
thin.stream(s)
|
|
2178
|
+
)
|
|
2179
|
+
}).digits(PATH_DIGITS);
|
|
2180
|
+
project = (lon, lat) => projection([lon, lat]) ?? null;
|
|
1710
2181
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2182
|
+
const regionGeo = regionGeoBucket(
|
|
2183
|
+
resolvedIn,
|
|
2184
|
+
JSON.stringify([
|
|
2185
|
+
width,
|
|
2186
|
+
height,
|
|
2187
|
+
[
|
|
2188
|
+
[-100, 40],
|
|
2189
|
+
[-120, 37],
|
|
2190
|
+
[-80, 40],
|
|
2191
|
+
[0, 0],
|
|
2192
|
+
[60, 30],
|
|
2193
|
+
[-60, -30],
|
|
2194
|
+
[150, 60],
|
|
2195
|
+
[-150, -60],
|
|
2196
|
+
[100, -40],
|
|
2197
|
+
[-30, 50]
|
|
2198
|
+
].map(([lo, la]) => project(lo, la))
|
|
2199
|
+
])
|
|
2200
|
+
);
|
|
2201
|
+
const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
|
|
2202
|
+
const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
|
|
2203
|
+
const { insets, insetRegions, insetLabelSeeds } = resolved.projection === "albers-usa" && usLayer && (akRef || hiRef) ? layoutInsets({
|
|
2204
|
+
resolved,
|
|
2205
|
+
projection,
|
|
2206
|
+
usLayer,
|
|
2207
|
+
worldLayer,
|
|
2208
|
+
regionById,
|
|
2209
|
+
width,
|
|
2210
|
+
height,
|
|
2211
|
+
fitPad: FIT_PAD,
|
|
2212
|
+
pathDigits: PATH_DIGITS,
|
|
2213
|
+
nonConus: US_NON_CONUS,
|
|
2214
|
+
colorizeActive,
|
|
2215
|
+
colorByIso,
|
|
2216
|
+
neutralFill,
|
|
2217
|
+
foreignFill,
|
|
2218
|
+
regionStroke,
|
|
2219
|
+
colorizeStroke,
|
|
2220
|
+
regionFill,
|
|
2221
|
+
alaskaProjection,
|
|
2222
|
+
hawaiiProjection,
|
|
2223
|
+
akRef,
|
|
2224
|
+
hiRef
|
|
2225
|
+
}) : { insets: [], insetRegions: [], insetLabelSeeds: [] };
|
|
2226
|
+
const conusFit = resolved.projection === "albers-usa" && !!usLayer;
|
|
2227
|
+
const classifyExtent = conusFit ? geoBounds(fitTarget) : resolved.extent;
|
|
2228
|
+
const dLonSpan = classifyExtent[1][0] - classifyExtent[0][0];
|
|
2229
|
+
const dLatSpan = classifyExtent[1][1] - classifyExtent[0][1];
|
|
2230
|
+
const isGlobalView = dLonSpan >= 270 || dLatSpan >= 130;
|
|
2231
|
+
const vLonCenter = (classifyExtent[0][0] + classifyExtent[1][0]) / 2;
|
|
2232
|
+
const normLon = (lon) => {
|
|
2233
|
+
let L = lon;
|
|
2234
|
+
while (L < vLonCenter - 180) L += 360;
|
|
2235
|
+
while (L > vLonCenter + 180) L -= 360;
|
|
2236
|
+
return L;
|
|
2237
|
+
};
|
|
2238
|
+
const ringOverlapsView = (ring) => {
|
|
2239
|
+
let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
|
|
2240
|
+
const lons = [];
|
|
2241
|
+
for (const [rawLon] of ring) {
|
|
2242
|
+
const lon = normLon(rawLon);
|
|
2243
|
+
lons.push(lon);
|
|
2244
|
+
if (lon < loMin) loMin = lon;
|
|
2245
|
+
if (lon > loMax) loMax = lon;
|
|
2246
|
+
if (rawLon < rawMin) rawMin = rawLon;
|
|
2247
|
+
if (rawLon > rawMax) rawMax = rawLon;
|
|
1715
2248
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
);
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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);
|
|
2249
|
+
lons.sort((a, b) => a - b);
|
|
2250
|
+
let maxGap = 0;
|
|
2251
|
+
for (let i = 1; i < lons.length; i++)
|
|
2252
|
+
maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
|
|
2253
|
+
if (lons.length > 1)
|
|
2254
|
+
maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
|
|
2255
|
+
const occupiedArc = 360 - maxGap;
|
|
2256
|
+
if (occupiedArc > 270) return false;
|
|
2257
|
+
if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
|
|
2258
|
+
let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
|
|
2259
|
+
for (const [lon, lat] of ring) {
|
|
2260
|
+
const p = project(lon, lat);
|
|
2261
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
2262
|
+
anyFinite = true;
|
|
2263
|
+
if (p[0] < px0) px0 = p[0];
|
|
2264
|
+
if (p[0] > px1) px1 = p[0];
|
|
2265
|
+
if (p[1] < py0) py0 = p[1];
|
|
2266
|
+
if (p[1] > py1) py1 = p[1];
|
|
1748
2267
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
if (
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
placePoi(e, fx, fy, clusterId);
|
|
1760
|
-
legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
|
|
2268
|
+
if (!anyFinite) return false;
|
|
2269
|
+
return !(px1 < 0 || px0 > width || py1 < 0 || py0 > height);
|
|
2270
|
+
};
|
|
2271
|
+
const cullFeatureToView = (f) => {
|
|
2272
|
+
if (isGlobalView) return f;
|
|
2273
|
+
const g = f.geometry;
|
|
2274
|
+
if (!g) return f;
|
|
2275
|
+
if (g.type === "Polygon") {
|
|
2276
|
+
const ring = g.coordinates[0];
|
|
2277
|
+
return ringOverlapsView(ring) ? f : null;
|
|
1761
2278
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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;
|
|
2279
|
+
if (g.type === "MultiPolygon") {
|
|
2280
|
+
const polys = g.coordinates;
|
|
2281
|
+
const keep = polys.filter(
|
|
2282
|
+
(p) => ringOverlapsView(p[0])
|
|
2283
|
+
);
|
|
2284
|
+
if (!keep.length) return null;
|
|
2285
|
+
if (keep.length === polys.length) return f;
|
|
2286
|
+
return { ...f, geometry: { ...g, coordinates: keep } };
|
|
2287
|
+
}
|
|
2288
|
+
return f;
|
|
1779
2289
|
};
|
|
1780
|
-
const
|
|
1781
|
-
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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}`;
|
|
2290
|
+
const SEAM_SLIVER_MAX_SPAN = 100;
|
|
2291
|
+
const ringIsFrameFiller = (ring) => {
|
|
2292
|
+
const lons = ring.map(([lon]) => lon).sort((a, b) => a - b);
|
|
2293
|
+
if (lons.length < 2) return false;
|
|
2294
|
+
let maxGap = -1;
|
|
2295
|
+
let gapIdx = 0;
|
|
2296
|
+
for (let i = 1; i < lons.length; i++) {
|
|
2297
|
+
const g = lons[i] - lons[i - 1];
|
|
2298
|
+
if (g > maxGap) {
|
|
2299
|
+
maxGap = g;
|
|
2300
|
+
gapIdx = i;
|
|
2301
|
+
}
|
|
1796
2302
|
}
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
const
|
|
1800
|
-
const
|
|
1801
|
-
|
|
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}`;
|
|
2303
|
+
const wrapGap = lons[0] + 360 - lons[lons.length - 1];
|
|
2304
|
+
if (wrapGap >= maxGap) return false;
|
|
2305
|
+
const span = 360 - maxGap;
|
|
2306
|
+
const east = lons[gapIdx - 1] + 360;
|
|
2307
|
+
return east > 180 && span < SEAM_SLIVER_MAX_SPAN;
|
|
1809
2308
|
};
|
|
1810
|
-
const
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
if (
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2309
|
+
const dropFrameFillers = (f) => {
|
|
2310
|
+
const g = f.geometry;
|
|
2311
|
+
if (!g) return f;
|
|
2312
|
+
if (g.type === "Polygon") {
|
|
2313
|
+
const ring = g.coordinates[0];
|
|
2314
|
+
return ringIsFrameFiller(ring) ? null : f;
|
|
2315
|
+
}
|
|
2316
|
+
if (g.type === "MultiPolygon") {
|
|
2317
|
+
const polys = g.coordinates;
|
|
2318
|
+
const keep = polys.filter(
|
|
2319
|
+
(p) => !ringIsFrameFiller(p[0])
|
|
2320
|
+
);
|
|
2321
|
+
if (!keep.length) return null;
|
|
2322
|
+
if (keep.length === polys.length) return f;
|
|
2323
|
+
return { ...f, geometry: { ...g, coordinates: keep } };
|
|
2324
|
+
}
|
|
2325
|
+
return f;
|
|
1822
2326
|
};
|
|
1823
|
-
const
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2327
|
+
const regions = [];
|
|
2328
|
+
const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
|
|
2329
|
+
for (const [iso, f] of layerFeatures) {
|
|
2330
|
+
if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
|
|
2331
|
+
continue;
|
|
2332
|
+
if (layerKind === "country" && usContext && iso === "US") continue;
|
|
2333
|
+
if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
|
|
2334
|
+
continue;
|
|
2335
|
+
const r = regionById.get(iso);
|
|
2336
|
+
const geoK = layerKind + ":" + iso;
|
|
2337
|
+
let geo = regionGeo.get(geoK);
|
|
2338
|
+
if (geo === void 0) {
|
|
2339
|
+
const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
|
|
2340
|
+
if (!viewF) {
|
|
2341
|
+
regionGeo.set(geoK, null);
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
const raw = path(viewF) ?? "";
|
|
2345
|
+
const d0 = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
|
|
2346
|
+
if (!d0) {
|
|
2347
|
+
regionGeo.set(geoK, null);
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
const anchor = WORLD_LABEL_ANCHORS[iso];
|
|
2351
|
+
const cc = anchor ? project(anchor[0], anchor[1]) : path.centroid(viewF);
|
|
2352
|
+
const ok = cc != null && Number.isFinite(cc[0]) && Number.isFinite(cc[1]);
|
|
2353
|
+
geo = { d: d0, cx: ok ? cc[0] : void 0, cy: ok ? cc[1] : void 0 };
|
|
2354
|
+
regionGeo.set(geoK, geo);
|
|
2355
|
+
} else if (geo === null) {
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
const d = geo.d;
|
|
2359
|
+
const hasCentroid = geo.cx !== void 0 && geo.cy !== void 0;
|
|
2360
|
+
const isThisLayer = r?.layer === layerKind;
|
|
2361
|
+
const isForeign = layerKind === "country" && usContext && iso !== "US";
|
|
2362
|
+
const baseFill = isForeign ? foreignFill : neutralFill;
|
|
2363
|
+
let fill = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
|
|
2364
|
+
let label;
|
|
2365
|
+
let lineNumber = -1;
|
|
2366
|
+
let layer = "base";
|
|
2367
|
+
if (isThisLayer) {
|
|
2368
|
+
fill = regionFill(r);
|
|
2369
|
+
lineNumber = r.lineNumber;
|
|
2370
|
+
layer = layerKind;
|
|
2371
|
+
label = r.name;
|
|
1839
2372
|
} else {
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2373
|
+
label = f.properties?.name;
|
|
2374
|
+
}
|
|
2375
|
+
regions.push({
|
|
2376
|
+
id: iso,
|
|
2377
|
+
d,
|
|
2378
|
+
fill,
|
|
2379
|
+
stroke: colorizeActive ? colorizeStroke(fill) : regionStroke,
|
|
2380
|
+
lineNumber,
|
|
2381
|
+
layer,
|
|
2382
|
+
...label !== void 0 && { label },
|
|
2383
|
+
...hasCentroid && { labelX: geo.cx, labelY: geo.cy },
|
|
2384
|
+
...isThisLayer && r.value !== void 0 && { value: r.value },
|
|
2385
|
+
...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
pushRegionLayer(worldLayer, "country", !isGlobalView);
|
|
2390
|
+
if (usLayer) pushRegionLayer(usLayer, "us-state", !conusFit && !isGlobalView);
|
|
2391
|
+
const lakesTopo = usCrisp && data.naLakes ? data.naLakes : data.lakes;
|
|
2392
|
+
if (lakesTopo) {
|
|
2393
|
+
for (const [, f] of decodeLayer(lakesTopo)) {
|
|
2394
|
+
const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
|
|
2395
|
+
if (!viewF) continue;
|
|
2396
|
+
const d = path(viewF) ?? "";
|
|
2397
|
+
if (!d) continue;
|
|
2398
|
+
regions.push({
|
|
2399
|
+
id: "lake",
|
|
2400
|
+
d,
|
|
2401
|
+
fill: water,
|
|
2402
|
+
stroke: lakeStroke,
|
|
2403
|
+
lineNumber: -1,
|
|
2404
|
+
layer: "base"
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
const pointInRings = (px, py, rings) => {
|
|
2409
|
+
let inside = false;
|
|
2410
|
+
for (const ring of rings) {
|
|
2411
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
2412
|
+
const [xi, yi] = ring[i];
|
|
2413
|
+
const [xj, yj] = ring[j];
|
|
2414
|
+
if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
|
|
2415
|
+
inside = !inside;
|
|
1845
2416
|
}
|
|
1846
2417
|
}
|
|
1847
|
-
return
|
|
2418
|
+
return inside;
|
|
1848
2419
|
};
|
|
1849
|
-
const
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
if (
|
|
2420
|
+
const fillHitTargets = [...regions, ...insetRegions].map((r) => {
|
|
2421
|
+
const rings = parsePathRings(r.d);
|
|
2422
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2423
|
+
for (const ring of rings)
|
|
2424
|
+
for (const p of ring) {
|
|
2425
|
+
if (p[0] < minX) minX = p[0];
|
|
2426
|
+
if (p[0] > maxX) maxX = p[0];
|
|
2427
|
+
if (p[1] < minY) minY = p[1];
|
|
2428
|
+
if (p[1] > maxY) maxY = p[1];
|
|
1857
2429
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
2430
|
+
r.bbox = [minX, minY, maxX, maxY];
|
|
2431
|
+
r.rings = rings;
|
|
2432
|
+
return { fill: r.fill, rings, minX, minY, maxX, maxY };
|
|
2433
|
+
});
|
|
2434
|
+
const fillAt = (x, y) => {
|
|
2435
|
+
let hit = water;
|
|
2436
|
+
for (const t of fillHitTargets) {
|
|
2437
|
+
if (x < t.minX || x > t.maxX || y < t.minY || y > t.maxY) continue;
|
|
2438
|
+
if (pointInRings(x, y, t.rings)) hit = t.fill;
|
|
1860
2439
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2440
|
+
return hit;
|
|
2441
|
+
};
|
|
2442
|
+
const labelOnFill = (fill) => {
|
|
2443
|
+
const color = contrastRatio(fill, palette.textOnFillDark) >= contrastRatio(fill, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
|
|
2444
|
+
const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
|
|
2445
|
+
return {
|
|
2446
|
+
color,
|
|
2447
|
+
halo: contrastRatio(fill, color) < REGION_LABEL_HALO_RATIO,
|
|
2448
|
+
haloColor
|
|
2449
|
+
};
|
|
2450
|
+
};
|
|
2451
|
+
const reliefAllowed = resolved.directives.noRelief !== true;
|
|
2452
|
+
const relief = [];
|
|
2453
|
+
let reliefHatch = null;
|
|
2454
|
+
if (reliefAllowed && data.mountainRanges) {
|
|
2455
|
+
for (const [, f] of decodeLayer(data.mountainRanges)) {
|
|
2456
|
+
const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
|
|
2457
|
+
if (!viewF) continue;
|
|
2458
|
+
const area = path.area(viewF);
|
|
2459
|
+
if (!Number.isFinite(area) || area < RELIEF_MIN_AREA) continue;
|
|
2460
|
+
const box = path.bounds(viewF);
|
|
2461
|
+
if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
|
|
2462
|
+
continue;
|
|
2463
|
+
const d = path(viewF) ?? "";
|
|
2464
|
+
if (!d) continue;
|
|
2465
|
+
relief.push({ d });
|
|
1876
2466
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
2467
|
+
if (relief.length) {
|
|
2468
|
+
const darkTone = isDark ? palette.bg : palette.text;
|
|
2469
|
+
const lightTone = isDark ? palette.text : palette.bg;
|
|
2470
|
+
const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
|
|
2471
|
+
const landLum = relativeLuminance(reliefLandRef);
|
|
2472
|
+
const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
|
|
2473
|
+
reliefHatch = {
|
|
2474
|
+
color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
|
|
2475
|
+
spacing: RELIEF_HATCH_SPACING,
|
|
2476
|
+
width: RELIEF_HATCH_WIDTH
|
|
2477
|
+
};
|
|
1879
2478
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
const
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const
|
|
1903
|
-
if (
|
|
1904
|
-
|
|
1905
|
-
bestPen = pen;
|
|
1906
|
-
}
|
|
1907
|
-
if (bestPen === 0) break;
|
|
2479
|
+
}
|
|
2480
|
+
let coastlineStyle = null;
|
|
2481
|
+
if (resolved.directives.noCoastline !== true) {
|
|
2482
|
+
const minDim = Math.min(width, height);
|
|
2483
|
+
coastlineStyle = {
|
|
2484
|
+
color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
|
|
2485
|
+
// N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
|
|
2486
|
+
// fades linearly from NEAR (innermost) to FAR (outermost).
|
|
2487
|
+
lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
|
|
2488
|
+
d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
|
|
2489
|
+
thickness: COASTLINE_THICKNESS * minDim,
|
|
2490
|
+
opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
|
|
2491
|
+
})),
|
|
2492
|
+
minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
const riverColor = mix(palette.colors.blue, water, 32);
|
|
2496
|
+
const rivers = [];
|
|
2497
|
+
if (data.rivers) {
|
|
2498
|
+
for (const [, f] of decodeLayer(data.rivers)) {
|
|
2499
|
+
const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
|
|
2500
|
+
if (!viewF) continue;
|
|
2501
|
+
const d = path(viewF) ?? "";
|
|
2502
|
+
if (!d) continue;
|
|
2503
|
+
rivers.push({ d, color: riverColor, width: RIVER_WIDTH });
|
|
1908
2504
|
}
|
|
1909
|
-
|
|
2505
|
+
}
|
|
2506
|
+
const sizeVals = resolved.pois.map((p) => Number(p.meta["size"])).filter((n) => Number.isFinite(n) && n > 0);
|
|
2507
|
+
const sizeMin = sizeVals.length ? Math.min(...sizeVals) : 0;
|
|
2508
|
+
const sizeMax = sizeVals.length ? Math.max(...sizeVals) : 0;
|
|
2509
|
+
const radiusFor = (p) => {
|
|
2510
|
+
const v = Number(p.meta["size"]);
|
|
2511
|
+
if (!Number.isFinite(v) || v <= 0 || sizeMax <= 0) return R_DEFAULT;
|
|
2512
|
+
const t = sizeMax > sizeMin ? (Math.sqrt(v) - Math.sqrt(sizeMin)) / (Math.sqrt(sizeMax) - Math.sqrt(sizeMin)) : 1;
|
|
2513
|
+
return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
|
|
1910
2514
|
};
|
|
1911
|
-
const
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
const routeWidthFor = (v) => {
|
|
1915
|
-
if (!Number.isFinite(v) || v <= 0 || rlMax <= 0) return W_MIN;
|
|
1916
|
-
const t = rlMax > rlMin ? (v - rlMin) / (rlMax - rlMin) : 1;
|
|
1917
|
-
return W_MIN + t * (W_MAX - W_MIN);
|
|
2515
|
+
const fillOpacityFor = (r) => {
|
|
2516
|
+
const t = Math.max(0, Math.min(1, (r - R_MIN) / (R_MAX - R_MIN)));
|
|
2517
|
+
return POI_FILL_OPACITY_MAX - t * (POI_FILL_OPACITY_MAX - POI_FILL_OPACITY_MIN);
|
|
1918
2518
|
};
|
|
1919
|
-
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
const lp = legLabelPoint(a, b, curvedLeg, chosenBow, center);
|
|
1932
|
-
const bow = {
|
|
1933
|
-
curved: curvedLeg,
|
|
1934
|
-
offset: chosenBow,
|
|
1935
|
-
center,
|
|
1936
|
-
labelX: lp.x,
|
|
1937
|
-
labelY: lp.y
|
|
1938
|
-
};
|
|
1939
|
-
placedLegSamples.push({
|
|
1940
|
-
fromId: leg.fromId,
|
|
1941
|
-
toId: leg.toId,
|
|
1942
|
-
pts: sampleLeg(a, b, curvedLeg, chosenBow)
|
|
1943
|
-
});
|
|
1944
|
-
const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
|
|
1945
|
-
const routeVal = Number(leg.value);
|
|
1946
|
-
legs.push({
|
|
1947
|
-
d: legPath(a, b, bow.curved, bow.offset, bow.center),
|
|
1948
|
-
width: routeWidthFor(routeVal),
|
|
1949
|
-
color: lineColor(leg.tags) ?? mix(palette.text, palette.bg, 72),
|
|
1950
|
-
arrow: true,
|
|
1951
|
-
fromId: leg.fromId,
|
|
1952
|
-
toId: leg.toId,
|
|
1953
|
-
...Number.isFinite(routeVal) && routeVal > 0 && { value: routeVal },
|
|
1954
|
-
...Object.keys(leg.tags).length > 0 && { tags: leg.tags },
|
|
1955
|
-
lineNumber: leg.lineNumber,
|
|
1956
|
-
...leg.label !== void 0 && {
|
|
1957
|
-
label: leg.label,
|
|
1958
|
-
labelX: bow.labelX,
|
|
1959
|
-
labelY: bow.labelY,
|
|
1960
|
-
labelColor: routeLabelStyle.color,
|
|
1961
|
-
labelHalo: routeLabelStyle.halo,
|
|
1962
|
-
labelHaloColor: routeLabelStyle.haloColor
|
|
1963
|
-
}
|
|
1964
|
-
});
|
|
2519
|
+
const poiFill = (p) => {
|
|
2520
|
+
const directHex = p.color ? resolveColor(p.color, palette) : null;
|
|
2521
|
+
if (directHex)
|
|
2522
|
+
return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
|
|
2523
|
+
for (const group of resolved.tagGroups) {
|
|
2524
|
+
const val = p.tags[tagAttrKey(group.name)];
|
|
2525
|
+
if (!val) continue;
|
|
2526
|
+
const entry = group.entries.find(
|
|
2527
|
+
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
2528
|
+
);
|
|
2529
|
+
const hex = entry?.color;
|
|
2530
|
+
if (hex) return { fill: hex, stroke: mix(hex, palette.text, 18) };
|
|
1965
2531
|
}
|
|
2532
|
+
return {
|
|
2533
|
+
fill: palette.colors.orange,
|
|
2534
|
+
stroke: mix(palette.colors.orange, palette.text, 18)
|
|
2535
|
+
};
|
|
2536
|
+
};
|
|
2537
|
+
const routeNumberById = /* @__PURE__ */ new Map();
|
|
2538
|
+
const originIds = /* @__PURE__ */ new Set();
|
|
2539
|
+
for (const rt of resolved.routes) {
|
|
2540
|
+
rt.stopIds.forEach((id, i) => {
|
|
2541
|
+
if (i === 0) originIds.add(id);
|
|
2542
|
+
if (!routeNumberById.has(id)) routeNumberById.set(id, i + 1);
|
|
2543
|
+
});
|
|
1966
2544
|
}
|
|
1967
|
-
const
|
|
1968
|
-
const
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2545
|
+
const poiScreen = /* @__PURE__ */ new Map();
|
|
2546
|
+
const pois = [];
|
|
2547
|
+
const orderedPois = [...resolved.pois].sort(
|
|
2548
|
+
(a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
|
|
2549
|
+
);
|
|
2550
|
+
const projected = [];
|
|
2551
|
+
for (const p of orderedPois) {
|
|
2552
|
+
const xy = project(p.lon, p.lat);
|
|
2553
|
+
if (xy) projected.push({ p, xy });
|
|
2554
|
+
}
|
|
2555
|
+
const placePoi = (e, cx, cy, clusterId) => {
|
|
2556
|
+
const { fill, stroke } = poiFill(e.p);
|
|
2557
|
+
const r = radiusFor(e.p);
|
|
2558
|
+
poiScreen.set(e.p.id, { cx, cy, r });
|
|
2559
|
+
const num = routeNumberById.get(e.p.id);
|
|
2560
|
+
pois.push({
|
|
2561
|
+
id: e.p.id,
|
|
2562
|
+
cx,
|
|
2563
|
+
cy,
|
|
2564
|
+
r,
|
|
2565
|
+
fill,
|
|
2566
|
+
fillOpacity: fillOpacityFor(r),
|
|
2567
|
+
stroke,
|
|
2568
|
+
lineNumber: e.p.lineNumber,
|
|
2569
|
+
implicit: !!e.p.implicit,
|
|
2570
|
+
isOrigin: originIds.has(e.p.id),
|
|
2571
|
+
...num !== void 0 && { routeNumber: num },
|
|
2572
|
+
...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
|
|
2573
|
+
...clusterId !== void 0 && { clusterId }
|
|
2574
|
+
});
|
|
1975
2575
|
};
|
|
1976
|
-
const
|
|
2576
|
+
const clusters = [];
|
|
2577
|
+
const connected = /* @__PURE__ */ new Set();
|
|
1977
2578
|
for (const e of resolved.edges) {
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (arr) arr.push(e);
|
|
1981
|
-
else pairGroups.set(key, [e]);
|
|
2579
|
+
connected.add(e.fromId);
|
|
2580
|
+
connected.add(e.toId);
|
|
1982
2581
|
}
|
|
1983
|
-
for (const
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2582
|
+
for (const rt of resolved.routes) {
|
|
2583
|
+
rt.stopIds.forEach((id) => connected.add(id));
|
|
2584
|
+
}
|
|
2585
|
+
const radiusOf = (e) => radiusFor(e.p);
|
|
2586
|
+
for (const e of projected) {
|
|
2587
|
+
if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
|
|
2588
|
+
}
|
|
2589
|
+
const groups = [];
|
|
2590
|
+
for (const e of projected) {
|
|
2591
|
+
if (connected.has(e.p.id)) continue;
|
|
2592
|
+
const r = radiusOf(e);
|
|
2593
|
+
const near = groups.find(
|
|
2594
|
+
(g) => g.some(
|
|
2595
|
+
(q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
|
|
2596
|
+
)
|
|
2597
|
+
);
|
|
2598
|
+
if (near) near.push(e);
|
|
2599
|
+
else groups.push([e]);
|
|
2600
|
+
}
|
|
2601
|
+
for (const g of groups) {
|
|
2602
|
+
if (g.length === 1) {
|
|
2603
|
+
placePoi(g[0], g[0].xy[0], g[0].xy[1]);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
const clusterId = g[0].p.id;
|
|
2607
|
+
const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
|
|
2608
|
+
const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
|
|
2609
|
+
const maxR = Math.max(...g.map(radiusOf));
|
|
2610
|
+
const sep = 2 * maxR + STACK_RING_GAP;
|
|
2611
|
+
const ringR = Math.max(
|
|
2612
|
+
COLO_R,
|
|
2613
|
+
sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
|
|
2614
|
+
);
|
|
2615
|
+
const positions = g.map((e, i) => {
|
|
2616
|
+
if (g.length <= STACK_RING_MAX) {
|
|
2617
|
+
const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
|
|
2618
|
+
return {
|
|
2619
|
+
e,
|
|
2620
|
+
mx: cx0 + Math.cos(ang2) * ringR,
|
|
2621
|
+
my: cy0 + Math.sin(ang2) * ringR
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
const ang = i * GOLDEN_ANGLE;
|
|
2625
|
+
const rr = ringR * Math.sqrt((i + 1) / g.length);
|
|
2626
|
+
return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
|
|
2627
|
+
});
|
|
2628
|
+
let minX = cx0 - maxR;
|
|
2629
|
+
let maxX = cx0 + maxR;
|
|
2630
|
+
let minY = cy0 - maxR;
|
|
2631
|
+
let maxY = cy0 + maxR;
|
|
2632
|
+
for (const { mx, my, e } of positions) {
|
|
2633
|
+
const r = radiusOf(e);
|
|
2634
|
+
minX = Math.min(minX, mx - r);
|
|
2635
|
+
maxX = Math.max(maxX, mx + r);
|
|
2636
|
+
minY = Math.min(minY, my - r);
|
|
2637
|
+
maxY = Math.max(maxY, my + r);
|
|
2638
|
+
}
|
|
2639
|
+
let dx = 0;
|
|
2640
|
+
let dy = 0;
|
|
2641
|
+
if (minX + dx < 2) dx = 2 - minX;
|
|
2642
|
+
if (maxX + dx > width - 2) dx = width - 2 - maxX;
|
|
2643
|
+
if (minY + dy < 2) dy = 2 - minY;
|
|
2644
|
+
if (maxY + dy > height - 2) dy = height - 2 - maxY;
|
|
2645
|
+
const legsOut = [];
|
|
2646
|
+
for (const { e, mx, my } of positions) {
|
|
2647
|
+
const fx = mx + dx;
|
|
2648
|
+
const fy = my + dy;
|
|
2649
|
+
placePoi(e, fx, fy, clusterId);
|
|
2650
|
+
legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
|
|
2651
|
+
}
|
|
2652
|
+
clusters.push({
|
|
2653
|
+
id: clusterId,
|
|
2654
|
+
cx: cx0 + dx,
|
|
2655
|
+
cy: cy0 + dy,
|
|
2656
|
+
count: g.length,
|
|
2657
|
+
hitR: ringR + maxR + 6,
|
|
2658
|
+
legs: legsOut
|
|
2026
2659
|
});
|
|
2027
2660
|
}
|
|
2661
|
+
const legs = buildConnectorLegs({
|
|
2662
|
+
resolved,
|
|
2663
|
+
poiScreen,
|
|
2664
|
+
palette,
|
|
2665
|
+
width,
|
|
2666
|
+
height,
|
|
2667
|
+
fillAt,
|
|
2668
|
+
labelOnFill
|
|
2669
|
+
});
|
|
2028
2670
|
const labels = [];
|
|
2029
2671
|
const obstacles = [...topReserved];
|
|
2030
2672
|
const regionLabelGuards = [];
|
|
@@ -2069,36 +2711,8 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2069
2711
|
const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
|
|
2070
2712
|
const isCompact = width < COMPACT_WIDTH_PX;
|
|
2071
2713
|
const usChoroplethZoom = resolved.projection === "mercator" && resolved.basemaps.subdivisions.includes("us-states") && activeIsScore;
|
|
2072
|
-
const
|
|
2073
|
-
const
|
|
2074
|
-
const VALUE_GAP = 1;
|
|
2075
|
-
const labelW = (text, font = FONT2) => measureLegendText(text, font) + 2 * LABEL_PADX;
|
|
2076
|
-
const labelH = FONT2 + 2 * LABEL_PADY;
|
|
2077
|
-
const stackW = (text, valueText, font = FONT2) => Math.max(
|
|
2078
|
-
labelW(text, font),
|
|
2079
|
-
valueText ? measureLegendText(valueText, Math.round(font * 0.82)) + 2 * LABEL_PADX : 0
|
|
2080
|
-
);
|
|
2081
|
-
const stackH = (hasValue, font = FONT2) => {
|
|
2082
|
-
const lh = font + 2 * LABEL_PADY;
|
|
2083
|
-
return hasValue ? lh + VALUE_GAP + Math.round(font * 0.82) : lh;
|
|
2084
|
-
};
|
|
2085
|
-
const REGION_FONT_MAX_ORIENT = 22;
|
|
2086
|
-
const REGION_SIZE_FRAC_MIN = 0.06;
|
|
2087
|
-
const REGION_SIZE_FRAC_MAX = 0.32;
|
|
2088
|
-
const SUBJECT_FONT_MAX = 18;
|
|
2089
|
-
const SUBJECT_MIN_PROMINENCE = 13;
|
|
2090
|
-
const SUBJECT_LEADER_FONT = 15;
|
|
2091
|
-
const canvasLinear = Math.sqrt(Math.max(1, width * height));
|
|
2092
|
-
const sizeT = (boxW, boxH) => {
|
|
2093
|
-
const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
|
|
2094
|
-
return Math.min(
|
|
2095
|
-
1,
|
|
2096
|
-
Math.max(
|
|
2097
|
-
0,
|
|
2098
|
-
(frac - REGION_SIZE_FRAC_MIN) / (REGION_SIZE_FRAC_MAX - REGION_SIZE_FRAC_MIN)
|
|
2099
|
-
)
|
|
2100
|
-
);
|
|
2101
|
-
};
|
|
2714
|
+
const labelMetrics = createLabelMetrics(size);
|
|
2715
|
+
const { labelW, labelH } = labelMetrics;
|
|
2102
2716
|
const pushRegionLabel = (x, y, text, fill, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
|
|
2103
2717
|
const { color: baseColor, haloColor } = labelOnFill(fill);
|
|
2104
2718
|
const color = colorOverride ?? baseColor;
|
|
@@ -2138,373 +2752,39 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2138
2752
|
return { x: cx - w / 2, y: cy - h / 2, w, h };
|
|
2139
2753
|
};
|
|
2140
2754
|
if (showRegionLabels) {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
const CLEAR_TOL = CLEAR_STEP;
|
|
2175
|
-
const clearanceAt = (px, py, ox, oy) => {
|
|
2176
|
-
const baseAng = Math.atan2(oy, ox);
|
|
2177
|
-
let minClear = CLEAR_MAX;
|
|
2178
|
-
for (let k = -2; k <= 2; k++) {
|
|
2179
|
-
const a = baseAng + k * Math.PI / 4;
|
|
2180
|
-
const rx = Math.cos(a);
|
|
2181
|
-
const ry = Math.sin(a);
|
|
2182
|
-
let d = CLEAR_STEP;
|
|
2183
|
-
for (; d <= CLEAR_MAX; d += CLEAR_STEP) {
|
|
2184
|
-
const qx = px + rx * d;
|
|
2185
|
-
const qy = py + ry * d;
|
|
2186
|
-
if (qx < FIT_PAD || qy < topPad || qx > width - FIT_PAD || qy > height - FIT_PAD)
|
|
2187
|
-
break;
|
|
2188
|
-
if (fillAt(qx, qy) !== water) break;
|
|
2189
|
-
}
|
|
2190
|
-
if (d < minClear) minClear = d;
|
|
2191
|
-
}
|
|
2192
|
-
return minClear;
|
|
2193
|
-
};
|
|
2194
|
-
const tryShortHopCallout = (cx, cy, fill, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
|
|
2195
|
-
const chipW = stackW(name, value, font);
|
|
2196
|
-
const chipH = stackH(value !== void 0, font);
|
|
2197
|
-
const D = Math.SQRT1_2;
|
|
2198
|
-
const dirs = [
|
|
2199
|
-
[0, -1],
|
|
2200
|
-
// north
|
|
2201
|
-
[0, 1],
|
|
2202
|
-
// south
|
|
2203
|
-
[1, 0],
|
|
2204
|
-
// east
|
|
2205
|
-
[-1, 0],
|
|
2206
|
-
// west
|
|
2207
|
-
[D, -D],
|
|
2208
|
-
// NE
|
|
2209
|
-
[D, D],
|
|
2210
|
-
// SE
|
|
2211
|
-
[-D, -D],
|
|
2212
|
-
// NW
|
|
2213
|
-
[-D, D]
|
|
2214
|
-
// SW
|
|
2215
|
-
];
|
|
2216
|
-
let best = null;
|
|
2217
|
-
for (const [dx, dy] of dirs) {
|
|
2218
|
-
let ex = cx;
|
|
2219
|
-
let ey = cy;
|
|
2220
|
-
let steps = 0;
|
|
2221
|
-
const maxSteps = Math.ceil(maxLen / STEP) + 1;
|
|
2222
|
-
while (steps < maxSteps && fillAt(ex, ey) === fill) {
|
|
2223
|
-
ex += dx * STEP;
|
|
2224
|
-
ey += dy * STEP;
|
|
2225
|
-
steps++;
|
|
2226
|
-
}
|
|
2227
|
-
if (fillAt(ex, ey) === fill) continue;
|
|
2228
|
-
const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
|
|
2229
|
-
const ccx = ex + dx * (gap + halfAlong);
|
|
2230
|
-
const ccy = ey + dy * (gap + halfAlong);
|
|
2231
|
-
const rect = {
|
|
2232
|
-
x: ccx - chipW / 2,
|
|
2233
|
-
y: ccy - chipH / 2,
|
|
2234
|
-
w: chipW,
|
|
2235
|
-
h: chipH
|
|
2236
|
-
};
|
|
2237
|
-
if (rect.x < FIT_PAD || rect.y < topPad || rect.x + rect.w > width - FIT_PAD || rect.y + rect.h > height - FIT_PAD)
|
|
2238
|
-
continue;
|
|
2239
|
-
const sx = [rect.x + 2, ccx, rect.x + rect.w - 2];
|
|
2240
|
-
const sy = [rect.y + 2, ccy, rect.y + rect.h - 2];
|
|
2241
|
-
let onEmpty = true;
|
|
2242
|
-
for (const px of sx)
|
|
2243
|
-
for (const py of sy)
|
|
2244
|
-
if (!isEmptyFill(fillAt(px, py))) {
|
|
2245
|
-
onEmpty = false;
|
|
2246
|
-
break;
|
|
2247
|
-
}
|
|
2248
|
-
if (!onEmpty) continue;
|
|
2249
|
-
if (placedRegionRects.some((p) => rectsOverlap(rect, p))) continue;
|
|
2250
|
-
if (poiObstacles.some((o) => rectsOverlap(rect, o))) continue;
|
|
2251
|
-
if (markers.some((m) => rectCircleOverlap(rect, m))) continue;
|
|
2252
|
-
const innerX = ccx - dx * (chipW / 2);
|
|
2253
|
-
const innerY = ccy - dy * (chipH / 2);
|
|
2254
|
-
const len = Math.hypot(innerX - cx, innerY - cy);
|
|
2255
|
-
if (len > maxLen) continue;
|
|
2256
|
-
if (placedLeaders.some(
|
|
2257
|
-
(l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
|
|
2258
|
-
))
|
|
2259
|
-
continue;
|
|
2260
|
-
if (placedRegionRects.some(
|
|
2261
|
-
(p) => segmentRectOverlap(cx, cy, innerX, innerY, p)
|
|
2262
|
-
))
|
|
2263
|
-
continue;
|
|
2264
|
-
const clear = clearanceAt(ccx, ccy, dx, dy);
|
|
2265
|
-
const better = best === null || clear > best.clear + CLEAR_TOL || clear >= best.clear - CLEAR_TOL && len < best.len;
|
|
2266
|
-
if (better)
|
|
2267
|
-
best = {
|
|
2268
|
-
x: ccx,
|
|
2269
|
-
y: ccy,
|
|
2270
|
-
rect,
|
|
2271
|
-
leader: [cx, cy, innerX, innerY],
|
|
2272
|
-
len,
|
|
2273
|
-
clear
|
|
2274
|
-
};
|
|
2275
|
-
}
|
|
2276
|
-
return best ? { x: best.x, y: best.y, rect: best.rect, leader: best.leader } : null;
|
|
2277
|
-
};
|
|
2278
|
-
for (const { r, c, boxW, boxH, candidates } of entries) {
|
|
2279
|
-
const valStr = regionValueStr(r.value);
|
|
2280
|
-
const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
|
|
2281
|
-
const fitsPois = (rect) => !poiObstacles.some((o) => rectsOverlap(rect, o));
|
|
2282
|
-
const isSubject = regionById.has(r.id);
|
|
2283
|
-
const extremesOnFill = (ax, ay, text) => {
|
|
2284
|
-
const halfW = measureLegendText(text, FONT2) / 2;
|
|
2285
|
-
return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
|
|
2286
|
-
};
|
|
2287
|
-
if (isSubject && valStr === void 0 && r.label !== void 0) {
|
|
2288
|
-
const name = candidates[0];
|
|
2289
|
-
let best = -1;
|
|
2290
|
-
for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
|
|
2291
|
-
if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
|
|
2292
|
-
continue;
|
|
2293
|
-
const rect = regionLabelRect(c[0], c[1], name, void 0, f);
|
|
2294
|
-
if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
|
|
2295
|
-
const halfW = measureLegendText(name, f) / 2;
|
|
2296
|
-
if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
|
|
2297
|
-
continue;
|
|
2298
|
-
best = f;
|
|
2299
|
-
break;
|
|
2300
|
-
}
|
|
2301
|
-
if (best >= SUBJECT_MIN_PROMINENCE) {
|
|
2302
|
-
const rect = regionLabelRect(c[0], c[1], name, void 0, best);
|
|
2303
|
-
placedRegionRects.push(rect);
|
|
2304
|
-
pushRegionLabel(
|
|
2305
|
-
c[0],
|
|
2306
|
-
c[1],
|
|
2307
|
-
name,
|
|
2308
|
-
r.fill,
|
|
2309
|
-
r.lineNumber,
|
|
2310
|
-
void 0,
|
|
2311
|
-
best
|
|
2312
|
-
);
|
|
2313
|
-
labeledRegionIds.add(r.id);
|
|
2314
|
-
regionLabelGuards.push({ label: labels[labels.length - 1], rect });
|
|
2315
|
-
} else {
|
|
2316
|
-
const hop = tryShortHopCallout(
|
|
2317
|
-
c[0],
|
|
2318
|
-
c[1],
|
|
2319
|
-
r.fill,
|
|
2320
|
-
r.label,
|
|
2321
|
-
void 0,
|
|
2322
|
-
SUBJECT_LEADER_FONT,
|
|
2323
|
-
HOP_GAP + SUBJECT_LEADER_FONT,
|
|
2324
|
-
SHORT_HOP_MAX * 2.2
|
|
2325
|
-
);
|
|
2326
|
-
if (hop) {
|
|
2327
|
-
placedRegionRects.push(hop.rect);
|
|
2328
|
-
placedLeaders.push(hop.leader);
|
|
2329
|
-
const dark = mix(r.fill, palette.text, 60);
|
|
2330
|
-
labels.push({
|
|
2331
|
-
x: hop.x,
|
|
2332
|
-
y: hop.y,
|
|
2333
|
-
text: r.label,
|
|
2334
|
-
anchor: "middle",
|
|
2335
|
-
color: palette.text,
|
|
2336
|
-
halo: false,
|
|
2337
|
-
haloColor: palette.bg,
|
|
2338
|
-
fontSize: SUBJECT_LEADER_FONT,
|
|
2339
|
-
leader: {
|
|
2340
|
-
x1: hop.leader[0],
|
|
2341
|
-
y1: hop.leader[1],
|
|
2342
|
-
x2: hop.leader[2],
|
|
2343
|
-
y2: hop.leader[3]
|
|
2344
|
-
},
|
|
2345
|
-
leaderColor: dark,
|
|
2346
|
-
calloutDot: { x: c[0], y: c[1], color: dark },
|
|
2347
|
-
lineNumber: r.lineNumber
|
|
2348
|
-
});
|
|
2349
|
-
labeledRegionIds.add(r.id);
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
continue;
|
|
2353
|
-
}
|
|
2354
|
-
const seekAnchors = [
|
|
2355
|
-
{ x: c[0], y: c[1], guard: false }
|
|
2356
|
-
];
|
|
2357
|
-
if (valStr) {
|
|
2358
|
-
seekAnchors.push(
|
|
2359
|
-
{ x: c[0], y: c[1] + boxH * 0.26, guard: true },
|
|
2360
|
-
{ x: c[0], y: c[1] - boxH * 0.26, guard: true },
|
|
2361
|
-
{ x: c[0] + boxW * 0.26, y: c[1], guard: true },
|
|
2362
|
-
{ x: c[0] - boxW * 0.26, y: c[1], guard: true },
|
|
2363
|
-
{ x: c[0] + boxW * 0.22, y: c[1] + boxH * 0.22, guard: true },
|
|
2364
|
-
{ x: c[0] - boxW * 0.22, y: c[1] + boxH * 0.22, guard: true },
|
|
2365
|
-
{ x: c[0] + boxW * 0.22, y: c[1] - boxH * 0.22, guard: true },
|
|
2366
|
-
{ x: c[0] - boxW * 0.22, y: c[1] - boxH * 0.22, guard: true }
|
|
2367
|
-
);
|
|
2368
|
-
}
|
|
2369
|
-
let chosen;
|
|
2370
|
-
for (const a of seekAnchors) {
|
|
2371
|
-
if (a.guard && fillAt(a.x, a.y) !== r.fill) continue;
|
|
2372
|
-
for (const t of candidates) {
|
|
2373
|
-
const nameRect = regionLabelRect(a.x, a.y, t);
|
|
2374
|
-
if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
|
|
2375
|
-
const stackRect = regionLabelRect(a.x, a.y, t, valStr);
|
|
2376
|
-
if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
|
|
2377
|
-
chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
|
|
2378
|
-
break;
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
if (labelW(t) <= boxW && labelH <= boxH) {
|
|
2382
|
-
if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
|
|
2383
|
-
chosen = { text: t, ax: a.x, ay: a.y };
|
|
2384
|
-
break;
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
if (chosen) break;
|
|
2389
|
-
}
|
|
2390
|
-
if (chosen === void 0 && r.label !== void 0 && isSubject) {
|
|
2391
|
-
const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
|
|
2392
|
-
if (hop) {
|
|
2393
|
-
placedRegionRects.push(hop.rect);
|
|
2394
|
-
placedLeaders.push(hop.leader);
|
|
2395
|
-
const dark = mix(r.fill, palette.text, 60);
|
|
2396
|
-
labels.push({
|
|
2397
|
-
x: hop.x,
|
|
2398
|
-
y: hop.y,
|
|
2399
|
-
text: r.label,
|
|
2400
|
-
anchor: "middle",
|
|
2401
|
-
color: palette.text,
|
|
2402
|
-
halo: false,
|
|
2403
|
-
haloColor: palette.bg,
|
|
2404
|
-
...valStr !== void 0 && { valueLine: valStr },
|
|
2405
|
-
leader: {
|
|
2406
|
-
x1: hop.leader[0],
|
|
2407
|
-
y1: hop.leader[1],
|
|
2408
|
-
x2: hop.leader[2],
|
|
2409
|
-
y2: hop.leader[3]
|
|
2410
|
-
},
|
|
2411
|
-
leaderColor: dark,
|
|
2412
|
-
calloutDot: { x: c[0], y: c[1], color: dark },
|
|
2413
|
-
lineNumber: r.lineNumber
|
|
2414
|
-
});
|
|
2415
|
-
labeledRegionIds.add(r.id);
|
|
2416
|
-
}
|
|
2417
|
-
continue;
|
|
2418
|
-
}
|
|
2419
|
-
if (chosen === void 0) continue;
|
|
2420
|
-
const isOrient = r.value === void 0 && r.layer === "base";
|
|
2421
|
-
let font = FONT2;
|
|
2422
|
-
if (isOrient) {
|
|
2423
|
-
const growT = sizeT(boxW, boxH);
|
|
2424
|
-
const desiredFont = Math.round(
|
|
2425
|
-
FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
|
|
2426
|
-
);
|
|
2427
|
-
const hasVal = chosen.valueLine !== void 0;
|
|
2428
|
-
const extremesClean = (f) => {
|
|
2429
|
-
const halfW = measureLegendText(chosen.text, f) / 2;
|
|
2430
|
-
return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
|
|
2431
|
-
const fAt = fillAt(sx, chosen.ay);
|
|
2432
|
-
return fAt === r.fill || isEmptyFill(fAt);
|
|
2433
|
-
});
|
|
2434
|
-
};
|
|
2435
|
-
let fit = -1;
|
|
2436
|
-
for (let f = desiredFont; f >= FONT2; f--) {
|
|
2437
|
-
if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
|
|
2438
|
-
continue;
|
|
2439
|
-
const gRect = regionLabelRect(
|
|
2440
|
-
chosen.ax,
|
|
2441
|
-
chosen.ay,
|
|
2442
|
-
chosen.text,
|
|
2443
|
-
chosen.valueLine,
|
|
2444
|
-
f
|
|
2445
|
-
);
|
|
2446
|
-
const gName = regionLabelRect(
|
|
2447
|
-
chosen.ax,
|
|
2448
|
-
chosen.ay,
|
|
2449
|
-
chosen.text,
|
|
2450
|
-
void 0,
|
|
2451
|
-
f
|
|
2452
|
-
);
|
|
2453
|
-
if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
|
|
2454
|
-
continue;
|
|
2455
|
-
if (!extremesClean(f)) continue;
|
|
2456
|
-
fit = f;
|
|
2457
|
-
break;
|
|
2458
|
-
}
|
|
2459
|
-
if (fit < 0) {
|
|
2460
|
-
if (!frameContainers.has(r.id)) continue;
|
|
2461
|
-
font = FONT2;
|
|
2462
|
-
} else {
|
|
2463
|
-
font = fit;
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
const rRect = regionLabelRect(
|
|
2467
|
-
chosen.ax,
|
|
2468
|
-
chosen.ay,
|
|
2469
|
-
chosen.text,
|
|
2470
|
-
chosen.valueLine,
|
|
2471
|
-
font
|
|
2472
|
-
);
|
|
2473
|
-
placedRegionRects.push(rRect);
|
|
2474
|
-
pushRegionLabel(
|
|
2475
|
-
chosen.ax,
|
|
2476
|
-
chosen.ay,
|
|
2477
|
-
chosen.text,
|
|
2478
|
-
r.fill,
|
|
2479
|
-
r.lineNumber,
|
|
2480
|
-
chosen.valueLine,
|
|
2481
|
-
font,
|
|
2482
|
-
isOrient ? backdropLandTone : void 0
|
|
2483
|
-
);
|
|
2484
|
-
labeledRegionIds.add(r.id);
|
|
2485
|
-
regionLabelGuards.push({
|
|
2486
|
-
label: labels[labels.length - 1],
|
|
2487
|
-
rect: rRect
|
|
2488
|
-
});
|
|
2489
|
-
}
|
|
2490
|
-
for (const seed of insetLabelSeeds) {
|
|
2491
|
-
const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
|
|
2492
|
-
const src = regionById.get(seed.iso);
|
|
2493
|
-
const valStr = regionValueStr(src?.value);
|
|
2494
|
-
pushRegionLabel(
|
|
2495
|
-
seed.x,
|
|
2496
|
-
seed.y,
|
|
2497
|
-
text,
|
|
2498
|
-
src ? regionFill(src) : neutralFill,
|
|
2499
|
-
seed.lineNumber,
|
|
2500
|
-
valStr
|
|
2501
|
-
);
|
|
2502
|
-
labeledRegionIds.add(seed.iso);
|
|
2503
|
-
regionLabelGuards.push({
|
|
2504
|
-
label: labels[labels.length - 1],
|
|
2505
|
-
rect: regionLabelRect(seed.x, seed.y, text, valStr)
|
|
2506
|
-
});
|
|
2507
|
-
}
|
|
2755
|
+
placeRegionLabels({
|
|
2756
|
+
resolved,
|
|
2757
|
+
palette,
|
|
2758
|
+
width,
|
|
2759
|
+
height,
|
|
2760
|
+
path,
|
|
2761
|
+
project,
|
|
2762
|
+
worldLayer,
|
|
2763
|
+
usLayer,
|
|
2764
|
+
regionById,
|
|
2765
|
+
regions,
|
|
2766
|
+
pois,
|
|
2767
|
+
insetLabelSeeds,
|
|
2768
|
+
topReserved,
|
|
2769
|
+
topPad,
|
|
2770
|
+
markers,
|
|
2771
|
+
collides,
|
|
2772
|
+
neutralFill,
|
|
2773
|
+
water,
|
|
2774
|
+
foreignFill,
|
|
2775
|
+
backdropLandTone,
|
|
2776
|
+
regionFill,
|
|
2777
|
+
fillAt,
|
|
2778
|
+
regionValueStr,
|
|
2779
|
+
isCompact,
|
|
2780
|
+
usChoroplethZoom,
|
|
2781
|
+
metrics: labelMetrics,
|
|
2782
|
+
pushRegionLabel,
|
|
2783
|
+
regionLabelRect,
|
|
2784
|
+
labels,
|
|
2785
|
+
labeledRegionIds,
|
|
2786
|
+
regionLabelGuards
|
|
2787
|
+
});
|
|
2508
2788
|
}
|
|
2509
2789
|
if (resolved.directives.noPoiLabels !== true) {
|
|
2510
2790
|
const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
|
|
@@ -2518,7 +2798,7 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2518
2798
|
const text = labelText(p);
|
|
2519
2799
|
return { text, w: measureLegendText(text, FONT2) };
|
|
2520
2800
|
};
|
|
2521
|
-
const
|
|
2801
|
+
const GAP2 = 3;
|
|
2522
2802
|
const POI_EDGE_CLEAR = Math.max(
|
|
2523
2803
|
20,
|
|
2524
2804
|
Math.round(Math.min(width, height) * 0.03)
|
|
@@ -2533,10 +2813,10 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2533
2813
|
const inlineRect = (p, w, side) => {
|
|
2534
2814
|
switch (side) {
|
|
2535
2815
|
case "right":
|
|
2536
|
-
return { x: p.cx + p.r +
|
|
2816
|
+
return { x: p.cx + p.r + GAP2, y: p.cy - poiLabH / 2, w, h: poiLabH };
|
|
2537
2817
|
case "left":
|
|
2538
2818
|
return {
|
|
2539
|
-
x: p.cx - p.r -
|
|
2819
|
+
x: p.cx - p.r - GAP2 - w,
|
|
2540
2820
|
y: p.cy - poiLabH / 2,
|
|
2541
2821
|
w,
|
|
2542
2822
|
h: poiLabH
|
|
@@ -2544,28 +2824,28 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2544
2824
|
case "above":
|
|
2545
2825
|
return {
|
|
2546
2826
|
x: p.cx - w / 2,
|
|
2547
|
-
y: p.cy - p.r -
|
|
2827
|
+
y: p.cy - p.r - GAP2 - poiLabH,
|
|
2548
2828
|
w,
|
|
2549
2829
|
h: poiLabH
|
|
2550
2830
|
};
|
|
2551
2831
|
case "below":
|
|
2552
|
-
return { x: p.cx - w / 2, y: p.cy + p.r +
|
|
2832
|
+
return { x: p.cx - w / 2, y: p.cy + p.r + GAP2, w, h: poiLabH };
|
|
2553
2833
|
// Diagonal corners: offset both axes by (r+GAP)/√2 so the box's near
|
|
2554
2834
|
// corner clears the dot rim by the same gap as a cardinal side.
|
|
2555
2835
|
case "below-right": {
|
|
2556
|
-
const d = (p.r +
|
|
2836
|
+
const d = (p.r + GAP2) * 0.7071;
|
|
2557
2837
|
return { x: p.cx + d, y: p.cy + d, w, h: poiLabH };
|
|
2558
2838
|
}
|
|
2559
2839
|
case "below-left": {
|
|
2560
|
-
const d = (p.r +
|
|
2840
|
+
const d = (p.r + GAP2) * 0.7071;
|
|
2561
2841
|
return { x: p.cx - d - w, y: p.cy + d, w, h: poiLabH };
|
|
2562
2842
|
}
|
|
2563
2843
|
case "above-right": {
|
|
2564
|
-
const d = (p.r +
|
|
2844
|
+
const d = (p.r + GAP2) * 0.7071;
|
|
2565
2845
|
return { x: p.cx + d, y: p.cy - d - poiLabH, w, h: poiLabH };
|
|
2566
2846
|
}
|
|
2567
2847
|
case "above-left": {
|
|
2568
|
-
const d = (p.r +
|
|
2848
|
+
const d = (p.r + GAP2) * 0.7071;
|
|
2569
2849
|
return { x: p.cx - d - w, y: p.cy - d - poiLabH, w, h: poiLabH };
|
|
2570
2850
|
}
|
|
2571
2851
|
}
|
|
@@ -2692,10 +2972,10 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2692
2972
|
};
|
|
2693
2973
|
const pushHidden = (p) => {
|
|
2694
2974
|
const { text, w } = labelInfo(p);
|
|
2695
|
-
let x = p.cx + p.r +
|
|
2975
|
+
let x = p.cx + p.r + GAP2;
|
|
2696
2976
|
let anchor = "start";
|
|
2697
2977
|
if (x + w > width) {
|
|
2698
|
-
x = p.cx - p.r -
|
|
2978
|
+
x = p.cx - p.r - GAP2 - w;
|
|
2699
2979
|
anchor = "end";
|
|
2700
2980
|
}
|
|
2701
2981
|
const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
|
|
@@ -2892,15 +3172,15 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2892
3172
|
}
|
|
2893
3173
|
}
|
|
2894
3174
|
if (regionLabelGuards.length > 0) {
|
|
2895
|
-
const
|
|
3175
|
+
const PAD2 = 2;
|
|
2896
3176
|
const poiRects = labels.filter((l) => l.poiId !== void 0 && l.hidden !== true).map((l) => {
|
|
2897
3177
|
const w = measureLegendText(l.text, FONT2);
|
|
2898
3178
|
const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
|
|
2899
3179
|
return {
|
|
2900
|
-
x: x -
|
|
3180
|
+
x: x - PAD2,
|
|
2901
3181
|
y: l.y - FONT2,
|
|
2902
|
-
w: w + 2 *
|
|
2903
|
-
h: FONT2 * 1.4 + 2 *
|
|
3182
|
+
w: w + 2 * PAD2,
|
|
3183
|
+
h: FONT2 * 1.4 + 2 * PAD2
|
|
2904
3184
|
};
|
|
2905
3185
|
});
|
|
2906
3186
|
for (const g of regionLabelGuards) {
|
|
@@ -2910,79 +3190,21 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
2910
3190
|
}
|
|
2911
3191
|
}
|
|
2912
3192
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
const dx = b.cx - a.cx;
|
|
2929
|
-
const dy = b.cy - a.cy;
|
|
2930
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
2931
|
-
const nx = -dy / len;
|
|
2932
|
-
const ny = dx / len;
|
|
2933
|
-
const perp = labelH + 2;
|
|
2934
|
-
const stroke = lg.width ?? W_MIN;
|
|
2935
|
-
const clearW = stroke / 2 + labelH / 2 + 3;
|
|
2936
|
-
const offList = stroke >= 4 ? [clearW, -clearW, clearW + perp, -(clearW + perp), 0] : [0, perp, -perp, 2 * perp, -2 * perp];
|
|
2937
|
-
const pm = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
|
|
2938
|
-
lg.d
|
|
2939
|
-
);
|
|
2940
|
-
const pointAt = (t) => {
|
|
2941
|
-
if (pm?.[5] !== void 0) {
|
|
2942
|
-
const u = 1 - t;
|
|
2943
|
-
return [
|
|
2944
|
-
u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
|
|
2945
|
-
u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
|
|
2946
|
-
];
|
|
2947
|
-
}
|
|
2948
|
-
if (pm?.[3] !== void 0)
|
|
2949
|
-
return [
|
|
2950
|
-
+pm[1] + (+pm[3] - +pm[1]) * t,
|
|
2951
|
-
+pm[2] + (+pm[4] - +pm[2]) * t
|
|
2952
|
-
];
|
|
2953
|
-
return [a.cx + dx * t, a.cy + dy * t];
|
|
2954
|
-
};
|
|
2955
|
-
const candidates = [];
|
|
2956
|
-
for (const off of offList)
|
|
2957
|
-
for (const t of T_LIST) {
|
|
2958
|
-
const [bx, by] = pointAt(t);
|
|
2959
|
-
candidates.push([bx + nx * off, by + ny * off - 4]);
|
|
2960
|
-
}
|
|
2961
|
-
const clean = ([cx2, cy2]) => {
|
|
2962
|
-
const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
|
|
2963
|
-
if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
|
|
2964
|
-
return false;
|
|
2965
|
-
if (markers.some((m) => rectCircleOverlap(rect2, m))) return false;
|
|
2966
|
-
if (committedBoxes.some((o) => rectsOverlap(o, rect2))) return false;
|
|
2967
|
-
if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
|
|
2968
|
-
return true;
|
|
2969
|
-
};
|
|
2970
|
-
const mid = pointAt(0.5);
|
|
2971
|
-
const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
|
|
2972
|
-
const style = labelOnFill(fillAt(cx, cy));
|
|
2973
|
-
legs[i] = {
|
|
2974
|
-
...lg,
|
|
2975
|
-
labelX: cx,
|
|
2976
|
-
labelY: cy,
|
|
2977
|
-
labelColor: style.color,
|
|
2978
|
-
labelHalo: style.halo,
|
|
2979
|
-
labelHaloColor: style.haloColor
|
|
2980
|
-
};
|
|
2981
|
-
const rect = { x: cx - w / 2, y: cy - labelH / 2, w, h: labelH };
|
|
2982
|
-
placedEdge.push(rect);
|
|
2983
|
-
obstacles.push(rect);
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
3193
|
+
obstacles.push(
|
|
3194
|
+
...layoutEdgeLabels({
|
|
3195
|
+
legs,
|
|
3196
|
+
committed: labels,
|
|
3197
|
+
poiScreen,
|
|
3198
|
+
markers,
|
|
3199
|
+
labelW: (text) => labelW(text),
|
|
3200
|
+
labelH,
|
|
3201
|
+
minStroke: W_MIN,
|
|
3202
|
+
fillAt,
|
|
3203
|
+
labelOnFill,
|
|
3204
|
+
width,
|
|
3205
|
+
height
|
|
3206
|
+
})
|
|
3207
|
+
);
|
|
2986
3208
|
if (resolved.directives.noContextLabels !== true) {
|
|
2987
3209
|
for (const l of labels) {
|
|
2988
3210
|
if (l.hidden) continue;
|
|
@@ -3098,49 +3320,13 @@ function layoutMap(resolvedIn, data, size, opts) {
|
|
|
3098
3320
|
});
|
|
3099
3321
|
labels.push(...contextLabels);
|
|
3100
3322
|
}
|
|
3101
|
-
const cityDots =
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
const CITY_POP_MIN = 5e4;
|
|
3109
|
-
const CITY_POP_MAX = 15e6;
|
|
3110
|
-
const LOG_MIN = Math.log10(CITY_POP_MIN);
|
|
3111
|
-
const LOG_SPAN = Math.log10(CITY_POP_MAX) - LOG_MIN;
|
|
3112
|
-
const cityDotRadius = (pop) => {
|
|
3113
|
-
if (!(pop > CITY_POP_MIN)) return CITY_DOT_R_MIN;
|
|
3114
|
-
const t = Math.min(1, (Math.log10(pop) - LOG_MIN) / LOG_SPAN);
|
|
3115
|
-
return CITY_DOT_R_MIN + t * (CITY_DOT_R_MAX - CITY_DOT_R_MIN);
|
|
3116
|
-
};
|
|
3117
|
-
const placed = pois.map((p) => ({
|
|
3118
|
-
x: p.cx,
|
|
3119
|
-
y: p.cy
|
|
3120
|
-
}));
|
|
3121
|
-
const sorted = [...data.gazetteer.cities].sort((a, b) => b[3] - a[3]);
|
|
3122
|
-
for (const c of sorted) {
|
|
3123
|
-
if (cityDots.length >= CITY_DOT_CAP) break;
|
|
3124
|
-
const lat = c[0];
|
|
3125
|
-
const lon = c[1];
|
|
3126
|
-
const p = project(lon, lat);
|
|
3127
|
-
if (!p) continue;
|
|
3128
|
-
const [px, py] = p;
|
|
3129
|
-
if (px < 0 || px > width || py < 0 || py > height) continue;
|
|
3130
|
-
let tooClose = false;
|
|
3131
|
-
for (const q of placed) {
|
|
3132
|
-
const dx = q.x - px;
|
|
3133
|
-
const dy = q.y - py;
|
|
3134
|
-
if (dx * dx + dy * dy < SPACING_SQ) {
|
|
3135
|
-
tooClose = true;
|
|
3136
|
-
break;
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
if (tooClose) continue;
|
|
3140
|
-
placed.push({ x: px, y: py });
|
|
3141
|
-
cityDots.push({ cx: px, cy: py, r: cityDotRadius(c[3]) });
|
|
3142
|
-
}
|
|
3143
|
-
}
|
|
3323
|
+
const cityDots = resolved.directives.noCities !== true ? layoutCityDots({
|
|
3324
|
+
cities: data.gazetteer.cities,
|
|
3325
|
+
occupied: pois.map((p) => ({ x: p.cx, y: p.cy })),
|
|
3326
|
+
project,
|
|
3327
|
+
width,
|
|
3328
|
+
height
|
|
3329
|
+
}) : [];
|
|
3144
3330
|
return {
|
|
3145
3331
|
width,
|
|
3146
3332
|
height,
|
|
@@ -3177,4 +3363,4 @@ export {
|
|
|
3177
3363
|
albersSkewFallback,
|
|
3178
3364
|
layoutMap
|
|
3179
3365
|
};
|
|
3180
|
-
//# sourceMappingURL=chunk-
|
|
3366
|
+
//# sourceMappingURL=chunk-37EHGTQ2.js.map
|