@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.js CHANGED
@@ -855,13 +855,9 @@ var init_reserved_key_registry = __esm({
855
855
  "icon"
856
856
  ]);
857
857
  MAP_REGISTRY = staticRegistry([
858
- "score",
858
+ "value",
859
859
  "label",
860
- "size",
861
- "description",
862
- "weight",
863
- "style",
864
- "date"
860
+ "style"
865
861
  ]);
866
862
  ORG_REGISTRY = staticRegistry([
867
863
  "color",
@@ -15844,7 +15840,8 @@ function parseMap(content) {
15844
15840
  continue;
15845
15841
  }
15846
15842
  if (open.route && indent > open.route.indent) {
15847
- open.route.route.stops.push(parseStop(trimmed, lineNumber));
15843
+ const leg = parseLeg(trimmed, lineNumber, open.route.route.style);
15844
+ open.route.route.legs.push(leg);
15848
15845
  continue;
15849
15846
  }
15850
15847
  if (open.poi && indent > open.poi.indent) {
@@ -15875,6 +15872,10 @@ function parseMap(content) {
15875
15872
  handleTag(trimmed, lineNumber);
15876
15873
  continue;
15877
15874
  }
15875
+ if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15876
+ handleDirective(firstWord, "", lineNumber);
15877
+ continue;
15878
+ }
15878
15879
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15879
15880
  handleDirective(
15880
15881
  firstWord,
@@ -15939,13 +15940,20 @@ function parseMap(content) {
15939
15940
  );
15940
15941
  d.projection = value;
15941
15942
  break;
15942
- case "metric":
15943
- dup(d.metric);
15944
- d.metric = value;
15943
+ case "region-metric": {
15944
+ dup(d.regionMetric);
15945
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
15946
+ d.regionMetric = rmLabel;
15947
+ if (rmColor) d.regionMetricColor = rmColor;
15948
+ break;
15949
+ }
15950
+ case "poi-metric":
15951
+ dup(d.poiMetric);
15952
+ d.poiMetric = value;
15945
15953
  break;
15946
- case "size-metric":
15947
- dup(d.sizeMetric);
15948
- d.sizeMetric = value;
15954
+ case "flow-metric":
15955
+ dup(d.flowMetric);
15956
+ d.flowMetric = value;
15949
15957
  break;
15950
15958
  case "scale":
15951
15959
  dup(d.scale);
@@ -15987,6 +15995,21 @@ function parseMap(content) {
15987
15995
  case "no-legend":
15988
15996
  d.noLegend = true;
15989
15997
  break;
15998
+ case "no-insets":
15999
+ d.noInsets = true;
16000
+ break;
16001
+ case "relief":
16002
+ d.relief = true;
16003
+ break;
16004
+ case "muted":
16005
+ case "natural":
16006
+ if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
16007
+ pushWarning(
16008
+ line12,
16009
+ `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16010
+ );
16011
+ d.basemapStyle = key;
16012
+ break;
15990
16013
  case "subtitle":
15991
16014
  dup(d.subtitle);
15992
16015
  d.subtitle = value;
@@ -16064,14 +16087,14 @@ function parseMap(content) {
16064
16087
  line12
16065
16088
  );
16066
16089
  const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
16067
- let scoreNum;
16068
- const score = meta["score"];
16069
- if (score !== void 0) {
16070
- delete meta["score"];
16071
- scoreNum = Number(score);
16072
- if (!Number.isFinite(scoreNum)) {
16073
- pushError(line12, `score must be a number (got "${score}").`);
16074
- scoreNum = void 0;
16090
+ let valueNum;
16091
+ const value = meta["value"];
16092
+ if (value !== void 0) {
16093
+ delete meta["value"];
16094
+ valueNum = Number(value);
16095
+ if (!Number.isFinite(valueNum)) {
16096
+ pushError(line12, `value must be a number (got "${value}").`);
16097
+ valueNum = void 0;
16075
16098
  }
16076
16099
  }
16077
16100
  let regionName = split.name;
@@ -16089,7 +16112,8 @@ function parseMap(content) {
16089
16112
  lineNumber: line12
16090
16113
  };
16091
16114
  if (regionScope !== void 0) region.scope = regionScope;
16092
- if (scoreNum !== void 0) region.score = scoreNum;
16115
+ if (valueNum !== void 0) region.value = valueNum;
16116
+ if (split.color) region.color = split.color;
16093
16117
  regions.push(region);
16094
16118
  }
16095
16119
  function handlePoi(rest, line12, indent) {
@@ -16114,28 +16138,81 @@ function parseMap(content) {
16114
16138
  const poi = { pos, tags, meta, lineNumber: line12 };
16115
16139
  if (split.alias) poi.alias = split.alias;
16116
16140
  if (label !== void 0) poi.label = label;
16141
+ if (split.color) poi.color = split.color;
16117
16142
  pois.push(poi);
16118
16143
  open.poi = { poi, indent };
16119
16144
  }
16120
16145
  function handleRoute(rest, line12, indent) {
16121
- const meta = rest ? splitNameAndMeta(rest, registry(), aliasMap).meta : {};
16122
- const route = { stops: [], meta, lineNumber: line12 };
16146
+ const split = rest ? splitNameAndMeta(
16147
+ rest,
16148
+ registry(),
16149
+ aliasMap,
16150
+ void 0,
16151
+ diagnostics,
16152
+ line12
16153
+ ) : { name: "", meta: {}, alias: void 0 };
16154
+ const pos = parsePos(split.name, line12);
16155
+ if (!pos || pos.kind === "name" && !pos.name) {
16156
+ pushError(
16157
+ line12,
16158
+ "route requires an origin: `route <origin> [style: arc]`."
16159
+ );
16160
+ return;
16161
+ }
16162
+ const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
16163
+ const originLabel = meta["label"];
16164
+ const originValue = meta["value"];
16165
+ const style = meta["style"] === "arc" ? "arc" : "straight";
16166
+ const route = {
16167
+ origin: pos,
16168
+ ...split.alias !== void 0 && { originAlias: split.alias },
16169
+ ...originLabel !== void 0 && { originLabel },
16170
+ ...originValue !== void 0 && { originValue },
16171
+ originTags: tags,
16172
+ style,
16173
+ legs: [],
16174
+ lineNumber: line12
16175
+ };
16123
16176
  routes.push(route);
16124
16177
  open.route = { route, indent };
16125
16178
  }
16126
- function parseStop(trimmed, line12) {
16127
- const split = splitNameAndMeta(trimmed, registry(), aliasMap);
16128
- const ref = parsePos(split.name, line12) ?? {
16179
+ function parseLeg(trimmed, line12, headerStyle) {
16180
+ let arrowStyle = "straight";
16181
+ let label;
16182
+ let rest = trimmed;
16183
+ const m = trimmed.match(LEG_ARROW_RE);
16184
+ if (m) {
16185
+ const arr = classifyArrow(m[1], line12);
16186
+ arrowStyle = arr.style;
16187
+ label = arr.label;
16188
+ rest = m[2];
16189
+ }
16190
+ const split = splitNameAndMeta(
16191
+ rest,
16192
+ registry(),
16193
+ aliasMap,
16194
+ void 0,
16195
+ diagnostics,
16196
+ line12
16197
+ );
16198
+ const pos = parsePos(split.name, line12) ?? {
16129
16199
  kind: "name",
16130
16200
  name: split.name
16131
16201
  };
16132
- const stop = {
16133
- ref,
16134
- meta: split.meta,
16202
+ const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
16203
+ const value = meta["value"];
16204
+ const destLabel = meta["label"];
16205
+ const style = arrowStyle === "arc" || headerStyle === "arc" ? "arc" : "straight";
16206
+ return {
16207
+ ...label !== void 0 && { label },
16208
+ style,
16209
+ ...value !== void 0 && { value },
16210
+ dest: pos,
16211
+ ...split.alias !== void 0 && { destAlias: split.alias },
16212
+ ...destLabel !== void 0 && { destLabel },
16213
+ destTags: tags,
16135
16214
  lineNumber: line12
16136
16215
  };
16137
- if (split.alias) stop.alias = split.alias;
16138
- return stop;
16139
16216
  }
16140
16217
  function handleEdges(trimmed, line12) {
16141
16218
  const parts = trimmed.split(ARROW_SPLIT);
@@ -16237,7 +16314,7 @@ function partitionMeta(meta, tagGroupNames) {
16237
16314
  function poiName(pos) {
16238
16315
  return pos.kind === "name" ? pos.name : void 0;
16239
16316
  }
16240
- var COORD_RE, NUMERIC_LEAD_RE, SCOPE_RE, ARROW_SPLIT, HUB_RE, AT_RE, DIRECTIVE_SET;
16317
+ var COORD_RE, NUMERIC_LEAD_RE, SCOPE_RE, ARROW_SPLIT, HUB_RE, LEG_ARROW_RE, AT_RE, DIRECTIVE_SET;
16241
16318
  var init_parser12 = __esm({
16242
16319
  "src/map/parser.ts"() {
16243
16320
  "use strict";
@@ -16251,12 +16328,14 @@ var init_parser12 = __esm({
16251
16328
  SCOPE_RE = /^[A-Z]{2}(?:-[A-Z0-9]{1,3})?$/;
16252
16329
  ARROW_SPLIT = /\s+(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+/;
16253
16330
  HUB_RE = /^(->|~>)\s+(.+)$/;
16331
+ LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16254
16332
  AT_RE = /(^|[\s,])at\s*:/i;
16255
16333
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16256
16334
  "region",
16257
16335
  "projection",
16258
- "metric",
16259
- "size-metric",
16336
+ "region-metric",
16337
+ "poi-metric",
16338
+ "flow-metric",
16260
16339
  "scale",
16261
16340
  "region-labels",
16262
16341
  "poi-labels",
@@ -16264,6 +16343,8 @@ var init_parser12 = __esm({
16264
16343
  "default-state",
16265
16344
  "active-tag",
16266
16345
  "no-legend",
16346
+ "no-insets",
16347
+ "relief",
16267
16348
  "subtitle",
16268
16349
  "caption"
16269
16350
  ]);
@@ -45775,7 +45856,7 @@ var init_renderer15 = __esm({
45775
45856
 
45776
45857
  // src/map/geo.ts
45777
45858
  import { feature } from "topojson-client";
45778
- import { geoBounds } from "d3-geo";
45859
+ import { geoBounds, geoArea } from "d3-geo";
45779
45860
  function geomObject(topo) {
45780
45861
  const key = Object.keys(topo.objects)[0];
45781
45862
  return topo.objects[key];
@@ -45792,6 +45873,84 @@ function featureIndex(topo) {
45792
45873
  }
45793
45874
  return idx;
45794
45875
  }
45876
+ function decodeFeatures(topo) {
45877
+ return geomObject(topo).geometries.map((g) => {
45878
+ const f = feature(topo, g);
45879
+ return {
45880
+ type: "Feature",
45881
+ id: g.id,
45882
+ properties: g.properties,
45883
+ geometry: f.geometry
45884
+ };
45885
+ });
45886
+ }
45887
+ function pointInRing(lon, lat, ring) {
45888
+ let inside = false;
45889
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45890
+ const xi = ring[i][0];
45891
+ const yi = ring[i][1];
45892
+ const xj = ring[j][0];
45893
+ const yj = ring[j][1];
45894
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
45895
+ if (intersect) inside = !inside;
45896
+ }
45897
+ return inside;
45898
+ }
45899
+ function pointOnRingEdge(lon, lat, ring) {
45900
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45901
+ const xi = ring[i][0];
45902
+ const yi = ring[i][1];
45903
+ const xj = ring[j][0];
45904
+ const yj = ring[j][1];
45905
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
45906
+ continue;
45907
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
45908
+ continue;
45909
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
45910
+ if (Math.abs(cross) <= EDGE_EPS) return true;
45911
+ }
45912
+ return false;
45913
+ }
45914
+ function pointInGeometry(geometry, lon, lat) {
45915
+ const g = geometry;
45916
+ if (!g) return false;
45917
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
45918
+ for (const rings of polys) {
45919
+ if (!rings.length) continue;
45920
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
45921
+ if (!pointInRing(lon, lat, rings[0])) continue;
45922
+ let inHole = false;
45923
+ for (let h = 1; h < rings.length; h++) {
45924
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
45925
+ inHole = true;
45926
+ break;
45927
+ }
45928
+ }
45929
+ if (!inHole) return true;
45930
+ }
45931
+ return false;
45932
+ }
45933
+ function regionAt(lonLat, countries, states) {
45934
+ const lon = lonLat[0];
45935
+ const lat = lonLat[1];
45936
+ let country = null;
45937
+ for (const f of countries) {
45938
+ if (pointInGeometry(f.geometry, lon, lat)) {
45939
+ country = { iso: f.id, name: f.properties.name };
45940
+ break;
45941
+ }
45942
+ }
45943
+ let state = null;
45944
+ if (country?.iso === "US" && states) {
45945
+ for (const f of states) {
45946
+ if (pointInGeometry(f.geometry, lon, lat)) {
45947
+ state = { iso: f.id, name: f.properties.name };
45948
+ break;
45949
+ }
45950
+ }
45951
+ }
45952
+ return { country, state };
45953
+ }
45795
45954
  function featureBbox(topo, geomId) {
45796
45955
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45797
45956
  if (!geom) return null;
@@ -45803,6 +45962,74 @@ function featureBbox(topo, geomId) {
45803
45962
  [b[1][0], b[1][1]]
45804
45963
  ];
45805
45964
  }
45965
+ function explodePolygons(gj) {
45966
+ const g = gj.geometry ?? gj;
45967
+ const t = g.type;
45968
+ const coords = g.coordinates;
45969
+ if (t === "Polygon") {
45970
+ return [
45971
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
45972
+ ];
45973
+ }
45974
+ if (t === "MultiPolygon") {
45975
+ return coords.map((rings) => ({
45976
+ type: "Feature",
45977
+ geometry: { type: "Polygon", coordinates: rings }
45978
+ }));
45979
+ }
45980
+ return [];
45981
+ }
45982
+ function bboxGap(a, b) {
45983
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
45984
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
45985
+ return Math.max(lonGap, latGap);
45986
+ }
45987
+ function featureBboxPrimary(topo, geomId) {
45988
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45989
+ if (!geom) return null;
45990
+ const gj = feature(topo, geom);
45991
+ const parts = explodePolygons(gj);
45992
+ if (parts.length <= 1) return featureBbox(topo, geomId);
45993
+ const polys = parts.map((p) => {
45994
+ const b = geoBounds(p);
45995
+ if (!b || !Number.isFinite(b[0][0])) return null;
45996
+ const wraps = b[1][0] < b[0][0];
45997
+ const bbox = [
45998
+ [b[0][0], b[0][1]],
45999
+ [b[1][0], b[1][1]]
46000
+ ];
46001
+ return { bbox, area: geoArea(p), wraps };
46002
+ }).filter(
46003
+ (p) => p !== null
46004
+ );
46005
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
46006
+ return featureBbox(topo, geomId);
46007
+ const maxArea = Math.max(...polys.map((p) => p.area));
46008
+ const anchor = polys.find((p) => p.area === maxArea);
46009
+ const cluster = [
46010
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
46011
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
46012
+ ];
46013
+ const remaining = polys.filter((p) => p !== anchor);
46014
+ let added = true;
46015
+ while (added) {
46016
+ added = false;
46017
+ for (let i = remaining.length - 1; i >= 0; i--) {
46018
+ const p = remaining[i];
46019
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46020
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46021
+ if (near || large) {
46022
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46023
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46024
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46025
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46026
+ remaining.splice(i, 1);
46027
+ added = true;
46028
+ }
46029
+ }
46030
+ }
46031
+ return cluster;
46032
+ }
45806
46033
  function unionExtent(boxes, points) {
45807
46034
  const lats = [];
45808
46035
  const lons = [];
@@ -45841,11 +46068,14 @@ function unionLongitudes(lons) {
45841
46068
  }
45842
46069
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45843
46070
  }
45844
- var fold;
46071
+ var fold, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45845
46072
  var init_geo = __esm({
45846
46073
  "src/map/geo.ts"() {
45847
46074
  "use strict";
45848
46075
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46076
+ EDGE_EPS = 1e-9;
46077
+ DETACH_GAP_DEG = 10;
46078
+ DETACH_AREA_FRAC = 0.25;
45849
46079
  }
45850
46080
  });
45851
46081
 
@@ -45854,6 +46084,11 @@ var resolver_exports = {};
45854
46084
  __export(resolver_exports, {
45855
46085
  resolveMap: () => resolveMap
45856
46086
  });
46087
+ function usStateFromBareScope(scope) {
46088
+ if (!scope) return null;
46089
+ const up = scope.toUpperCase();
46090
+ return US_STATE_POSTAL.has(up) ? `US-${up}` : null;
46091
+ }
45857
46092
  function looksUS(lat, lon) {
45858
46093
  if (lat < 15 || lat > 72) return false;
45859
46094
  return lon >= -180 && lon <= -64 || lon >= 172;
@@ -45903,9 +46138,9 @@ function resolveMap(parsed, data) {
45903
46138
  const f = fold(r.name);
45904
46139
  return usStateIndex.has(f) && !countryIndex.has(f);
45905
46140
  }) || parsed.regions.some(
45906
- (r) => r.scope === "US" || r.scope?.startsWith("US-")
46141
+ (r) => r.scope === "US" || r.scope?.startsWith("US-") || usStateFromBareScope(r.scope) !== null
45907
46142
  ) || parsed.pois.some(
45908
- (p) => p.pos.kind === "name" && p.pos.scope?.startsWith("US-")
46143
+ (p) => p.pos.kind === "name" && (p.pos.scope?.startsWith("US-") || usStateFromBareScope(p.pos.scope) !== null)
45909
46144
  );
45910
46145
  const regions = [];
45911
46146
  const seenRegion = /* @__PURE__ */ new Map();
@@ -45944,12 +46179,12 @@ function resolveMap(parsed, data) {
45944
46179
  chosen = { ...inState, layer: "us-state" };
45945
46180
  } else {
45946
46181
  chosen = { ...inCountry, layer: "country" };
46182
+ warn(
46183
+ r.lineNumber,
46184
+ `"${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}").`,
46185
+ "W_MAP_REGION_AMBIGUOUS"
46186
+ );
45947
46187
  }
45948
- warn(
45949
- r.lineNumber,
45950
- `"${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}").`,
45951
- "W_MAP_REGION_AMBIGUOUS"
45952
- );
45953
46188
  } else if (inState) {
45954
46189
  chosen = { ...inState, layer: "us-state" };
45955
46190
  } else if (inCountry) {
@@ -45972,7 +46207,8 @@ function resolveMap(parsed, data) {
45972
46207
  iso: chosen.id,
45973
46208
  name: chosen.name,
45974
46209
  layer: chosen.layer,
45975
- ...r.score !== void 0 && { score: r.score },
46210
+ ...r.value !== void 0 && { value: r.value },
46211
+ ...r.color !== void 0 && { color: r.color },
45976
46212
  tags: r.tags,
45977
46213
  meta: r.meta,
45978
46214
  lineNumber: r.lineNumber
@@ -46030,9 +46266,10 @@ function resolveMap(parsed, data) {
46030
46266
  let cands = idxs.map((i) => data.gazetteer.cities[i]);
46031
46267
  const scopeUse = scope ?? scopeHint;
46032
46268
  if (scopeUse) {
46033
- const isSub = /^[A-Za-z]{2}-/.test(scopeUse);
46269
+ const bareState = usStateFromBareScope(scopeUse);
46270
+ const subScope = /^[A-Za-z]{2}-/.test(scopeUse) ? scopeUse : bareState;
46034
46271
  const filtered = cands.filter(
46035
- (c2) => isSub ? c2[5] === scopeUse : c2[2] === scopeUse
46272
+ (c2) => subScope ? c2[5] === subScope : c2[2] === scopeUse
46036
46273
  );
46037
46274
  if (filtered.length) cands = filtered;
46038
46275
  else if (scope) {
@@ -46113,6 +46350,7 @@ function resolveMap(parsed, data) {
46113
46350
  lat,
46114
46351
  lon,
46115
46352
  ...p.label !== void 0 && { label: p.label },
46353
+ ...p.color !== void 0 && { color: p.color },
46116
46354
  tags: p.tags,
46117
46355
  meta: p.meta,
46118
46356
  lineNumber: p.lineNumber
@@ -46161,33 +46399,89 @@ function resolveMap(parsed, data) {
46161
46399
  lineNumber: e.lineNumber
46162
46400
  });
46163
46401
  }
46164
- const routes = [];
46165
- for (const rt of parsed.routes) {
46166
- const stopIds = [];
46167
- for (const stop of rt.stops) {
46168
- let id;
46169
- if (stop.ref.kind === "coords") {
46170
- id = stop.alias ? fold(stop.alias) : `@${stop.ref.lat},${stop.ref.lon}`;
46171
- if (!looksUS(stop.ref.lat, stop.ref.lon)) anyNonUsPoi = true;
46172
- if (!registry.has(id)) {
46173
- const poi = {
46402
+ const resolveStop = (pos, alias, label, tags, sizeValue, line12) => {
46403
+ const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46404
+ if (pos.kind === "coords") {
46405
+ const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46406
+ if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46407
+ if (!registry.has(id)) {
46408
+ registerPoi(
46409
+ id,
46410
+ {
46174
46411
  id,
46175
- ...stop.alias !== void 0 && { name: stop.alias },
46176
- lat: stop.ref.lat,
46177
- lon: stop.ref.lon,
46178
- tags: {},
46179
- meta: stop.meta,
46180
- lineNumber: stop.lineNumber,
46181
- implicit: true
46182
- };
46183
- registerPoi(id, poi, stop.lineNumber);
46184
- }
46185
- } else {
46186
- id = stop.alias && registry.has(fold(stop.alias)) ? fold(stop.alias) : resolveEndpoint2(stop.ref.name, stop.lineNumber);
46412
+ ...alias !== void 0 && { name: alias },
46413
+ lat: pos.lat,
46414
+ lon: pos.lon,
46415
+ ...label !== void 0 && { label },
46416
+ tags,
46417
+ meta,
46418
+ lineNumber: line12
46419
+ },
46420
+ line12
46421
+ );
46187
46422
  }
46188
- if (id) stopIds.push(id);
46423
+ return id;
46424
+ }
46425
+ const f = fold(pos.name);
46426
+ if (registry.has(f)) return f;
46427
+ const aliased = declaredByName.get(f);
46428
+ if (aliased) return aliased;
46429
+ const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46430
+ if (got.kind !== "ok") return null;
46431
+ noteCountry(got.iso);
46432
+ registerPoi(
46433
+ f,
46434
+ {
46435
+ id: f,
46436
+ name: pos.name,
46437
+ lat: got.lat,
46438
+ lon: got.lon,
46439
+ ...label !== void 0 && { label },
46440
+ tags,
46441
+ meta,
46442
+ lineNumber: line12
46443
+ },
46444
+ line12
46445
+ );
46446
+ return f;
46447
+ };
46448
+ const routes = [];
46449
+ for (const rt of parsed.routes) {
46450
+ const originId = resolveStop(
46451
+ rt.origin,
46452
+ rt.originAlias,
46453
+ rt.originLabel,
46454
+ rt.originTags,
46455
+ rt.originValue,
46456
+ rt.lineNumber
46457
+ );
46458
+ if (!originId) continue;
46459
+ const stopIds = [originId];
46460
+ const legs = [];
46461
+ let prevId = originId;
46462
+ for (const leg of rt.legs) {
46463
+ const destId = resolveStop(
46464
+ leg.dest,
46465
+ leg.destAlias,
46466
+ leg.destLabel,
46467
+ leg.destTags,
46468
+ void 0,
46469
+ // a leg's `value:` is leg thickness, not the dest's size
46470
+ leg.lineNumber
46471
+ );
46472
+ if (!destId) continue;
46473
+ legs.push({
46474
+ fromId: prevId,
46475
+ toId: destId,
46476
+ ...leg.label !== void 0 && { label: leg.label },
46477
+ style: leg.style,
46478
+ ...leg.value !== void 0 && { value: leg.value },
46479
+ lineNumber: leg.lineNumber
46480
+ });
46481
+ if (!stopIds.includes(destId)) stopIds.push(destId);
46482
+ prevId = destId;
46189
46483
  }
46190
- routes.push({ stopIds, meta: rt.meta, lineNumber: rt.lineNumber });
46484
+ routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46191
46485
  }
46192
46486
  const subdivisions = [];
46193
46487
  if (usSubdivisionReferenced || parsed.directives.region === "us-states")
@@ -46199,7 +46493,7 @@ function resolveMap(parsed, data) {
46199
46493
  }
46200
46494
  for (const r of regions) {
46201
46495
  if (r.layer === "country") {
46202
- const bb = featureBbox(data.worldCoarse, r.iso);
46496
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46203
46497
  if (bb) regionBoxes.push(bb);
46204
46498
  }
46205
46499
  }
@@ -46213,6 +46507,7 @@ function resolveMap(parsed, data) {
46213
46507
  const lonSpan = extent2[1][0] - extent2[0][0];
46214
46508
  const latSpan = extent2[1][1] - extent2[0][1];
46215
46509
  const span = Math.max(lonSpan, latSpan);
46510
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46216
46511
  const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46217
46512
  let projection;
46218
46513
  const override = parsed.directives.projection;
@@ -46220,12 +46515,10 @@ function resolveMap(parsed, data) {
46220
46515
  projection = override;
46221
46516
  } else if (usDominant) {
46222
46517
  projection = "albers-usa";
46223
- } else if (span > WORLD_SPAN) {
46518
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46224
46519
  projection = "equirectangular";
46225
- } else if (span < MERCATOR_MAX_SPAN) {
46226
- projection = "mercator";
46227
46520
  } else {
46228
- projection = "equirectangular";
46521
+ projection = "mercator";
46229
46522
  }
46230
46523
  if (lonSpan >= 180) {
46231
46524
  extent2 = [
@@ -46279,14 +46572,14 @@ function firstError(diags) {
46279
46572
  const e = diags.find((d) => d.severity === "error");
46280
46573
  return e ? formatDgmoError(e) : null;
46281
46574
  }
46282
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES;
46575
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46283
46576
  var init_resolver2 = __esm({
46284
46577
  "src/map/resolver.ts"() {
46285
46578
  "use strict";
46286
46579
  init_diagnostics();
46287
46580
  init_geo();
46288
46581
  WORLD_SPAN = 90;
46289
- MERCATOR_MAX_SPAN = 25;
46582
+ MERCATOR_MAX_LAT = 80;
46290
46583
  PAD_FRACTION = 0.05;
46291
46584
  WORLD_LAT_SOUTH = -58;
46292
46585
  WORLD_LAT_NORTH = 78;
@@ -46311,114 +46604,59 @@ var init_resolver2 = __esm({
46311
46604
  "north macedonia": "macedonia",
46312
46605
  "czech republic": "czechia"
46313
46606
  };
46314
- }
46315
- });
46316
-
46317
- // src/map/load-data.ts
46318
- var load_data_exports = {};
46319
- __export(load_data_exports, {
46320
- loadMapData: () => loadMapData
46321
- });
46322
- async function loadNodeBuiltins() {
46323
- const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
46324
- import("fs/promises"),
46325
- import("url"),
46326
- import("path")
46327
- ]);
46328
- return { readFile, fileURLToPath, dirname: dirname2, resolve };
46329
- }
46330
- async function readJson(nb, dir, name) {
46331
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46332
- }
46333
- async function firstExistingDir(nb, baseDir) {
46334
- for (const rel of CANDIDATE_DIRS) {
46335
- const dir = nb.resolve(baseDir, rel);
46336
- try {
46337
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46338
- return dir;
46339
- } catch {
46340
- }
46341
- }
46342
- throw new Error(
46343
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46344
- );
46345
- }
46346
- function validate(data) {
46347
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46348
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46349
- throw new Error("map data assets are malformed (failed shape validation)");
46350
- }
46351
- return data;
46352
- }
46353
- function moduleBaseDir(nb) {
46354
- try {
46355
- const url = import.meta.url;
46356
- if (url) return nb.dirname(nb.fileURLToPath(url));
46357
- } catch {
46358
- }
46359
- if (typeof __dirname !== "undefined") return __dirname;
46360
- return process.cwd();
46361
- }
46362
- function loadMapData() {
46363
- cache ??= (async () => {
46364
- const nb = await loadNodeBuiltins();
46365
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46366
- const [
46367
- worldCoarse,
46368
- worldDetail,
46369
- usStates,
46370
- lakes,
46371
- rivers,
46372
- naLand,
46373
- naLakes,
46374
- gazetteer
46375
- ] = await Promise.all([
46376
- readJson(nb, dir, FILES.worldCoarse),
46377
- readJson(nb, dir, FILES.worldDetail),
46378
- readJson(nb, dir, FILES.usStates),
46379
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46380
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46381
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46382
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46383
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46384
- readJson(nb, dir, FILES.gazetteer)
46607
+ US_STATE_POSTAL = /* @__PURE__ */ new Set([
46608
+ "AL",
46609
+ "AK",
46610
+ "AZ",
46611
+ "AR",
46612
+ "CA",
46613
+ "CO",
46614
+ "CT",
46615
+ "DE",
46616
+ "FL",
46617
+ "GA",
46618
+ "HI",
46619
+ "ID",
46620
+ "IL",
46621
+ "IN",
46622
+ "IA",
46623
+ "KS",
46624
+ "KY",
46625
+ "LA",
46626
+ "ME",
46627
+ "MD",
46628
+ "MA",
46629
+ "MI",
46630
+ "MN",
46631
+ "MS",
46632
+ "MO",
46633
+ "MT",
46634
+ "NE",
46635
+ "NV",
46636
+ "NH",
46637
+ "NJ",
46638
+ "NM",
46639
+ "NY",
46640
+ "NC",
46641
+ "ND",
46642
+ "OH",
46643
+ "OK",
46644
+ "OR",
46645
+ "PA",
46646
+ "RI",
46647
+ "SC",
46648
+ "SD",
46649
+ "TN",
46650
+ "TX",
46651
+ "UT",
46652
+ "VT",
46653
+ "VA",
46654
+ "WA",
46655
+ "WV",
46656
+ "WI",
46657
+ "WY",
46658
+ "DC"
46385
46659
  ]);
46386
- return validate({
46387
- worldCoarse,
46388
- worldDetail,
46389
- usStates,
46390
- gazetteer,
46391
- ...lakes && { lakes },
46392
- ...rivers && { rivers },
46393
- ...naLand && { naLand },
46394
- ...naLakes && { naLakes }
46395
- });
46396
- })().catch((e) => {
46397
- cache = void 0;
46398
- throw e;
46399
- });
46400
- return cache;
46401
- }
46402
- var FILES, CANDIDATE_DIRS, cache;
46403
- var init_load_data = __esm({
46404
- "src/map/load-data.ts"() {
46405
- "use strict";
46406
- FILES = {
46407
- worldCoarse: "world-coarse.json",
46408
- worldDetail: "world-detail.json",
46409
- usStates: "us-states.json",
46410
- lakes: "lakes.json",
46411
- rivers: "rivers.json",
46412
- naLand: "na-land.json",
46413
- naLakes: "na-lakes.json",
46414
- gazetteer: "gazetteer.json"
46415
- };
46416
- CANDIDATE_DIRS = [
46417
- "./data",
46418
- "./map-data",
46419
- "../map-data",
46420
- "../src/map/data"
46421
- ];
46422
46660
  }
46423
46661
  });
46424
46662
 
@@ -46458,10 +46696,14 @@ function projectionFor(family) {
46458
46696
  return geoEquirectangular();
46459
46697
  }
46460
46698
  }
46461
- function mapBackgroundColor(palette) {
46462
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
46699
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
46700
+ return mix(
46701
+ palette.colors.blue,
46702
+ palette.bg,
46703
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
46704
+ );
46463
46705
  }
46464
- function mapNeutralLandColor(palette, isDark) {
46706
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46465
46707
  return mix(
46466
46708
  palette.colors.green,
46467
46709
  palette.bg,
@@ -46487,28 +46729,19 @@ function layoutMap(resolved, data, size, opts) {
46487
46729
  }
46488
46730
  }
46489
46731
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
46490
- const landTint = isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT;
46491
- const neutralFill = mix(palette.colors.green, palette.bg, landTint);
46492
- const water = mapBackgroundColor(palette);
46493
46732
  const usContext = usLayer !== null;
46494
- const foreignFill = mix(
46495
- palette.colors.gray,
46496
- palette.bg,
46497
- isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46498
- );
46499
46733
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46500
- const scores = resolved.regions.filter((r) => r.score !== void 0).map((r) => r.score);
46734
+ const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46501
46735
  const scaleOverride = resolved.directives.scale;
46502
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...scores);
46503
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...scores);
46504
- const rampHue = palette.colors.red;
46505
- const hasRamp = scores.length > 0;
46506
- const SCORE_NAME = hasRamp ? resolved.directives.metric?.trim() || "Score" : null;
46736
+ const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46737
+ const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46738
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46739
+ const hasRamp = values.length > 0;
46740
+ const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46507
46741
  const matchColorGroup = (v) => {
46508
46742
  const lv = v.trim().toLowerCase();
46509
46743
  if (lv === "none") return null;
46510
- if (SCORE_NAME && (lv === "score" || lv === SCORE_NAME.toLowerCase()))
46511
- return SCORE_NAME;
46744
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
46512
46745
  const tg = resolved.tagGroups.find((g) => g.name.toLowerCase() === lv);
46513
46746
  return tg ? tg.name : v;
46514
46747
  };
@@ -46519,11 +46752,20 @@ function layoutMap(resolved, data, size, opts) {
46519
46752
  } else if (resolved.directives.activeTag !== void 0) {
46520
46753
  activeGroup = matchColorGroup(resolved.directives.activeTag);
46521
46754
  } else {
46522
- activeGroup = SCORE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46755
+ activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46523
46756
  }
46524
- const activeIsScore = SCORE_NAME !== null && activeGroup === SCORE_NAME;
46757
+ const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46758
+ const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
46759
+ const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46760
+ const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46761
+ const lakeStroke = mix(regionStroke, water, 45);
46762
+ const foreignFill = mix(
46763
+ palette.colors.gray,
46764
+ palette.bg,
46765
+ mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46766
+ );
46525
46767
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46526
- const fillForScore = (s) => {
46768
+ const fillForValue = (s) => {
46527
46769
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
46528
46770
  const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
46529
46771
  return mix(rampHue, rampBase, pct);
@@ -46546,9 +46788,16 @@ function layoutMap(resolved, data, size, opts) {
46546
46788
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46547
46789
  );
46548
46790
  };
46791
+ const directFill = (name) => {
46792
+ const hex = name ? resolveColor(name, palette) : null;
46793
+ if (!hex) return null;
46794
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
46795
+ };
46549
46796
  const regionFill = (r) => {
46797
+ const direct = directFill(r.color);
46798
+ if (direct) return direct;
46550
46799
  if (activeIsScore) {
46551
- return r.score !== void 0 ? fillForScore(r.score) : neutralFill;
46800
+ return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46552
46801
  }
46553
46802
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46554
46803
  };
@@ -46600,6 +46849,7 @@ function layoutMap(resolved, data, size, opts) {
46600
46849
  const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46601
46850
  let path;
46602
46851
  let project;
46852
+ let stretchParams = null;
46603
46853
  if (fitIsGlobal) {
46604
46854
  const cb = geoPath(projection).bounds(fitTarget);
46605
46855
  const bx0 = cb[0][0];
@@ -46610,6 +46860,7 @@ function layoutMap(resolved, data, size, opts) {
46610
46860
  const oy = fitBox[0][1];
46611
46861
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46612
46862
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
46863
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46613
46864
  const stretch = (x, y) => [
46614
46865
  ox + (x - bx0) * sx,
46615
46866
  oy + (y - by0) * sy
@@ -46641,7 +46892,7 @@ function layoutMap(resolved, data, size, opts) {
46641
46892
  const insets = [];
46642
46893
  const insetRegions = [];
46643
46894
  const insetLabelSeeds = [];
46644
- if (resolved.projection === "albers-usa" && usLayer) {
46895
+ if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
46645
46896
  const PAD = 8;
46646
46897
  const GAP = 12;
46647
46898
  const yB = height - FIT_PAD;
@@ -46672,38 +46923,14 @@ function layoutMap(resolved, data, size, opts) {
46672
46923
  }
46673
46924
  return y;
46674
46925
  };
46675
- const coastTop = (x0, xr) => {
46926
+ const coastFloor = (x0, xr) => {
46676
46927
  const n = 24;
46677
- const pts = [];
46678
46928
  let maxY = -Infinity;
46679
46929
  for (let i = 0; i <= n; i++) {
46680
- const x = x0 + (xr - x0) * i / n;
46681
- const y = at(x);
46682
- if (y > -Infinity) {
46683
- pts.push([x, y]);
46684
- if (y > maxY) maxY = y;
46685
- }
46686
- }
46687
- if (pts.length === 0) return () => yB - height * 0.42;
46688
- let m = 0;
46689
- if (pts.length >= 2) {
46690
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46691
- for (const [x, y] of pts) {
46692
- sx += x;
46693
- sy += y;
46694
- sxx += x * x;
46695
- sxy += x * y;
46696
- }
46697
- const den = pts.length * sxx - sx * sx;
46698
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46699
- }
46700
- m = Math.max(-0.35, Math.min(0.35, m));
46701
- let c = -Infinity;
46702
- for (const [x, y] of pts) {
46703
- const need = y - m * x + GAP;
46704
- if (need > c) c = need;
46705
- }
46706
- return (x) => m * x + c;
46930
+ const y = at(x0 + (xr - x0) * i / n);
46931
+ if (y > maxY) maxY = y;
46932
+ }
46933
+ return maxY;
46707
46934
  };
46708
46935
  const placeInset = (iso, proj, boxX, iwReq) => {
46709
46936
  const f = usLayer.get(iso);
@@ -46712,19 +46939,15 @@ function layoutMap(resolved, data, size, opts) {
46712
46939
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46713
46940
  if (iw < 24) return boxX;
46714
46941
  const xr = x0 + iw + 2 * PAD;
46715
- const top = coastTop(x0, xr);
46716
- const yL = top(x0);
46717
- const yR = top(xr);
46942
+ const floor = coastFloor(x0, xr);
46943
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46718
46944
  proj.fitWidth(iw, f);
46719
46945
  const bb = geoPath(proj).bounds(f);
46720
46946
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46721
46947
  const needH = sh + 2 * PAD;
46722
- let topFit = Math.max(yL, yR);
46948
+ let topFit = topGuess;
46723
46949
  const bottom = Math.min(topFit + needH, yB);
46724
46950
  if (bottom - topFit < needH) topFit = bottom - needH;
46725
- const lift = topFit - Math.max(yL, yR);
46726
- const topL = yL + lift;
46727
- const topR = yR + lift;
46728
46951
  proj.fitExtent(
46729
46952
  [
46730
46953
  [x0 + PAD, topFit + PAD],
@@ -46743,15 +46966,18 @@ function layoutMap(resolved, data, size, opts) {
46743
46966
  }
46744
46967
  insets.push({
46745
46968
  x: x0,
46746
- y: Math.min(topL, topR),
46969
+ y: topFit,
46747
46970
  w: xr - x0,
46748
- h: bottom - Math.min(topL, topR),
46971
+ h: bottom - topFit,
46749
46972
  points: [
46750
- [x0, topL],
46751
- [xr, topR],
46973
+ [x0, topFit],
46974
+ [xr, topFit],
46752
46975
  [xr, bottom],
46753
46976
  [x0, bottom]
46754
- ]
46977
+ ],
46978
+ // The FITTED inset projection (just fit to this box) — captured so the
46979
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
46980
+ projection: proj
46755
46981
  });
46756
46982
  insetRegions.push({
46757
46983
  id: iso,
@@ -46760,7 +46986,7 @@ function layoutMap(resolved, data, size, opts) {
46760
46986
  stroke: regionStroke,
46761
46987
  lineNumber,
46762
46988
  layer: "us-state",
46763
- ...r?.score !== void 0 && { score: r.score },
46989
+ ...r?.value !== void 0 && { value: r.value },
46764
46990
  ...r && Object.keys(r.tags).length > 0 && { tags: r.tags }
46765
46991
  });
46766
46992
  const ctr = geoPath(proj).centroid(f);
@@ -46903,7 +47129,7 @@ function layoutMap(resolved, data, size, opts) {
46903
47129
  lineNumber,
46904
47130
  layer,
46905
47131
  ...label !== void 0 && { label },
46906
- ...isThisLayer && r.score !== void 0 && { score: r.score },
47132
+ ...isThisLayer && r.value !== void 0 && { value: r.value },
46907
47133
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
46908
47134
  });
46909
47135
  }
@@ -46921,13 +47147,40 @@ function layoutMap(resolved, data, size, opts) {
46921
47147
  id: "lake",
46922
47148
  d,
46923
47149
  fill: water,
46924
- stroke: "none",
47150
+ stroke: lakeStroke,
46925
47151
  lineNumber: -1,
46926
47152
  layer: "base"
46927
47153
  });
46928
47154
  }
46929
47155
  }
46930
- const riverColor = water;
47156
+ const relief = [];
47157
+ let reliefHatch = null;
47158
+ if (resolved.directives.relief === true && data.mountainRanges) {
47159
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
47160
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
47161
+ if (!viewF) continue;
47162
+ const area2 = path.area(viewF);
47163
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
47164
+ const box = path.bounds(viewF);
47165
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
47166
+ continue;
47167
+ const d = path(viewF) ?? "";
47168
+ if (!d) continue;
47169
+ relief.push({ d });
47170
+ }
47171
+ if (relief.length) {
47172
+ const darkTone = isDark ? palette.bg : palette.text;
47173
+ const lightTone = isDark ? palette.text : palette.bg;
47174
+ const landLum = relativeLuminance(neutralFill);
47175
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
47176
+ reliefHatch = {
47177
+ color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
47178
+ spacing: RELIEF_HATCH_SPACING,
47179
+ width: RELIEF_HATCH_WIDTH
47180
+ };
47181
+ }
47182
+ }
47183
+ const riverColor = mix(water, regionStroke, 16);
46931
47184
  const rivers = [];
46932
47185
  if (data.rivers) {
46933
47186
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46938,16 +47191,19 @@ function layoutMap(resolved, data, size, opts) {
46938
47191
  rivers.push({ d, color: riverColor, width: RIVER_WIDTH });
46939
47192
  }
46940
47193
  }
46941
- const sizeVals = resolved.pois.map((p) => Number(p.meta["size"])).filter((n) => Number.isFinite(n) && n > 0);
47194
+ const sizeVals = resolved.pois.map((p) => Number(p.meta["value"])).filter((n) => Number.isFinite(n) && n > 0);
46942
47195
  const sizeMin = sizeVals.length ? Math.min(...sizeVals) : 0;
46943
47196
  const sizeMax = sizeVals.length ? Math.max(...sizeVals) : 0;
46944
47197
  const radiusFor = (p) => {
46945
- const v = Number(p.meta["size"]);
47198
+ const v = Number(p.meta["value"]);
46946
47199
  if (!Number.isFinite(v) || v <= 0 || sizeMax <= 0) return R_DEFAULT;
46947
47200
  const t = sizeMax > sizeMin ? (Math.sqrt(v) - Math.sqrt(sizeMin)) / (Math.sqrt(sizeMax) - Math.sqrt(sizeMin)) : 1;
46948
47201
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46949
47202
  };
46950
47203
  const poiFill = (p) => {
47204
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
47205
+ if (directHex)
47206
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46951
47207
  for (const group of resolved.tagGroups) {
46952
47208
  const val = p.tags[group.name.toLowerCase()];
46953
47209
  if (!val) continue;
@@ -47009,7 +47265,8 @@ function layoutMap(resolved, data, size, opts) {
47009
47265
  lineNumber: e.p.lineNumber,
47010
47266
  implicit: !!e.p.implicit,
47011
47267
  isOrigin: originIds.has(e.p.id),
47012
- ...num !== void 0 && { routeNumber: num }
47268
+ ...num !== void 0 && { routeNumber: num },
47269
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
47013
47270
  });
47014
47271
  });
47015
47272
  }
@@ -47045,26 +47302,40 @@ function layoutMap(resolved, data, size, opts) {
47045
47302
  const by = b.cy - (b.cy - py) / tb * trimB;
47046
47303
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
47047
47304
  };
47305
+ const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
47306
+ const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
47307
+ const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
47308
+ const routeWidthFor = (v) => {
47309
+ if (!Number.isFinite(v) || v <= 0 || rlMax <= 0) return W_MIN;
47310
+ const t = rlMax > rlMin ? (v - rlMin) / (rlMax - rlMin) : 1;
47311
+ return W_MIN + t * (W_MAX - W_MIN);
47312
+ };
47048
47313
  for (const rt of resolved.routes) {
47049
- const curved = rt.meta["style"] === "arc";
47050
- for (let i = 1; i < rt.stopIds.length; i++) {
47051
- const a = poiScreen.get(rt.stopIds[i - 1]);
47052
- const b = poiScreen.get(rt.stopIds[i]);
47314
+ for (const leg of rt.legs) {
47315
+ const a = poiScreen.get(leg.fromId);
47316
+ const b = poiScreen.get(leg.toId);
47053
47317
  if (!a || !b) continue;
47318
+ const mx = (a.cx + b.cx) / 2;
47319
+ const my = (a.cy + b.cy) / 2;
47054
47320
  legs.push({
47055
- d: legPath(a, b, curved, 0),
47056
- width: W_MIN,
47321
+ d: legPath(a, b, leg.style === "arc", 0),
47322
+ width: routeWidthFor(Number(leg.value)),
47057
47323
  color: mix(palette.text, palette.bg, 72),
47058
47324
  arrow: true,
47059
- lineNumber: rt.lineNumber
47325
+ lineNumber: leg.lineNumber,
47326
+ ...leg.label !== void 0 && {
47327
+ label: leg.label,
47328
+ labelX: mx,
47329
+ labelY: my - 4
47330
+ }
47060
47331
  });
47061
47332
  }
47062
47333
  }
47063
- const weightVals = resolved.edges.map((e) => Number(e.meta["weight"])).filter((n) => Number.isFinite(n) && n > 0);
47334
+ const weightVals = resolved.edges.map((e) => Number(e.meta["value"])).filter((n) => Number.isFinite(n) && n > 0);
47064
47335
  const wMin = weightVals.length ? Math.min(...weightVals) : 0;
47065
47336
  const wMax = weightVals.length ? Math.max(...weightVals) : 0;
47066
47337
  const widthFor = (e) => {
47067
- const v = Number(e.meta["weight"]);
47338
+ const v = Number(e.meta["value"]);
47068
47339
  if (!Number.isFinite(v) || v <= 0 || wMax <= 0) return W_MIN;
47069
47340
  const t = wMax > wMin ? (v - wMin) / (wMax - wMin) : 1;
47070
47341
  return W_MIN + t * (W_MAX - W_MIN);
@@ -47327,8 +47598,8 @@ function layoutMap(resolved, data, size, opts) {
47327
47598
  activeGroup,
47328
47599
  ...hasRamp && {
47329
47600
  ramp: {
47330
- ...resolved.directives.metric !== void 0 && {
47331
- metric: resolved.directives.metric
47601
+ ...resolved.directives.regionMetric !== void 0 && {
47602
+ metric: resolved.directives.regionMetric
47332
47603
  },
47333
47604
  min: rampMin,
47334
47605
  max: rampMax,
@@ -47348,19 +47619,24 @@ function layoutMap(resolved, data, size, opts) {
47348
47619
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47349
47620
  regions,
47350
47621
  rivers,
47622
+ relief,
47623
+ reliefHatch,
47351
47624
  legs,
47352
47625
  pois,
47353
47626
  labels,
47354
47627
  legend,
47355
47628
  insets,
47356
- insetRegions
47629
+ insetRegions,
47630
+ projection,
47631
+ stretch: stretchParams
47357
47632
  };
47358
47633
  }
47359
- var 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;
47634
+ var 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;
47360
47635
  var init_layout15 = __esm({
47361
47636
  "src/map/layout.ts"() {
47362
47637
  "use strict";
47363
47638
  init_color_utils();
47639
+ init_colors();
47364
47640
  init_label_layout();
47365
47641
  init_legend_constants();
47366
47642
  init_title_constants();
@@ -47373,14 +47649,22 @@ var init_layout15 = __esm({
47373
47649
  W_MAX = 8;
47374
47650
  FONT = 11;
47375
47651
  COLO_EPS = 1.5;
47376
- LAND_TINT_LIGHT = 58;
47377
- LAND_TINT_DARK = 75;
47652
+ LAND_TINT_LIGHT = 12;
47653
+ LAND_TINT_DARK = 24;
47378
47654
  TAG_TINT_LIGHT = 60;
47379
47655
  TAG_TINT_DARK = 68;
47380
- WATER_TINT = 55;
47656
+ WATER_TINT_LIGHT = 13;
47657
+ WATER_TINT_DARK = 14;
47381
47658
  RIVER_WIDTH = 1.3;
47659
+ RELIEF_MIN_AREA = 12;
47660
+ RELIEF_MIN_DIM = 2;
47661
+ RELIEF_HATCH_SPACING = 3;
47662
+ RELIEF_HATCH_WIDTH = 0.25;
47663
+ RELIEF_HATCH_STRENGTH = 32;
47382
47664
  FOREIGN_TINT_LIGHT = 30;
47383
47665
  FOREIGN_TINT_DARK = 62;
47666
+ MUTED_FOREIGN_LIGHT = 28;
47667
+ MUTED_FOREIGN_DARK = 16;
47384
47668
  COLO_R = 9;
47385
47669
  GOLDEN_ANGLE = 2.399963229728653;
47386
47670
  FAN_STEP = 16;
@@ -47435,7 +47719,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47435
47719
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
47436
47720
  if (r.layer !== "base") {
47437
47721
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47438
- if (r.score !== void 0) p.attr("data-score", r.score);
47722
+ if (r.value !== void 0) p.attr("data-value", r.value);
47439
47723
  if (r.tags) {
47440
47724
  for (const [group, value] of Object.entries(r.tags)) {
47441
47725
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47453,6 +47737,20 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47453
47737
  }
47454
47738
  };
47455
47739
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
47740
+ if (layout.relief.length && layout.reliefHatch) {
47741
+ const h = layout.reliefHatch;
47742
+ const rangeClipId = "dgmo-relief-clip";
47743
+ const landClipId = "dgmo-relief-land";
47744
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
47745
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
47746
+ const landClip = defs.append("clipPath").attr("id", landClipId);
47747
+ for (const r of layout.regions)
47748
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47749
+ 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");
47750
+ for (let y = h.spacing; y < height; y += h.spacing) {
47751
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47752
+ }
47753
+ }
47456
47754
  if (layout.rivers.length) {
47457
47755
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47458
47756
  for (const r of layout.rivers) {
@@ -47496,6 +47794,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47496
47794
  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);
47497
47795
  }
47498
47796
  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);
47797
+ if (poi.tags) {
47798
+ for (const [group, value] of Object.entries(poi.tags)) {
47799
+ c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
47800
+ }
47801
+ }
47499
47802
  if (onClickItem) {
47500
47803
  c.style("cursor", "pointer").on(
47501
47804
  "click",
@@ -47545,7 +47848,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47545
47848
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
47546
47849
  const ramp = layout.legend.ramp;
47547
47850
  const scoreGroup = ramp ? {
47548
- name: ramp.metric?.trim() || "Score",
47851
+ name: ramp.metric?.trim() || "Value",
47549
47852
  entries: [],
47550
47853
  gradient: {
47551
47854
  min: ramp.min,
@@ -47572,7 +47875,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47572
47875
  }
47573
47876
  }
47574
47877
  if (layout.title) {
47575
- 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);
47878
+ 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);
47576
47879
  }
47577
47880
  if (layout.subtitle) {
47578
47881
  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);
@@ -47604,6 +47907,120 @@ var init_renderer16 = __esm({
47604
47907
  }
47605
47908
  });
47606
47909
 
47910
+ // src/map/load-data.ts
47911
+ var load_data_exports = {};
47912
+ __export(load_data_exports, {
47913
+ loadMapData: () => loadMapData
47914
+ });
47915
+ async function loadNodeBuiltins() {
47916
+ const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
47917
+ import("fs/promises"),
47918
+ import("url"),
47919
+ import("path")
47920
+ ]);
47921
+ return { readFile, fileURLToPath, dirname: dirname2, resolve };
47922
+ }
47923
+ async function readJson(nb, dir, name) {
47924
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
47925
+ }
47926
+ async function firstExistingDir(nb, baseDir) {
47927
+ for (const rel of CANDIDATE_DIRS) {
47928
+ const dir = nb.resolve(baseDir, rel);
47929
+ try {
47930
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
47931
+ return dir;
47932
+ } catch {
47933
+ }
47934
+ }
47935
+ throw new Error(
47936
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
47937
+ );
47938
+ }
47939
+ function validate(data) {
47940
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
47941
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
47942
+ throw new Error("map data assets are malformed (failed shape validation)");
47943
+ }
47944
+ return data;
47945
+ }
47946
+ function moduleBaseDir(nb) {
47947
+ try {
47948
+ const url = import.meta.url;
47949
+ if (url) return nb.dirname(nb.fileURLToPath(url));
47950
+ } catch {
47951
+ }
47952
+ if (typeof __dirname !== "undefined") return __dirname;
47953
+ return process.cwd();
47954
+ }
47955
+ function loadMapData() {
47956
+ cache ??= (async () => {
47957
+ const nb = await loadNodeBuiltins();
47958
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
47959
+ const [
47960
+ worldCoarse,
47961
+ worldDetail,
47962
+ usStates,
47963
+ lakes,
47964
+ rivers,
47965
+ mountainRanges,
47966
+ naLand,
47967
+ naLakes,
47968
+ gazetteer
47969
+ ] = await Promise.all([
47970
+ readJson(nb, dir, FILES.worldCoarse),
47971
+ readJson(nb, dir, FILES.worldDetail),
47972
+ readJson(nb, dir, FILES.usStates),
47973
+ // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
47974
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
47975
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
47976
+ readJson(nb, dir, FILES.mountainRanges).catch(
47977
+ () => void 0
47978
+ ),
47979
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
47980
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
47981
+ readJson(nb, dir, FILES.gazetteer)
47982
+ ]);
47983
+ return validate({
47984
+ worldCoarse,
47985
+ worldDetail,
47986
+ usStates,
47987
+ gazetteer,
47988
+ ...lakes && { lakes },
47989
+ ...rivers && { rivers },
47990
+ ...mountainRanges && { mountainRanges },
47991
+ ...naLand && { naLand },
47992
+ ...naLakes && { naLakes }
47993
+ });
47994
+ })().catch((e) => {
47995
+ cache = void 0;
47996
+ throw e;
47997
+ });
47998
+ return cache;
47999
+ }
48000
+ var FILES, CANDIDATE_DIRS, cache;
48001
+ var init_load_data = __esm({
48002
+ "src/map/load-data.ts"() {
48003
+ "use strict";
48004
+ FILES = {
48005
+ worldCoarse: "world-coarse.json",
48006
+ worldDetail: "world-detail.json",
48007
+ usStates: "us-states.json",
48008
+ lakes: "lakes.json",
48009
+ rivers: "rivers.json",
48010
+ mountainRanges: "mountain-ranges.json",
48011
+ naLand: "na-land.json",
48012
+ naLakes: "na-lakes.json",
48013
+ gazetteer: "gazetteer.json"
48014
+ };
48015
+ CANDIDATE_DIRS = [
48016
+ "./data",
48017
+ "./map-data",
48018
+ "../map-data",
48019
+ "../src/map/data"
48020
+ ];
48021
+ }
48022
+ });
48023
+
47607
48024
  // src/pyramid/renderer.ts
47608
48025
  var renderer_exports17 = {};
47609
48026
  __export(renderer_exports17, {
@@ -55788,15 +56205,17 @@ async function renderForExport(content, theme, palette, viewState, options) {
55788
56205
  if (detectedType === "map") {
55789
56206
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55790
56207
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55791
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55792
56208
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
55793
56209
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55794
56210
  const mapParsed = parseMap2(content);
55795
- let mapData;
55796
- try {
55797
- mapData = await loadMapData2();
55798
- } catch {
55799
- return "";
56211
+ let mapData = options?.mapData;
56212
+ if (!mapData) {
56213
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
56214
+ try {
56215
+ mapData = await loadMapData2();
56216
+ } catch {
56217
+ return "";
56218
+ }
55800
56219
  }
55801
56220
  const mapResolved = resolveMap2(mapParsed, mapData);
55802
56221
  const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
@@ -57090,6 +57509,160 @@ init_load_data();
57090
57509
  init_layout15();
57091
57510
  init_renderer16();
57092
57511
 
57512
+ // src/map/geo-query.ts
57513
+ init_parser12();
57514
+ init_resolver2();
57515
+ init_layout15();
57516
+ init_geo();
57517
+
57518
+ // src/map/invert.ts
57519
+ function inInsetFrame(inset, px, py) {
57520
+ return px >= inset.x && px <= inset.x + inset.w && py >= inset.y && py <= inset.y + inset.h;
57521
+ }
57522
+ function unstretch(layout, px, py) {
57523
+ const s = layout.stretch;
57524
+ return [
57525
+ s.bx0 + (s.sx !== 0 ? (px - s.ox) / s.sx : 0),
57526
+ s.by0 + (s.sy !== 0 ? (py - s.oy) / s.sy : 0)
57527
+ ];
57528
+ }
57529
+ function applyStretch(layout, x, y) {
57530
+ const s = layout.stretch;
57531
+ return [s.ox + (x - s.bx0) * s.sx, s.oy + (y - s.by0) * s.sy];
57532
+ }
57533
+ function pixelToLonLat(layout, px, py) {
57534
+ for (const inset of layout.insets) {
57535
+ if (inInsetFrame(inset, px, py)) {
57536
+ const ll2 = inset.projection.invert?.([px, py]);
57537
+ return ll2 && Number.isFinite(ll2[0]) && Number.isFinite(ll2[1]) ? [ll2[0], ll2[1]] : null;
57538
+ }
57539
+ }
57540
+ const [x, y] = layout.stretch ? unstretch(layout, px, py) : [px, py];
57541
+ const ll = layout.projection.invert?.([x, y]);
57542
+ return ll && Number.isFinite(ll[0]) && Number.isFinite(ll[1]) ? [ll[0], ll[1]] : null;
57543
+ }
57544
+ function lonLatToPixel(layout, lonLat) {
57545
+ const pt = [lonLat[0], lonLat[1]];
57546
+ const main = layout.projection(pt);
57547
+ const mainPx = main && Number.isFinite(main[0]) && Number.isFinite(main[1]) ? layout.stretch ? applyStretch(layout, main[0], main[1]) : [main[0], main[1]] : null;
57548
+ const onCanvas = !!mainPx && mainPx[0] >= 0 && mainPx[0] <= layout.width && mainPx[1] >= 0 && mainPx[1] <= layout.height;
57549
+ if (onCanvas) return mainPx;
57550
+ for (const inset of layout.insets) {
57551
+ const p = inset.projection(pt);
57552
+ if (p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && inInsetFrame(inset, p[0], p[1]))
57553
+ return [p[0], p[1]];
57554
+ }
57555
+ return mainPx;
57556
+ }
57557
+
57558
+ // src/map/geo-query.ts
57559
+ var EARTH_R_KM = 6371;
57560
+ var DEG = Math.PI / 180;
57561
+ function haversineKm(lat1, lon1, lat2, lon2) {
57562
+ const dLat = (lat2 - lat1) * DEG;
57563
+ const dLon = (lon2 - lon1) * DEG;
57564
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * DEG) * Math.cos(lat2 * DEG) * Math.sin(dLon / 2) ** 2;
57565
+ return 2 * EARTH_R_KM * Math.asin(Math.min(1, Math.sqrt(a)));
57566
+ }
57567
+ var POP_PULL_KM = 12;
57568
+ function nearestCity(lonLat, gazetteer) {
57569
+ const [lon, lat] = lonLat;
57570
+ let best = null;
57571
+ const cities = gazetteer.cities;
57572
+ for (let i = 0; i < cities.length; i++) {
57573
+ const c2 = cities[i];
57574
+ const dist = haversineKm(lat, lon, c2[0], c2[1]);
57575
+ const score = dist - POP_PULL_KM * Math.log10((c2[3] || 0) + 1);
57576
+ if (!best || score < best.score) best = { score, idx: i, dist };
57577
+ }
57578
+ if (!best) return null;
57579
+ const c = cities[best.idx];
57580
+ return {
57581
+ name: c[4],
57582
+ iso: c[2],
57583
+ ...c[5] !== void 0 && { sub: c[5] },
57584
+ distanceKm: best.dist
57585
+ };
57586
+ }
57587
+ function roundCoord(n) {
57588
+ return Number(n.toFixed(2));
57589
+ }
57590
+ function buildTokens(lonLat, region, city) {
57591
+ const coordPoiLine = `poi ${roundCoord(lonLat[1])} ${roundCoord(lonLat[0])}`;
57592
+ let stateTok = null;
57593
+ if (region.state) {
57594
+ const { iso, name } = region.state;
57595
+ stateTok = { primary: `${name} ${iso}`, alternates: [iso, name] };
57596
+ }
57597
+ let countryTok = null;
57598
+ if (region.country) {
57599
+ const { iso, name } = region.country;
57600
+ countryTok = { primary: name, alternates: [iso] };
57601
+ }
57602
+ let cityTok = null;
57603
+ if (city) {
57604
+ const scope = city.sub ?? (city.iso || "");
57605
+ cityTok = scope ? { token: `poi ${city.name} ${scope}`, ambiguous: false } : { token: `poi ${city.name}`, ambiguous: true };
57606
+ }
57607
+ return { coordPoiLine, state: stateTok, country: countryTok, city: cityTok };
57608
+ }
57609
+ var MAX_CITY_DOTS = 250;
57610
+ function createMapGeoQuery(opts) {
57611
+ const { content, width, height, data, palette, isDark } = opts;
57612
+ const resolved = resolveMap(parseMap(content), data);
57613
+ const layout = layoutMap(
57614
+ resolved,
57615
+ data,
57616
+ { width, height },
57617
+ { palette, isDark }
57618
+ );
57619
+ const countries = decodeFeatures(data.worldDetail);
57620
+ const states = decodeFeatures(data.usStates);
57621
+ const gazetteer = data.gazetteer;
57622
+ const invert = (px, py) => pixelToLonLat(layout, px, py);
57623
+ const project = (lonLat) => lonLatToPixel(layout, lonLat);
57624
+ const locate = (px, py) => {
57625
+ const lonLat = invert(px, py);
57626
+ if (!lonLat) return null;
57627
+ const region = regionAt(lonLat, countries, states);
57628
+ const city = nearestCity(lonLat, gazetteer);
57629
+ return {
57630
+ lonLat,
57631
+ country: region.country,
57632
+ state: region.state,
57633
+ nearestCity: city,
57634
+ tokens: buildTokens(lonLat, region, city)
57635
+ };
57636
+ };
57637
+ const cities = (extent2) => {
57638
+ const sorted = [...gazetteer.cities].sort((a, b) => b[3] - a[3]);
57639
+ const out = [];
57640
+ for (const c of sorted) {
57641
+ const [lat, lon, iso, pop, name, sub] = c;
57642
+ if (extent2) {
57643
+ const [[w, s], [e, n]] = extent2;
57644
+ if (lon < w || lon > e || lat < s || lat > n) continue;
57645
+ }
57646
+ const p = project([lon, lat]);
57647
+ if (!p) continue;
57648
+ if (p[0] < 0 || p[0] > width || p[1] < 0 || p[1] > height) continue;
57649
+ out.push({
57650
+ name,
57651
+ iso,
57652
+ ...sub !== void 0 && { sub },
57653
+ lon,
57654
+ lat,
57655
+ px: p[0],
57656
+ py: p[1],
57657
+ pop
57658
+ });
57659
+ if (out.length >= MAX_CITY_DOTS) break;
57660
+ }
57661
+ return out;
57662
+ };
57663
+ return { invert, project, locate, cities };
57664
+ }
57665
+
57093
57666
  // src/map/completion.ts
57094
57667
  var fold2 = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
57095
57668
  var groupThousands = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -58212,7 +58785,7 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58212
58785
  [
58213
58786
  "map",
58214
58787
  // Geographic map directives (§24B.2/.7). `poi`/`route` are content
58215
- // keywords, not directives; metadata keys (score/size/label) live in the
58788
+ // keywords, not directives; metadata keys (value/label/style) live in the
58216
58789
  // reserved-key registry.
58217
58790
  withGlobals({
58218
58791
  region: {
@@ -58223,9 +58796,14 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58223
58796
  description: "Override the auto projection",
58224
58797
  values: ["equirectangular", "natural-earth", "albers-usa", "mercator"]
58225
58798
  },
58226
- metric: { description: "Label for the region score ramp" },
58227
- "size-metric": { description: "Label for the POI size channel" },
58228
- scale: { description: "Override score ramp anchors: scale <min> <max>" },
58799
+ "region-metric": { description: "Label for the region value ramp" },
58800
+ "poi-metric": {
58801
+ description: "Label for the POI value (marker size) channel"
58802
+ },
58803
+ "flow-metric": {
58804
+ description: "Label for the edge/leg value (thickness) channel"
58805
+ },
58806
+ scale: { description: "Override value ramp anchors: scale <min> <max>" },
58229
58807
  "region-labels": {
58230
58808
  description: "Subdivision name labels",
58231
58809
  values: ["full", "abbrev", "off"]
@@ -58237,6 +58815,7 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58237
58815
  "default-country": { description: "ISO scope for bare city resolution" },
58238
58816
  "default-state": { description: "ISO subdivision scope" },
58239
58817
  "no-legend": { description: "Suppress the legend" },
58818
+ relief: { description: "Subtle mountain-range relief shading" },
58240
58819
  subtitle: { description: "Subtitle line" },
58241
58820
  caption: { description: "Caption line" }
58242
58821
  })
@@ -59747,6 +60326,7 @@ export {
59747
60326
  computeTimeTicks,
59748
60327
  contrastText,
59749
60328
  controlsGroupCapsuleWidth,
60329
+ createMapGeoQuery,
59750
60330
  decodeDiagramUrl,
59751
60331
  decodeViewState,
59752
60332
  displayName,