@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.
- package/CHANGELOG.md +8 -0
- package/dist/src/cluster-renderer.d.ts +1 -1
- package/dist/src/district-layer.d.ts +1 -1
- package/dist/src/district-layer.js +1 -1
- package/dist/src/district-style.d.ts +1 -1
- package/dist/src/explorer-data.d.ts +1 -1
- package/dist/src/guide-map.js +3 -3
- package/dist/src/interactive/configuration-controller.d.ts +21 -0
- package/dist/src/interactive/configuration-controller.js +43 -0
- package/dist/src/{district-controller.d.ts → interactive/district-controller.d.ts} +12 -12
- package/dist/src/{district-controller.js → interactive/district-controller.js} +9 -9
- package/dist/src/{explorer-sidebar.d.ts → interactive/explorer-sidebar.d.ts} +6 -6
- package/dist/src/{explorer-sidebar.js → interactive/explorer-sidebar.js} +4 -4
- package/dist/src/{label-coordinator.d.ts → interactive/label-coordinator.d.ts} +7 -7
- package/dist/src/{label-coordinator.js → interactive/label-coordinator.js} +2 -2
- package/dist/src/{label-renderer.d.ts → interactive/label-renderer.d.ts} +2 -2
- package/dist/src/{label-renderer.js → interactive/label-renderer.js} +2 -2
- package/dist/src/{explorer-controls.d.ts → interactive/map-controls.d.ts} +4 -4
- package/dist/src/{explorer-controls.js → interactive/map-controls.js} +4 -4
- package/dist/src/{explorer-core.d.ts → interactive/map-engine.d.ts} +9 -8
- package/dist/src/{explorer-core.js → interactive/map-engine.js} +154 -140
- package/dist/src/{explorer-shell.d.ts → interactive/map-shell.d.ts} +2 -2
- package/dist/src/{explorer-shell.js → interactive/map-shell.js} +2 -2
- package/dist/src/{marker-controller.d.ts → interactive/marker-controller.d.ts} +8 -8
- package/dist/src/{marker-controller.js → interactive/marker-controller.js} +3 -3
- package/dist/src/{marker-layer.d.ts → interactive/marker-renderer.d.ts} +3 -3
- package/dist/src/{marker-layer.js → interactive/marker-renderer.js} +2 -2
- package/dist/src/map.js +15 -38
- package/dist/src/marker-catalog.d.ts +2 -2
- package/dist/src/{layers.d.ts → rendering/svg-layers.d.ts} +5 -5
- package/dist/src/{layers.js → rendering/svg-layers.js} +1 -1
- package/dist/src/{map-core.d.ts → rendering/svg-map-renderer.d.ts} +4 -4
- package/dist/src/{map-core.js → rendering/svg-map-renderer.js} +6 -6
- package/dist/src/static-labels.d.ts +1 -1
- package/dist/src/static-labels.js +1 -1
- package/dist/src/static.d.ts +2 -2
- package/dist/src/static.js +1 -1
- package/dist/src/transit.js +1 -1
- package/dist/src/types.d.ts +2 -2
- package/llms-full.txt +29 -21
- package/llms.txt +1 -1
- package/package.json +1 -1
- /package/dist/src/{svg.d.ts → rendering/svg-primitives.d.ts} +0 -0
- /package/dist/src/{svg.js → rendering/svg-primitives.js} +0 -0
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { copyAppearance } from '
|
|
2
|
-
import { createCamera, validateCameraOptions } from '
|
|
3
|
-
import { createClusterLayer } from '
|
|
4
|
-
import { clusterPoints } from '
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
275
|
-
|
|
274
|
+
updateNeighborhoodResults();
|
|
275
|
+
updateSelectionDetail();
|
|
276
276
|
},
|
|
277
277
|
source() {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
608
|
-
sidebar?.
|
|
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
|
|
616
|
-
sidebar?.
|
|
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
|
-
|
|
632
|
+
syncLayerComposition();
|
|
633
633
|
}
|
|
634
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
970
|
-
|
|
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
|
-
|
|
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 '
|
|
2
|
-
import type { NeighborhoodExplorerOptions } from '
|
|
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 '
|
|
2
|
-
import { sourceNames } from '
|
|
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 '
|
|
2
|
-
import { type View } from '
|
|
3
|
-
import type { normalizeFeatures } from '
|
|
4
|
-
import type { createMarkerCatalog } from '
|
|
5
|
-
import type { createMarkerChooser } from '
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
8
|
-
import type {
|
|
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 '
|
|
2
|
-
import { fitBounds } from '
|
|
3
|
-
import { fitViewport } from '
|
|
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 '
|
|
2
|
-
import type { MapMarker } from '
|
|
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
|
|
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 '
|
|
1
|
+
import { svgElement } from '../dom.js';
|
|
2
2
|
/** Own marker visuals and entrances; selection/events remain with the controller. */
|
|
3
|
-
export function
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
46
|
-
layers:
|
|
47
|
-
controls:
|
|
47
|
+
features: configuration.current.features,
|
|
48
|
+
layers: configuration.current.layers,
|
|
49
|
+
controls: configuration.current.controls,
|
|
48
50
|
}),
|
|
49
|
-
appearance: copyAppearance(
|
|
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
|
-
(
|
|
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,
|
|
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
|
-
|
|
222
|
+
configuration.invalidate();
|
|
246
223
|
element.setDistrictYear(...args);
|
|
247
224
|
}),
|
|
248
225
|
setDistrictStyle: use((style) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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 '
|
|
2
|
-
import type { DistrictRow } from '
|
|
3
|
-
import type { Project } from '
|
|
4
|
-
import type { BartStation, KeyRoad, Landmark } from '
|
|
5
|
-
import type { MapMarker, SFMapOptions } from '
|
|
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))
|