@treasuryspatial/map-react 0.1.19 → 0.1.22
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/dist/MapModeViewport.d.ts +29 -8
- package/dist/MapModeViewport.d.ts.map +1 -1
- package/dist/MapModeViewport.js +598 -131
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -3
package/dist/MapModeViewport.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
|
5
4
|
import * as THREE from 'three';
|
|
6
|
-
import { buildGeoJsonPolygon, computeCentroid, computeFootprintStats, createMapThreeLayer,
|
|
5
|
+
import { buildGeoJsonPolygon, buildGeoJsonLineString, computeCentroid, computeFootprintStats, computePolylineStats, createMapThreeLayer, ensureSelectionLayers, getOuterRing, offsetLngLatFromLocalMeters, toLocalMeters, toLineFeature, toPolygonFeature, } from '@treasuryspatial/map-kit';
|
|
7
6
|
const DEFAULT_CENTER = [-87.6298, 41.8781];
|
|
8
7
|
const DEFAULT_ZOOM = 16.5;
|
|
9
8
|
const DEFAULT_PITCH = 55;
|
|
10
|
-
|
|
9
|
+
const DEFAULT_VIEW_DURATION_MS = 900;
|
|
10
|
+
const DEFAULT_PERSPECTIVE_PADDING = { top: 120, right: 120, bottom: 150, left: 120 };
|
|
11
|
+
const DEFAULT_INTERIOR_PADDING = { top: 110, right: 96, bottom: 210, left: 96 };
|
|
12
|
+
const MIN_VIEW_SPAN_METERS = 8;
|
|
13
|
+
const MAX_MAP_ZOOM = 21;
|
|
14
|
+
const SUPPORTED_MAP_SOURCE_KINDS = new Set(['building', 'waterway', 'road', 'rail']);
|
|
15
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
const toneMappingLookup = {
|
|
17
|
+
neutral: THREE.NeutralToneMapping,
|
|
18
|
+
aces: THREE.ACESFilmicToneMapping,
|
|
19
|
+
linear: THREE.LinearToneMapping,
|
|
20
|
+
};
|
|
21
|
+
const shadowMapTypeLookup = {
|
|
22
|
+
pcfsoft: THREE.PCFSoftShadowMap,
|
|
23
|
+
pcf: THREE.PCFShadowMap,
|
|
24
|
+
basic: THREE.BasicShadowMap,
|
|
25
|
+
};
|
|
26
|
+
export const MapModeViewport = forwardRef(function MapModeViewport({ active, geometry3dm, meshGroup, materialSettings, lightingPreset = 'studio', extrusionsVisible = false, extrusionColor = '#262b31', selectionConfig, confirmedSelection, confirmedFeature, candidate, mapVisibility, onCandidateSelect, onCandidateClear, accessToken: accessTokenProp, mapStyle: mapStyleProp, applyLightingPreset, resolveMeshGroup, }, ref) {
|
|
11
27
|
const containerRef = useRef(null);
|
|
12
28
|
const mapRef = useRef(null);
|
|
13
29
|
const mapboxRef = useRef(null);
|
|
@@ -19,14 +35,29 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
19
35
|
const lightingPresetRef = useRef(lightingPreset);
|
|
20
36
|
const handlersBoundRef = useRef(false);
|
|
21
37
|
const confirmedSelectionRef = useRef(confirmedSelection ?? null);
|
|
38
|
+
const candidateSelectionRef = useRef(candidate?.selection ?? null);
|
|
22
39
|
const onCandidateSelectRef = useRef(onCandidateSelect);
|
|
23
40
|
const onCandidateClearRef = useRef(onCandidateClear);
|
|
41
|
+
const selectionConfigRef = useRef(selectionConfig ?? null);
|
|
24
42
|
const lastGeocodeLabelRef = useRef(null);
|
|
25
43
|
const reverseGeocodeRequestRef = useRef(0);
|
|
26
44
|
const resizeObserverRef = useRef(null);
|
|
27
45
|
const lastOriginRef = useRef(confirmedSelection?.origin ?? null);
|
|
46
|
+
const candidateFeatureRef = useRef(candidate?.feature ?? null);
|
|
47
|
+
const confirmedFeatureRef = useRef(confirmedFeature ?? null);
|
|
48
|
+
const activeMeshGroupRef = useRef(meshGroup ?? null);
|
|
49
|
+
const extrusionsVisibleRef = useRef(extrusionsVisible);
|
|
50
|
+
const extrusionColorRef = useRef(extrusionColor);
|
|
51
|
+
const mapVisibilityRef = useRef(mapVisibility);
|
|
28
52
|
const [mapReady, setMapReady] = useState(false);
|
|
29
53
|
const [mapError, setMapError] = useState('');
|
|
54
|
+
const selectionConfigError = useMemo(() => {
|
|
55
|
+
const sourceKind = selectionConfig?.sourceKind;
|
|
56
|
+
if (!sourceKind || SUPPORTED_MAP_SOURCE_KINDS.has(sourceKind))
|
|
57
|
+
return '';
|
|
58
|
+
return `Unsupported map feature sourceKind '${sourceKind}'. Manifest must use building, waterway, road, or rail.`;
|
|
59
|
+
}, [selectionConfig?.sourceKind]);
|
|
60
|
+
const activeMapError = mapError || selectionConfigError;
|
|
30
61
|
const [threeLayerReady, setThreeLayerReady] = useState(false);
|
|
31
62
|
const debug = useMemo(() => {
|
|
32
63
|
if (typeof window === 'undefined')
|
|
@@ -34,34 +65,89 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
34
65
|
const params = new URLSearchParams(window.location.search);
|
|
35
66
|
return params.has('mapDebug') || process.env.NODE_ENV !== 'production';
|
|
36
67
|
}, []);
|
|
68
|
+
const [themeMode, setThemeMode] = useState(() => {
|
|
69
|
+
if (typeof document === 'undefined')
|
|
70
|
+
return 'dark';
|
|
71
|
+
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
72
|
+
});
|
|
37
73
|
const log = useCallback((...args) => {
|
|
38
74
|
if (!debug)
|
|
39
75
|
return;
|
|
40
76
|
console.info('[map-react]', ...args);
|
|
41
77
|
}, [debug]);
|
|
78
|
+
const fmt = useCallback((value) => (Number.isFinite(value) ? value.toFixed(3) : 'nan'), []);
|
|
42
79
|
useEffect(() => {
|
|
43
80
|
lightingPresetRef.current = lightingPreset;
|
|
44
81
|
}, [lightingPreset]);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
extrusionsVisibleRef.current = extrusionsVisible;
|
|
84
|
+
}, [extrusionsVisible]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
extrusionColorRef.current = extrusionColor;
|
|
87
|
+
}, [extrusionColor]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
mapVisibilityRef.current = mapVisibility;
|
|
90
|
+
}, [mapVisibility]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
selectionConfigRef.current = selectionConfig ?? null;
|
|
93
|
+
layerControllerRef.current?.setConfig(selectionConfig ?? null);
|
|
94
|
+
}, [selectionConfig]);
|
|
95
|
+
const applySceneLighting = useCallback((scene, renderer, presetKey) => {
|
|
96
|
+
const maybePromise = (applyLightingPreset ?? defaultApplyLightingPreset)(scene, renderer, presetKey);
|
|
97
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
98
|
+
void Promise.resolve(maybePromise).finally(() => {
|
|
99
|
+
applyLightingReadabilityBoost(renderer, presetKey);
|
|
100
|
+
mapRef.current?.triggerRepaint();
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
applyLightingReadabilityBoost(renderer, presetKey);
|
|
105
|
+
mapRef.current?.triggerRepaint();
|
|
106
|
+
}, [applyLightingPreset]);
|
|
45
107
|
useEffect(() => {
|
|
46
108
|
confirmedSelectionRef.current = confirmedSelection ?? null;
|
|
47
109
|
if (confirmedSelection?.origin) {
|
|
48
110
|
lastOriginRef.current = confirmedSelection.origin;
|
|
49
111
|
}
|
|
50
112
|
}, [confirmedSelection]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
candidateSelectionRef.current = candidate?.selection ?? null;
|
|
115
|
+
if (candidate?.selection?.origin) {
|
|
116
|
+
lastOriginRef.current = candidate.selection.origin;
|
|
117
|
+
}
|
|
118
|
+
}, [candidate]);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
candidateFeatureRef.current = candidate?.feature ?? null;
|
|
121
|
+
}, [candidate]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
confirmedFeatureRef.current = confirmedFeature ?? null;
|
|
124
|
+
}, [confirmedFeature]);
|
|
51
125
|
useEffect(() => {
|
|
52
126
|
onCandidateSelectRef.current = onCandidateSelect;
|
|
53
127
|
}, [onCandidateSelect]);
|
|
54
128
|
useEffect(() => {
|
|
55
129
|
onCandidateClearRef.current = onCandidateClear;
|
|
56
130
|
}, [onCandidateClear]);
|
|
131
|
+
const applyCurrentSelectionToController = useCallback(() => {
|
|
132
|
+
const controller = layerControllerRef.current;
|
|
133
|
+
if (!controller)
|
|
134
|
+
return;
|
|
135
|
+
const selectedFeature = candidateFeatureRef.current ?? confirmedFeatureRef.current ?? null;
|
|
136
|
+
const selectedSourceKind = candidateSelectionRef.current?.sourceKind ?? confirmedSelectionRef.current?.sourceKind ?? null;
|
|
137
|
+
controller.setSelected(selectedFeature, selectedSourceKind);
|
|
138
|
+
}, []);
|
|
57
139
|
const accessToken = useMemo(() => accessTokenProp ?? process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN ?? '', [accessTokenProp]);
|
|
58
140
|
const mapStyle = useMemo(() => {
|
|
59
141
|
if (mapStyleProp)
|
|
60
142
|
return mapStyleProp;
|
|
61
143
|
const styleDark = process.env.NEXT_PUBLIC_MAPBOX_FOOTPRINT_STYLE_DARK;
|
|
62
144
|
const styleLight = process.env.NEXT_PUBLIC_MAPBOX_FOOTPRINT_STYLE_LIGHT;
|
|
145
|
+
const publicFallbackStyle = themeMode === 'light'
|
|
146
|
+
? 'mapbox://styles/mapbox/light-v11'
|
|
147
|
+
: 'mapbox://styles/mapbox/dark-v11';
|
|
63
148
|
if (typeof window !== 'undefined' && (styleDark || styleLight)) {
|
|
64
|
-
const prefersDark =
|
|
149
|
+
const prefersDark = themeMode === 'dark' ||
|
|
150
|
+
(themeMode !== 'light' && (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false));
|
|
65
151
|
const chosen = prefersDark ? (styleDark ?? styleLight) : (styleLight ?? styleDark);
|
|
66
152
|
if (chosen)
|
|
67
153
|
return chosen;
|
|
@@ -69,16 +155,151 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
69
155
|
return (process.env.NEXT_PUBLIC_MAPBOX_FOOTPRINT_STYLE ??
|
|
70
156
|
styleDark ??
|
|
71
157
|
styleLight ??
|
|
72
|
-
|
|
73
|
-
}, [mapStyleProp]);
|
|
158
|
+
publicFallbackStyle);
|
|
159
|
+
}, [mapStyleProp, themeMode]);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (typeof document === 'undefined')
|
|
162
|
+
return;
|
|
163
|
+
const root = document.documentElement;
|
|
164
|
+
const readTheme = () => root.getAttribute('data-theme') === 'light' ? setThemeMode('light') : setThemeMode('dark');
|
|
165
|
+
readTheme();
|
|
166
|
+
const observer = new MutationObserver(readTheme);
|
|
167
|
+
observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] });
|
|
168
|
+
return () => observer.disconnect();
|
|
169
|
+
}, []);
|
|
170
|
+
const fetchSearchSuggestions = useCallback(async (query, limit = 12) => {
|
|
171
|
+
const trimmed = query.trim();
|
|
172
|
+
if (!trimmed || !accessToken)
|
|
173
|
+
return [];
|
|
174
|
+
const map = mapRef.current;
|
|
175
|
+
const center = map?.getCenter().wrap();
|
|
176
|
+
const proximity = center && Number.isFinite(center.lng) && Number.isFinite(center.lat)
|
|
177
|
+
? `${center.lng},${center.lat}`
|
|
178
|
+
: `${DEFAULT_CENTER[0]},${DEFAULT_CENTER[1]}`;
|
|
179
|
+
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(trimmed)}.json` +
|
|
180
|
+
`?access_token=${encodeURIComponent(accessToken)}` +
|
|
181
|
+
`&limit=${Math.max(1, Math.min(12, limit))}` +
|
|
182
|
+
`&autocomplete=true&types=address,place,locality,neighborhood,poi` +
|
|
183
|
+
`&proximity=${encodeURIComponent(proximity)}`;
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetch(url);
|
|
186
|
+
if (!response.ok)
|
|
187
|
+
return [];
|
|
188
|
+
const data = await response.json();
|
|
189
|
+
const features = Array.isArray(data?.features) ? data.features : [];
|
|
190
|
+
return features
|
|
191
|
+
.map((feature) => {
|
|
192
|
+
const featureCenter = feature?.center;
|
|
193
|
+
if (!Array.isArray(featureCenter) || featureCenter.length < 2)
|
|
194
|
+
return null;
|
|
195
|
+
const label = String(feature?.text ?? feature?.place_name ?? '').trim();
|
|
196
|
+
const placeName = String(feature?.place_name ?? '').trim();
|
|
197
|
+
const secondaryLabel = placeName && placeName !== label
|
|
198
|
+
? placeName.replace(new RegExp(`^${escapeRegExp(label)}(?:,\\s*)?`, 'i'), '').trim() || undefined
|
|
199
|
+
: undefined;
|
|
200
|
+
const bboxValue = Array.isArray(feature?.bbox) && feature.bbox.length === 4
|
|
201
|
+
? feature.bbox.map(Number)
|
|
202
|
+
: undefined;
|
|
203
|
+
return {
|
|
204
|
+
id: String(feature?.id ?? placeName ?? `${featureCenter[0]},${featureCenter[1]}`),
|
|
205
|
+
label: label || placeName || trimmed,
|
|
206
|
+
secondaryLabel,
|
|
207
|
+
center: [Number(featureCenter[0]), Number(featureCenter[1])],
|
|
208
|
+
bbox: bboxValue,
|
|
209
|
+
};
|
|
210
|
+
})
|
|
211
|
+
.filter((value) => Boolean(value));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}, [accessToken]);
|
|
217
|
+
const applySearchSuggestion = useCallback((suggestion) => {
|
|
218
|
+
const map = mapRef.current;
|
|
219
|
+
if (!map)
|
|
220
|
+
return false;
|
|
221
|
+
lastGeocodeLabelRef.current = suggestion.secondaryLabel
|
|
222
|
+
? `${suggestion.label}, ${suggestion.secondaryLabel}`
|
|
223
|
+
: suggestion.label;
|
|
224
|
+
map.stop();
|
|
225
|
+
map.flyTo({
|
|
226
|
+
center: suggestion.center,
|
|
227
|
+
zoom: Math.max(map.getZoom(), suggestion.bbox ? 16.8 : 17.4),
|
|
228
|
+
speed: 1.8,
|
|
229
|
+
curve: 1.12,
|
|
230
|
+
essential: true,
|
|
231
|
+
});
|
|
232
|
+
return true;
|
|
233
|
+
}, []);
|
|
234
|
+
const searchAddress = useCallback(async (query) => {
|
|
235
|
+
const [firstSuggestion] = await fetchSearchSuggestions(query, 1);
|
|
236
|
+
if (!firstSuggestion)
|
|
237
|
+
return false;
|
|
238
|
+
return applySearchSuggestion(firstSuggestion);
|
|
239
|
+
}, [applySearchSuggestion, fetchSearchSuggestions]);
|
|
74
240
|
useImperativeHandle(ref, () => ({
|
|
75
241
|
setLightingPreset: (presetKey) => {
|
|
76
242
|
lightingPresetRef.current = presetKey;
|
|
77
243
|
if (sceneRef.current && rendererRef.current) {
|
|
78
|
-
(
|
|
244
|
+
applySceneLighting(sceneRef.current, rendererRef.current, presetKey);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
animateToView: (viewKey) => {
|
|
248
|
+
const map = mapRef.current;
|
|
249
|
+
const mapboxgl = mapboxRef.current;
|
|
250
|
+
if (!map || !mapboxgl)
|
|
251
|
+
return;
|
|
252
|
+
const origin = confirmedSelectionRef.current?.origin ??
|
|
253
|
+
candidateSelectionRef.current?.origin ??
|
|
254
|
+
lastOriginRef.current ??
|
|
255
|
+
null;
|
|
256
|
+
if (!origin)
|
|
257
|
+
return;
|
|
258
|
+
const frame = resolveGroundFrameMeters(activeMeshGroupRef.current) ?? resolveSelectionFrameMeters(confirmedSelectionRef.current ?? candidateSelectionRef.current ?? null);
|
|
259
|
+
if (!frame)
|
|
260
|
+
return;
|
|
261
|
+
const center = offsetLngLatFromLocalMeters(origin, {
|
|
262
|
+
eastMeters: frame.centerEastMeters,
|
|
263
|
+
northMeters: frame.centerNorthMeters,
|
|
264
|
+
}, mapboxgl);
|
|
265
|
+
const halfEast = Math.max(MIN_VIEW_SPAN_METERS, frame.spanEastMeters) / 2;
|
|
266
|
+
const halfNorth = Math.max(MIN_VIEW_SPAN_METERS, frame.spanNorthMeters) / 2;
|
|
267
|
+
const corners = [
|
|
268
|
+
offsetLngLatFromLocalMeters(origin, { eastMeters: frame.centerEastMeters - halfEast, northMeters: frame.centerNorthMeters - halfNorth }, mapboxgl),
|
|
269
|
+
offsetLngLatFromLocalMeters(origin, { eastMeters: frame.centerEastMeters - halfEast, northMeters: frame.centerNorthMeters + halfNorth }, mapboxgl),
|
|
270
|
+
offsetLngLatFromLocalMeters(origin, { eastMeters: frame.centerEastMeters + halfEast, northMeters: frame.centerNorthMeters - halfNorth }, mapboxgl),
|
|
271
|
+
offsetLngLatFromLocalMeters(origin, { eastMeters: frame.centerEastMeters + halfEast, northMeters: frame.centerNorthMeters + halfNorth }, mapboxgl),
|
|
272
|
+
];
|
|
273
|
+
const bounds = corners.reduce((acc, point) => acc.extend([point.lng, point.lat]), new mapboxgl.LngLatBounds([corners[0].lng, corners[0].lat], [corners[0].lng, corners[0].lat]));
|
|
274
|
+
const isInteriorView = viewKey === 'interior';
|
|
275
|
+
const pitch = isInteriorView ? 72 : DEFAULT_PITCH;
|
|
276
|
+
const padding = isInteriorView ? DEFAULT_INTERIOR_PADDING : DEFAULT_PERSPECTIVE_PADDING;
|
|
277
|
+
const bearing = map.getBearing();
|
|
278
|
+
const camera = map.cameraForBounds(bounds, { padding, pitch, bearing });
|
|
279
|
+
const zoomDelta = isInteriorView ? 0.85 : 0;
|
|
280
|
+
if (camera) {
|
|
281
|
+
map.easeTo({
|
|
282
|
+
center: camera.center ?? center,
|
|
283
|
+
zoom: Math.min(MAX_MAP_ZOOM, (camera.zoom ?? map.getZoom()) + zoomDelta),
|
|
284
|
+
bearing,
|
|
285
|
+
pitch,
|
|
286
|
+
duration: DEFAULT_VIEW_DURATION_MS,
|
|
287
|
+
essential: true,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
79
290
|
}
|
|
291
|
+
map.easeTo({
|
|
292
|
+
center: [center.lng, center.lat],
|
|
293
|
+
zoom: Math.min(MAX_MAP_ZOOM, map.getZoom() + (isInteriorView ? 0.9 : 0.2)),
|
|
294
|
+
bearing,
|
|
295
|
+
pitch,
|
|
296
|
+
duration: DEFAULT_VIEW_DURATION_MS,
|
|
297
|
+
essential: true,
|
|
298
|
+
});
|
|
80
299
|
},
|
|
81
|
-
|
|
300
|
+
searchSuggestions: (query) => fetchSearchSuggestions(query),
|
|
301
|
+
applySearchSuggestion,
|
|
302
|
+
searchAddress,
|
|
82
303
|
capturePngDataUrl: () => {
|
|
83
304
|
const map = mapRef.current;
|
|
84
305
|
if (!map)
|
|
@@ -90,7 +311,10 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
90
311
|
return '';
|
|
91
312
|
}
|
|
92
313
|
},
|
|
93
|
-
|
|
314
|
+
requestRepaint: () => {
|
|
315
|
+
mapRef.current?.triggerRepaint();
|
|
316
|
+
},
|
|
317
|
+
}), [applySceneLighting, applySearchSuggestion, fetchSearchSuggestions, searchAddress]);
|
|
94
318
|
useEffect(() => {
|
|
95
319
|
if (!active)
|
|
96
320
|
return;
|
|
@@ -135,15 +359,6 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
135
359
|
pitch: startPitch,
|
|
136
360
|
});
|
|
137
361
|
map.addControl(new mapboxgl.NavigationControl({ visualizePitch: true }), 'bottom-right');
|
|
138
|
-
const geocoder = new MapboxGeocoder({
|
|
139
|
-
accessToken,
|
|
140
|
-
mapboxgl: mapboxgl,
|
|
141
|
-
marker: false,
|
|
142
|
-
collapsed: false,
|
|
143
|
-
placeholder: 'search address',
|
|
144
|
-
proximity: { longitude: DEFAULT_CENTER[0], latitude: DEFAULT_CENTER[1] },
|
|
145
|
-
});
|
|
146
|
-
map.addControl(geocoder, 'top-left');
|
|
147
362
|
mapRef.current = map;
|
|
148
363
|
const onMapError = (event) => {
|
|
149
364
|
const err = event?.error ?? event;
|
|
@@ -175,13 +390,12 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
175
390
|
if (!mapRef.current)
|
|
176
391
|
return;
|
|
177
392
|
mapRef.current.resize();
|
|
393
|
+
mapRef.current.triggerRepaint();
|
|
178
394
|
if (debug) {
|
|
179
395
|
const mapCanvas = mapRef.current.getCanvas();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
containerRect: container.getBoundingClientRect(),
|
|
184
|
-
});
|
|
396
|
+
const canvasRect = mapCanvas.getBoundingClientRect();
|
|
397
|
+
const containerRect = container.getBoundingClientRect();
|
|
398
|
+
log(`map resize canvas=${mapCanvas.width}x${mapCanvas.height} canvasRect=${fmt(canvasRect.width)}x${fmt(canvasRect.height)} containerRect=${fmt(containerRect.width)}x${fmt(containerRect.height)}`);
|
|
185
399
|
}
|
|
186
400
|
});
|
|
187
401
|
observer.observe(container);
|
|
@@ -191,40 +405,38 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
191
405
|
if (!map.isStyleLoaded())
|
|
192
406
|
return;
|
|
193
407
|
opacityCacheRef.current.clear();
|
|
194
|
-
layerControllerRef.current =
|
|
195
|
-
|
|
408
|
+
layerControllerRef.current = ensureSelectionLayers(map);
|
|
409
|
+
layerControllerRef.current.setConfig(selectionConfigRef.current);
|
|
410
|
+
layerControllerRef.current.setBuildingsVisible(extrusionsVisibleRef.current);
|
|
411
|
+
layerControllerRef.current.setBuildingsColor(extrusionColorRef.current);
|
|
412
|
+
applyCurrentSelectionToController();
|
|
196
413
|
ensureCustomLayer(map);
|
|
197
|
-
applyMapVisibility(map,
|
|
198
|
-
|
|
414
|
+
applyMapVisibility(map, mapVisibilityRef.current, opacityCacheRef);
|
|
415
|
+
bindSelectionHandlers(map);
|
|
199
416
|
});
|
|
200
417
|
map.on('load', () => {
|
|
201
418
|
if (!map.isStyleLoaded())
|
|
202
419
|
return;
|
|
203
|
-
layerControllerRef.current =
|
|
204
|
-
|
|
420
|
+
layerControllerRef.current = ensureSelectionLayers(map);
|
|
421
|
+
layerControllerRef.current.setConfig(selectionConfigRef.current);
|
|
422
|
+
layerControllerRef.current.setBuildingsVisible(extrusionsVisibleRef.current);
|
|
423
|
+
layerControllerRef.current.setBuildingsColor(extrusionColorRef.current);
|
|
424
|
+
applyCurrentSelectionToController();
|
|
205
425
|
ensureCustomLayer(map);
|
|
206
|
-
applyMapVisibility(map,
|
|
426
|
+
applyMapVisibility(map, mapVisibilityRef.current, opacityCacheRef);
|
|
207
427
|
setMapReady(true);
|
|
208
|
-
|
|
428
|
+
bindSelectionHandlers(map);
|
|
209
429
|
requestAnimationFrame(() => {
|
|
210
430
|
map.resize();
|
|
431
|
+
map.triggerRepaint();
|
|
211
432
|
if (debug) {
|
|
212
433
|
const mapCanvas = map.getCanvas();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
containerRect: map.getContainer().getBoundingClientRect(),
|
|
217
|
-
});
|
|
434
|
+
const canvasRect = mapCanvas.getBoundingClientRect();
|
|
435
|
+
const containerRect = map.getContainer().getBoundingClientRect();
|
|
436
|
+
log(`map load canvas=${mapCanvas.width}x${mapCanvas.height} canvasRect=${fmt(canvasRect.width)}x${fmt(canvasRect.height)} containerRect=${fmt(containerRect.width)}x${fmt(containerRect.height)}`);
|
|
218
437
|
}
|
|
219
438
|
});
|
|
220
439
|
});
|
|
221
|
-
geocoder.on('result', (event) => {
|
|
222
|
-
const center = event?.result?.center;
|
|
223
|
-
if (!Array.isArray(center) || center.length < 2)
|
|
224
|
-
return;
|
|
225
|
-
lastGeocodeLabelRef.current = event?.result?.place_name ?? event?.result?.text ?? null;
|
|
226
|
-
map.easeTo({ center: [Number(center[0]), Number(center[1])], zoom: Math.max(map.getZoom(), 17) });
|
|
227
|
-
});
|
|
228
440
|
}
|
|
229
441
|
catch (err) {
|
|
230
442
|
const message = err instanceof Error ? err.message : 'Failed to initialize Mapbox';
|
|
@@ -250,30 +462,40 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
250
462
|
cleanupCanvasEvents?.();
|
|
251
463
|
cleanupMapEvents?.();
|
|
252
464
|
};
|
|
253
|
-
}, [active, accessToken,
|
|
465
|
+
}, [active, accessToken, applyCurrentSelectionToController, mapStyle]);
|
|
254
466
|
useEffect(() => {
|
|
255
467
|
const map = mapRef.current;
|
|
256
468
|
if (!map || !mapReady)
|
|
257
469
|
return;
|
|
258
470
|
applyMapVisibility(map, mapVisibility, opacityCacheRef);
|
|
259
471
|
}, [mapVisibility, mapReady]);
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
if (!mapReady)
|
|
474
|
+
return;
|
|
475
|
+
layerControllerRef.current?.setBuildingsVisible(extrusionsVisible);
|
|
476
|
+
}, [extrusionsVisible, mapReady]);
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
if (!mapReady)
|
|
479
|
+
return;
|
|
480
|
+
layerControllerRef.current?.setBuildingsColor(extrusionColor);
|
|
481
|
+
}, [extrusionColor, mapReady]);
|
|
260
482
|
useEffect(() => {
|
|
261
483
|
const controller = layerControllerRef.current;
|
|
262
484
|
if (!controller)
|
|
263
485
|
return;
|
|
264
486
|
if (candidate?.feature) {
|
|
265
|
-
controller.setSelected(candidate.feature);
|
|
487
|
+
controller.setSelected(candidate.feature, candidate.selection?.sourceKind ?? null);
|
|
266
488
|
if (candidate.selection?.origin) {
|
|
267
489
|
lastOriginRef.current = candidate.selection.origin;
|
|
268
490
|
}
|
|
269
491
|
}
|
|
270
492
|
else if (confirmedFeature) {
|
|
271
|
-
controller.setSelected(confirmedFeature);
|
|
493
|
+
controller.setSelected(confirmedFeature, confirmedSelection?.sourceKind ?? null);
|
|
272
494
|
}
|
|
273
495
|
else {
|
|
274
|
-
controller.setSelected(null);
|
|
496
|
+
controller.setSelected(null, null);
|
|
275
497
|
}
|
|
276
|
-
}, [candidate, confirmedFeature]);
|
|
498
|
+
}, [candidate, confirmedFeature, confirmedSelection?.sourceKind, mapReady]);
|
|
277
499
|
useEffect(() => {
|
|
278
500
|
if (!mapReady)
|
|
279
501
|
return;
|
|
@@ -282,12 +504,7 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
282
504
|
return;
|
|
283
505
|
const selectionOrigin = confirmedSelection?.origin ?? candidate?.selection?.origin ?? null;
|
|
284
506
|
if (debug) {
|
|
285
|
-
log(
|
|
286
|
-
hasLayer: Boolean(threeLayerRef.current),
|
|
287
|
-
selectionOrigin,
|
|
288
|
-
fallbackOrigin: lastOriginRef.current,
|
|
289
|
-
hasGeometry: Boolean(geometry3dm || meshGroup),
|
|
290
|
-
});
|
|
507
|
+
log(`origin sync hasLayer=${threeLayerRef.current ? '1' : '0'} selectionOrigin=${selectionOrigin ? `${fmt(selectionOrigin.lng)},${fmt(selectionOrigin.lat)}` : 'null'} fallbackOrigin=${lastOriginRef.current ? `${fmt(lastOriginRef.current.lng)},${fmt(lastOriginRef.current.lat)}` : 'null'} hasGeometry=${geometry3dm || meshGroup ? '1' : '0'}`);
|
|
291
508
|
}
|
|
292
509
|
if (selectionOrigin) {
|
|
293
510
|
lastOriginRef.current = selectionOrigin;
|
|
@@ -323,6 +540,7 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
323
540
|
else if (geometry3dm && resolveMeshGroup) {
|
|
324
541
|
group = await resolveMeshGroup(geometry3dm);
|
|
325
542
|
}
|
|
543
|
+
activeMeshGroupRef.current = group;
|
|
326
544
|
if (!group || cancelled)
|
|
327
545
|
return;
|
|
328
546
|
applyMaterialOverrides(group, materialSettings);
|
|
@@ -331,6 +549,7 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
331
549
|
void applyGroup();
|
|
332
550
|
return () => {
|
|
333
551
|
cancelled = true;
|
|
552
|
+
activeMeshGroupRef.current = null;
|
|
334
553
|
};
|
|
335
554
|
}, [mapReady, geometry3dm, meshGroup, materialSettings, resolveMeshGroup]);
|
|
336
555
|
const ensureCustomLayer = (map) => {
|
|
@@ -342,9 +561,9 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
342
561
|
onInit: (scene, renderer) => {
|
|
343
562
|
sceneRef.current = scene;
|
|
344
563
|
rendererRef.current = renderer;
|
|
345
|
-
renderer.shadowMap.enabled =
|
|
564
|
+
renderer.shadowMap.enabled = false;
|
|
346
565
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
347
|
-
(
|
|
566
|
+
applySceneLighting(scene, renderer, lightingPresetRef.current);
|
|
348
567
|
},
|
|
349
568
|
});
|
|
350
569
|
setThreeLayerReady(true);
|
|
@@ -380,34 +599,83 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
380
599
|
}
|
|
381
600
|
}
|
|
382
601
|
};
|
|
383
|
-
const
|
|
602
|
+
const bindSelectionHandlers = (map) => {
|
|
384
603
|
if (handlersBoundRef.current)
|
|
385
604
|
return;
|
|
386
605
|
handlersBoundRef.current = true;
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
606
|
+
const hitLayerForConfig = () => {
|
|
607
|
+
const config = selectionConfigRef.current;
|
|
608
|
+
if (!config)
|
|
609
|
+
return null;
|
|
610
|
+
switch (config.sourceKind) {
|
|
611
|
+
case 'building':
|
|
612
|
+
return 'building-footprints-fill';
|
|
613
|
+
case 'waterway':
|
|
614
|
+
return 'waterway-hit-line';
|
|
615
|
+
case 'road':
|
|
616
|
+
case 'rail':
|
|
617
|
+
return 'road-hit-line';
|
|
618
|
+
default:
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const buildCandidateFromFeature = (rawFeature, config) => {
|
|
623
|
+
if (!rawFeature || !config)
|
|
624
|
+
return null;
|
|
402
625
|
const mapboxgl = mapboxRef.current;
|
|
403
626
|
if (!mapboxgl)
|
|
404
|
-
return;
|
|
405
|
-
|
|
627
|
+
return null;
|
|
628
|
+
if (config.geometryKind === 'polyline' &&
|
|
629
|
+
(config.sourceKind === 'waterway' || config.sourceKind === 'road' || config.sourceKind === 'rail')) {
|
|
630
|
+
const line = toLineFeature(rawFeature);
|
|
631
|
+
if (!line?.geometry?.coordinates?.length)
|
|
632
|
+
return null;
|
|
633
|
+
const coords = line.geometry.coordinates;
|
|
634
|
+
const origin = computeCentroid(coords) ?? { lng: map.getCenter().lng, lat: map.getCenter().lat };
|
|
635
|
+
const pointsLocal = toLocalMeters(coords, origin, mapboxgl);
|
|
636
|
+
if (pointsLocal.length < 2)
|
|
637
|
+
return null;
|
|
638
|
+
const selection = {
|
|
639
|
+
sourceKind: config.sourceKind,
|
|
640
|
+
geometryKind: 'polyline',
|
|
641
|
+
closed: config.closed ?? false,
|
|
642
|
+
geojson: buildGeoJsonLineString(pointsLocal),
|
|
643
|
+
featureGeojson: JSON.stringify(line),
|
|
644
|
+
origin,
|
|
645
|
+
label: config.label,
|
|
646
|
+
mapView: {
|
|
647
|
+
center: { lng: map.getCenter().lng, lat: map.getCenter().lat },
|
|
648
|
+
zoom: map.getZoom(),
|
|
649
|
+
bearing: map.getBearing(),
|
|
650
|
+
pitch: map.getPitch(),
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
return {
|
|
654
|
+
selection,
|
|
655
|
+
feature: line,
|
|
656
|
+
pointsLocal,
|
|
657
|
+
stats: computePolylineStats(pointsLocal),
|
|
658
|
+
address: lastGeocodeLabelRef.current ?? '',
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (config.geometryKind !== 'polygon' || config.sourceKind !== 'building')
|
|
662
|
+
return null;
|
|
663
|
+
const polygon = toPolygonFeature(rawFeature);
|
|
664
|
+
if (!polygon?.geometry)
|
|
665
|
+
return null;
|
|
666
|
+
const ring = getOuterRing(polygon.geometry);
|
|
667
|
+
const origin = computeCentroid(ring) ?? { lng: map.getCenter().lng, lat: map.getCenter().lat };
|
|
668
|
+
const pointsLocal = toLocalMeters(ring, origin, mapboxgl);
|
|
406
669
|
if (pointsLocal.length < 3)
|
|
407
|
-
return;
|
|
670
|
+
return null;
|
|
408
671
|
const selection = {
|
|
672
|
+
sourceKind: 'building',
|
|
673
|
+
geometryKind: 'polygon',
|
|
674
|
+
closed: config.closed ?? true,
|
|
409
675
|
geojson: buildGeoJsonPolygon(pointsLocal),
|
|
410
|
-
|
|
676
|
+
featureGeojson: JSON.stringify(polygon),
|
|
677
|
+
origin,
|
|
678
|
+
label: config.label,
|
|
411
679
|
mapView: {
|
|
412
680
|
center: { lng: map.getCenter().lng, lat: map.getCenter().lat },
|
|
413
681
|
zoom: map.getZoom(),
|
|
@@ -415,16 +683,46 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
415
683
|
pitch: map.getPitch(),
|
|
416
684
|
},
|
|
417
685
|
};
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
686
|
+
return {
|
|
687
|
+
selection,
|
|
688
|
+
feature: polygon,
|
|
689
|
+
pointsLocal,
|
|
690
|
+
stats: computeFootprintStats(pointsLocal),
|
|
691
|
+
address: lastGeocodeLabelRef.current ?? '',
|
|
692
|
+
};
|
|
693
|
+
};
|
|
694
|
+
map.on('mousemove', (e) => {
|
|
695
|
+
const config = selectionConfigRef.current;
|
|
696
|
+
const layerId = hitLayerForConfig();
|
|
697
|
+
if (!config || !layerId) {
|
|
698
|
+
layerControllerRef.current?.setHover(null, null);
|
|
699
|
+
map.getCanvas().style.cursor = '';
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const feature = map.queryRenderedFeatures(e.point, { layers: [layerId] })?.[0];
|
|
703
|
+
const candidate = buildCandidateFromFeature(feature, config);
|
|
704
|
+
layerControllerRef.current?.setHover(candidate?.feature ?? null, candidate?.selection.sourceKind ?? null);
|
|
705
|
+
map.getCanvas().style.cursor = candidate ? 'pointer' : '';
|
|
706
|
+
});
|
|
707
|
+
map.on('mouseout', () => {
|
|
708
|
+
layerControllerRef.current?.setHover(null, null);
|
|
709
|
+
map.getCanvas().style.cursor = '';
|
|
422
710
|
});
|
|
423
711
|
map.on('click', (e) => {
|
|
424
|
-
const
|
|
425
|
-
|
|
712
|
+
const config = selectionConfigRef.current;
|
|
713
|
+
const layerId = hitLayerForConfig();
|
|
714
|
+
if (!config || !layerId) {
|
|
426
715
|
onCandidateClearRef.current();
|
|
716
|
+
return;
|
|
427
717
|
}
|
|
718
|
+
const feature = map.queryRenderedFeatures(e.point, { layers: [layerId] })?.[0];
|
|
719
|
+
const candidate = buildCandidateFromFeature(feature, config);
|
|
720
|
+
if (!candidate) {
|
|
721
|
+
onCandidateClearRef.current();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
onCandidateSelectRef.current(candidate);
|
|
725
|
+
void reverseGeocodeAddress(candidate.selection.origin, candidate.selection, candidate.feature, candidate.pointsLocal, candidate.stats, candidate.address ?? '');
|
|
428
726
|
});
|
|
429
727
|
};
|
|
430
728
|
const reverseGeocodeAddress = async (center, selection, feature, pointsLocal, stats, initialAddress) => {
|
|
@@ -454,7 +752,48 @@ export const MapModeViewport = forwardRef(function MapModeViewport({ active, geo
|
|
|
454
752
|
// ignore geocode errors
|
|
455
753
|
}
|
|
456
754
|
};
|
|
457
|
-
return (_jsxs("div", {
|
|
755
|
+
return (_jsxs("div", { style: {
|
|
756
|
+
position: 'absolute',
|
|
757
|
+
inset: 0,
|
|
758
|
+
width: '100%',
|
|
759
|
+
height: '100%',
|
|
760
|
+
}, children: [_jsx("div", { ref: containerRef, style: {
|
|
761
|
+
width: '100%',
|
|
762
|
+
height: '100%',
|
|
763
|
+
} }), !accessToken && (_jsx("div", { style: {
|
|
764
|
+
position: 'absolute',
|
|
765
|
+
inset: 0,
|
|
766
|
+
zIndex: 2,
|
|
767
|
+
display: 'flex',
|
|
768
|
+
alignItems: 'center',
|
|
769
|
+
justifyContent: 'center',
|
|
770
|
+
background: 'rgba(0, 0, 0, 0.7)',
|
|
771
|
+
color: '#ffffff',
|
|
772
|
+
fontSize: 12,
|
|
773
|
+
fontFamily: 'var(--ui-font-family, system-ui, sans-serif)',
|
|
774
|
+
}, children: "Mapbox access token missing." })), activeMapError && (_jsx("div", { style: {
|
|
775
|
+
position: 'absolute',
|
|
776
|
+
inset: 0,
|
|
777
|
+
zIndex: 2,
|
|
778
|
+
display: 'flex',
|
|
779
|
+
alignItems: 'center',
|
|
780
|
+
justifyContent: 'center',
|
|
781
|
+
background: 'rgba(0, 0, 0, 0.7)',
|
|
782
|
+
color: '#ffffff',
|
|
783
|
+
fontSize: 12,
|
|
784
|
+
fontFamily: 'var(--ui-font-family, system-ui, sans-serif)',
|
|
785
|
+
}, children: activeMapError })), !mapReady && !activeMapError && accessToken && (_jsx("div", { style: {
|
|
786
|
+
position: 'absolute',
|
|
787
|
+
inset: 0,
|
|
788
|
+
zIndex: 2,
|
|
789
|
+
display: 'flex',
|
|
790
|
+
alignItems: 'center',
|
|
791
|
+
justifyContent: 'center',
|
|
792
|
+
background: 'rgba(0, 0, 0, 0.6)',
|
|
793
|
+
color: '#ffffff',
|
|
794
|
+
fontSize: 12,
|
|
795
|
+
fontFamily: 'var(--ui-font-family, system-ui, sans-serif)',
|
|
796
|
+
}, children: "loading map\u2026" }))] }));
|
|
458
797
|
});
|
|
459
798
|
const ensureGeometryUvs = (geometry) => {
|
|
460
799
|
if (geometry.getAttribute('uv'))
|
|
@@ -484,7 +823,7 @@ const ensureGeometryUvs = (geometry) => {
|
|
|
484
823
|
function applyMaterialOverrides(group, settings) {
|
|
485
824
|
if (!group || typeof group.traverse !== 'function')
|
|
486
825
|
return;
|
|
487
|
-
const color = new THREE.Color(settings
|
|
826
|
+
const color = settings?.color ? new THREE.Color(settings.color) : null;
|
|
488
827
|
const textureUrl = settings?.textureUrl;
|
|
489
828
|
const repeat = settings?.textureRepeat ?? 2;
|
|
490
829
|
let texture = null;
|
|
@@ -501,48 +840,136 @@ function applyMaterialOverrides(group, settings) {
|
|
|
501
840
|
return;
|
|
502
841
|
const mesh = child;
|
|
503
842
|
mesh.frustumCulled = false;
|
|
504
|
-
if (mesh.material) {
|
|
505
|
-
const material = mesh.material;
|
|
506
|
-
if (Array.isArray(material))
|
|
507
|
-
material.forEach((m) => m?.dispose?.());
|
|
508
|
-
else
|
|
509
|
-
material?.dispose?.();
|
|
510
|
-
}
|
|
511
843
|
if (texture) {
|
|
512
844
|
ensureGeometryUvs(mesh.geometry);
|
|
513
845
|
}
|
|
514
|
-
mesh.material
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
846
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
847
|
+
materials.forEach((material) => {
|
|
848
|
+
if (!material)
|
|
849
|
+
return;
|
|
850
|
+
const anyMaterial = material;
|
|
851
|
+
if (color && anyMaterial.color instanceof THREE.Color) {
|
|
852
|
+
anyMaterial.color.copy(color);
|
|
853
|
+
}
|
|
854
|
+
if (texture && 'map' in anyMaterial) {
|
|
855
|
+
anyMaterial.map = texture;
|
|
856
|
+
}
|
|
857
|
+
if ('side' in anyMaterial) {
|
|
858
|
+
anyMaterial.side = THREE.DoubleSide;
|
|
859
|
+
}
|
|
860
|
+
anyMaterial.needsUpdate = true;
|
|
520
861
|
});
|
|
521
|
-
mesh.material.transparent = false;
|
|
522
|
-
mesh.material.opacity = 1;
|
|
523
|
-
mesh.material.depthWrite = true;
|
|
524
|
-
mesh.material.depthTest = true;
|
|
525
|
-
mesh.material.polygonOffset = true;
|
|
526
|
-
mesh.material.polygonOffsetFactor = 1.0;
|
|
527
|
-
mesh.material.polygonOffsetUnits = 1.0;
|
|
528
862
|
mesh.castShadow = false;
|
|
529
863
|
mesh.receiveShadow = false;
|
|
530
864
|
});
|
|
531
865
|
}
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
|
|
866
|
+
function parseLightingPresetSpec(presetKey) {
|
|
867
|
+
if (!presetKey)
|
|
868
|
+
return null;
|
|
869
|
+
try {
|
|
870
|
+
const parsed = JSON.parse(presetKey);
|
|
871
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
switch (presetKey) {
|
|
875
|
+
case 'dark':
|
|
876
|
+
return {
|
|
877
|
+
renderer: { toneMapping: 'neutral', toneMappingExposure: 0.95, outputColorSpace: 'srgb' },
|
|
878
|
+
lighting: {
|
|
879
|
+
ambient: { color: '#dce4f0', intensity: 0.26 },
|
|
880
|
+
hemisphere: { skyColor: '#e8edf5', groundColor: '#8a8f97', intensity: 0.9 },
|
|
881
|
+
key: { color: '#ffffff', intensity: 0.95, position: [8, 12, 6], castShadow: false },
|
|
882
|
+
fill: { color: '#ffffff', intensity: 0.42, position: [-8, 9, -5] },
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
case 'light':
|
|
886
|
+
return {
|
|
887
|
+
renderer: { toneMapping: 'neutral', toneMappingExposure: 1.08, outputColorSpace: 'srgb' },
|
|
888
|
+
lighting: {
|
|
889
|
+
ambient: { color: '#ffffff', intensity: 0.42 },
|
|
890
|
+
hemisphere: { skyColor: '#f8fbff', groundColor: '#e5e7eb', intensity: 1.1 },
|
|
891
|
+
key: { color: '#ffffff', intensity: 1.15, position: [10, 15, 8], castShadow: false },
|
|
892
|
+
fill: { color: '#ffffff', intensity: 0.5, position: [-10, 10, -6] },
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
default:
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function applyLightingReadabilityBoost(renderer, presetKey) {
|
|
901
|
+
const preset = parseLightingPresetSpec(presetKey);
|
|
902
|
+
if (preset?.renderer?.outputColorSpace) {
|
|
903
|
+
renderer.outputColorSpace =
|
|
904
|
+
preset.renderer.outputColorSpace === 'linear' ? THREE.LinearSRGBColorSpace : THREE.SRGBColorSpace;
|
|
905
|
+
}
|
|
906
|
+
if (preset?.renderer?.toneMapping) {
|
|
907
|
+
renderer.toneMapping = toneMappingLookup[preset.renderer.toneMapping];
|
|
908
|
+
}
|
|
909
|
+
if (typeof preset?.renderer?.toneMappingExposure === 'number') {
|
|
910
|
+
renderer.toneMappingExposure = preset.renderer.toneMappingExposure;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
renderer.toneMappingExposure = 1.02;
|
|
914
|
+
}
|
|
915
|
+
const shadowMapEnabled = preset?.renderer?.shadowMap?.enabled ?? preset?.lighting?.key?.castShadow ?? false;
|
|
916
|
+
renderer.shadowMap.enabled = shadowMapEnabled;
|
|
917
|
+
renderer.shadowMap.type = shadowMapTypeLookup[preset?.renderer?.shadowMap?.type ?? 'pcfsoft'];
|
|
918
|
+
}
|
|
919
|
+
function resolveColor(value, fallback) {
|
|
920
|
+
return typeof value === 'string' || typeof value === 'number' ? value : fallback;
|
|
921
|
+
}
|
|
922
|
+
function configureDirectionalShadow(light, shadow) {
|
|
923
|
+
if (!light.shadow)
|
|
924
|
+
return;
|
|
925
|
+
const cameraSize = shadow?.cameraSize ?? 25;
|
|
926
|
+
light.shadow.camera.left = -cameraSize;
|
|
927
|
+
light.shadow.camera.right = cameraSize;
|
|
928
|
+
light.shadow.camera.top = cameraSize;
|
|
929
|
+
light.shadow.camera.bottom = -cameraSize;
|
|
930
|
+
light.shadow.camera.near = shadow?.near ?? 0.1;
|
|
931
|
+
light.shadow.camera.far = shadow?.far ?? 60;
|
|
932
|
+
const mapSize = shadow?.mapSize ?? 2048;
|
|
933
|
+
light.shadow.mapSize.set(mapSize, mapSize);
|
|
934
|
+
light.shadow.bias = shadow?.bias ?? -0.0001;
|
|
935
|
+
light.shadow.radius = shadow?.radius ?? 3;
|
|
936
|
+
if (typeof light.shadow.camera.updateProjectionMatrix === 'function') {
|
|
937
|
+
light.shadow.camera.updateProjectionMatrix();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function defaultApplyLightingPreset(scene, renderer, presetKey) {
|
|
941
|
+
scene.background = null;
|
|
942
|
+
const lightsToRemove = scene.children.filter((child) => child instanceof THREE.Light || child.userData?.__mapLightTarget === true);
|
|
535
943
|
lightsToRemove.forEach((light) => scene.remove(light));
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const fill =
|
|
542
|
-
|
|
543
|
-
scene.add(
|
|
544
|
-
|
|
545
|
-
|
|
944
|
+
const preset = parseLightingPresetSpec(presetKey);
|
|
945
|
+
applyLightingReadabilityBoost(renderer, presetKey);
|
|
946
|
+
const ambient = preset?.lighting?.ambient;
|
|
947
|
+
const hemi = preset?.lighting?.hemisphere;
|
|
948
|
+
const key = preset?.lighting?.key;
|
|
949
|
+
const fill = preset?.lighting?.fill;
|
|
950
|
+
const lightTarget = preset?.sceneExtras?.lightTarget ?? [0, 1.5, 0];
|
|
951
|
+
scene.add(new THREE.AmbientLight(new THREE.Color(resolveColor(ambient?.color, '#ffffff')), ambient?.intensity ?? 0.22));
|
|
952
|
+
scene.add(new THREE.HemisphereLight(new THREE.Color(resolveColor(hemi?.skyColor ?? hemi?.sky, '#f8fbff')), new THREE.Color(resolveColor(hemi?.groundColor ?? hemi?.ground, '#d4d4d4')), hemi?.intensity ?? 0.85));
|
|
953
|
+
const keyLight = new THREE.DirectionalLight(new THREE.Color(resolveColor(key?.color, '#ffffff')), key?.intensity ?? 0.9);
|
|
954
|
+
keyLight.position.set(...(key?.position ?? [8, 12, 6]));
|
|
955
|
+
keyLight.castShadow = key?.castShadow ?? false;
|
|
956
|
+
if (keyLight.castShadow) {
|
|
957
|
+
configureDirectionalShadow(keyLight, preset?.lighting?.shadow);
|
|
958
|
+
}
|
|
959
|
+
const keyTarget = new THREE.Object3D();
|
|
960
|
+
keyTarget.userData.__mapLightTarget = true;
|
|
961
|
+
keyTarget.position.set(...lightTarget);
|
|
962
|
+
keyLight.target = keyTarget;
|
|
963
|
+
scene.add(keyTarget);
|
|
964
|
+
scene.add(keyLight);
|
|
965
|
+
const fillLight = new THREE.DirectionalLight(new THREE.Color(resolveColor(fill?.color, '#ffffff')), fill?.intensity ?? 0.38);
|
|
966
|
+
fillLight.position.set(...(fill?.position ?? [-6, 8, -6]));
|
|
967
|
+
const fillTarget = new THREE.Object3D();
|
|
968
|
+
fillTarget.userData.__mapLightTarget = true;
|
|
969
|
+
fillTarget.position.set(...lightTarget);
|
|
970
|
+
fillLight.target = fillTarget;
|
|
971
|
+
scene.add(fillTarget);
|
|
972
|
+
scene.add(fillLight);
|
|
546
973
|
}
|
|
547
974
|
function applyMapVisibility(map, mode, cacheRef) {
|
|
548
975
|
if (!map?.getStyle?.())
|
|
@@ -607,18 +1034,58 @@ function applyMapVisibility(map, mode, cacheRef) {
|
|
|
607
1034
|
}
|
|
608
1035
|
}
|
|
609
1036
|
}
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1037
|
+
function resolveGroundFrameMeters(group) {
|
|
1038
|
+
if (!group || typeof group.updateWorldMatrix !== 'function')
|
|
1039
|
+
return null;
|
|
1040
|
+
group.updateWorldMatrix(true, true);
|
|
1041
|
+
const box = new THREE.Box3().setFromObject(group);
|
|
1042
|
+
if (box.isEmpty())
|
|
1043
|
+
return null;
|
|
1044
|
+
const size = box.getSize(new THREE.Vector3());
|
|
1045
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
1046
|
+
return {
|
|
1047
|
+
centerEastMeters: center.x,
|
|
1048
|
+
centerNorthMeters: -center.z,
|
|
1049
|
+
spanEastMeters: Math.max(MIN_VIEW_SPAN_METERS, size.x),
|
|
1050
|
+
spanNorthMeters: Math.max(MIN_VIEW_SPAN_METERS, size.z),
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function resolveSelectionFrameMeters(selection) {
|
|
1054
|
+
if (!selection?.geojson)
|
|
1055
|
+
return null;
|
|
1056
|
+
try {
|
|
1057
|
+
const parsed = JSON.parse(selection.geojson);
|
|
1058
|
+
const points = parsed.type === 'Polygon'
|
|
1059
|
+
? (parsed.coordinates?.[0] ?? [])
|
|
1060
|
+
: (parsed.coordinates ?? []);
|
|
1061
|
+
if (!points.length)
|
|
1062
|
+
return null;
|
|
1063
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
1064
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
1065
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
1066
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
1067
|
+
points.forEach(([x, y]) => {
|
|
1068
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
1069
|
+
return;
|
|
1070
|
+
if (x < minX)
|
|
1071
|
+
minX = x;
|
|
1072
|
+
if (x > maxX)
|
|
1073
|
+
maxX = x;
|
|
1074
|
+
if (y < minY)
|
|
1075
|
+
minY = y;
|
|
1076
|
+
if (y > maxY)
|
|
1077
|
+
maxY = y;
|
|
1078
|
+
});
|
|
1079
|
+
if (![minX, maxX, minY, maxY].every(Number.isFinite))
|
|
1080
|
+
return null;
|
|
1081
|
+
return {
|
|
1082
|
+
centerEastMeters: (minX + maxX) / 2,
|
|
1083
|
+
centerNorthMeters: (minY + maxY) / 2,
|
|
1084
|
+
spanEastMeters: Math.max(MIN_VIEW_SPAN_METERS, maxX - minX),
|
|
1085
|
+
spanNorthMeters: Math.max(MIN_VIEW_SPAN_METERS, maxY - minY),
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
624
1091
|
}
|