@osm-editor-kit/street-imagery-react 0.1.0-alpha.1 → 0.1.0-alpha.10

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 (52) hide show
  1. package/README.md +178 -3
  2. package/dist/MapillaryPanel-FAXVZH37.js +2 -0
  3. package/dist/PanoramaxPanel-TWKTSOXM.js +2 -0
  4. package/dist/StreetLevelImagerySelectionOverlay.d.ts +22 -3
  5. package/dist/StreetLevelImagerySourcesAndLayers.d.ts +24 -1
  6. package/dist/StreetLevelImageryViewCone.d.ts +13 -1
  7. package/dist/StreetLevelImageryViewer.d.ts +23 -1
  8. package/dist/chunk-6EMFFIL2.js +147 -0
  9. package/dist/chunk-SGIB7AVC.js +309 -0
  10. package/dist/chunk-XQXYV6SR.js +466 -0
  11. package/dist/filters/PhotoDateRangeFilter.d.ts +46 -0
  12. package/dist/hooks/useAllProviderMapFeatures.d.ts +0 -4
  13. package/dist/hooks/useProviderData.d.ts +8 -3
  14. package/dist/i18n/PhotoCount.d.ts +12 -0
  15. package/dist/i18n/PhotoDate.d.ts +16 -0
  16. package/dist/i18n/StreetImageryLocaleProvider.d.ts +21 -0
  17. package/dist/i18n/messages.d.ts +45 -0
  18. package/dist/index.d.ts +19 -4
  19. package/dist/index.js +1542 -141
  20. package/dist/mapillary/MapillaryFeatureBar.d.ts +16 -0
  21. package/dist/mapillary/SelectedMapFeatureLayer.d.ts +19 -0
  22. package/dist/mapillary/featureBarLabels.d.ts +16 -0
  23. package/dist/mapillary/useMapillaryFeatures.d.ts +54 -0
  24. package/dist/openers/LocationPickOnMap.d.ts +10 -0
  25. package/dist/openers/useLocationPickStore.d.ts +9 -0
  26. package/dist/panels/MapillaryPanel.d.ts +19 -1
  27. package/dist/panels/PanoramaxPanel.d.ts +14 -4
  28. package/dist/panels/PanoramaxWebViewerPanel.d.ts +20 -0
  29. package/dist/panels/mapillaryLookAt.d.ts +41 -0
  30. package/dist/panels/panoramaxDataProvider.d.ts +44 -0
  31. package/dist/panels/panoramaxItem.d.ts +96 -0
  32. package/dist/panels/panoramaxLimitPanning.d.ts +54 -0
  33. package/dist/panels/panoramaxPhotoFromMetadata.d.ts +8 -0
  34. package/dist/panoramax-web-viewer/index.d.ts +1 -0
  35. package/dist/panoramax-web-viewer.js +314 -0
  36. package/dist/photo-details/PhotoDetailsDialog.d.ts +21 -0
  37. package/dist/photo-details/index.d.ts +6 -0
  38. package/dist/photo-details/labels.d.ts +33 -0
  39. package/dist/photo-details.js +235 -0
  40. package/dist/streetImageryClick.d.ts +4 -0
  41. package/dist/viewer-panels/index.d.ts +5 -0
  42. package/dist/viewer-panels.js +3 -0
  43. package/dist/viewpoints/FloatingPhotoViewer.d.ts +73 -0
  44. package/dist/viewpoints/ViewpointLayer.d.ts +17 -0
  45. package/dist/viewpoints/useViewSuggestions.d.ts +23 -0
  46. package/dist/viewpoints/useViewpointSessionStore.d.ts +35 -0
  47. package/dist/viewpoints/viewDirectionLabels.d.ts +4 -0
  48. package/package.json +37 -7
  49. package/dist/MapillaryPanel-QX6NESX7.js +0 -2
  50. package/dist/PanoramaxPanel-J6ANR6LB.js +0 -2
  51. package/dist/chunk-BS6LPDMY.js +0 -143
  52. package/dist/chunk-ZH3TPW4D.js +0 -165
package/dist/index.js CHANGED
@@ -1,66 +1,129 @@
1
- export { MapillaryPanel } from './chunk-BS6LPDMY.js';
2
- export { PanoramaxPanel } from './chunk-ZH3TPW4D.js';
1
+ import { useStreetImageryI18n } from './chunk-6EMFFIL2.js';
2
+ export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n } from './chunk-6EMFFIL2.js';
3
3
  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';
4
+ import { distanceMeters, sequencesToFeatureCollection, emptyLineCollection, adapterById, providerById, providerCoversBbox, fetchedSequencesMinZoom, viewConeGeoJson, viewShapeRadiusMeters, setStreetImageryConfig, viewfieldLayerId, photoLayerId, photoTargetLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, formatLocalDateTime, formatMonth, formatRelativeDate, formatDateTooltip, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, fetchImageDetections, bestTargetImage, findLocationOpener, MAP_FEATURE_COLOR, mapillaryValueName, mapillaryIconUrl, dateRangeToSliderPositions, sliderPositionsToDateRange, dateSliderBins, dateSliderPosition, yearsAgoMs, parseIsoDateStartMs, openLocationInNewTab, formatDate, isIsoDate, alignLineToPoints, coneRadiusMeters, mapFeaturesToFeatureCollection, emptyPointCollection, buildMapFeatureLayerFilter, featureSourceId, photosToFeatureCollection, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceTilesSourceId, renameExpressionProperties, sequenceTilesLayerId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
5
5
  import { Source, Layer, useMap } from 'react-map-gl/maplibre';
6
6
  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';
7
+ import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState, useRef, useEffectEvent } from 'react';
8
+ import { useQuery, useQueries } from '@tanstack/react-query';
9
+ import { create } from 'zustand';
9
10
 
10
- var HIGHLIGHT_SOURCE_ID = "selection-highlight";
11
- var HIGHLIGHT_LAYER_ID = "selection-highlight-layer";
11
+ var POINTS_SOURCE_ID = "selection-highlight";
12
+ var CONNECTOR_SOURCE_ID = "selection-connector";
12
13
  var SEQUENCE_HIGHLIGHT_SOURCE_ID = "sequence-highlight";
13
- var SEQUENCE_HIGHLIGHT_LAYER_ID = "sequence-highlight-layer";
14
+ var BASE_COLOR = "#171717";
15
+ var SELECTION_COLOR = "#f97316";
16
+ var SAME_POSITION_METERS = 0.75;
17
+ var ACTIVE_LINE_WIDTH = 2.5;
14
18
  var StreetLevelImagerySelectionOverlay = ({
15
19
  selectedPhoto,
16
- selectedSequence
20
+ selectedSequence,
21
+ cameraLngLat,
22
+ sequenceBeforeId,
23
+ beforeId
17
24
  }) => {
18
- const highlightCollection = selectedPhoto ? photosToFeatureCollection([selectedPhoto]) : emptyPointCollection();
19
- const sequenceHighlightCollection = selectedSequence ? sequencesToFeatureCollection([selectedSequence]) : emptyLineCollection();
20
- if (!selectedPhoto && sequenceHighlightCollection.features.length === 0) {
25
+ const photoLngLat = selectedPhoto?.lngLat;
26
+ const cameraDiffers = photoLngLat != null && cameraLngLat != null && distanceMeters(photoLngLat, cameraLngLat) > SAME_POSITION_METERS;
27
+ const camera = cameraDiffers ? cameraLngLat : null;
28
+ const dot = photoLngLat && cameraLngLat && !cameraDiffers ? cameraLngLat : photoLngLat;
29
+ const sequenceLines = selectedSequence ? sequencesToFeatureCollection([selectedSequence]) : emptyLineCollection();
30
+ const throughDot = (line) => dot ? alignLineToPoints(line, [dot]) : line;
31
+ const sequenceCollection = {
32
+ ...sequenceLines,
33
+ features: sequenceLines.features.map((feature) => ({
34
+ ...feature,
35
+ geometry: feature.geometry.type === "LineString" ? { ...feature.geometry, coordinates: throughDot(feature.geometry.coordinates) } : { ...feature.geometry, coordinates: feature.geometry.coordinates.map(throughDot) }
36
+ }))
37
+ };
38
+ if (!selectedPhoto && sequenceCollection.features.length === 0) {
21
39
  return null;
22
40
  }
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,
41
+ const points = {
42
+ type: "FeatureCollection",
43
+ features: [
44
+ ...dot ? [
28
45
  {
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
- }
46
+ type: "Feature",
47
+ properties: { kind: "dot" },
48
+ geometry: { type: "Point", coordinates: dot }
38
49
  }
39
- )
40
- ] }) : null,
41
- sequenceHighlightCollection.features.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
42
- /* @__PURE__ */ jsx(
43
- Source,
50
+ ] : [],
51
+ ...camera ? [
44
52
  {
45
- id: SEQUENCE_HIGHLIGHT_SOURCE_ID,
46
- type: "geojson",
47
- data: sequenceHighlightCollection
53
+ type: "Feature",
54
+ properties: { kind: "camera" },
55
+ geometry: { type: "Point", coordinates: camera }
48
56
  }
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
- }
57
+ ] : []
58
+ ]
59
+ };
60
+ const connector = {
61
+ type: "FeatureCollection",
62
+ features: dot && camera ? [
63
+ {
64
+ type: "Feature",
65
+ properties: {},
66
+ geometry: { type: "LineString", coordinates: [dot, camera] }
67
+ }
68
+ ] : []
69
+ };
70
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
71
+ /* @__PURE__ */ jsx(Source, { id: SEQUENCE_HIGHLIGHT_SOURCE_ID, type: "geojson", data: sequenceCollection }),
72
+ /* @__PURE__ */ jsx(
73
+ Layer,
74
+ {
75
+ beforeId: sequenceBeforeId ?? beforeId,
76
+ id: "sequence-highlight-layer",
77
+ type: "line",
78
+ source: SEQUENCE_HIGHLIGHT_SOURCE_ID,
79
+ layout: { "line-cap": "round", "line-join": "round" },
80
+ paint: { "line-color": BASE_COLOR, "line-width": ACTIVE_LINE_WIDTH }
81
+ }
82
+ ),
83
+ /* @__PURE__ */ jsx(Source, { id: CONNECTOR_SOURCE_ID, type: "geojson", data: connector }),
84
+ /* @__PURE__ */ jsx(
85
+ Layer,
86
+ {
87
+ beforeId,
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
+ beforeId,
99
+ id: "selection-highlight-layer",
100
+ type: "circle",
101
+ source: POINTS_SOURCE_ID,
102
+ filter: ["==", ["get", "kind"], "dot"],
103
+ paint: {
104
+ "circle-radius": 6,
105
+ "circle-color": SELECTION_COLOR,
106
+ "circle-stroke-width": 2,
107
+ "circle-stroke-color": BASE_COLOR
61
108
  }
62
- )
63
- ] }) : null
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsx(
112
+ Layer,
113
+ {
114
+ beforeId,
115
+ id: "selection-camera-layer",
116
+ type: "circle",
117
+ source: POINTS_SOURCE_ID,
118
+ filter: ["==", ["get", "kind"], "camera"],
119
+ paint: {
120
+ "circle-radius": 3.5,
121
+ "circle-color": SELECTION_COLOR,
122
+ "circle-stroke-width": 1.5,
123
+ "circle-stroke-color": BASE_COLOR
124
+ }
125
+ }
126
+ )
64
127
  ] });
65
128
  };
66
129
  var resolveSelectedSequence = (selectedPhoto, sequences, sequenceId) => {
@@ -76,9 +139,10 @@ var withinBbox = (bbox) => (item) => {
76
139
  const [lng, lat] = item.lngLat;
77
140
  return lng >= bbox[0] && lng <= bbox[2] && lat >= bbox[1] && lat <= bbox[3];
78
141
  };
79
- var useProviderPhotos = (providerId, bbox, zoom) => {
142
+ var useProviderPhotos = (providerId, bbox, zoom, { minZoom = 0 } = {}) => {
80
143
  const adapter = adapterById[providerId];
81
- const enabled = adapter.kind === "photo" && bbox != null && zoom >= adapter.minZoom && adapter.fetchPhotos != null;
144
+ const meta = providerById[providerId];
145
+ const enabled = meta.kind === "photo" && bbox != null && providerCoversBbox(providerId, bbox) && zoom >= Math.max(meta.minZoom, minZoom) && adapter?.fetchPhotos != null;
82
146
  return useQuery({
83
147
  queryKey: ["provider-photos", providerId, bboxKey(bbox), zoom],
84
148
  queryFn: async ({ signal }) => {
@@ -89,20 +153,22 @@ var useProviderPhotos = (providerId, bbox, zoom) => {
89
153
  placeholderData: (previous) => previous
90
154
  });
91
155
  };
92
- var useProviderSequences = (providerId, bbox, zoom) => {
156
+ var useProviderSequences = (providerId, bbox, zoom, { minZoom = 0 } = {}) => {
93
157
  const adapter = adapterById[providerId];
94
- const sequencesMinZoom = adapter.sequencesMinZoom ?? adapter.minZoom;
95
- const enabled = adapter.kind === "photo" && bbox != null && zoom >= sequencesMinZoom && adapter.fetchSequences != null;
158
+ const meta = providerById[providerId];
159
+ const sequencesMinZoom = fetchedSequencesMinZoom(providerId);
160
+ const enabled = meta.kind === "photo" && bbox != null && providerCoversBbox(providerId, bbox) && zoom >= Math.max(sequencesMinZoom, minZoom) && adapter?.fetchSequences != null;
96
161
  return useQuery({
97
162
  queryKey: ["provider-sequences", providerId, bboxKey(bbox), zoom],
98
- queryFn: ({ signal }) => adapter.fetchSequences?.(bbox, zoom, signal) ?? [],
163
+ queryFn: ({ signal }) => adapter?.fetchSequences?.(bbox, zoom, signal) ?? [],
99
164
  enabled,
100
165
  placeholderData: (previous) => previous
101
166
  });
102
167
  };
103
- var useProviderMapFeatures = (providerId, bbox, zoom) => {
168
+ var useProviderMapFeatures = (providerId, bbox, zoom, { minZoom = 0 } = {}) => {
104
169
  const adapter = adapterById[providerId];
105
- const enabled = adapter.kind === "mapFeature" && bbox != null && zoom >= adapter.minZoom && adapter.fetchMapFeatures != null;
170
+ const meta = providerById[providerId];
171
+ const enabled = meta.kind === "mapFeature" && bbox != null && providerCoversBbox(providerId, bbox) && zoom >= Math.max(meta.minZoom, minZoom) && adapter?.fetchMapFeatures != null;
106
172
  return useQuery({
107
173
  queryKey: ["provider-map-features", providerId, bboxKey(bbox), zoom],
108
174
  queryFn: async ({ signal }) => {
@@ -115,7 +181,8 @@ var useProviderMapFeatures = (providerId, bbox, zoom) => {
115
181
  };
116
182
  var CONE_SOURCE_ID = "view-direction-cone";
117
183
  var CONE_FILL_LAYER_ID = "view-direction-cone-fill";
118
- var CONE_LINE_LAYER_ID = "view-direction-cone-line";
184
+ var MIN_CONE_FOV_DEG = 45;
185
+ var VIEW_SHAPE_FILL_OPACITY = 0.25;
119
186
  var INTERACTIVE_PANO_PROVIDERS = /* @__PURE__ */ new Set([
120
187
  "mapillary",
121
188
  "panoramax",
@@ -127,7 +194,10 @@ var INTERACTIVE_PANO_PROVIDERS = /* @__PURE__ */ new Set([
127
194
  var StreetLevelImageryViewCone = ({
128
195
  selectedPhoto,
129
196
  zoom,
130
- viewerPov
197
+ viewerPov,
198
+ color = SELECTION_COLOR,
199
+ scale = 2.5,
200
+ beforeId
131
201
  }) => {
132
202
  const apex = viewerPov?.lngLat ?? selectedPhoto.lngLat;
133
203
  const isPano = selectedPhoto.isPano === true;
@@ -140,6 +210,9 @@ var StreetLevelImageryViewCone = ({
140
210
  } else if (selectedPhoto.providerId === "panoramax") {
141
211
  bearing = viewerPov?.bearing ?? selectedPhoto.heading;
142
212
  fov = 30;
213
+ } else if (selectedPhoto.providerId === "mapillary") {
214
+ bearing = viewerPov?.bearing ?? selectedPhoto.heading;
215
+ fov = viewerPov?.hfov ?? 30;
143
216
  } else {
144
217
  bearing = selectedPhoto.heading;
145
218
  fov = 30;
@@ -147,47 +220,29 @@ var StreetLevelImageryViewCone = ({
147
220
  if (bearing == null) {
148
221
  return null;
149
222
  }
150
- const coneFeature = viewConeGeoJson(apex, bearing, fov, coneRadiusMeters(zoom));
223
+ fov = Math.max(fov, MIN_CONE_FOV_DEG);
224
+ const coneFeature = {
225
+ ...viewConeGeoJson(apex, bearing, fov, viewShapeRadiusMeters(zoom) * scale),
226
+ properties: { isPano: selectedPhoto.isPano, capturedAt: selectedPhoto.capturedAt }
227
+ };
151
228
  return /* @__PURE__ */ jsxs(Fragment, { children: [
152
229
  /* @__PURE__ */ jsx(Source, { id: CONE_SOURCE_ID, type: "geojson", data: coneFeature }),
153
230
  /* @__PURE__ */ jsx(
154
231
  Layer,
155
232
  {
233
+ beforeId,
156
234
  id: CONE_FILL_LAYER_ID,
157
235
  type: "fill",
158
236
  source: CONE_SOURCE_ID,
159
237
  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
238
+ "fill-color": color,
239
+ "fill-opacity": VIEW_SHAPE_FILL_OPACITY,
240
+ "fill-outline-color": "rgba(0, 0, 0, 0)"
175
241
  }
176
242
  }
177
243
  )
178
244
  ] });
179
245
  };
180
- var CIRCLE_RADIUS = [
181
- "interpolate",
182
- ["linear"],
183
- ["zoom"],
184
- 10,
185
- 2,
186
- 14,
187
- 4,
188
- 18,
189
- 6
190
- ];
191
246
  var FEATURE_CIRCLE_RADIUS = [
192
247
  "interpolate",
193
248
  ["linear"],
@@ -199,6 +254,31 @@ var FEATURE_CIRCLE_RADIUS = [
199
254
  18,
200
255
  4
201
256
  ];
257
+ var padBbox = ([west, south, east, north], factor) => {
258
+ const dx = (east - west) * factor;
259
+ const dy = (north - south) * factor;
260
+ return [west - dx, south - dy, east + dx, north + dy];
261
+ };
262
+ var lngLatInBbox = ([lng, lat], [west, south, east, north]) => lng >= west && lng <= east && lat >= south && lat <= north;
263
+ var YEAR_MS = 365.25 * 24 * 60 * 60 * 1e3;
264
+ var LOADED_AT = Date.now();
265
+ var AGE_SHADE = [
266
+ "case",
267
+ ["==", ["get", "capturedAt"], null],
268
+ BASE_COLOR,
269
+ [
270
+ "step",
271
+ ["get", "capturedAt"],
272
+ "#909090",
273
+ LOADED_AT - 3 * YEAR_MS,
274
+ "#686868",
275
+ LOADED_AT - 2 * YEAR_MS,
276
+ "#404040",
277
+ LOADED_AT - YEAR_MS,
278
+ BASE_COLOR
279
+ ]
280
+ ];
281
+ var PHOTO_TARGET_RADIUS_PX = 10;
202
282
  var PHOTO_SORT_KEY = ["coalesce", ["get", "capturedAt"], 0];
203
283
  var FEATURE_SORT_KEY = ["coalesce", ["get", "lastSeenAt"], 0];
204
284
  var PhotoProviderLayer = ({
@@ -208,14 +288,62 @@ var PhotoProviderLayer = ({
208
288
  filter,
209
289
  showSequences,
210
290
  showViewfields,
211
- photoCircleColor
291
+ photoCircleColor,
292
+ activeSequenceId,
293
+ hasActiveSequence,
294
+ shownPhotoId,
295
+ beforeId,
296
+ minZoom
212
297
  }) => {
213
298
  const adapter = adapterById[providerId];
214
- const { data: photos = [] } = useProviderPhotos(providerId, bbox, zoom);
215
- const { data: sequences = [] } = useProviderSequences(providerId, bbox, zoom);
216
- 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();
299
+ const meta = providerById[providerId];
300
+ const photosMinZoom = Math.max(meta.minZoom, minZoom);
301
+ const fetchedLinesMinZoom = fetchedSequencesMinZoom(providerId);
302
+ const sequencesMinZoom = Math.max(fetchedLinesMinZoom, minZoom);
303
+ const { data: photos = [] } = useProviderPhotos(providerId, bbox, zoom, { minZoom });
304
+ const { data: sequences = [] } = useProviderSequences(providerId, bbox, zoom, { minZoom });
305
+ const visiblePhotos = zoom >= photosMinZoom ? photos.filter((photo) => photoMatchesFilters(photo, filter?.photoTypes, filter?.date)) : [];
306
+ const drawBbox = bbox ? padBbox(bbox, 0.25) : null;
307
+ const photoCollection = zoom >= photosMinZoom ? photosToFeatureCollection(
308
+ drawBbox ? visiblePhotos.filter((photo) => lngLatInBbox(photo.lngLat, drawBbox)) : visiblePhotos
309
+ ) : emptyPointCollection();
310
+ const viewfieldsActive = showViewfields && zoom >= VIEWFIELD_MIN_ZOOM;
311
+ const viewfieldCollection = viewfieldsActive ? photosToViewfieldsFeatureCollection(visiblePhotos, zoom, { bbox }) : emptyPolygonCollection();
312
+ const inActiveSequence = [
313
+ "==",
314
+ ["get", "sequenceId"],
315
+ activeSequenceId ?? ""
316
+ ];
317
+ const lineOpacity = [
318
+ "case",
319
+ inActiveSequence,
320
+ 1,
321
+ hasActiveSequence ? 0.45 : 1
322
+ ];
323
+ const dotRadius = [
324
+ "interpolate",
325
+ ["linear"],
326
+ ["zoom"],
327
+ 10,
328
+ ["case", inActiveSequence, 2, 1.2],
329
+ 14,
330
+ ["case", inActiveSequence, 4, 2],
331
+ // Up to here there are no view shapes and the dots are 20 % smaller.
332
+ VIEWFIELD_MIN_ZOOM - 0.01,
333
+ ["case", inActiveSequence, 5.5, 2.9],
334
+ VIEWFIELD_MIN_ZOOM,
335
+ ["case", inActiveSequence, 5.5, 3.6],
336
+ 18,
337
+ ["case", inActiveSequence, 6, 4]
338
+ ];
339
+ const lineAndDotColor = viewfieldsActive ? ["case", inActiveSequence, BASE_COLOR, AGE_SHADE] : photoCircleColor;
340
+ const viewShapeOpacity = (full) => [
341
+ "case",
342
+ inActiveSequence,
343
+ full,
344
+ hasActiveSequence ? full * 0.4 : full
345
+ ];
346
+ const notShownPhoto = ["!=", ["get", "photoId"], shownPhotoId ?? ""];
219
347
  const photoFilter = buildPhotoLayerFilter(filter?.photoTypes, filter?.date);
220
348
  const photoSrcId = photoSourceId(providerId);
221
349
  const viewfieldSrcId = viewfieldSourceId(providerId);
@@ -227,7 +355,7 @@ var PhotoProviderLayer = ({
227
355
  sequenceCapturedAtById.set(photo.sequenceId, photo.capturedAt);
228
356
  }
229
357
  }
230
- const filteredSequences = showSequences && zoom >= (adapter.sequencesMinZoom ?? adapter.minZoom) ? sequences.map((sequence) => {
358
+ const filteredSequences = showSequences && zoom >= sequencesMinZoom ? sequences.map((sequence) => {
231
359
  if (sequence.capturedAt != null) return sequence;
232
360
  const fromPhotos = sequenceCapturedAtById.get(sequence.sequenceId);
233
361
  return fromPhotos != null ? { ...sequence, capturedAt: fromPhotos } : sequence;
@@ -243,9 +371,103 @@ var PhotoProviderLayer = ({
243
371
  };
244
372
  return photoMatchesFilters(asPhoto, filter?.photoTypes, filter?.date);
245
373
  }) : [];
246
- const sequenceCollection = filteredSequences.length > 0 ? sequencesToFeatureCollection(filteredSequences) : emptyLineCollection();
374
+ const photoLngLatsBySequence = /* @__PURE__ */ new Map();
375
+ for (const photo of photos) {
376
+ if (photo.sequenceId == null) continue;
377
+ const list = photoLngLatsBySequence.get(photo.sequenceId);
378
+ if (list) {
379
+ list.push(photo.lngLat);
380
+ } else {
381
+ photoLngLatsBySequence.set(photo.sequenceId, [photo.lngLat]);
382
+ }
383
+ }
384
+ const alignedSequences = filteredSequences.map((sequence) => {
385
+ const points = photoLngLatsBySequence.get(sequence.sequenceId);
386
+ if (!points) return sequence;
387
+ const { geometry } = sequence;
388
+ return {
389
+ ...sequence,
390
+ geometry: geometry.type === "LineString" ? {
391
+ ...geometry,
392
+ coordinates: alignLineToPoints(geometry.coordinates, points)
393
+ } : {
394
+ ...geometry,
395
+ coordinates: geometry.coordinates.map(
396
+ (line) => alignLineToPoints(line, points)
397
+ )
398
+ }
399
+ };
400
+ });
401
+ const sequenceTiles = showSequences ? adapter?.sequenceTiles?.() : null;
402
+ const sequenceTilesMinZoom = sequenceTiles ? Math.max(sequenceTiles.minZoom, minZoom) : 0;
403
+ const sequenceCollection = alignedSequences.length > 0 ? sequencesToFeatureCollection(alignedSequences) : emptyLineCollection();
247
404
  return /* @__PURE__ */ jsxs(Fragment, { children: [
248
- adapter.fetchSequences && showSequences ? /* @__PURE__ */ jsxs(Fragment, { children: [
405
+ meta.coverageTiles ? /* @__PURE__ */ jsxs(Fragment, { children: [
406
+ /* @__PURE__ */ jsx(
407
+ Source,
408
+ {
409
+ id: `coverage-tiles-source-${providerId}`,
410
+ minzoom: meta.coverageTiles.minZoom,
411
+ tileSize: meta.coverageTiles.tileSize,
412
+ tiles: [meta.coverageTiles.url],
413
+ type: "raster"
414
+ }
415
+ ),
416
+ /* @__PURE__ */ jsx(
417
+ Layer,
418
+ {
419
+ beforeId,
420
+ id: `coverage-tiles-${providerId}`,
421
+ minzoom: meta.coverageTiles.minZoom,
422
+ paint: { "raster-opacity": 0.7 },
423
+ source: `coverage-tiles-source-${providerId}`,
424
+ type: "raster"
425
+ }
426
+ )
427
+ ] }) : null,
428
+ sequenceTiles && sequenceTilesMinZoom < fetchedLinesMinZoom ? /* @__PURE__ */ jsxs(Fragment, { children: [
429
+ /* @__PURE__ */ jsx(
430
+ Source,
431
+ {
432
+ id: sequenceTilesSourceId(providerId),
433
+ type: "vector",
434
+ tiles: sequenceTiles.tiles,
435
+ minzoom: sequenceTiles.minZoom,
436
+ maxzoom: sequenceTiles.maxZoom
437
+ },
438
+ sequenceTilesSourceId(providerId)
439
+ ),
440
+ /* @__PURE__ */ jsx(
441
+ Layer,
442
+ {
443
+ beforeId,
444
+ id: sequenceTilesLayerId(providerId),
445
+ type: "line",
446
+ source: sequenceTilesSourceId(providerId),
447
+ "source-layer": sequenceTiles.sourceLayer,
448
+ minzoom: sequenceTilesMinZoom,
449
+ maxzoom: fetchedLinesMinZoom,
450
+ filter: renameExpressionProperties(photoFilter, sequenceTiles.properties),
451
+ layout: {
452
+ "line-cap": "round",
453
+ "line-join": "round",
454
+ "line-sort-key": renameExpressionProperties(
455
+ ["case", inActiveSequence, 1, 0],
456
+ sequenceTiles.properties
457
+ )
458
+ },
459
+ paint: {
460
+ "line-color": renameExpressionProperties(photoCircleColor, sequenceTiles.properties),
461
+ "line-width": renameExpressionProperties(
462
+ ["case", inActiveSequence, ACTIVE_LINE_WIDTH, 1.25],
463
+ sequenceTiles.properties
464
+ ),
465
+ "line-opacity": renameExpressionProperties(lineOpacity, sequenceTiles.properties)
466
+ }
467
+ }
468
+ )
469
+ ] }) : null,
470
+ adapter?.fetchSequences && showSequences ? /* @__PURE__ */ jsxs(Fragment, { children: [
249
471
  /* @__PURE__ */ jsx(
250
472
  Source,
251
473
  {
@@ -258,13 +480,19 @@ var PhotoProviderLayer = ({
258
480
  /* @__PURE__ */ jsx(
259
481
  Layer,
260
482
  {
483
+ beforeId,
261
484
  id: sequenceLayerId(providerId),
262
485
  type: "line",
263
486
  source: sequenceSourceId(providerId),
487
+ layout: {
488
+ "line-cap": "round",
489
+ "line-join": "round",
490
+ "line-sort-key": ["case", inActiveSequence, 1, 0]
491
+ },
264
492
  paint: {
265
- "line-color": adapter.color,
266
- "line-width": 2,
267
- "line-opacity": 0.35
493
+ "line-color": lineAndDotColor,
494
+ "line-width": ["case", inActiveSequence, ACTIVE_LINE_WIDTH, 1.25],
495
+ "line-opacity": lineOpacity
268
496
  }
269
497
  }
270
498
  )
@@ -282,25 +510,30 @@ var PhotoProviderLayer = ({
282
510
  /* @__PURE__ */ jsx(
283
511
  Layer,
284
512
  {
513
+ beforeId,
285
514
  id: viewfieldLayerId(providerId),
286
515
  type: "fill",
287
516
  source: viewfieldSrcId,
517
+ filter: notShownPhoto,
288
518
  paint: {
289
519
  "fill-color": photoCircleColor,
290
- "fill-opacity": 0.22
520
+ "fill-opacity": viewShapeOpacity(VIEW_SHAPE_FILL_OPACITY),
521
+ "fill-outline-color": "rgba(0, 0, 0, 0)"
291
522
  }
292
523
  }
293
524
  ),
294
525
  /* @__PURE__ */ jsx(
295
526
  Layer,
296
527
  {
528
+ beforeId,
297
529
  id: viewfieldLineLayerId(providerId),
298
530
  type: "line",
299
531
  source: viewfieldSrcId,
532
+ filter: notShownPhoto,
300
533
  paint: {
301
534
  "line-color": photoCircleColor,
302
- "line-width": 1,
303
- "line-opacity": 0.45
535
+ "line-width": 0.5,
536
+ "line-opacity": viewShapeOpacity(0.3)
304
537
  }
305
538
  }
306
539
  )
@@ -318,16 +551,27 @@ var PhotoProviderLayer = ({
318
551
  /* @__PURE__ */ jsx(
319
552
  Layer,
320
553
  {
554
+ beforeId,
555
+ id: photoTargetLayerId(providerId),
556
+ type: "circle",
557
+ source: photoSrcId,
558
+ filter: photoFilter,
559
+ layout: { "circle-sort-key": PHOTO_SORT_KEY },
560
+ paint: { "circle-radius": PHOTO_TARGET_RADIUS_PX, "circle-opacity": 0 }
561
+ }
562
+ ),
563
+ /* @__PURE__ */ jsx(
564
+ Layer,
565
+ {
566
+ beforeId,
321
567
  id: photoLayerId(providerId),
322
568
  type: "circle",
323
569
  source: photoSrcId,
324
570
  filter: photoFilter,
325
571
  layout: { "circle-sort-key": PHOTO_SORT_KEY },
326
572
  paint: {
327
- "circle-radius": CIRCLE_RADIUS,
328
- "circle-color": photoCircleColor,
329
- "circle-stroke-width": 1,
330
- "circle-stroke-color": "#ffffff"
573
+ "circle-radius": dotRadius,
574
+ "circle-color": lineAndDotColor
331
575
  }
332
576
  }
333
577
  )
@@ -338,11 +582,16 @@ var MapFeatureProviderLayer = ({
338
582
  bbox,
339
583
  zoom,
340
584
  filter,
341
- mapFeatureCircleColor
585
+ mapFeatureCircleColor,
586
+ beforeId,
587
+ minZoom
342
588
  }) => {
343
- const adapter = adapterById[providerId];
344
- const { data: features = [] } = useProviderMapFeatures(providerId, bbox, zoom);
345
- const featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(features) : emptyPointCollection();
589
+ const meta = providerById[providerId];
590
+ const { data: features = [] } = useProviderMapFeatures(providerId, bbox, zoom, { minZoom });
591
+ const valueFilter = filter?.mapFeatureValue;
592
+ const featureCollection = zoom >= Math.max(meta.minZoom, minZoom) ? mapFeaturesToFeatureCollection(
593
+ valueFilter ? features.filter((feature) => valueFilter(feature.value)) : features
594
+ ) : emptyPointCollection();
346
595
  const featureFilter = buildMapFeatureLayerFilter(filter?.date);
347
596
  const featureSrcId = featureSourceId(providerId);
348
597
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -359,6 +608,7 @@ var MapFeatureProviderLayer = ({
359
608
  /* @__PURE__ */ jsx(
360
609
  Layer,
361
610
  {
611
+ beforeId,
362
612
  id: featureLayerId(providerId),
363
613
  type: "circle",
364
614
  source: featureSrcId,
@@ -366,17 +616,14 @@ var MapFeatureProviderLayer = ({
366
616
  layout: { "circle-sort-key": FEATURE_SORT_KEY },
367
617
  paint: {
368
618
  "circle-radius": FEATURE_CIRCLE_RADIUS,
369
- "circle-color": mapFeatureCircleColor,
370
- "circle-stroke-width": 1,
371
- "circle-stroke-color": "#ffffff"
619
+ "circle-color": mapFeatureCircleColor
372
620
  }
373
621
  }
374
622
  )
375
623
  ] });
376
624
  };
377
625
  var ProviderLayer = (props) => {
378
- const adapter = adapterById[props.providerId];
379
- return adapter.kind === "mapFeature" ? /* @__PURE__ */ jsx(MapFeatureProviderLayer, { ...props }) : /* @__PURE__ */ jsx(PhotoProviderLayer, { ...props });
626
+ return providerById[props.providerId].kind === "mapFeature" ? /* @__PURE__ */ jsx(MapFeatureProviderLayer, { ...props }) : /* @__PURE__ */ jsx(PhotoProviderLayer, { ...props });
380
627
  };
381
628
  var StreetLevelImagerySourcesAndLayers = ({
382
629
  providers,
@@ -394,8 +641,12 @@ var StreetLevelImagerySourcesAndLayers = ({
394
641
  selectedPhoto,
395
642
  selectedSequenceId,
396
643
  viewerPov,
644
+ viewConeColor,
645
+ viewConeScale,
397
646
  photoCircleColor,
398
- mapFeatureCircleColor
647
+ mapFeatureCircleColor,
648
+ beforeId,
649
+ minZoom = 0
399
650
  } = options;
400
651
  useEffect(
401
652
  function applyStreetImageryConfig() {
@@ -411,18 +662,25 @@ var StreetLevelImagerySourcesAndLayers = ({
411
662
  showSelectionHighlight && selectedProviderId ? bbox : null,
412
663
  zoom
413
664
  );
414
- const selectedSequence = resolveSelectedSequence(
415
- selectedPhoto,
416
- sequences,
417
- selectedSequenceId ?? selectedPhoto?.sequenceId
665
+ const activeSequenceId = selectedSequenceId ?? selectedPhoto?.sequenceId;
666
+ const selectedSequence = resolveSelectedSequence(selectedPhoto, sequences, activeSequenceId);
667
+ const providerDrawsSequences = selectedProviderId != null && showSequences && providers.includes(selectedProviderId);
668
+ const firstPhotoProvider = providers.find(
669
+ (id) => providerById[id].kind === "photo" && adapterById[id]?.fetchPhotos
418
670
  );
671
+ const sequenceBeforeId = firstPhotoProvider ? showViewfields ? viewfieldLayerId(firstPhotoProvider) : photoLayerId(firstPhotoProvider) : void 0;
419
672
  return /* @__PURE__ */ jsxs(Fragment, { children: [
420
673
  providers.map((providerId) => /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(
421
674
  ProviderLayer,
422
675
  {
676
+ beforeId,
677
+ minZoom,
423
678
  bbox,
424
679
  filter,
425
680
  mapFeatureCircleColor,
681
+ activeSequenceId: providerId === selectedProviderId ? activeSequenceId : null,
682
+ hasActiveSequence: activeSequenceId != null,
683
+ shownPhotoId: showViewCone && providerId === selectedProviderId ? selectedPhoto?.photoId : null,
426
684
  photoCircleColor,
427
685
  providerId,
428
686
  showSequences,
@@ -433,6 +691,9 @@ var StreetLevelImagerySourcesAndLayers = ({
433
691
  showViewCone && selectedPhoto ? /* @__PURE__ */ jsx(
434
692
  StreetLevelImageryViewCone,
435
693
  {
694
+ beforeId,
695
+ color: viewConeColor ?? SELECTION_COLOR,
696
+ scale: viewConeScale,
436
697
  selectedPhoto,
437
698
  viewerPov,
438
699
  zoom
@@ -441,45 +702,65 @@ var StreetLevelImagerySourcesAndLayers = ({
441
702
  showSelectionHighlight ? /* @__PURE__ */ jsx(
442
703
  StreetLevelImagerySelectionOverlay,
443
704
  {
705
+ beforeId,
706
+ cameraLngLat: viewerPov?.lngLat,
444
707
  selectedPhoto,
445
- selectedSequence
708
+ selectedSequence: providerDrawsSequences ? null : selectedSequence,
709
+ sequenceBeforeId
446
710
  }
447
711
  ) : null
448
712
  ] });
449
713
  };
450
- var MapillaryPanel2 = lazy(
451
- () => import('./MapillaryPanel-QX6NESX7.js').then((module) => ({
714
+ var MapillaryPanel = lazy(
715
+ () => import('./MapillaryPanel-FAXVZH37.js').then((module) => ({
452
716
  default: module.MapillaryPanel
453
717
  }))
454
718
  );
455
- var PanoramaxPanel2 = lazy(
456
- () => import('./PanoramaxPanel-J6ANR6LB.js').then((module) => ({
719
+ var PanoramaxPanel = lazy(
720
+ () => import('./PanoramaxPanel-TWKTSOXM.js').then((module) => ({
457
721
  default: module.PanoramaxPanel
458
722
  }))
459
723
  );
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" }) });
724
+ var ViewerPanelPlaceholder = () => {
725
+ const { messages } = useStreetImageryI18n();
726
+ 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 }) });
727
+ };
461
728
  var StreetLevelImageryViewer = ({
462
729
  photo,
463
730
  groupPhotos,
464
731
  onPhotoSelected,
465
- onEaseMapToPoint
732
+ onEaseMapToPoint,
733
+ onViewerPhoto,
734
+ lookAtBearing,
735
+ lookAt,
736
+ outlines,
737
+ hideAttribution,
738
+ panoramaxPanel: PanoramaxPanelOverride
466
739
  }) => {
467
740
  if (photo.providerId === "mapillary") {
468
741
  return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewerPanelPlaceholder, {}), children: /* @__PURE__ */ jsx(
469
- MapillaryPanel2,
742
+ MapillaryPanel,
470
743
  {
471
744
  groupPhotos,
745
+ hideAttribution,
472
746
  onEaseMapToPoint,
473
747
  onPhotoSelected,
748
+ lookAt,
749
+ lookAtBearing,
750
+ outlines,
751
+ onViewerPhoto,
474
752
  photo
475
753
  }
476
754
  ) });
477
755
  }
478
756
  if (photo.providerId === "panoramax") {
757
+ const Panel = PanoramaxPanelOverride ?? PanoramaxPanel;
479
758
  return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewerPanelPlaceholder, {}), children: /* @__PURE__ */ jsx(
480
- PanoramaxPanel2,
759
+ Panel,
481
760
  {
482
761
  groupPhotos,
762
+ hideAttribution,
763
+ onViewerPhoto,
483
764
  onEaseMapToPoint,
484
765
  onPhotoSelected,
485
766
  photo
@@ -490,6 +771,7 @@ var StreetLevelImageryViewer = ({
490
771
  };
491
772
  var streetImageryInteractiveLayerIds = (providers) => [
492
773
  ...providers.map((providerId) => photoLayerId(providerId)),
774
+ ...providers.map((providerId) => photoTargetLayerId(providerId)),
493
775
  ...providers.map((providerId) => viewfieldLayerId(providerId)),
494
776
  ...providers.map((providerId) => featureLayerId(providerId))
495
777
  ];
@@ -498,6 +780,10 @@ var parseProviderIdFromLayerId = (layerId) => {
498
780
  if (photoMatch) {
499
781
  return photoMatch[1];
500
782
  }
783
+ const targetMatch = layerId.match(/^photo-targets-(.+)$/);
784
+ if (targetMatch) {
785
+ return targetMatch[1];
786
+ }
501
787
  const viewfieldMatch = layerId.match(/^viewfields-(.+)$/);
502
788
  if (viewfieldMatch) {
503
789
  return viewfieldMatch[1];
@@ -522,7 +808,7 @@ var queryStreetImageryFeatures = (event) => {
522
808
  continue;
523
809
  }
524
810
  const props = feature.properties ?? {};
525
- if (layerId.startsWith("photos-") || layerId.startsWith("viewfields-")) {
811
+ if (layerId.startsWith("photos-") || layerId.startsWith("photo-targets-") || layerId.startsWith("viewfields-")) {
526
812
  const photoId = props.photoId;
527
813
  if (photoId == null) {
528
814
  continue;
@@ -567,18 +853,6 @@ var useAllProviderMapFeatures = (providerIds, bbox, zoom, date) => {
567
853
  return data.filter((feature) => mapFeatureMatchesDateRange(feature, date));
568
854
  });
569
855
  };
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
856
  var useProviderPhotosMaybe = (providerId, enabled, bbox, zoom) => useProviderPhotos(providerId, enabled ? bbox : null, zoom);
583
857
  var useAllProviderPhotos = (providerIds, bbox, zoom, photoTypes, date) => {
584
858
  const enabled = new Set(providerIds);
@@ -655,5 +929,1132 @@ var usePhotoThumbnail = (photo) => useQuery({
655
929
  enabled: photo != null,
656
930
  staleTime: 24 * 60 * 60 * 1e3
657
931
  });
932
+ var PhotoCount = ({ index, total, className }) => /* @__PURE__ */ jsxs("span", { className: `inline-flex shrink-0 items-center gap-0.5 tabular-nums ${className ?? ""}`, children: [
933
+ /* @__PURE__ */ jsx(
934
+ "svg",
935
+ {
936
+ "aria-hidden": true,
937
+ className: "size-3 shrink-0",
938
+ fill: "none",
939
+ stroke: "currentColor",
940
+ strokeLinecap: "round",
941
+ strokeLinejoin: "round",
942
+ strokeWidth: 1.75,
943
+ viewBox: "0 0 24 24",
944
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6h16v12H4ZM4 15l4.5-4.5 4 4L15 12l5 5M9 9.5h.01" })
945
+ }
946
+ ),
947
+ index != null ? `${index}/` : "",
948
+ total
949
+ ] });
950
+ var PhotoDate = ({ timestamp, className, localDateTime }) => {
951
+ const { locale, messages } = useStreetImageryI18n();
952
+ if (timestamp == null) {
953
+ return /* @__PURE__ */ jsx("span", { className, children: messages.unknownDate });
954
+ }
955
+ const precise = localDateTime ? formatLocalDateTime(localDateTime, locale) : null;
956
+ return /* @__PURE__ */ jsx(
957
+ "time",
958
+ {
959
+ className,
960
+ dateTime: new Date(timestamp).toISOString(),
961
+ title: precise ? `${precise}
962
+ ${formatRelativeDate(timestamp, locale)}` : formatDateTooltip(timestamp, locale),
963
+ children: formatMonth(timestamp, locale)
964
+ }
965
+ );
966
+ };
967
+
968
+ // src/viewpoints/viewDirectionLabels.ts
969
+ var viewSuggestionLabel = ({ viewpoint, direction }, messages) => messages.viewDirection(viewpoint.label ?? messages.viewpointRole[viewpoint.role], direction.kind);
970
+ var DEFAULT_LAYOUT = { right: 16, bottom: 32, width: 420, minimized: false };
971
+ var MIN_WIDTH = 280;
972
+ var EDGE = 8;
973
+ var hasStoredLayout = (storageKey) => {
974
+ try {
975
+ return localStorage.getItem(storageKey) != null;
976
+ } catch {
977
+ return false;
978
+ }
979
+ };
980
+ var readLayout = (storageKey) => {
981
+ try {
982
+ const raw = localStorage.getItem(storageKey);
983
+ return raw ? { ...DEFAULT_LAYOUT, ...JSON.parse(raw) } : DEFAULT_LAYOUT;
984
+ } catch {
985
+ return DEFAULT_LAYOUT;
986
+ }
987
+ };
988
+ var writeLayout = (storageKey, layout) => {
989
+ try {
990
+ localStorage.setItem(storageKey, JSON.stringify(layout));
991
+ } catch {
992
+ }
993
+ };
994
+ var Icon = ({ path, className = "size-4" }) => /* @__PURE__ */ jsx(
995
+ "svg",
996
+ {
997
+ "aria-hidden": true,
998
+ className,
999
+ fill: "none",
1000
+ stroke: "currentColor",
1001
+ strokeLinecap: "round",
1002
+ strokeLinejoin: "round",
1003
+ strokeWidth: 1.75,
1004
+ viewBox: "0 0 24 24",
1005
+ children: /* @__PURE__ */ jsx("path", { d: path })
1006
+ }
1007
+ );
1008
+ var ICONS = {
1009
+ back: "M15 18l-6-6 6-6",
1010
+ undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11",
1011
+ redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13",
1012
+ forward: "M9 18l6-6-6-6",
1013
+ close: "M18 6 6 18M6 6l12 12",
1014
+ minimize: "M5 12h14",
1015
+ expand: "M4 14v6h6M20 10V4h-6M14 10l6-6M10 14l-6 6",
1016
+ arrow: "M12 20V5M6 11l6-6 6 6",
1017
+ info: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 11v5M12 8h.01"
1018
+ };
1019
+ var ToolbarButton = ({
1020
+ label,
1021
+ onClick,
1022
+ disabled,
1023
+ children
1024
+ }) => /* @__PURE__ */ jsx(
1025
+ "button",
1026
+ {
1027
+ "aria-label": label,
1028
+ 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",
1029
+ disabled,
1030
+ onClick,
1031
+ title: label,
1032
+ type: "button",
1033
+ children
1034
+ }
1035
+ );
1036
+ var SuggestionChip = ({
1037
+ suggestion,
1038
+ active,
1039
+ shownPhotoId,
1040
+ showPlace,
1041
+ onSelect
1042
+ }) => {
1043
+ const { messages } = useStreetImageryI18n();
1044
+ const { candidates } = suggestion;
1045
+ const best = candidates[0];
1046
+ const shownIndex = active ? candidates.findIndex((candidate) => candidate.photo.photoId === shownPhotoId) : -1;
1047
+ const shown = shownIndex >= 0 ? candidates[shownIndex] : best;
1048
+ const canStep = active && candidates.length > 1;
1049
+ const hasCount = candidates.length > 0;
1050
+ const name = viewSuggestionLabel(suggestion, messages);
1051
+ const label = canStep ? messages.viewer.nextPhotoOfView(name) : name;
1052
+ return /* @__PURE__ */ jsxs(
1053
+ "button",
1054
+ {
1055
+ "aria-label": best ? label : messages.viewer.noPhoto(label),
1056
+ "aria-pressed": active,
1057
+ className: [
1058
+ "flex min-w-0 flex-col items-center rounded-md border px-1 py-0.5 text-xs leading-tight",
1059
+ 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"
1060
+ ].join(" "),
1061
+ disabled: !best,
1062
+ onClick: () => {
1063
+ const next = canStep ? candidates[(shownIndex + 1) % candidates.length] : best;
1064
+ if (next) {
1065
+ onSelect(next);
1066
+ }
1067
+ },
1068
+ title: best ? label : messages.viewer.noMatchingPhoto(label),
1069
+ type: "button",
1070
+ children: [
1071
+ /* @__PURE__ */ jsxs(
1072
+ "span",
1073
+ {
1074
+ className: `flex items-center gap-1 font-medium ${hasCount ? "w-full justify-between px-1" : "max-w-full"}`,
1075
+ children: [
1076
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
1077
+ /* @__PURE__ */ jsx(
1078
+ "span",
1079
+ {
1080
+ "aria-hidden": true,
1081
+ className: "inline-flex shrink-0",
1082
+ style: { transform: `rotate(${suggestion.direction.bearing}deg)` },
1083
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-3", path: ICONS.arrow })
1084
+ }
1085
+ ),
1086
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
1087
+ showPlace ? `${suggestion.viewpoint.label ?? messages.viewpointRole[suggestion.viewpoint.role]} ` : "",
1088
+ messages.compass[Math.round(suggestion.direction.bearing / 45) % 8]
1089
+ ] })
1090
+ ] }),
1091
+ hasCount ? /* @__PURE__ */ jsx(
1092
+ PhotoCount,
1093
+ {
1094
+ className: `font-normal ${active ? "text-fuchsia-100" : "text-slate-500"}`,
1095
+ index: shownIndex >= 0 ? shownIndex + 1 : void 0,
1096
+ total: candidates.length
1097
+ }
1098
+ ) : null
1099
+ ]
1100
+ }
1101
+ ),
1102
+ shown ? /* @__PURE__ */ jsx(
1103
+ PhotoDate,
1104
+ {
1105
+ className: `max-w-full truncate ${active ? "text-fuchsia-100" : "text-slate-400"}`,
1106
+ timestamp: shown.photo.capturedAt
1107
+ }
1108
+ ) : /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "\u2014" })
1109
+ ]
1110
+ }
1111
+ );
1112
+ };
1113
+ var FloatingViewerInfoButton = ({
1114
+ label,
1115
+ onClick
1116
+ }) => /* @__PURE__ */ jsx(ToolbarButton, { label, onClick, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.info }) });
1117
+ var FloatingPhotoViewer = ({
1118
+ title,
1119
+ titleActions,
1120
+ suggestions,
1121
+ activeDirectionKey,
1122
+ onSelectSuggestion,
1123
+ shownPhotoId,
1124
+ canGoBack = false,
1125
+ canGoForward = false,
1126
+ onBack,
1127
+ onForward,
1128
+ onClose,
1129
+ step,
1130
+ toolbar,
1131
+ status,
1132
+ children,
1133
+ footer,
1134
+ drawer,
1135
+ storageKey = "street-imagery:floating-viewer",
1136
+ defaultPosition
1137
+ }) => {
1138
+ const { messages } = useStreetImageryI18n();
1139
+ const severalViewpoints = new Set(suggestions.map((suggestion) => suggestion.viewpoint.id)).size > 1;
1140
+ const [layout, setLayout] = useState(() => readLayout(storageKey));
1141
+ const [drawerOpen, setDrawerOpen] = useState(false);
1142
+ const [moved, setMoved] = useState(() => hasStoredLayout(storageKey));
1143
+ const atTopLeft = defaultPosition?.corner === "top-left" && !moved;
1144
+ const position = atTopLeft ? { left: defaultPosition.left ?? EDGE, top: defaultPosition.top ?? EDGE } : { right: layout.right, bottom: layout.bottom };
1145
+ const dragRef = useRef(null);
1146
+ const updateLayout = (next) => {
1147
+ setLayout(next);
1148
+ writeLayout(storageKey, next);
1149
+ };
1150
+ const onShortcut = useEffectEvent((event) => {
1151
+ const target = event.target;
1152
+ if (target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"]')) {
1153
+ return;
1154
+ }
1155
+ if (event.key === "Escape") {
1156
+ onClose();
1157
+ } else if (event.key === "[" && canGoBack) {
1158
+ onBack?.();
1159
+ } else if (event.key === "]" && canGoForward) {
1160
+ onForward?.();
1161
+ } else if (event.key === "ArrowLeft" && event.altKey) {
1162
+ step?.onPrevious?.();
1163
+ } else if (event.key === "ArrowRight" && event.altKey) {
1164
+ step?.onNext?.();
1165
+ }
1166
+ });
1167
+ useEffect(function subscribeKeyboardShortcuts() {
1168
+ window.addEventListener("keydown", onShortcut);
1169
+ return () => window.removeEventListener("keydown", onShortcut);
1170
+ }, []);
1171
+ const startDrag = (event, mode) => {
1172
+ if (event.button !== 0 || event.target.closest("button")) {
1173
+ return;
1174
+ }
1175
+ event.currentTarget.setPointerCapture(event.pointerId);
1176
+ const box = event.currentTarget.closest("section");
1177
+ const container = box?.offsetParent;
1178
+ const start = atTopLeft && box && container ? {
1179
+ ...layout,
1180
+ right: container.clientWidth - box.offsetLeft - box.offsetWidth,
1181
+ bottom: container.clientHeight - box.offsetTop - box.offsetHeight
1182
+ } : layout;
1183
+ setLayout(start);
1184
+ setMoved(true);
1185
+ dragRef.current = { x: event.clientX, y: event.clientY, layout: start, mode };
1186
+ };
1187
+ const onDrag = (event) => {
1188
+ const drag = dragRef.current;
1189
+ if (!drag) {
1190
+ return;
1191
+ }
1192
+ const dx = event.clientX - drag.x;
1193
+ const dy = event.clientY - drag.y;
1194
+ setLayout(
1195
+ drag.mode === "move" ? {
1196
+ ...drag.layout,
1197
+ right: Math.max(EDGE, drag.layout.right - dx),
1198
+ bottom: Math.max(EDGE, drag.layout.bottom - dy)
1199
+ } : { ...drag.layout, width: Math.max(MIN_WIDTH, drag.layout.width - dx) }
1200
+ );
1201
+ };
1202
+ const endDrag = () => {
1203
+ if (dragRef.current) {
1204
+ dragRef.current = null;
1205
+ writeLayout(storageKey, layout);
1206
+ }
1207
+ };
1208
+ const dragHandlers = { onPointerMove: onDrag, onPointerUp: endDrag, onPointerCancel: endDrag };
1209
+ if (layout.minimized) {
1210
+ return /* @__PURE__ */ jsxs(
1211
+ "button",
1212
+ {
1213
+ 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",
1214
+ onClick: () => updateLayout({ ...layout, minimized: false }),
1215
+ style: position,
1216
+ type: "button",
1217
+ children: [
1218
+ /* @__PURE__ */ jsx(Icon, { path: ICONS.expand }),
1219
+ title
1220
+ ]
1221
+ }
1222
+ );
1223
+ }
1224
+ return /* @__PURE__ */ jsxs(
1225
+ "section",
1226
+ {
1227
+ "aria-label": messages.viewer.regionLabel,
1228
+ 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",
1229
+ style: {
1230
+ ...position,
1231
+ width: `min(${layout.width}px, calc(100% - 1rem))`
1232
+ },
1233
+ children: [
1234
+ /* @__PURE__ */ jsx(
1235
+ "div",
1236
+ {
1237
+ "aria-hidden": true,
1238
+ className: "absolute inset-y-0 left-0 z-10 w-1.5 cursor-ew-resize hover:bg-fuchsia-500/20",
1239
+ onPointerDown: (event) => startDrag(event, "resize"),
1240
+ ...dragHandlers
1241
+ }
1242
+ ),
1243
+ /* @__PURE__ */ jsxs(
1244
+ "header",
1245
+ {
1246
+ className: "flex cursor-move touch-none items-center gap-1 border-b border-slate-100 px-2 py-1.5 select-none",
1247
+ onPointerDown: (event) => startDrag(event, "move"),
1248
+ ...dragHandlers,
1249
+ children: [
1250
+ step ? /* @__PURE__ */ jsxs(Fragment, { children: [
1251
+ /* @__PURE__ */ jsx(
1252
+ ToolbarButton,
1253
+ {
1254
+ disabled: !step.onPrevious,
1255
+ label: step.previousLabel,
1256
+ onClick: () => step.onPrevious?.(),
1257
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.back })
1258
+ }
1259
+ ),
1260
+ /* @__PURE__ */ jsx(
1261
+ ToolbarButton,
1262
+ {
1263
+ disabled: !step.onNext,
1264
+ label: step.nextLabel,
1265
+ onClick: () => step.onNext?.(),
1266
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.forward })
1267
+ }
1268
+ )
1269
+ ] }) : null,
1270
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-0.5", children: [
1271
+ /* @__PURE__ */ jsx("h2", { className: "min-w-0 truncate px-1 text-sm font-semibold text-slate-800", children: title }),
1272
+ titleActions
1273
+ ] }),
1274
+ onBack && onForward ? /* @__PURE__ */ jsxs(Fragment, { children: [
1275
+ /* @__PURE__ */ jsx(
1276
+ ToolbarButton,
1277
+ {
1278
+ disabled: !canGoBack,
1279
+ label: messages.viewer.backInHistory,
1280
+ onClick: onBack,
1281
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.undo })
1282
+ }
1283
+ ),
1284
+ /* @__PURE__ */ jsx(
1285
+ ToolbarButton,
1286
+ {
1287
+ disabled: !canGoForward,
1288
+ label: messages.viewer.forwardInHistory,
1289
+ onClick: onForward,
1290
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.redo })
1291
+ }
1292
+ )
1293
+ ] }) : null,
1294
+ /* @__PURE__ */ jsx(
1295
+ ToolbarButton,
1296
+ {
1297
+ label: messages.viewer.minimize,
1298
+ onClick: () => updateLayout({ ...layout, minimized: true }),
1299
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.minimize })
1300
+ }
1301
+ ),
1302
+ /* @__PURE__ */ jsx(ToolbarButton, { label: messages.viewer.close, onClick: onClose, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.close }) })
1303
+ ]
1304
+ }
1305
+ ),
1306
+ toolbar ? /* @__PURE__ */ jsx("div", { className: "border-b border-slate-100 px-2 py-1.5", children: toolbar }) : suggestions.length > 0 ? /* @__PURE__ */ jsx(
1307
+ "nav",
1308
+ {
1309
+ "aria-label": messages.viewer.suggestedViews,
1310
+ className: "grid grid-cols-[repeat(auto-fit,minmax(5.25rem,1fr))] gap-1 border-b border-slate-100 px-2 py-1.5",
1311
+ title: messages.viewer.suggestedViewsHint,
1312
+ children: suggestions.map((suggestion) => /* @__PURE__ */ jsx(
1313
+ SuggestionChip,
1314
+ {
1315
+ active: suggestion.direction.key === activeDirectionKey,
1316
+ onSelect: (candidate) => onSelectSuggestion(suggestion, candidate),
1317
+ shownPhotoId,
1318
+ showPlace: severalViewpoints,
1319
+ suggestion
1320
+ },
1321
+ suggestion.direction.key
1322
+ ))
1323
+ }
1324
+ ) : null,
1325
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 overflow-y-auto", children: [
1326
+ status ? /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-xs text-slate-500", role: "status", children: status }) : null,
1327
+ children,
1328
+ footer ? /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-xs text-slate-600", children: footer }) : null,
1329
+ drawer ? /* @__PURE__ */ jsxs("div", { className: "border-t border-slate-100", children: [
1330
+ /* @__PURE__ */ jsxs(
1331
+ "button",
1332
+ {
1333
+ "aria-expanded": drawerOpen,
1334
+ className: "flex w-full items-center justify-between px-3 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50",
1335
+ onClick: () => setDrawerOpen((open) => !open),
1336
+ type: "button",
1337
+ children: [
1338
+ drawer.label,
1339
+ /* @__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 }) })
1340
+ ]
1341
+ }
1342
+ ),
1343
+ drawerOpen ? /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: drawer.content }) : null
1344
+ ] }) : null
1345
+ ] })
1346
+ ]
1347
+ }
1348
+ );
1349
+ };
1350
+ var SOURCE_DIRECTIONS = "viewpoint-directions";
1351
+ var SOURCE_POINTS = "viewpoint-points";
1352
+ var SOURCE_LINE = "viewpoint-line";
1353
+ var VIEWPOINT_DIRECTION_LAYER_ID = "viewpoint-direction-fill";
1354
+ var VIEWPOINT_DEFAULT_COLOR = "#c026d3";
1355
+ var directionLengthMeters = (zoom) => Math.max(8, coneRadiusMeters(zoom) * 3);
1356
+ var ViewpointLayer = ({
1357
+ viewpoints,
1358
+ suggestions,
1359
+ activeDirectionKey,
1360
+ zoom,
1361
+ line,
1362
+ color = VIEWPOINT_DEFAULT_COLOR
1363
+ }) => {
1364
+ const length = directionLengthMeters(zoom);
1365
+ const directions = {
1366
+ type: "FeatureCollection",
1367
+ features: suggestions.map(({ viewpoint, direction, candidates }) => ({
1368
+ ...viewConeGeoJson(viewpoint.lngLat, direction.bearing, 40, length),
1369
+ properties: {
1370
+ key: direction.key,
1371
+ active: direction.key === activeDirectionKey,
1372
+ hasPhoto: candidates.length > 0
1373
+ }
1374
+ }))
1375
+ };
1376
+ const points = {
1377
+ type: "FeatureCollection",
1378
+ features: viewpoints.map(
1379
+ (viewpoint) => ({
1380
+ type: "Feature",
1381
+ properties: { id: viewpoint.id },
1382
+ geometry: { type: "Point", coordinates: viewpoint.lngLat }
1383
+ })
1384
+ )
1385
+ };
1386
+ const lineFeature = {
1387
+ type: "FeatureCollection",
1388
+ features: line && line.length > 1 ? [{ type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: line } }] : []
1389
+ };
1390
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1391
+ /* @__PURE__ */ jsx(Source, { id: SOURCE_LINE, type: "geojson", data: lineFeature }),
1392
+ /* @__PURE__ */ jsx(
1393
+ Layer,
1394
+ {
1395
+ id: "viewpoint-line",
1396
+ type: "line",
1397
+ source: SOURCE_LINE,
1398
+ layout: { "line-cap": "round", "line-join": "round" },
1399
+ paint: { "line-color": color, "line-width": 5, "line-opacity": 0.5 }
1400
+ }
1401
+ ),
1402
+ /* @__PURE__ */ jsx(Source, { id: SOURCE_DIRECTIONS, type: "geojson", data: directions }),
1403
+ /* @__PURE__ */ jsx(
1404
+ Layer,
1405
+ {
1406
+ id: VIEWPOINT_DIRECTION_LAYER_ID,
1407
+ type: "fill",
1408
+ source: SOURCE_DIRECTIONS,
1409
+ paint: {
1410
+ "fill-color": color,
1411
+ "fill-opacity": ["case", ["get", "active"], 0.55, ["get", "hasPhoto"], 0.18, 0.04]
1412
+ }
1413
+ }
1414
+ ),
1415
+ /* @__PURE__ */ jsx(
1416
+ Layer,
1417
+ {
1418
+ id: "viewpoint-direction-line",
1419
+ type: "line",
1420
+ source: SOURCE_DIRECTIONS,
1421
+ paint: {
1422
+ "line-color": color,
1423
+ "line-width": ["case", ["get", "active"], 2, 1],
1424
+ "line-dasharray": [2, 1.5],
1425
+ "line-opacity": ["case", ["get", "hasPhoto"], 0.9, 0.35]
1426
+ }
1427
+ }
1428
+ ),
1429
+ /* @__PURE__ */ jsx(Source, { id: SOURCE_POINTS, type: "geojson", data: points }),
1430
+ /* @__PURE__ */ jsx(
1431
+ Layer,
1432
+ {
1433
+ id: "viewpoint-point",
1434
+ type: "circle",
1435
+ source: SOURCE_POINTS,
1436
+ paint: {
1437
+ "circle-radius": 5,
1438
+ "circle-color": "#ffffff",
1439
+ "circle-stroke-color": color,
1440
+ "circle-stroke-width": 2.5
1441
+ }
1442
+ }
1443
+ )
1444
+ ] });
1445
+ };
1446
+ var viewDirectionKeyFromFeatures = (features) => {
1447
+ const hit = features?.find((feature) => feature.layer?.id === VIEWPOINT_DIRECTION_LAYER_ID);
1448
+ const key = hit?.properties?.key;
1449
+ return typeof key === "string" ? key : null;
1450
+ };
1451
+ var samePhoto = (a, b) => a != null && a.providerId === b.providerId && a.photoId === b.photoId;
1452
+ var useViewpointSessionStore = create()((set, get) => ({
1453
+ sessionId: 0,
1454
+ viewpoints: [],
1455
+ line: null,
1456
+ activeDirectionKey: null,
1457
+ history: [],
1458
+ historyIndex: -1,
1459
+ actions: {
1460
+ open: ({ viewpoints, line = null }) => set((state) => ({
1461
+ sessionId: state.sessionId + 1,
1462
+ viewpoints,
1463
+ line,
1464
+ activeDirectionKey: null
1465
+ })),
1466
+ showPhoto: (entry) => set((state) => {
1467
+ const current = state.history[state.historyIndex];
1468
+ if (samePhoto(current?.photo, entry.photo)) {
1469
+ if (entry.directionKey == null || entry.directionKey === current?.directionKey) {
1470
+ return entry.directionKey != null && entry.directionKey !== state.activeDirectionKey ? { activeDirectionKey: entry.directionKey } : state;
1471
+ }
1472
+ const history2 = [...state.history];
1473
+ history2[state.historyIndex] = { ...entry };
1474
+ return { history: history2, activeDirectionKey: entry.directionKey };
1475
+ }
1476
+ const history = [...state.history.slice(0, state.historyIndex + 1), entry].slice(
1477
+ -100
1478
+ );
1479
+ return {
1480
+ history,
1481
+ historyIndex: history.length - 1,
1482
+ activeDirectionKey: entry.directionKey ?? state.activeDirectionKey
1483
+ };
1484
+ }),
1485
+ back: () => {
1486
+ const { history, historyIndex } = get();
1487
+ const entry = history[historyIndex - 1];
1488
+ if (!entry) {
1489
+ return null;
1490
+ }
1491
+ set({ historyIndex: historyIndex - 1, activeDirectionKey: entry.directionKey });
1492
+ return entry;
1493
+ },
1494
+ forward: () => {
1495
+ const { history, historyIndex } = get();
1496
+ const entry = history[historyIndex + 1];
1497
+ if (!entry) {
1498
+ return null;
1499
+ }
1500
+ set({ historyIndex: historyIndex + 1, activeDirectionKey: entry.directionKey });
1501
+ return entry;
1502
+ },
1503
+ close: () => set({ viewpoints: [], line: null, activeDirectionKey: null }),
1504
+ reset: () => set({
1505
+ viewpoints: [],
1506
+ line: null,
1507
+ activeDirectionKey: null,
1508
+ history: [],
1509
+ historyIndex: -1
1510
+ })
1511
+ }
1512
+ }));
1513
+ var useViewpoints = () => useViewpointSessionStore((state) => state.viewpoints);
1514
+ var useViewpointLine = () => useViewpointSessionStore((state) => state.line);
1515
+ var useActiveDirectionKey = () => useViewpointSessionStore((state) => state.activeDirectionKey);
1516
+ var useCurrentHistoryEntry = () => useViewpointSessionStore((state) => state.history[state.historyIndex] ?? null);
1517
+ var useCanGoBack = () => useViewpointSessionStore((state) => state.historyIndex > 0);
1518
+ var useCanGoForward = () => useViewpointSessionStore((state) => state.historyIndex < state.history.length - 1);
1519
+ var getViewpointSession = () => {
1520
+ const { actions, history, historyIndex } = useViewpointSessionStore.getState();
1521
+ return { actions, current: history[historyIndex] ?? null };
1522
+ };
1523
+ var DEFAULT_SOURCES = [mapillaryPhotoSource];
1524
+ var useViewSuggestions = (viewpoints, {
1525
+ maxAgeYears,
1526
+ photoTypes,
1527
+ limit,
1528
+ filterPhoto,
1529
+ sources = DEFAULT_SOURCES,
1530
+ enabled = true
1531
+ } = {}) => {
1532
+ const [now] = useState(() => Date.now());
1533
+ const requests = viewpoints.flatMap(
1534
+ (viewpoint) => sources.map((source) => ({ viewpoint, source }))
1535
+ );
1536
+ const queries = useQueries({
1537
+ queries: requests.map(({ viewpoint, source }) => ({
1538
+ queryKey: [
1539
+ "street-imagery",
1540
+ "photos-near",
1541
+ source.id,
1542
+ viewpoint.lngLat[0],
1543
+ viewpoint.lngLat[1]
1544
+ ],
1545
+ queryFn: ({ signal }) => source.fetchNear(viewpoint.lngLat, signal),
1546
+ enabled,
1547
+ staleTime: 10 * 60 * 1e3
1548
+ }))
1549
+ });
1550
+ const photosByViewpointId = /* @__PURE__ */ new Map();
1551
+ requests.forEach(({ viewpoint }, index) => {
1552
+ const photos = queries[index]?.data ?? [];
1553
+ const kept = filterPhoto ? photos.filter(filterPhoto) : photos;
1554
+ photosByViewpointId.set(viewpoint.id, [
1555
+ ...photosByViewpointId.get(viewpoint.id) ?? [],
1556
+ ...kept
1557
+ ]);
1558
+ });
1559
+ const suggestions = buildViewSuggestions(
1560
+ viewpoints,
1561
+ (viewpoint) => photosByViewpointId.get(viewpoint.id) ?? [],
1562
+ {
1563
+ now,
1564
+ maxAgeMs: maxAgeYears == null ? void 0 : yearsToMs(maxAgeYears),
1565
+ photoTypes,
1566
+ limit
1567
+ }
1568
+ );
1569
+ return {
1570
+ suggestions,
1571
+ isLoading: enabled && queries.some((query) => query.isPending),
1572
+ // An error only when every source failed; one failing provider still leaves suggestions.
1573
+ isError: queries.length > 0 && queries.every((query) => query.isError)
1574
+ };
1575
+ };
1576
+ var STALE_TIME = 10 * 60 * 1e3;
1577
+ var useMapillaryMapFeatureImages = (featureId, { shape = SIGN_TARGET } = {}) => useQuery({
1578
+ queryKey: ["street-imagery", "mapillary-map-feature", featureId, shape],
1579
+ queryFn: ({ signal }) => fetchMapFeatureImages(featureId, { shape, signal }),
1580
+ enabled: !!featureId,
1581
+ staleTime: STALE_TIME
1582
+ });
1583
+ var useMapillaryMapFeaturesNear = (lngLat, {
1584
+ radiusMeters = 40,
1585
+ objectValues,
1586
+ enabled = true
1587
+ } = {}) => useQuery({
1588
+ queryKey: ["street-imagery", "mapillary-map-features-near", lngLat, radiusMeters, objectValues],
1589
+ queryFn: async ({ signal }) => {
1590
+ const center = lngLat;
1591
+ const features = await fetchMapillaryMapFeaturesInBbox(bboxAround(center, radiusMeters), {
1592
+ objectValues,
1593
+ signal
1594
+ });
1595
+ return features.map((feature) => ({ ...feature, distanceMeters: distanceMeters(center, feature.lngLat) })).filter((feature) => feature.distanceMeters <= radiusMeters).sort((a, b) => a.distanceMeters - b.distanceMeters);
1596
+ },
1597
+ enabled: enabled && !!lngLat,
1598
+ staleTime: STALE_TIME
1599
+ });
1600
+ var useMapillaryImageDetections = (imageId, { filter, filterKey } = {}) => useQuery({
1601
+ queryKey: ["street-imagery", "mapillary-detections", imageId, filterKey ?? null],
1602
+ queryFn: ({ signal }) => fetchImageDetections(imageId, { filter, signal }),
1603
+ enabled: !!imageId,
1604
+ staleTime: STALE_TIME
1605
+ });
1606
+ var useSelectedMapillaryFeature = ({
1607
+ featureId,
1608
+ shownPhotoId,
1609
+ minCapturedAt,
1610
+ shape
1611
+ }) => {
1612
+ const { data, isLoading, isError } = useMapillaryMapFeatureImages(featureId, { shape });
1613
+ const shownImage = data?.images.find((image) => image.id === shownPhotoId) ?? null;
1614
+ const firstImage = data ? bestTargetImage(data.images, data.feature.lngLat, {
1615
+ minCapturedAt: minCapturedAt ?? void 0,
1616
+ shape
1617
+ }) ?? null : null;
1618
+ return { data: data ?? null, isLoading, isError, shownImage, firstImage };
1619
+ };
1620
+ var useLocationPickStore = create()((set) => ({
1621
+ armedOpenerId: null,
1622
+ actions: {
1623
+ arm: (openerId) => set({ armedOpenerId: openerId }),
1624
+ disarm: () => set((state) => state.armedOpenerId == null ? state : { armedOpenerId: null }),
1625
+ toggle: (openerId) => set((state) => ({ armedOpenerId: state.armedOpenerId === openerId ? null : openerId }))
1626
+ }
1627
+ }));
1628
+ var useArmedLocationOpenerId = () => useLocationPickStore((state) => state.armedOpenerId);
1629
+ var useLocationPickActions = () => useLocationPickStore((state) => state.actions);
1630
+
1631
+ // src/openers/LocationPickOnMap.tsx
1632
+ var roundCoordinate = (value) => Number(value.toFixed(7));
1633
+ var LocationPickOnMap = ({ onPick }) => {
1634
+ const { current: map } = useMap();
1635
+ const armedOpenerId = useArmedLocationOpenerId();
1636
+ const { disarm } = useLocationPickActions();
1637
+ useEffect(
1638
+ function openArmedOpenerOnNextMapClick() {
1639
+ if (!map || !armedOpenerId) {
1640
+ return;
1641
+ }
1642
+ const opener = findLocationOpener(armedOpenerId);
1643
+ if (!opener) {
1644
+ return;
1645
+ }
1646
+ const onClick = (event) => {
1647
+ const target = {
1648
+ lngLat: [roundCoordinate(event.lngLat.lng), roundCoordinate(event.lngLat.lat)],
1649
+ zoom: map.getZoom()
1650
+ };
1651
+ openLocationInNewTab(opener, target);
1652
+ disarm();
1653
+ onPick?.(target, opener);
1654
+ };
1655
+ const onKeyDown = (event) => {
1656
+ if (event.key === "Escape") {
1657
+ disarm();
1658
+ }
1659
+ };
1660
+ map.on("click", onClick);
1661
+ window.addEventListener("keydown", onKeyDown);
1662
+ return function stopListeningForPick() {
1663
+ map.off("click", onClick);
1664
+ window.removeEventListener("keydown", onKeyDown);
1665
+ };
1666
+ },
1667
+ [armedOpenerId, disarm, map, onPick]
1668
+ );
1669
+ return null;
1670
+ };
1671
+ var SelectedMapFeatureLayer = ({
1672
+ data,
1673
+ shownImage,
1674
+ providers,
1675
+ bbox,
1676
+ zoom,
1677
+ color = MAP_FEATURE_COLOR
1678
+ }) => {
1679
+ const drawnFeatures = useAllProviderMapFeatures(providers, bbox, zoom);
1680
+ const target = data ? drawnFeatures.find((feature) => feature.featureId === data.feature.id)?.lngLat ?? data.feature.lngLat : null;
1681
+ const camera = shownImage?.lngLat;
1682
+ const points = {
1683
+ type: "FeatureCollection",
1684
+ features: target ? [{ type: "Feature", properties: {}, geometry: { type: "Point", coordinates: target } }] : []
1685
+ };
1686
+ const line = {
1687
+ type: "FeatureCollection",
1688
+ features: target && camera ? [
1689
+ {
1690
+ type: "Feature",
1691
+ properties: {},
1692
+ geometry: { type: "LineString", coordinates: [camera, target] }
1693
+ }
1694
+ ] : []
1695
+ };
1696
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1697
+ /* @__PURE__ */ jsx(Source, { data: line, id: "selected-map-feature-line", type: "geojson" }),
1698
+ /* @__PURE__ */ jsx(
1699
+ Layer,
1700
+ {
1701
+ id: "selected-map-feature-line",
1702
+ paint: { "line-color": SELECTION_COLOR, "line-width": 1.5, "line-dasharray": [2, 1.5] },
1703
+ source: "selected-map-feature-line",
1704
+ type: "line"
1705
+ }
1706
+ ),
1707
+ /* @__PURE__ */ jsx(Source, { data: points, id: "selected-map-feature-point", type: "geojson" }),
1708
+ /* @__PURE__ */ jsx(
1709
+ Layer,
1710
+ {
1711
+ id: "selected-map-feature-disk",
1712
+ paint: {
1713
+ "circle-radius": 17,
1714
+ "circle-color": color,
1715
+ "circle-opacity": VIEW_SHAPE_FILL_OPACITY
1716
+ },
1717
+ source: "selected-map-feature-point",
1718
+ type: "circle"
1719
+ }
1720
+ ),
1721
+ /* @__PURE__ */ jsx(
1722
+ Layer,
1723
+ {
1724
+ id: "selected-map-feature-dot",
1725
+ paint: {
1726
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 1.8, 14, 3.6, 18, 4.8],
1727
+ "circle-color": color
1728
+ },
1729
+ source: "selected-map-feature-point",
1730
+ type: "circle"
1731
+ }
1732
+ )
1733
+ ] });
1734
+ };
1735
+ var MAPILLARY_FEATURE_BAR_LABELS = {
1736
+ en: {
1737
+ name: (value) => mapillaryValueName(value, "en"),
1738
+ seen: "Seen",
1739
+ noPhotos: "Mapillary lists no photos for this feature.",
1740
+ daysSummary: (photos, days) => `${photos} photos on ${days} days`,
1741
+ dayPhotos: (photos, canStep) => `${photos} photo${photos === 1 ? "" : "s"}${canStep ? " \u2014 click for the next one" : ""}`
1742
+ },
1743
+ de: {
1744
+ name: (value) => mapillaryValueName(value, "de"),
1745
+ seen: "Gesehen",
1746
+ noPhotos: "Mapillary f\xFChrt keine Fotos zu diesem Objekt.",
1747
+ daysSummary: (photos, days) => `${photos} Fotos an ${days} Tagen`,
1748
+ dayPhotos: (photos, canStep) => `${photos} Foto${photos === 1 ? "" : "s"}${canStep ? " \u2014 klicken f\xFCr das n\xE4chste" : ""}`
1749
+ }
1750
+ };
1751
+ var MapillaryFeatureBar = ({
1752
+ data,
1753
+ shownImage,
1754
+ onShow,
1755
+ labels: labelOverrides
1756
+ }) => {
1757
+ const { locale } = useStreetImageryI18n();
1758
+ const labels = { ...MAPILLARY_FEATURE_BAR_LABELS[locale], ...labelOverrides };
1759
+ const { feature, days, images } = data;
1760
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1761
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", title: feature.value, children: [
1762
+ /* @__PURE__ */ jsx(
1763
+ "img",
1764
+ {
1765
+ alt: "",
1766
+ className: "size-7 shrink-0",
1767
+ onError: (event) => {
1768
+ event.currentTarget.style.display = "none";
1769
+ },
1770
+ src: mapillaryIconUrl(feature.value)
1771
+ },
1772
+ feature.value
1773
+ ),
1774
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1775
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm leading-tight font-medium text-slate-900", children: labels.name(feature.value) }),
1776
+ /* @__PURE__ */ jsxs("p", { className: "truncate text-xs leading-tight text-slate-500", children: [
1777
+ labels.seen,
1778
+ " ",
1779
+ /* @__PURE__ */ jsx(PhotoDate, { timestamp: feature.firstSeenAt }),
1780
+ " \u2013",
1781
+ " ",
1782
+ /* @__PURE__ */ jsx(PhotoDate, { timestamp: feature.lastSeenAt })
1783
+ ] })
1784
+ ] })
1785
+ ] }),
1786
+ days.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500", children: labels.noPhotos }) : /* @__PURE__ */ jsx(
1787
+ "div",
1788
+ {
1789
+ "aria-label": labels.daysSummary(images.length, days.length),
1790
+ className: "flex gap-1 overflow-x-auto",
1791
+ role: "group",
1792
+ children: days.map((day) => {
1793
+ const noon = /* @__PURE__ */ new Date(`${day.day}T12:00:00Z`);
1794
+ const shownIndex = shownImage ? day.images.findIndex((image) => image.id === shownImage.id) : -1;
1795
+ const active = shownIndex >= 0;
1796
+ return /* @__PURE__ */ jsxs(
1797
+ "button",
1798
+ {
1799
+ "aria-pressed": active,
1800
+ 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"}`,
1801
+ onClick: () => {
1802
+ const next = active ? day.images[(shownIndex + 1) % day.images.length] : day.best;
1803
+ if (next) {
1804
+ onShow(next);
1805
+ }
1806
+ },
1807
+ title: `${formatDateTooltip(noon, locale)}
1808
+ ${labels.dayPhotos(day.images.length, active && day.images.length > 1)}`,
1809
+ type: "button",
1810
+ children: [
1811
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: formatMonth(noon, locale) }),
1812
+ " ",
1813
+ /* @__PURE__ */ jsx(
1814
+ PhotoCount,
1815
+ {
1816
+ className: `align-[-1px] ${active ? "text-amber-800" : "text-slate-500"}`,
1817
+ index: active ? shownIndex + 1 : void 0,
1818
+ total: day.images.length
1819
+ }
1820
+ )
1821
+ ]
1822
+ },
1823
+ day.day
1824
+ );
1825
+ })
1826
+ }
1827
+ )
1828
+ ] });
1829
+ };
1830
+ var DEFAULT_YEAR_LINES = [1, 2, 3, 4];
1831
+ var BINS = 60;
1832
+ var STEP = 1e-3;
1833
+ var CHART_HEIGHT_PX = 36;
1834
+ var RED = "#dc2626";
1835
+ var HANDLE_CLASS = "pointer-events-none absolute inset-0 m-0 h-full w-full cursor-pointer appearance-none bg-transparent focus:outline-none [&::-webkit-slider-runnable-track]:appearance-none [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-slate-800 [&::-webkit-slider-thumb]:shadow [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-slate-800 [&::-moz-range-thumb]:shadow focus-visible:[&::-webkit-slider-thumb]:ring-2 focus-visible:[&::-webkit-slider-thumb]:ring-sky-500 focus-visible:[&::-moz-range-thumb]:ring-2 focus-visible:[&::-moz-range-thumb]:ring-sky-500";
1836
+ var percent = (position) => `${position * 100}%`;
1837
+ var PhotoDateRangeFilter = ({
1838
+ value,
1839
+ onChange,
1840
+ capturedAt,
1841
+ yearLines = DEFAULT_YEAR_LINES,
1842
+ recommendedMaxAgeYears,
1843
+ markers = [],
1844
+ maxYears,
1845
+ now: nowProp,
1846
+ dateInputs = "collapsed",
1847
+ className,
1848
+ allDatesAction = true
1849
+ }) => {
1850
+ const { locale, messages } = useStreetImageryI18n();
1851
+ const text = messages.dateFilter;
1852
+ const [startNow] = useState(() => Date.now());
1853
+ const now = nowProp ?? startNow;
1854
+ const scale = { now, maxYears };
1855
+ const [dragged, setDragged] = useState(null);
1856
+ const fromInput = useRef(null);
1857
+ const toInput = useRef(null);
1858
+ const positions = dragged ?? dateRangeToSliderPositions(value, scale);
1859
+ const shownRange = dragged ? sliderPositionsToDateRange(dragged, scale) : value;
1860
+ const readPositions = (moved) => {
1861
+ const from = Number(fromInput.current?.value ?? positions.from);
1862
+ const to = Number(toInput.current?.value ?? positions.to);
1863
+ return moved === "from" ? { from: Math.min(from, to), to } : { from, to: Math.max(from, to) };
1864
+ };
1865
+ const commitOnRelease = (moved) => (input) => {
1866
+ if (!input) {
1867
+ return void 0;
1868
+ }
1869
+ const commit = () => {
1870
+ setDragged(null);
1871
+ const next = sliderPositionsToDateRange(readPositions(moved), scale);
1872
+ const other = moved === "from" ? "to" : "from";
1873
+ onChange({ ...next, [other]: value[other] });
1874
+ };
1875
+ input.addEventListener("change", commit);
1876
+ return () => {
1877
+ input.removeEventListener("change", commit);
1878
+ };
1879
+ };
1880
+ const trackDrag = (moved, event) => {
1881
+ if (event.type === "input") {
1882
+ setDragged(readPositions(moved));
1883
+ }
1884
+ };
1885
+ const bins = capturedAt ? dateSliderBins(capturedAt, scale, BINS) : [];
1886
+ const maxCount = Math.max(1, ...bins);
1887
+ const years = [
1888
+ ...new Set(recommendedMaxAgeYears ? [...yearLines, recommendedMaxAgeYears] : yearLines)
1889
+ ].map((count) => ({ count, position: dateSliderPosition(yearsAgoMs(count, now), scale) })).filter(({ position }) => position > 0 && position < 1);
1890
+ const shownMarkers = markers.map((marker) => {
1891
+ const time = typeof marker.date === "number" ? marker.date : parseIsoDateStartMs(marker.date);
1892
+ return { ...marker, position: dateSliderPosition(time, scale) };
1893
+ }).filter(({ position }) => position > 0 && position < 1);
1894
+ const day = (isoDate) => formatDate(parseIsoDateStartMs(isoDate), locale);
1895
+ const rangeLabel = shownRange.from && shownRange.to ? text.between(day(shownRange.from), day(shownRange.to)) : shownRange.from ? text.since(day(shownRange.from)) : shownRange.to ? text.until(day(shownRange.to)) : text.allDates;
1896
+ const setYears = (count) => {
1897
+ onChange({ from: new Date(yearsAgoMs(count, now)).toISOString().slice(0, 10) });
1898
+ };
1899
+ const setDay = (key, input) => {
1900
+ if (input === "") {
1901
+ onChange({ ...value, [key]: void 0 });
1902
+ } else if (isIsoDate(input)) {
1903
+ onChange({ ...value, [key]: input });
1904
+ }
1905
+ };
1906
+ const isAllDates = !shownRange.from && !shownRange.to;
1907
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1908
+ /* @__PURE__ */ jsx("div", { className: "mx-2", children: /* @__PURE__ */ jsxs(
1909
+ "div",
1910
+ {
1911
+ className: "relative",
1912
+ style: { height: CHART_HEIGHT_PX, marginTop: shownMarkers.length > 0 ? 14 : 0 },
1913
+ children: [
1914
+ /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "absolute inset-0 flex items-end", children: bins.map((count, index) => {
1915
+ const start = index / BINS;
1916
+ const inRange = start + 1 / BINS > positions.from && start < positions.to;
1917
+ return /* @__PURE__ */ jsx(
1918
+ "div",
1919
+ {
1920
+ className: `flex-1 rounded-t-[1px] ${inRange ? "bg-slate-500" : "bg-slate-300"}`,
1921
+ style: {
1922
+ height: count === 0 ? 0 : Math.max(2, Math.log1p(count) / Math.log1p(maxCount) * CHART_HEIGHT_PX)
1923
+ },
1924
+ title: count > 0 ? text.photoCount(count) : void 0
1925
+ },
1926
+ index
1927
+ );
1928
+ }) }),
1929
+ years.map(({ count, position }) => /* @__PURE__ */ jsx(
1930
+ "div",
1931
+ {
1932
+ "aria-hidden": true,
1933
+ className: "pointer-events-none absolute inset-y-0 w-px -translate-x-1/2",
1934
+ style: {
1935
+ left: percent(position),
1936
+ backgroundColor: count === recommendedMaxAgeYears ? RED : "rgb(148 163 184 / 0.7)"
1937
+ }
1938
+ },
1939
+ count
1940
+ )),
1941
+ shownMarkers.map((marker) => /* @__PURE__ */ jsx(
1942
+ "div",
1943
+ {
1944
+ className: "pointer-events-none absolute inset-y-0 w-0 border-l-2 border-dashed",
1945
+ style: { left: percent(marker.position), borderColor: marker.color ?? RED },
1946
+ children: /* @__PURE__ */ jsx(
1947
+ "span",
1948
+ {
1949
+ className: "absolute -top-3.5 -translate-x-1/2 text-[10px] leading-none whitespace-nowrap",
1950
+ style: { color: marker.color ?? RED },
1951
+ children: marker.label
1952
+ }
1953
+ )
1954
+ },
1955
+ `${marker.date}-${marker.label}`
1956
+ ))
1957
+ ]
1958
+ }
1959
+ ) }),
1960
+ /* @__PURE__ */ jsxs("div", { className: "relative h-4", children: [
1961
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-2 top-1/2 h-1 -translate-y-1/2 rounded-full bg-slate-200", children: /* @__PURE__ */ jsx(
1962
+ "div",
1963
+ {
1964
+ className: "absolute inset-y-0 rounded-full bg-slate-700",
1965
+ style: {
1966
+ left: percent(positions.from),
1967
+ right: percent(1 - positions.to)
1968
+ }
1969
+ }
1970
+ ) }),
1971
+ /* @__PURE__ */ jsx(
1972
+ "input",
1973
+ {
1974
+ "aria-label": text.fromHandle,
1975
+ "aria-valuetext": shownRange.from ? day(shownRange.from) : text.allDates,
1976
+ className: HANDLE_CLASS,
1977
+ max: 1,
1978
+ min: 0,
1979
+ onChange: (event) => {
1980
+ trackDrag("from", event.nativeEvent);
1981
+ },
1982
+ ref: (input) => {
1983
+ fromInput.current = input;
1984
+ return commitOnRelease("from")(input);
1985
+ },
1986
+ step: STEP,
1987
+ type: "range",
1988
+ value: positions.from
1989
+ }
1990
+ ),
1991
+ /* @__PURE__ */ jsx(
1992
+ "input",
1993
+ {
1994
+ "aria-label": text.toHandle,
1995
+ "aria-valuetext": shownRange.to ? day(shownRange.to) : text.allDates,
1996
+ className: HANDLE_CLASS,
1997
+ max: 1,
1998
+ min: 0,
1999
+ onChange: (event) => {
2000
+ trackDrag("to", event.nativeEvent);
2001
+ },
2002
+ ref: (input) => {
2003
+ toInput.current = input;
2004
+ return commitOnRelease("to")(input);
2005
+ },
2006
+ step: STEP,
2007
+ type: "range",
2008
+ value: positions.to
2009
+ }
2010
+ )
2011
+ ] }),
2012
+ /* @__PURE__ */ jsx("div", { className: "relative mx-2 h-4", children: years.map(({ count, position }) => /* @__PURE__ */ jsx(
2013
+ "button",
2014
+ {
2015
+ className: `absolute top-0 -translate-x-1/2 rounded px-0.5 text-[10px] leading-4 whitespace-nowrap hover:underline ${count === recommendedMaxAgeYears ? "font-semibold text-red-600" : "text-slate-500 hover:text-slate-900"}`,
2016
+ onClick: () => {
2017
+ setYears(count);
2018
+ },
2019
+ style: { left: percent(position) },
2020
+ title: count === recommendedMaxAgeYears ? text.recommendedMaxAge(count) : text.lastYears(count),
2021
+ type: "button",
2022
+ children: text.yearsShort(count)
2023
+ },
2024
+ count
2025
+ )) }),
2026
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-baseline justify-between gap-2 text-xs", children: [
2027
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-700", children: rangeLabel }),
2028
+ isAllDates || !allDatesAction ? null : /* @__PURE__ */ jsx(
2029
+ "button",
2030
+ {
2031
+ className: "shrink-0 font-medium text-slate-600 underline decoration-slate-300 underline-offset-2 hover:text-slate-900",
2032
+ onClick: () => {
2033
+ onChange({});
2034
+ },
2035
+ type: "button",
2036
+ children: text.allDates
2037
+ }
2038
+ )
2039
+ ] }),
2040
+ dateInputs === "none" ? null : /* @__PURE__ */ jsxs("details", { className: "mt-1 text-xs text-slate-600", open: dateInputs === "open" || void 0, children: [
2041
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer select-none hover:text-slate-900", children: text.exactDates }),
2042
+ /* @__PURE__ */ jsx("div", { className: "mt-1.5 grid grid-cols-2 gap-2", children: ["from", "to"].map((key) => /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1", children: [
2043
+ text[key],
2044
+ /* @__PURE__ */ jsx(
2045
+ "input",
2046
+ {
2047
+ className: "rounded-md border border-slate-200 px-2 py-1.5 text-sm text-slate-800",
2048
+ onChange: (event) => {
2049
+ setDay(key, event.target.value);
2050
+ },
2051
+ type: "date",
2052
+ value: value[key] ?? ""
2053
+ }
2054
+ )
2055
+ ] }, key)) })
2056
+ ] })
2057
+ ] });
2058
+ };
658
2059
 
659
- export { StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useAllProviderMapFeatures, useAllProviderMapFeaturesLoading, useAllProviderPhotos, useMapViewportBbox, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences };
2060
+ export { BASE_COLOR, FloatingPhotoViewer, FloatingViewerInfoButton, LocationPickOnMap, MAPILLARY_FEATURE_BAR_LABELS, MapillaryFeatureBar, PhotoCount, PhotoDate, PhotoDateRangeFilter, SELECTION_COLOR, SelectedMapFeatureLayer, 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, useSelectedMapillaryFeature, useViewSuggestions, useViewpointLine, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel };