@kahwee/sf-map-svg 5.1.2 → 5.1.4

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 (44) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/src/cluster-renderer.d.ts +1 -1
  3. package/dist/src/district-layer.d.ts +1 -1
  4. package/dist/src/district-layer.js +1 -1
  5. package/dist/src/district-style.d.ts +1 -1
  6. package/dist/src/explorer-data.d.ts +1 -1
  7. package/dist/src/guide-map.js +3 -3
  8. package/dist/src/interactive/configuration-controller.d.ts +21 -0
  9. package/dist/src/interactive/configuration-controller.js +43 -0
  10. package/dist/src/{district-controller.d.ts → interactive/district-controller.d.ts} +12 -12
  11. package/dist/src/{district-controller.js → interactive/district-controller.js} +9 -9
  12. package/dist/src/{explorer-sidebar.d.ts → interactive/explorer-sidebar.d.ts} +6 -6
  13. package/dist/src/{explorer-sidebar.js → interactive/explorer-sidebar.js} +4 -4
  14. package/dist/src/{label-coordinator.d.ts → interactive/label-coordinator.d.ts} +7 -7
  15. package/dist/src/{label-coordinator.js → interactive/label-coordinator.js} +2 -2
  16. package/dist/src/{label-renderer.d.ts → interactive/label-renderer.d.ts} +2 -2
  17. package/dist/src/{label-renderer.js → interactive/label-renderer.js} +2 -2
  18. package/dist/src/{explorer-controls.d.ts → interactive/map-controls.d.ts} +4 -4
  19. package/dist/src/{explorer-controls.js → interactive/map-controls.js} +4 -4
  20. package/dist/src/{explorer-core.d.ts → interactive/map-engine.d.ts} +9 -8
  21. package/dist/src/{explorer-core.js → interactive/map-engine.js} +154 -140
  22. package/dist/src/{explorer-shell.d.ts → interactive/map-shell.d.ts} +2 -2
  23. package/dist/src/{explorer-shell.js → interactive/map-shell.js} +2 -2
  24. package/dist/src/{marker-controller.d.ts → interactive/marker-controller.d.ts} +8 -8
  25. package/dist/src/{marker-controller.js → interactive/marker-controller.js} +3 -3
  26. package/dist/src/{marker-layer.d.ts → interactive/marker-renderer.d.ts} +3 -3
  27. package/dist/src/{marker-layer.js → interactive/marker-renderer.js} +2 -2
  28. package/dist/src/map.js +15 -38
  29. package/dist/src/marker-catalog.d.ts +2 -2
  30. package/dist/src/{layers.d.ts → rendering/svg-layers.d.ts} +5 -5
  31. package/dist/src/{layers.js → rendering/svg-layers.js} +1 -1
  32. package/dist/src/{map-core.d.ts → rendering/svg-map-renderer.d.ts} +4 -4
  33. package/dist/src/{map-core.js → rendering/svg-map-renderer.js} +6 -6
  34. package/dist/src/static-labels.d.ts +1 -1
  35. package/dist/src/static-labels.js +1 -1
  36. package/dist/src/static.d.ts +2 -2
  37. package/dist/src/static.js +1 -1
  38. package/dist/src/transit.js +1 -1
  39. package/dist/src/types.d.ts +2 -2
  40. package/llms-full.txt +29 -21
  41. package/llms.txt +1 -1
  42. package/package.json +1 -1
  43. /package/dist/src/{svg.d.ts → rendering/svg-primitives.d.ts} +0 -0
  44. /package/dist/src/{svg.js → rendering/svg-primitives.js} +0 -0
@@ -1,28 +1,28 @@
1
- import { copyAppearance } from './appearance.js';
2
- import { createCamera, validateCameraOptions } from './camera.js';
3
- import { createClusterLayer } from './cluster-layer.js';
4
- import { clusterPoints } from './clusters.js';
1
+ import { copyAppearance } from '../appearance.js';
2
+ import { createCamera, validateCameraOptions } from '../camera.js';
3
+ import { createClusterLayer } from '../cluster-layer.js';
4
+ import { clusterPoints } from '../clusters.js';
5
+ import { prepareDistrictStyles } from '../district-style.js';
6
+ import { element, svgElement } from '../dom.js';
7
+ import { fitBounds, projectedBounds } from '../explorer-layout.js';
8
+ import { controlKeys, layerKeys, normalizeFeatures, validateSwitchPatch } from '../features.js';
9
+ import { createFrameScheduler } from '../frame-scheduler.js';
10
+ import { createLayerTransitions } from '../layer-transition.js';
11
+ import { createMarkerCatalog } from '../marker-catalog.js';
12
+ import { createMarkerChooser } from '../marker-chooser.js';
13
+ import { createMarkerNavigation } from '../marker-navigation.js';
14
+ import { createNeighborhoodLayer, createNeighborhoodLookup, formatSourceLabel, } from '../neighborhood-layer.js';
15
+ import { createOverlayLayer } from '../overlay-layer.js';
16
+ import { createSFMapWithData, districtColors, getLayerPathsWithData, resolveMapColors, } from '../rendering/svg-map-renderer.js';
17
+ import { validateExplorerOptions } from '../validation.js';
18
+ import { fitViewport, validateViewport } from '../viewport.js';
5
19
  import { createDistrictController } from './district-controller.js';
6
- import { prepareDistrictStyles } from './district-style.js';
7
- import { element, svgElement } from './dom.js';
8
- import { createExplorerControls } from './explorer-controls.js';
9
- import { fitBounds, projectedBounds } from './explorer-layout.js';
10
- import { controlKeys, layerKeys, normalizeFeatures, validateSwitchPatch } from './features.js';
11
- import { createFrameScheduler } from './frame-scheduler.js';
12
20
  import { createLabelCoordinator } from './label-coordinator.js';
13
- import { createLayerTransitions } from './layer-transition.js';
14
- import { createSFMapWithData, districtColors, getLayerPathsWithData, resolveMapColors, } from './map-core.js';
15
- import { createMarkerCatalog } from './marker-catalog.js';
16
- import { createMarkerChooser } from './marker-chooser.js';
21
+ import { createMapControls } from './map-controls.js';
17
22
  import { createMarkerController } from './marker-controller.js';
18
- import { createMarkerLayer } from './marker-layer.js';
19
- import { createMarkerNavigation } from './marker-navigation.js';
20
- import { createNeighborhoodLayer, createNeighborhoodLookup, formatSourceLabel, } from './neighborhood-layer.js';
21
- import { createOverlayLayer } from './overlay-layer.js';
22
- import { validateExplorerOptions } from './validation.js';
23
- import { fitViewport, validateViewport } from './viewport.js';
23
+ import { createMarkerRenderer } from './marker-renderer.js';
24
24
  /** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
25
- export function createNeighborhoodExplorerCore(options = {}, data, ui) {
25
+ export function createMapEngine(options = {}, data, ui) {
26
26
  validateExplorerOptions(options);
27
27
  let { source = 'realtor', mode = 'neighborhoods', labels = true, neighborhood, year = 2022, theme = 'transit', colors = {}, labelStyle = {}, areaStyle = {}, districtStyle: initialDistrictStyle, motion = false, markerEntrance = false, selectedMarkerRing, clustering = false, legend: legendOptions = {}, attribution: attributionMode = 'full', northArrow = false, scaleBar = false, layerTransitions = false, districtMorph: districtMorphOption = false, interface: chrome = 'explorer', layers = {}, selectableNeighborhoods = true, labelSize = {}, fitPadding = 24, markers: initialMarkers = [], markerRadius = 6, markerHitSize = 44, markerColor = colors.marker ?? '#245b61', selectedMarkerColor = colors.selected ?? '#f04f32', onMarkerActivate, overlays: initialOverlays = [], onOverlayActivate, style: styleOptions = {}, strings = {}, controls = {}, } = options;
28
28
  if (!['neighborhoods', 'districts', 'basemap'].includes(mode))
@@ -188,7 +188,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
188
188
  reducedMotion,
189
189
  enabled,
190
190
  fitGeometry,
191
- updateComposition,
191
+ syncLayerComposition,
192
192
  scheduleLabels,
193
193
  get year() {
194
194
  return year;
@@ -229,7 +229,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
229
229
  cancel: (id) => cancelAnimationFrame(id),
230
230
  render: drawLabels,
231
231
  });
232
- const markerRenderer = createMarkerLayer();
232
+ const markerRenderer = createMarkerRenderer();
233
233
  const cancelEntrances = markerRenderer.cancelEntrances;
234
234
  const camera = createCamera({
235
235
  read: getViewport,
@@ -271,13 +271,13 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
271
271
  refresh: {
272
272
  selection() {
273
273
  scheduleLabels();
274
- updateResults();
275
- updateDetail();
274
+ updateNeighborhoodResults();
275
+ updateSelectionDetail();
276
276
  },
277
277
  source() {
278
- updateResults();
279
- updateDetail();
280
- updateComposition();
278
+ updateNeighborhoodResults();
279
+ updateSelectionDetail();
280
+ syncLayerComposition();
281
281
  },
282
282
  },
283
283
  moveView,
@@ -466,7 +466,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
466
466
  isDestroyed: () => destroyed,
467
467
  });
468
468
  const setOverlays = overlays.set;
469
- const mapControls = createExplorerControls(shell, {
469
+ const mapControls = createMapControls(shell, {
470
470
  svg,
471
471
  signal: controller.signal,
472
472
  strings,
@@ -483,7 +483,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
483
483
  setSource,
484
484
  selectNeighborhood,
485
485
  selectMarker,
486
- updateResults,
486
+ updateNeighborhoodResults,
487
487
  search(query) {
488
488
  const exact = getNeighborhood(query, neighborhoods.source);
489
489
  const matches = searchNeighborhoods(query, neighborhoods.source);
@@ -604,16 +604,16 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
604
604
  labelCoordinator.draw();
605
605
  }
606
606
  const sidebar = ui.createSidebar?.(shell, data, chrome, strings.emptyResults, searchNeighborhoods);
607
- function updateDetail() {
608
- sidebar?.updateDetail({
607
+ function updateSelectionDetail() {
608
+ sidebar?.updateSelectionDetail({
609
609
  mode,
610
610
  year,
611
611
  source: neighborhoods.source,
612
612
  selected: neighborhoods.selected,
613
613
  });
614
614
  }
615
- function updateResults() {
616
- sidebar?.updateResults({
615
+ function updateNeighborhoodResults() {
616
+ sidebar?.updateNeighborhoodResults({
617
617
  mode,
618
618
  year,
619
619
  source: neighborhoods.source,
@@ -629,9 +629,9 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
629
629
  validateSwitchPatch(patch, layerKeys);
630
630
  const next = { ...layers, ...patch };
631
631
  layers = next;
632
- updateComposition();
632
+ syncLayerComposition();
633
633
  }
634
- function updateComposition() {
634
+ function syncLayerComposition() {
635
635
  for (const [name, key] of [
636
636
  ['landmarks', 'landmarks'],
637
637
  ['highways', 'highways'],
@@ -715,7 +715,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
715
715
  controls = next;
716
716
  mapControls.update(controls);
717
717
  markerLabel.hidden = !markerCatalog.items.length || controls.markerPicker === false;
718
- updateComposition();
718
+ syncLayerComposition();
719
719
  }
720
720
  function setMode(next, reset = true) {
721
721
  if (destroyed)
@@ -734,8 +734,8 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
734
734
  const districts = mode !== 'neighborhoods';
735
735
  for (const node of [sourceLabel, searchLabel, count, results])
736
736
  node.hidden = districts;
737
- updateComposition();
738
- updateDetail();
737
+ syncLayerComposition();
738
+ updateSelectionDetail();
739
739
  status.textContent =
740
740
  mode === 'basemap'
741
741
  ? 'San Francisco basemap.'
@@ -755,7 +755,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
755
755
  labels = visible;
756
756
  mapControls.updateLabels(labels);
757
757
  root.dataset.labels = String(labels);
758
- updateComposition();
758
+ syncLayerComposition();
759
759
  if (!labels) {
760
760
  labelLayer.replaceChildren();
761
761
  root.dataset.visibleLabels = '0';
@@ -860,7 +860,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
860
860
  root.dispatchEvent(new CustomEvent('mapresize', { bubbles: true }));
861
861
  };
862
862
  let presentationRevision = 0;
863
- function applyPresentation(patch, onCommit, isCurrent = () => true) {
863
+ function preparePresentation(patch) {
864
864
  const revision = ++presentationRevision;
865
865
  const nextYear = patch.year ?? year;
866
866
  const nextMode = patch.mode ?? mode;
@@ -880,11 +880,8 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
880
880
  const prepared = appearance || nextYear !== year
881
881
  ? prepareDistrictStyles(data.map.districts?.[nextYear] ?? [], nextStyle)
882
882
  : districts.styles;
883
- if (destroyed ||
884
- !isCurrent() ||
885
- revision !== presentationRevision ||
886
- districtVersion !== districts.revision)
887
- return false;
883
+ if (destroyed || revision !== presentationRevision || districtVersion !== districts.revision)
884
+ return undefined;
888
885
  if (nextYear !== year) {
889
886
  // Validate target paths and label coordinates before any presentation group commits.
890
887
  getLayerPathsWithData({ year: nextYear }, data.map, false);
@@ -892,103 +889,120 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
892
889
  for (const point of feature.properties.labelPoints)
893
890
  map.project(point);
894
891
  }
895
- onCommit?.();
896
- if (appearance) {
897
- districts.invalidate();
898
- const next = copyAppearance(appearance);
899
- theme = next.theme ?? 'transit';
900
- colors = next.colors ?? {};
901
- labelStyle = next.labelStyle ?? {};
902
- areaStyle = next.areaStyle ?? {};
903
- styleOptions = next.style ?? {};
904
- minLabelSize = next.labelSize?.min ?? 11;
905
- maxLabelSize = next.labelSize?.max ?? 12;
906
- markerRadius = next.markerRadius ?? 6;
907
- markerHitSize = next.markerHitSize ?? 44;
908
- markerColor = next.markerColor ?? colors.marker ?? '#245b61';
909
- selectedMarkerColor = next.selectedMarkerColor ?? colors.selected ?? '#f04f32';
910
- districtTransition.cancel();
911
- const fills = districtLayers.find((layer) => layer.dataset.layer === 'district-fills');
912
- const fade = layerFades.duration();
913
- const stylesChanged = nextStyle !== districts.style ||
914
- [...prepared].some(([id, value]) => ['fill', 'stroke', 'opacity'].some((key) => value[key] !== districts.styles.get(id)?.[key]));
915
- if (fills && fade && enabled('districtFills') && nextYear === year && stylesChanged)
916
- districtTransition.fade(fills, fade);
917
- districts.commitStyles(nextStyle, prepared);
918
- const palette = resolveMapColors(theme, colors);
919
- svg.querySelector('rect')?.setAttribute('fill', palette.water);
920
- svg.querySelector('[data-layer="coast"]')?.setAttribute('fill', palette.land);
921
- svg.querySelector('[data-layer="coastline"]')?.setAttribute('stroke', palette.district);
922
- for (const [layer, color] of [
923
- ['landmarks', palette.park],
924
- ['highways', palette.highway],
925
- ['key-roads', palette.road],
926
- ])
927
- for (const node of svg.querySelectorAll(`[data-layer="${layer}"] path`)) {
928
- node.setAttribute(layer === 'landmarks' ? 'fill' : 'stroke', color);
929
- if (layer === 'highways')
930
- node.setAttribute('stroke-width', theme === 'transit' ? '2' : '1.4');
931
- }
932
- for (const [token, fallback, name] of [
933
- ['ink', '#18364f', 'ink'],
934
- ['surface', '#fff', 'surface'],
935
- ['accent', '#163d61', 'accent'],
936
- ['border', '#cedae3', 'border'],
937
- ['focus', '#1676b8', 'focus'],
938
- ['controlGap', '6px', 'control-gap'],
939
- ['font', 'system-ui,sans-serif', 'font'],
940
- ])
941
- root.style.setProperty(`--sf-map-${name}`, styleOptions[token] ?? fallback);
942
- labelLayer.setAttribute('font-family', labelStyle.fontFamily ?? styleOptions.font ?? 'system-ui,sans-serif');
943
- labelCoordinator.invalidateMetrics();
944
- for (const station of stationItems)
945
- station.node.setAttribute('stroke', colors.bart ?? '#0073ae');
946
- for (const kind of ['bart', 'park', 'highway', 'road']) {
947
- const symbol = legend.querySelector(`.sf-explorer-legend-${kind}`);
948
- if (symbol) {
949
- symbol.style.removeProperty(kind === 'bart' ? 'border-color' : 'background-color');
950
- if (colors[kind])
951
- symbol.style.setProperty(kind === 'bart' ? 'border-color' : 'background-color', colors[kind]);
892
+ let consumed = false;
893
+ return {
894
+ commit(onCommit, isCurrent) {
895
+ if (consumed ||
896
+ destroyed ||
897
+ !isCurrent() ||
898
+ revision !== presentationRevision ||
899
+ districtVersion !== districts.revision)
900
+ return false;
901
+ consumed = true;
902
+ onCommit();
903
+ if (destroyed ||
904
+ !isCurrent() ||
905
+ revision !== presentationRevision ||
906
+ districtVersion !== districts.revision)
907
+ return false;
908
+ if (appearance) {
909
+ districts.invalidate();
910
+ const next = copyAppearance(appearance);
911
+ theme = next.theme ?? 'transit';
912
+ colors = next.colors ?? {};
913
+ labelStyle = next.labelStyle ?? {};
914
+ areaStyle = next.areaStyle ?? {};
915
+ styleOptions = next.style ?? {};
916
+ minLabelSize = next.labelSize?.min ?? 11;
917
+ maxLabelSize = next.labelSize?.max ?? 12;
918
+ markerRadius = next.markerRadius ?? 6;
919
+ markerHitSize = next.markerHitSize ?? 44;
920
+ markerColor = next.markerColor ?? colors.marker ?? '#245b61';
921
+ selectedMarkerColor = next.selectedMarkerColor ?? colors.selected ?? '#f04f32';
922
+ districtTransition.cancel();
923
+ const fills = districtLayers.find((layer) => layer.dataset.layer === 'district-fills');
924
+ const fade = layerFades.duration();
925
+ const stylesChanged = nextStyle !== districts.style ||
926
+ [...prepared].some(([id, value]) => ['fill', 'stroke', 'opacity'].some((key) => value[key] !== districts.styles.get(id)?.[key]));
927
+ if (fills && fade && enabled('districtFills') && nextYear === year && stylesChanged)
928
+ districtTransition.fade(fills, fade);
929
+ districts.commitStyles(nextStyle, prepared);
930
+ const palette = resolveMapColors(theme, colors);
931
+ svg.querySelector('rect')?.setAttribute('fill', palette.water);
932
+ svg.querySelector('[data-layer="coast"]')?.setAttribute('fill', palette.land);
933
+ svg.querySelector('[data-layer="coastline"]')?.setAttribute('stroke', palette.district);
934
+ for (const [layer, color] of [
935
+ ['landmarks', palette.park],
936
+ ['highways', palette.highway],
937
+ ['key-roads', palette.road],
938
+ ])
939
+ for (const node of svg.querySelectorAll(`[data-layer="${layer}"] path`)) {
940
+ node.setAttribute(layer === 'landmarks' ? 'fill' : 'stroke', color);
941
+ if (layer === 'highways')
942
+ node.setAttribute('stroke-width', theme === 'transit' ? '2' : '1.4');
943
+ }
944
+ for (const [token, fallback, name] of [
945
+ ['ink', '#18364f', 'ink'],
946
+ ['surface', '#fff', 'surface'],
947
+ ['accent', '#163d61', 'accent'],
948
+ ['border', '#cedae3', 'border'],
949
+ ['focus', '#1676b8', 'focus'],
950
+ ['controlGap', '6px', 'control-gap'],
951
+ ['font', 'system-ui,sans-serif', 'font'],
952
+ ])
953
+ root.style.setProperty(`--sf-map-${name}`, styleOptions[token] ?? fallback);
954
+ labelLayer.setAttribute('font-family', labelStyle.fontFamily ?? styleOptions.font ?? 'system-ui,sans-serif');
955
+ labelCoordinator.invalidateMetrics();
956
+ for (const station of stationItems)
957
+ station.node.setAttribute('stroke', colors.bart ?? '#0073ae');
958
+ for (const kind of ['bart', 'park', 'highway', 'road']) {
959
+ const symbol = legend.querySelector(`.sf-explorer-legend-${kind}`);
960
+ if (symbol) {
961
+ symbol.style.removeProperty(kind === 'bart' ? 'border-color' : 'background-color');
962
+ if (colors[kind])
963
+ symbol.style.setProperty(kind === 'bart' ? 'border-color' : 'background-color', colors[kind]);
964
+ }
965
+ }
966
+ for (const item of neighborhoods.items) {
967
+ const active = item.feature === neighborhoods.selected;
968
+ item.node.setAttribute('fill', active ? (areaStyle.selectedFill ?? '#408dbe') : 'transparent');
969
+ item.node.setAttribute('stroke', active ? (areaStyle.selectedStroke ?? '#176ba2') : (colors.neighborhood ?? '#9caebc'));
970
+ }
971
+ for (const item of markerCatalog.items) {
972
+ const visual = item.visual;
973
+ if (!visual)
974
+ continue;
975
+ visual.dot.setAttribute('fill', item.marker.id === markers.selected
976
+ ? selectedMarkerColor
977
+ : (item.marker.color ?? markerColor));
978
+ visual.ring.setAttribute('stroke', features.selectedMarkerRing
979
+ ? (features.selectedMarkerRing.color ?? selectedMarkerColor)
980
+ : selectedMarkerColor);
981
+ }
982
+ invalidateClusters();
983
+ updateDistrictAppearance();
952
984
  }
953
- }
954
- for (const item of neighborhoods.items) {
955
- const active = item.feature === neighborhoods.selected;
956
- item.node.setAttribute('fill', active ? (areaStyle.selectedFill ?? '#408dbe') : 'transparent');
957
- item.node.setAttribute('stroke', active ? (areaStyle.selectedStroke ?? '#176ba2') : (colors.neighborhood ?? '#9caebc'));
958
- }
959
- for (const item of markerCatalog.items) {
960
- const visual = item.visual;
961
- if (!visual)
962
- continue;
963
- visual.dot.setAttribute('fill', item.marker.id === markers.selected
964
- ? selectedMarkerColor
965
- : (item.marker.color ?? markerColor));
966
- visual.ring.setAttribute('stroke', features.selectedMarkerRing
967
- ? (features.selectedMarkerRing.color ?? selectedMarkerColor)
968
- : selectedMarkerColor);
969
- }
970
- invalidateClusters();
971
- updateDistrictAppearance();
972
- }
973
- if (nextYear !== year)
974
- setDistrictYear(nextYear, {}, prepared);
975
- if (destroyed || revision !== presentationRevision || !isCurrent())
976
- return false;
977
- commitSource?.();
978
- if (destroyed || revision !== presentationRevision || !isCurrent())
979
- return false;
980
- if (commitSource && appearance)
981
- for (const item of neighborhoods.items)
982
- item.node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
983
- if (patch.mode !== undefined)
984
- setMode(patch.mode, false);
985
- if (patch.labels !== undefined)
986
- setLabels(patch.labels);
987
- scheduleLabels();
988
- return true;
985
+ if (nextYear !== year)
986
+ setDistrictYear(nextYear, {}, prepared);
987
+ if (destroyed || revision !== presentationRevision || !isCurrent())
988
+ return false;
989
+ commitSource?.();
990
+ if (destroyed || revision !== presentationRevision || !isCurrent())
991
+ return false;
992
+ if (commitSource && appearance)
993
+ for (const item of neighborhoods.items)
994
+ item.node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
995
+ if (patch.mode !== undefined)
996
+ setMode(patch.mode, false);
997
+ if (patch.labels !== undefined)
998
+ setLabels(patch.labels);
999
+ scheduleLabels();
1000
+ return true;
1001
+ },
1002
+ };
989
1003
  }
990
1004
  const explorer = Object.assign(root, {
991
- applyPresentation,
1005
+ preparePresentation,
992
1006
  getMapState: () => ({ mode, source: neighborhoods.source, year, labels }),
993
1007
  selectNeighborhood,
994
1008
  getSelection,
@@ -1,5 +1,5 @@
1
- import type { NeighborhoodSource } from '../data/types.js';
2
- import type { NeighborhoodExplorerOptions } from './types.js';
1
+ import type { NeighborhoodSource } from '../../data/types.js';
2
+ import type { NeighborhoodExplorerOptions } from '../types.js';
3
3
  /** Construct shared map chrome; optional explorer content has its own factory below. */
4
4
  export declare function createMapShell({ chrome, styleOptions, strings, legendOptions, colors, attributionMode, }: {
5
5
  source: NeighborhoodSource;
@@ -1,5 +1,5 @@
1
- import { element } from './dom.js';
2
- import { sourceNames } from './neighborhood-layer.js';
1
+ import { element } from '../dom.js';
2
+ import { sourceNames } from '../neighborhood-layer.js';
3
3
  let explorerCount = 0;
4
4
  /** Construct shared map chrome; optional explorer content has its own factory below. */
5
5
  export function createMapShell({ chrome, styleOptions, strings, legendOptions, colors, attributionMode, }) {
@@ -1,11 +1,11 @@
1
- import type { Position } from '../data/types.js';
2
- import { type View } from './explorer-layout.js';
3
- import type { normalizeFeatures } from './features.js';
4
- import type { createMarkerCatalog } from './marker-catalog.js';
5
- import type { createMarkerChooser } from './marker-chooser.js';
6
- import type { MarkerItem, MarkerVisual } from './marker-layer.js';
7
- import type { createMarkerNavigation } from './marker-navigation.js';
8
- import type { CameraOptions, MapMarker, NeighborhoodExplorerOptions } from './types.js';
1
+ import type { Position } from '../../data/types.js';
2
+ import { type View } from '../explorer-layout.js';
3
+ import type { normalizeFeatures } from '../features.js';
4
+ import type { createMarkerCatalog } from '../marker-catalog.js';
5
+ import type { createMarkerChooser } from '../marker-chooser.js';
6
+ import type { createMarkerNavigation } from '../marker-navigation.js';
7
+ import type { CameraOptions, MapMarker, NeighborhoodExplorerOptions } from '../types.js';
8
+ import type { MarkerItem, MarkerVisual } from './marker-renderer.js';
9
9
  interface MarkerHost {
10
10
  root: HTMLElement;
11
11
  canvas: HTMLElement;
@@ -1,6 +1,6 @@
1
- import { validateCameraOptions } from './camera.js';
2
- import { fitBounds } from './explorer-layout.js';
3
- import { fitViewport } from './viewport.js';
1
+ import { validateCameraOptions } from '../camera.js';
2
+ import { fitBounds } from '../explorer-layout.js';
3
+ import { fitViewport } from '../viewport.js';
4
4
  /** Own selection revisions, activation and focus recovery independently of marker rendering. */
5
5
  export function createMarkerController(host) {
6
6
  const { root, canvas, svg, markerLayer, markerSelect, markerLabel, markerCatalog, markerNavigation, strings, fitPadding, onMarkerActivate, invalidateClusters, drawClusters, materializeMarker, scheduleLabels, syncFeatureControls, moveView, } = host;
@@ -1,5 +1,5 @@
1
- import type { normalizeFeatures } from './features.js';
2
- import type { MapMarker } from './types.js';
1
+ import type { normalizeFeatures } from '../features.js';
2
+ import type { MapMarker } from '../types.js';
3
3
  export interface MarkerVisual {
4
4
  node: SVGGElement;
5
5
  dot: SVGCircleElement;
@@ -15,7 +15,7 @@ export interface MarkerItem {
15
15
  visual: MarkerVisual | undefined;
16
16
  }
17
17
  /** Own marker visuals and entrances; selection/events remain with the controller. */
18
- export declare function createMarkerLayer(): {
18
+ export declare function createMarkerRenderer(): {
19
19
  cancelEntrances: () => void;
20
20
  remove(item: MarkerItem): void;
21
21
  update(item: MarkerItem, marker: MapMarker, point: [number, number], index: number): void;
@@ -1,6 +1,6 @@
1
- import { svgElement } from './dom.js';
1
+ import { svgElement } from '../dom.js';
2
2
  /** Own marker visuals and entrances; selection/events remain with the controller. */
3
- export function createMarkerLayer() {
3
+ export function createMarkerRenderer() {
4
4
  const records = new Set();
5
5
  const animations = new Map();
6
6
  function cancelEntrances() {
package/dist/src/map.js CHANGED
@@ -1,27 +1,29 @@
1
1
  import { copyAppearance } from './appearance.js';
2
2
  import { validateCameraOptions } from './camera.js';
3
3
  import { configurationPatch, expandMapOptions, prepareConfiguration } from './configuration.js';
4
- import { createNeighborhoodExplorerCore } from './explorer-core.js';
5
- import { createMapShell } from './explorer-shell.js';
6
- import { controlKeys, layerKeys } from './features.js';
4
+ import { layerKeys } from './features.js';
5
+ import { createConfigurationController } from './interactive/configuration-controller.js';
6
+ import { createMapEngine } from './interactive/map-engine.js';
7
+ import { createMapShell } from './interactive/map-shell.js';
7
8
  import { assertOptions } from './validation.js';
8
9
  /** No geography is imported. Supply a preset or your own immutable data. */
9
10
  export function createMap(data, options = {}) {
10
11
  const expanded = expandMapOptions(options);
11
- let config = prepareConfiguration({ features: {}, layers: {}, controls: {}, appearance: {} }, {
12
+ const initialConfiguration = prepareConfiguration({ features: {}, layers: {}, controls: {}, appearance: {} }, {
12
13
  features: options.features,
13
14
  layers: options.layers,
14
15
  controls: options.controls,
15
16
  appearance: options.appearance,
16
17
  });
17
- const element = createNeighborhoodExplorerCore({
18
+ const element = createMapEngine({
18
19
  mode: 'basemap',
19
20
  ...expanded,
20
21
  interface: 'map',
21
22
  onOverlayActivate: () => { },
22
23
  }, data, { createShell: createMapShell });
23
24
  const subscriptions = new Set();
24
- let configurationRevision = 0;
25
+ const configuration = createConfigurationController(initialConfiguration, element, () => destroyed);
26
+ const configure = configuration.configure;
25
27
  const sourceList = ['realtor', 'sf-find', 'analysis'].filter((source) => !!data.neighborhoods[source]);
26
28
  const yearList = [2002, 2012, 2022].filter((year) => !!data.map.districts?.[year] && !!data.districts?.[year]);
27
29
  const capabilities = () => ({
@@ -42,11 +44,11 @@ export function createMap(data, options = {}) {
42
44
  const snapshot = () => ({
43
45
  ...element.getMapState(),
44
46
  ...structuredClone({
45
- features: config.features,
46
- layers: config.layers,
47
- controls: config.controls,
47
+ features: configuration.current.features,
48
+ layers: configuration.current.layers,
49
+ controls: configuration.current.controls,
48
50
  }),
49
- appearance: copyAppearance(config.appearance),
51
+ appearance: copyAppearance(configuration.current.appearance),
50
52
  });
51
53
  const resolved = () => {
52
54
  const state = element.getMapState();
@@ -58,7 +60,7 @@ export function createMap(data, options = {}) {
58
60
  key,
59
61
  supplied[key] &&
60
62
  (!key.endsWith('Labels') || state.labels) &&
61
- (config.layers[key] ??
63
+ (configuration.current.layers[key] ??
62
64
  (key.startsWith('district')
63
65
  ? state.mode === 'districts'
64
66
  : key.startsWith('neighborhood')
@@ -119,31 +121,6 @@ export function createMap(data, options = {}) {
119
121
  element.fitGeometry(geometry, padding, motion);
120
122
  }),
121
123
  });
122
- function configure(patch) {
123
- const revision = ++configurationRevision;
124
- const next = prepareConfiguration(config, patch);
125
- const hasFeatures = 'features' in patch, hasLayers = 'layers' in patch, hasControls = 'controls' in patch;
126
- const layers = Object.fromEntries(layerKeys.map((key) => [key, next.layers[key]]));
127
- const controls = Object.fromEntries(controlKeys.map((key) => [key, next.controls[key]]));
128
- const features = patch.features === undefined
129
- ? next.features
130
- : Object.fromEntries(Object.keys(patch.features).map((key) => [
131
- key,
132
- next.features[key],
133
- ]));
134
- let committed = false;
135
- element.applyPresentation({ ...patch, ...('appearance' in patch ? { appearance: next.appearance } : {}) }, () => {
136
- committed = true;
137
- config = next;
138
- if (hasFeatures)
139
- element.setFeatures(features);
140
- if (hasLayers)
141
- element.setLayers(layers);
142
- if (hasControls)
143
- element.setControls(controls);
144
- }, () => !destroyed && revision === configurationRevision);
145
- return committed && !destroyed && revision === configurationRevision;
146
- }
147
124
  return Object.freeze({
148
125
  element,
149
126
  overlayElement: element.overlayElement,
@@ -152,7 +129,7 @@ export function createMap(data, options = {}) {
152
129
  return destroyed;
153
130
  },
154
131
  configure: use((patch) => {
155
- configure(configurationPatch(patch, config));
132
+ configure(configurationPatch(patch, configuration.current));
156
133
  }),
157
134
  resetConfiguration: use((groups = ['features', 'layers', 'controls', 'appearance']) => {
158
135
  if (!Array.isArray(groups) ||
@@ -242,7 +219,7 @@ export function createMap(data, options = {}) {
242
219
  selectDistrict: use(element.selectDistrict),
243
220
  getSelectedDistrict: use(element.getSelectedDistrict),
244
221
  setDistrictYear: use((...args) => {
245
- configurationRevision++;
222
+ configuration.invalidate();
246
223
  element.setDistrictYear(...args);
247
224
  }),
248
225
  setDistrictStyle: use((style) => {
@@ -1,7 +1,7 @@
1
- import type { createMarkerLayer, MarkerItem } from './marker-layer.js';
1
+ import type { createMarkerRenderer, MarkerItem } from './interactive/marker-renderer.js';
2
2
  import type { MapMarker } from './types.js';
3
3
  /** Own ordered records and picker reconciliation; prepare before committing DOM changes. */
4
- export declare function createMarkerCatalog(renderer: ReturnType<typeof createMarkerLayer>, picker: HTMLSelectElement, project: (point: import('../data/types.js').Position) => [number, number]): {
4
+ export declare function createMarkerCatalog(renderer: ReturnType<typeof createMarkerRenderer>, picker: HTMLSelectElement, project: (point: import('../data/types.js').Position) => [number, number]): {
5
5
  readonly items: MarkerItem[];
6
6
  prepare(markers: readonly MapMarker[], selected: string | null): {
7
7
  selection: string | null;
@@ -1,8 +1,8 @@
1
- import type { Geometry } from '../data/types.js';
2
- import type { DistrictRow } from './data.js';
3
- import type { Project } from './geometry.js';
4
- import type { BartStation, KeyRoad, Landmark } from './overlays.js';
5
- import type { MapMarker, SFMapOptions } from './types.js';
1
+ import type { Geometry } from '../../data/types.js';
2
+ import type { DistrictRow } from '../data.js';
3
+ import type { Project } from '../geometry.js';
4
+ import type { BartStation, KeyRoad, Landmark } from '../overlays.js';
5
+ import type { MapMarker, SFMapOptions } from '../types.js';
6
6
  interface LayerContext {
7
7
  labels: boolean;
8
8
  /** Animated static maps index items for staggering and measure lines for drawing. */
@@ -1,4 +1,4 @@
1
- import { escapeXml, number, overlayLabel, stroke } from './svg.js';
1
+ import { escapeXml, number, overlayLabel, stroke } from './svg-primitives.js';
2
2
  /** Custom JavaScript/JSON rows must not introduce markup or CSS through numeric IDs. */
3
3
  function districtId(id) {
4
4
  if (!Number.isFinite(id))