@osm-editor-kit/street-imagery-react 0.1.0-alpha.3 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +113 -0
  2. package/dist/MapillaryPanel-N45NQY4C.js +2 -0
  3. package/dist/PanoramaxPanel-MFI4L67Z.js +2 -0
  4. package/dist/StreetLevelImagerySelectionOverlay.d.ts +20 -3
  5. package/dist/StreetLevelImagerySourcesAndLayers.d.ts +14 -1
  6. package/dist/StreetLevelImageryViewCone.d.ts +11 -1
  7. package/dist/StreetLevelImageryViewer.d.ts +15 -1
  8. package/dist/chunk-53RA4OSJ.js +304 -0
  9. package/dist/chunk-EQ2JKLGT.js +311 -0
  10. package/dist/chunk-HA4GJWHI.js +120 -0
  11. package/dist/hooks/useAllProviderMapFeatures.d.ts +0 -4
  12. package/dist/i18n/PhotoDate.d.ts +16 -0
  13. package/dist/i18n/StreetImageryLocaleProvider.d.ts +21 -0
  14. package/dist/i18n/messages.d.ts +35 -0
  15. package/dist/index.d.ts +15 -2
  16. package/dist/index.js +984 -117
  17. package/dist/mapillary/MapillaryFeatureBar.d.ts +13 -0
  18. package/dist/mapillary/useMapillaryFeatures.d.ts +36 -0
  19. package/dist/openers/LocationPickOnMap.d.ts +10 -0
  20. package/dist/openers/useLocationPickStore.d.ts +9 -0
  21. package/dist/panels/MapillaryPanel.d.ts +19 -1
  22. package/dist/panels/PanoramaxPanel.d.ts +8 -1
  23. package/dist/panels/mapillaryLookAt.d.ts +41 -0
  24. package/dist/panels/panoramaxLimitPanning.d.ts +54 -0
  25. package/dist/panels/panoramaxPhotoFromMetadata.d.ts +8 -0
  26. package/dist/photo-details/PhotoDetailsDialog.d.ts +21 -0
  27. package/dist/photo-details/index.d.ts +6 -0
  28. package/dist/photo-details/labels.d.ts +33 -0
  29. package/dist/photo-details.js +235 -0
  30. package/dist/viewpoints/FloatingPhotoViewer.d.ts +54 -0
  31. package/dist/viewpoints/ViewpointLayer.d.ts +17 -0
  32. package/dist/viewpoints/useViewSuggestions.d.ts +23 -0
  33. package/dist/viewpoints/useViewpointSessionStore.d.ts +32 -0
  34. package/dist/viewpoints/viewDirectionLabels.d.ts +4 -0
  35. package/package.json +11 -3
  36. package/dist/MapillaryPanel-QX6NESX7.js +0 -2
  37. package/dist/PanoramaxPanel-J6ANR6LB.js +0 -2
  38. package/dist/chunk-BS6LPDMY.js +0 -143
  39. package/dist/chunk-ZH3TPW4D.js +0 -165
package/dist/index.js CHANGED
@@ -1,66 +1,127 @@
1
- export { MapillaryPanel } from './chunk-BS6LPDMY.js';
2
- export { PanoramaxPanel } from './chunk-ZH3TPW4D.js';
1
+ import { useStreetImageryI18n } from './chunk-HA4GJWHI.js';
2
+ export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n } from './chunk-HA4GJWHI.js';
3
+ export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo } from './chunk-53RA4OSJ.js';
4
+ export { PanoramaxPanel } from './chunk-EQ2JKLGT.js';
3
5
  export { useViewerActions, useViewerBearing, useViewerHfov, useViewerLngLat } from './chunk-25UGGXIE.js';
4
- import { photosToFeatureCollection, emptyPointCollection, sequencesToFeatureCollection, emptyLineCollection, adapterById, viewConeGeoJson, coneRadiusMeters, setStreetImageryConfig, photoLayerId, viewfieldLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, mapFeaturesToFeatureCollection, buildMapFeatureLayerFilter, featureSourceId, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId } from '@osm-editor-kit/street-imagery';
6
+ import { distanceMeters, sequencesToFeatureCollection, emptyLineCollection, adapterById, viewConeGeoJson, viewShapeRadiusMeters, setStreetImageryConfig, viewfieldLayerId, photoLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, formatLocalDateTime, formatMonth, formatRelativeDate, formatDateTooltip, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, fetchImageDetections, findLocationOpener, mapillaryIconUrl, openLocationInNewTab, alignLineToPoints, coneRadiusMeters, mapFeaturesToFeatureCollection, emptyPointCollection, buildMapFeatureLayerFilter, featureSourceId, photosToFeatureCollection, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
5
7
  import { Source, Layer, useMap } from 'react-map-gl/maplibre';
6
8
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
7
- import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState } from 'react';
8
- import { useQuery } from '@tanstack/react-query';
9
+ import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState, useRef, useEffectEvent } from 'react';
10
+ import { useQuery, useQueries } from '@tanstack/react-query';
11
+ import { create } from 'zustand';
9
12
 
10
- var HIGHLIGHT_SOURCE_ID = "selection-highlight";
11
- var HIGHLIGHT_LAYER_ID = "selection-highlight-layer";
13
+ var POINTS_SOURCE_ID = "selection-highlight";
14
+ var CONNECTOR_SOURCE_ID = "selection-connector";
12
15
  var SEQUENCE_HIGHLIGHT_SOURCE_ID = "sequence-highlight";
13
- var SEQUENCE_HIGHLIGHT_LAYER_ID = "sequence-highlight-layer";
16
+ var BASE_COLOR = "#171717";
17
+ var SELECTION_COLOR = "#f97316";
18
+ var SAME_POSITION_METERS = 0.75;
19
+ var ACTIVE_LINE_WIDTH = 2.5;
14
20
  var StreetLevelImagerySelectionOverlay = ({
15
21
  selectedPhoto,
16
- selectedSequence
22
+ selectedSequence,
23
+ cameraLngLat,
24
+ sequenceBeforeId
17
25
  }) => {
18
- const highlightCollection = selectedPhoto ? photosToFeatureCollection([selectedPhoto]) : emptyPointCollection();
19
- const sequenceHighlightCollection = selectedSequence ? sequencesToFeatureCollection([selectedSequence]) : emptyLineCollection();
20
- if (!selectedPhoto && sequenceHighlightCollection.features.length === 0) {
26
+ const photoLngLat = selectedPhoto?.lngLat;
27
+ const cameraDiffers = photoLngLat != null && cameraLngLat != null && distanceMeters(photoLngLat, cameraLngLat) > SAME_POSITION_METERS;
28
+ const camera = cameraDiffers ? cameraLngLat : null;
29
+ const dot = photoLngLat && cameraLngLat && !cameraDiffers ? cameraLngLat : photoLngLat;
30
+ const sequenceLines = selectedSequence ? sequencesToFeatureCollection([selectedSequence]) : emptyLineCollection();
31
+ const throughDot = (line) => dot ? alignLineToPoints(line, [dot]) : line;
32
+ const sequenceCollection = {
33
+ ...sequenceLines,
34
+ features: sequenceLines.features.map((feature) => ({
35
+ ...feature,
36
+ geometry: feature.geometry.type === "LineString" ? { ...feature.geometry, coordinates: throughDot(feature.geometry.coordinates) } : { ...feature.geometry, coordinates: feature.geometry.coordinates.map(throughDot) }
37
+ }))
38
+ };
39
+ if (!selectedPhoto && sequenceCollection.features.length === 0) {
21
40
  return null;
22
41
  }
23
- return /* @__PURE__ */ jsxs(Fragment, { children: [
24
- selectedPhoto ? /* @__PURE__ */ jsxs(Fragment, { children: [
25
- /* @__PURE__ */ jsx(Source, { id: HIGHLIGHT_SOURCE_ID, type: "geojson", data: highlightCollection }),
26
- /* @__PURE__ */ jsx(
27
- Layer,
42
+ const points = {
43
+ type: "FeatureCollection",
44
+ features: [
45
+ ...dot ? [
28
46
  {
29
- id: HIGHLIGHT_LAYER_ID,
30
- type: "circle",
31
- source: HIGHLIGHT_SOURCE_ID,
32
- paint: {
33
- "circle-radius": 10,
34
- "circle-color": "#ffffff",
35
- "circle-stroke-width": 3,
36
- "circle-stroke-color": "#0f172a"
37
- }
47
+ type: "Feature",
48
+ properties: { kind: "dot" },
49
+ geometry: { type: "Point", coordinates: dot }
38
50
  }
39
- )
40
- ] }) : null,
41
- sequenceHighlightCollection.features.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
42
- /* @__PURE__ */ jsx(
43
- Source,
51
+ ] : [],
52
+ ...camera ? [
44
53
  {
45
- id: SEQUENCE_HIGHLIGHT_SOURCE_ID,
46
- type: "geojson",
47
- data: sequenceHighlightCollection
54
+ type: "Feature",
55
+ properties: { kind: "camera" },
56
+ geometry: { type: "Point", coordinates: camera }
48
57
  }
49
- ),
50
- /* @__PURE__ */ jsx(
51
- Layer,
52
- {
53
- id: SEQUENCE_HIGHLIGHT_LAYER_ID,
54
- type: "line",
55
- source: SEQUENCE_HIGHLIGHT_SOURCE_ID,
56
- paint: {
57
- "line-color": "#0f172a",
58
- "line-width": 4,
59
- "line-opacity": 0.75
60
- }
58
+ ] : []
59
+ ]
60
+ };
61
+ const connector = {
62
+ type: "FeatureCollection",
63
+ features: dot && camera ? [
64
+ {
65
+ type: "Feature",
66
+ properties: {},
67
+ geometry: { type: "LineString", coordinates: [dot, camera] }
68
+ }
69
+ ] : []
70
+ };
71
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ /* @__PURE__ */ jsx(Source, { id: SEQUENCE_HIGHLIGHT_SOURCE_ID, type: "geojson", data: sequenceCollection }),
73
+ /* @__PURE__ */ jsx(
74
+ Layer,
75
+ {
76
+ beforeId: sequenceBeforeId,
77
+ id: "sequence-highlight-layer",
78
+ type: "line",
79
+ source: SEQUENCE_HIGHLIGHT_SOURCE_ID,
80
+ layout: { "line-cap": "round", "line-join": "round" },
81
+ paint: { "line-color": BASE_COLOR, "line-width": ACTIVE_LINE_WIDTH }
82
+ }
83
+ ),
84
+ /* @__PURE__ */ jsx(Source, { id: CONNECTOR_SOURCE_ID, type: "geojson", data: connector }),
85
+ /* @__PURE__ */ jsx(
86
+ Layer,
87
+ {
88
+ id: "selection-connector-layer",
89
+ type: "line",
90
+ source: CONNECTOR_SOURCE_ID,
91
+ paint: { "line-color": SELECTION_COLOR, "line-width": 1.5, "line-dasharray": [2, 1.5] }
92
+ }
93
+ ),
94
+ /* @__PURE__ */ jsx(Source, { id: POINTS_SOURCE_ID, type: "geojson", data: points }),
95
+ /* @__PURE__ */ jsx(
96
+ Layer,
97
+ {
98
+ id: "selection-highlight-layer",
99
+ type: "circle",
100
+ source: POINTS_SOURCE_ID,
101
+ filter: ["==", ["get", "kind"], "dot"],
102
+ paint: {
103
+ "circle-radius": 6,
104
+ "circle-color": SELECTION_COLOR,
105
+ "circle-stroke-width": 2,
106
+ "circle-stroke-color": BASE_COLOR
61
107
  }
62
- )
63
- ] }) : null
108
+ }
109
+ ),
110
+ /* @__PURE__ */ jsx(
111
+ Layer,
112
+ {
113
+ id: "selection-camera-layer",
114
+ type: "circle",
115
+ source: POINTS_SOURCE_ID,
116
+ filter: ["==", ["get", "kind"], "camera"],
117
+ paint: {
118
+ "circle-radius": 3.5,
119
+ "circle-color": SELECTION_COLOR,
120
+ "circle-stroke-width": 1.5,
121
+ "circle-stroke-color": BASE_COLOR
122
+ }
123
+ }
124
+ )
64
125
  ] });
65
126
  };
66
127
  var resolveSelectedSequence = (selectedPhoto, sequences, sequenceId) => {
@@ -115,7 +176,8 @@ var useProviderMapFeatures = (providerId, bbox, zoom) => {
115
176
  };
116
177
  var CONE_SOURCE_ID = "view-direction-cone";
117
178
  var CONE_FILL_LAYER_ID = "view-direction-cone-fill";
118
- var CONE_LINE_LAYER_ID = "view-direction-cone-line";
179
+ var MIN_CONE_FOV_DEG = 45;
180
+ var VIEW_SHAPE_FILL_OPACITY = 0.25;
119
181
  var INTERACTIVE_PANO_PROVIDERS = /* @__PURE__ */ new Set([
120
182
  "mapillary",
121
183
  "panoramax",
@@ -127,7 +189,9 @@ var INTERACTIVE_PANO_PROVIDERS = /* @__PURE__ */ new Set([
127
189
  var StreetLevelImageryViewCone = ({
128
190
  selectedPhoto,
129
191
  zoom,
130
- viewerPov
192
+ viewerPov,
193
+ color = SELECTION_COLOR,
194
+ scale = 2.5
131
195
  }) => {
132
196
  const apex = viewerPov?.lngLat ?? selectedPhoto.lngLat;
133
197
  const isPano = selectedPhoto.isPano === true;
@@ -147,7 +211,11 @@ var StreetLevelImageryViewCone = ({
147
211
  if (bearing == null) {
148
212
  return null;
149
213
  }
150
- const coneFeature = viewConeGeoJson(apex, bearing, fov, coneRadiusMeters(zoom));
214
+ fov = Math.max(fov, MIN_CONE_FOV_DEG);
215
+ const coneFeature = {
216
+ ...viewConeGeoJson(apex, bearing, fov, viewShapeRadiusMeters(zoom) * scale),
217
+ properties: { isPano: selectedPhoto.isPano, capturedAt: selectedPhoto.capturedAt }
218
+ };
151
219
  return /* @__PURE__ */ jsxs(Fragment, { children: [
152
220
  /* @__PURE__ */ jsx(Source, { id: CONE_SOURCE_ID, type: "geojson", data: coneFeature }),
153
221
  /* @__PURE__ */ jsx(
@@ -157,37 +225,14 @@ var StreetLevelImageryViewCone = ({
157
225
  type: "fill",
158
226
  source: CONE_SOURCE_ID,
159
227
  paint: {
160
- "fill-color": "#0f172a",
161
- "fill-opacity": 0.15
162
- }
163
- }
164
- ),
165
- /* @__PURE__ */ jsx(
166
- Layer,
167
- {
168
- id: CONE_LINE_LAYER_ID,
169
- type: "line",
170
- source: CONE_SOURCE_ID,
171
- paint: {
172
- "line-color": "#0f172a",
173
- "line-width": 1.5,
174
- "line-opacity": 0.5
228
+ "fill-color": color,
229
+ "fill-opacity": VIEW_SHAPE_FILL_OPACITY,
230
+ "fill-outline-color": "rgba(0, 0, 0, 0)"
175
231
  }
176
232
  }
177
233
  )
178
234
  ] });
179
235
  };
180
- var CIRCLE_RADIUS = [
181
- "interpolate",
182
- ["linear"],
183
- ["zoom"],
184
- 10,
185
- 2,
186
- 14,
187
- 4,
188
- 18,
189
- 6
190
- ];
191
236
  var FEATURE_CIRCLE_RADIUS = [
192
237
  "interpolate",
193
238
  ["linear"],
@@ -199,6 +244,30 @@ var FEATURE_CIRCLE_RADIUS = [
199
244
  18,
200
245
  4
201
246
  ];
247
+ var padBbox = ([west, south, east, north], factor) => {
248
+ const dx = (east - west) * factor;
249
+ const dy = (north - south) * factor;
250
+ return [west - dx, south - dy, east + dx, north + dy];
251
+ };
252
+ var lngLatInBbox = ([lng, lat], [west, south, east, north]) => lng >= west && lng <= east && lat >= south && lat <= north;
253
+ var YEAR_MS = 365.25 * 24 * 60 * 60 * 1e3;
254
+ var LOADED_AT = Date.now();
255
+ var AGE_SHADE = [
256
+ "case",
257
+ ["==", ["get", "capturedAt"], null],
258
+ BASE_COLOR,
259
+ [
260
+ "step",
261
+ ["get", "capturedAt"],
262
+ "#909090",
263
+ LOADED_AT - 3 * YEAR_MS,
264
+ "#686868",
265
+ LOADED_AT - 2 * YEAR_MS,
266
+ "#404040",
267
+ LOADED_AT - YEAR_MS,
268
+ BASE_COLOR
269
+ ]
270
+ ];
202
271
  var PHOTO_SORT_KEY = ["coalesce", ["get", "capturedAt"], 0];
203
272
  var FEATURE_SORT_KEY = ["coalesce", ["get", "lastSeenAt"], 0];
204
273
  var PhotoProviderLayer = ({
@@ -208,14 +277,56 @@ var PhotoProviderLayer = ({
208
277
  filter,
209
278
  showSequences,
210
279
  showViewfields,
211
- photoCircleColor
280
+ photoCircleColor,
281
+ activeSequenceId,
282
+ hasActiveSequence,
283
+ shownPhotoId
212
284
  }) => {
213
285
  const adapter = adapterById[providerId];
214
286
  const { data: photos = [] } = useProviderPhotos(providerId, bbox, zoom);
215
287
  const { data: sequences = [] } = useProviderSequences(providerId, bbox, zoom);
216
288
  const visiblePhotos = zoom >= adapter.minZoom ? photos.filter((photo) => photoMatchesFilters(photo, filter?.photoTypes, filter?.date)) : [];
217
- const photoCollection = zoom >= adapter.minZoom ? photosToFeatureCollection(photos) : emptyPointCollection();
218
- const viewfieldCollection = showViewfields ? photosToViewfieldsFeatureCollection(visiblePhotos, zoom) : emptyPolygonCollection();
289
+ const drawBbox = bbox ? padBbox(bbox, 0.25) : null;
290
+ const photoCollection = zoom >= adapter.minZoom ? photosToFeatureCollection(
291
+ drawBbox ? visiblePhotos.filter((photo) => lngLatInBbox(photo.lngLat, drawBbox)) : visiblePhotos
292
+ ) : emptyPointCollection();
293
+ const viewfieldsActive = showViewfields && zoom >= VIEWFIELD_MIN_ZOOM;
294
+ const viewfieldCollection = viewfieldsActive ? photosToViewfieldsFeatureCollection(visiblePhotos, zoom, { bbox }) : emptyPolygonCollection();
295
+ const inActiveSequence = [
296
+ "==",
297
+ ["get", "sequenceId"],
298
+ activeSequenceId ?? ""
299
+ ];
300
+ const lineOpacity = [
301
+ "case",
302
+ inActiveSequence,
303
+ 1,
304
+ hasActiveSequence ? 0.45 : 1
305
+ ];
306
+ const dotRadius = [
307
+ "interpolate",
308
+ ["linear"],
309
+ ["zoom"],
310
+ 10,
311
+ ["case", inActiveSequence, 2, 1.2],
312
+ 14,
313
+ ["case", inActiveSequence, 4, 2],
314
+ // Up to here there are no view shapes and the dots are 20 % smaller.
315
+ VIEWFIELD_MIN_ZOOM - 0.01,
316
+ ["case", inActiveSequence, 5.5, 2.9],
317
+ VIEWFIELD_MIN_ZOOM,
318
+ ["case", inActiveSequence, 5.5, 3.6],
319
+ 18,
320
+ ["case", inActiveSequence, 6, 4]
321
+ ];
322
+ const lineAndDotColor = viewfieldsActive ? ["case", inActiveSequence, BASE_COLOR, AGE_SHADE] : photoCircleColor;
323
+ const viewShapeOpacity = (full) => [
324
+ "case",
325
+ inActiveSequence,
326
+ full,
327
+ hasActiveSequence ? full * 0.4 : full
328
+ ];
329
+ const notShownPhoto = ["!=", ["get", "photoId"], shownPhotoId ?? ""];
219
330
  const photoFilter = buildPhotoLayerFilter(filter?.photoTypes, filter?.date);
220
331
  const photoSrcId = photoSourceId(providerId);
221
332
  const viewfieldSrcId = viewfieldSourceId(providerId);
@@ -243,7 +354,34 @@ var PhotoProviderLayer = ({
243
354
  };
244
355
  return photoMatchesFilters(asPhoto, filter?.photoTypes, filter?.date);
245
356
  }) : [];
246
- const sequenceCollection = filteredSequences.length > 0 ? sequencesToFeatureCollection(filteredSequences) : emptyLineCollection();
357
+ const photoLngLatsBySequence = /* @__PURE__ */ new Map();
358
+ for (const photo of photos) {
359
+ if (photo.sequenceId == null) continue;
360
+ const list = photoLngLatsBySequence.get(photo.sequenceId);
361
+ if (list) {
362
+ list.push(photo.lngLat);
363
+ } else {
364
+ photoLngLatsBySequence.set(photo.sequenceId, [photo.lngLat]);
365
+ }
366
+ }
367
+ const alignedSequences = filteredSequences.map((sequence) => {
368
+ const points = photoLngLatsBySequence.get(sequence.sequenceId);
369
+ if (!points) return sequence;
370
+ const { geometry } = sequence;
371
+ return {
372
+ ...sequence,
373
+ geometry: geometry.type === "LineString" ? {
374
+ ...geometry,
375
+ coordinates: alignLineToPoints(geometry.coordinates, points)
376
+ } : {
377
+ ...geometry,
378
+ coordinates: geometry.coordinates.map(
379
+ (line) => alignLineToPoints(line, points)
380
+ )
381
+ }
382
+ };
383
+ });
384
+ const sequenceCollection = alignedSequences.length > 0 ? sequencesToFeatureCollection(alignedSequences) : emptyLineCollection();
247
385
  return /* @__PURE__ */ jsxs(Fragment, { children: [
248
386
  adapter.fetchSequences && showSequences ? /* @__PURE__ */ jsxs(Fragment, { children: [
249
387
  /* @__PURE__ */ jsx(
@@ -261,10 +399,15 @@ var PhotoProviderLayer = ({
261
399
  id: sequenceLayerId(providerId),
262
400
  type: "line",
263
401
  source: sequenceSourceId(providerId),
402
+ layout: {
403
+ "line-cap": "round",
404
+ "line-join": "round",
405
+ "line-sort-key": ["case", inActiveSequence, 1, 0]
406
+ },
264
407
  paint: {
265
- "line-color": adapter.color,
266
- "line-width": 2,
267
- "line-opacity": 0.35
408
+ "line-color": lineAndDotColor,
409
+ "line-width": ["case", inActiveSequence, ACTIVE_LINE_WIDTH, 1.25],
410
+ "line-opacity": lineOpacity
268
411
  }
269
412
  }
270
413
  )
@@ -285,9 +428,11 @@ var PhotoProviderLayer = ({
285
428
  id: viewfieldLayerId(providerId),
286
429
  type: "fill",
287
430
  source: viewfieldSrcId,
431
+ filter: notShownPhoto,
288
432
  paint: {
289
433
  "fill-color": photoCircleColor,
290
- "fill-opacity": 0.22
434
+ "fill-opacity": viewShapeOpacity(VIEW_SHAPE_FILL_OPACITY),
435
+ "fill-outline-color": "rgba(0, 0, 0, 0)"
291
436
  }
292
437
  }
293
438
  ),
@@ -297,10 +442,11 @@ var PhotoProviderLayer = ({
297
442
  id: viewfieldLineLayerId(providerId),
298
443
  type: "line",
299
444
  source: viewfieldSrcId,
445
+ filter: notShownPhoto,
300
446
  paint: {
301
447
  "line-color": photoCircleColor,
302
- "line-width": 1,
303
- "line-opacity": 0.45
448
+ "line-width": 0.5,
449
+ "line-opacity": viewShapeOpacity(0.3)
304
450
  }
305
451
  }
306
452
  )
@@ -324,10 +470,8 @@ var PhotoProviderLayer = ({
324
470
  filter: photoFilter,
325
471
  layout: { "circle-sort-key": PHOTO_SORT_KEY },
326
472
  paint: {
327
- "circle-radius": CIRCLE_RADIUS,
328
- "circle-color": photoCircleColor,
329
- "circle-stroke-width": 1,
330
- "circle-stroke-color": "#ffffff"
473
+ "circle-radius": dotRadius,
474
+ "circle-color": lineAndDotColor
331
475
  }
332
476
  }
333
477
  )
@@ -342,7 +486,10 @@ var MapFeatureProviderLayer = ({
342
486
  }) => {
343
487
  const adapter = adapterById[providerId];
344
488
  const { data: features = [] } = useProviderMapFeatures(providerId, bbox, zoom);
345
- const featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(features) : emptyPointCollection();
489
+ const valueFilter = filter?.mapFeatureValue;
490
+ const featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(
491
+ valueFilter ? features.filter((feature) => valueFilter(feature.value)) : features
492
+ ) : emptyPointCollection();
346
493
  const featureFilter = buildMapFeatureLayerFilter(filter?.date);
347
494
  const featureSrcId = featureSourceId(providerId);
348
495
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -366,9 +513,7 @@ var MapFeatureProviderLayer = ({
366
513
  layout: { "circle-sort-key": FEATURE_SORT_KEY },
367
514
  paint: {
368
515
  "circle-radius": FEATURE_CIRCLE_RADIUS,
369
- "circle-color": mapFeatureCircleColor,
370
- "circle-stroke-width": 1,
371
- "circle-stroke-color": "#ffffff"
516
+ "circle-color": mapFeatureCircleColor
372
517
  }
373
518
  }
374
519
  )
@@ -394,6 +539,8 @@ var StreetLevelImagerySourcesAndLayers = ({
394
539
  selectedPhoto,
395
540
  selectedSequenceId,
396
541
  viewerPov,
542
+ viewConeColor,
543
+ viewConeScale,
397
544
  photoCircleColor,
398
545
  mapFeatureCircleColor
399
546
  } = options;
@@ -411,11 +558,13 @@ var StreetLevelImagerySourcesAndLayers = ({
411
558
  showSelectionHighlight && selectedProviderId ? bbox : null,
412
559
  zoom
413
560
  );
414
- const selectedSequence = resolveSelectedSequence(
415
- selectedPhoto,
416
- sequences,
417
- selectedSequenceId ?? selectedPhoto?.sequenceId
561
+ const activeSequenceId = selectedSequenceId ?? selectedPhoto?.sequenceId;
562
+ const selectedSequence = resolveSelectedSequence(selectedPhoto, sequences, activeSequenceId);
563
+ const providerDrawsSequences = selectedProviderId != null && showSequences && providers.includes(selectedProviderId);
564
+ const firstPhotoProvider = providers.find(
565
+ (id) => adapterById[id].kind === "photo" && adapterById[id].fetchPhotos
418
566
  );
567
+ const sequenceBeforeId = firstPhotoProvider ? showViewfields ? viewfieldLayerId(firstPhotoProvider) : photoLayerId(firstPhotoProvider) : void 0;
419
568
  return /* @__PURE__ */ jsxs(Fragment, { children: [
420
569
  providers.map((providerId) => /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(
421
570
  ProviderLayer,
@@ -423,6 +572,9 @@ var StreetLevelImagerySourcesAndLayers = ({
423
572
  bbox,
424
573
  filter,
425
574
  mapFeatureCircleColor,
575
+ activeSequenceId: providerId === selectedProviderId ? activeSequenceId : null,
576
+ hasActiveSequence: activeSequenceId != null,
577
+ shownPhotoId: showViewCone && providerId === selectedProviderId ? selectedPhoto?.photoId : null,
426
578
  photoCircleColor,
427
579
  providerId,
428
580
  showSequences,
@@ -433,6 +585,8 @@ var StreetLevelImagerySourcesAndLayers = ({
433
585
  showViewCone && selectedPhoto ? /* @__PURE__ */ jsx(
434
586
  StreetLevelImageryViewCone,
435
587
  {
588
+ color: viewConeColor ?? SELECTION_COLOR,
589
+ scale: viewConeScale,
436
590
  selectedPhoto,
437
591
  viewerPov,
438
592
  zoom
@@ -441,36 +595,51 @@ var StreetLevelImagerySourcesAndLayers = ({
441
595
  showSelectionHighlight ? /* @__PURE__ */ jsx(
442
596
  StreetLevelImagerySelectionOverlay,
443
597
  {
598
+ cameraLngLat: viewerPov?.lngLat,
444
599
  selectedPhoto,
445
- selectedSequence
600
+ selectedSequence: providerDrawsSequences ? null : selectedSequence,
601
+ sequenceBeforeId
446
602
  }
447
603
  ) : null
448
604
  ] });
449
605
  };
450
606
  var MapillaryPanel2 = lazy(
451
- () => import('./MapillaryPanel-QX6NESX7.js').then((module) => ({
607
+ () => import('./MapillaryPanel-N45NQY4C.js').then((module) => ({
452
608
  default: module.MapillaryPanel
453
609
  }))
454
610
  );
455
611
  var PanoramaxPanel2 = lazy(
456
- () => import('./PanoramaxPanel-J6ANR6LB.js').then((module) => ({
612
+ () => import('./PanoramaxPanel-MFI4L67Z.js').then((module) => ({
457
613
  default: module.PanoramaxPanel
458
614
  }))
459
615
  );
460
- var ViewerPanelPlaceholder = () => /* @__PURE__ */ jsx("div", { className: "flex min-h-48 animate-pulse items-center justify-center rounded-lg border border-slate-200 bg-slate-100", children: /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: "Loading viewer\u2026" }) });
616
+ var ViewerPanelPlaceholder = () => {
617
+ const { messages } = useStreetImageryI18n();
618
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-48 animate-pulse items-center justify-center rounded-lg border border-slate-200 bg-slate-100", children: /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: messages.viewer.loading }) });
619
+ };
461
620
  var StreetLevelImageryViewer = ({
462
621
  photo,
463
622
  groupPhotos,
464
623
  onPhotoSelected,
465
- onEaseMapToPoint
624
+ onEaseMapToPoint,
625
+ onViewerPhoto,
626
+ lookAtBearing,
627
+ lookAt,
628
+ outlines,
629
+ hideAttribution
466
630
  }) => {
467
631
  if (photo.providerId === "mapillary") {
468
632
  return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewerPanelPlaceholder, {}), children: /* @__PURE__ */ jsx(
469
633
  MapillaryPanel2,
470
634
  {
471
635
  groupPhotos,
636
+ hideAttribution,
472
637
  onEaseMapToPoint,
473
638
  onPhotoSelected,
639
+ lookAt,
640
+ lookAtBearing,
641
+ outlines,
642
+ onViewerPhoto,
474
643
  photo
475
644
  }
476
645
  ) });
@@ -480,6 +649,8 @@ var StreetLevelImageryViewer = ({
480
649
  PanoramaxPanel2,
481
650
  {
482
651
  groupPhotos,
652
+ hideLegend: hideAttribution,
653
+ onViewerPhoto,
483
654
  onEaseMapToPoint,
484
655
  onPhotoSelected,
485
656
  photo
@@ -567,18 +738,6 @@ var useAllProviderMapFeatures = (providerIds, bbox, zoom, date) => {
567
738
  return data.filter((feature) => mapFeatureMatchesDateRange(feature, date));
568
739
  });
569
740
  };
570
- var useAllProviderMapFeaturesLoading = (providerIds, bbox, zoom) => {
571
- const enabled = new Set(providerIds);
572
- const queries = PROVIDER_IDS.map((providerId) => ({
573
- providerId,
574
- query: useProviderMapFeaturesMaybe(providerId, enabled.has(providerId), bbox, zoom)
575
- }));
576
- const activeQueries = queries.filter(({ providerId }) => enabled.has(providerId));
577
- return {
578
- isLoading: activeQueries.some(({ query }) => query.isLoading),
579
- isFetching: activeQueries.some(({ query }) => query.isFetching)
580
- };
581
- };
582
741
  var useProviderPhotosMaybe = (providerId, enabled, bbox, zoom) => useProviderPhotos(providerId, enabled ? bbox : null, zoom);
583
742
  var useAllProviderPhotos = (providerIds, bbox, zoom, photoTypes, date) => {
584
743
  const enabled = new Set(providerIds);
@@ -655,5 +814,713 @@ var usePhotoThumbnail = (photo) => useQuery({
655
814
  enabled: photo != null,
656
815
  staleTime: 24 * 60 * 60 * 1e3
657
816
  });
817
+ var PhotoDate = ({ timestamp, className, localDateTime }) => {
818
+ const { locale, messages } = useStreetImageryI18n();
819
+ if (timestamp == null) {
820
+ return /* @__PURE__ */ jsx("span", { className, children: messages.unknownDate });
821
+ }
822
+ const precise = localDateTime ? formatLocalDateTime(localDateTime, locale) : null;
823
+ return /* @__PURE__ */ jsx(
824
+ "time",
825
+ {
826
+ className,
827
+ dateTime: new Date(timestamp).toISOString(),
828
+ title: precise ? `${precise}
829
+ ${formatRelativeDate(timestamp, locale)}` : formatDateTooltip(timestamp, locale),
830
+ children: formatMonth(timestamp, locale)
831
+ }
832
+ );
833
+ };
834
+
835
+ // src/viewpoints/viewDirectionLabels.ts
836
+ var viewSuggestionLabel = ({ viewpoint, direction }, messages) => messages.viewDirection(viewpoint.label ?? messages.viewpointRole[viewpoint.role], direction.kind);
837
+ var DEFAULT_LAYOUT = { right: 16, bottom: 32, width: 420, minimized: false };
838
+ var MIN_WIDTH = 280;
839
+ var EDGE = 8;
840
+ var readLayout = (storageKey) => {
841
+ try {
842
+ const raw = localStorage.getItem(storageKey);
843
+ return raw ? { ...DEFAULT_LAYOUT, ...JSON.parse(raw) } : DEFAULT_LAYOUT;
844
+ } catch {
845
+ return DEFAULT_LAYOUT;
846
+ }
847
+ };
848
+ var writeLayout = (storageKey, layout) => {
849
+ try {
850
+ localStorage.setItem(storageKey, JSON.stringify(layout));
851
+ } catch {
852
+ }
853
+ };
854
+ var Icon = ({ path, className = "size-4" }) => /* @__PURE__ */ jsx(
855
+ "svg",
856
+ {
857
+ "aria-hidden": true,
858
+ className,
859
+ fill: "none",
860
+ stroke: "currentColor",
861
+ strokeLinecap: "round",
862
+ strokeLinejoin: "round",
863
+ strokeWidth: 1.75,
864
+ viewBox: "0 0 24 24",
865
+ children: /* @__PURE__ */ jsx("path", { d: path })
866
+ }
867
+ );
868
+ var ICONS = {
869
+ back: "M15 18l-6-6 6-6",
870
+ undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11",
871
+ redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13",
872
+ forward: "M9 18l6-6-6-6",
873
+ close: "M18 6 6 18M6 6l12 12",
874
+ minimize: "M5 12h14",
875
+ expand: "M4 14v6h6M20 10V4h-6M14 10l6-6M10 14l-6 6",
876
+ arrow: "M12 20V5M6 11l6-6 6 6",
877
+ info: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 11v5M12 8h.01"
878
+ };
879
+ var ToolbarButton = ({
880
+ label,
881
+ onClick,
882
+ disabled,
883
+ children
884
+ }) => /* @__PURE__ */ jsx(
885
+ "button",
886
+ {
887
+ "aria-label": label,
888
+ className: "inline-flex size-7 items-center justify-center rounded-md text-slate-600 hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent",
889
+ disabled,
890
+ onClick,
891
+ title: label,
892
+ type: "button",
893
+ children
894
+ }
895
+ );
896
+ var SuggestionChip = ({
897
+ suggestion,
898
+ active,
899
+ onSelect
900
+ }) => {
901
+ const { messages } = useStreetImageryI18n();
902
+ const best = suggestion.candidates[0];
903
+ const label = viewSuggestionLabel(suggestion, messages);
904
+ return /* @__PURE__ */ jsxs(
905
+ "button",
906
+ {
907
+ "aria-label": best ? label : messages.viewer.noPhoto(label),
908
+ "aria-pressed": active,
909
+ className: [
910
+ "inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs whitespace-nowrap",
911
+ active ? "border-fuchsia-600 bg-fuchsia-600 text-white" : best ? "border-slate-300 bg-white text-slate-700 hover:border-fuchsia-500" : "border-dashed border-slate-200 bg-white text-slate-400"
912
+ ].join(" "),
913
+ disabled: !best,
914
+ onClick: onSelect,
915
+ title: best ? label : messages.viewer.noMatchingPhoto(label),
916
+ type: "button",
917
+ children: [
918
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: suggestion.viewpoint.label ?? messages.viewpointRole[suggestion.viewpoint.role] }),
919
+ /* @__PURE__ */ jsx(
920
+ "span",
921
+ {
922
+ "aria-hidden": true,
923
+ className: "inline-flex",
924
+ style: { transform: `rotate(${suggestion.direction.bearing}deg)` },
925
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-3", path: ICONS.arrow })
926
+ }
927
+ ),
928
+ best ? /* @__PURE__ */ jsx(
929
+ PhotoDate,
930
+ {
931
+ className: active ? "text-fuchsia-100" : "text-slate-400",
932
+ timestamp: best.photo.capturedAt
933
+ }
934
+ ) : /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "\u2014" })
935
+ ]
936
+ }
937
+ );
938
+ };
939
+ var FloatingViewerInfoButton = ({
940
+ label,
941
+ onClick
942
+ }) => /* @__PURE__ */ jsx(ToolbarButton, { label, onClick, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.info }) });
943
+ var FloatingPhotoViewer = ({
944
+ title,
945
+ titleActions,
946
+ suggestions,
947
+ activeDirectionKey,
948
+ onSelectSuggestion,
949
+ canGoBack,
950
+ canGoForward,
951
+ onBack,
952
+ onForward,
953
+ onClose,
954
+ step,
955
+ toolbar,
956
+ status,
957
+ children,
958
+ footer,
959
+ drawer,
960
+ storageKey = "street-imagery:floating-viewer"
961
+ }) => {
962
+ const { messages } = useStreetImageryI18n();
963
+ const [layout, setLayout] = useState(() => readLayout(storageKey));
964
+ const [drawerOpen, setDrawerOpen] = useState(false);
965
+ const dragRef = useRef(null);
966
+ const updateLayout = (next) => {
967
+ setLayout(next);
968
+ writeLayout(storageKey, next);
969
+ };
970
+ const onShortcut = useEffectEvent((event) => {
971
+ const target = event.target;
972
+ if (target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"]')) {
973
+ return;
974
+ }
975
+ if (event.key === "Escape") {
976
+ onClose();
977
+ } else if (event.key === "[" && canGoBack) {
978
+ onBack();
979
+ } else if (event.key === "]" && canGoForward) {
980
+ onForward();
981
+ } else if (event.key === "ArrowLeft" && event.altKey) {
982
+ step?.onPrevious?.();
983
+ } else if (event.key === "ArrowRight" && event.altKey) {
984
+ step?.onNext?.();
985
+ }
986
+ });
987
+ useEffect(function subscribeKeyboardShortcuts() {
988
+ window.addEventListener("keydown", onShortcut);
989
+ return () => window.removeEventListener("keydown", onShortcut);
990
+ }, []);
991
+ const startDrag = (event, mode) => {
992
+ if (event.button !== 0 || event.target.closest("button")) {
993
+ return;
994
+ }
995
+ event.currentTarget.setPointerCapture(event.pointerId);
996
+ dragRef.current = { x: event.clientX, y: event.clientY, layout, mode };
997
+ };
998
+ const onDrag = (event) => {
999
+ const drag = dragRef.current;
1000
+ if (!drag) {
1001
+ return;
1002
+ }
1003
+ const dx = event.clientX - drag.x;
1004
+ const dy = event.clientY - drag.y;
1005
+ setLayout(
1006
+ drag.mode === "move" ? {
1007
+ ...drag.layout,
1008
+ right: Math.max(EDGE, drag.layout.right - dx),
1009
+ bottom: Math.max(EDGE, drag.layout.bottom - dy)
1010
+ } : { ...drag.layout, width: Math.max(MIN_WIDTH, drag.layout.width - dx) }
1011
+ );
1012
+ };
1013
+ const endDrag = () => {
1014
+ if (dragRef.current) {
1015
+ dragRef.current = null;
1016
+ writeLayout(storageKey, layout);
1017
+ }
1018
+ };
1019
+ const dragHandlers = { onPointerMove: onDrag, onPointerUp: endDrag, onPointerCancel: endDrag };
1020
+ if (layout.minimized) {
1021
+ return /* @__PURE__ */ jsxs(
1022
+ "button",
1023
+ {
1024
+ className: "absolute z-10 inline-flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-sm font-medium text-slate-800 shadow-lg ring-1 ring-slate-200 hover:bg-slate-50",
1025
+ onClick: () => updateLayout({ ...layout, minimized: false }),
1026
+ style: { right: layout.right, bottom: layout.bottom },
1027
+ type: "button",
1028
+ children: [
1029
+ /* @__PURE__ */ jsx(Icon, { path: ICONS.expand }),
1030
+ title
1031
+ ]
1032
+ }
1033
+ );
1034
+ }
1035
+ return /* @__PURE__ */ jsxs(
1036
+ "section",
1037
+ {
1038
+ "aria-label": messages.viewer.regionLabel,
1039
+ className: "absolute z-10 flex max-h-[calc(100%-1rem)] flex-col overflow-hidden rounded-xl bg-white shadow-xl ring-1 ring-slate-200",
1040
+ style: {
1041
+ right: layout.right,
1042
+ bottom: layout.bottom,
1043
+ width: `min(${layout.width}px, calc(100% - 1rem))`
1044
+ },
1045
+ children: [
1046
+ /* @__PURE__ */ jsx(
1047
+ "div",
1048
+ {
1049
+ "aria-hidden": true,
1050
+ className: "absolute inset-y-0 left-0 z-10 w-1.5 cursor-ew-resize hover:bg-fuchsia-500/20",
1051
+ onPointerDown: (event) => startDrag(event, "resize"),
1052
+ ...dragHandlers
1053
+ }
1054
+ ),
1055
+ /* @__PURE__ */ jsxs(
1056
+ "header",
1057
+ {
1058
+ className: "flex cursor-move touch-none items-center gap-1 border-b border-slate-100 px-2 py-1.5 select-none",
1059
+ onPointerDown: (event) => startDrag(event, "move"),
1060
+ ...dragHandlers,
1061
+ children: [
1062
+ step ? /* @__PURE__ */ jsxs(Fragment, { children: [
1063
+ /* @__PURE__ */ jsx(
1064
+ ToolbarButton,
1065
+ {
1066
+ disabled: !step.onPrevious,
1067
+ label: step.previousLabel,
1068
+ onClick: () => step.onPrevious?.(),
1069
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.back })
1070
+ }
1071
+ ),
1072
+ /* @__PURE__ */ jsx(
1073
+ ToolbarButton,
1074
+ {
1075
+ disabled: !step.onNext,
1076
+ label: step.nextLabel,
1077
+ onClick: () => step.onNext?.(),
1078
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.forward })
1079
+ }
1080
+ )
1081
+ ] }) : null,
1082
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-0.5", children: [
1083
+ /* @__PURE__ */ jsx("h2", { className: "min-w-0 truncate px-1 text-sm font-semibold text-slate-800", children: title }),
1084
+ titleActions
1085
+ ] }),
1086
+ /* @__PURE__ */ jsx(ToolbarButton, { disabled: !canGoBack, label: messages.viewer.backInHistory, onClick: onBack, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.undo }) }),
1087
+ /* @__PURE__ */ jsx(
1088
+ ToolbarButton,
1089
+ {
1090
+ disabled: !canGoForward,
1091
+ label: messages.viewer.forwardInHistory,
1092
+ onClick: onForward,
1093
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.redo })
1094
+ }
1095
+ ),
1096
+ /* @__PURE__ */ jsx(
1097
+ ToolbarButton,
1098
+ {
1099
+ label: messages.viewer.minimize,
1100
+ onClick: () => updateLayout({ ...layout, minimized: true }),
1101
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.minimize })
1102
+ }
1103
+ ),
1104
+ /* @__PURE__ */ jsx(ToolbarButton, { label: messages.viewer.close, onClick: onClose, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.close }) })
1105
+ ]
1106
+ }
1107
+ ),
1108
+ toolbar ? /* @__PURE__ */ jsx("div", { className: "border-b border-slate-100 px-2 py-1.5", children: toolbar }) : suggestions.length > 0 ? /* @__PURE__ */ jsxs(
1109
+ "nav",
1110
+ {
1111
+ "aria-label": messages.viewer.suggestedViews,
1112
+ className: "flex gap-1 overflow-x-auto border-b border-slate-100 px-2 py-1.5",
1113
+ title: messages.viewer.suggestedViewsHint,
1114
+ children: [
1115
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 self-center pr-0.5 text-xs text-slate-500", children: messages.viewer.views }),
1116
+ suggestions.map((suggestion) => /* @__PURE__ */ jsx(
1117
+ SuggestionChip,
1118
+ {
1119
+ active: suggestion.direction.key === activeDirectionKey,
1120
+ onSelect: () => onSelectSuggestion(suggestion),
1121
+ suggestion
1122
+ },
1123
+ suggestion.direction.key
1124
+ ))
1125
+ ]
1126
+ }
1127
+ ) : null,
1128
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 overflow-y-auto", children: [
1129
+ status ? /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-xs text-slate-500", role: "status", children: status }) : null,
1130
+ children,
1131
+ footer ? /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-xs text-slate-600", children: footer }) : null,
1132
+ drawer ? /* @__PURE__ */ jsxs("div", { className: "border-t border-slate-100", children: [
1133
+ /* @__PURE__ */ jsxs(
1134
+ "button",
1135
+ {
1136
+ "aria-expanded": drawerOpen,
1137
+ className: "flex w-full items-center justify-between px-3 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50",
1138
+ onClick: () => setDrawerOpen((open) => !open),
1139
+ type: "button",
1140
+ children: [
1141
+ drawer.label,
1142
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: drawerOpen ? "inline-flex rotate-90" : "inline-flex", children: /* @__PURE__ */ jsx(Icon, { className: "size-3.5", path: ICONS.forward }) })
1143
+ ]
1144
+ }
1145
+ ),
1146
+ drawerOpen ? /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: drawer.content }) : null
1147
+ ] }) : null
1148
+ ] })
1149
+ ]
1150
+ }
1151
+ );
1152
+ };
1153
+ var SOURCE_DIRECTIONS = "viewpoint-directions";
1154
+ var SOURCE_POINTS = "viewpoint-points";
1155
+ var SOURCE_LINE = "viewpoint-line";
1156
+ var VIEWPOINT_DIRECTION_LAYER_ID = "viewpoint-direction-fill";
1157
+ var VIEWPOINT_DEFAULT_COLOR = "#c026d3";
1158
+ var directionLengthMeters = (zoom) => Math.max(8, coneRadiusMeters(zoom) * 3);
1159
+ var ViewpointLayer = ({
1160
+ viewpoints,
1161
+ suggestions,
1162
+ activeDirectionKey,
1163
+ zoom,
1164
+ line,
1165
+ color = VIEWPOINT_DEFAULT_COLOR
1166
+ }) => {
1167
+ const length = directionLengthMeters(zoom);
1168
+ const directions = {
1169
+ type: "FeatureCollection",
1170
+ features: suggestions.map(({ viewpoint, direction, candidates }) => ({
1171
+ ...viewConeGeoJson(viewpoint.lngLat, direction.bearing, 40, length),
1172
+ properties: {
1173
+ key: direction.key,
1174
+ active: direction.key === activeDirectionKey,
1175
+ hasPhoto: candidates.length > 0
1176
+ }
1177
+ }))
1178
+ };
1179
+ const points = {
1180
+ type: "FeatureCollection",
1181
+ features: viewpoints.map(
1182
+ (viewpoint) => ({
1183
+ type: "Feature",
1184
+ properties: { id: viewpoint.id },
1185
+ geometry: { type: "Point", coordinates: viewpoint.lngLat }
1186
+ })
1187
+ )
1188
+ };
1189
+ const lineFeature = {
1190
+ type: "FeatureCollection",
1191
+ features: line && line.length > 1 ? [{ type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: line } }] : []
1192
+ };
1193
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1194
+ /* @__PURE__ */ jsx(Source, { id: SOURCE_LINE, type: "geojson", data: lineFeature }),
1195
+ /* @__PURE__ */ jsx(
1196
+ Layer,
1197
+ {
1198
+ id: "viewpoint-line",
1199
+ type: "line",
1200
+ source: SOURCE_LINE,
1201
+ layout: { "line-cap": "round", "line-join": "round" },
1202
+ paint: { "line-color": color, "line-width": 5, "line-opacity": 0.5 }
1203
+ }
1204
+ ),
1205
+ /* @__PURE__ */ jsx(Source, { id: SOURCE_DIRECTIONS, type: "geojson", data: directions }),
1206
+ /* @__PURE__ */ jsx(
1207
+ Layer,
1208
+ {
1209
+ id: VIEWPOINT_DIRECTION_LAYER_ID,
1210
+ type: "fill",
1211
+ source: SOURCE_DIRECTIONS,
1212
+ paint: {
1213
+ "fill-color": color,
1214
+ "fill-opacity": ["case", ["get", "active"], 0.55, ["get", "hasPhoto"], 0.18, 0.04]
1215
+ }
1216
+ }
1217
+ ),
1218
+ /* @__PURE__ */ jsx(
1219
+ Layer,
1220
+ {
1221
+ id: "viewpoint-direction-line",
1222
+ type: "line",
1223
+ source: SOURCE_DIRECTIONS,
1224
+ paint: {
1225
+ "line-color": color,
1226
+ "line-width": ["case", ["get", "active"], 2, 1],
1227
+ "line-dasharray": [2, 1.5],
1228
+ "line-opacity": ["case", ["get", "hasPhoto"], 0.9, 0.35]
1229
+ }
1230
+ }
1231
+ ),
1232
+ /* @__PURE__ */ jsx(Source, { id: SOURCE_POINTS, type: "geojson", data: points }),
1233
+ /* @__PURE__ */ jsx(
1234
+ Layer,
1235
+ {
1236
+ id: "viewpoint-point",
1237
+ type: "circle",
1238
+ source: SOURCE_POINTS,
1239
+ paint: {
1240
+ "circle-radius": 5,
1241
+ "circle-color": "#ffffff",
1242
+ "circle-stroke-color": color,
1243
+ "circle-stroke-width": 2.5
1244
+ }
1245
+ }
1246
+ )
1247
+ ] });
1248
+ };
1249
+ var viewDirectionKeyFromFeatures = (features) => {
1250
+ const hit = features?.find((feature) => feature.layer?.id === VIEWPOINT_DIRECTION_LAYER_ID);
1251
+ const key = hit?.properties?.key;
1252
+ return typeof key === "string" ? key : null;
1253
+ };
1254
+ var samePhoto = (a, b) => a != null && a.providerId === b.providerId && a.photoId === b.photoId;
1255
+ var useViewpointSessionStore = create()((set, get) => ({
1256
+ sessionId: 0,
1257
+ viewpoints: [],
1258
+ line: null,
1259
+ activeDirectionKey: null,
1260
+ history: [],
1261
+ historyIndex: -1,
1262
+ actions: {
1263
+ open: ({ viewpoints, line = null }) => set((state) => ({
1264
+ sessionId: state.sessionId + 1,
1265
+ viewpoints,
1266
+ line,
1267
+ activeDirectionKey: null
1268
+ })),
1269
+ showPhoto: (entry) => set((state) => {
1270
+ const current = state.history[state.historyIndex];
1271
+ if (samePhoto(current?.photo, entry.photo)) {
1272
+ return entry.directionKey != null && entry.directionKey !== state.activeDirectionKey ? { activeDirectionKey: entry.directionKey } : state;
1273
+ }
1274
+ const history = [...state.history.slice(0, state.historyIndex + 1), entry].slice(
1275
+ -100
1276
+ );
1277
+ return {
1278
+ history,
1279
+ historyIndex: history.length - 1,
1280
+ activeDirectionKey: entry.directionKey ?? state.activeDirectionKey
1281
+ };
1282
+ }),
1283
+ back: () => {
1284
+ const { history, historyIndex } = get();
1285
+ const entry = history[historyIndex - 1];
1286
+ if (!entry) {
1287
+ return null;
1288
+ }
1289
+ set({ historyIndex: historyIndex - 1, activeDirectionKey: entry.directionKey });
1290
+ return entry;
1291
+ },
1292
+ forward: () => {
1293
+ const { history, historyIndex } = get();
1294
+ const entry = history[historyIndex + 1];
1295
+ if (!entry) {
1296
+ return null;
1297
+ }
1298
+ set({ historyIndex: historyIndex + 1, activeDirectionKey: entry.directionKey });
1299
+ return entry;
1300
+ },
1301
+ close: () => set({ viewpoints: [], line: null, activeDirectionKey: null }),
1302
+ reset: () => set({
1303
+ viewpoints: [],
1304
+ line: null,
1305
+ activeDirectionKey: null,
1306
+ history: [],
1307
+ historyIndex: -1
1308
+ })
1309
+ }
1310
+ }));
1311
+ var useViewpoints = () => useViewpointSessionStore((state) => state.viewpoints);
1312
+ var useViewpointLine = () => useViewpointSessionStore((state) => state.line);
1313
+ var useActiveDirectionKey = () => useViewpointSessionStore((state) => state.activeDirectionKey);
1314
+ var useCurrentHistoryEntry = () => useViewpointSessionStore((state) => state.history[state.historyIndex] ?? null);
1315
+ var useCanGoBack = () => useViewpointSessionStore((state) => state.historyIndex > 0);
1316
+ var useCanGoForward = () => useViewpointSessionStore((state) => state.historyIndex < state.history.length - 1);
1317
+ var getViewpointSession = () => {
1318
+ const { actions, history, historyIndex } = useViewpointSessionStore.getState();
1319
+ return { actions, current: history[historyIndex] ?? null };
1320
+ };
1321
+ var DEFAULT_SOURCES = [mapillaryPhotoSource];
1322
+ var useViewSuggestions = (viewpoints, {
1323
+ maxAgeYears,
1324
+ photoTypes,
1325
+ limit,
1326
+ filterPhoto,
1327
+ sources = DEFAULT_SOURCES,
1328
+ enabled = true
1329
+ } = {}) => {
1330
+ const [now] = useState(() => Date.now());
1331
+ const requests = viewpoints.flatMap(
1332
+ (viewpoint) => sources.map((source) => ({ viewpoint, source }))
1333
+ );
1334
+ const queries = useQueries({
1335
+ queries: requests.map(({ viewpoint, source }) => ({
1336
+ queryKey: [
1337
+ "street-imagery",
1338
+ "photos-near",
1339
+ source.id,
1340
+ viewpoint.lngLat[0],
1341
+ viewpoint.lngLat[1]
1342
+ ],
1343
+ queryFn: ({ signal }) => source.fetchNear(viewpoint.lngLat, signal),
1344
+ enabled,
1345
+ staleTime: 10 * 60 * 1e3
1346
+ }))
1347
+ });
1348
+ const photosByViewpointId = /* @__PURE__ */ new Map();
1349
+ requests.forEach(({ viewpoint }, index) => {
1350
+ const photos = queries[index]?.data ?? [];
1351
+ const kept = filterPhoto ? photos.filter(filterPhoto) : photos;
1352
+ photosByViewpointId.set(viewpoint.id, [
1353
+ ...photosByViewpointId.get(viewpoint.id) ?? [],
1354
+ ...kept
1355
+ ]);
1356
+ });
1357
+ const suggestions = buildViewSuggestions(
1358
+ viewpoints,
1359
+ (viewpoint) => photosByViewpointId.get(viewpoint.id) ?? [],
1360
+ {
1361
+ now,
1362
+ maxAgeMs: maxAgeYears == null ? void 0 : yearsToMs(maxAgeYears),
1363
+ photoTypes,
1364
+ limit
1365
+ }
1366
+ );
1367
+ return {
1368
+ suggestions,
1369
+ isLoading: enabled && queries.some((query) => query.isPending),
1370
+ // An error only when every source failed; one failing provider still leaves suggestions.
1371
+ isError: queries.length > 0 && queries.every((query) => query.isError)
1372
+ };
1373
+ };
1374
+ var STALE_TIME = 10 * 60 * 1e3;
1375
+ var useMapillaryMapFeatureImages = (featureId, { shape = SIGN_TARGET } = {}) => useQuery({
1376
+ queryKey: ["street-imagery", "mapillary-map-feature", featureId, shape],
1377
+ queryFn: ({ signal }) => fetchMapFeatureImages(featureId, { shape, signal }),
1378
+ enabled: !!featureId,
1379
+ staleTime: STALE_TIME
1380
+ });
1381
+ var useMapillaryMapFeaturesNear = (lngLat, {
1382
+ radiusMeters = 40,
1383
+ objectValues,
1384
+ enabled = true
1385
+ } = {}) => useQuery({
1386
+ queryKey: ["street-imagery", "mapillary-map-features-near", lngLat, radiusMeters, objectValues],
1387
+ queryFn: async ({ signal }) => {
1388
+ const center = lngLat;
1389
+ const features = await fetchMapillaryMapFeaturesInBbox(bboxAround(center, radiusMeters), {
1390
+ objectValues,
1391
+ signal
1392
+ });
1393
+ return features.map((feature) => ({ ...feature, distanceMeters: distanceMeters(center, feature.lngLat) })).filter((feature) => feature.distanceMeters <= radiusMeters).sort((a, b) => a.distanceMeters - b.distanceMeters);
1394
+ },
1395
+ enabled: enabled && !!lngLat,
1396
+ staleTime: STALE_TIME
1397
+ });
1398
+ var useMapillaryImageDetections = (imageId, { filter, filterKey } = {}) => useQuery({
1399
+ queryKey: ["street-imagery", "mapillary-detections", imageId, filterKey ?? null],
1400
+ queryFn: ({ signal }) => fetchImageDetections(imageId, { filter, signal }),
1401
+ enabled: !!imageId,
1402
+ staleTime: STALE_TIME
1403
+ });
1404
+ var useLocationPickStore = create()((set) => ({
1405
+ armedOpenerId: null,
1406
+ actions: {
1407
+ arm: (openerId) => set({ armedOpenerId: openerId }),
1408
+ disarm: () => set((state) => state.armedOpenerId == null ? state : { armedOpenerId: null }),
1409
+ toggle: (openerId) => set((state) => ({ armedOpenerId: state.armedOpenerId === openerId ? null : openerId }))
1410
+ }
1411
+ }));
1412
+ var useArmedLocationOpenerId = () => useLocationPickStore((state) => state.armedOpenerId);
1413
+ var useLocationPickActions = () => useLocationPickStore((state) => state.actions);
1414
+
1415
+ // src/openers/LocationPickOnMap.tsx
1416
+ var roundCoordinate = (value) => Number(value.toFixed(7));
1417
+ var LocationPickOnMap = ({ onPick }) => {
1418
+ const { current: map } = useMap();
1419
+ const armedOpenerId = useArmedLocationOpenerId();
1420
+ const { disarm } = useLocationPickActions();
1421
+ useEffect(
1422
+ function openArmedOpenerOnNextMapClick() {
1423
+ if (!map || !armedOpenerId) {
1424
+ return;
1425
+ }
1426
+ const opener = findLocationOpener(armedOpenerId);
1427
+ if (!opener) {
1428
+ return;
1429
+ }
1430
+ const onClick = (event) => {
1431
+ const target = {
1432
+ lngLat: [roundCoordinate(event.lngLat.lng), roundCoordinate(event.lngLat.lat)],
1433
+ zoom: map.getZoom()
1434
+ };
1435
+ openLocationInNewTab(opener, target);
1436
+ disarm();
1437
+ onPick?.(target, opener);
1438
+ };
1439
+ const onKeyDown = (event) => {
1440
+ if (event.key === "Escape") {
1441
+ disarm();
1442
+ }
1443
+ };
1444
+ map.on("click", onClick);
1445
+ window.addEventListener("keydown", onKeyDown);
1446
+ return function stopListeningForPick() {
1447
+ map.off("click", onClick);
1448
+ window.removeEventListener("keydown", onKeyDown);
1449
+ };
1450
+ },
1451
+ [armedOpenerId, disarm, map, onPick]
1452
+ );
1453
+ return null;
1454
+ };
1455
+ var MapillaryFeatureBar = ({ data, shownImage, onShow }) => {
1456
+ const { locale, messages } = useStreetImageryI18n();
1457
+ const labels = messages.feature;
1458
+ const { feature, days, images } = data;
1459
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1460
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", title: feature.value, children: [
1461
+ /* @__PURE__ */ jsx(
1462
+ "img",
1463
+ {
1464
+ alt: "",
1465
+ className: "size-7 shrink-0",
1466
+ onError: (event) => {
1467
+ event.currentTarget.style.display = "none";
1468
+ },
1469
+ src: mapillaryIconUrl(feature.value)
1470
+ },
1471
+ feature.value
1472
+ ),
1473
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1474
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm leading-tight font-medium text-slate-900", children: labels.name(feature.value) }),
1475
+ /* @__PURE__ */ jsxs("p", { className: "truncate text-xs leading-tight text-slate-500", children: [
1476
+ labels.seen,
1477
+ " ",
1478
+ /* @__PURE__ */ jsx(PhotoDate, { timestamp: feature.firstSeenAt }),
1479
+ " \u2013",
1480
+ " ",
1481
+ /* @__PURE__ */ jsx(PhotoDate, { timestamp: feature.lastSeenAt })
1482
+ ] })
1483
+ ] })
1484
+ ] }),
1485
+ days.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500", children: labels.noPhotos }) : /* @__PURE__ */ jsx(
1486
+ "div",
1487
+ {
1488
+ "aria-label": labels.daysSummary(images.length, days.length),
1489
+ className: "flex gap-1 overflow-x-auto",
1490
+ role: "group",
1491
+ children: days.map((day) => {
1492
+ const noon = /* @__PURE__ */ new Date(`${day.day}T12:00:00Z`);
1493
+ const shownIndex = shownImage ? day.images.findIndex((image) => image.id === shownImage.id) : -1;
1494
+ const active = shownIndex >= 0;
1495
+ return /* @__PURE__ */ jsxs(
1496
+ "button",
1497
+ {
1498
+ "aria-pressed": active,
1499
+ className: `shrink-0 rounded-md border px-2 py-1 text-xs whitespace-nowrap ${active ? "border-amber-500 bg-amber-100 text-amber-950" : "border-slate-300 bg-white text-slate-700 hover:border-slate-400"}`,
1500
+ onClick: () => {
1501
+ const next = active ? day.images[(shownIndex + 1) % day.images.length] : day.best;
1502
+ if (next) {
1503
+ onShow(next);
1504
+ }
1505
+ },
1506
+ title: `${formatDateTooltip(noon, locale)}
1507
+ ${labels.dayPhotos(day.images.length, active && day.images.length > 1)}`,
1508
+ type: "button",
1509
+ children: [
1510
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: formatMonth(noon, locale) }),
1511
+ " ",
1512
+ /* @__PURE__ */ jsxs("span", { className: active ? "text-amber-800" : "text-slate-500", children: [
1513
+ active ? `${shownIndex + 1}/` : "",
1514
+ day.images.length
1515
+ ] })
1516
+ ]
1517
+ },
1518
+ day.day
1519
+ );
1520
+ })
1521
+ }
1522
+ )
1523
+ ] });
1524
+ };
658
1525
 
659
- export { StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useAllProviderMapFeatures, useAllProviderMapFeaturesLoading, useAllProviderPhotos, useMapViewportBbox, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences };
1526
+ export { BASE_COLOR, FloatingPhotoViewer, FloatingViewerInfoButton, LocationPickOnMap, MapillaryFeatureBar, PhotoDate, SELECTION_COLOR, StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, VIEW_SHAPE_FILL_OPACITY, ViewpointLayer, getViewpointSession, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useActiveDirectionKey, useAllProviderMapFeatures, useAllProviderPhotos, useArmedLocationOpenerId, useCanGoBack, useCanGoForward, useCurrentHistoryEntry, useLocationPickActions, useMapViewportBbox, useMapillaryImageDetections, useMapillaryMapFeatureImages, useMapillaryMapFeaturesNear, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences, useViewSuggestions, useViewpointLine, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel };