@kahwee/sf-map-svg 4.4.0 → 5.0.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 +25 -0
- package/README.md +32 -0
- package/dist/data/README.md +7 -0
- 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/configuration.d.ts +2 -0
- package/dist/src/configuration.js +30 -0
- package/dist/src/controller-types.d.ts +11 -15
- 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 +156 -667
- 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/geometry.d.ts +6 -1
- package/dist/src/geometry.js +80 -0
- package/dist/src/guide-map.js +2 -1
- package/dist/src/guide-static.d.ts +1 -1
- package/dist/src/label-spread.d.ts +5 -0
- package/dist/src/label-spread.js +36 -0
- package/dist/src/layers.js +1 -1
- package/dist/src/map.d.ts +1 -1
- package/dist/src/map.js +24 -13
- package/dist/src/marker-catalog.d.ts +1 -1
- package/dist/src/marker-catalog.js +11 -5
- 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.js +19 -4
- package/dist/src/static.d.ts +2 -2
- package/dist/src/static.js +21 -2
- package/dist/src/types.d.ts +21 -9
- package/dist/src/validation.js +7 -5
- package/docs/API.md +75 -5
- package/docs/EXAMPLES.md +29 -0
- package/docs/api-audit.md +22 -5
- package/docs/consumer-integration.md +86 -2
- package/docs/developer-guide.md +29 -7
- package/docs/guide-bundle-report.md +4 -4
- package/docs/migration-v5.md +100 -0
- package/llms-full.txt +508 -53
- package/llms.txt +2 -1
- package/package.json +17 -8
|
@@ -1,16 +1,16 @@
|
|
|
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 { geometryPath } from './geometry.js';
|
|
14
14
|
import { labelCandidates, styleLabels, } from './label-candidates.js';
|
|
15
15
|
import { createLabelRenderer } from './label-renderer.js';
|
|
16
16
|
import { createLayerTransitions } from './layer-transition.js';
|
|
@@ -19,24 +19,12 @@ import { createMarkerCatalog } from './marker-catalog.js';
|
|
|
19
19
|
import { createMarkerChooser } from './marker-chooser.js';
|
|
20
20
|
import { createMarkerLayer } from './marker-layer.js';
|
|
21
21
|
import { createMarkerNavigation } from './marker-navigation.js';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { createNeighborhoodLayer, createNeighborhoodLookup, formatSourceLabel, } from './neighborhood-layer.js';
|
|
23
|
+
import { createOverlayLayer } from './overlay-layer.js';
|
|
24
|
+
import { validateExplorerOptions } from './validation.js';
|
|
24
25
|
import { fitViewport, validateViewport } from './viewport.js';
|
|
25
|
-
const sourceNames = {
|
|
26
|
-
realtor: 'SFAR realtor · 92 areas',
|
|
27
|
-
'sf-find': 'SF Find · 117 areas',
|
|
28
|
-
analysis: 'City analysis · 41 areas',
|
|
29
|
-
};
|
|
30
|
-
const normalizeName = (value) => value
|
|
31
|
-
.normalize('NFKD')
|
|
32
|
-
.toLowerCase()
|
|
33
|
-
.replace(/[\u0300-\u036f'’]/g, '')
|
|
34
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
35
|
-
.trim();
|
|
36
|
-
const formatSourceLabel = (source) => sourceNames[source] ?? `${source} neighborhoods`;
|
|
37
|
-
let explorerCount = 0;
|
|
38
26
|
/** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
|
|
39
|
-
export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
27
|
+
export function createNeighborhoodExplorerCore(options = {}, data, ui) {
|
|
40
28
|
validateExplorerOptions(options);
|
|
41
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;
|
|
42
30
|
if (!['neighborhoods', 'districts', 'basemap'].includes(mode))
|
|
@@ -95,28 +83,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
95
83
|
throw new RangeError('No neighborhood dataset was supplied.');
|
|
96
84
|
if (neighborhood === '')
|
|
97
85
|
neighborhood = undefined;
|
|
98
|
-
const getNeighborhood
|
|
99
|
-
const term = normalizeName(name);
|
|
100
|
-
if (!term)
|
|
101
|
-
return undefined;
|
|
102
|
-
return collections[selectedSource]?.features.find((feature) => [
|
|
103
|
-
feature.id,
|
|
104
|
-
feature.properties.canonicalName,
|
|
105
|
-
feature.properties.sourceName,
|
|
106
|
-
...feature.properties.aliases,
|
|
107
|
-
].some((value) => normalizeName(value) === term));
|
|
108
|
-
};
|
|
109
|
-
const searchNeighborhoods = (query, selectedSource) => {
|
|
110
|
-
const term = normalizeName(query);
|
|
111
|
-
return (collections[selectedSource]?.features ?? [])
|
|
112
|
-
.filter((feature) => [
|
|
113
|
-
feature.id,
|
|
114
|
-
feature.properties.canonicalName,
|
|
115
|
-
feature.properties.sourceName,
|
|
116
|
-
...feature.properties.aliases,
|
|
117
|
-
].some((value) => normalizeName(value).includes(term)))
|
|
118
|
-
.map((feature) => ({ id: feature.id, canonicalName: feature.properties.canonicalName }));
|
|
119
|
-
};
|
|
86
|
+
const { getNeighborhood, searchNeighborhoods } = createNeighborhoodLookup(collections);
|
|
120
87
|
if (neighborhood !== undefined && !getNeighborhood(neighborhood, source))
|
|
121
88
|
throw new RangeError(`Unknown neighborhood: ${neighborhood}`);
|
|
122
89
|
if (typeof document === 'undefined')
|
|
@@ -126,191 +93,18 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
126
93
|
function listen(node, event, handler, options = {}) {
|
|
127
94
|
node.addEventListener(event, (incoming) => handler(incoming), { ...options, signal: controller.signal });
|
|
128
95
|
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
.sf-explorer button[aria-pressed=true]{color:#123d63;background:#e6f1f9;border-color:#5689b0;font-weight:650}
|
|
142
|
-
.sf-explorer :focus-visible{outline:3px solid var(--sf-map-focus);outline-offset:2px}
|
|
143
|
-
.sf-explorer-header{padding:24px;border-top:4px solid var(--sf-map-accent);border-bottom:1px solid var(--sf-map-border)}
|
|
144
|
-
.sf-explorer-header h2{font-size:25px;line-height:1.25;font-weight:700;letter-spacing:-.7px}
|
|
145
|
-
.sf-explorer-header p{color:#586f80;margin-top:8px;max-width:58ch}
|
|
146
|
-
.sf-explorer-body{display:grid;grid-template-columns:280px minmax(0,1fr);grid-template-areas:"panel map" "detail map";grid-template-rows:auto 1fr}
|
|
147
|
-
.sf-explorer-panel{grid-area:panel;padding:18px;display:flex;flex-direction:column;gap:14px;border-right:1px solid #dce5eb;min-width:0}
|
|
148
|
-
.sf-explorer-panel label{display:flex;flex-direction:column;gap:6px;font-size:12px;font-weight:650}
|
|
149
|
-
.sf-explorer-panel input,.sf-explorer-panel select{width:100%;font-weight:400;font-size:14px}
|
|
150
|
-
.sf-explorer-panel input::placeholder{color:#6a7f8e}
|
|
151
|
-
.sf-explorer-results{display:flex;flex-direction:column;gap:5px;max-height:255px;overflow:auto;padding:3px;margin:-3px;overscroll-behavior:contain}
|
|
152
|
-
.sf-explorer-results button{text-align:left;flex-shrink:0}
|
|
153
|
-
.sf-explorer-count,.sf-explorer-note{font-size:12px;color:#586f80}
|
|
154
|
-
.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}
|
|
155
|
-
.sf-explorer-detail details{font-size:12px;margin-top:12px}
|
|
156
|
-
.sf-explorer-detail summary{cursor:pointer;color:#496578;min-height:32px}
|
|
157
|
-
.sf-explorer-detail details p{margin-top:8px}
|
|
158
|
-
.sf-explorer-detail h3{margin:0 0 7px;font-size:18px;line-height:1.3;letter-spacing:-.2px}
|
|
159
|
-
.sf-explorer-detail p{font-size:12px;margin-bottom:10px}
|
|
160
|
-
.sf-explorer-download{display:block;margin:8px 0;color:#12649c;text-underline-offset:3px}
|
|
161
|
-
.sf-explorer-map-column{grid-area:map;min-width:0;background:#e4f2f8}
|
|
162
|
-
.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)}
|
|
163
|
-
.sf-explorer-control-group{display:flex;align-items:center;flex-wrap:wrap;gap:6px;max-width:100%}
|
|
164
|
-
.sf-explorer-pan-controls{margin-left:auto}
|
|
165
|
-
.sf-explorer-toolbar button{min-width:44px;padding:6px 10px}
|
|
166
|
-
.sf-explorer-zoom{font-size:12px;min-width:42px;text-align:center;font-variant-numeric:tabular-nums;color:#496578}
|
|
167
|
-
.sf-explorer-canvas{position:relative;aspect-ratio:1;overflow:hidden;touch-action:pan-y pinch-zoom}
|
|
168
|
-
.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}
|
|
169
|
-
.sf-marker-chooser button{min-height:44px;text-align:left;overflow-wrap:anywhere}
|
|
170
|
-
.sf-explorer-canvas>svg{display:block;width:100%;height:100%;max-width:none;cursor:grab;user-select:none}
|
|
171
|
-
.sf-explorer-canvas>svg:active{cursor:grabbing}
|
|
172
|
-
.sf-explorer-canvas path[data-neighborhood-id]{cursor:pointer}
|
|
173
|
-
.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}
|
|
174
|
-
.sf-explorer-legend-item{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
|
|
175
|
-
.sf-explorer-legend-symbol{display:inline-block;flex:none;width:15px;height:11px;border-radius:2px}
|
|
176
|
-
.sf-explorer-legend-bart{width:10px;height:10px;margin:0 2px;border:2px solid #0073ae;border-radius:50%;background:#fff}
|
|
177
|
-
.sf-explorer-legend-park{background:#c6dfbd;border:1px solid #a8c69d}
|
|
178
|
-
.sf-explorer-legend-road{height:2px;border-radius:2px;background:#bcc3c5}
|
|
179
|
-
.sf-explorer-legend-highway{height:3px;border-radius:2px;background:#b9a18a}
|
|
180
|
-
.sf-explorer-hint{padding:6px 16px 10px;font-size:12px;color:#586f80;background:#fff}
|
|
181
|
-
.sf-explorer text{pointer-events:none}
|
|
182
|
-
.sf-explorer-status{padding:0 16px 14px;font-size:12px;background:var(--sf-map-surface);color:#496578}
|
|
183
|
-
.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}
|
|
184
|
-
.sf-explorer[data-interface=map] .sf-explorer-body{display:block}
|
|
185
|
-
.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}
|
|
186
|
-
.sf-explorer-feature-controls{display:flex;flex-wrap:wrap;gap:8px;padding:10px;background:#fff}
|
|
187
|
-
.sf-explorer-feature-controls label{display:flex;flex:1;flex-direction:column;min-width:0}
|
|
188
|
-
.sf-explorer-feature-controls select{width:100%;min-width:0}
|
|
189
|
-
.sf-explorer svg [role=button]:focus-visible{outline:none;stroke:#102f72;stroke-width:3;fill-opacity:.3}
|
|
190
|
-
@media(prefers-reduced-motion:reduce){.sf-explorer button{transition:none}}
|
|
191
|
-
.sf-explorer button:disabled{opacity:.45;cursor:default}
|
|
192
|
-
@container sf-neighborhood-explorer (max-width:650px){
|
|
193
|
-
.sf-explorer-body{grid-template-columns:1fr;grid-template-areas:"panel" "map" "detail";grid-template-rows:auto auto auto}
|
|
194
|
-
.sf-explorer-header{padding:18px}
|
|
195
|
-
.sf-explorer-header h2{font-size:23px}
|
|
196
|
-
.sf-explorer-header p{font-size:13px}
|
|
197
|
-
.sf-explorer-panel{border-right:0;border-bottom:1px solid #dce5eb;padding:14px;gap:10px}
|
|
198
|
-
.sf-explorer-results{max-height:140px}
|
|
199
|
-
.sf-explorer-feature-controls{flex-direction:column}
|
|
200
|
-
.sf-explorer-detail{padding:14px;border-right:0}
|
|
201
|
-
.sf-explorer-toolbar{padding:8px;gap:6px}
|
|
202
|
-
.sf-explorer-control-group{gap:4px}
|
|
203
|
-
.sf-explorer-pan-controls{flex-basis:100%;margin-left:0}
|
|
204
|
-
.sf-explorer-toolbar button{padding:5px 8px}
|
|
205
|
-
.sf-explorer-legend{gap:8px 16px;padding:12px 14px 8px}
|
|
206
|
-
.sf-explorer-download{display:inline-block;margin:4px 14px 4px 0}
|
|
207
|
-
}`;
|
|
208
|
-
root.dataset.interface = chrome;
|
|
209
|
-
root.style.setProperty('--sf-map-ink', styleOptions.ink ?? '#18364f');
|
|
210
|
-
root.style.setProperty('--sf-map-surface', styleOptions.surface ?? '#fff');
|
|
211
|
-
root.style.setProperty('--sf-map-accent', styleOptions.accent ?? '#163d61');
|
|
212
|
-
root.style.setProperty('--sf-map-border', styleOptions.border ?? '#cedae3');
|
|
213
|
-
root.style.setProperty('--sf-map-focus', styleOptions.focus ?? '#1676b8');
|
|
214
|
-
root.style.setProperty('--sf-map-control-gap', styleOptions.controlGap ?? '6px');
|
|
215
|
-
root.style.setProperty('--sf-map-font', styleOptions.font ?? 'system-ui,sans-serif');
|
|
216
|
-
root.append(style);
|
|
217
|
-
const header = element('header', '', 'sf-explorer-header');
|
|
218
|
-
header.append(element('h2', strings.title ?? 'Explore San Francisco'), element('p', 'Explore the city, from familiar names to the places in between.'));
|
|
219
|
-
root.append(header);
|
|
220
|
-
const body = element('div', '', 'sf-explorer-body');
|
|
221
|
-
const panel = element('div', '', 'sf-explorer-panel');
|
|
222
|
-
const modeLabel = element('label', strings.mode ?? 'Map mode');
|
|
223
|
-
const modeSelect = element('select');
|
|
224
|
-
for (const [value, title] of [
|
|
225
|
-
['neighborhoods', 'Neighborhoods'],
|
|
226
|
-
['districts', 'Districts'],
|
|
227
|
-
['basemap', 'Basemap'],
|
|
228
|
-
]) {
|
|
229
|
-
if (value === 'districts' && !data.districts?.[year])
|
|
230
|
-
continue;
|
|
231
|
-
if (value === 'neighborhoods' && !sources.length)
|
|
232
|
-
continue;
|
|
233
|
-
const option = element('option', title);
|
|
234
|
-
option.value = value;
|
|
235
|
-
modeSelect.append(option);
|
|
236
|
-
}
|
|
237
|
-
modeLabel.append(modeSelect);
|
|
238
|
-
const sourceLabel = element('label', strings.source ?? 'Neighborhood definitions');
|
|
239
|
-
const sourceSelect = element('select');
|
|
240
|
-
for (const value of sources) {
|
|
241
|
-
const title = sourceNames[value];
|
|
242
|
-
const option = element('option', title);
|
|
243
|
-
option.value = value;
|
|
244
|
-
sourceSelect.append(option);
|
|
245
|
-
}
|
|
246
|
-
sourceSelect.value = source;
|
|
247
|
-
sourceLabel.append(sourceSelect);
|
|
248
|
-
const searchLabel = element('label', strings.search ?? 'Find a neighborhood');
|
|
249
|
-
const search = element('input');
|
|
250
|
-
search.type = 'search';
|
|
251
|
-
search.placeholder = 'Try Mission, Outer Mission, or NoPa';
|
|
252
|
-
search.autocomplete = 'off';
|
|
253
|
-
searchLabel.append(search);
|
|
254
|
-
const count = element('p', '', 'sf-explorer-count');
|
|
255
|
-
count.setAttribute('aria-live', 'polite');
|
|
256
|
-
const results = element('div', '', 'sf-explorer-results');
|
|
257
|
-
results.setAttribute('aria-label', 'Matching neighborhoods');
|
|
258
|
-
results.setAttribute('role', 'group');
|
|
259
|
-
const detail = element('div', '', 'sf-explorer-detail');
|
|
260
|
-
panel.append(modeLabel, sourceLabel, searchLabel, count, results);
|
|
261
|
-
const column = element('div', '', 'sf-explorer-map-column');
|
|
262
|
-
const toolbar = element('div', '', 'sf-explorer-toolbar');
|
|
263
|
-
toolbar.setAttribute('aria-label', 'Map controls');
|
|
264
|
-
const canvas = element('div', '', 'sf-explorer-canvas');
|
|
265
|
-
const hint = element('p', strings.gestureHelp ??
|
|
266
|
-
'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');
|
|
267
|
-
hint.id = `sf-explorer-help-${++explorerCount}`;
|
|
268
|
-
const status = element('p', '', 'sf-explorer-status');
|
|
269
|
-
status.setAttribute('aria-live', 'polite');
|
|
270
|
-
const legend = element('div', '', 'sf-explorer-legend');
|
|
271
|
-
legend.setAttribute('aria-label', 'Map legend');
|
|
272
|
-
for (const [kind, label] of [
|
|
273
|
-
['bart', 'BART station'],
|
|
274
|
-
['park', 'Park'],
|
|
275
|
-
['highway', 'Highway'],
|
|
276
|
-
['road', 'Roads'],
|
|
277
|
-
]) {
|
|
278
|
-
if (legendOptions.builtins === false || legendOptions.hidden?.includes(kind))
|
|
279
|
-
continue;
|
|
280
|
-
const entry = element('span', '', 'sf-explorer-legend-item');
|
|
281
|
-
const symbol = element('span', '', `sf-explorer-legend-symbol sf-explorer-legend-${kind}`);
|
|
282
|
-
symbol.setAttribute('aria-hidden', 'true');
|
|
283
|
-
if (colors[kind]) {
|
|
284
|
-
if (kind === 'bart')
|
|
285
|
-
symbol.style.borderColor = colors[kind];
|
|
286
|
-
else
|
|
287
|
-
symbol.style.backgroundColor = colors[kind];
|
|
288
|
-
}
|
|
289
|
-
entry.append(symbol, document.createTextNode(label));
|
|
290
|
-
legend.append(entry);
|
|
291
|
-
}
|
|
292
|
-
for (const item of legendOptions.items ?? []) {
|
|
293
|
-
const entry = element('span', '', 'sf-explorer-legend-item');
|
|
294
|
-
const symbol = element('span', '', 'sf-explorer-legend-symbol');
|
|
295
|
-
symbol.style.backgroundColor = item.color;
|
|
296
|
-
symbol.setAttribute('aria-hidden', 'true');
|
|
297
|
-
entry.append(symbol, document.createTextNode(item.label));
|
|
298
|
-
legend.append(entry);
|
|
299
|
-
}
|
|
300
|
-
const featureControls = element('div', '', 'sf-explorer-feature-controls');
|
|
301
|
-
const neighborhoodLabel = element('label', strings.chooseNeighborhood ?? 'Select neighborhood');
|
|
302
|
-
const neighborhoodSelect = element('select');
|
|
303
|
-
neighborhoodLabel.append(neighborhoodSelect);
|
|
304
|
-
const markerLabel = element('label', strings.chooseMarker ?? 'Choose marker');
|
|
305
|
-
const markerSelect = element('select');
|
|
306
|
-
markerLabel.append(markerSelect);
|
|
307
|
-
featureControls.append(neighborhoodLabel, markerLabel);
|
|
308
|
-
const attribution = element('p', '', 'sf-explorer-hint');
|
|
309
|
-
const attributionDisclosure = element('details', '', 'sf-explorer-hint');
|
|
310
|
-
attributionDisclosure.append(element('summary', 'Map data · DataSF / BART'), attribution);
|
|
311
|
-
column.append(toolbar, featureControls, canvas, legend, hint, status, attributionMode === 'compact' ? attributionDisclosure : attribution);
|
|
312
|
-
body.append(panel, column, detail);
|
|
313
|
-
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;
|
|
314
108
|
const map = createSFMapWithData({
|
|
315
109
|
year,
|
|
316
110
|
theme,
|
|
@@ -346,8 +140,10 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
346
140
|
arrow.hidden = !features.northArrow;
|
|
347
141
|
}
|
|
348
142
|
syncFurniture();
|
|
349
|
-
function projectToScreen(
|
|
350
|
-
|
|
143
|
+
function projectToScreen(position) {
|
|
144
|
+
if (!Array.isArray(position) || position.length < 2)
|
|
145
|
+
throw new RangeError('Expected a [longitude, latitude] position.');
|
|
146
|
+
const [x, y] = map.project(position);
|
|
351
147
|
const width = canvas.getBoundingClientRect().width;
|
|
352
148
|
if (!width)
|
|
353
149
|
throw new Error('Mount the map before projecting to screen.');
|
|
@@ -386,7 +182,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
386
182
|
const labelRenderer = createLabelRenderer(labelLayer);
|
|
387
183
|
geography.append(areas, stations, labelLayer, overlayLayer, markerLayer);
|
|
388
184
|
let view = [0, 0, 800];
|
|
389
|
-
let selected;
|
|
390
185
|
let selectedDistrict = null;
|
|
391
186
|
let hoveredDistrict = null;
|
|
392
187
|
let districtStyle = initialDistrictStyle;
|
|
@@ -552,8 +347,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
552
347
|
}));
|
|
553
348
|
}
|
|
554
349
|
updateDistrictAppearance();
|
|
555
|
-
let items = [];
|
|
556
|
-
let orderedNeighborhoods = [];
|
|
557
350
|
let destroyed = false;
|
|
558
351
|
let initialized = false;
|
|
559
352
|
const layerFades = createLayerTransitions(() => initialized && features.layerTransitions && !reducedMotion.matches
|
|
@@ -578,6 +371,49 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
578
371
|
});
|
|
579
372
|
const stopAnimation = camera.stop;
|
|
580
373
|
const moveView = camera.move;
|
|
374
|
+
const neighborhoods = createNeighborhoodLayer({
|
|
375
|
+
collections,
|
|
376
|
+
initialSource: source,
|
|
377
|
+
project: map.project,
|
|
378
|
+
root,
|
|
379
|
+
areas,
|
|
380
|
+
canvas,
|
|
381
|
+
sourceSelect,
|
|
382
|
+
neighborhoodSelect,
|
|
383
|
+
status,
|
|
384
|
+
readPresentation: () => ({
|
|
385
|
+
mode,
|
|
386
|
+
year,
|
|
387
|
+
fitPadding,
|
|
388
|
+
areaStyle,
|
|
389
|
+
colors,
|
|
390
|
+
selectableNeighborhoods,
|
|
391
|
+
lines: enabled('neighborhoodLines'),
|
|
392
|
+
}),
|
|
393
|
+
isDestroyed: () => destroyed,
|
|
394
|
+
ensureVisible() {
|
|
395
|
+
if (!enabled('neighborhoodLines') &&
|
|
396
|
+
!enabled('neighborhoodLabels') &&
|
|
397
|
+
(layers.neighborhoodLines === undefined || layers.neighborhoodLabels === undefined))
|
|
398
|
+
setMode('neighborhoods', false);
|
|
399
|
+
},
|
|
400
|
+
refresh: {
|
|
401
|
+
selection() {
|
|
402
|
+
scheduleLabels();
|
|
403
|
+
updateResults();
|
|
404
|
+
updateDetail();
|
|
405
|
+
},
|
|
406
|
+
source() {
|
|
407
|
+
updateResults();
|
|
408
|
+
updateDetail();
|
|
409
|
+
updateComposition();
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
moveView,
|
|
413
|
+
resetView,
|
|
414
|
+
crossfade: layerFades.crossfade,
|
|
415
|
+
});
|
|
416
|
+
const { getSelection, selectNeighborhood, prepareSource, setSource } = neighborhoods;
|
|
581
417
|
function getFeatures() {
|
|
582
418
|
return structuredClone(features);
|
|
583
419
|
}
|
|
@@ -611,7 +447,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
611
447
|
}
|
|
612
448
|
scheduleLabels();
|
|
613
449
|
}
|
|
614
|
-
const downloads = new Set();
|
|
615
450
|
const stationItems = (data.map.bartStations ?? []).map((feature) => {
|
|
616
451
|
return {
|
|
617
452
|
point: map.project(feature.coordinates),
|
|
@@ -661,7 +496,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
661
496
|
?.visual?.node.focus({ preventScroll: true });
|
|
662
497
|
});
|
|
663
498
|
let markerRevision = 0;
|
|
664
|
-
let neighborhoodRevision = 0;
|
|
665
499
|
const clusterLayer = svgElement('g', { 'data-layer': 'marker-clusters' });
|
|
666
500
|
geography.insertBefore(clusterLayer, markerLayer);
|
|
667
501
|
function materializeMarker(item) {
|
|
@@ -675,10 +509,13 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
675
509
|
}),
|
|
676
510
|
});
|
|
677
511
|
}
|
|
678
|
-
const clusters =
|
|
512
|
+
const clusters = createClusterLayer(svg, markerLayer, clusterLayer, markerNavigation, materializeMarker, (group, node, signal) => {
|
|
679
513
|
if (destroyed || signal.aborted)
|
|
680
514
|
return;
|
|
681
|
-
const markers = group.map((item) => ({
|
|
515
|
+
const markers = group.map((item) => ({
|
|
516
|
+
...item.marker,
|
|
517
|
+
position: [...item.marker.position],
|
|
518
|
+
}));
|
|
682
519
|
const minimumUnit = 800 / 12 / Math.max(1, canvas.getBoundingClientRect().width);
|
|
683
520
|
if (clusterPoints(group, minimumUnit, features.clustering ? features.clustering.radius : 36)
|
|
684
521
|
.length === 1)
|
|
@@ -686,7 +523,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
686
523
|
else
|
|
687
524
|
fitGeometry({
|
|
688
525
|
type: 'MultiPoint',
|
|
689
|
-
coordinates: markers.map((marker) =>
|
|
526
|
+
coordinates: markers.map((marker) => marker.position),
|
|
690
527
|
});
|
|
691
528
|
if (!destroyed && !signal.aborted)
|
|
692
529
|
root.dispatchEvent(new CustomEvent('clusteractivate', { bubbles: true, detail: { markers } }));
|
|
@@ -712,86 +549,39 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
712
549
|
renderedMarkers = rendered.markers;
|
|
713
550
|
renderedClusters = rendered.clusters;
|
|
714
551
|
}
|
|
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
|
-
|
|
748
|
-
return;
|
|
749
|
-
onOverlayActivate(structuredClone(overlay));
|
|
750
|
-
if (destroyed || events.signal.aborted)
|
|
751
|
-
return;
|
|
752
|
-
root.dispatchEvent(new CustomEvent('overlayactivate', {
|
|
753
|
-
bubbles: true,
|
|
754
|
-
detail: { overlay: structuredClone(overlay) },
|
|
755
|
-
}));
|
|
756
|
-
};
|
|
757
|
-
node.addEventListener('click', activate, { signal: events.signal });
|
|
758
|
-
node.addEventListener('keydown', (event) => {
|
|
759
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
760
|
-
event.preventDefault();
|
|
761
|
-
activate();
|
|
762
|
-
}
|
|
763
|
-
}, { signal: events.signal });
|
|
764
|
-
}
|
|
765
|
-
return node;
|
|
766
|
-
});
|
|
767
|
-
overlayEvents.abort();
|
|
768
|
-
overlayEvents = events;
|
|
769
|
-
overlayLayer.replaceChildren(...next);
|
|
770
|
-
}
|
|
771
|
-
const zoomControls = element('div', '', 'sf-explorer-control-group');
|
|
772
|
-
const panControls = element('div', '', 'sf-explorer-control-group sf-explorer-pan-controls');
|
|
773
|
-
toolbar.append(zoomControls, panControls);
|
|
774
|
-
function button(text, label, action, group = zoomControls) {
|
|
775
|
-
const node = element('button', text);
|
|
776
|
-
node.type = 'button';
|
|
777
|
-
node.setAttribute('aria-label', label);
|
|
778
|
-
listen(node, 'click', () => action());
|
|
779
|
-
group.append(node);
|
|
780
|
-
return node;
|
|
781
|
-
}
|
|
782
|
-
const zoomOut = button('−', 'Zoom out', () => zoomBy(1 / 1.5));
|
|
783
|
-
const zoomText = element('span', '100%', 'sf-explorer-zoom');
|
|
784
|
-
zoomControls.append(zoomText);
|
|
785
|
-
const zoomIn = button('+', 'Zoom in', () => zoomBy(1.5));
|
|
786
|
-
const resetButton = button(strings.reset ?? 'Reset', strings.reset ?? 'Reset map to city view', resetView);
|
|
787
|
-
const labelsButton = button('Labels', 'Show map labels', () => setLabels(!labels));
|
|
788
|
-
for (const [symbol, label, dx, dy] of [
|
|
789
|
-
['←', 'Pan west', -1, 0],
|
|
790
|
-
['↑', 'Pan north', 0, -1],
|
|
791
|
-
['↓', 'Pan south', 0, 1],
|
|
792
|
-
['→', 'Pan east', 1, 0],
|
|
793
|
-
])
|
|
794
|
-
button(symbol, label, () => moveView([view[0] + (dx * view[2]) / 4, view[1] + (dy * view[2]) / 4, view[2]]), panControls);
|
|
552
|
+
const overlays = createOverlayLayer({
|
|
553
|
+
layer: overlayLayer,
|
|
554
|
+
root,
|
|
555
|
+
project: map.project,
|
|
556
|
+
onOverlayActivate,
|
|
557
|
+
isDestroyed: () => destroyed,
|
|
558
|
+
});
|
|
559
|
+
const setOverlays = overlays.set;
|
|
560
|
+
const mapControls = createExplorerControls(shell, {
|
|
561
|
+
svg,
|
|
562
|
+
signal: controller.signal,
|
|
563
|
+
strings,
|
|
564
|
+
isDestroyed: () => destroyed,
|
|
565
|
+
getLabels: () => labels,
|
|
566
|
+
getViewport,
|
|
567
|
+
moveView,
|
|
568
|
+
zoomBy,
|
|
569
|
+
resetView,
|
|
570
|
+
setLabels,
|
|
571
|
+
stopAnimation,
|
|
572
|
+
setView,
|
|
573
|
+
setMode,
|
|
574
|
+
setSource,
|
|
575
|
+
selectNeighborhood,
|
|
576
|
+
selectMarker,
|
|
577
|
+
updateResults,
|
|
578
|
+
search(query) {
|
|
579
|
+
const exact = getNeighborhood(query, neighborhoods.source);
|
|
580
|
+
const matches = searchNeighborhoods(query, neighborhoods.source);
|
|
581
|
+
return exact?.id ?? (matches.length === 1 ? matches[0].id : undefined);
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
const setTouchNavigation = mapControls.setTouchNavigation;
|
|
795
585
|
function panBy(x, y, options = {}) {
|
|
796
586
|
if (destroyed)
|
|
797
587
|
return;
|
|
@@ -821,10 +611,8 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
821
611
|
view = validateViewport(next);
|
|
822
612
|
svg.setAttribute('viewBox', `${view[0]} ${view[1]} ${view[2]} ${view[2]}`);
|
|
823
613
|
const zoom = 800 / view[2];
|
|
824
|
-
|
|
614
|
+
mapControls.updateZoom(zoom);
|
|
825
615
|
root.dataset.zoom = String(zoom);
|
|
826
|
-
zoomIn.disabled = zoom >= 12;
|
|
827
|
-
zoomOut.disabled = zoom <= 1;
|
|
828
616
|
if (view.some((value, i) => value !== previous[i]))
|
|
829
617
|
root.dispatchEvent(new CustomEvent('viewportchange', { bubbles: true, detail: { viewport: getViewport() } }));
|
|
830
618
|
scheduleLabels();
|
|
@@ -850,8 +638,8 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
850
638
|
function prepareLabels(width, zoom) {
|
|
851
639
|
// Only screen-space positioning changes during pan or within these zoom/width bands.
|
|
852
640
|
const inputs = [
|
|
853
|
-
items,
|
|
854
|
-
selected,
|
|
641
|
+
neighborhoods.items,
|
|
642
|
+
neighborhoods.selected,
|
|
855
643
|
districtItems,
|
|
856
644
|
markerRevision,
|
|
857
645
|
layers,
|
|
@@ -867,7 +655,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
867
655
|
if (inputs.every((value, index) => value === labelInputs[index]))
|
|
868
656
|
return preparedLabels;
|
|
869
657
|
labelInputs = inputs;
|
|
870
|
-
const selectedItem = items.find((item) => item.feature === selected);
|
|
658
|
+
const selectedItem = neighborhoods.items.find((item) => item.feature === neighborhoods.selected);
|
|
871
659
|
const candidates = [];
|
|
872
660
|
const marker = markerCatalog.items.find((item) => item.marker.id === selectedMarker);
|
|
873
661
|
if (marker)
|
|
@@ -889,7 +677,10 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
889
677
|
parks: enabled('landmarks') ? parkItems : [],
|
|
890
678
|
roads: enabled('roadLabels') ? roadItems : [],
|
|
891
679
|
neighborhoods: enabled('neighborhoodLabels')
|
|
892
|
-
?
|
|
680
|
+
? neighborhoods.items
|
|
681
|
+
.filter((item) => item.point !== undefined)
|
|
682
|
+
.sort((a, b) => b.area - a.area || a.feature.id.localeCompare(b.feature.id))
|
|
683
|
+
.flatMap((item) => item.feature !== neighborhoods.selected && item.point
|
|
893
684
|
? [
|
|
894
685
|
{
|
|
895
686
|
point: item.point,
|
|
@@ -974,260 +765,22 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
974
765
|
}),
|
|
975
766
|
]));
|
|
976
767
|
}
|
|
977
|
-
|
|
978
|
-
for (const url of downloads)
|
|
979
|
-
URL.revokeObjectURL(url);
|
|
980
|
-
downloads.clear();
|
|
981
|
-
}
|
|
982
|
-
function downloadLink(text, value, filename) {
|
|
983
|
-
const link = element('a', text, 'sf-explorer-download');
|
|
984
|
-
const url = URL.createObjectURL(new Blob([`${JSON.stringify(value, null, 2)}\n`], { type: 'application/geo+json' }));
|
|
985
|
-
downloads.add(url);
|
|
986
|
-
link.href = url;
|
|
987
|
-
link.download = filename;
|
|
988
|
-
return link;
|
|
989
|
-
}
|
|
768
|
+
const sidebar = ui.createSidebar?.(shell, data, chrome, strings.emptyResults, searchNeighborhoods);
|
|
990
769
|
function updateDetail() {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (!collection) {
|
|
998
|
-
detail.append(element('h3', `${year} supervisorial districts`));
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
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));
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (mode === 'basemap') {
|
|
1005
|
-
detail.append(element('h3', 'San Francisco basemap'));
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
const collection = collections[source];
|
|
1009
|
-
if (!collection)
|
|
1010
|
-
return;
|
|
1011
|
-
detail.append(element('h3', selected ? selected.properties.canonicalName : 'Explore all neighborhoods'));
|
|
1012
|
-
const description = selected
|
|
1013
|
-
? `Source name: ${selected.properties.sourceName}${selected.properties.aliases.length ? `. Also known as: ${selected.properties.aliases.join(', ')}` : ''}.`
|
|
1014
|
-
: 'Choose a name or click an area to highlight its full boundary.';
|
|
1015
|
-
const sourceSummary = {
|
|
1016
|
-
realtor: 'SFAR realtor boundaries · August 2010 · 92 distinct areas.',
|
|
1017
|
-
'sf-find': 'SF Find neighborhoods · 2006 · 117 approximate areas.',
|
|
1018
|
-
analysis: 'City analysis neighborhoods · 41 census-tract-based areas.',
|
|
1019
|
-
};
|
|
1020
|
-
detail.append(element('p', description), element('p', sourceSummary[source]));
|
|
1021
|
-
if (selected)
|
|
1022
|
-
detail.append(downloadLink('Download neighborhood GeoJSON', { ...collection, selection: selected.id, features: [selected] }, `${source}-${selected.id}.geojson`));
|
|
1023
|
-
detail.append(downloadLink(`Download all ${collection.features.length} areas`, collection, `neighborhoods-${source}.geojson`));
|
|
1024
|
-
const provenance = element('details');
|
|
1025
|
-
provenance.append(element('summary', 'About these boundaries'), element('p', collection.definition.description));
|
|
1026
|
-
detail.append(provenance);
|
|
770
|
+
sidebar?.updateDetail({
|
|
771
|
+
mode,
|
|
772
|
+
year,
|
|
773
|
+
source: neighborhoods.source,
|
|
774
|
+
selected: neighborhoods.selected,
|
|
775
|
+
});
|
|
1027
776
|
}
|
|
1028
777
|
function updateResults() {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
: undefined;
|
|
1035
|
-
const matches = searchNeighborhoods(search.value, source).sort((a, b) => a.canonicalName.localeCompare(b.canonicalName));
|
|
1036
|
-
count.textContent = `${matches.length} ${matches.length === 1 ? 'neighborhood' : 'neighborhoods'}${search.value ? ' found' : ' in this collection'}`;
|
|
1037
|
-
results.replaceChildren();
|
|
1038
|
-
for (const match of matches) {
|
|
1039
|
-
const node = element('button', match.canonicalName);
|
|
1040
|
-
node.type = 'button';
|
|
1041
|
-
node.dataset.neighborhoodResult = match.id;
|
|
1042
|
-
node.setAttribute('aria-pressed', String(selected?.id === match.id));
|
|
1043
|
-
results.append(node);
|
|
1044
|
-
if (focusedId === match.id)
|
|
1045
|
-
node.focus({ preventScroll: true });
|
|
1046
|
-
}
|
|
1047
|
-
if (!matches.length)
|
|
1048
|
-
results.append(element('p', strings.emptyResults ?? 'No matches. Try another name or definition source.', 'sf-explorer-note'));
|
|
1049
|
-
}
|
|
1050
|
-
function getSelection() {
|
|
1051
|
-
return selected
|
|
1052
|
-
? {
|
|
1053
|
-
id: selected.id,
|
|
1054
|
-
name: selected.properties.canonicalName,
|
|
1055
|
-
source,
|
|
1056
|
-
feature: structuredClone(selected),
|
|
1057
|
-
}
|
|
1058
|
-
: null;
|
|
1059
|
-
}
|
|
1060
|
-
function selectNeighborhood(name, options = {}) {
|
|
1061
|
-
if (destroyed)
|
|
1062
|
-
return false;
|
|
1063
|
-
const feature = name === null
|
|
1064
|
-
? undefined
|
|
1065
|
-
: (collections[source]?.features.find((entry) => entry.id === name) ??
|
|
1066
|
-
getNeighborhood(name, source));
|
|
1067
|
-
if (name !== null && !feature)
|
|
1068
|
-
return false;
|
|
1069
|
-
validateCameraOptions(options);
|
|
1070
|
-
const selectedItem = items.find((item) => item.feature === feature);
|
|
1071
|
-
const width = canvas.getBoundingClientRect().width;
|
|
1072
|
-
const target = selectedItem && options.fit !== false
|
|
1073
|
-
? width
|
|
1074
|
-
? fitViewport(selectedItem.bounds, width, fitPadding)
|
|
1075
|
-
: fitBounds(selectedItem.bounds)
|
|
1076
|
-
: null;
|
|
1077
|
-
if (feature &&
|
|
1078
|
-
!enabled('neighborhoodLines') &&
|
|
1079
|
-
!enabled('neighborhoodLabels') &&
|
|
1080
|
-
(layers.neighborhoodLines === undefined || layers.neighborhoodLabels === undefined))
|
|
1081
|
-
setMode('neighborhoods', false);
|
|
1082
|
-
const revision = ++neighborhoodRevision;
|
|
1083
|
-
const changed = selected !== feature;
|
|
1084
|
-
selected = feature;
|
|
1085
|
-
for (const item of items) {
|
|
1086
|
-
const active = item.feature === selected;
|
|
1087
|
-
item.node.setAttribute('fill', active ? (areaStyle.selectedFill ?? '#408dbe') : 'transparent');
|
|
1088
|
-
item.node.setAttribute('fill-opacity', active ? '.16' : '1');
|
|
1089
|
-
item.node.setAttribute('stroke', active ? (areaStyle.selectedStroke ?? '#176ba2') : (colors.neighborhood ?? '#9caebc'));
|
|
1090
|
-
item.node.setAttribute('stroke-width', active ? '2.2' : enabled('neighborhoodLines') ? '.55' : '0');
|
|
1091
|
-
if (selectableNeighborhoods)
|
|
1092
|
-
item.node.setAttribute('aria-pressed', String(active));
|
|
1093
|
-
if (active)
|
|
1094
|
-
areas.append(item.node);
|
|
1095
|
-
}
|
|
1096
|
-
if (feature)
|
|
1097
|
-
root.dataset.selectedNeighborhood = feature.id;
|
|
1098
|
-
else
|
|
1099
|
-
delete root.dataset.selectedNeighborhood;
|
|
1100
|
-
neighborhoodSelect.value = feature?.id ?? '';
|
|
1101
|
-
scheduleLabels();
|
|
1102
|
-
updateResults();
|
|
1103
|
-
updateDetail();
|
|
1104
|
-
status.textContent = feature
|
|
1105
|
-
? `${feature.properties.canonicalName} selected. ${formatSourceLabel(source)}.`
|
|
1106
|
-
: 'Neighborhood selection cleared.';
|
|
1107
|
-
if (changed)
|
|
1108
|
-
root.dispatchEvent(new CustomEvent('neighborhoodchange', {
|
|
1109
|
-
bubbles: true,
|
|
1110
|
-
detail: {
|
|
1111
|
-
feature: feature ? structuredClone(feature) : null,
|
|
1112
|
-
source,
|
|
1113
|
-
id: feature?.id ?? null,
|
|
1114
|
-
name: feature?.properties.canonicalName ?? null,
|
|
1115
|
-
},
|
|
1116
|
-
}));
|
|
1117
|
-
if (!destroyed && neighborhoodRevision === revision && target)
|
|
1118
|
-
moveView(target, options);
|
|
1119
|
-
return true;
|
|
1120
|
-
}
|
|
1121
|
-
function prepareSource(next, reset = true) {
|
|
1122
|
-
if (!Object.hasOwn(collections, next))
|
|
1123
|
-
throw new RangeError(`Unknown neighborhood source: ${next}`);
|
|
1124
|
-
const collection = collections[next];
|
|
1125
|
-
if (!collection ||
|
|
1126
|
-
!Array.isArray(collection.features) ||
|
|
1127
|
-
typeof collection.title !== 'string' ||
|
|
1128
|
-
typeof collection.definition?.description !== 'string')
|
|
1129
|
-
throw new TypeError('Neighborhood collections require features, title and definition description.');
|
|
1130
|
-
const ids = new Set();
|
|
1131
|
-
const nextOptions = [];
|
|
1132
|
-
const nextItems = collection.features.map((feature) => {
|
|
1133
|
-
if (!feature ||
|
|
1134
|
-
typeof feature.id !== 'string' ||
|
|
1135
|
-
!feature.id ||
|
|
1136
|
-
ids.has(feature.id) ||
|
|
1137
|
-
!feature.properties ||
|
|
1138
|
-
typeof feature.properties.canonicalName !== 'string' ||
|
|
1139
|
-
typeof feature.properties.sourceName !== 'string' ||
|
|
1140
|
-
!Array.isArray(feature.properties.aliases) ||
|
|
1141
|
-
Array.from(feature.properties.aliases).some((alias) => typeof alias !== 'string') ||
|
|
1142
|
-
!['Polygon', 'MultiPolygon'].includes(feature.geometry?.type))
|
|
1143
|
-
throw new TypeError('Neighborhood features require unique IDs, names, aliases and polygon geometry.');
|
|
1144
|
-
ids.add(feature.id);
|
|
1145
|
-
const bounds = projectedBounds(feature.geometry, map.project);
|
|
1146
|
-
if (!bounds.every(Number.isFinite))
|
|
1147
|
-
throw new RangeError('Neighborhood geometry must be nonempty and finite.');
|
|
1148
|
-
const node = svgElement('path', {
|
|
1149
|
-
d: geometryPath(feature.geometry, map.project),
|
|
1150
|
-
'data-neighborhood-id': feature.id,
|
|
1151
|
-
fill: 'transparent',
|
|
1152
|
-
'fill-rule': 'evenodd',
|
|
1153
|
-
stroke: colors.neighborhood ?? '#9caebc',
|
|
1154
|
-
'stroke-width': '.55',
|
|
1155
|
-
'vector-effect': 'non-scaling-stroke',
|
|
1156
|
-
role: 'button',
|
|
1157
|
-
tabindex: -1,
|
|
1158
|
-
'aria-label': feature.properties.canonicalName,
|
|
1159
|
-
'aria-pressed': 'false',
|
|
1160
|
-
});
|
|
1161
|
-
const option = element('option', feature.properties.canonicalName);
|
|
1162
|
-
option.value = feature.id;
|
|
1163
|
-
nextOptions.push(option);
|
|
1164
|
-
const title = svgElement('title');
|
|
1165
|
-
title.textContent = feature.properties.canonicalName;
|
|
1166
|
-
node.append(title);
|
|
1167
|
-
node.addEventListener('pointerenter', () => {
|
|
1168
|
-
if (destroyed ||
|
|
1169
|
-
!selectableNeighborhoods ||
|
|
1170
|
-
selected?.id === feature.id ||
|
|
1171
|
-
(!areaStyle.hoverFill && !areaStyle.hoverStroke))
|
|
1172
|
-
return;
|
|
1173
|
-
node.setAttribute('fill', areaStyle.hoverFill ?? '#408dbe');
|
|
1174
|
-
node.setAttribute('fill-opacity', '.12');
|
|
1175
|
-
node.setAttribute('stroke', areaStyle.hoverStroke ?? colors.neighborhood ?? '#176ba2');
|
|
1176
|
-
});
|
|
1177
|
-
node.addEventListener('pointerleave', () => {
|
|
1178
|
-
if (destroyed || selected?.id === feature.id)
|
|
1179
|
-
return;
|
|
1180
|
-
node.setAttribute('fill', 'transparent');
|
|
1181
|
-
node.setAttribute('fill-opacity', '1');
|
|
1182
|
-
node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
|
|
1183
|
-
});
|
|
1184
|
-
return {
|
|
1185
|
-
feature,
|
|
1186
|
-
node,
|
|
1187
|
-
bounds,
|
|
1188
|
-
point: interiorAnchor(feature.geometry, map.project),
|
|
1189
|
-
area: (bounds[2] - bounds[0]) * (bounds[3] - bounds[1]),
|
|
1190
|
-
};
|
|
778
|
+
sidebar?.updateResults({
|
|
779
|
+
mode,
|
|
780
|
+
year,
|
|
781
|
+
source: neighborhoods.source,
|
|
782
|
+
selected: neighborhoods.selected,
|
|
1191
783
|
});
|
|
1192
|
-
return () => {
|
|
1193
|
-
const hadSelection = !!selected;
|
|
1194
|
-
source = next;
|
|
1195
|
-
sourceSelect.value = source;
|
|
1196
|
-
selected = undefined;
|
|
1197
|
-
const revision = ++neighborhoodRevision;
|
|
1198
|
-
delete root.dataset.selectedNeighborhood;
|
|
1199
|
-
root.dataset.source = source;
|
|
1200
|
-
layerFades.crossfade(areas, () => areas.replaceChildren(...nextItems.map((item) => item.node)));
|
|
1201
|
-
neighborhoodSelect.replaceChildren(element('option', 'No neighborhood selected'));
|
|
1202
|
-
neighborhoodSelect.options[0].value = '';
|
|
1203
|
-
neighborhoodSelect.append(...nextOptions);
|
|
1204
|
-
items = nextItems;
|
|
1205
|
-
orderedNeighborhoods = nextItems
|
|
1206
|
-
.filter((item) => item.point !== undefined)
|
|
1207
|
-
.sort((a, b) => b.area - a.area || a.feature.id.localeCompare(b.feature.id));
|
|
1208
|
-
if (items[0])
|
|
1209
|
-
items[0].node.setAttribute('tabindex', '0');
|
|
1210
|
-
updateResults();
|
|
1211
|
-
updateDetail();
|
|
1212
|
-
updateComposition();
|
|
1213
|
-
status.textContent =
|
|
1214
|
-
mode === 'basemap'
|
|
1215
|
-
? 'San Francisco basemap.'
|
|
1216
|
-
: mode === 'districts'
|
|
1217
|
-
? `${year} supervisorial districts. Numbers identify each district.`
|
|
1218
|
-
: `${formatSourceLabel(source)}. Select a neighborhood to begin.`;
|
|
1219
|
-
if (hadSelection)
|
|
1220
|
-
root.dispatchEvent(new CustomEvent('neighborhoodchange', {
|
|
1221
|
-
bubbles: true,
|
|
1222
|
-
detail: { feature: null, source, id: null, name: null },
|
|
1223
|
-
}));
|
|
1224
|
-
if (reset && !destroyed && neighborhoodRevision === revision)
|
|
1225
|
-
resetView();
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
function setSource(next) {
|
|
1229
|
-
if (!destroyed)
|
|
1230
|
-
prepareSource(next)();
|
|
1231
784
|
}
|
|
1232
785
|
function syncFeatureControls() {
|
|
1233
786
|
featureControls.hidden = neighborhoodLabel.hidden && markerLabel.hidden;
|
|
@@ -1273,9 +826,13 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1273
826
|
chrome === 'explorer' ||
|
|
1274
827
|
controls.neighborhoodPicker === false;
|
|
1275
828
|
syncFeatureControls();
|
|
1276
|
-
for (const item of items) {
|
|
1277
|
-
item.node.setAttribute('stroke-width', item.feature === selected
|
|
1278
|
-
|
|
829
|
+
for (const item of neighborhoods.items) {
|
|
830
|
+
item.node.setAttribute('stroke-width', item.feature === neighborhoods.selected
|
|
831
|
+
? '2.2'
|
|
832
|
+
: enabled('neighborhoodLines')
|
|
833
|
+
? '.55'
|
|
834
|
+
: '0');
|
|
835
|
+
item.node.setAttribute('tabindex', selectableNeighborhoods && item === neighborhoods.items[0] ? '0' : '-1');
|
|
1279
836
|
item.node.setAttribute('role', selectableNeighborhoods ? 'button' : 'img');
|
|
1280
837
|
if (!selectableNeighborhoods)
|
|
1281
838
|
item.node.removeAttribute('aria-pressed');
|
|
@@ -1290,7 +847,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1290
847
|
if (districtsVisible)
|
|
1291
848
|
descriptions.push(`${year} supervisorial districts, DataSF.`);
|
|
1292
849
|
if (neighborhoodsVisible) {
|
|
1293
|
-
const collection = collections[source];
|
|
850
|
+
const collection = collections[neighborhoods.source];
|
|
1294
851
|
if (collection)
|
|
1295
852
|
descriptions.push(`${collection.title}. ${collection.definition.description}`);
|
|
1296
853
|
}
|
|
@@ -1314,7 +871,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1314
871
|
}
|
|
1315
872
|
function getSelectedMarker() {
|
|
1316
873
|
const marker = markerCatalog.items.find((item) => item.marker.id === selectedMarker)?.marker;
|
|
1317
|
-
return marker ? { ...marker } : null;
|
|
874
|
+
return marker ? { ...marker, position: [...marker.position] } : null;
|
|
1318
875
|
}
|
|
1319
876
|
function selectMarker(id, options = {}) {
|
|
1320
877
|
if (destroyed)
|
|
@@ -1403,22 +960,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1403
960
|
validateSwitchPatch(patch, controlKeys);
|
|
1404
961
|
const next = { ...controls, ...patch };
|
|
1405
962
|
controls = next;
|
|
1406
|
-
|
|
1407
|
-
node.hidden = controls.zoom === false;
|
|
1408
|
-
panControls.hidden = controls.pan === false;
|
|
1409
|
-
resetButton.hidden = controls.reset === false;
|
|
1410
|
-
labelsButton.hidden = controls.labels === false;
|
|
1411
|
-
touchButton.hidden = controls.touch === false;
|
|
1412
|
-
if (touchButton.hidden && touchNavigation)
|
|
1413
|
-
setTouchNavigation(false);
|
|
1414
|
-
zoomControls.hidden = [...zoomControls.children].every((node) => node.hidden);
|
|
1415
|
-
toolbar.hidden = zoomControls.hidden && panControls.hidden;
|
|
1416
|
-
hint.hidden = controls.help === false;
|
|
1417
|
-
if (hint.hidden)
|
|
1418
|
-
svg.setAttribute('aria-describedby', hint.id);
|
|
1419
|
-
else
|
|
1420
|
-
svg.removeAttribute('aria-describedby');
|
|
1421
|
-
status.classList.toggle('sf-explorer-visually-hidden', controls.status === false);
|
|
963
|
+
mapControls.update(controls);
|
|
1422
964
|
markerLabel.hidden = !markerCatalog.items.length || controls.markerPicker === false;
|
|
1423
965
|
updateComposition();
|
|
1424
966
|
}
|
|
@@ -1446,9 +988,9 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1446
988
|
? 'San Francisco basemap.'
|
|
1447
989
|
: districts
|
|
1448
990
|
? `${year} supervisorial districts. Numbers identify each district.`
|
|
1449
|
-
: selected
|
|
1450
|
-
? `${selected.properties.canonicalName} selected. ${formatSourceLabel(source)}.`
|
|
1451
|
-
: `${formatSourceLabel(source)}. Select a neighborhood to begin.`;
|
|
991
|
+
: neighborhoods.selected
|
|
992
|
+
? `${neighborhoods.selected.properties.canonicalName} selected. ${formatSourceLabel(neighborhoods.source)}.`
|
|
993
|
+
: `${formatSourceLabel(neighborhoods.source)}. Select a neighborhood to begin.`;
|
|
1452
994
|
if (reset)
|
|
1453
995
|
resetView();
|
|
1454
996
|
}
|
|
@@ -1458,7 +1000,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1458
1000
|
if (typeof visible !== 'boolean')
|
|
1459
1001
|
throw new TypeError('Labels must be a boolean.');
|
|
1460
1002
|
labels = visible;
|
|
1461
|
-
|
|
1003
|
+
mapControls.updateLabels(labels);
|
|
1462
1004
|
root.dataset.labels = String(labels);
|
|
1463
1005
|
updateComposition();
|
|
1464
1006
|
if (!labels) {
|
|
@@ -1467,61 +1009,6 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1467
1009
|
}
|
|
1468
1010
|
scheduleLabels();
|
|
1469
1011
|
}
|
|
1470
|
-
listen(modeSelect, 'change', () => {
|
|
1471
|
-
const next = modeSelect.value;
|
|
1472
|
-
if (next === 'neighborhoods' || next === 'districts' || next === 'basemap')
|
|
1473
|
-
setMode(next);
|
|
1474
|
-
});
|
|
1475
|
-
listen(search, 'input', updateResults);
|
|
1476
|
-
listen(search, 'keydown', (event) => {
|
|
1477
|
-
if (event.key === 'Enter') {
|
|
1478
|
-
const exact = getNeighborhood(search.value, source);
|
|
1479
|
-
const matches = searchNeighborhoods(search.value, source);
|
|
1480
|
-
if (exact || matches.length === 1) {
|
|
1481
|
-
event.preventDefault();
|
|
1482
|
-
selectNeighborhood(exact?.id ?? matches[0].id);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
});
|
|
1486
|
-
listen(sourceSelect, 'change', () => {
|
|
1487
|
-
const next = sourceSelect.value;
|
|
1488
|
-
if (next === 'realtor' || next === 'sf-find' || next === 'analysis')
|
|
1489
|
-
setSource(next);
|
|
1490
|
-
});
|
|
1491
|
-
listen(results, 'click', (event) => {
|
|
1492
|
-
const node = event.target instanceof Element
|
|
1493
|
-
? event.target.closest('[data-neighborhood-result]')
|
|
1494
|
-
: null;
|
|
1495
|
-
if (node?.dataset.neighborhoodResult)
|
|
1496
|
-
selectNeighborhood(node.dataset.neighborhoodResult);
|
|
1497
|
-
});
|
|
1498
|
-
const touchLabel = strings.touchNavigation ?? 'Touch navigation';
|
|
1499
|
-
const touchButton = button(touchLabel, strings.touchNavigationLabel ?? `Enable ${touchLabel.toLowerCase()}`, () => setTouchNavigation(!touchNavigation));
|
|
1500
|
-
touchButton.hidden = controls.touch === false;
|
|
1501
|
-
let touchNavigation = false;
|
|
1502
|
-
const navigation = attachNavigation(svg, getViewport, (next) => {
|
|
1503
|
-
stopAnimation();
|
|
1504
|
-
setView(next);
|
|
1505
|
-
}, controller.signal, () => setTouchNavigation(false));
|
|
1506
|
-
function setTouchNavigation(enabled) {
|
|
1507
|
-
if (destroyed)
|
|
1508
|
-
return;
|
|
1509
|
-
if (typeof enabled !== 'boolean')
|
|
1510
|
-
throw new TypeError('Touch navigation must be a boolean.');
|
|
1511
|
-
touchNavigation = enabled;
|
|
1512
|
-
navigation.setTouchNavigation(enabled);
|
|
1513
|
-
canvas.style.touchAction = enabled ? 'none' : 'pan-y pinch-zoom';
|
|
1514
|
-
touchButton.textContent = enabled
|
|
1515
|
-
? (strings.touchNavigationDone ?? 'Done: page scrolling')
|
|
1516
|
-
: touchLabel;
|
|
1517
|
-
touchButton.setAttribute('aria-label', enabled
|
|
1518
|
-
? (strings.touchNavigationExitLabel ?? `Exit ${touchLabel.toLowerCase()}`)
|
|
1519
|
-
: (strings.touchNavigationLabel ?? `Enable ${touchLabel.toLowerCase()}`));
|
|
1520
|
-
touchButton.setAttribute('aria-pressed', String(enabled));
|
|
1521
|
-
root.dataset.touchNavigation = String(enabled);
|
|
1522
|
-
}
|
|
1523
|
-
listen(neighborhoodSelect, 'change', () => selectNeighborhood(neighborhoodSelect.value || null));
|
|
1524
|
-
listen(markerSelect, 'change', () => selectMarker(markerSelect.value || null));
|
|
1525
1012
|
listen(svg, 'keydown', (event) => {
|
|
1526
1013
|
const target = event.target instanceof SVGElement ? event.target : null;
|
|
1527
1014
|
const districtId = Number(target?.dataset.district);
|
|
@@ -1555,9 +1042,10 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1555
1042
|
}
|
|
1556
1043
|
else if (event.key === '[' || event.key === ']') {
|
|
1557
1044
|
event.preventDefault();
|
|
1558
|
-
const i = items.findIndex((item) => item.feature.id === id);
|
|
1559
|
-
const next = items[(i + (event.key === ']' ? 1 : items.length - 1)) %
|
|
1560
|
-
|
|
1045
|
+
const i = neighborhoods.items.findIndex((item) => item.feature.id === id);
|
|
1046
|
+
const next = neighborhoods.items[(i + (event.key === ']' ? 1 : neighborhoods.items.length - 1)) %
|
|
1047
|
+
neighborhoods.items.length];
|
|
1048
|
+
for (const item of neighborhoods.items)
|
|
1561
1049
|
item.node.setAttribute('tabindex', item === next ? '0' : '-1');
|
|
1562
1050
|
next.node.focus();
|
|
1563
1051
|
}
|
|
@@ -1629,7 +1117,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1629
1117
|
listen(svg, 'pointerdown', stopAnimation);
|
|
1630
1118
|
let observer;
|
|
1631
1119
|
const onResize = () => {
|
|
1632
|
-
|
|
1120
|
+
mapControls.cancelNavigation();
|
|
1633
1121
|
scheduleLabels();
|
|
1634
1122
|
root.dispatchEvent(new CustomEvent('mapresize', { bubbles: true }));
|
|
1635
1123
|
};
|
|
@@ -1645,7 +1133,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1645
1133
|
throw new RangeError('No neighborhood dataset was supplied.');
|
|
1646
1134
|
if (patch.source !== undefined && !Object.hasOwn(collections, patch.source))
|
|
1647
1135
|
throw new RangeError(`Unknown neighborhood source: ${patch.source}`);
|
|
1648
|
-
const commitSource = patch.source !== undefined && patch.source !== source
|
|
1136
|
+
const commitSource = patch.source !== undefined && patch.source !== neighborhoods.source
|
|
1649
1137
|
? prepareSource(patch.source, false)
|
|
1650
1138
|
: undefined;
|
|
1651
1139
|
const appearance = patch.appearance;
|
|
@@ -1726,8 +1214,8 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1726
1214
|
symbol.style.setProperty(kind === 'bart' ? 'border-color' : 'background-color', colors[kind]);
|
|
1727
1215
|
}
|
|
1728
1216
|
}
|
|
1729
|
-
for (const item of items) {
|
|
1730
|
-
const active = item.feature === selected;
|
|
1217
|
+
for (const item of neighborhoods.items) {
|
|
1218
|
+
const active = item.feature === neighborhoods.selected;
|
|
1731
1219
|
item.node.setAttribute('fill', active ? (areaStyle.selectedFill ?? '#408dbe') : 'transparent');
|
|
1732
1220
|
item.node.setAttribute('stroke', active ? (areaStyle.selectedStroke ?? '#176ba2') : (colors.neighborhood ?? '#9caebc'));
|
|
1733
1221
|
}
|
|
@@ -1753,7 +1241,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1753
1241
|
if (destroyed || revision !== presentationRevision || !isCurrent())
|
|
1754
1242
|
return false;
|
|
1755
1243
|
if (commitSource && appearance)
|
|
1756
|
-
for (const item of items)
|
|
1244
|
+
for (const item of neighborhoods.items)
|
|
1757
1245
|
item.node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
|
|
1758
1246
|
if (patch.mode !== undefined)
|
|
1759
1247
|
setMode(patch.mode, false);
|
|
@@ -1764,7 +1252,7 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1764
1252
|
}
|
|
1765
1253
|
const explorer = Object.assign(root, {
|
|
1766
1254
|
applyPresentation,
|
|
1767
|
-
getMapState: () => ({ mode, source, year, labels }),
|
|
1255
|
+
getMapState: () => ({ mode, source: neighborhoods.source, year, labels }),
|
|
1768
1256
|
selectNeighborhood,
|
|
1769
1257
|
getSelection,
|
|
1770
1258
|
selectDistrict,
|
|
@@ -1801,13 +1289,14 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1801
1289
|
districtMorph.stop();
|
|
1802
1290
|
cancelEntrances();
|
|
1803
1291
|
controller.abort();
|
|
1804
|
-
|
|
1292
|
+
overlays.destroy();
|
|
1805
1293
|
clusters.destroy();
|
|
1806
1294
|
canvas.style.touchAction = 'pan-y pinch-zoom';
|
|
1807
1295
|
observer?.disconnect();
|
|
1808
1296
|
frames.destroy();
|
|
1809
1297
|
labelRenderer.clear();
|
|
1810
|
-
|
|
1298
|
+
sidebar?.destroy();
|
|
1299
|
+
neighborhoods.destroy();
|
|
1811
1300
|
},
|
|
1812
1301
|
});
|
|
1813
1302
|
try {
|
|
@@ -1828,12 +1317,12 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1828
1317
|
}, { signal: controller.signal });
|
|
1829
1318
|
const initialMode = mode;
|
|
1830
1319
|
if (sources.length)
|
|
1831
|
-
setSource(source);
|
|
1320
|
+
setSource(neighborhoods.source);
|
|
1832
1321
|
if (neighborhood !== undefined)
|
|
1833
1322
|
selectNeighborhood(neighborhood);
|
|
1834
1323
|
setMode(initialMode);
|
|
1835
|
-
if (initialMode === 'neighborhoods' && selected) {
|
|
1836
|
-
const selectedItem = items.find((item) => item.feature === selected);
|
|
1324
|
+
if (initialMode === 'neighborhoods' && neighborhoods.selected) {
|
|
1325
|
+
const selectedItem = neighborhoods.items.find((item) => item.feature === neighborhoods.selected);
|
|
1837
1326
|
if (selectedItem)
|
|
1838
1327
|
setView(fitBounds(selectedItem.bounds));
|
|
1839
1328
|
}
|