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

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