@osm-editor-kit/street-imagery-react 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (36) hide show
  1. package/README.md +45 -0
  2. package/dist/MapillaryPanel-N45NQY4C.js +2 -0
  3. package/dist/PanoramaxPanel-MFI4L67Z.js +2 -0
  4. package/dist/StreetLevelImagerySelectionOverlay.d.ts +20 -3
  5. package/dist/StreetLevelImagerySourcesAndLayers.d.ts +9 -1
  6. package/dist/StreetLevelImageryViewCone.d.ts +11 -1
  7. package/dist/StreetLevelImageryViewer.d.ts +7 -2
  8. package/dist/{chunk-72HJV6II.js → chunk-53RA4OSJ.js} +6 -2
  9. package/dist/chunk-EE7V3OIZ.js +116 -0
  10. package/dist/chunk-EQ2JKLGT.js +311 -0
  11. package/dist/hooks/useAllProviderMapFeatures.d.ts +0 -4
  12. package/dist/i18n/PhotoCount.d.ts +12 -0
  13. package/dist/i18n/PhotoDate.d.ts +16 -0
  14. package/dist/i18n/StreetImageryLocaleProvider.d.ts +21 -0
  15. package/dist/i18n/messages.d.ts +27 -0
  16. package/dist/index.d.ts +12 -4
  17. package/dist/index.js +593 -182
  18. package/dist/mapillary/MapillaryFeatureBar.d.ts +16 -0
  19. package/dist/mapillary/featureBarLabels.d.ts +16 -0
  20. package/dist/openers/LocationPickOnMap.d.ts +10 -0
  21. package/dist/openers/useLocationPickStore.d.ts +9 -0
  22. package/dist/panels/MapillaryPanel.d.ts +7 -1
  23. package/dist/panels/PanoramaxPanel.d.ts +8 -1
  24. package/dist/panels/panoramaxLimitPanning.d.ts +54 -0
  25. package/dist/panels/panoramaxPhotoFromMetadata.d.ts +8 -0
  26. package/dist/photo-details/PhotoDetailsDialog.d.ts +21 -0
  27. package/dist/photo-details/index.d.ts +6 -0
  28. package/dist/photo-details/labels.d.ts +33 -0
  29. package/dist/photo-details.js +235 -0
  30. package/dist/viewpoints/FloatingPhotoViewer.d.ts +33 -4
  31. package/dist/viewpoints/useViewpointSessionStore.d.ts +6 -10
  32. package/dist/viewpoints/viewDirectionLabels.d.ts +4 -4
  33. package/package.json +11 -2
  34. package/dist/MapillaryPanel-I5GKT6YT.js +0 -2
  35. package/dist/PanoramaxPanel-J6ANR6LB.js +0 -2
  36. package/dist/chunk-ZH3TPW4D.js +0 -165
package/dist/index.js CHANGED
@@ -1,67 +1,127 @@
1
- export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo } from './chunk-72HJV6II.js';
2
- export { PanoramaxPanel } from './chunk-ZH3TPW4D.js';
1
+ import { useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
2
+ export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
3
+ export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo } from './chunk-53RA4OSJ.js';
4
+ export { PanoramaxPanel } from './chunk-EQ2JKLGT.js';
3
5
  export { useViewerActions, useViewerBearing, useViewerHfov, useViewerLngLat } from './chunk-25UGGXIE.js';
4
- import { photosToFeatureCollection, emptyPointCollection, sequencesToFeatureCollection, emptyLineCollection, adapterById, viewConeGeoJson, coneRadiusMeters, setStreetImageryConfig, photoLayerId, viewfieldLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, distanceMeters, fetchImageDetections, mapFeaturesToFeatureCollection, buildMapFeatureLayerFilter, featureSourceId, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
6
+ import { distanceMeters, sequencesToFeatureCollection, emptyLineCollection, adapterById, providerById, providerCoversBbox, viewConeGeoJson, viewShapeRadiusMeters, setStreetImageryConfig, viewfieldLayerId, photoLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, formatLocalDateTime, formatMonth, formatRelativeDate, formatDateTooltip, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, fetchImageDetections, findLocationOpener, mapillaryValueName, mapillaryIconUrl, openLocationInNewTab, alignLineToPoints, coneRadiusMeters, mapFeaturesToFeatureCollection, emptyPointCollection, buildMapFeatureLayerFilter, featureSourceId, photosToFeatureCollection, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
5
7
  import { Source, Layer, useMap } from 'react-map-gl/maplibre';
6
8
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
7
9
  import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState, useRef, useEffectEvent } from 'react';
8
10
  import { useQuery, useQueries } from '@tanstack/react-query';
9
11
  import { create } from 'zustand';
10
12
 
11
- var HIGHLIGHT_SOURCE_ID = "selection-highlight";
12
- var HIGHLIGHT_LAYER_ID = "selection-highlight-layer";
13
+ var POINTS_SOURCE_ID = "selection-highlight";
14
+ var CONNECTOR_SOURCE_ID = "selection-connector";
13
15
  var SEQUENCE_HIGHLIGHT_SOURCE_ID = "sequence-highlight";
14
- var SEQUENCE_HIGHLIGHT_LAYER_ID = "sequence-highlight-layer";
16
+ var BASE_COLOR = "#171717";
17
+ var SELECTION_COLOR = "#f97316";
18
+ var SAME_POSITION_METERS = 0.75;
19
+ var ACTIVE_LINE_WIDTH = 2.5;
15
20
  var StreetLevelImagerySelectionOverlay = ({
16
21
  selectedPhoto,
17
- selectedSequence
22
+ selectedSequence,
23
+ cameraLngLat,
24
+ sequenceBeforeId
18
25
  }) => {
19
- const highlightCollection = selectedPhoto ? photosToFeatureCollection([selectedPhoto]) : emptyPointCollection();
20
- const sequenceHighlightCollection = selectedSequence ? sequencesToFeatureCollection([selectedSequence]) : emptyLineCollection();
21
- if (!selectedPhoto && sequenceHighlightCollection.features.length === 0) {
26
+ const photoLngLat = selectedPhoto?.lngLat;
27
+ const cameraDiffers = photoLngLat != null && cameraLngLat != null && distanceMeters(photoLngLat, cameraLngLat) > SAME_POSITION_METERS;
28
+ const camera = cameraDiffers ? cameraLngLat : null;
29
+ const dot = photoLngLat && cameraLngLat && !cameraDiffers ? cameraLngLat : photoLngLat;
30
+ const sequenceLines = selectedSequence ? sequencesToFeatureCollection([selectedSequence]) : emptyLineCollection();
31
+ const throughDot = (line) => dot ? alignLineToPoints(line, [dot]) : line;
32
+ const sequenceCollection = {
33
+ ...sequenceLines,
34
+ features: sequenceLines.features.map((feature) => ({
35
+ ...feature,
36
+ geometry: feature.geometry.type === "LineString" ? { ...feature.geometry, coordinates: throughDot(feature.geometry.coordinates) } : { ...feature.geometry, coordinates: feature.geometry.coordinates.map(throughDot) }
37
+ }))
38
+ };
39
+ if (!selectedPhoto && sequenceCollection.features.length === 0) {
22
40
  return null;
23
41
  }
24
- return /* @__PURE__ */ jsxs(Fragment, { children: [
25
- selectedPhoto ? /* @__PURE__ */ jsxs(Fragment, { children: [
26
- /* @__PURE__ */ jsx(Source, { id: HIGHLIGHT_SOURCE_ID, type: "geojson", data: highlightCollection }),
27
- /* @__PURE__ */ jsx(
28
- Layer,
42
+ const points = {
43
+ type: "FeatureCollection",
44
+ features: [
45
+ ...dot ? [
29
46
  {
30
- id: HIGHLIGHT_LAYER_ID,
31
- type: "circle",
32
- source: HIGHLIGHT_SOURCE_ID,
33
- paint: {
34
- "circle-radius": 10,
35
- "circle-color": "#ffffff",
36
- "circle-stroke-width": 3,
37
- "circle-stroke-color": "#0f172a"
38
- }
47
+ type: "Feature",
48
+ properties: { kind: "dot" },
49
+ geometry: { type: "Point", coordinates: dot }
39
50
  }
40
- )
41
- ] }) : null,
42
- sequenceHighlightCollection.features.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
43
- /* @__PURE__ */ jsx(
44
- Source,
51
+ ] : [],
52
+ ...camera ? [
45
53
  {
46
- id: SEQUENCE_HIGHLIGHT_SOURCE_ID,
47
- type: "geojson",
48
- data: sequenceHighlightCollection
54
+ type: "Feature",
55
+ properties: { kind: "camera" },
56
+ geometry: { type: "Point", coordinates: camera }
49
57
  }
50
- ),
51
- /* @__PURE__ */ jsx(
52
- Layer,
53
- {
54
- id: SEQUENCE_HIGHLIGHT_LAYER_ID,
55
- type: "line",
56
- source: SEQUENCE_HIGHLIGHT_SOURCE_ID,
57
- paint: {
58
- "line-color": "#0f172a",
59
- "line-width": 4,
60
- "line-opacity": 0.75
61
- }
58
+ ] : []
59
+ ]
60
+ };
61
+ const connector = {
62
+ type: "FeatureCollection",
63
+ features: dot && camera ? [
64
+ {
65
+ type: "Feature",
66
+ properties: {},
67
+ geometry: { type: "LineString", coordinates: [dot, camera] }
68
+ }
69
+ ] : []
70
+ };
71
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ /* @__PURE__ */ jsx(Source, { id: SEQUENCE_HIGHLIGHT_SOURCE_ID, type: "geojson", data: sequenceCollection }),
73
+ /* @__PURE__ */ jsx(
74
+ Layer,
75
+ {
76
+ beforeId: sequenceBeforeId,
77
+ id: "sequence-highlight-layer",
78
+ type: "line",
79
+ source: SEQUENCE_HIGHLIGHT_SOURCE_ID,
80
+ layout: { "line-cap": "round", "line-join": "round" },
81
+ paint: { "line-color": BASE_COLOR, "line-width": ACTIVE_LINE_WIDTH }
82
+ }
83
+ ),
84
+ /* @__PURE__ */ jsx(Source, { id: CONNECTOR_SOURCE_ID, type: "geojson", data: connector }),
85
+ /* @__PURE__ */ jsx(
86
+ Layer,
87
+ {
88
+ id: "selection-connector-layer",
89
+ type: "line",
90
+ source: CONNECTOR_SOURCE_ID,
91
+ paint: { "line-color": SELECTION_COLOR, "line-width": 1.5, "line-dasharray": [2, 1.5] }
92
+ }
93
+ ),
94
+ /* @__PURE__ */ jsx(Source, { id: POINTS_SOURCE_ID, type: "geojson", data: points }),
95
+ /* @__PURE__ */ jsx(
96
+ Layer,
97
+ {
98
+ id: "selection-highlight-layer",
99
+ type: "circle",
100
+ source: POINTS_SOURCE_ID,
101
+ filter: ["==", ["get", "kind"], "dot"],
102
+ paint: {
103
+ "circle-radius": 6,
104
+ "circle-color": SELECTION_COLOR,
105
+ "circle-stroke-width": 2,
106
+ "circle-stroke-color": BASE_COLOR
62
107
  }
63
- )
64
- ] }) : null
108
+ }
109
+ ),
110
+ /* @__PURE__ */ jsx(
111
+ Layer,
112
+ {
113
+ id: "selection-camera-layer",
114
+ type: "circle",
115
+ source: POINTS_SOURCE_ID,
116
+ filter: ["==", ["get", "kind"], "camera"],
117
+ paint: {
118
+ "circle-radius": 3.5,
119
+ "circle-color": SELECTION_COLOR,
120
+ "circle-stroke-width": 1.5,
121
+ "circle-stroke-color": BASE_COLOR
122
+ }
123
+ }
124
+ )
65
125
  ] });
66
126
  };
67
127
  var resolveSelectedSequence = (selectedPhoto, sequences, sequenceId) => {
@@ -79,7 +139,8 @@ var withinBbox = (bbox) => (item) => {
79
139
  };
80
140
  var useProviderPhotos = (providerId, bbox, zoom) => {
81
141
  const adapter = adapterById[providerId];
82
- const enabled = adapter.kind === "photo" && bbox != null && zoom >= adapter.minZoom && adapter.fetchPhotos != null;
142
+ const meta = providerById[providerId];
143
+ const enabled = meta.kind === "photo" && bbox != null && providerCoversBbox(providerId, bbox) && zoom >= meta.minZoom && adapter?.fetchPhotos != null;
83
144
  return useQuery({
84
145
  queryKey: ["provider-photos", providerId, bboxKey(bbox), zoom],
85
146
  queryFn: async ({ signal }) => {
@@ -92,18 +153,20 @@ var useProviderPhotos = (providerId, bbox, zoom) => {
92
153
  };
93
154
  var useProviderSequences = (providerId, bbox, zoom) => {
94
155
  const adapter = adapterById[providerId];
95
- const sequencesMinZoom = adapter.sequencesMinZoom ?? adapter.minZoom;
96
- const enabled = adapter.kind === "photo" && bbox != null && zoom >= sequencesMinZoom && adapter.fetchSequences != null;
156
+ const meta = providerById[providerId];
157
+ const sequencesMinZoom = meta.sequencesMinZoom;
158
+ const enabled = meta.kind === "photo" && bbox != null && providerCoversBbox(providerId, bbox) && zoom >= sequencesMinZoom && adapter?.fetchSequences != null;
97
159
  return useQuery({
98
160
  queryKey: ["provider-sequences", providerId, bboxKey(bbox), zoom],
99
- queryFn: ({ signal }) => adapter.fetchSequences?.(bbox, zoom, signal) ?? [],
161
+ queryFn: ({ signal }) => adapter?.fetchSequences?.(bbox, zoom, signal) ?? [],
100
162
  enabled,
101
163
  placeholderData: (previous) => previous
102
164
  });
103
165
  };
104
166
  var useProviderMapFeatures = (providerId, bbox, zoom) => {
105
167
  const adapter = adapterById[providerId];
106
- const enabled = adapter.kind === "mapFeature" && bbox != null && zoom >= adapter.minZoom && adapter.fetchMapFeatures != null;
168
+ const meta = providerById[providerId];
169
+ const enabled = meta.kind === "mapFeature" && bbox != null && providerCoversBbox(providerId, bbox) && zoom >= meta.minZoom && adapter?.fetchMapFeatures != null;
107
170
  return useQuery({
108
171
  queryKey: ["provider-map-features", providerId, bboxKey(bbox), zoom],
109
172
  queryFn: async ({ signal }) => {
@@ -116,7 +179,8 @@ var useProviderMapFeatures = (providerId, bbox, zoom) => {
116
179
  };
117
180
  var CONE_SOURCE_ID = "view-direction-cone";
118
181
  var CONE_FILL_LAYER_ID = "view-direction-cone-fill";
119
- var CONE_LINE_LAYER_ID = "view-direction-cone-line";
182
+ var MIN_CONE_FOV_DEG = 45;
183
+ var VIEW_SHAPE_FILL_OPACITY = 0.25;
120
184
  var INTERACTIVE_PANO_PROVIDERS = /* @__PURE__ */ new Set([
121
185
  "mapillary",
122
186
  "panoramax",
@@ -128,7 +192,9 @@ var INTERACTIVE_PANO_PROVIDERS = /* @__PURE__ */ new Set([
128
192
  var StreetLevelImageryViewCone = ({
129
193
  selectedPhoto,
130
194
  zoom,
131
- viewerPov
195
+ viewerPov,
196
+ color = SELECTION_COLOR,
197
+ scale = 2.5
132
198
  }) => {
133
199
  const apex = viewerPov?.lngLat ?? selectedPhoto.lngLat;
134
200
  const isPano = selectedPhoto.isPano === true;
@@ -148,7 +214,11 @@ var StreetLevelImageryViewCone = ({
148
214
  if (bearing == null) {
149
215
  return null;
150
216
  }
151
- const coneFeature = viewConeGeoJson(apex, bearing, fov, coneRadiusMeters(zoom));
217
+ fov = Math.max(fov, MIN_CONE_FOV_DEG);
218
+ const coneFeature = {
219
+ ...viewConeGeoJson(apex, bearing, fov, viewShapeRadiusMeters(zoom) * scale),
220
+ properties: { isPano: selectedPhoto.isPano, capturedAt: selectedPhoto.capturedAt }
221
+ };
152
222
  return /* @__PURE__ */ jsxs(Fragment, { children: [
153
223
  /* @__PURE__ */ jsx(Source, { id: CONE_SOURCE_ID, type: "geojson", data: coneFeature }),
154
224
  /* @__PURE__ */ jsx(
@@ -158,37 +228,14 @@ var StreetLevelImageryViewCone = ({
158
228
  type: "fill",
159
229
  source: CONE_SOURCE_ID,
160
230
  paint: {
161
- "fill-color": "#0f172a",
162
- "fill-opacity": 0.15
163
- }
164
- }
165
- ),
166
- /* @__PURE__ */ jsx(
167
- Layer,
168
- {
169
- id: CONE_LINE_LAYER_ID,
170
- type: "line",
171
- source: CONE_SOURCE_ID,
172
- paint: {
173
- "line-color": "#0f172a",
174
- "line-width": 1.5,
175
- "line-opacity": 0.5
231
+ "fill-color": color,
232
+ "fill-opacity": VIEW_SHAPE_FILL_OPACITY,
233
+ "fill-outline-color": "rgba(0, 0, 0, 0)"
176
234
  }
177
235
  }
178
236
  )
179
237
  ] });
180
238
  };
181
- var CIRCLE_RADIUS = [
182
- "interpolate",
183
- ["linear"],
184
- ["zoom"],
185
- 10,
186
- 2,
187
- 14,
188
- 4,
189
- 18,
190
- 6
191
- ];
192
239
  var FEATURE_CIRCLE_RADIUS = [
193
240
  "interpolate",
194
241
  ["linear"],
@@ -206,6 +253,24 @@ var padBbox = ([west, south, east, north], factor) => {
206
253
  return [west - dx, south - dy, east + dx, north + dy];
207
254
  };
208
255
  var lngLatInBbox = ([lng, lat], [west, south, east, north]) => lng >= west && lng <= east && lat >= south && lat <= north;
256
+ var YEAR_MS = 365.25 * 24 * 60 * 60 * 1e3;
257
+ var LOADED_AT = Date.now();
258
+ var AGE_SHADE = [
259
+ "case",
260
+ ["==", ["get", "capturedAt"], null],
261
+ BASE_COLOR,
262
+ [
263
+ "step",
264
+ ["get", "capturedAt"],
265
+ "#909090",
266
+ LOADED_AT - 3 * YEAR_MS,
267
+ "#686868",
268
+ LOADED_AT - 2 * YEAR_MS,
269
+ "#404040",
270
+ LOADED_AT - YEAR_MS,
271
+ BASE_COLOR
272
+ ]
273
+ ];
209
274
  var PHOTO_SORT_KEY = ["coalesce", ["get", "capturedAt"], 0];
210
275
  var FEATURE_SORT_KEY = ["coalesce", ["get", "lastSeenAt"], 0];
211
276
  var PhotoProviderLayer = ({
@@ -215,18 +280,57 @@ var PhotoProviderLayer = ({
215
280
  filter,
216
281
  showSequences,
217
282
  showViewfields,
218
- photoCircleColor
283
+ photoCircleColor,
284
+ activeSequenceId,
285
+ hasActiveSequence,
286
+ shownPhotoId
219
287
  }) => {
220
288
  const adapter = adapterById[providerId];
289
+ const meta = providerById[providerId];
221
290
  const { data: photos = [] } = useProviderPhotos(providerId, bbox, zoom);
222
291
  const { data: sequences = [] } = useProviderSequences(providerId, bbox, zoom);
223
- const visiblePhotos = zoom >= adapter.minZoom ? photos.filter((photo) => photoMatchesFilters(photo, filter?.photoTypes, filter?.date)) : [];
292
+ const visiblePhotos = zoom >= meta.minZoom ? photos.filter((photo) => photoMatchesFilters(photo, filter?.photoTypes, filter?.date)) : [];
224
293
  const drawBbox = bbox ? padBbox(bbox, 0.25) : null;
225
- const photoCollection = zoom >= adapter.minZoom ? photosToFeatureCollection(
294
+ const photoCollection = zoom >= meta.minZoom ? photosToFeatureCollection(
226
295
  drawBbox ? visiblePhotos.filter((photo) => lngLatInBbox(photo.lngLat, drawBbox)) : visiblePhotos
227
296
  ) : emptyPointCollection();
228
297
  const viewfieldsActive = showViewfields && zoom >= VIEWFIELD_MIN_ZOOM;
229
298
  const viewfieldCollection = viewfieldsActive ? photosToViewfieldsFeatureCollection(visiblePhotos, zoom, { bbox }) : emptyPolygonCollection();
299
+ const inActiveSequence = [
300
+ "==",
301
+ ["get", "sequenceId"],
302
+ activeSequenceId ?? ""
303
+ ];
304
+ const lineOpacity = [
305
+ "case",
306
+ inActiveSequence,
307
+ 1,
308
+ hasActiveSequence ? 0.45 : 1
309
+ ];
310
+ const dotRadius = [
311
+ "interpolate",
312
+ ["linear"],
313
+ ["zoom"],
314
+ 10,
315
+ ["case", inActiveSequence, 2, 1.2],
316
+ 14,
317
+ ["case", inActiveSequence, 4, 2],
318
+ // Up to here there are no view shapes and the dots are 20 % smaller.
319
+ VIEWFIELD_MIN_ZOOM - 0.01,
320
+ ["case", inActiveSequence, 5.5, 2.9],
321
+ VIEWFIELD_MIN_ZOOM,
322
+ ["case", inActiveSequence, 5.5, 3.6],
323
+ 18,
324
+ ["case", inActiveSequence, 6, 4]
325
+ ];
326
+ const lineAndDotColor = viewfieldsActive ? ["case", inActiveSequence, BASE_COLOR, AGE_SHADE] : photoCircleColor;
327
+ const viewShapeOpacity = (full) => [
328
+ "case",
329
+ inActiveSequence,
330
+ full,
331
+ hasActiveSequence ? full * 0.4 : full
332
+ ];
333
+ const notShownPhoto = ["!=", ["get", "photoId"], shownPhotoId ?? ""];
230
334
  const photoFilter = buildPhotoLayerFilter(filter?.photoTypes, filter?.date);
231
335
  const photoSrcId = photoSourceId(providerId);
232
336
  const viewfieldSrcId = viewfieldSourceId(providerId);
@@ -238,7 +342,7 @@ var PhotoProviderLayer = ({
238
342
  sequenceCapturedAtById.set(photo.sequenceId, photo.capturedAt);
239
343
  }
240
344
  }
241
- const filteredSequences = showSequences && zoom >= (adapter.sequencesMinZoom ?? adapter.minZoom) ? sequences.map((sequence) => {
345
+ const filteredSequences = showSequences && zoom >= meta.sequencesMinZoom ? sequences.map((sequence) => {
242
346
  if (sequence.capturedAt != null) return sequence;
243
347
  const fromPhotos = sequenceCapturedAtById.get(sequence.sequenceId);
244
348
  return fromPhotos != null ? { ...sequence, capturedAt: fromPhotos } : sequence;
@@ -254,9 +358,58 @@ var PhotoProviderLayer = ({
254
358
  };
255
359
  return photoMatchesFilters(asPhoto, filter?.photoTypes, filter?.date);
256
360
  }) : [];
257
- const sequenceCollection = filteredSequences.length > 0 ? sequencesToFeatureCollection(filteredSequences) : emptyLineCollection();
361
+ const photoLngLatsBySequence = /* @__PURE__ */ new Map();
362
+ for (const photo of photos) {
363
+ if (photo.sequenceId == null) continue;
364
+ const list = photoLngLatsBySequence.get(photo.sequenceId);
365
+ if (list) {
366
+ list.push(photo.lngLat);
367
+ } else {
368
+ photoLngLatsBySequence.set(photo.sequenceId, [photo.lngLat]);
369
+ }
370
+ }
371
+ const alignedSequences = filteredSequences.map((sequence) => {
372
+ const points = photoLngLatsBySequence.get(sequence.sequenceId);
373
+ if (!points) return sequence;
374
+ const { geometry } = sequence;
375
+ return {
376
+ ...sequence,
377
+ geometry: geometry.type === "LineString" ? {
378
+ ...geometry,
379
+ coordinates: alignLineToPoints(geometry.coordinates, points)
380
+ } : {
381
+ ...geometry,
382
+ coordinates: geometry.coordinates.map(
383
+ (line) => alignLineToPoints(line, points)
384
+ )
385
+ }
386
+ };
387
+ });
388
+ const sequenceCollection = alignedSequences.length > 0 ? sequencesToFeatureCollection(alignedSequences) : emptyLineCollection();
258
389
  return /* @__PURE__ */ jsxs(Fragment, { children: [
259
- adapter.fetchSequences && showSequences ? /* @__PURE__ */ jsxs(Fragment, { children: [
390
+ meta.coverageTiles ? /* @__PURE__ */ jsxs(Fragment, { children: [
391
+ /* @__PURE__ */ jsx(
392
+ Source,
393
+ {
394
+ id: `coverage-tiles-source-${providerId}`,
395
+ minzoom: meta.coverageTiles.minZoom,
396
+ tileSize: meta.coverageTiles.tileSize,
397
+ tiles: [meta.coverageTiles.url],
398
+ type: "raster"
399
+ }
400
+ ),
401
+ /* @__PURE__ */ jsx(
402
+ Layer,
403
+ {
404
+ id: `coverage-tiles-${providerId}`,
405
+ minzoom: meta.coverageTiles.minZoom,
406
+ paint: { "raster-opacity": 0.7 },
407
+ source: `coverage-tiles-source-${providerId}`,
408
+ type: "raster"
409
+ }
410
+ )
411
+ ] }) : null,
412
+ adapter?.fetchSequences && showSequences ? /* @__PURE__ */ jsxs(Fragment, { children: [
260
413
  /* @__PURE__ */ jsx(
261
414
  Source,
262
415
  {
@@ -272,10 +425,15 @@ var PhotoProviderLayer = ({
272
425
  id: sequenceLayerId(providerId),
273
426
  type: "line",
274
427
  source: sequenceSourceId(providerId),
428
+ layout: {
429
+ "line-cap": "round",
430
+ "line-join": "round",
431
+ "line-sort-key": ["case", inActiveSequence, 1, 0]
432
+ },
275
433
  paint: {
276
- "line-color": adapter.color,
277
- "line-width": 2,
278
- "line-opacity": 0.35
434
+ "line-color": lineAndDotColor,
435
+ "line-width": ["case", inActiveSequence, ACTIVE_LINE_WIDTH, 1.25],
436
+ "line-opacity": lineOpacity
279
437
  }
280
438
  }
281
439
  )
@@ -296,9 +454,11 @@ var PhotoProviderLayer = ({
296
454
  id: viewfieldLayerId(providerId),
297
455
  type: "fill",
298
456
  source: viewfieldSrcId,
457
+ filter: notShownPhoto,
299
458
  paint: {
300
459
  "fill-color": photoCircleColor,
301
- "fill-opacity": 0.22
460
+ "fill-opacity": viewShapeOpacity(VIEW_SHAPE_FILL_OPACITY),
461
+ "fill-outline-color": "rgba(0, 0, 0, 0)"
302
462
  }
303
463
  }
304
464
  ),
@@ -308,10 +468,11 @@ var PhotoProviderLayer = ({
308
468
  id: viewfieldLineLayerId(providerId),
309
469
  type: "line",
310
470
  source: viewfieldSrcId,
471
+ filter: notShownPhoto,
311
472
  paint: {
312
473
  "line-color": photoCircleColor,
313
- "line-width": 1,
314
- "line-opacity": 0.45
474
+ "line-width": 0.5,
475
+ "line-opacity": viewShapeOpacity(0.3)
315
476
  }
316
477
  }
317
478
  )
@@ -335,10 +496,8 @@ var PhotoProviderLayer = ({
335
496
  filter: photoFilter,
336
497
  layout: { "circle-sort-key": PHOTO_SORT_KEY },
337
498
  paint: {
338
- "circle-radius": CIRCLE_RADIUS,
339
- "circle-color": photoCircleColor,
340
- "circle-stroke-width": 1,
341
- "circle-stroke-color": "#ffffff"
499
+ "circle-radius": dotRadius,
500
+ "circle-color": lineAndDotColor
342
501
  }
343
502
  }
344
503
  )
@@ -351,10 +510,10 @@ var MapFeatureProviderLayer = ({
351
510
  filter,
352
511
  mapFeatureCircleColor
353
512
  }) => {
354
- const adapter = adapterById[providerId];
513
+ const meta = providerById[providerId];
355
514
  const { data: features = [] } = useProviderMapFeatures(providerId, bbox, zoom);
356
515
  const valueFilter = filter?.mapFeatureValue;
357
- const featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(
516
+ const featureCollection = zoom >= meta.minZoom ? mapFeaturesToFeatureCollection(
358
517
  valueFilter ? features.filter((feature) => valueFilter(feature.value)) : features
359
518
  ) : emptyPointCollection();
360
519
  const featureFilter = buildMapFeatureLayerFilter(filter?.date);
@@ -380,17 +539,14 @@ var MapFeatureProviderLayer = ({
380
539
  layout: { "circle-sort-key": FEATURE_SORT_KEY },
381
540
  paint: {
382
541
  "circle-radius": FEATURE_CIRCLE_RADIUS,
383
- "circle-color": mapFeatureCircleColor,
384
- "circle-stroke-width": 1,
385
- "circle-stroke-color": "#ffffff"
542
+ "circle-color": mapFeatureCircleColor
386
543
  }
387
544
  }
388
545
  )
389
546
  ] });
390
547
  };
391
548
  var ProviderLayer = (props) => {
392
- const adapter = adapterById[props.providerId];
393
- return adapter.kind === "mapFeature" ? /* @__PURE__ */ jsx(MapFeatureProviderLayer, { ...props }) : /* @__PURE__ */ jsx(PhotoProviderLayer, { ...props });
549
+ return providerById[props.providerId].kind === "mapFeature" ? /* @__PURE__ */ jsx(MapFeatureProviderLayer, { ...props }) : /* @__PURE__ */ jsx(PhotoProviderLayer, { ...props });
394
550
  };
395
551
  var StreetLevelImagerySourcesAndLayers = ({
396
552
  providers,
@@ -408,6 +564,8 @@ var StreetLevelImagerySourcesAndLayers = ({
408
564
  selectedPhoto,
409
565
  selectedSequenceId,
410
566
  viewerPov,
567
+ viewConeColor,
568
+ viewConeScale,
411
569
  photoCircleColor,
412
570
  mapFeatureCircleColor
413
571
  } = options;
@@ -425,11 +583,13 @@ var StreetLevelImagerySourcesAndLayers = ({
425
583
  showSelectionHighlight && selectedProviderId ? bbox : null,
426
584
  zoom
427
585
  );
428
- const selectedSequence = resolveSelectedSequence(
429
- selectedPhoto,
430
- sequences,
431
- selectedSequenceId ?? selectedPhoto?.sequenceId
586
+ const activeSequenceId = selectedSequenceId ?? selectedPhoto?.sequenceId;
587
+ const selectedSequence = resolveSelectedSequence(selectedPhoto, sequences, activeSequenceId);
588
+ const providerDrawsSequences = selectedProviderId != null && showSequences && providers.includes(selectedProviderId);
589
+ const firstPhotoProvider = providers.find(
590
+ (id) => providerById[id].kind === "photo" && adapterById[id]?.fetchPhotos
432
591
  );
592
+ const sequenceBeforeId = firstPhotoProvider ? showViewfields ? viewfieldLayerId(firstPhotoProvider) : photoLayerId(firstPhotoProvider) : void 0;
433
593
  return /* @__PURE__ */ jsxs(Fragment, { children: [
434
594
  providers.map((providerId) => /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(
435
595
  ProviderLayer,
@@ -437,6 +597,9 @@ var StreetLevelImagerySourcesAndLayers = ({
437
597
  bbox,
438
598
  filter,
439
599
  mapFeatureCircleColor,
600
+ activeSequenceId: providerId === selectedProviderId ? activeSequenceId : null,
601
+ hasActiveSequence: activeSequenceId != null,
602
+ shownPhotoId: showViewCone && providerId === selectedProviderId ? selectedPhoto?.photoId : null,
440
603
  photoCircleColor,
441
604
  providerId,
442
605
  showSequences,
@@ -447,6 +610,8 @@ var StreetLevelImagerySourcesAndLayers = ({
447
610
  showViewCone && selectedPhoto ? /* @__PURE__ */ jsx(
448
611
  StreetLevelImageryViewCone,
449
612
  {
613
+ color: viewConeColor ?? SELECTION_COLOR,
614
+ scale: viewConeScale,
450
615
  selectedPhoto,
451
616
  viewerPov,
452
617
  zoom
@@ -455,23 +620,28 @@ var StreetLevelImagerySourcesAndLayers = ({
455
620
  showSelectionHighlight ? /* @__PURE__ */ jsx(
456
621
  StreetLevelImagerySelectionOverlay,
457
622
  {
623
+ cameraLngLat: viewerPov?.lngLat,
458
624
  selectedPhoto,
459
- selectedSequence
625
+ selectedSequence: providerDrawsSequences ? null : selectedSequence,
626
+ sequenceBeforeId
460
627
  }
461
628
  ) : null
462
629
  ] });
463
630
  };
464
631
  var MapillaryPanel2 = lazy(
465
- () => import('./MapillaryPanel-I5GKT6YT.js').then((module) => ({
632
+ () => import('./MapillaryPanel-N45NQY4C.js').then((module) => ({
466
633
  default: module.MapillaryPanel
467
634
  }))
468
635
  );
469
636
  var PanoramaxPanel2 = lazy(
470
- () => import('./PanoramaxPanel-J6ANR6LB.js').then((module) => ({
637
+ () => import('./PanoramaxPanel-MFI4L67Z.js').then((module) => ({
471
638
  default: module.PanoramaxPanel
472
639
  }))
473
640
  );
474
- 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" }) });
641
+ var ViewerPanelPlaceholder = () => {
642
+ const { messages } = useStreetImageryI18n();
643
+ 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 }) });
644
+ };
475
645
  var StreetLevelImageryViewer = ({
476
646
  photo,
477
647
  groupPhotos,
@@ -480,13 +650,15 @@ var StreetLevelImageryViewer = ({
480
650
  onViewerPhoto,
481
651
  lookAtBearing,
482
652
  lookAt,
483
- outlines
653
+ outlines,
654
+ hideAttribution
484
655
  }) => {
485
656
  if (photo.providerId === "mapillary") {
486
657
  return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewerPanelPlaceholder, {}), children: /* @__PURE__ */ jsx(
487
658
  MapillaryPanel2,
488
659
  {
489
660
  groupPhotos,
661
+ hideAttribution,
490
662
  onEaseMapToPoint,
491
663
  onPhotoSelected,
492
664
  lookAt,
@@ -502,6 +674,8 @@ var StreetLevelImageryViewer = ({
502
674
  PanoramaxPanel2,
503
675
  {
504
676
  groupPhotos,
677
+ hideLegend: hideAttribution,
678
+ onViewerPhoto,
505
679
  onEaseMapToPoint,
506
680
  onPhotoSelected,
507
681
  photo
@@ -589,18 +763,6 @@ var useAllProviderMapFeatures = (providerIds, bbox, zoom, date) => {
589
763
  return data.filter((feature) => mapFeatureMatchesDateRange(feature, date));
590
764
  });
591
765
  };
592
- var useAllProviderMapFeaturesLoading = (providerIds, bbox, zoom) => {
593
- const enabled = new Set(providerIds);
594
- const queries = PROVIDER_IDS.map((providerId) => ({
595
- providerId,
596
- query: useProviderMapFeaturesMaybe(providerId, enabled.has(providerId), bbox, zoom)
597
- }));
598
- const activeQueries = queries.filter(({ providerId }) => enabled.has(providerId));
599
- return {
600
- isLoading: activeQueries.some(({ query }) => query.isLoading),
601
- isFetching: activeQueries.some(({ query }) => query.isFetching)
602
- };
603
- };
604
766
  var useProviderPhotosMaybe = (providerId, enabled, bbox, zoom) => useProviderPhotos(providerId, enabled ? bbox : null, zoom);
605
767
  var useAllProviderPhotos = (providerIds, bbox, zoom, photoTypes, date) => {
606
768
  const enabled = new Set(providerIds);
@@ -677,32 +839,44 @@ var usePhotoThumbnail = (photo) => useQuery({
677
839
  enabled: photo != null,
678
840
  staleTime: 24 * 60 * 60 * 1e3
679
841
  });
680
-
681
- // src/viewpoints/viewDirectionLabels.ts
682
- var ROLE_LABELS = {
683
- here: "Here",
684
- "line-start": "Start",
685
- "line-end": "End",
686
- "into-node": "Junction",
687
- custom: "View"
688
- };
689
- var viewpointRoleLabel = (role) => ROLE_LABELS[role];
690
- var viewSuggestionLabel = ({ viewpoint, direction }) => {
691
- const role = viewpoint.label ?? viewpointRoleLabel(viewpoint.role);
692
- switch (direction.kind) {
693
- case "forward":
694
- return `${role}, looking along the street`;
695
- case "back":
696
- return `${role}, looking back along the street`;
697
- case "into":
698
- return `${role}, looking into the junction`;
699
- case "N":
700
- case "E":
701
- case "S":
702
- case "W":
703
- return `${role}, looking ${direction.kind}`;
842
+ var PhotoCount = ({ index, total, className }) => /* @__PURE__ */ jsxs("span", { className: `inline-flex shrink-0 items-center gap-0.5 tabular-nums ${className ?? ""}`, children: [
843
+ /* @__PURE__ */ jsx(
844
+ "svg",
845
+ {
846
+ "aria-hidden": true,
847
+ className: "size-3 shrink-0",
848
+ fill: "none",
849
+ stroke: "currentColor",
850
+ strokeLinecap: "round",
851
+ strokeLinejoin: "round",
852
+ strokeWidth: 1.75,
853
+ viewBox: "0 0 24 24",
854
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6h16v12H4ZM4 15l4.5-4.5 4 4L15 12l5 5M9 9.5h.01" })
855
+ }
856
+ ),
857
+ index != null ? `${index}/` : "",
858
+ total
859
+ ] });
860
+ var PhotoDate = ({ timestamp, className, localDateTime }) => {
861
+ const { locale, messages } = useStreetImageryI18n();
862
+ if (timestamp == null) {
863
+ return /* @__PURE__ */ jsx("span", { className, children: messages.unknownDate });
704
864
  }
865
+ const precise = localDateTime ? formatLocalDateTime(localDateTime, locale) : null;
866
+ return /* @__PURE__ */ jsx(
867
+ "time",
868
+ {
869
+ className,
870
+ dateTime: new Date(timestamp).toISOString(),
871
+ title: precise ? `${precise}
872
+ ${formatRelativeDate(timestamp, locale)}` : formatDateTooltip(timestamp, locale),
873
+ children: formatMonth(timestamp, locale)
874
+ }
875
+ );
705
876
  };
877
+
878
+ // src/viewpoints/viewDirectionLabels.ts
879
+ var viewSuggestionLabel = ({ viewpoint, direction }, messages) => messages.viewDirection(viewpoint.label ?? messages.viewpointRole[viewpoint.role], direction.kind);
706
880
  var DEFAULT_LAYOUT = { right: 16, bottom: 32, width: 420, minimized: false };
707
881
  var MIN_WIDTH = 280;
708
882
  var EDGE = 8;
@@ -736,11 +910,14 @@ var Icon = ({ path, className = "size-4" }) => /* @__PURE__ */ jsx(
736
910
  );
737
911
  var ICONS = {
738
912
  back: "M15 18l-6-6 6-6",
913
+ undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11",
914
+ redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13",
739
915
  forward: "M9 18l6-6-6-6",
740
916
  close: "M18 6 6 18M6 6l12 12",
741
917
  minimize: "M5 12h14",
742
918
  expand: "M4 14v6h6M20 10V4h-6M14 10l6-6M10 14l-6 6",
743
- arrow: "M12 20V5M6 11l6-6 6 6"
919
+ arrow: "M12 20V5M6 11l6-6 6 6",
920
+ info: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 11v5M12 8h.01"
744
921
  };
745
922
  var ToolbarButton = ({
746
923
  label,
@@ -759,64 +936,112 @@ var ToolbarButton = ({
759
936
  children
760
937
  }
761
938
  );
762
- var formatShortDate = (capturedAt) => capturedAt == null ? "\u2014" : new Intl.DateTimeFormat("en", { year: "2-digit", month: "short" }).format(capturedAt);
763
939
  var SuggestionChip = ({
764
940
  suggestion,
765
941
  active,
942
+ shownPhotoId,
943
+ showPlace,
766
944
  onSelect
767
945
  }) => {
768
- const best = suggestion.candidates[0];
769
- const label = viewSuggestionLabel(suggestion);
946
+ const { messages } = useStreetImageryI18n();
947
+ const { candidates } = suggestion;
948
+ const best = candidates[0];
949
+ const shownIndex = active ? candidates.findIndex((candidate) => candidate.photo.photoId === shownPhotoId) : -1;
950
+ const shown = shownIndex >= 0 ? candidates[shownIndex] : best;
951
+ const canStep = active && candidates.length > 1;
952
+ const hasCount = candidates.length > 0;
953
+ const name = viewSuggestionLabel(suggestion, messages);
954
+ const label = canStep ? messages.viewer.nextPhotoOfView(name) : name;
770
955
  return /* @__PURE__ */ jsxs(
771
956
  "button",
772
957
  {
773
- "aria-label": best ? label : `${label} (no photo)`,
958
+ "aria-label": best ? label : messages.viewer.noPhoto(label),
774
959
  "aria-pressed": active,
775
960
  className: [
776
- "inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs whitespace-nowrap",
961
+ "flex min-w-0 flex-col items-center rounded-md border px-1 py-0.5 text-xs leading-tight",
777
962
  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"
778
963
  ].join(" "),
779
964
  disabled: !best,
780
- onClick: onSelect,
781
- title: best ? label : `${label}: no matching photo`,
965
+ onClick: () => {
966
+ const next = canStep ? candidates[(shownIndex + 1) % candidates.length] : best;
967
+ if (next) {
968
+ onSelect(next);
969
+ }
970
+ },
971
+ title: best ? label : messages.viewer.noMatchingPhoto(label),
782
972
  type: "button",
783
973
  children: [
784
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: viewpointRoleLabel(suggestion.viewpoint.role) }),
785
- /* @__PURE__ */ jsx(
974
+ /* @__PURE__ */ jsxs(
786
975
  "span",
787
976
  {
788
- "aria-hidden": true,
789
- className: "inline-flex",
790
- style: { transform: `rotate(${suggestion.direction.bearing}deg)` },
791
- children: /* @__PURE__ */ jsx(Icon, { className: "size-3", path: ICONS.arrow })
977
+ className: `flex items-center gap-1 font-medium ${hasCount ? "w-full justify-between px-1" : "max-w-full"}`,
978
+ children: [
979
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
980
+ /* @__PURE__ */ jsx(
981
+ "span",
982
+ {
983
+ "aria-hidden": true,
984
+ className: "inline-flex shrink-0",
985
+ style: { transform: `rotate(${suggestion.direction.bearing}deg)` },
986
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-3", path: ICONS.arrow })
987
+ }
988
+ ),
989
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
990
+ showPlace ? `${suggestion.viewpoint.label ?? messages.viewpointRole[suggestion.viewpoint.role]} ` : "",
991
+ messages.compass[Math.round(suggestion.direction.bearing / 45) % 8]
992
+ ] })
993
+ ] }),
994
+ hasCount ? /* @__PURE__ */ jsx(
995
+ PhotoCount,
996
+ {
997
+ className: `font-normal ${active ? "text-fuchsia-100" : "text-slate-500"}`,
998
+ index: shownIndex >= 0 ? shownIndex + 1 : void 0,
999
+ total: candidates.length
1000
+ }
1001
+ ) : null
1002
+ ]
792
1003
  }
793
1004
  ),
794
- /* @__PURE__ */ jsx("span", { className: active ? "text-fuchsia-100" : "text-slate-400", children: formatShortDate(best?.photo.capturedAt) })
1005
+ shown ? /* @__PURE__ */ jsx(
1006
+ PhotoDate,
1007
+ {
1008
+ className: `max-w-full truncate ${active ? "text-fuchsia-100" : "text-slate-400"}`,
1009
+ timestamp: shown.photo.capturedAt
1010
+ }
1011
+ ) : /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "\u2014" })
795
1012
  ]
796
1013
  }
797
1014
  );
798
1015
  };
1016
+ var FloatingViewerInfoButton = ({
1017
+ label,
1018
+ onClick
1019
+ }) => /* @__PURE__ */ jsx(ToolbarButton, { label, onClick, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.info }) });
799
1020
  var FloatingPhotoViewer = ({
800
1021
  title,
1022
+ titleActions,
801
1023
  suggestions,
802
1024
  activeDirectionKey,
803
1025
  onSelectSuggestion,
1026
+ shownPhotoId,
804
1027
  canGoBack,
805
1028
  canGoForward,
806
1029
  onBack,
807
1030
  onForward,
808
1031
  onClose,
1032
+ step,
1033
+ toolbar,
809
1034
  status,
810
1035
  children,
811
1036
  footer,
812
1037
  drawer,
813
1038
  storageKey = "street-imagery:floating-viewer"
814
1039
  }) => {
1040
+ const { messages } = useStreetImageryI18n();
1041
+ const severalViewpoints = new Set(suggestions.map((suggestion) => suggestion.viewpoint.id)).size > 1;
815
1042
  const [layout, setLayout] = useState(() => readLayout(storageKey));
816
1043
  const [drawerOpen, setDrawerOpen] = useState(false);
817
- const dragRef = useRef(
818
- null
819
- );
1044
+ const dragRef = useRef(null);
820
1045
  const updateLayout = (next) => {
821
1046
  setLayout(next);
822
1047
  writeLayout(storageKey, next);
@@ -832,6 +1057,10 @@ var FloatingPhotoViewer = ({
832
1057
  onBack();
833
1058
  } else if (event.key === "]" && canGoForward) {
834
1059
  onForward();
1060
+ } else if (event.key === "ArrowLeft" && event.altKey) {
1061
+ step?.onPrevious?.();
1062
+ } else if (event.key === "ArrowRight" && event.altKey) {
1063
+ step?.onNext?.();
835
1064
  }
836
1065
  });
837
1066
  useEffect(function subscribeKeyboardShortcuts() {
@@ -885,7 +1114,7 @@ var FloatingPhotoViewer = ({
885
1114
  return /* @__PURE__ */ jsxs(
886
1115
  "section",
887
1116
  {
888
- "aria-label": "Photo viewer",
1117
+ "aria-label": messages.viewer.regionLabel,
889
1118
  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",
890
1119
  style: {
891
1120
  right: layout.right,
@@ -909,39 +1138,65 @@ var FloatingPhotoViewer = ({
909
1138
  onPointerDown: (event) => startDrag(event, "move"),
910
1139
  ...dragHandlers,
911
1140
  children: [
912
- /* @__PURE__ */ jsx(ToolbarButton, { disabled: !canGoBack, label: "Previous photo in history ([)", onClick: onBack, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.back }) }),
1141
+ step ? /* @__PURE__ */ jsxs(Fragment, { children: [
1142
+ /* @__PURE__ */ jsx(
1143
+ ToolbarButton,
1144
+ {
1145
+ disabled: !step.onPrevious,
1146
+ label: step.previousLabel,
1147
+ onClick: () => step.onPrevious?.(),
1148
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.back })
1149
+ }
1150
+ ),
1151
+ /* @__PURE__ */ jsx(
1152
+ ToolbarButton,
1153
+ {
1154
+ disabled: !step.onNext,
1155
+ label: step.nextLabel,
1156
+ onClick: () => step.onNext?.(),
1157
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.forward })
1158
+ }
1159
+ )
1160
+ ] }) : null,
1161
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-0.5", children: [
1162
+ /* @__PURE__ */ jsx("h2", { className: "min-w-0 truncate px-1 text-sm font-semibold text-slate-800", children: title }),
1163
+ titleActions
1164
+ ] }),
1165
+ /* @__PURE__ */ jsx(ToolbarButton, { disabled: !canGoBack, label: messages.viewer.backInHistory, onClick: onBack, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.undo }) }),
913
1166
  /* @__PURE__ */ jsx(
914
1167
  ToolbarButton,
915
1168
  {
916
1169
  disabled: !canGoForward,
917
- label: "Next photo in history (])",
1170
+ label: messages.viewer.forwardInHistory,
918
1171
  onClick: onForward,
919
- children: /* @__PURE__ */ jsx(Icon, { path: ICONS.forward })
1172
+ children: /* @__PURE__ */ jsx(Icon, { path: ICONS.redo })
920
1173
  }
921
1174
  ),
922
- /* @__PURE__ */ jsx("h2", { className: "min-w-0 flex-1 truncate px-1 text-sm font-semibold text-slate-800", children: title }),
923
1175
  /* @__PURE__ */ jsx(
924
1176
  ToolbarButton,
925
1177
  {
926
- label: "Minimize",
1178
+ label: messages.viewer.minimize,
927
1179
  onClick: () => updateLayout({ ...layout, minimized: true }),
928
1180
  children: /* @__PURE__ */ jsx(Icon, { path: ICONS.minimize })
929
1181
  }
930
1182
  ),
931
- /* @__PURE__ */ jsx(ToolbarButton, { label: "Close (Esc)", onClick: onClose, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.close }) })
1183
+ /* @__PURE__ */ jsx(ToolbarButton, { label: messages.viewer.close, onClick: onClose, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.close }) })
932
1184
  ]
933
1185
  }
934
1186
  ),
935
- suggestions.length > 0 ? /* @__PURE__ */ jsx(
1187
+ toolbar ? /* @__PURE__ */ jsx("div", { className: "border-b border-slate-100 px-2 py-1.5", children: toolbar }) : suggestions.length > 0 ? /* @__PURE__ */ jsx(
936
1188
  "nav",
937
1189
  {
938
- "aria-label": "Suggested views",
939
- className: "flex gap-1 overflow-x-auto border-b border-slate-100 px-2 py-1.5",
1190
+ "aria-label": messages.viewer.suggestedViews,
1191
+ className: "grid grid-cols-[repeat(auto-fit,minmax(5.25rem,1fr))] gap-1 border-b border-slate-100 px-2 py-1.5",
1192
+ title: messages.viewer.suggestedViewsHint,
940
1193
  children: suggestions.map((suggestion) => /* @__PURE__ */ jsx(
941
1194
  SuggestionChip,
942
1195
  {
943
1196
  active: suggestion.direction.key === activeDirectionKey,
944
- onSelect: () => onSelectSuggestion(suggestion),
1197
+ onSelect: (candidate) => onSelectSuggestion(suggestion, candidate),
1198
+ shownPhotoId,
1199
+ showPlace: severalViewpoints,
945
1200
  suggestion
946
1201
  },
947
1202
  suggestion.direction.key
@@ -1092,7 +1347,12 @@ var useViewpointSessionStore = create()((set, get) => ({
1092
1347
  showPhoto: (entry) => set((state) => {
1093
1348
  const current = state.history[state.historyIndex];
1094
1349
  if (samePhoto(current?.photo, entry.photo)) {
1095
- return entry.directionKey != null && entry.directionKey !== state.activeDirectionKey ? { activeDirectionKey: entry.directionKey } : state;
1350
+ if (entry.directionKey == null || entry.directionKey === current?.directionKey) {
1351
+ return entry.directionKey != null && entry.directionKey !== state.activeDirectionKey ? { activeDirectionKey: entry.directionKey } : state;
1352
+ }
1353
+ const history2 = [...state.history];
1354
+ history2[state.historyIndex] = { ...entry };
1355
+ return { history: history2, activeDirectionKey: entry.directionKey };
1096
1356
  }
1097
1357
  const history = [...state.history.slice(0, state.historyIndex + 1), entry].slice(
1098
1358
  -100
@@ -1121,17 +1381,22 @@ var useViewpointSessionStore = create()((set, get) => ({
1121
1381
  set({ historyIndex: historyIndex + 1, activeDirectionKey: entry.directionKey });
1122
1382
  return entry;
1123
1383
  },
1124
- close: () => set({ viewpoints: [], line: null, activeDirectionKey: null })
1384
+ close: () => set({ viewpoints: [], line: null, activeDirectionKey: null }),
1385
+ reset: () => set({
1386
+ viewpoints: [],
1387
+ line: null,
1388
+ activeDirectionKey: null,
1389
+ history: [],
1390
+ historyIndex: -1
1391
+ })
1125
1392
  }
1126
1393
  }));
1127
- var useViewpointSessionId = () => useViewpointSessionStore((state) => state.sessionId);
1128
1394
  var useViewpoints = () => useViewpointSessionStore((state) => state.viewpoints);
1129
1395
  var useViewpointLine = () => useViewpointSessionStore((state) => state.line);
1130
1396
  var useActiveDirectionKey = () => useViewpointSessionStore((state) => state.activeDirectionKey);
1131
1397
  var useCurrentHistoryEntry = () => useViewpointSessionStore((state) => state.history[state.historyIndex] ?? null);
1132
1398
  var useCanGoBack = () => useViewpointSessionStore((state) => state.historyIndex > 0);
1133
1399
  var useCanGoForward = () => useViewpointSessionStore((state) => state.historyIndex < state.history.length - 1);
1134
- var useViewpointSessionActions = () => useViewpointSessionStore((state) => state.actions);
1135
1400
  var getViewpointSession = () => {
1136
1401
  const { actions, history, historyIndex } = useViewpointSessionStore.getState();
1137
1402
  return { actions, current: history[historyIndex] ?? null };
@@ -1219,5 +1484,151 @@ var useMapillaryImageDetections = (imageId, { filter, filterKey } = {}) => useQu
1219
1484
  enabled: !!imageId,
1220
1485
  staleTime: STALE_TIME
1221
1486
  });
1487
+ var useLocationPickStore = create()((set) => ({
1488
+ armedOpenerId: null,
1489
+ actions: {
1490
+ arm: (openerId) => set({ armedOpenerId: openerId }),
1491
+ disarm: () => set((state) => state.armedOpenerId == null ? state : { armedOpenerId: null }),
1492
+ toggle: (openerId) => set((state) => ({ armedOpenerId: state.armedOpenerId === openerId ? null : openerId }))
1493
+ }
1494
+ }));
1495
+ var useArmedLocationOpenerId = () => useLocationPickStore((state) => state.armedOpenerId);
1496
+ var useLocationPickActions = () => useLocationPickStore((state) => state.actions);
1497
+
1498
+ // src/openers/LocationPickOnMap.tsx
1499
+ var roundCoordinate = (value) => Number(value.toFixed(7));
1500
+ var LocationPickOnMap = ({ onPick }) => {
1501
+ const { current: map } = useMap();
1502
+ const armedOpenerId = useArmedLocationOpenerId();
1503
+ const { disarm } = useLocationPickActions();
1504
+ useEffect(
1505
+ function openArmedOpenerOnNextMapClick() {
1506
+ if (!map || !armedOpenerId) {
1507
+ return;
1508
+ }
1509
+ const opener = findLocationOpener(armedOpenerId);
1510
+ if (!opener) {
1511
+ return;
1512
+ }
1513
+ const onClick = (event) => {
1514
+ const target = {
1515
+ lngLat: [roundCoordinate(event.lngLat.lng), roundCoordinate(event.lngLat.lat)],
1516
+ zoom: map.getZoom()
1517
+ };
1518
+ openLocationInNewTab(opener, target);
1519
+ disarm();
1520
+ onPick?.(target, opener);
1521
+ };
1522
+ const onKeyDown = (event) => {
1523
+ if (event.key === "Escape") {
1524
+ disarm();
1525
+ }
1526
+ };
1527
+ map.on("click", onClick);
1528
+ window.addEventListener("keydown", onKeyDown);
1529
+ return function stopListeningForPick() {
1530
+ map.off("click", onClick);
1531
+ window.removeEventListener("keydown", onKeyDown);
1532
+ };
1533
+ },
1534
+ [armedOpenerId, disarm, map, onPick]
1535
+ );
1536
+ return null;
1537
+ };
1538
+ var MAPILLARY_FEATURE_BAR_LABELS = {
1539
+ en: {
1540
+ name: (value) => mapillaryValueName(value, "en"),
1541
+ seen: "Seen",
1542
+ noPhotos: "Mapillary lists no photos for this feature.",
1543
+ daysSummary: (photos, days) => `${photos} photos on ${days} days`,
1544
+ dayPhotos: (photos, canStep) => `${photos} photo${photos === 1 ? "" : "s"}${canStep ? " \u2014 click for the next one" : ""}`
1545
+ },
1546
+ de: {
1547
+ name: (value) => mapillaryValueName(value, "de"),
1548
+ seen: "Gesehen",
1549
+ noPhotos: "Mapillary f\xFChrt keine Fotos zu diesem Objekt.",
1550
+ daysSummary: (photos, days) => `${photos} Fotos an ${days} Tagen`,
1551
+ dayPhotos: (photos, canStep) => `${photos} Foto${photos === 1 ? "" : "s"}${canStep ? " \u2014 klicken f\xFCr das n\xE4chste" : ""}`
1552
+ }
1553
+ };
1554
+ var MapillaryFeatureBar = ({
1555
+ data,
1556
+ shownImage,
1557
+ onShow,
1558
+ labels: labelOverrides
1559
+ }) => {
1560
+ const { locale } = useStreetImageryI18n();
1561
+ const labels = { ...MAPILLARY_FEATURE_BAR_LABELS[locale], ...labelOverrides };
1562
+ const { feature, days, images } = data;
1563
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1564
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", title: feature.value, children: [
1565
+ /* @__PURE__ */ jsx(
1566
+ "img",
1567
+ {
1568
+ alt: "",
1569
+ className: "size-7 shrink-0",
1570
+ onError: (event) => {
1571
+ event.currentTarget.style.display = "none";
1572
+ },
1573
+ src: mapillaryIconUrl(feature.value)
1574
+ },
1575
+ feature.value
1576
+ ),
1577
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1578
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm leading-tight font-medium text-slate-900", children: labels.name(feature.value) }),
1579
+ /* @__PURE__ */ jsxs("p", { className: "truncate text-xs leading-tight text-slate-500", children: [
1580
+ labels.seen,
1581
+ " ",
1582
+ /* @__PURE__ */ jsx(PhotoDate, { timestamp: feature.firstSeenAt }),
1583
+ " \u2013",
1584
+ " ",
1585
+ /* @__PURE__ */ jsx(PhotoDate, { timestamp: feature.lastSeenAt })
1586
+ ] })
1587
+ ] })
1588
+ ] }),
1589
+ days.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500", children: labels.noPhotos }) : /* @__PURE__ */ jsx(
1590
+ "div",
1591
+ {
1592
+ "aria-label": labels.daysSummary(images.length, days.length),
1593
+ className: "flex gap-1 overflow-x-auto",
1594
+ role: "group",
1595
+ children: days.map((day) => {
1596
+ const noon = /* @__PURE__ */ new Date(`${day.day}T12:00:00Z`);
1597
+ const shownIndex = shownImage ? day.images.findIndex((image) => image.id === shownImage.id) : -1;
1598
+ const active = shownIndex >= 0;
1599
+ return /* @__PURE__ */ jsxs(
1600
+ "button",
1601
+ {
1602
+ "aria-pressed": active,
1603
+ 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"}`,
1604
+ onClick: () => {
1605
+ const next = active ? day.images[(shownIndex + 1) % day.images.length] : day.best;
1606
+ if (next) {
1607
+ onShow(next);
1608
+ }
1609
+ },
1610
+ title: `${formatDateTooltip(noon, locale)}
1611
+ ${labels.dayPhotos(day.images.length, active && day.images.length > 1)}`,
1612
+ type: "button",
1613
+ children: [
1614
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: formatMonth(noon, locale) }),
1615
+ " ",
1616
+ /* @__PURE__ */ jsx(
1617
+ PhotoCount,
1618
+ {
1619
+ className: `align-[-1px] ${active ? "text-amber-800" : "text-slate-500"}`,
1620
+ index: active ? shownIndex + 1 : void 0,
1621
+ total: day.images.length
1622
+ }
1623
+ )
1624
+ ]
1625
+ },
1626
+ day.day
1627
+ );
1628
+ })
1629
+ }
1630
+ )
1631
+ ] });
1632
+ };
1222
1633
 
1223
- export { FloatingPhotoViewer, StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, ViewpointLayer, getViewpointSession, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useActiveDirectionKey, useAllProviderMapFeatures, useAllProviderMapFeaturesLoading, useAllProviderPhotos, useCanGoBack, useCanGoForward, useCurrentHistoryEntry, useMapViewportBbox, useMapillaryImageDetections, useMapillaryMapFeatureImages, useMapillaryMapFeaturesNear, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences, useViewSuggestions, useViewpointLine, useViewpointSessionActions, useViewpointSessionId, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel, viewpointRoleLabel };
1634
+ export { BASE_COLOR, FloatingPhotoViewer, FloatingViewerInfoButton, LocationPickOnMap, MAPILLARY_FEATURE_BAR_LABELS, MapillaryFeatureBar, PhotoCount, PhotoDate, SELECTION_COLOR, StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, VIEW_SHAPE_FILL_OPACITY, ViewpointLayer, getViewpointSession, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useActiveDirectionKey, useAllProviderMapFeatures, useAllProviderPhotos, useArmedLocationOpenerId, useCanGoBack, useCanGoForward, useCurrentHistoryEntry, useLocationPickActions, useMapViewportBbox, useMapillaryImageDetections, useMapillaryMapFeatureImages, useMapillaryMapFeaturesNear, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences, useViewSuggestions, useViewpointLine, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel };