@diagrammo/dgmo 0.20.3 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/advanced.cjs +867 -286
  2. package/dist/advanced.js +866 -286
  3. package/dist/auto.cjs +635 -284
  4. package/dist/auto.js +113 -113
  5. package/dist/auto.mjs +635 -284
  6. package/dist/cli.cjs +156 -156
  7. package/dist/editor.cjs +6 -2
  8. package/dist/editor.js +6 -2
  9. package/dist/highlight.cjs +6 -2
  10. package/dist/highlight.js +6 -2
  11. package/dist/index.cjs +628 -281
  12. package/dist/index.js +628 -281
  13. package/dist/internal.cjs +867 -286
  14. package/dist/internal.js +866 -286
  15. package/dist/map-data/PROVENANCE.json +1 -1
  16. package/dist/map-data/mountain-ranges.json +1 -0
  17. package/docs/language-reference.md +27 -25
  18. package/gallery/fixtures/map-choropleth.dgmo +7 -7
  19. package/gallery/fixtures/map-direct-color.dgmo +10 -0
  20. package/gallery/fixtures/map-pois.dgmo +4 -4
  21. package/gallery/fixtures/map-region-scope.dgmo +8 -8
  22. package/gallery/fixtures/map-route.dgmo +5 -6
  23. package/package.json +1 -1
  24. package/src/advanced.ts +14 -0
  25. package/src/completion.ts +10 -4
  26. package/src/d3.ts +15 -9
  27. package/src/editor/keywords.ts +6 -2
  28. package/src/map/data/PROVENANCE.json +1 -1
  29. package/src/map/data/mountain-ranges.json +1 -0
  30. package/src/map/geo-query.ts +277 -0
  31. package/src/map/geo.ts +258 -1
  32. package/src/map/invert.ts +111 -0
  33. package/src/map/layout.ts +333 -139
  34. package/src/map/load-data.ts +7 -1
  35. package/src/map/parser.ts +142 -33
  36. package/src/map/renderer.ts +57 -6
  37. package/src/map/resolved-types.ts +21 -2
  38. package/src/map/resolver.ts +219 -53
  39. package/src/map/types.ts +57 -14
  40. package/src/utils/reserved-key-registry.ts +7 -7
  41. package/dist/advanced.d.cts +0 -5290
  42. package/dist/advanced.d.ts +0 -5290
  43. package/dist/auto.d.cts +0 -39
  44. package/dist/auto.d.ts +0 -39
  45. package/dist/index.d.cts +0 -336
  46. package/dist/index.d.ts +0 -336
  47. package/dist/internal.d.cts +0 -5290
  48. package/dist/internal.d.ts +0 -5290
package/dist/internal.cjs CHANGED
@@ -857,13 +857,9 @@ var init_reserved_key_registry = __esm({
857
857
  "icon"
858
858
  ]);
859
859
  MAP_REGISTRY = staticRegistry([
860
- "score",
860
+ "value",
861
861
  "label",
862
- "size",
863
- "description",
864
- "weight",
865
- "style",
866
- "date"
862
+ "style"
867
863
  ]);
868
864
  ORG_REGISTRY = staticRegistry([
869
865
  "color",
@@ -15828,7 +15824,8 @@ function parseMap(content) {
15828
15824
  continue;
15829
15825
  }
15830
15826
  if (open.route && indent > open.route.indent) {
15831
- open.route.route.stops.push(parseStop(trimmed, lineNumber));
15827
+ const leg = parseLeg(trimmed, lineNumber, open.route.route.style);
15828
+ open.route.route.legs.push(leg);
15832
15829
  continue;
15833
15830
  }
15834
15831
  if (open.poi && indent > open.poi.indent) {
@@ -15859,6 +15856,10 @@ function parseMap(content) {
15859
15856
  handleTag(trimmed, lineNumber);
15860
15857
  continue;
15861
15858
  }
15859
+ if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15860
+ handleDirective(firstWord, "", lineNumber);
15861
+ continue;
15862
+ }
15862
15863
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15863
15864
  handleDirective(
15864
15865
  firstWord,
@@ -15923,13 +15924,20 @@ function parseMap(content) {
15923
15924
  );
15924
15925
  d.projection = value;
15925
15926
  break;
15926
- case "metric":
15927
- dup(d.metric);
15928
- d.metric = value;
15927
+ case "region-metric": {
15928
+ dup(d.regionMetric);
15929
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
15930
+ d.regionMetric = rmLabel;
15931
+ if (rmColor) d.regionMetricColor = rmColor;
15932
+ break;
15933
+ }
15934
+ case "poi-metric":
15935
+ dup(d.poiMetric);
15936
+ d.poiMetric = value;
15929
15937
  break;
15930
- case "size-metric":
15931
- dup(d.sizeMetric);
15932
- d.sizeMetric = value;
15938
+ case "flow-metric":
15939
+ dup(d.flowMetric);
15940
+ d.flowMetric = value;
15933
15941
  break;
15934
15942
  case "scale":
15935
15943
  dup(d.scale);
@@ -15971,6 +15979,21 @@ function parseMap(content) {
15971
15979
  case "no-legend":
15972
15980
  d.noLegend = true;
15973
15981
  break;
15982
+ case "no-insets":
15983
+ d.noInsets = true;
15984
+ break;
15985
+ case "relief":
15986
+ d.relief = true;
15987
+ break;
15988
+ case "muted":
15989
+ case "natural":
15990
+ if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
15991
+ pushWarning(
15992
+ line12,
15993
+ `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
15994
+ );
15995
+ d.basemapStyle = key;
15996
+ break;
15974
15997
  case "subtitle":
15975
15998
  dup(d.subtitle);
15976
15999
  d.subtitle = value;
@@ -16048,14 +16071,14 @@ function parseMap(content) {
16048
16071
  line12
16049
16072
  );
16050
16073
  const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
16051
- let scoreNum;
16052
- const score = meta["score"];
16053
- if (score !== void 0) {
16054
- delete meta["score"];
16055
- scoreNum = Number(score);
16056
- if (!Number.isFinite(scoreNum)) {
16057
- pushError(line12, `score must be a number (got "${score}").`);
16058
- scoreNum = void 0;
16074
+ let valueNum;
16075
+ const value = meta["value"];
16076
+ if (value !== void 0) {
16077
+ delete meta["value"];
16078
+ valueNum = Number(value);
16079
+ if (!Number.isFinite(valueNum)) {
16080
+ pushError(line12, `value must be a number (got "${value}").`);
16081
+ valueNum = void 0;
16059
16082
  }
16060
16083
  }
16061
16084
  let regionName = split.name;
@@ -16073,7 +16096,8 @@ function parseMap(content) {
16073
16096
  lineNumber: line12
16074
16097
  };
16075
16098
  if (regionScope !== void 0) region.scope = regionScope;
16076
- if (scoreNum !== void 0) region.score = scoreNum;
16099
+ if (valueNum !== void 0) region.value = valueNum;
16100
+ if (split.color) region.color = split.color;
16077
16101
  regions.push(region);
16078
16102
  }
16079
16103
  function handlePoi(rest, line12, indent) {
@@ -16098,28 +16122,81 @@ function parseMap(content) {
16098
16122
  const poi = { pos, tags, meta, lineNumber: line12 };
16099
16123
  if (split.alias) poi.alias = split.alias;
16100
16124
  if (label !== void 0) poi.label = label;
16125
+ if (split.color) poi.color = split.color;
16101
16126
  pois.push(poi);
16102
16127
  open.poi = { poi, indent };
16103
16128
  }
16104
16129
  function handleRoute(rest, line12, indent) {
16105
- const meta = rest ? splitNameAndMeta(rest, registry(), aliasMap).meta : {};
16106
- const route = { stops: [], meta, lineNumber: line12 };
16130
+ const split = rest ? splitNameAndMeta(
16131
+ rest,
16132
+ registry(),
16133
+ aliasMap,
16134
+ void 0,
16135
+ diagnostics,
16136
+ line12
16137
+ ) : { name: "", meta: {}, alias: void 0 };
16138
+ const pos = parsePos(split.name, line12);
16139
+ if (!pos || pos.kind === "name" && !pos.name) {
16140
+ pushError(
16141
+ line12,
16142
+ "route requires an origin: `route <origin> [style: arc]`."
16143
+ );
16144
+ return;
16145
+ }
16146
+ const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
16147
+ const originLabel = meta["label"];
16148
+ const originValue = meta["value"];
16149
+ const style = meta["style"] === "arc" ? "arc" : "straight";
16150
+ const route = {
16151
+ origin: pos,
16152
+ ...split.alias !== void 0 && { originAlias: split.alias },
16153
+ ...originLabel !== void 0 && { originLabel },
16154
+ ...originValue !== void 0 && { originValue },
16155
+ originTags: tags,
16156
+ style,
16157
+ legs: [],
16158
+ lineNumber: line12
16159
+ };
16107
16160
  routes.push(route);
16108
16161
  open.route = { route, indent };
16109
16162
  }
16110
- function parseStop(trimmed, line12) {
16111
- const split = splitNameAndMeta(trimmed, registry(), aliasMap);
16112
- const ref = parsePos(split.name, line12) ?? {
16163
+ function parseLeg(trimmed, line12, headerStyle) {
16164
+ let arrowStyle = "straight";
16165
+ let label;
16166
+ let rest = trimmed;
16167
+ const m = trimmed.match(LEG_ARROW_RE);
16168
+ if (m) {
16169
+ const arr = classifyArrow(m[1], line12);
16170
+ arrowStyle = arr.style;
16171
+ label = arr.label;
16172
+ rest = m[2];
16173
+ }
16174
+ const split = splitNameAndMeta(
16175
+ rest,
16176
+ registry(),
16177
+ aliasMap,
16178
+ void 0,
16179
+ diagnostics,
16180
+ line12
16181
+ );
16182
+ const pos = parsePos(split.name, line12) ?? {
16113
16183
  kind: "name",
16114
16184
  name: split.name
16115
16185
  };
16116
- const stop = {
16117
- ref,
16118
- meta: split.meta,
16186
+ const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
16187
+ const value = meta["value"];
16188
+ const destLabel = meta["label"];
16189
+ const style = arrowStyle === "arc" || headerStyle === "arc" ? "arc" : "straight";
16190
+ return {
16191
+ ...label !== void 0 && { label },
16192
+ style,
16193
+ ...value !== void 0 && { value },
16194
+ dest: pos,
16195
+ ...split.alias !== void 0 && { destAlias: split.alias },
16196
+ ...destLabel !== void 0 && { destLabel },
16197
+ destTags: tags,
16119
16198
  lineNumber: line12
16120
16199
  };
16121
- if (split.alias) stop.alias = split.alias;
16122
- return stop;
16123
16200
  }
16124
16201
  function handleEdges(trimmed, line12) {
16125
16202
  const parts = trimmed.split(ARROW_SPLIT);
@@ -16221,7 +16298,7 @@ function partitionMeta(meta, tagGroupNames) {
16221
16298
  function poiName(pos) {
16222
16299
  return pos.kind === "name" ? pos.name : void 0;
16223
16300
  }
16224
- var COORD_RE, NUMERIC_LEAD_RE, SCOPE_RE, ARROW_SPLIT, HUB_RE, AT_RE, DIRECTIVE_SET;
16301
+ var COORD_RE, NUMERIC_LEAD_RE, SCOPE_RE, ARROW_SPLIT, HUB_RE, LEG_ARROW_RE, AT_RE, DIRECTIVE_SET;
16225
16302
  var init_parser12 = __esm({
16226
16303
  "src/map/parser.ts"() {
16227
16304
  "use strict";
@@ -16235,12 +16312,14 @@ var init_parser12 = __esm({
16235
16312
  SCOPE_RE = /^[A-Z]{2}(?:-[A-Z0-9]{1,3})?$/;
16236
16313
  ARROW_SPLIT = /\s+(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+/;
16237
16314
  HUB_RE = /^(->|~>)\s+(.+)$/;
16315
+ LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16238
16316
  AT_RE = /(^|[\s,])at\s*:/i;
16239
16317
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16240
16318
  "region",
16241
16319
  "projection",
16242
- "metric",
16243
- "size-metric",
16320
+ "region-metric",
16321
+ "poi-metric",
16322
+ "flow-metric",
16244
16323
  "scale",
16245
16324
  "region-labels",
16246
16325
  "poi-labels",
@@ -16248,6 +16327,8 @@ var init_parser12 = __esm({
16248
16327
  "default-state",
16249
16328
  "active-tag",
16250
16329
  "no-legend",
16330
+ "no-insets",
16331
+ "relief",
16251
16332
  "subtitle",
16252
16333
  "caption"
16253
16334
  ]);
@@ -45774,6 +45855,84 @@ function featureIndex(topo) {
45774
45855
  }
45775
45856
  return idx;
45776
45857
  }
45858
+ function decodeFeatures(topo) {
45859
+ return geomObject(topo).geometries.map((g) => {
45860
+ const f = (0, import_topojson_client.feature)(topo, g);
45861
+ return {
45862
+ type: "Feature",
45863
+ id: g.id,
45864
+ properties: g.properties,
45865
+ geometry: f.geometry
45866
+ };
45867
+ });
45868
+ }
45869
+ function pointInRing(lon, lat, ring) {
45870
+ let inside = false;
45871
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45872
+ const xi = ring[i][0];
45873
+ const yi = ring[i][1];
45874
+ const xj = ring[j][0];
45875
+ const yj = ring[j][1];
45876
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
45877
+ if (intersect) inside = !inside;
45878
+ }
45879
+ return inside;
45880
+ }
45881
+ function pointOnRingEdge(lon, lat, ring) {
45882
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45883
+ const xi = ring[i][0];
45884
+ const yi = ring[i][1];
45885
+ const xj = ring[j][0];
45886
+ const yj = ring[j][1];
45887
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
45888
+ continue;
45889
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
45890
+ continue;
45891
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
45892
+ if (Math.abs(cross) <= EDGE_EPS) return true;
45893
+ }
45894
+ return false;
45895
+ }
45896
+ function pointInGeometry(geometry, lon, lat) {
45897
+ const g = geometry;
45898
+ if (!g) return false;
45899
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
45900
+ for (const rings of polys) {
45901
+ if (!rings.length) continue;
45902
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
45903
+ if (!pointInRing(lon, lat, rings[0])) continue;
45904
+ let inHole = false;
45905
+ for (let h = 1; h < rings.length; h++) {
45906
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
45907
+ inHole = true;
45908
+ break;
45909
+ }
45910
+ }
45911
+ if (!inHole) return true;
45912
+ }
45913
+ return false;
45914
+ }
45915
+ function regionAt(lonLat, countries, states) {
45916
+ const lon = lonLat[0];
45917
+ const lat = lonLat[1];
45918
+ let country = null;
45919
+ for (const f of countries) {
45920
+ if (pointInGeometry(f.geometry, lon, lat)) {
45921
+ country = { iso: f.id, name: f.properties.name };
45922
+ break;
45923
+ }
45924
+ }
45925
+ let state = null;
45926
+ if (country?.iso === "US" && states) {
45927
+ for (const f of states) {
45928
+ if (pointInGeometry(f.geometry, lon, lat)) {
45929
+ state = { iso: f.id, name: f.properties.name };
45930
+ break;
45931
+ }
45932
+ }
45933
+ }
45934
+ return { country, state };
45935
+ }
45777
45936
  function featureBbox(topo, geomId) {
45778
45937
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45779
45938
  if (!geom) return null;
@@ -45785,6 +45944,74 @@ function featureBbox(topo, geomId) {
45785
45944
  [b[1][0], b[1][1]]
45786
45945
  ];
45787
45946
  }
45947
+ function explodePolygons(gj) {
45948
+ const g = gj.geometry ?? gj;
45949
+ const t = g.type;
45950
+ const coords = g.coordinates;
45951
+ if (t === "Polygon") {
45952
+ return [
45953
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
45954
+ ];
45955
+ }
45956
+ if (t === "MultiPolygon") {
45957
+ return coords.map((rings) => ({
45958
+ type: "Feature",
45959
+ geometry: { type: "Polygon", coordinates: rings }
45960
+ }));
45961
+ }
45962
+ return [];
45963
+ }
45964
+ function bboxGap(a, b) {
45965
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
45966
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
45967
+ return Math.max(lonGap, latGap);
45968
+ }
45969
+ function featureBboxPrimary(topo, geomId) {
45970
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45971
+ if (!geom) return null;
45972
+ const gj = (0, import_topojson_client.feature)(topo, geom);
45973
+ const parts = explodePolygons(gj);
45974
+ if (parts.length <= 1) return featureBbox(topo, geomId);
45975
+ const polys = parts.map((p) => {
45976
+ const b = (0, import_d3_geo.geoBounds)(p);
45977
+ if (!b || !Number.isFinite(b[0][0])) return null;
45978
+ const wraps = b[1][0] < b[0][0];
45979
+ const bbox = [
45980
+ [b[0][0], b[0][1]],
45981
+ [b[1][0], b[1][1]]
45982
+ ];
45983
+ return { bbox, area: (0, import_d3_geo.geoArea)(p), wraps };
45984
+ }).filter(
45985
+ (p) => p !== null
45986
+ );
45987
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
45988
+ return featureBbox(topo, geomId);
45989
+ const maxArea = Math.max(...polys.map((p) => p.area));
45990
+ const anchor = polys.find((p) => p.area === maxArea);
45991
+ const cluster = [
45992
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
45993
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
45994
+ ];
45995
+ const remaining = polys.filter((p) => p !== anchor);
45996
+ let added = true;
45997
+ while (added) {
45998
+ added = false;
45999
+ for (let i = remaining.length - 1; i >= 0; i--) {
46000
+ const p = remaining[i];
46001
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46002
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46003
+ if (near || large) {
46004
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46005
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46006
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46007
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46008
+ remaining.splice(i, 1);
46009
+ added = true;
46010
+ }
46011
+ }
46012
+ }
46013
+ return cluster;
46014
+ }
45788
46015
  function unionExtent(boxes, points) {
45789
46016
  const lats = [];
45790
46017
  const lons = [];
@@ -45823,13 +46050,16 @@ function unionLongitudes(lons) {
45823
46050
  }
45824
46051
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45825
46052
  }
45826
- var import_topojson_client, import_d3_geo, fold;
46053
+ var import_topojson_client, import_d3_geo, fold, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45827
46054
  var init_geo = __esm({
45828
46055
  "src/map/geo.ts"() {
45829
46056
  "use strict";
45830
46057
  import_topojson_client = require("topojson-client");
45831
46058
  import_d3_geo = require("d3-geo");
45832
46059
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46060
+ EDGE_EPS = 1e-9;
46061
+ DETACH_GAP_DEG = 10;
46062
+ DETACH_AREA_FRAC = 0.25;
45833
46063
  }
45834
46064
  });
45835
46065
 
@@ -45838,6 +46068,11 @@ var resolver_exports = {};
45838
46068
  __export(resolver_exports, {
45839
46069
  resolveMap: () => resolveMap
45840
46070
  });
46071
+ function usStateFromBareScope(scope) {
46072
+ if (!scope) return null;
46073
+ const up = scope.toUpperCase();
46074
+ return US_STATE_POSTAL.has(up) ? `US-${up}` : null;
46075
+ }
45841
46076
  function looksUS(lat, lon) {
45842
46077
  if (lat < 15 || lat > 72) return false;
45843
46078
  return lon >= -180 && lon <= -64 || lon >= 172;
@@ -45887,9 +46122,9 @@ function resolveMap(parsed, data) {
45887
46122
  const f = fold(r.name);
45888
46123
  return usStateIndex.has(f) && !countryIndex.has(f);
45889
46124
  }) || parsed.regions.some(
45890
- (r) => r.scope === "US" || r.scope?.startsWith("US-")
46125
+ (r) => r.scope === "US" || r.scope?.startsWith("US-") || usStateFromBareScope(r.scope) !== null
45891
46126
  ) || parsed.pois.some(
45892
- (p) => p.pos.kind === "name" && p.pos.scope?.startsWith("US-")
46127
+ (p) => p.pos.kind === "name" && (p.pos.scope?.startsWith("US-") || usStateFromBareScope(p.pos.scope) !== null)
45893
46128
  );
45894
46129
  const regions = [];
45895
46130
  const seenRegion = /* @__PURE__ */ new Map();
@@ -45928,12 +46163,12 @@ function resolveMap(parsed, data) {
45928
46163
  chosen = { ...inState, layer: "us-state" };
45929
46164
  } else {
45930
46165
  chosen = { ...inCountry, layer: "country" };
46166
+ warn(
46167
+ r.lineNumber,
46168
+ `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
46169
+ "W_MAP_REGION_AMBIGUOUS"
46170
+ );
45931
46171
  }
45932
- warn(
45933
- r.lineNumber,
45934
- `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
45935
- "W_MAP_REGION_AMBIGUOUS"
45936
- );
45937
46172
  } else if (inState) {
45938
46173
  chosen = { ...inState, layer: "us-state" };
45939
46174
  } else if (inCountry) {
@@ -45956,7 +46191,8 @@ function resolveMap(parsed, data) {
45956
46191
  iso: chosen.id,
45957
46192
  name: chosen.name,
45958
46193
  layer: chosen.layer,
45959
- ...r.score !== void 0 && { score: r.score },
46194
+ ...r.value !== void 0 && { value: r.value },
46195
+ ...r.color !== void 0 && { color: r.color },
45960
46196
  tags: r.tags,
45961
46197
  meta: r.meta,
45962
46198
  lineNumber: r.lineNumber
@@ -46014,9 +46250,10 @@ function resolveMap(parsed, data) {
46014
46250
  let cands = idxs.map((i) => data.gazetteer.cities[i]);
46015
46251
  const scopeUse = scope ?? scopeHint;
46016
46252
  if (scopeUse) {
46017
- const isSub = /^[A-Za-z]{2}-/.test(scopeUse);
46253
+ const bareState = usStateFromBareScope(scopeUse);
46254
+ const subScope = /^[A-Za-z]{2}-/.test(scopeUse) ? scopeUse : bareState;
46018
46255
  const filtered = cands.filter(
46019
- (c2) => isSub ? c2[5] === scopeUse : c2[2] === scopeUse
46256
+ (c2) => subScope ? c2[5] === subScope : c2[2] === scopeUse
46020
46257
  );
46021
46258
  if (filtered.length) cands = filtered;
46022
46259
  else if (scope) {
@@ -46097,6 +46334,7 @@ function resolveMap(parsed, data) {
46097
46334
  lat,
46098
46335
  lon,
46099
46336
  ...p.label !== void 0 && { label: p.label },
46337
+ ...p.color !== void 0 && { color: p.color },
46100
46338
  tags: p.tags,
46101
46339
  meta: p.meta,
46102
46340
  lineNumber: p.lineNumber
@@ -46145,33 +46383,89 @@ function resolveMap(parsed, data) {
46145
46383
  lineNumber: e.lineNumber
46146
46384
  });
46147
46385
  }
46148
- const routes = [];
46149
- for (const rt of parsed.routes) {
46150
- const stopIds = [];
46151
- for (const stop of rt.stops) {
46152
- let id;
46153
- if (stop.ref.kind === "coords") {
46154
- id = stop.alias ? fold(stop.alias) : `@${stop.ref.lat},${stop.ref.lon}`;
46155
- if (!looksUS(stop.ref.lat, stop.ref.lon)) anyNonUsPoi = true;
46156
- if (!registry.has(id)) {
46157
- const poi = {
46386
+ const resolveStop = (pos, alias, label, tags, sizeValue, line12) => {
46387
+ const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46388
+ if (pos.kind === "coords") {
46389
+ const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46390
+ if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46391
+ if (!registry.has(id)) {
46392
+ registerPoi(
46393
+ id,
46394
+ {
46158
46395
  id,
46159
- ...stop.alias !== void 0 && { name: stop.alias },
46160
- lat: stop.ref.lat,
46161
- lon: stop.ref.lon,
46162
- tags: {},
46163
- meta: stop.meta,
46164
- lineNumber: stop.lineNumber,
46165
- implicit: true
46166
- };
46167
- registerPoi(id, poi, stop.lineNumber);
46168
- }
46169
- } else {
46170
- id = stop.alias && registry.has(fold(stop.alias)) ? fold(stop.alias) : resolveEndpoint2(stop.ref.name, stop.lineNumber);
46396
+ ...alias !== void 0 && { name: alias },
46397
+ lat: pos.lat,
46398
+ lon: pos.lon,
46399
+ ...label !== void 0 && { label },
46400
+ tags,
46401
+ meta,
46402
+ lineNumber: line12
46403
+ },
46404
+ line12
46405
+ );
46171
46406
  }
46172
- if (id) stopIds.push(id);
46407
+ return id;
46173
46408
  }
46174
- routes.push({ stopIds, meta: rt.meta, lineNumber: rt.lineNumber });
46409
+ const f = fold(pos.name);
46410
+ if (registry.has(f)) return f;
46411
+ const aliased = declaredByName.get(f);
46412
+ if (aliased) return aliased;
46413
+ const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46414
+ if (got.kind !== "ok") return null;
46415
+ noteCountry(got.iso);
46416
+ registerPoi(
46417
+ f,
46418
+ {
46419
+ id: f,
46420
+ name: pos.name,
46421
+ lat: got.lat,
46422
+ lon: got.lon,
46423
+ ...label !== void 0 && { label },
46424
+ tags,
46425
+ meta,
46426
+ lineNumber: line12
46427
+ },
46428
+ line12
46429
+ );
46430
+ return f;
46431
+ };
46432
+ const routes = [];
46433
+ for (const rt of parsed.routes) {
46434
+ const originId = resolveStop(
46435
+ rt.origin,
46436
+ rt.originAlias,
46437
+ rt.originLabel,
46438
+ rt.originTags,
46439
+ rt.originValue,
46440
+ rt.lineNumber
46441
+ );
46442
+ if (!originId) continue;
46443
+ const stopIds = [originId];
46444
+ const legs = [];
46445
+ let prevId = originId;
46446
+ for (const leg of rt.legs) {
46447
+ const destId = resolveStop(
46448
+ leg.dest,
46449
+ leg.destAlias,
46450
+ leg.destLabel,
46451
+ leg.destTags,
46452
+ void 0,
46453
+ // a leg's `value:` is leg thickness, not the dest's size
46454
+ leg.lineNumber
46455
+ );
46456
+ if (!destId) continue;
46457
+ legs.push({
46458
+ fromId: prevId,
46459
+ toId: destId,
46460
+ ...leg.label !== void 0 && { label: leg.label },
46461
+ style: leg.style,
46462
+ ...leg.value !== void 0 && { value: leg.value },
46463
+ lineNumber: leg.lineNumber
46464
+ });
46465
+ if (!stopIds.includes(destId)) stopIds.push(destId);
46466
+ prevId = destId;
46467
+ }
46468
+ routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46175
46469
  }
46176
46470
  const subdivisions = [];
46177
46471
  if (usSubdivisionReferenced || parsed.directives.region === "us-states")
@@ -46183,7 +46477,7 @@ function resolveMap(parsed, data) {
46183
46477
  }
46184
46478
  for (const r of regions) {
46185
46479
  if (r.layer === "country") {
46186
- const bb = featureBbox(data.worldCoarse, r.iso);
46480
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46187
46481
  if (bb) regionBoxes.push(bb);
46188
46482
  }
46189
46483
  }
@@ -46197,6 +46491,7 @@ function resolveMap(parsed, data) {
46197
46491
  const lonSpan = extent2[1][0] - extent2[0][0];
46198
46492
  const latSpan = extent2[1][1] - extent2[0][1];
46199
46493
  const span = Math.max(lonSpan, latSpan);
46494
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46200
46495
  const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46201
46496
  let projection;
46202
46497
  const override = parsed.directives.projection;
@@ -46204,12 +46499,10 @@ function resolveMap(parsed, data) {
46204
46499
  projection = override;
46205
46500
  } else if (usDominant) {
46206
46501
  projection = "albers-usa";
46207
- } else if (span > WORLD_SPAN) {
46502
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46208
46503
  projection = "equirectangular";
46209
- } else if (span < MERCATOR_MAX_SPAN) {
46210
- projection = "mercator";
46211
46504
  } else {
46212
- projection = "equirectangular";
46505
+ projection = "mercator";
46213
46506
  }
46214
46507
  if (lonSpan >= 180) {
46215
46508
  extent2 = [
@@ -46263,14 +46556,14 @@ function firstError(diags) {
46263
46556
  const e = diags.find((d) => d.severity === "error");
46264
46557
  return e ? formatDgmoError(e) : null;
46265
46558
  }
46266
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES;
46559
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46267
46560
  var init_resolver2 = __esm({
46268
46561
  "src/map/resolver.ts"() {
46269
46562
  "use strict";
46270
46563
  init_diagnostics();
46271
46564
  init_geo();
46272
46565
  WORLD_SPAN = 90;
46273
- MERCATOR_MAX_SPAN = 25;
46566
+ MERCATOR_MAX_LAT = 80;
46274
46567
  PAD_FRACTION = 0.05;
46275
46568
  WORLD_LAT_SOUTH = -58;
46276
46569
  WORLD_LAT_NORTH = 78;
@@ -46295,115 +46588,59 @@ var init_resolver2 = __esm({
46295
46588
  "north macedonia": "macedonia",
46296
46589
  "czech republic": "czechia"
46297
46590
  };
46298
- }
46299
- });
46300
-
46301
- // src/map/load-data.ts
46302
- var load_data_exports = {};
46303
- __export(load_data_exports, {
46304
- loadMapData: () => loadMapData
46305
- });
46306
- async function loadNodeBuiltins() {
46307
- const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
46308
- import("fs/promises"),
46309
- import("url"),
46310
- import("path")
46311
- ]);
46312
- return { readFile, fileURLToPath, dirname: dirname2, resolve };
46313
- }
46314
- async function readJson(nb, dir, name) {
46315
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46316
- }
46317
- async function firstExistingDir(nb, baseDir) {
46318
- for (const rel of CANDIDATE_DIRS) {
46319
- const dir = nb.resolve(baseDir, rel);
46320
- try {
46321
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46322
- return dir;
46323
- } catch {
46324
- }
46325
- }
46326
- throw new Error(
46327
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46328
- );
46329
- }
46330
- function validate(data) {
46331
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46332
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46333
- throw new Error("map data assets are malformed (failed shape validation)");
46334
- }
46335
- return data;
46336
- }
46337
- function moduleBaseDir(nb) {
46338
- try {
46339
- const url = import_meta.url;
46340
- if (url) return nb.dirname(nb.fileURLToPath(url));
46341
- } catch {
46342
- }
46343
- if (typeof __dirname !== "undefined") return __dirname;
46344
- return process.cwd();
46345
- }
46346
- function loadMapData() {
46347
- cache ??= (async () => {
46348
- const nb = await loadNodeBuiltins();
46349
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46350
- const [
46351
- worldCoarse,
46352
- worldDetail,
46353
- usStates,
46354
- lakes,
46355
- rivers,
46356
- naLand,
46357
- naLakes,
46358
- gazetteer
46359
- ] = await Promise.all([
46360
- readJson(nb, dir, FILES.worldCoarse),
46361
- readJson(nb, dir, FILES.worldDetail),
46362
- readJson(nb, dir, FILES.usStates),
46363
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46364
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46365
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46366
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46367
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46368
- readJson(nb, dir, FILES.gazetteer)
46591
+ US_STATE_POSTAL = /* @__PURE__ */ new Set([
46592
+ "AL",
46593
+ "AK",
46594
+ "AZ",
46595
+ "AR",
46596
+ "CA",
46597
+ "CO",
46598
+ "CT",
46599
+ "DE",
46600
+ "FL",
46601
+ "GA",
46602
+ "HI",
46603
+ "ID",
46604
+ "IL",
46605
+ "IN",
46606
+ "IA",
46607
+ "KS",
46608
+ "KY",
46609
+ "LA",
46610
+ "ME",
46611
+ "MD",
46612
+ "MA",
46613
+ "MI",
46614
+ "MN",
46615
+ "MS",
46616
+ "MO",
46617
+ "MT",
46618
+ "NE",
46619
+ "NV",
46620
+ "NH",
46621
+ "NJ",
46622
+ "NM",
46623
+ "NY",
46624
+ "NC",
46625
+ "ND",
46626
+ "OH",
46627
+ "OK",
46628
+ "OR",
46629
+ "PA",
46630
+ "RI",
46631
+ "SC",
46632
+ "SD",
46633
+ "TN",
46634
+ "TX",
46635
+ "UT",
46636
+ "VT",
46637
+ "VA",
46638
+ "WA",
46639
+ "WV",
46640
+ "WI",
46641
+ "WY",
46642
+ "DC"
46369
46643
  ]);
46370
- return validate({
46371
- worldCoarse,
46372
- worldDetail,
46373
- usStates,
46374
- gazetteer,
46375
- ...lakes && { lakes },
46376
- ...rivers && { rivers },
46377
- ...naLand && { naLand },
46378
- ...naLakes && { naLakes }
46379
- });
46380
- })().catch((e) => {
46381
- cache = void 0;
46382
- throw e;
46383
- });
46384
- return cache;
46385
- }
46386
- var import_meta, FILES, CANDIDATE_DIRS, cache;
46387
- var init_load_data = __esm({
46388
- "src/map/load-data.ts"() {
46389
- "use strict";
46390
- import_meta = {};
46391
- FILES = {
46392
- worldCoarse: "world-coarse.json",
46393
- worldDetail: "world-detail.json",
46394
- usStates: "us-states.json",
46395
- lakes: "lakes.json",
46396
- rivers: "rivers.json",
46397
- naLand: "na-land.json",
46398
- naLakes: "na-lakes.json",
46399
- gazetteer: "gazetteer.json"
46400
- };
46401
- CANDIDATE_DIRS = [
46402
- "./data",
46403
- "./map-data",
46404
- "../map-data",
46405
- "../src/map/data"
46406
- ];
46407
46644
  }
46408
46645
  });
46409
46646
 
@@ -46433,10 +46670,14 @@ function projectionFor(family) {
46433
46670
  return (0, import_d3_geo2.geoEquirectangular)();
46434
46671
  }
46435
46672
  }
46436
- function mapBackgroundColor(palette) {
46437
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
46673
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
46674
+ return mix(
46675
+ palette.colors.blue,
46676
+ palette.bg,
46677
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
46678
+ );
46438
46679
  }
46439
- function mapNeutralLandColor(palette, isDark) {
46680
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46440
46681
  return mix(
46441
46682
  palette.colors.green,
46442
46683
  palette.bg,
@@ -46462,28 +46703,19 @@ function layoutMap(resolved, data, size, opts) {
46462
46703
  }
46463
46704
  }
46464
46705
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
46465
- const landTint = isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT;
46466
- const neutralFill = mix(palette.colors.green, palette.bg, landTint);
46467
- const water = mapBackgroundColor(palette);
46468
46706
  const usContext = usLayer !== null;
46469
- const foreignFill = mix(
46470
- palette.colors.gray,
46471
- palette.bg,
46472
- isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46473
- );
46474
46707
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46475
- const scores = resolved.regions.filter((r) => r.score !== void 0).map((r) => r.score);
46708
+ const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46476
46709
  const scaleOverride = resolved.directives.scale;
46477
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...scores);
46478
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...scores);
46479
- const rampHue = palette.colors.red;
46480
- const hasRamp = scores.length > 0;
46481
- const SCORE_NAME = hasRamp ? resolved.directives.metric?.trim() || "Score" : null;
46710
+ const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46711
+ const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46712
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46713
+ const hasRamp = values.length > 0;
46714
+ const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46482
46715
  const matchColorGroup = (v) => {
46483
46716
  const lv = v.trim().toLowerCase();
46484
46717
  if (lv === "none") return null;
46485
- if (SCORE_NAME && (lv === "score" || lv === SCORE_NAME.toLowerCase()))
46486
- return SCORE_NAME;
46718
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
46487
46719
  const tg = resolved.tagGroups.find((g) => g.name.toLowerCase() === lv);
46488
46720
  return tg ? tg.name : v;
46489
46721
  };
@@ -46494,11 +46726,20 @@ function layoutMap(resolved, data, size, opts) {
46494
46726
  } else if (resolved.directives.activeTag !== void 0) {
46495
46727
  activeGroup = matchColorGroup(resolved.directives.activeTag);
46496
46728
  } else {
46497
- activeGroup = SCORE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46729
+ activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46498
46730
  }
46499
- const activeIsScore = SCORE_NAME !== null && activeGroup === SCORE_NAME;
46731
+ const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46732
+ const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
46733
+ const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46734
+ const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46735
+ const lakeStroke = mix(regionStroke, water, 45);
46736
+ const foreignFill = mix(
46737
+ palette.colors.gray,
46738
+ palette.bg,
46739
+ mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46740
+ );
46500
46741
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46501
- const fillForScore = (s) => {
46742
+ const fillForValue = (s) => {
46502
46743
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
46503
46744
  const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
46504
46745
  return mix(rampHue, rampBase, pct);
@@ -46521,9 +46762,16 @@ function layoutMap(resolved, data, size, opts) {
46521
46762
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46522
46763
  );
46523
46764
  };
46765
+ const directFill = (name) => {
46766
+ const hex = name ? resolveColor(name, palette) : null;
46767
+ if (!hex) return null;
46768
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
46769
+ };
46524
46770
  const regionFill = (r) => {
46771
+ const direct = directFill(r.color);
46772
+ if (direct) return direct;
46525
46773
  if (activeIsScore) {
46526
- return r.score !== void 0 ? fillForScore(r.score) : neutralFill;
46774
+ return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46527
46775
  }
46528
46776
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46529
46777
  };
@@ -46575,6 +46823,7 @@ function layoutMap(resolved, data, size, opts) {
46575
46823
  const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46576
46824
  let path;
46577
46825
  let project;
46826
+ let stretchParams = null;
46578
46827
  if (fitIsGlobal) {
46579
46828
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46580
46829
  const bx0 = cb[0][0];
@@ -46585,6 +46834,7 @@ function layoutMap(resolved, data, size, opts) {
46585
46834
  const oy = fitBox[0][1];
46586
46835
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46587
46836
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
46837
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46588
46838
  const stretch = (x, y) => [
46589
46839
  ox + (x - bx0) * sx,
46590
46840
  oy + (y - by0) * sy
@@ -46616,7 +46866,7 @@ function layoutMap(resolved, data, size, opts) {
46616
46866
  const insets = [];
46617
46867
  const insetRegions = [];
46618
46868
  const insetLabelSeeds = [];
46619
- if (resolved.projection === "albers-usa" && usLayer) {
46869
+ if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
46620
46870
  const PAD = 8;
46621
46871
  const GAP = 12;
46622
46872
  const yB = height - FIT_PAD;
@@ -46647,38 +46897,14 @@ function layoutMap(resolved, data, size, opts) {
46647
46897
  }
46648
46898
  return y;
46649
46899
  };
46650
- const coastTop = (x0, xr) => {
46900
+ const coastFloor = (x0, xr) => {
46651
46901
  const n = 24;
46652
- const pts = [];
46653
46902
  let maxY = -Infinity;
46654
46903
  for (let i = 0; i <= n; i++) {
46655
- const x = x0 + (xr - x0) * i / n;
46656
- const y = at(x);
46657
- if (y > -Infinity) {
46658
- pts.push([x, y]);
46659
- if (y > maxY) maxY = y;
46660
- }
46661
- }
46662
- if (pts.length === 0) return () => yB - height * 0.42;
46663
- let m = 0;
46664
- if (pts.length >= 2) {
46665
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46666
- for (const [x, y] of pts) {
46667
- sx += x;
46668
- sy += y;
46669
- sxx += x * x;
46670
- sxy += x * y;
46671
- }
46672
- const den = pts.length * sxx - sx * sx;
46673
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46674
- }
46675
- m = Math.max(-0.35, Math.min(0.35, m));
46676
- let c = -Infinity;
46677
- for (const [x, y] of pts) {
46678
- const need = y - m * x + GAP;
46679
- if (need > c) c = need;
46680
- }
46681
- return (x) => m * x + c;
46904
+ const y = at(x0 + (xr - x0) * i / n);
46905
+ if (y > maxY) maxY = y;
46906
+ }
46907
+ return maxY;
46682
46908
  };
46683
46909
  const placeInset = (iso, proj, boxX, iwReq) => {
46684
46910
  const f = usLayer.get(iso);
@@ -46687,19 +46913,15 @@ function layoutMap(resolved, data, size, opts) {
46687
46913
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46688
46914
  if (iw < 24) return boxX;
46689
46915
  const xr = x0 + iw + 2 * PAD;
46690
- const top = coastTop(x0, xr);
46691
- const yL = top(x0);
46692
- const yR = top(xr);
46916
+ const floor = coastFloor(x0, xr);
46917
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46693
46918
  proj.fitWidth(iw, f);
46694
46919
  const bb = (0, import_d3_geo2.geoPath)(proj).bounds(f);
46695
46920
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46696
46921
  const needH = sh + 2 * PAD;
46697
- let topFit = Math.max(yL, yR);
46922
+ let topFit = topGuess;
46698
46923
  const bottom = Math.min(topFit + needH, yB);
46699
46924
  if (bottom - topFit < needH) topFit = bottom - needH;
46700
- const lift = topFit - Math.max(yL, yR);
46701
- const topL = yL + lift;
46702
- const topR = yR + lift;
46703
46925
  proj.fitExtent(
46704
46926
  [
46705
46927
  [x0 + PAD, topFit + PAD],
@@ -46718,15 +46940,18 @@ function layoutMap(resolved, data, size, opts) {
46718
46940
  }
46719
46941
  insets.push({
46720
46942
  x: x0,
46721
- y: Math.min(topL, topR),
46943
+ y: topFit,
46722
46944
  w: xr - x0,
46723
- h: bottom - Math.min(topL, topR),
46945
+ h: bottom - topFit,
46724
46946
  points: [
46725
- [x0, topL],
46726
- [xr, topR],
46947
+ [x0, topFit],
46948
+ [xr, topFit],
46727
46949
  [xr, bottom],
46728
46950
  [x0, bottom]
46729
- ]
46951
+ ],
46952
+ // The FITTED inset projection (just fit to this box) — captured so the
46953
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
46954
+ projection: proj
46730
46955
  });
46731
46956
  insetRegions.push({
46732
46957
  id: iso,
@@ -46735,7 +46960,7 @@ function layoutMap(resolved, data, size, opts) {
46735
46960
  stroke: regionStroke,
46736
46961
  lineNumber,
46737
46962
  layer: "us-state",
46738
- ...r?.score !== void 0 && { score: r.score },
46963
+ ...r?.value !== void 0 && { value: r.value },
46739
46964
  ...r && Object.keys(r.tags).length > 0 && { tags: r.tags }
46740
46965
  });
46741
46966
  const ctr = (0, import_d3_geo2.geoPath)(proj).centroid(f);
@@ -46878,7 +47103,7 @@ function layoutMap(resolved, data, size, opts) {
46878
47103
  lineNumber,
46879
47104
  layer,
46880
47105
  ...label !== void 0 && { label },
46881
- ...isThisLayer && r.score !== void 0 && { score: r.score },
47106
+ ...isThisLayer && r.value !== void 0 && { value: r.value },
46882
47107
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
46883
47108
  });
46884
47109
  }
@@ -46896,13 +47121,40 @@ function layoutMap(resolved, data, size, opts) {
46896
47121
  id: "lake",
46897
47122
  d,
46898
47123
  fill: water,
46899
- stroke: "none",
47124
+ stroke: lakeStroke,
46900
47125
  lineNumber: -1,
46901
47126
  layer: "base"
46902
47127
  });
46903
47128
  }
46904
47129
  }
46905
- const riverColor = water;
47130
+ const relief = [];
47131
+ let reliefHatch = null;
47132
+ if (resolved.directives.relief === true && data.mountainRanges) {
47133
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
47134
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
47135
+ if (!viewF) continue;
47136
+ const area2 = path.area(viewF);
47137
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
47138
+ const box = path.bounds(viewF);
47139
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
47140
+ continue;
47141
+ const d = path(viewF) ?? "";
47142
+ if (!d) continue;
47143
+ relief.push({ d });
47144
+ }
47145
+ if (relief.length) {
47146
+ const darkTone = isDark ? palette.bg : palette.text;
47147
+ const lightTone = isDark ? palette.text : palette.bg;
47148
+ const landLum = relativeLuminance(neutralFill);
47149
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
47150
+ reliefHatch = {
47151
+ color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
47152
+ spacing: RELIEF_HATCH_SPACING,
47153
+ width: RELIEF_HATCH_WIDTH
47154
+ };
47155
+ }
47156
+ }
47157
+ const riverColor = mix(water, regionStroke, 16);
46906
47158
  const rivers = [];
46907
47159
  if (data.rivers) {
46908
47160
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46913,16 +47165,19 @@ function layoutMap(resolved, data, size, opts) {
46913
47165
  rivers.push({ d, color: riverColor, width: RIVER_WIDTH });
46914
47166
  }
46915
47167
  }
46916
- const sizeVals = resolved.pois.map((p) => Number(p.meta["size"])).filter((n) => Number.isFinite(n) && n > 0);
47168
+ const sizeVals = resolved.pois.map((p) => Number(p.meta["value"])).filter((n) => Number.isFinite(n) && n > 0);
46917
47169
  const sizeMin = sizeVals.length ? Math.min(...sizeVals) : 0;
46918
47170
  const sizeMax = sizeVals.length ? Math.max(...sizeVals) : 0;
46919
47171
  const radiusFor = (p) => {
46920
- const v = Number(p.meta["size"]);
47172
+ const v = Number(p.meta["value"]);
46921
47173
  if (!Number.isFinite(v) || v <= 0 || sizeMax <= 0) return R_DEFAULT;
46922
47174
  const t = sizeMax > sizeMin ? (Math.sqrt(v) - Math.sqrt(sizeMin)) / (Math.sqrt(sizeMax) - Math.sqrt(sizeMin)) : 1;
46923
47175
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46924
47176
  };
46925
47177
  const poiFill = (p) => {
47178
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
47179
+ if (directHex)
47180
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46926
47181
  for (const group of resolved.tagGroups) {
46927
47182
  const val = p.tags[group.name.toLowerCase()];
46928
47183
  if (!val) continue;
@@ -46984,7 +47239,8 @@ function layoutMap(resolved, data, size, opts) {
46984
47239
  lineNumber: e.p.lineNumber,
46985
47240
  implicit: !!e.p.implicit,
46986
47241
  isOrigin: originIds.has(e.p.id),
46987
- ...num !== void 0 && { routeNumber: num }
47242
+ ...num !== void 0 && { routeNumber: num },
47243
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46988
47244
  });
46989
47245
  });
46990
47246
  }
@@ -47020,26 +47276,40 @@ function layoutMap(resolved, data, size, opts) {
47020
47276
  const by = b.cy - (b.cy - py) / tb * trimB;
47021
47277
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
47022
47278
  };
47279
+ const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
47280
+ const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
47281
+ const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
47282
+ const routeWidthFor = (v) => {
47283
+ if (!Number.isFinite(v) || v <= 0 || rlMax <= 0) return W_MIN;
47284
+ const t = rlMax > rlMin ? (v - rlMin) / (rlMax - rlMin) : 1;
47285
+ return W_MIN + t * (W_MAX - W_MIN);
47286
+ };
47023
47287
  for (const rt of resolved.routes) {
47024
- const curved = rt.meta["style"] === "arc";
47025
- for (let i = 1; i < rt.stopIds.length; i++) {
47026
- const a = poiScreen.get(rt.stopIds[i - 1]);
47027
- const b = poiScreen.get(rt.stopIds[i]);
47288
+ for (const leg of rt.legs) {
47289
+ const a = poiScreen.get(leg.fromId);
47290
+ const b = poiScreen.get(leg.toId);
47028
47291
  if (!a || !b) continue;
47292
+ const mx = (a.cx + b.cx) / 2;
47293
+ const my = (a.cy + b.cy) / 2;
47029
47294
  legs.push({
47030
- d: legPath(a, b, curved, 0),
47031
- width: W_MIN,
47295
+ d: legPath(a, b, leg.style === "arc", 0),
47296
+ width: routeWidthFor(Number(leg.value)),
47032
47297
  color: mix(palette.text, palette.bg, 72),
47033
47298
  arrow: true,
47034
- lineNumber: rt.lineNumber
47299
+ lineNumber: leg.lineNumber,
47300
+ ...leg.label !== void 0 && {
47301
+ label: leg.label,
47302
+ labelX: mx,
47303
+ labelY: my - 4
47304
+ }
47035
47305
  });
47036
47306
  }
47037
47307
  }
47038
- const weightVals = resolved.edges.map((e) => Number(e.meta["weight"])).filter((n) => Number.isFinite(n) && n > 0);
47308
+ const weightVals = resolved.edges.map((e) => Number(e.meta["value"])).filter((n) => Number.isFinite(n) && n > 0);
47039
47309
  const wMin = weightVals.length ? Math.min(...weightVals) : 0;
47040
47310
  const wMax = weightVals.length ? Math.max(...weightVals) : 0;
47041
47311
  const widthFor = (e) => {
47042
- const v = Number(e.meta["weight"]);
47312
+ const v = Number(e.meta["value"]);
47043
47313
  if (!Number.isFinite(v) || v <= 0 || wMax <= 0) return W_MIN;
47044
47314
  const t = wMax > wMin ? (v - wMin) / (wMax - wMin) : 1;
47045
47315
  return W_MIN + t * (W_MAX - W_MIN);
@@ -47302,8 +47572,8 @@ function layoutMap(resolved, data, size, opts) {
47302
47572
  activeGroup,
47303
47573
  ...hasRamp && {
47304
47574
  ramp: {
47305
- ...resolved.directives.metric !== void 0 && {
47306
- metric: resolved.directives.metric
47575
+ ...resolved.directives.regionMetric !== void 0 && {
47576
+ metric: resolved.directives.regionMetric
47307
47577
  },
47308
47578
  min: rampMin,
47309
47579
  max: rampMax,
@@ -47323,21 +47593,26 @@ function layoutMap(resolved, data, size, opts) {
47323
47593
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47324
47594
  regions,
47325
47595
  rivers,
47596
+ relief,
47597
+ reliefHatch,
47326
47598
  legs,
47327
47599
  pois,
47328
47600
  labels,
47329
47601
  legend,
47330
47602
  insets,
47331
- insetRegions
47603
+ insetRegions,
47604
+ projection,
47605
+ stretch: stretchParams
47332
47606
  };
47333
47607
  }
47334
- var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
47608
+ var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
47335
47609
  var init_layout15 = __esm({
47336
47610
  "src/map/layout.ts"() {
47337
47611
  "use strict";
47338
47612
  import_d3_geo2 = require("d3-geo");
47339
47613
  import_topojson_client2 = require("topojson-client");
47340
47614
  init_color_utils();
47615
+ init_colors();
47341
47616
  init_label_layout();
47342
47617
  init_legend_constants();
47343
47618
  init_title_constants();
@@ -47350,14 +47625,22 @@ var init_layout15 = __esm({
47350
47625
  W_MAX = 8;
47351
47626
  FONT = 11;
47352
47627
  COLO_EPS = 1.5;
47353
- LAND_TINT_LIGHT = 58;
47354
- LAND_TINT_DARK = 75;
47628
+ LAND_TINT_LIGHT = 12;
47629
+ LAND_TINT_DARK = 24;
47355
47630
  TAG_TINT_LIGHT = 60;
47356
47631
  TAG_TINT_DARK = 68;
47357
- WATER_TINT = 55;
47632
+ WATER_TINT_LIGHT = 13;
47633
+ WATER_TINT_DARK = 14;
47358
47634
  RIVER_WIDTH = 1.3;
47635
+ RELIEF_MIN_AREA = 12;
47636
+ RELIEF_MIN_DIM = 2;
47637
+ RELIEF_HATCH_SPACING = 3;
47638
+ RELIEF_HATCH_WIDTH = 0.25;
47639
+ RELIEF_HATCH_STRENGTH = 32;
47359
47640
  FOREIGN_TINT_LIGHT = 30;
47360
47641
  FOREIGN_TINT_DARK = 62;
47642
+ MUTED_FOREIGN_LIGHT = 28;
47643
+ MUTED_FOREIGN_DARK = 16;
47361
47644
  COLO_R = 9;
47362
47645
  GOLDEN_ANGLE = 2.399963229728653;
47363
47646
  FAN_STEP = 16;
@@ -47411,7 +47694,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47411
47694
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
47412
47695
  if (r.layer !== "base") {
47413
47696
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47414
- if (r.score !== void 0) p.attr("data-score", r.score);
47697
+ if (r.value !== void 0) p.attr("data-value", r.value);
47415
47698
  if (r.tags) {
47416
47699
  for (const [group, value] of Object.entries(r.tags)) {
47417
47700
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47429,6 +47712,20 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47429
47712
  }
47430
47713
  };
47431
47714
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
47715
+ if (layout.relief.length && layout.reliefHatch) {
47716
+ const h = layout.reliefHatch;
47717
+ const rangeClipId = "dgmo-relief-clip";
47718
+ const landClipId = "dgmo-relief-land";
47719
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
47720
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
47721
+ const landClip = defs.append("clipPath").attr("id", landClipId);
47722
+ for (const r of layout.regions)
47723
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47724
+ const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).append("g").attr("class", "dgmo-map-relief").attr("clip-path", `url(#${rangeClipId})`).attr("stroke", h.color).attr("stroke-width", h.width).attr("vector-effect", "non-scaling-stroke");
47725
+ for (let y = h.spacing; y < height; y += h.spacing) {
47726
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47727
+ }
47728
+ }
47432
47729
  if (layout.rivers.length) {
47433
47730
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47434
47731
  for (const r of layout.rivers) {
@@ -47472,6 +47769,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47472
47769
  gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r + 3).attr("fill", "none").attr("stroke", poi.stroke).attr("stroke-width", 1.5);
47473
47770
  }
47474
47771
  const c = gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r).attr("fill", poi.fill).attr("stroke", poi.stroke).attr("stroke-width", 1).attr("data-line-number", poi.lineNumber).attr("data-poi", poi.id);
47772
+ if (poi.tags) {
47773
+ for (const [group, value] of Object.entries(poi.tags)) {
47774
+ c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
47775
+ }
47776
+ }
47475
47777
  if (onClickItem) {
47476
47778
  c.style("cursor", "pointer").on(
47477
47779
  "click",
@@ -47521,7 +47823,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47521
47823
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
47522
47824
  const ramp = layout.legend.ramp;
47523
47825
  const scoreGroup = ramp ? {
47524
- name: ramp.metric?.trim() || "Score",
47826
+ name: ramp.metric?.trim() || "Value",
47525
47827
  entries: [],
47526
47828
  gradient: {
47527
47829
  min: ramp.min,
@@ -47548,7 +47850,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47548
47850
  }
47549
47851
  }
47550
47852
  if (layout.title) {
47551
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47853
+ svg.append("text").attr("class", "dgmo-map-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47552
47854
  }
47553
47855
  if (layout.subtitle) {
47554
47856
  svg.append("text").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
@@ -47581,6 +47883,121 @@ var init_renderer16 = __esm({
47581
47883
  }
47582
47884
  });
47583
47885
 
47886
+ // src/map/load-data.ts
47887
+ var load_data_exports = {};
47888
+ __export(load_data_exports, {
47889
+ loadMapData: () => loadMapData
47890
+ });
47891
+ async function loadNodeBuiltins() {
47892
+ const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
47893
+ import("fs/promises"),
47894
+ import("url"),
47895
+ import("path")
47896
+ ]);
47897
+ return { readFile, fileURLToPath, dirname: dirname2, resolve };
47898
+ }
47899
+ async function readJson(nb, dir, name) {
47900
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
47901
+ }
47902
+ async function firstExistingDir(nb, baseDir) {
47903
+ for (const rel of CANDIDATE_DIRS) {
47904
+ const dir = nb.resolve(baseDir, rel);
47905
+ try {
47906
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
47907
+ return dir;
47908
+ } catch {
47909
+ }
47910
+ }
47911
+ throw new Error(
47912
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
47913
+ );
47914
+ }
47915
+ function validate(data) {
47916
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
47917
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
47918
+ throw new Error("map data assets are malformed (failed shape validation)");
47919
+ }
47920
+ return data;
47921
+ }
47922
+ function moduleBaseDir(nb) {
47923
+ try {
47924
+ const url = import_meta.url;
47925
+ if (url) return nb.dirname(nb.fileURLToPath(url));
47926
+ } catch {
47927
+ }
47928
+ if (typeof __dirname !== "undefined") return __dirname;
47929
+ return process.cwd();
47930
+ }
47931
+ function loadMapData() {
47932
+ cache ??= (async () => {
47933
+ const nb = await loadNodeBuiltins();
47934
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
47935
+ const [
47936
+ worldCoarse,
47937
+ worldDetail,
47938
+ usStates,
47939
+ lakes,
47940
+ rivers,
47941
+ mountainRanges,
47942
+ naLand,
47943
+ naLakes,
47944
+ gazetteer
47945
+ ] = await Promise.all([
47946
+ readJson(nb, dir, FILES.worldCoarse),
47947
+ readJson(nb, dir, FILES.worldDetail),
47948
+ readJson(nb, dir, FILES.usStates),
47949
+ // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
47950
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
47951
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
47952
+ readJson(nb, dir, FILES.mountainRanges).catch(
47953
+ () => void 0
47954
+ ),
47955
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
47956
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
47957
+ readJson(nb, dir, FILES.gazetteer)
47958
+ ]);
47959
+ return validate({
47960
+ worldCoarse,
47961
+ worldDetail,
47962
+ usStates,
47963
+ gazetteer,
47964
+ ...lakes && { lakes },
47965
+ ...rivers && { rivers },
47966
+ ...mountainRanges && { mountainRanges },
47967
+ ...naLand && { naLand },
47968
+ ...naLakes && { naLakes }
47969
+ });
47970
+ })().catch((e) => {
47971
+ cache = void 0;
47972
+ throw e;
47973
+ });
47974
+ return cache;
47975
+ }
47976
+ var import_meta, FILES, CANDIDATE_DIRS, cache;
47977
+ var init_load_data = __esm({
47978
+ "src/map/load-data.ts"() {
47979
+ "use strict";
47980
+ import_meta = {};
47981
+ FILES = {
47982
+ worldCoarse: "world-coarse.json",
47983
+ worldDetail: "world-detail.json",
47984
+ usStates: "us-states.json",
47985
+ lakes: "lakes.json",
47986
+ rivers: "rivers.json",
47987
+ mountainRanges: "mountain-ranges.json",
47988
+ naLand: "na-land.json",
47989
+ naLakes: "na-lakes.json",
47990
+ gazetteer: "gazetteer.json"
47991
+ };
47992
+ CANDIDATE_DIRS = [
47993
+ "./data",
47994
+ "./map-data",
47995
+ "../map-data",
47996
+ "../src/map/data"
47997
+ ];
47998
+ }
47999
+ });
48000
+
47584
48001
  // src/pyramid/renderer.ts
47585
48002
  var renderer_exports17 = {};
47586
48003
  __export(renderer_exports17, {
@@ -55760,15 +56177,17 @@ async function renderForExport(content, theme, palette, viewState, options) {
55760
56177
  if (detectedType === "map") {
55761
56178
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55762
56179
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55763
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55764
56180
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
55765
56181
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55766
56182
  const mapParsed = parseMap2(content);
55767
- let mapData;
55768
- try {
55769
- mapData = await loadMapData2();
55770
- } catch {
55771
- return "";
56183
+ let mapData = options?.mapData;
56184
+ if (!mapData) {
56185
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
56186
+ try {
56187
+ mapData = await loadMapData2();
56188
+ } catch {
56189
+ return "";
56190
+ }
55772
56191
  }
55773
56192
  const mapResolved = resolveMap2(mapParsed, mapData);
55774
56193
  const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
@@ -56619,6 +57038,7 @@ __export(internal_exports, {
56619
57038
  computeTimeTicks: () => computeTimeTicks,
56620
57039
  contrastText: () => contrastText,
56621
57040
  controlsGroupCapsuleWidth: () => controlsGroupCapsuleWidth,
57041
+ createMapGeoQuery: () => createMapGeoQuery,
56622
57042
  decodeDiagramUrl: () => decodeDiagramUrl,
56623
57043
  decodeViewState: () => decodeViewState,
56624
57044
  displayName: () => displayName,
@@ -57347,6 +57767,160 @@ init_load_data();
57347
57767
  init_layout15();
57348
57768
  init_renderer16();
57349
57769
 
57770
+ // src/map/geo-query.ts
57771
+ init_parser12();
57772
+ init_resolver2();
57773
+ init_layout15();
57774
+ init_geo();
57775
+
57776
+ // src/map/invert.ts
57777
+ function inInsetFrame(inset, px, py) {
57778
+ return px >= inset.x && px <= inset.x + inset.w && py >= inset.y && py <= inset.y + inset.h;
57779
+ }
57780
+ function unstretch(layout, px, py) {
57781
+ const s = layout.stretch;
57782
+ return [
57783
+ s.bx0 + (s.sx !== 0 ? (px - s.ox) / s.sx : 0),
57784
+ s.by0 + (s.sy !== 0 ? (py - s.oy) / s.sy : 0)
57785
+ ];
57786
+ }
57787
+ function applyStretch(layout, x, y) {
57788
+ const s = layout.stretch;
57789
+ return [s.ox + (x - s.bx0) * s.sx, s.oy + (y - s.by0) * s.sy];
57790
+ }
57791
+ function pixelToLonLat(layout, px, py) {
57792
+ for (const inset of layout.insets) {
57793
+ if (inInsetFrame(inset, px, py)) {
57794
+ const ll2 = inset.projection.invert?.([px, py]);
57795
+ return ll2 && Number.isFinite(ll2[0]) && Number.isFinite(ll2[1]) ? [ll2[0], ll2[1]] : null;
57796
+ }
57797
+ }
57798
+ const [x, y] = layout.stretch ? unstretch(layout, px, py) : [px, py];
57799
+ const ll = layout.projection.invert?.([x, y]);
57800
+ return ll && Number.isFinite(ll[0]) && Number.isFinite(ll[1]) ? [ll[0], ll[1]] : null;
57801
+ }
57802
+ function lonLatToPixel(layout, lonLat) {
57803
+ const pt = [lonLat[0], lonLat[1]];
57804
+ const main = layout.projection(pt);
57805
+ const mainPx = main && Number.isFinite(main[0]) && Number.isFinite(main[1]) ? layout.stretch ? applyStretch(layout, main[0], main[1]) : [main[0], main[1]] : null;
57806
+ const onCanvas = !!mainPx && mainPx[0] >= 0 && mainPx[0] <= layout.width && mainPx[1] >= 0 && mainPx[1] <= layout.height;
57807
+ if (onCanvas) return mainPx;
57808
+ for (const inset of layout.insets) {
57809
+ const p = inset.projection(pt);
57810
+ if (p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && inInsetFrame(inset, p[0], p[1]))
57811
+ return [p[0], p[1]];
57812
+ }
57813
+ return mainPx;
57814
+ }
57815
+
57816
+ // src/map/geo-query.ts
57817
+ var EARTH_R_KM = 6371;
57818
+ var DEG = Math.PI / 180;
57819
+ function haversineKm(lat1, lon1, lat2, lon2) {
57820
+ const dLat = (lat2 - lat1) * DEG;
57821
+ const dLon = (lon2 - lon1) * DEG;
57822
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * DEG) * Math.cos(lat2 * DEG) * Math.sin(dLon / 2) ** 2;
57823
+ return 2 * EARTH_R_KM * Math.asin(Math.min(1, Math.sqrt(a)));
57824
+ }
57825
+ var POP_PULL_KM = 12;
57826
+ function nearestCity(lonLat, gazetteer) {
57827
+ const [lon, lat] = lonLat;
57828
+ let best = null;
57829
+ const cities = gazetteer.cities;
57830
+ for (let i = 0; i < cities.length; i++) {
57831
+ const c2 = cities[i];
57832
+ const dist = haversineKm(lat, lon, c2[0], c2[1]);
57833
+ const score = dist - POP_PULL_KM * Math.log10((c2[3] || 0) + 1);
57834
+ if (!best || score < best.score) best = { score, idx: i, dist };
57835
+ }
57836
+ if (!best) return null;
57837
+ const c = cities[best.idx];
57838
+ return {
57839
+ name: c[4],
57840
+ iso: c[2],
57841
+ ...c[5] !== void 0 && { sub: c[5] },
57842
+ distanceKm: best.dist
57843
+ };
57844
+ }
57845
+ function roundCoord(n) {
57846
+ return Number(n.toFixed(2));
57847
+ }
57848
+ function buildTokens(lonLat, region, city) {
57849
+ const coordPoiLine = `poi ${roundCoord(lonLat[1])} ${roundCoord(lonLat[0])}`;
57850
+ let stateTok = null;
57851
+ if (region.state) {
57852
+ const { iso, name } = region.state;
57853
+ stateTok = { primary: `${name} ${iso}`, alternates: [iso, name] };
57854
+ }
57855
+ let countryTok = null;
57856
+ if (region.country) {
57857
+ const { iso, name } = region.country;
57858
+ countryTok = { primary: name, alternates: [iso] };
57859
+ }
57860
+ let cityTok = null;
57861
+ if (city) {
57862
+ const scope = city.sub ?? (city.iso || "");
57863
+ cityTok = scope ? { token: `poi ${city.name} ${scope}`, ambiguous: false } : { token: `poi ${city.name}`, ambiguous: true };
57864
+ }
57865
+ return { coordPoiLine, state: stateTok, country: countryTok, city: cityTok };
57866
+ }
57867
+ var MAX_CITY_DOTS = 250;
57868
+ function createMapGeoQuery(opts) {
57869
+ const { content, width, height, data, palette, isDark } = opts;
57870
+ const resolved = resolveMap(parseMap(content), data);
57871
+ const layout = layoutMap(
57872
+ resolved,
57873
+ data,
57874
+ { width, height },
57875
+ { palette, isDark }
57876
+ );
57877
+ const countries = decodeFeatures(data.worldDetail);
57878
+ const states = decodeFeatures(data.usStates);
57879
+ const gazetteer = data.gazetteer;
57880
+ const invert = (px, py) => pixelToLonLat(layout, px, py);
57881
+ const project = (lonLat) => lonLatToPixel(layout, lonLat);
57882
+ const locate = (px, py) => {
57883
+ const lonLat = invert(px, py);
57884
+ if (!lonLat) return null;
57885
+ const region = regionAt(lonLat, countries, states);
57886
+ const city = nearestCity(lonLat, gazetteer);
57887
+ return {
57888
+ lonLat,
57889
+ country: region.country,
57890
+ state: region.state,
57891
+ nearestCity: city,
57892
+ tokens: buildTokens(lonLat, region, city)
57893
+ };
57894
+ };
57895
+ const cities = (extent2) => {
57896
+ const sorted = [...gazetteer.cities].sort((a, b) => b[3] - a[3]);
57897
+ const out = [];
57898
+ for (const c of sorted) {
57899
+ const [lat, lon, iso, pop, name, sub] = c;
57900
+ if (extent2) {
57901
+ const [[w, s], [e, n]] = extent2;
57902
+ if (lon < w || lon > e || lat < s || lat > n) continue;
57903
+ }
57904
+ const p = project([lon, lat]);
57905
+ if (!p) continue;
57906
+ if (p[0] < 0 || p[0] > width || p[1] < 0 || p[1] > height) continue;
57907
+ out.push({
57908
+ name,
57909
+ iso,
57910
+ ...sub !== void 0 && { sub },
57911
+ lon,
57912
+ lat,
57913
+ px: p[0],
57914
+ py: p[1],
57915
+ pop
57916
+ });
57917
+ if (out.length >= MAX_CITY_DOTS) break;
57918
+ }
57919
+ return out;
57920
+ };
57921
+ return { invert, project, locate, cities };
57922
+ }
57923
+
57350
57924
  // src/map/completion.ts
57351
57925
  var fold2 = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
57352
57926
  var groupThousands = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -58469,7 +59043,7 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58469
59043
  [
58470
59044
  "map",
58471
59045
  // Geographic map directives (§24B.2/.7). `poi`/`route` are content
58472
- // keywords, not directives; metadata keys (score/size/label) live in the
59046
+ // keywords, not directives; metadata keys (value/label/style) live in the
58473
59047
  // reserved-key registry.
58474
59048
  withGlobals({
58475
59049
  region: {
@@ -58480,9 +59054,14 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58480
59054
  description: "Override the auto projection",
58481
59055
  values: ["equirectangular", "natural-earth", "albers-usa", "mercator"]
58482
59056
  },
58483
- metric: { description: "Label for the region score ramp" },
58484
- "size-metric": { description: "Label for the POI size channel" },
58485
- scale: { description: "Override score ramp anchors: scale <min> <max>" },
59057
+ "region-metric": { description: "Label for the region value ramp" },
59058
+ "poi-metric": {
59059
+ description: "Label for the POI value (marker size) channel"
59060
+ },
59061
+ "flow-metric": {
59062
+ description: "Label for the edge/leg value (thickness) channel"
59063
+ },
59064
+ scale: { description: "Override value ramp anchors: scale <min> <max>" },
58486
59065
  "region-labels": {
58487
59066
  description: "Subdivision name labels",
58488
59067
  values: ["full", "abbrev", "off"]
@@ -58494,6 +59073,7 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58494
59073
  "default-country": { description: "ISO scope for bare city resolution" },
58495
59074
  "default-state": { description: "ISO subdivision scope" },
58496
59075
  "no-legend": { description: "Suppress the legend" },
59076
+ relief: { description: "Subtle mountain-range relief shading" },
58497
59077
  subtitle: { description: "Subtitle line" },
58498
59078
  caption: { description: "Caption line" }
58499
59079
  })
@@ -59999,6 +60579,7 @@ function formatLineDiff(path, original, migrated) {
59999
60579
  computeTimeTicks,
60000
60580
  contrastText,
60001
60581
  controlsGroupCapsuleWidth,
60582
+ createMapGeoQuery,
60002
60583
  decodeDiagramUrl,
60003
60584
  decodeViewState,
60004
60585
  displayName,