@kahwee/sf-map-svg 4.3.2 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +18 -1
- package/dist/src/cards.d.ts +15 -0
- package/dist/src/cards.js +138 -0
- package/dist/src/cluster-layer.d.ts +2 -0
- package/dist/src/cluster-layer.js +2 -0
- package/dist/src/explorer-controls.d.ts +26 -0
- package/dist/src/explorer-controls.js +120 -0
- package/dist/src/explorer-core.d.ts +6 -1
- package/dist/src/explorer-core.js +168 -701
- package/dist/src/explorer-shell.d.ts +62 -0
- package/dist/src/explorer-shell.js +228 -0
- package/dist/src/explorer-sidebar.d.ts +20 -0
- package/dist/src/explorer-sidebar.js +79 -0
- package/dist/src/guide-data.js +1 -0
- package/dist/src/guide-map.js +2 -1
- package/dist/src/guide-static.js +1 -0
- package/dist/src/label-candidates.d.ts +24 -0
- package/dist/src/label-candidates.js +29 -0
- package/dist/src/label-renderer.d.ts +1 -10
- package/dist/src/label-spread.d.ts +5 -0
- package/dist/src/label-spread.js +36 -0
- package/dist/src/map-core.d.ts +3 -1
- package/dist/src/map-core.js +7 -5
- package/dist/src/map.js +2 -1
- package/dist/src/neighborhood-layer.d.ts +67 -0
- package/dist/src/neighborhood-layer.js +256 -0
- package/dist/src/overlay-layer.d.ts +13 -0
- package/dist/src/overlay-layer.js +68 -0
- package/dist/src/palettes.d.ts +48 -0
- package/dist/src/palettes.js +40 -0
- package/dist/src/presets.d.ts +1 -0
- package/dist/src/presets.js +1 -0
- package/dist/src/static-labels.d.ts +7 -0
- package/dist/src/static-labels.js +115 -0
- package/dist/src/static.js +31 -3
- package/dist/src/types.d.ts +16 -1
- package/docs/API.md +38 -2
- package/docs/EXAMPLES.md +1 -0
- package/docs/consumer-integration.md +81 -1
- package/docs/guide-bundle-report.md +4 -4
- package/llms-full.txt +274 -26
- package/llms.txt +1 -1
- package/package.json +16 -8
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { copyAppearance } from './appearance.js';
|
|
2
2
|
import { createCamera, validateCameraOptions } from './camera.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createClusterLayer } from './cluster-layer.js';
|
|
4
4
|
import { clusterPoints } from './clusters.js';
|
|
5
5
|
import { renderDistrictAppearance } from './district-layer.js';
|
|
6
6
|
import { createDistrictMorph } from './district-morph.js';
|
|
7
7
|
import { prepareDistrictStyles } from './district-style.js';
|
|
8
8
|
import { createDistrictTransition } from './district-transition.js';
|
|
9
9
|
import { element, setAttributeIfChanged, svgElement } from './dom.js';
|
|
10
|
-
import {
|
|
10
|
+
import { createExplorerControls } from './explorer-controls.js';
|
|
11
|
+
import { fitBounds, projectedBounds } from './explorer-layout.js';
|
|
11
12
|
import { controlKeys, layerKeys, normalizeFeatures, validateSwitchPatch } from './features.js';
|
|
12
13
|
import { createFrameScheduler } from './frame-scheduler.js';
|
|
13
|
-
import {
|
|
14
|
+
import { labelCandidates, styleLabels, } from './label-candidates.js';
|
|
14
15
|
import { createLabelRenderer } from './label-renderer.js';
|
|
15
16
|
import { createLayerTransitions } from './layer-transition.js';
|
|
16
17
|
import { createSFMapWithData, districtColors, getLayerPathsWithData, resolveMapColors, } from './map-core.js';
|
|
@@ -18,24 +19,12 @@ import { createMarkerCatalog } from './marker-catalog.js';
|
|
|
18
19
|
import { createMarkerChooser } from './marker-chooser.js';
|
|
19
20
|
import { createMarkerLayer } from './marker-layer.js';
|
|
20
21
|
import { createMarkerNavigation } from './marker-navigation.js';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
22
|
+
import { createNeighborhoodLayer, createNeighborhoodLookup, formatSourceLabel, } from './neighborhood-layer.js';
|
|
23
|
+
import { createOverlayLayer } from './overlay-layer.js';
|
|
24
|
+
import { validateExplorerOptions } from './validation.js';
|
|
23
25
|
import { fitViewport, validateViewport } from './viewport.js';
|
|
24
|
-
const sourceNames = {
|
|
25
|
-
realtor: 'SFAR realtor · 92 areas',
|
|
26
|
-
'sf-find': 'SF Find · 117 areas',
|
|
27
|
-
analysis: 'City analysis · 41 areas',
|
|
28
|
-
};
|
|
29
|
-
const normalizeName = (value) => value
|
|
30
|
-
.normalize('NFKD')
|
|
31
|
-
.toLowerCase()
|
|
32
|
-
.replace(/[\u0300-\u036f'’]/g, '')
|
|
33
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
34
|
-
.trim();
|
|
35
|
-
const formatSourceLabel = (source) => sourceNames[source] ?? `${source} neighborhoods`;
|
|
36
|
-
let explorerCount = 0;
|
|
37
26
|
/** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
|
|
38
|
-
export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
27
|
+
export function createNeighborhoodExplorerCore(options = {}, data, ui) {
|
|
39
28
|
validateExplorerOptions(options);
|
|
40
29
|
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;
|
|
41
30
|
if (!['neighborhoods', 'districts', 'basemap'].includes(mode))
|
|
@@ -94,28 +83,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
94
83
|
throw new RangeError('No neighborhood dataset was supplied.');
|
|
95
84
|
if (neighborhood === '')
|
|
96
85
|
neighborhood = undefined;
|
|
97
|
-
const getNeighborhood
|
|
98
|
-
const term = normalizeName(name);
|
|
99
|
-
if (!term)
|
|
100
|
-
return undefined;
|
|
101
|
-
return collections[selectedSource]?.features.find((feature) => [
|
|
102
|
-
feature.id,
|
|
103
|
-
feature.properties.canonicalName,
|
|
104
|
-
feature.properties.sourceName,
|
|
105
|
-
...feature.properties.aliases,
|
|
106
|
-
].some((value) => normalizeName(value) === term));
|
|
107
|
-
};
|
|
108
|
-
const searchNeighborhoods = (query, selectedSource) => {
|
|
109
|
-
const term = normalizeName(query);
|
|
110
|
-
return (collections[selectedSource]?.features ?? [])
|
|
111
|
-
.filter((feature) => [
|
|
112
|
-
feature.id,
|
|
113
|
-
feature.properties.canonicalName,
|
|
114
|
-
feature.properties.sourceName,
|
|
115
|
-
...feature.properties.aliases,
|
|
116
|
-
].some((value) => normalizeName(value).includes(term)))
|
|
117
|
-
.map((feature) => ({ id: feature.id, canonicalName: feature.properties.canonicalName }));
|
|
118
|
-
};
|
|
86
|
+
const { getNeighborhood, searchNeighborhoods } = createNeighborhoodLookup(collections);
|
|
119
87
|
if (neighborhood !== undefined && !getNeighborhood(neighborhood, source))
|
|
120
88
|
throw new RangeError(`Unknown neighborhood: ${neighborhood}`);
|
|
121
89
|
if (typeof document === 'undefined')
|
|
@@ -125,191 +93,18 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
125
93
|
function listen(node, event, handler, options = {}) {
|
|
126
94
|
node.addEventListener(event, (incoming) => handler(incoming), { ...options, signal: controller.signal });
|
|
127
95
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.sf-explorer button[aria-pressed=true]{color:#123d63;background:#e6f1f9;border-color:#5689b0;font-weight:650}
|
|
141
|
-
.sf-explorer :focus-visible{outline:3px solid var(--sf-map-focus);outline-offset:2px}
|
|
142
|
-
.sf-explorer-header{padding:24px;border-top:4px solid var(--sf-map-accent);border-bottom:1px solid var(--sf-map-border)}
|
|
143
|
-
.sf-explorer-header h2{font-size:25px;line-height:1.25;font-weight:700;letter-spacing:-.7px}
|
|
144
|
-
.sf-explorer-header p{color:#586f80;margin-top:8px;max-width:58ch}
|
|
145
|
-
.sf-explorer-body{display:grid;grid-template-columns:280px minmax(0,1fr);grid-template-areas:"panel map" "detail map";grid-template-rows:auto 1fr}
|
|
146
|
-
.sf-explorer-panel{grid-area:panel;padding:18px;display:flex;flex-direction:column;gap:14px;border-right:1px solid #dce5eb;min-width:0}
|
|
147
|
-
.sf-explorer-panel label{display:flex;flex-direction:column;gap:6px;font-size:12px;font-weight:650}
|
|
148
|
-
.sf-explorer-panel input,.sf-explorer-panel select{width:100%;font-weight:400;font-size:14px}
|
|
149
|
-
.sf-explorer-panel input::placeholder{color:#6a7f8e}
|
|
150
|
-
.sf-explorer-results{display:flex;flex-direction:column;gap:5px;max-height:255px;overflow:auto;padding:3px;margin:-3px;overscroll-behavior:contain}
|
|
151
|
-
.sf-explorer-results button{text-align:left;flex-shrink:0}
|
|
152
|
-
.sf-explorer-count,.sf-explorer-note{font-size:12px;color:#586f80}
|
|
153
|
-
.sf-explorer-detail{grid-area:detail;border-top:1px solid #dce5eb;border-right:1px solid #dce5eb;padding:18px;overflow-wrap:anywhere;align-self:stretch;background:#fafcfd}
|
|
154
|
-
.sf-explorer-detail details{font-size:12px;margin-top:12px}
|
|
155
|
-
.sf-explorer-detail summary{cursor:pointer;color:#496578;min-height:32px}
|
|
156
|
-
.sf-explorer-detail details p{margin-top:8px}
|
|
157
|
-
.sf-explorer-detail h3{margin:0 0 7px;font-size:18px;line-height:1.3;letter-spacing:-.2px}
|
|
158
|
-
.sf-explorer-detail p{font-size:12px;margin-bottom:10px}
|
|
159
|
-
.sf-explorer-download{display:block;margin:8px 0;color:#12649c;text-underline-offset:3px}
|
|
160
|
-
.sf-explorer-map-column{grid-area:map;min-width:0;background:#e4f2f8}
|
|
161
|
-
.sf-explorer-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sf-map-control-gap);padding:12px;background:var(--sf-map-surface);border-bottom:1px solid var(--sf-map-border)}
|
|
162
|
-
.sf-explorer-control-group{display:flex;align-items:center;flex-wrap:wrap;gap:6px;max-width:100%}
|
|
163
|
-
.sf-explorer-pan-controls{margin-left:auto}
|
|
164
|
-
.sf-explorer-toolbar button{min-width:44px;padding:6px 10px}
|
|
165
|
-
.sf-explorer-zoom{font-size:12px;min-width:42px;text-align:center;font-variant-numeric:tabular-nums;color:#496578}
|
|
166
|
-
.sf-explorer-canvas{position:relative;aspect-ratio:1;overflow:hidden;touch-action:pan-y pinch-zoom}
|
|
167
|
-
.sf-marker-chooser{position:absolute;z-index:2;top:12px;right:12px;display:flex;flex-direction:column;gap:6px;box-sizing:border-box;width:280px;max-width:calc(100% - 24px);max-height:calc(100% - 24px);overflow:auto;padding:12px;border:1px solid var(--sf-map-border);border-radius:8px;background:var(--sf-map-surface);color:var(--sf-map-ink);box-shadow:0 6px 20px #0003}
|
|
168
|
-
.sf-marker-chooser button{min-height:44px;text-align:left;overflow-wrap:anywhere}
|
|
169
|
-
.sf-explorer-canvas>svg{display:block;width:100%;height:100%;max-width:none;cursor:grab;user-select:none}
|
|
170
|
-
.sf-explorer-canvas>svg:active{cursor:grabbing}
|
|
171
|
-
.sf-explorer-canvas path[data-neighborhood-id]{cursor:pointer}
|
|
172
|
-
.sf-explorer-legend{display:flex;flex-wrap:wrap;gap:8px 20px;padding:14px 16px 8px;background:var(--sf-map-surface);border-top:1px solid #dce5eb;color:#496578;font-size:12px}
|
|
173
|
-
.sf-explorer-legend-item{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
|
|
174
|
-
.sf-explorer-legend-symbol{display:inline-block;flex:none;width:15px;height:11px;border-radius:2px}
|
|
175
|
-
.sf-explorer-legend-bart{width:10px;height:10px;margin:0 2px;border:2px solid #0073ae;border-radius:50%;background:#fff}
|
|
176
|
-
.sf-explorer-legend-park{background:#c6dfbd;border:1px solid #a8c69d}
|
|
177
|
-
.sf-explorer-legend-road{height:2px;border-radius:2px;background:#bcc3c5}
|
|
178
|
-
.sf-explorer-legend-highway{height:3px;border-radius:2px;background:#b9a18a}
|
|
179
|
-
.sf-explorer-hint{padding:6px 16px 10px;font-size:12px;color:#586f80;background:#fff}
|
|
180
|
-
.sf-explorer text{pointer-events:none}
|
|
181
|
-
.sf-explorer-status{padding:0 16px 14px;font-size:12px;background:var(--sf-map-surface);color:#496578}
|
|
182
|
-
.sf-explorer-visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
|
|
183
|
-
.sf-explorer[data-interface=map] .sf-explorer-body{display:block}
|
|
184
|
-
.sf-explorer[data-interface=map] .sf-explorer-header,.sf-explorer[data-interface=map] .sf-explorer-panel,.sf-explorer[data-interface=map] .sf-explorer-detail{display:none}
|
|
185
|
-
.sf-explorer-feature-controls{display:flex;flex-wrap:wrap;gap:8px;padding:10px;background:#fff}
|
|
186
|
-
.sf-explorer-feature-controls label{display:flex;flex:1;flex-direction:column;min-width:0}
|
|
187
|
-
.sf-explorer-feature-controls select{width:100%;min-width:0}
|
|
188
|
-
.sf-explorer svg [role=button]:focus-visible{outline:none;stroke:#102f72;stroke-width:3;fill-opacity:.3}
|
|
189
|
-
@media(prefers-reduced-motion:reduce){.sf-explorer button{transition:none}}
|
|
190
|
-
.sf-explorer button:disabled{opacity:.45;cursor:default}
|
|
191
|
-
@container sf-neighborhood-explorer (max-width:650px){
|
|
192
|
-
.sf-explorer-body{grid-template-columns:1fr;grid-template-areas:"panel" "map" "detail";grid-template-rows:auto auto auto}
|
|
193
|
-
.sf-explorer-header{padding:18px}
|
|
194
|
-
.sf-explorer-header h2{font-size:23px}
|
|
195
|
-
.sf-explorer-header p{font-size:13px}
|
|
196
|
-
.sf-explorer-panel{border-right:0;border-bottom:1px solid #dce5eb;padding:14px;gap:10px}
|
|
197
|
-
.sf-explorer-results{max-height:140px}
|
|
198
|
-
.sf-explorer-feature-controls{flex-direction:column}
|
|
199
|
-
.sf-explorer-detail{padding:14px;border-right:0}
|
|
200
|
-
.sf-explorer-toolbar{padding:8px;gap:6px}
|
|
201
|
-
.sf-explorer-control-group{gap:4px}
|
|
202
|
-
.sf-explorer-pan-controls{flex-basis:100%;margin-left:0}
|
|
203
|
-
.sf-explorer-toolbar button{padding:5px 8px}
|
|
204
|
-
.sf-explorer-legend{gap:8px 16px;padding:12px 14px 8px}
|
|
205
|
-
.sf-explorer-download{display:inline-block;margin:4px 14px 4px 0}
|
|
206
|
-
}`;
|
|
207
|
-
root.dataset.interface = chrome;
|
|
208
|
-
root.style.setProperty('--sf-map-ink', styleOptions.ink ?? '#18364f');
|
|
209
|
-
root.style.setProperty('--sf-map-surface', styleOptions.surface ?? '#fff');
|
|
210
|
-
root.style.setProperty('--sf-map-accent', styleOptions.accent ?? '#163d61');
|
|
211
|
-
root.style.setProperty('--sf-map-border', styleOptions.border ?? '#cedae3');
|
|
212
|
-
root.style.setProperty('--sf-map-focus', styleOptions.focus ?? '#1676b8');
|
|
213
|
-
root.style.setProperty('--sf-map-control-gap', styleOptions.controlGap ?? '6px');
|
|
214
|
-
root.style.setProperty('--sf-map-font', styleOptions.font ?? 'system-ui,sans-serif');
|
|
215
|
-
root.append(style);
|
|
216
|
-
const header = element('header', '', 'sf-explorer-header');
|
|
217
|
-
header.append(element('h2', strings.title ?? 'Explore San Francisco'), element('p', 'Explore the city, from familiar names to the places in between.'));
|
|
218
|
-
root.append(header);
|
|
219
|
-
const body = element('div', '', 'sf-explorer-body');
|
|
220
|
-
const panel = element('div', '', 'sf-explorer-panel');
|
|
221
|
-
const modeLabel = element('label', strings.mode ?? 'Map mode');
|
|
222
|
-
const modeSelect = element('select');
|
|
223
|
-
for (const [value, title] of [
|
|
224
|
-
['neighborhoods', 'Neighborhoods'],
|
|
225
|
-
['districts', 'Districts'],
|
|
226
|
-
['basemap', 'Basemap'],
|
|
227
|
-
]) {
|
|
228
|
-
if (value === 'districts' && !data.districts?.[year])
|
|
229
|
-
continue;
|
|
230
|
-
if (value === 'neighborhoods' && !sources.length)
|
|
231
|
-
continue;
|
|
232
|
-
const option = element('option', title);
|
|
233
|
-
option.value = value;
|
|
234
|
-
modeSelect.append(option);
|
|
235
|
-
}
|
|
236
|
-
modeLabel.append(modeSelect);
|
|
237
|
-
const sourceLabel = element('label', strings.source ?? 'Neighborhood definitions');
|
|
238
|
-
const sourceSelect = element('select');
|
|
239
|
-
for (const value of sources) {
|
|
240
|
-
const title = sourceNames[value];
|
|
241
|
-
const option = element('option', title);
|
|
242
|
-
option.value = value;
|
|
243
|
-
sourceSelect.append(option);
|
|
244
|
-
}
|
|
245
|
-
sourceSelect.value = source;
|
|
246
|
-
sourceLabel.append(sourceSelect);
|
|
247
|
-
const searchLabel = element('label', strings.search ?? 'Find a neighborhood');
|
|
248
|
-
const search = element('input');
|
|
249
|
-
search.type = 'search';
|
|
250
|
-
search.placeholder = 'Try Mission, Outer Mission, or NoPa';
|
|
251
|
-
search.autocomplete = 'off';
|
|
252
|
-
searchLabel.append(search);
|
|
253
|
-
const count = element('p', '', 'sf-explorer-count');
|
|
254
|
-
count.setAttribute('aria-live', 'polite');
|
|
255
|
-
const results = element('div', '', 'sf-explorer-results');
|
|
256
|
-
results.setAttribute('aria-label', 'Matching neighborhoods');
|
|
257
|
-
results.setAttribute('role', 'group');
|
|
258
|
-
const detail = element('div', '', 'sf-explorer-detail');
|
|
259
|
-
panel.append(modeLabel, sourceLabel, searchLabel, count, results);
|
|
260
|
-
const column = element('div', '', 'sf-explorer-map-column');
|
|
261
|
-
const toolbar = element('div', '', 'sf-explorer-toolbar');
|
|
262
|
-
toolbar.setAttribute('aria-label', 'Map controls');
|
|
263
|
-
const canvas = element('div', '', 'sf-explorer-canvas');
|
|
264
|
-
const hint = element('p', strings.gestureHelp ??
|
|
265
|
-
'Drag to pan · Ctrl/⌘ + scroll to zoom · Focus map: arrows pan, +/− zoom, Home resets. Touch: scroll the page, or enable Touch navigation to pan and pinch the map. Escape exits.', 'sf-explorer-hint');
|
|
266
|
-
hint.id = `sf-explorer-help-${++explorerCount}`;
|
|
267
|
-
const status = element('p', '', 'sf-explorer-status');
|
|
268
|
-
status.setAttribute('aria-live', 'polite');
|
|
269
|
-
const legend = element('div', '', 'sf-explorer-legend');
|
|
270
|
-
legend.setAttribute('aria-label', 'Map legend');
|
|
271
|
-
for (const [kind, label] of [
|
|
272
|
-
['bart', 'BART station'],
|
|
273
|
-
['park', 'Park'],
|
|
274
|
-
['highway', 'Highway'],
|
|
275
|
-
['road', 'Roads'],
|
|
276
|
-
]) {
|
|
277
|
-
if (legendOptions.builtins === false || legendOptions.hidden?.includes(kind))
|
|
278
|
-
continue;
|
|
279
|
-
const entry = element('span', '', 'sf-explorer-legend-item');
|
|
280
|
-
const symbol = element('span', '', `sf-explorer-legend-symbol sf-explorer-legend-${kind}`);
|
|
281
|
-
symbol.setAttribute('aria-hidden', 'true');
|
|
282
|
-
if (colors[kind]) {
|
|
283
|
-
if (kind === 'bart')
|
|
284
|
-
symbol.style.borderColor = colors[kind];
|
|
285
|
-
else
|
|
286
|
-
symbol.style.backgroundColor = colors[kind];
|
|
287
|
-
}
|
|
288
|
-
entry.append(symbol, document.createTextNode(label));
|
|
289
|
-
legend.append(entry);
|
|
290
|
-
}
|
|
291
|
-
for (const item of legendOptions.items ?? []) {
|
|
292
|
-
const entry = element('span', '', 'sf-explorer-legend-item');
|
|
293
|
-
const symbol = element('span', '', 'sf-explorer-legend-symbol');
|
|
294
|
-
symbol.style.backgroundColor = item.color;
|
|
295
|
-
symbol.setAttribute('aria-hidden', 'true');
|
|
296
|
-
entry.append(symbol, document.createTextNode(item.label));
|
|
297
|
-
legend.append(entry);
|
|
298
|
-
}
|
|
299
|
-
const featureControls = element('div', '', 'sf-explorer-feature-controls');
|
|
300
|
-
const neighborhoodLabel = element('label', strings.chooseNeighborhood ?? 'Select neighborhood');
|
|
301
|
-
const neighborhoodSelect = element('select');
|
|
302
|
-
neighborhoodLabel.append(neighborhoodSelect);
|
|
303
|
-
const markerLabel = element('label', strings.chooseMarker ?? 'Choose marker');
|
|
304
|
-
const markerSelect = element('select');
|
|
305
|
-
markerLabel.append(markerSelect);
|
|
306
|
-
featureControls.append(neighborhoodLabel, markerLabel);
|
|
307
|
-
const attribution = element('p', '', 'sf-explorer-hint');
|
|
308
|
-
const attributionDisclosure = element('details', '', 'sf-explorer-hint');
|
|
309
|
-
attributionDisclosure.append(element('summary', 'Map data · DataSF / BART'), attribution);
|
|
310
|
-
column.append(toolbar, featureControls, canvas, legend, hint, status, attributionMode === 'compact' ? attributionDisclosure : attribution);
|
|
311
|
-
body.append(panel, column, detail);
|
|
312
|
-
root.append(body);
|
|
96
|
+
const shell = ui.createShell({
|
|
97
|
+
source,
|
|
98
|
+
sources,
|
|
99
|
+
hasDistricts: !!data.districts?.[year],
|
|
100
|
+
chrome,
|
|
101
|
+
styleOptions,
|
|
102
|
+
strings,
|
|
103
|
+
legendOptions,
|
|
104
|
+
colors,
|
|
105
|
+
attributionMode,
|
|
106
|
+
});
|
|
107
|
+
const { root, modeSelect, sourceLabel, sourceSelect, searchLabel, count, results, canvas, status, legend, featureControls, neighborhoodLabel, neighborhoodSelect, markerLabel, markerSelect, attribution, } = shell;
|
|
313
108
|
const map = createSFMapWithData({
|
|
314
109
|
year,
|
|
315
110
|
theme,
|
|
@@ -385,7 +180,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
385
180
|
const labelRenderer = createLabelRenderer(labelLayer);
|
|
386
181
|
geography.append(areas, stations, labelLayer, overlayLayer, markerLayer);
|
|
387
182
|
let view = [0, 0, 800];
|
|
388
|
-
let selected;
|
|
389
183
|
let selectedDistrict = null;
|
|
390
184
|
let hoveredDistrict = null;
|
|
391
185
|
let districtStyle = initialDistrictStyle;
|
|
@@ -551,8 +345,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
551
345
|
}));
|
|
552
346
|
}
|
|
553
347
|
updateDistrictAppearance();
|
|
554
|
-
let items = [];
|
|
555
|
-
let orderedNeighborhoods = [];
|
|
556
348
|
let destroyed = false;
|
|
557
349
|
let initialized = false;
|
|
558
350
|
const layerFades = createLayerTransitions(() => initialized && features.layerTransitions && !reducedMotion.matches
|
|
@@ -577,6 +369,49 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
577
369
|
});
|
|
578
370
|
const stopAnimation = camera.stop;
|
|
579
371
|
const moveView = camera.move;
|
|
372
|
+
const neighborhoods = createNeighborhoodLayer({
|
|
373
|
+
collections,
|
|
374
|
+
initialSource: source,
|
|
375
|
+
project: map.project,
|
|
376
|
+
root,
|
|
377
|
+
areas,
|
|
378
|
+
canvas,
|
|
379
|
+
sourceSelect,
|
|
380
|
+
neighborhoodSelect,
|
|
381
|
+
status,
|
|
382
|
+
readPresentation: () => ({
|
|
383
|
+
mode,
|
|
384
|
+
year,
|
|
385
|
+
fitPadding,
|
|
386
|
+
areaStyle,
|
|
387
|
+
colors,
|
|
388
|
+
selectableNeighborhoods,
|
|
389
|
+
lines: enabled('neighborhoodLines'),
|
|
390
|
+
}),
|
|
391
|
+
isDestroyed: () => destroyed,
|
|
392
|
+
ensureVisible() {
|
|
393
|
+
if (!enabled('neighborhoodLines') &&
|
|
394
|
+
!enabled('neighborhoodLabels') &&
|
|
395
|
+
(layers.neighborhoodLines === undefined || layers.neighborhoodLabels === undefined))
|
|
396
|
+
setMode('neighborhoods', false);
|
|
397
|
+
},
|
|
398
|
+
refresh: {
|
|
399
|
+
selection() {
|
|
400
|
+
scheduleLabels();
|
|
401
|
+
updateResults();
|
|
402
|
+
updateDetail();
|
|
403
|
+
},
|
|
404
|
+
source() {
|
|
405
|
+
updateResults();
|
|
406
|
+
updateDetail();
|
|
407
|
+
updateComposition();
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
moveView,
|
|
411
|
+
resetView,
|
|
412
|
+
crossfade: layerFades.crossfade,
|
|
413
|
+
});
|
|
414
|
+
const { getSelection, selectNeighborhood, prepareSource, setSource } = neighborhoods;
|
|
580
415
|
function getFeatures() {
|
|
581
416
|
return structuredClone(features);
|
|
582
417
|
}
|
|
@@ -610,7 +445,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
610
445
|
}
|
|
611
446
|
scheduleLabels();
|
|
612
447
|
}
|
|
613
|
-
const downloads = new Set();
|
|
614
448
|
const stationItems = (data.map.bartStations ?? []).map((feature) => {
|
|
615
449
|
return {
|
|
616
450
|
point: map.project(feature.coordinates),
|
|
@@ -660,7 +494,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
660
494
|
?.visual?.node.focus({ preventScroll: true });
|
|
661
495
|
});
|
|
662
496
|
let markerRevision = 0;
|
|
663
|
-
let neighborhoodRevision = 0;
|
|
664
497
|
const clusterLayer = svgElement('g', { 'data-layer': 'marker-clusters' });
|
|
665
498
|
geography.insertBefore(clusterLayer, markerLayer);
|
|
666
499
|
function materializeMarker(item) {
|
|
@@ -674,7 +507,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
674
507
|
}),
|
|
675
508
|
});
|
|
676
509
|
}
|
|
677
|
-
const clusters =
|
|
510
|
+
const clusters = createClusterLayer(svg, markerLayer, clusterLayer, markerNavigation, materializeMarker, (group, node, signal) => {
|
|
678
511
|
if (destroyed || signal.aborted)
|
|
679
512
|
return;
|
|
680
513
|
const markers = group.map((item) => ({ ...item.marker }));
|
|
@@ -711,86 +544,39 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
711
544
|
renderedMarkers = rendered.markers;
|
|
712
545
|
renderedClusters = rendered.clusters;
|
|
713
546
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return;
|
|
748
|
-
onOverlayActivate(structuredClone(overlay));
|
|
749
|
-
if (destroyed || events.signal.aborted)
|
|
750
|
-
return;
|
|
751
|
-
root.dispatchEvent(new CustomEvent('overlayactivate', {
|
|
752
|
-
bubbles: true,
|
|
753
|
-
detail: { overlay: structuredClone(overlay) },
|
|
754
|
-
}));
|
|
755
|
-
};
|
|
756
|
-
node.addEventListener('click', activate, { signal: events.signal });
|
|
757
|
-
node.addEventListener('keydown', (event) => {
|
|
758
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
759
|
-
event.preventDefault();
|
|
760
|
-
activate();
|
|
761
|
-
}
|
|
762
|
-
}, { signal: events.signal });
|
|
763
|
-
}
|
|
764
|
-
return node;
|
|
765
|
-
});
|
|
766
|
-
overlayEvents.abort();
|
|
767
|
-
overlayEvents = events;
|
|
768
|
-
overlayLayer.replaceChildren(...next);
|
|
769
|
-
}
|
|
770
|
-
const zoomControls = element('div', '', 'sf-explorer-control-group');
|
|
771
|
-
const panControls = element('div', '', 'sf-explorer-control-group sf-explorer-pan-controls');
|
|
772
|
-
toolbar.append(zoomControls, panControls);
|
|
773
|
-
function button(text, label, action, group = zoomControls) {
|
|
774
|
-
const node = element('button', text);
|
|
775
|
-
node.type = 'button';
|
|
776
|
-
node.setAttribute('aria-label', label);
|
|
777
|
-
listen(node, 'click', () => action());
|
|
778
|
-
group.append(node);
|
|
779
|
-
return node;
|
|
780
|
-
}
|
|
781
|
-
const zoomOut = button('−', 'Zoom out', () => zoomBy(1 / 1.5));
|
|
782
|
-
const zoomText = element('span', '100%', 'sf-explorer-zoom');
|
|
783
|
-
zoomControls.append(zoomText);
|
|
784
|
-
const zoomIn = button('+', 'Zoom in', () => zoomBy(1.5));
|
|
785
|
-
const resetButton = button(strings.reset ?? 'Reset', strings.reset ?? 'Reset map to city view', resetView);
|
|
786
|
-
const labelsButton = button('Labels', 'Show map labels', () => setLabels(!labels));
|
|
787
|
-
for (const [symbol, label, dx, dy] of [
|
|
788
|
-
['←', 'Pan west', -1, 0],
|
|
789
|
-
['↑', 'Pan north', 0, -1],
|
|
790
|
-
['↓', 'Pan south', 0, 1],
|
|
791
|
-
['→', 'Pan east', 1, 0],
|
|
792
|
-
])
|
|
793
|
-
button(symbol, label, () => moveView([view[0] + (dx * view[2]) / 4, view[1] + (dy * view[2]) / 4, view[2]]), panControls);
|
|
547
|
+
const overlays = createOverlayLayer({
|
|
548
|
+
layer: overlayLayer,
|
|
549
|
+
root,
|
|
550
|
+
project: map.project,
|
|
551
|
+
onOverlayActivate,
|
|
552
|
+
isDestroyed: () => destroyed,
|
|
553
|
+
});
|
|
554
|
+
const setOverlays = overlays.set;
|
|
555
|
+
const mapControls = createExplorerControls(shell, {
|
|
556
|
+
svg,
|
|
557
|
+
signal: controller.signal,
|
|
558
|
+
strings,
|
|
559
|
+
isDestroyed: () => destroyed,
|
|
560
|
+
getLabels: () => labels,
|
|
561
|
+
getViewport,
|
|
562
|
+
moveView,
|
|
563
|
+
zoomBy,
|
|
564
|
+
resetView,
|
|
565
|
+
setLabels,
|
|
566
|
+
stopAnimation,
|
|
567
|
+
setView,
|
|
568
|
+
setMode,
|
|
569
|
+
setSource,
|
|
570
|
+
selectNeighborhood,
|
|
571
|
+
selectMarker,
|
|
572
|
+
updateResults,
|
|
573
|
+
search(query) {
|
|
574
|
+
const exact = getNeighborhood(query, neighborhoods.source);
|
|
575
|
+
const matches = searchNeighborhoods(query, neighborhoods.source);
|
|
576
|
+
return exact?.id ?? (matches.length === 1 ? matches[0].id : undefined);
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
const setTouchNavigation = mapControls.setTouchNavigation;
|
|
794
580
|
function panBy(x, y, options = {}) {
|
|
795
581
|
if (destroyed)
|
|
796
582
|
return;
|
|
@@ -820,10 +606,8 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
820
606
|
view = validateViewport(next);
|
|
821
607
|
svg.setAttribute('viewBox', `${view[0]} ${view[1]} ${view[2]} ${view[2]}`);
|
|
822
608
|
const zoom = 800 / view[2];
|
|
823
|
-
|
|
609
|
+
mapControls.updateZoom(zoom);
|
|
824
610
|
root.dataset.zoom = String(zoom);
|
|
825
|
-
zoomIn.disabled = zoom >= 12;
|
|
826
|
-
zoomOut.disabled = zoom <= 1;
|
|
827
611
|
if (view.some((value, i) => value !== previous[i]))
|
|
828
612
|
root.dispatchEvent(new CustomEvent('viewportchange', { bubbles: true, detail: { viewport: getViewport() } }));
|
|
829
613
|
scheduleLabels();
|
|
@@ -849,8 +633,8 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
849
633
|
function prepareLabels(width, zoom) {
|
|
850
634
|
// Only screen-space positioning changes during pan or within these zoom/width bands.
|
|
851
635
|
const inputs = [
|
|
852
|
-
items,
|
|
853
|
-
selected,
|
|
636
|
+
neighborhoods.items,
|
|
637
|
+
neighborhoods.selected,
|
|
854
638
|
districtItems,
|
|
855
639
|
markerRevision,
|
|
856
640
|
layers,
|
|
@@ -866,7 +650,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
866
650
|
if (inputs.every((value, index) => value === labelInputs[index]))
|
|
867
651
|
return preparedLabels;
|
|
868
652
|
labelInputs = inputs;
|
|
869
|
-
const selectedItem = items.find((item) => item.feature === selected);
|
|
653
|
+
const selectedItem = neighborhoods.items.find((item) => item.feature === neighborhoods.selected);
|
|
870
654
|
const candidates = [];
|
|
871
655
|
const marker = markerCatalog.items.find((item) => item.marker.id === selectedMarker);
|
|
872
656
|
if (marker)
|
|
@@ -881,46 +665,31 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
881
665
|
name: selectedItem.feature.properties.canonicalName,
|
|
882
666
|
kind: 'selected',
|
|
883
667
|
});
|
|
884
|
-
|
|
885
|
-
candidates
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
(
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
fontWeight: labelStyle.fontWeight ??
|
|
910
|
-
(item.kind.startsWith('selected') || item.kind === 'district' ? 700 : 550),
|
|
911
|
-
fill: colors.label ??
|
|
912
|
-
(item.kind === 'park'
|
|
913
|
-
? (colors.landmark ?? '#426641')
|
|
914
|
-
: item.kind === 'road'
|
|
915
|
-
? '#77736b'
|
|
916
|
-
: '#163d61'),
|
|
917
|
-
halo: labelStyle.haloColor ?? '#ffffff',
|
|
918
|
-
offset: item.kind === 'selected-marker'
|
|
919
|
-
? (marker?.marker.radius ?? markerRadius) + 9
|
|
920
|
-
: item.kind === 'bart'
|
|
921
|
-
? 11
|
|
922
|
-
: 0,
|
|
923
|
-
};
|
|
668
|
+
preparedLabels = styleLabels(labelCandidates({
|
|
669
|
+
selected: candidates,
|
|
670
|
+
districts: enabled('districtLabels') ? districtItems : [],
|
|
671
|
+
stations: enabled('bartStations') ? stationItems : [],
|
|
672
|
+
parks: enabled('landmarks') ? parkItems : [],
|
|
673
|
+
roads: enabled('roadLabels') ? roadItems : [],
|
|
674
|
+
neighborhoods: enabled('neighborhoodLabels')
|
|
675
|
+
? neighborhoods.items
|
|
676
|
+
.filter((item) => item.point !== undefined)
|
|
677
|
+
.sort((a, b) => b.area - a.area || a.feature.id.localeCompare(b.feature.id))
|
|
678
|
+
.flatMap((item) => item.feature !== neighborhoods.selected && item.point
|
|
679
|
+
? [
|
|
680
|
+
{
|
|
681
|
+
point: item.point,
|
|
682
|
+
name: item.feature.properties.canonicalName,
|
|
683
|
+
kind: 'neighborhood',
|
|
684
|
+
},
|
|
685
|
+
]
|
|
686
|
+
: [])
|
|
687
|
+
: [],
|
|
688
|
+
}, width, zoom), {
|
|
689
|
+
colors,
|
|
690
|
+
labelStyle,
|
|
691
|
+
labelSize: { min: minLabelSize, max: maxLabelSize },
|
|
692
|
+
markerRadius: marker?.marker.radius ?? markerRadius,
|
|
924
693
|
});
|
|
925
694
|
return preparedLabels;
|
|
926
695
|
}
|
|
@@ -991,260 +760,22 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
991
760
|
}),
|
|
992
761
|
]));
|
|
993
762
|
}
|
|
994
|
-
|
|
995
|
-
for (const url of downloads)
|
|
996
|
-
URL.revokeObjectURL(url);
|
|
997
|
-
downloads.clear();
|
|
998
|
-
}
|
|
999
|
-
function downloadLink(text, value, filename) {
|
|
1000
|
-
const link = element('a', text, 'sf-explorer-download');
|
|
1001
|
-
const url = URL.createObjectURL(new Blob([`${JSON.stringify(value, null, 2)}\n`], { type: 'application/geo+json' }));
|
|
1002
|
-
downloads.add(url);
|
|
1003
|
-
link.href = url;
|
|
1004
|
-
link.download = filename;
|
|
1005
|
-
return link;
|
|
1006
|
-
}
|
|
763
|
+
const sidebar = ui.createSidebar?.(shell, data, chrome, strings.emptyResults, searchNeighborhoods);
|
|
1007
764
|
function updateDetail() {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (!collection) {
|
|
1015
|
-
detail.append(element('h3', `${year} supervisorial districts`));
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
detail.append(element('h3', `${year} supervisorial districts`), element('p', 'Eleven districts, identified by number. Switch to Neighborhoods to explore familiar place names.'), downloadLink('Download district GeoJSON', collection, `districts-${year}.geojson`), element('p', collection.definition.description));
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
if (mode === 'basemap') {
|
|
1022
|
-
detail.append(element('h3', 'San Francisco basemap'));
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
const collection = collections[source];
|
|
1026
|
-
if (!collection)
|
|
1027
|
-
return;
|
|
1028
|
-
detail.append(element('h3', selected ? selected.properties.canonicalName : 'Explore all neighborhoods'));
|
|
1029
|
-
const description = selected
|
|
1030
|
-
? `Source name: ${selected.properties.sourceName}${selected.properties.aliases.length ? `. Also known as: ${selected.properties.aliases.join(', ')}` : ''}.`
|
|
1031
|
-
: 'Choose a name or click an area to highlight its full boundary.';
|
|
1032
|
-
const sourceSummary = {
|
|
1033
|
-
realtor: 'SFAR realtor boundaries · August 2010 · 92 distinct areas.',
|
|
1034
|
-
'sf-find': 'SF Find neighborhoods · 2006 · 117 approximate areas.',
|
|
1035
|
-
analysis: 'City analysis neighborhoods · 41 census-tract-based areas.',
|
|
1036
|
-
};
|
|
1037
|
-
detail.append(element('p', description), element('p', sourceSummary[source]));
|
|
1038
|
-
if (selected)
|
|
1039
|
-
detail.append(downloadLink('Download neighborhood GeoJSON', { ...collection, selection: selected.id, features: [selected] }, `${source}-${selected.id}.geojson`));
|
|
1040
|
-
detail.append(downloadLink(`Download all ${collection.features.length} areas`, collection, `neighborhoods-${source}.geojson`));
|
|
1041
|
-
const provenance = element('details');
|
|
1042
|
-
provenance.append(element('summary', 'About these boundaries'), element('p', collection.definition.description));
|
|
1043
|
-
detail.append(provenance);
|
|
765
|
+
sidebar?.updateDetail({
|
|
766
|
+
mode,
|
|
767
|
+
year,
|
|
768
|
+
source: neighborhoods.source,
|
|
769
|
+
selected: neighborhoods.selected,
|
|
770
|
+
});
|
|
1044
771
|
}
|
|
1045
772
|
function updateResults() {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
: undefined;
|
|
1052
|
-
const matches = searchNeighborhoods(search.value, source).sort((a, b) => a.canonicalName.localeCompare(b.canonicalName));
|
|
1053
|
-
count.textContent = `${matches.length} ${matches.length === 1 ? 'neighborhood' : 'neighborhoods'}${search.value ? ' found' : ' in this collection'}`;
|
|
1054
|
-
results.replaceChildren();
|
|
1055
|
-
for (const match of matches) {
|
|
1056
|
-
const node = element('button', match.canonicalName);
|
|
1057
|
-
node.type = 'button';
|
|
1058
|
-
node.dataset.neighborhoodResult = match.id;
|
|
1059
|
-
node.setAttribute('aria-pressed', String(selected?.id === match.id));
|
|
1060
|
-
results.append(node);
|
|
1061
|
-
if (focusedId === match.id)
|
|
1062
|
-
node.focus({ preventScroll: true });
|
|
1063
|
-
}
|
|
1064
|
-
if (!matches.length)
|
|
1065
|
-
results.append(element('p', strings.emptyResults ?? 'No matches. Try another name or definition source.', 'sf-explorer-note'));
|
|
1066
|
-
}
|
|
1067
|
-
function getSelection() {
|
|
1068
|
-
return selected
|
|
1069
|
-
? {
|
|
1070
|
-
id: selected.id,
|
|
1071
|
-
name: selected.properties.canonicalName,
|
|
1072
|
-
source,
|
|
1073
|
-
feature: structuredClone(selected),
|
|
1074
|
-
}
|
|
1075
|
-
: null;
|
|
1076
|
-
}
|
|
1077
|
-
function selectNeighborhood(name, options = {}) {
|
|
1078
|
-
if (destroyed)
|
|
1079
|
-
return false;
|
|
1080
|
-
const feature = name === null
|
|
1081
|
-
? undefined
|
|
1082
|
-
: (collections[source]?.features.find((entry) => entry.id === name) ??
|
|
1083
|
-
getNeighborhood(name, source));
|
|
1084
|
-
if (name !== null && !feature)
|
|
1085
|
-
return false;
|
|
1086
|
-
validateCameraOptions(options);
|
|
1087
|
-
const selectedItem = items.find((item) => item.feature === feature);
|
|
1088
|
-
const width = canvas.getBoundingClientRect().width;
|
|
1089
|
-
const target = selectedItem && options.fit !== false
|
|
1090
|
-
? width
|
|
1091
|
-
? fitViewport(selectedItem.bounds, width, fitPadding)
|
|
1092
|
-
: fitBounds(selectedItem.bounds)
|
|
1093
|
-
: null;
|
|
1094
|
-
if (feature &&
|
|
1095
|
-
!enabled('neighborhoodLines') &&
|
|
1096
|
-
!enabled('neighborhoodLabels') &&
|
|
1097
|
-
(layers.neighborhoodLines === undefined || layers.neighborhoodLabels === undefined))
|
|
1098
|
-
setMode('neighborhoods', false);
|
|
1099
|
-
const revision = ++neighborhoodRevision;
|
|
1100
|
-
const changed = selected !== feature;
|
|
1101
|
-
selected = feature;
|
|
1102
|
-
for (const item of items) {
|
|
1103
|
-
const active = item.feature === selected;
|
|
1104
|
-
item.node.setAttribute('fill', active ? (areaStyle.selectedFill ?? '#408dbe') : 'transparent');
|
|
1105
|
-
item.node.setAttribute('fill-opacity', active ? '.16' : '1');
|
|
1106
|
-
item.node.setAttribute('stroke', active ? (areaStyle.selectedStroke ?? '#176ba2') : (colors.neighborhood ?? '#9caebc'));
|
|
1107
|
-
item.node.setAttribute('stroke-width', active ? '2.2' : enabled('neighborhoodLines') ? '.55' : '0');
|
|
1108
|
-
if (selectableNeighborhoods)
|
|
1109
|
-
item.node.setAttribute('aria-pressed', String(active));
|
|
1110
|
-
if (active)
|
|
1111
|
-
areas.append(item.node);
|
|
1112
|
-
}
|
|
1113
|
-
if (feature)
|
|
1114
|
-
root.dataset.selectedNeighborhood = feature.id;
|
|
1115
|
-
else
|
|
1116
|
-
delete root.dataset.selectedNeighborhood;
|
|
1117
|
-
neighborhoodSelect.value = feature?.id ?? '';
|
|
1118
|
-
scheduleLabels();
|
|
1119
|
-
updateResults();
|
|
1120
|
-
updateDetail();
|
|
1121
|
-
status.textContent = feature
|
|
1122
|
-
? `${feature.properties.canonicalName} selected. ${formatSourceLabel(source)}.`
|
|
1123
|
-
: 'Neighborhood selection cleared.';
|
|
1124
|
-
if (changed)
|
|
1125
|
-
root.dispatchEvent(new CustomEvent('neighborhoodchange', {
|
|
1126
|
-
bubbles: true,
|
|
1127
|
-
detail: {
|
|
1128
|
-
feature: feature ? structuredClone(feature) : null,
|
|
1129
|
-
source,
|
|
1130
|
-
id: feature?.id ?? null,
|
|
1131
|
-
name: feature?.properties.canonicalName ?? null,
|
|
1132
|
-
},
|
|
1133
|
-
}));
|
|
1134
|
-
if (!destroyed && neighborhoodRevision === revision && target)
|
|
1135
|
-
moveView(target, options);
|
|
1136
|
-
return true;
|
|
1137
|
-
}
|
|
1138
|
-
function prepareSource(next, reset = true) {
|
|
1139
|
-
if (!Object.hasOwn(collections, next))
|
|
1140
|
-
throw new RangeError(`Unknown neighborhood source: ${next}`);
|
|
1141
|
-
const collection = collections[next];
|
|
1142
|
-
if (!collection ||
|
|
1143
|
-
!Array.isArray(collection.features) ||
|
|
1144
|
-
typeof collection.title !== 'string' ||
|
|
1145
|
-
typeof collection.definition?.description !== 'string')
|
|
1146
|
-
throw new TypeError('Neighborhood collections require features, title and definition description.');
|
|
1147
|
-
const ids = new Set();
|
|
1148
|
-
const nextOptions = [];
|
|
1149
|
-
const nextItems = collection.features.map((feature) => {
|
|
1150
|
-
if (!feature ||
|
|
1151
|
-
typeof feature.id !== 'string' ||
|
|
1152
|
-
!feature.id ||
|
|
1153
|
-
ids.has(feature.id) ||
|
|
1154
|
-
!feature.properties ||
|
|
1155
|
-
typeof feature.properties.canonicalName !== 'string' ||
|
|
1156
|
-
typeof feature.properties.sourceName !== 'string' ||
|
|
1157
|
-
!Array.isArray(feature.properties.aliases) ||
|
|
1158
|
-
Array.from(feature.properties.aliases).some((alias) => typeof alias !== 'string') ||
|
|
1159
|
-
!['Polygon', 'MultiPolygon'].includes(feature.geometry?.type))
|
|
1160
|
-
throw new TypeError('Neighborhood features require unique IDs, names, aliases and polygon geometry.');
|
|
1161
|
-
ids.add(feature.id);
|
|
1162
|
-
const bounds = projectedBounds(feature.geometry, map.project);
|
|
1163
|
-
if (!bounds.every(Number.isFinite))
|
|
1164
|
-
throw new RangeError('Neighborhood geometry must be nonempty and finite.');
|
|
1165
|
-
const node = svgElement('path', {
|
|
1166
|
-
d: geometryPath(feature.geometry, map.project),
|
|
1167
|
-
'data-neighborhood-id': feature.id,
|
|
1168
|
-
fill: 'transparent',
|
|
1169
|
-
'fill-rule': 'evenodd',
|
|
1170
|
-
stroke: colors.neighborhood ?? '#9caebc',
|
|
1171
|
-
'stroke-width': '.55',
|
|
1172
|
-
'vector-effect': 'non-scaling-stroke',
|
|
1173
|
-
role: 'button',
|
|
1174
|
-
tabindex: -1,
|
|
1175
|
-
'aria-label': feature.properties.canonicalName,
|
|
1176
|
-
'aria-pressed': 'false',
|
|
1177
|
-
});
|
|
1178
|
-
const option = element('option', feature.properties.canonicalName);
|
|
1179
|
-
option.value = feature.id;
|
|
1180
|
-
nextOptions.push(option);
|
|
1181
|
-
const title = svgElement('title');
|
|
1182
|
-
title.textContent = feature.properties.canonicalName;
|
|
1183
|
-
node.append(title);
|
|
1184
|
-
node.addEventListener('pointerenter', () => {
|
|
1185
|
-
if (destroyed ||
|
|
1186
|
-
!selectableNeighborhoods ||
|
|
1187
|
-
selected?.id === feature.id ||
|
|
1188
|
-
(!areaStyle.hoverFill && !areaStyle.hoverStroke))
|
|
1189
|
-
return;
|
|
1190
|
-
node.setAttribute('fill', areaStyle.hoverFill ?? '#408dbe');
|
|
1191
|
-
node.setAttribute('fill-opacity', '.12');
|
|
1192
|
-
node.setAttribute('stroke', areaStyle.hoverStroke ?? colors.neighborhood ?? '#176ba2');
|
|
1193
|
-
});
|
|
1194
|
-
node.addEventListener('pointerleave', () => {
|
|
1195
|
-
if (destroyed || selected?.id === feature.id)
|
|
1196
|
-
return;
|
|
1197
|
-
node.setAttribute('fill', 'transparent');
|
|
1198
|
-
node.setAttribute('fill-opacity', '1');
|
|
1199
|
-
node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
|
|
1200
|
-
});
|
|
1201
|
-
return {
|
|
1202
|
-
feature,
|
|
1203
|
-
node,
|
|
1204
|
-
bounds,
|
|
1205
|
-
point: interiorAnchor(feature.geometry, map.project),
|
|
1206
|
-
area: (bounds[2] - bounds[0]) * (bounds[3] - bounds[1]),
|
|
1207
|
-
};
|
|
773
|
+
sidebar?.updateResults({
|
|
774
|
+
mode,
|
|
775
|
+
year,
|
|
776
|
+
source: neighborhoods.source,
|
|
777
|
+
selected: neighborhoods.selected,
|
|
1208
778
|
});
|
|
1209
|
-
return () => {
|
|
1210
|
-
const hadSelection = !!selected;
|
|
1211
|
-
source = next;
|
|
1212
|
-
sourceSelect.value = source;
|
|
1213
|
-
selected = undefined;
|
|
1214
|
-
const revision = ++neighborhoodRevision;
|
|
1215
|
-
delete root.dataset.selectedNeighborhood;
|
|
1216
|
-
root.dataset.source = source;
|
|
1217
|
-
layerFades.crossfade(areas, () => areas.replaceChildren(...nextItems.map((item) => item.node)));
|
|
1218
|
-
neighborhoodSelect.replaceChildren(element('option', 'No neighborhood selected'));
|
|
1219
|
-
neighborhoodSelect.options[0].value = '';
|
|
1220
|
-
neighborhoodSelect.append(...nextOptions);
|
|
1221
|
-
items = nextItems;
|
|
1222
|
-
orderedNeighborhoods = nextItems
|
|
1223
|
-
.filter((item) => item.point !== undefined)
|
|
1224
|
-
.sort((a, b) => b.area - a.area || a.feature.id.localeCompare(b.feature.id));
|
|
1225
|
-
if (items[0])
|
|
1226
|
-
items[0].node.setAttribute('tabindex', '0');
|
|
1227
|
-
updateResults();
|
|
1228
|
-
updateDetail();
|
|
1229
|
-
updateComposition();
|
|
1230
|
-
status.textContent =
|
|
1231
|
-
mode === 'basemap'
|
|
1232
|
-
? 'San Francisco basemap.'
|
|
1233
|
-
: mode === 'districts'
|
|
1234
|
-
? `${year} supervisorial districts. Numbers identify each district.`
|
|
1235
|
-
: `${formatSourceLabel(source)}. Select a neighborhood to begin.`;
|
|
1236
|
-
if (hadSelection)
|
|
1237
|
-
root.dispatchEvent(new CustomEvent('neighborhoodchange', {
|
|
1238
|
-
bubbles: true,
|
|
1239
|
-
detail: { feature: null, source, id: null, name: null },
|
|
1240
|
-
}));
|
|
1241
|
-
if (reset && !destroyed && neighborhoodRevision === revision)
|
|
1242
|
-
resetView();
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
function setSource(next) {
|
|
1246
|
-
if (!destroyed)
|
|
1247
|
-
prepareSource(next)();
|
|
1248
779
|
}
|
|
1249
780
|
function syncFeatureControls() {
|
|
1250
781
|
featureControls.hidden = neighborhoodLabel.hidden && markerLabel.hidden;
|
|
@@ -1290,9 +821,13 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1290
821
|
chrome === 'explorer' ||
|
|
1291
822
|
controls.neighborhoodPicker === false;
|
|
1292
823
|
syncFeatureControls();
|
|
1293
|
-
for (const item of items) {
|
|
1294
|
-
item.node.setAttribute('stroke-width', item.feature === selected
|
|
1295
|
-
|
|
824
|
+
for (const item of neighborhoods.items) {
|
|
825
|
+
item.node.setAttribute('stroke-width', item.feature === neighborhoods.selected
|
|
826
|
+
? '2.2'
|
|
827
|
+
: enabled('neighborhoodLines')
|
|
828
|
+
? '.55'
|
|
829
|
+
: '0');
|
|
830
|
+
item.node.setAttribute('tabindex', selectableNeighborhoods && item === neighborhoods.items[0] ? '0' : '-1');
|
|
1296
831
|
item.node.setAttribute('role', selectableNeighborhoods ? 'button' : 'img');
|
|
1297
832
|
if (!selectableNeighborhoods)
|
|
1298
833
|
item.node.removeAttribute('aria-pressed');
|
|
@@ -1307,7 +842,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1307
842
|
if (districtsVisible)
|
|
1308
843
|
descriptions.push(`${year} supervisorial districts, DataSF.`);
|
|
1309
844
|
if (neighborhoodsVisible) {
|
|
1310
|
-
const collection = collections[source];
|
|
845
|
+
const collection = collections[neighborhoods.source];
|
|
1311
846
|
if (collection)
|
|
1312
847
|
descriptions.push(`${collection.title}. ${collection.definition.description}`);
|
|
1313
848
|
}
|
|
@@ -1420,22 +955,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1420
955
|
validateSwitchPatch(patch, controlKeys);
|
|
1421
956
|
const next = { ...controls, ...patch };
|
|
1422
957
|
controls = next;
|
|
1423
|
-
|
|
1424
|
-
node.hidden = controls.zoom === false;
|
|
1425
|
-
panControls.hidden = controls.pan === false;
|
|
1426
|
-
resetButton.hidden = controls.reset === false;
|
|
1427
|
-
labelsButton.hidden = controls.labels === false;
|
|
1428
|
-
touchButton.hidden = controls.touch === false;
|
|
1429
|
-
if (touchButton.hidden && touchNavigation)
|
|
1430
|
-
setTouchNavigation(false);
|
|
1431
|
-
zoomControls.hidden = [...zoomControls.children].every((node) => node.hidden);
|
|
1432
|
-
toolbar.hidden = zoomControls.hidden && panControls.hidden;
|
|
1433
|
-
hint.hidden = controls.help === false;
|
|
1434
|
-
if (hint.hidden)
|
|
1435
|
-
svg.setAttribute('aria-describedby', hint.id);
|
|
1436
|
-
else
|
|
1437
|
-
svg.removeAttribute('aria-describedby');
|
|
1438
|
-
status.classList.toggle('sf-explorer-visually-hidden', controls.status === false);
|
|
958
|
+
mapControls.update(controls);
|
|
1439
959
|
markerLabel.hidden = !markerCatalog.items.length || controls.markerPicker === false;
|
|
1440
960
|
updateComposition();
|
|
1441
961
|
}
|
|
@@ -1463,9 +983,9 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1463
983
|
? 'San Francisco basemap.'
|
|
1464
984
|
: districts
|
|
1465
985
|
? `${year} supervisorial districts. Numbers identify each district.`
|
|
1466
|
-
: selected
|
|
1467
|
-
? `${selected.properties.canonicalName} selected. ${formatSourceLabel(source)}.`
|
|
1468
|
-
: `${formatSourceLabel(source)}. Select a neighborhood to begin.`;
|
|
986
|
+
: neighborhoods.selected
|
|
987
|
+
? `${neighborhoods.selected.properties.canonicalName} selected. ${formatSourceLabel(neighborhoods.source)}.`
|
|
988
|
+
: `${formatSourceLabel(neighborhoods.source)}. Select a neighborhood to begin.`;
|
|
1469
989
|
if (reset)
|
|
1470
990
|
resetView();
|
|
1471
991
|
}
|
|
@@ -1475,7 +995,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1475
995
|
if (typeof visible !== 'boolean')
|
|
1476
996
|
throw new TypeError('Labels must be a boolean.');
|
|
1477
997
|
labels = visible;
|
|
1478
|
-
|
|
998
|
+
mapControls.updateLabels(labels);
|
|
1479
999
|
root.dataset.labels = String(labels);
|
|
1480
1000
|
updateComposition();
|
|
1481
1001
|
if (!labels) {
|
|
@@ -1484,61 +1004,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1484
1004
|
}
|
|
1485
1005
|
scheduleLabels();
|
|
1486
1006
|
}
|
|
1487
|
-
listen(modeSelect, 'change', () => {
|
|
1488
|
-
const next = modeSelect.value;
|
|
1489
|
-
if (next === 'neighborhoods' || next === 'districts' || next === 'basemap')
|
|
1490
|
-
setMode(next);
|
|
1491
|
-
});
|
|
1492
|
-
listen(search, 'input', updateResults);
|
|
1493
|
-
listen(search, 'keydown', (event) => {
|
|
1494
|
-
if (event.key === 'Enter') {
|
|
1495
|
-
const exact = getNeighborhood(search.value, source);
|
|
1496
|
-
const matches = searchNeighborhoods(search.value, source);
|
|
1497
|
-
if (exact || matches.length === 1) {
|
|
1498
|
-
event.preventDefault();
|
|
1499
|
-
selectNeighborhood(exact?.id ?? matches[0].id);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
});
|
|
1503
|
-
listen(sourceSelect, 'change', () => {
|
|
1504
|
-
const next = sourceSelect.value;
|
|
1505
|
-
if (next === 'realtor' || next === 'sf-find' || next === 'analysis')
|
|
1506
|
-
setSource(next);
|
|
1507
|
-
});
|
|
1508
|
-
listen(results, 'click', (event) => {
|
|
1509
|
-
const node = event.target instanceof Element
|
|
1510
|
-
? event.target.closest('[data-neighborhood-result]')
|
|
1511
|
-
: null;
|
|
1512
|
-
if (node?.dataset.neighborhoodResult)
|
|
1513
|
-
selectNeighborhood(node.dataset.neighborhoodResult);
|
|
1514
|
-
});
|
|
1515
|
-
const touchLabel = strings.touchNavigation ?? 'Touch navigation';
|
|
1516
|
-
const touchButton = button(touchLabel, strings.touchNavigationLabel ?? `Enable ${touchLabel.toLowerCase()}`, () => setTouchNavigation(!touchNavigation));
|
|
1517
|
-
touchButton.hidden = controls.touch === false;
|
|
1518
|
-
let touchNavigation = false;
|
|
1519
|
-
const navigation = attachNavigation(svg, getViewport, (next) => {
|
|
1520
|
-
stopAnimation();
|
|
1521
|
-
setView(next);
|
|
1522
|
-
}, controller.signal, () => setTouchNavigation(false));
|
|
1523
|
-
function setTouchNavigation(enabled) {
|
|
1524
|
-
if (destroyed)
|
|
1525
|
-
return;
|
|
1526
|
-
if (typeof enabled !== 'boolean')
|
|
1527
|
-
throw new TypeError('Touch navigation must be a boolean.');
|
|
1528
|
-
touchNavigation = enabled;
|
|
1529
|
-
navigation.setTouchNavigation(enabled);
|
|
1530
|
-
canvas.style.touchAction = enabled ? 'none' : 'pan-y pinch-zoom';
|
|
1531
|
-
touchButton.textContent = enabled
|
|
1532
|
-
? (strings.touchNavigationDone ?? 'Done: page scrolling')
|
|
1533
|
-
: touchLabel;
|
|
1534
|
-
touchButton.setAttribute('aria-label', enabled
|
|
1535
|
-
? (strings.touchNavigationExitLabel ?? `Exit ${touchLabel.toLowerCase()}`)
|
|
1536
|
-
: (strings.touchNavigationLabel ?? `Enable ${touchLabel.toLowerCase()}`));
|
|
1537
|
-
touchButton.setAttribute('aria-pressed', String(enabled));
|
|
1538
|
-
root.dataset.touchNavigation = String(enabled);
|
|
1539
|
-
}
|
|
1540
|
-
listen(neighborhoodSelect, 'change', () => selectNeighborhood(neighborhoodSelect.value || null));
|
|
1541
|
-
listen(markerSelect, 'change', () => selectMarker(markerSelect.value || null));
|
|
1542
1007
|
listen(svg, 'keydown', (event) => {
|
|
1543
1008
|
const target = event.target instanceof SVGElement ? event.target : null;
|
|
1544
1009
|
const districtId = Number(target?.dataset.district);
|
|
@@ -1572,9 +1037,10 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1572
1037
|
}
|
|
1573
1038
|
else if (event.key === '[' || event.key === ']') {
|
|
1574
1039
|
event.preventDefault();
|
|
1575
|
-
const i = items.findIndex((item) => item.feature.id === id);
|
|
1576
|
-
const next = items[(i + (event.key === ']' ? 1 : items.length - 1)) %
|
|
1577
|
-
|
|
1040
|
+
const i = neighborhoods.items.findIndex((item) => item.feature.id === id);
|
|
1041
|
+
const next = neighborhoods.items[(i + (event.key === ']' ? 1 : neighborhoods.items.length - 1)) %
|
|
1042
|
+
neighborhoods.items.length];
|
|
1043
|
+
for (const item of neighborhoods.items)
|
|
1578
1044
|
item.node.setAttribute('tabindex', item === next ? '0' : '-1');
|
|
1579
1045
|
next.node.focus();
|
|
1580
1046
|
}
|
|
@@ -1646,7 +1112,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1646
1112
|
listen(svg, 'pointerdown', stopAnimation);
|
|
1647
1113
|
let observer;
|
|
1648
1114
|
const onResize = () => {
|
|
1649
|
-
|
|
1115
|
+
mapControls.cancelNavigation();
|
|
1650
1116
|
scheduleLabels();
|
|
1651
1117
|
root.dispatchEvent(new CustomEvent('mapresize', { bubbles: true }));
|
|
1652
1118
|
};
|
|
@@ -1662,7 +1128,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1662
1128
|
throw new RangeError('No neighborhood dataset was supplied.');
|
|
1663
1129
|
if (patch.source !== undefined && !Object.hasOwn(collections, patch.source))
|
|
1664
1130
|
throw new RangeError(`Unknown neighborhood source: ${patch.source}`);
|
|
1665
|
-
const commitSource = patch.source !== undefined && patch.source !== source
|
|
1131
|
+
const commitSource = patch.source !== undefined && patch.source !== neighborhoods.source
|
|
1666
1132
|
? prepareSource(patch.source, false)
|
|
1667
1133
|
: undefined;
|
|
1668
1134
|
const appearance = patch.appearance;
|
|
@@ -1743,8 +1209,8 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1743
1209
|
symbol.style.setProperty(kind === 'bart' ? 'border-color' : 'background-color', colors[kind]);
|
|
1744
1210
|
}
|
|
1745
1211
|
}
|
|
1746
|
-
for (const item of items) {
|
|
1747
|
-
const active = item.feature === selected;
|
|
1212
|
+
for (const item of neighborhoods.items) {
|
|
1213
|
+
const active = item.feature === neighborhoods.selected;
|
|
1748
1214
|
item.node.setAttribute('fill', active ? (areaStyle.selectedFill ?? '#408dbe') : 'transparent');
|
|
1749
1215
|
item.node.setAttribute('stroke', active ? (areaStyle.selectedStroke ?? '#176ba2') : (colors.neighborhood ?? '#9caebc'));
|
|
1750
1216
|
}
|
|
@@ -1770,7 +1236,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1770
1236
|
if (destroyed || revision !== presentationRevision || !isCurrent())
|
|
1771
1237
|
return false;
|
|
1772
1238
|
if (commitSource && appearance)
|
|
1773
|
-
for (const item of items)
|
|
1239
|
+
for (const item of neighborhoods.items)
|
|
1774
1240
|
item.node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
|
|
1775
1241
|
if (patch.mode !== undefined)
|
|
1776
1242
|
setMode(patch.mode, false);
|
|
@@ -1781,7 +1247,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1781
1247
|
}
|
|
1782
1248
|
const explorer = Object.assign(root, {
|
|
1783
1249
|
applyPresentation,
|
|
1784
|
-
getMapState: () => ({ mode, source, year, labels }),
|
|
1250
|
+
getMapState: () => ({ mode, source: neighborhoods.source, year, labels }),
|
|
1785
1251
|
selectNeighborhood,
|
|
1786
1252
|
getSelection,
|
|
1787
1253
|
selectDistrict,
|
|
@@ -1818,13 +1284,14 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1818
1284
|
districtMorph.stop();
|
|
1819
1285
|
cancelEntrances();
|
|
1820
1286
|
controller.abort();
|
|
1821
|
-
|
|
1287
|
+
overlays.destroy();
|
|
1822
1288
|
clusters.destroy();
|
|
1823
1289
|
canvas.style.touchAction = 'pan-y pinch-zoom';
|
|
1824
1290
|
observer?.disconnect();
|
|
1825
1291
|
frames.destroy();
|
|
1826
1292
|
labelRenderer.clear();
|
|
1827
|
-
|
|
1293
|
+
sidebar?.destroy();
|
|
1294
|
+
neighborhoods.destroy();
|
|
1828
1295
|
},
|
|
1829
1296
|
});
|
|
1830
1297
|
try {
|
|
@@ -1845,12 +1312,12 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1845
1312
|
}, { signal: controller.signal });
|
|
1846
1313
|
const initialMode = mode;
|
|
1847
1314
|
if (sources.length)
|
|
1848
|
-
setSource(source);
|
|
1315
|
+
setSource(neighborhoods.source);
|
|
1849
1316
|
if (neighborhood !== undefined)
|
|
1850
1317
|
selectNeighborhood(neighborhood);
|
|
1851
1318
|
setMode(initialMode);
|
|
1852
|
-
if (initialMode === 'neighborhoods' && selected) {
|
|
1853
|
-
const selectedItem = items.find((item) => item.feature === selected);
|
|
1319
|
+
if (initialMode === 'neighborhoods' && neighborhoods.selected) {
|
|
1320
|
+
const selectedItem = neighborhoods.items.find((item) => item.feature === neighborhoods.selected);
|
|
1854
1321
|
if (selectedItem)
|
|
1855
1322
|
setView(fitBounds(selectedItem.bounds));
|
|
1856
1323
|
}
|