@osm-editor-kit/street-imagery-react 0.1.0-alpha.6 → 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.
@@ -1,4 +1,4 @@
1
- import { DEFAULT_STREET_IMAGERY_LOCALE, mapillaryValueName } from '@osm-editor-kit/street-imagery';
1
+ import { DEFAULT_STREET_IMAGERY_LOCALE } from '@osm-editor-kit/street-imagery';
2
2
  import { createContext, use } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
@@ -11,11 +11,11 @@ var en = {
11
11
  forwardInHistory: "Forward in history (])",
12
12
  minimize: "Minimize",
13
13
  close: "Close (Esc)",
14
- views: "Views",
15
14
  suggestedViews: "Suggested views",
16
15
  suggestedViewsHint: "Suggested views: where the photo is taken from, and which way it looks",
17
16
  noPhoto: (label) => `${label} (no photo)`,
18
- noMatchingPhoto: (label) => `${label}: no matching photo`
17
+ noMatchingPhoto: (label) => `${label}: no matching photo`,
18
+ nextPhotoOfView: (label) => `${label} \u2014 click for the next photo`
19
19
  },
20
20
  unknownDate: "Unknown date",
21
21
  viewpointRole: {
@@ -25,24 +25,22 @@ var en = {
25
25
  "into-node": "Junction",
26
26
  custom: "View"
27
27
  },
28
+ compass: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"],
28
29
  viewDirection: (role, kind) => {
29
30
  switch (kind) {
30
31
  case "forward":
31
32
  return `${role}, looking along the street`;
32
33
  case "back":
33
34
  return `${role}, looking back along the street`;
35
+ case "right":
36
+ return `${role}, looking right across the street`;
37
+ case "left":
38
+ return `${role}, looking left across the street`;
34
39
  case "into":
35
40
  return `${role}, looking into the junction`;
36
41
  default:
37
42
  return `${role}, looking ${kind}`;
38
43
  }
39
- },
40
- feature: {
41
- name: (value) => mapillaryValueName(value, "en"),
42
- seen: "Seen",
43
- noPhotos: "Mapillary lists no photos for this feature.",
44
- daysSummary: (photos, days) => `${photos} photos on ${days} days`,
45
- dayPhotos: (photos, canStep) => `${photos} photo${photos === 1 ? "" : "s"}${canStep ? " \u2014 click for the next one" : ""}`
46
44
  }
47
45
  };
48
46
  var COMPASS_DE = { N: "Norden", E: "Osten", S: "S\xFCden", W: "Westen" };
@@ -54,11 +52,11 @@ var de = {
54
52
  forwardInHistory: "Vor im Verlauf (])",
55
53
  minimize: "Minimieren",
56
54
  close: "Schlie\xDFen (Esc)",
57
- views: "Blicke",
58
55
  suggestedViews: "Vorgeschlagene Blicke",
59
56
  suggestedViewsHint: "Vorgeschlagene Blicke: von wo das Foto aufgenommen ist und wohin es schaut",
60
57
  noPhoto: (label) => `${label} (kein Foto)`,
61
- noMatchingPhoto: (label) => `${label}: kein passendes Foto`
58
+ noMatchingPhoto: (label) => `${label}: kein passendes Foto`,
59
+ nextPhotoOfView: (label) => `${label} \u2014 klicken f\xFCr das n\xE4chste Foto`
62
60
  },
63
61
  unknownDate: "Datum unbekannt",
64
62
  viewpointRole: {
@@ -68,24 +66,22 @@ var de = {
68
66
  "into-node": "Knoten",
69
67
  custom: "Blick"
70
68
  },
69
+ compass: ["N", "NO", "O", "SO", "S", "SW", "W", "NW"],
71
70
  viewDirection: (role, kind) => {
72
71
  switch (kind) {
73
72
  case "forward":
74
73
  return `${role}, Blick entlang der Stra\xDFe`;
75
74
  case "back":
76
75
  return `${role}, Blick zur\xFCck entlang der Stra\xDFe`;
76
+ case "right":
77
+ return `${role}, Blick nach rechts quer zur Stra\xDFe`;
78
+ case "left":
79
+ return `${role}, Blick nach links quer zur Stra\xDFe`;
77
80
  case "into":
78
81
  return `${role}, Blick in den Knoten`;
79
82
  default:
80
83
  return `${role}, Blick nach ${COMPASS_DE[kind] ?? kind}`;
81
84
  }
82
- },
83
- feature: {
84
- name: (value) => mapillaryValueName(value, "de"),
85
- seen: "Gesehen",
86
- noPhotos: "Mapillary f\xFChrt keine Fotos zu diesem Objekt.",
87
- daysSummary: (photos, days) => `${photos} Fotos an ${days} Tagen`,
88
- dayPhotos: (photos, canStep) => `${photos} Foto${photos === 1 ? "" : "s"}${canStep ? " \u2014 klicken f\xFCr das n\xE4chste" : ""}`
89
85
  }
90
86
  };
91
87
  var STREET_IMAGERY_MESSAGES = {
@@ -101,8 +97,8 @@ var mergeMessages = (base, overrides) => overrides ? {
101
97
  unknownDate: overrides.unknownDate ?? base.unknownDate,
102
98
  viewer: { ...base.viewer, ...overrides.viewer },
103
99
  viewpointRole: { ...base.viewpointRole, ...overrides.viewpointRole },
104
- viewDirection: overrides.viewDirection ?? base.viewDirection,
105
- feature: { ...base.feature, ...overrides.feature }
100
+ compass: overrides.compass?.length === 8 ? overrides.compass : base.compass,
101
+ viewDirection: overrides.viewDirection ?? base.viewDirection
106
102
  } : base;
107
103
  var StreetImageryLocaleProvider = ({
108
104
  locale,
@@ -0,0 +1,12 @@
1
+ type PhotoCountProps = {
2
+ /** Position of the shown photo, starting at 1; leave out to show only the total. */
3
+ index?: number;
4
+ total: number;
5
+ className?: string;
6
+ };
7
+ /**
8
+ * "1/5" behind a small picture icon: this button has several photos and a click on it shows the
9
+ * next one.
10
+ */
11
+ export declare const PhotoCount: ({ index, total, className }: PhotoCountProps) => import("react").JSX.Element;
12
+ export {};
@@ -7,7 +7,7 @@ type StreetImageryI18n = {
7
7
  };
8
8
  /** Overrides per group, e.g. `{ feature: { name: germanSignName } }`. */
9
9
  export type StreetImageryMessageOverrides = {
10
- [Group in keyof StreetImageryMessages]?: StreetImageryMessages[Group] extends object ? StreetImageryMessages[Group] extends (...args: never[]) => unknown ? StreetImageryMessages[Group] : Partial<StreetImageryMessages[Group]> : StreetImageryMessages[Group];
10
+ [Group in keyof StreetImageryMessages]?: StreetImageryMessages[Group] extends object ? StreetImageryMessages[Group] extends ((...args: never[]) => unknown) | readonly unknown[] ? StreetImageryMessages[Group] : Partial<StreetImageryMessages[Group]> : StreetImageryMessages[Group];
11
11
  };
12
12
  type StreetImageryLocaleProviderProps = {
13
13
  /** Language of the package's texts and date formats. Without a provider: English. */
@@ -11,25 +11,17 @@ export type StreetImageryMessages = {
11
11
  forwardInHistory: string;
12
12
  minimize: string;
13
13
  close: string;
14
- views: string;
15
14
  suggestedViews: string;
16
15
  suggestedViewsHint: string;
17
16
  noPhoto: (label: string) => string;
18
17
  noMatchingPhoto: (label: string) => string;
18
+ nextPhotoOfView: (label: string) => string;
19
19
  };
20
20
  viewpointRole: Record<ViewpointRole, string>;
21
+ /** Eight compass points, clockwise from north: N, NE, E, SE, S, SW, W, NW. */
22
+ compass: readonly string[];
21
23
  unknownDate: string;
22
24
  /** e.g. "Start, looking along the street"; `role` is the viewpoint's label. */
23
25
  viewDirection: (role: string, kind: ViewDirection['kind']) => string;
24
- feature: {
25
- /** Name of a Mapillary value like `regulatory--turn-right-ahead--g1`. */
26
- name: (value: string) => string;
27
- /** Line below the name; `first` and `last` are the rendered dates. */
28
- seen: string;
29
- noPhotos: string;
30
- daysSummary: (photos: number, days: number) => string;
31
- /** Third tooltip line of a day button, below date and age. */
32
- dayPhotos: (photos: number, canStep: boolean) => string;
33
- };
34
26
  };
35
27
  export declare const STREET_IMAGERY_MESSAGES: Record<StreetImageryLocale, StreetImageryMessages>;
package/dist/index.d.ts CHANGED
@@ -22,6 +22,8 @@ export * from './mapillary/useMapillaryFeatures';
22
22
  export { LocationPickOnMap, type LocationPickOnMapProps } from './openers/LocationPickOnMap';
23
23
  export * from './openers/useLocationPickStore';
24
24
  export { MapillaryFeatureBar, type MapillaryFeatureBarProps } from './mapillary/MapillaryFeatureBar';
25
+ export { MAPILLARY_FEATURE_BAR_LABELS, type MapillaryFeatureBarLabels, } from './mapillary/featureBarLabels';
25
26
  export { STREET_IMAGERY_MESSAGES, type StreetImageryMessages } from './i18n/messages';
26
27
  export { StreetImageryLocaleProvider, useStreetImageryI18n, type StreetImageryMessageOverrides, } from './i18n/StreetImageryLocaleProvider';
27
28
  export { PhotoDate } from './i18n/PhotoDate';
29
+ export { PhotoCount } from './i18n/PhotoCount';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { useStreetImageryI18n } from './chunk-HA4GJWHI.js';
2
- export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n } from './chunk-HA4GJWHI.js';
1
+ import { useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
2
+ export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
3
3
  export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo } from './chunk-53RA4OSJ.js';
4
4
  export { PanoramaxPanel } from './chunk-EQ2JKLGT.js';
5
5
  export { useViewerActions, useViewerBearing, useViewerHfov, useViewerLngLat } from './chunk-25UGGXIE.js';
6
- import { distanceMeters, sequencesToFeatureCollection, emptyLineCollection, adapterById, viewConeGeoJson, viewShapeRadiusMeters, setStreetImageryConfig, viewfieldLayerId, photoLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, formatLocalDateTime, formatMonth, formatRelativeDate, formatDateTooltip, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, fetchImageDetections, findLocationOpener, mapillaryIconUrl, openLocationInNewTab, alignLineToPoints, coneRadiusMeters, mapFeaturesToFeatureCollection, emptyPointCollection, buildMapFeatureLayerFilter, featureSourceId, photosToFeatureCollection, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
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';
7
7
  import { Source, Layer, useMap } from 'react-map-gl/maplibre';
8
8
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
9
9
  import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState, useRef, useEffectEvent } from 'react';
@@ -139,7 +139,8 @@ var withinBbox = (bbox) => (item) => {
139
139
  };
140
140
  var useProviderPhotos = (providerId, bbox, zoom) => {
141
141
  const adapter = adapterById[providerId];
142
- 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;
143
144
  return useQuery({
144
145
  queryKey: ["provider-photos", providerId, bboxKey(bbox), zoom],
145
146
  queryFn: async ({ signal }) => {
@@ -152,18 +153,20 @@ var useProviderPhotos = (providerId, bbox, zoom) => {
152
153
  };
153
154
  var useProviderSequences = (providerId, bbox, zoom) => {
154
155
  const adapter = adapterById[providerId];
155
- const sequencesMinZoom = adapter.sequencesMinZoom ?? adapter.minZoom;
156
- 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;
157
159
  return useQuery({
158
160
  queryKey: ["provider-sequences", providerId, bboxKey(bbox), zoom],
159
- queryFn: ({ signal }) => adapter.fetchSequences?.(bbox, zoom, signal) ?? [],
161
+ queryFn: ({ signal }) => adapter?.fetchSequences?.(bbox, zoom, signal) ?? [],
160
162
  enabled,
161
163
  placeholderData: (previous) => previous
162
164
  });
163
165
  };
164
166
  var useProviderMapFeatures = (providerId, bbox, zoom) => {
165
167
  const adapter = adapterById[providerId];
166
- 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;
167
170
  return useQuery({
168
171
  queryKey: ["provider-map-features", providerId, bboxKey(bbox), zoom],
169
172
  queryFn: async ({ signal }) => {
@@ -283,11 +286,12 @@ var PhotoProviderLayer = ({
283
286
  shownPhotoId
284
287
  }) => {
285
288
  const adapter = adapterById[providerId];
289
+ const meta = providerById[providerId];
286
290
  const { data: photos = [] } = useProviderPhotos(providerId, bbox, zoom);
287
291
  const { data: sequences = [] } = useProviderSequences(providerId, bbox, zoom);
288
- 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)) : [];
289
293
  const drawBbox = bbox ? padBbox(bbox, 0.25) : null;
290
- const photoCollection = zoom >= adapter.minZoom ? photosToFeatureCollection(
294
+ const photoCollection = zoom >= meta.minZoom ? photosToFeatureCollection(
291
295
  drawBbox ? visiblePhotos.filter((photo) => lngLatInBbox(photo.lngLat, drawBbox)) : visiblePhotos
292
296
  ) : emptyPointCollection();
293
297
  const viewfieldsActive = showViewfields && zoom >= VIEWFIELD_MIN_ZOOM;
@@ -338,7 +342,7 @@ var PhotoProviderLayer = ({
338
342
  sequenceCapturedAtById.set(photo.sequenceId, photo.capturedAt);
339
343
  }
340
344
  }
341
- const filteredSequences = showSequences && zoom >= (adapter.sequencesMinZoom ?? adapter.minZoom) ? sequences.map((sequence) => {
345
+ const filteredSequences = showSequences && zoom >= meta.sequencesMinZoom ? sequences.map((sequence) => {
342
346
  if (sequence.capturedAt != null) return sequence;
343
347
  const fromPhotos = sequenceCapturedAtById.get(sequence.sequenceId);
344
348
  return fromPhotos != null ? { ...sequence, capturedAt: fromPhotos } : sequence;
@@ -383,7 +387,29 @@ var PhotoProviderLayer = ({
383
387
  });
384
388
  const sequenceCollection = alignedSequences.length > 0 ? sequencesToFeatureCollection(alignedSequences) : emptyLineCollection();
385
389
  return /* @__PURE__ */ jsxs(Fragment, { children: [
386
- 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: [
387
413
  /* @__PURE__ */ jsx(
388
414
  Source,
389
415
  {
@@ -484,10 +510,10 @@ var MapFeatureProviderLayer = ({
484
510
  filter,
485
511
  mapFeatureCircleColor
486
512
  }) => {
487
- const adapter = adapterById[providerId];
513
+ const meta = providerById[providerId];
488
514
  const { data: features = [] } = useProviderMapFeatures(providerId, bbox, zoom);
489
515
  const valueFilter = filter?.mapFeatureValue;
490
- const featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(
516
+ const featureCollection = zoom >= meta.minZoom ? mapFeaturesToFeatureCollection(
491
517
  valueFilter ? features.filter((feature) => valueFilter(feature.value)) : features
492
518
  ) : emptyPointCollection();
493
519
  const featureFilter = buildMapFeatureLayerFilter(filter?.date);
@@ -520,8 +546,7 @@ var MapFeatureProviderLayer = ({
520
546
  ] });
521
547
  };
522
548
  var ProviderLayer = (props) => {
523
- const adapter = adapterById[props.providerId];
524
- 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 });
525
550
  };
526
551
  var StreetLevelImagerySourcesAndLayers = ({
527
552
  providers,
@@ -562,7 +587,7 @@ var StreetLevelImagerySourcesAndLayers = ({
562
587
  const selectedSequence = resolveSelectedSequence(selectedPhoto, sequences, activeSequenceId);
563
588
  const providerDrawsSequences = selectedProviderId != null && showSequences && providers.includes(selectedProviderId);
564
589
  const firstPhotoProvider = providers.find(
565
- (id) => adapterById[id].kind === "photo" && adapterById[id].fetchPhotos
590
+ (id) => providerById[id].kind === "photo" && adapterById[id]?.fetchPhotos
566
591
  );
567
592
  const sequenceBeforeId = firstPhotoProvider ? showViewfields ? viewfieldLayerId(firstPhotoProvider) : photoLayerId(firstPhotoProvider) : void 0;
568
593
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -814,6 +839,24 @@ var usePhotoThumbnail = (photo) => useQuery({
814
839
  enabled: photo != null,
815
840
  staleTime: 24 * 60 * 60 * 1e3
816
841
  });
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
+ ] });
817
860
  var PhotoDate = ({ timestamp, className, localDateTime }) => {
818
861
  const { locale, messages } = useStreetImageryI18n();
819
862
  if (timestamp == null) {
@@ -896,40 +939,74 @@ var ToolbarButton = ({
896
939
  var SuggestionChip = ({
897
940
  suggestion,
898
941
  active,
942
+ shownPhotoId,
943
+ showPlace,
899
944
  onSelect
900
945
  }) => {
901
946
  const { messages } = useStreetImageryI18n();
902
- const best = suggestion.candidates[0];
903
- const label = viewSuggestionLabel(suggestion, messages);
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;
904
955
  return /* @__PURE__ */ jsxs(
905
956
  "button",
906
957
  {
907
958
  "aria-label": best ? label : messages.viewer.noPhoto(label),
908
959
  "aria-pressed": active,
909
960
  className: [
910
- "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",
911
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"
912
963
  ].join(" "),
913
964
  disabled: !best,
914
- onClick: onSelect,
965
+ onClick: () => {
966
+ const next = canStep ? candidates[(shownIndex + 1) % candidates.length] : best;
967
+ if (next) {
968
+ onSelect(next);
969
+ }
970
+ },
915
971
  title: best ? label : messages.viewer.noMatchingPhoto(label),
916
972
  type: "button",
917
973
  children: [
918
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: suggestion.viewpoint.label ?? messages.viewpointRole[suggestion.viewpoint.role] }),
919
- /* @__PURE__ */ jsx(
974
+ /* @__PURE__ */ jsxs(
920
975
  "span",
921
976
  {
922
- "aria-hidden": true,
923
- className: "inline-flex",
924
- style: { transform: `rotate(${suggestion.direction.bearing}deg)` },
925
- children: /* @__PURE__ */ jsx(Icon, { className: "size-3", path: ICONS.arrow })
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
+ ]
926
1003
  }
927
1004
  ),
928
- best ? /* @__PURE__ */ jsx(
1005
+ shown ? /* @__PURE__ */ jsx(
929
1006
  PhotoDate,
930
1007
  {
931
- className: active ? "text-fuchsia-100" : "text-slate-400",
932
- timestamp: best.photo.capturedAt
1008
+ className: `max-w-full truncate ${active ? "text-fuchsia-100" : "text-slate-400"}`,
1009
+ timestamp: shown.photo.capturedAt
933
1010
  }
934
1011
  ) : /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "\u2014" })
935
1012
  ]
@@ -946,6 +1023,7 @@ var FloatingPhotoViewer = ({
946
1023
  suggestions,
947
1024
  activeDirectionKey,
948
1025
  onSelectSuggestion,
1026
+ shownPhotoId,
949
1027
  canGoBack,
950
1028
  canGoForward,
951
1029
  onBack,
@@ -960,6 +1038,7 @@ var FloatingPhotoViewer = ({
960
1038
  storageKey = "street-imagery:floating-viewer"
961
1039
  }) => {
962
1040
  const { messages } = useStreetImageryI18n();
1041
+ const severalViewpoints = new Set(suggestions.map((suggestion) => suggestion.viewpoint.id)).size > 1;
963
1042
  const [layout, setLayout] = useState(() => readLayout(storageKey));
964
1043
  const [drawerOpen, setDrawerOpen] = useState(false);
965
1044
  const dragRef = useRef(null);
@@ -1105,24 +1184,23 @@ var FloatingPhotoViewer = ({
1105
1184
  ]
1106
1185
  }
1107
1186
  ),
1108
- toolbar ? /* @__PURE__ */ jsx("div", { className: "border-b border-slate-100 px-2 py-1.5", children: toolbar }) : suggestions.length > 0 ? /* @__PURE__ */ jsxs(
1187
+ toolbar ? /* @__PURE__ */ jsx("div", { className: "border-b border-slate-100 px-2 py-1.5", children: toolbar }) : suggestions.length > 0 ? /* @__PURE__ */ jsx(
1109
1188
  "nav",
1110
1189
  {
1111
1190
  "aria-label": messages.viewer.suggestedViews,
1112
- className: "flex gap-1 overflow-x-auto border-b border-slate-100 px-2 py-1.5",
1191
+ className: "grid grid-cols-[repeat(auto-fit,minmax(5.25rem,1fr))] gap-1 border-b border-slate-100 px-2 py-1.5",
1113
1192
  title: messages.viewer.suggestedViewsHint,
1114
- children: [
1115
- /* @__PURE__ */ jsx("span", { className: "shrink-0 self-center pr-0.5 text-xs text-slate-500", children: messages.viewer.views }),
1116
- suggestions.map((suggestion) => /* @__PURE__ */ jsx(
1117
- SuggestionChip,
1118
- {
1119
- active: suggestion.direction.key === activeDirectionKey,
1120
- onSelect: () => onSelectSuggestion(suggestion),
1121
- suggestion
1122
- },
1123
- suggestion.direction.key
1124
- ))
1125
- ]
1193
+ children: suggestions.map((suggestion) => /* @__PURE__ */ jsx(
1194
+ SuggestionChip,
1195
+ {
1196
+ active: suggestion.direction.key === activeDirectionKey,
1197
+ onSelect: (candidate) => onSelectSuggestion(suggestion, candidate),
1198
+ shownPhotoId,
1199
+ showPlace: severalViewpoints,
1200
+ suggestion
1201
+ },
1202
+ suggestion.direction.key
1203
+ ))
1126
1204
  }
1127
1205
  ) : null,
1128
1206
  /* @__PURE__ */ jsxs("div", { className: "min-h-0 overflow-y-auto", children: [
@@ -1269,7 +1347,12 @@ var useViewpointSessionStore = create()((set, get) => ({
1269
1347
  showPhoto: (entry) => set((state) => {
1270
1348
  const current = state.history[state.historyIndex];
1271
1349
  if (samePhoto(current?.photo, entry.photo)) {
1272
- 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 };
1273
1356
  }
1274
1357
  const history = [...state.history.slice(0, state.historyIndex + 1), entry].slice(
1275
1358
  -100
@@ -1452,9 +1535,30 @@ var LocationPickOnMap = ({ onPick }) => {
1452
1535
  );
1453
1536
  return null;
1454
1537
  };
1455
- var MapillaryFeatureBar = ({ data, shownImage, onShow }) => {
1456
- const { locale, messages } = useStreetImageryI18n();
1457
- const labels = messages.feature;
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 };
1458
1562
  const { feature, days, images } = data;
1459
1563
  return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1460
1564
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", title: feature.value, children: [
@@ -1509,10 +1613,14 @@ ${labels.dayPhotos(day.images.length, active && day.images.length > 1)}`,
1509
1613
  children: [
1510
1614
  /* @__PURE__ */ jsx("span", { className: "font-medium", children: formatMonth(noon, locale) }),
1511
1615
  " ",
1512
- /* @__PURE__ */ jsxs("span", { className: active ? "text-amber-800" : "text-slate-500", children: [
1513
- active ? `${shownIndex + 1}/` : "",
1514
- day.images.length
1515
- ] })
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
+ )
1516
1624
  ]
1517
1625
  },
1518
1626
  day.day
@@ -1523,4 +1631,4 @@ ${labels.dayPhotos(day.images.length, active && day.images.length > 1)}`,
1523
1631
  ] });
1524
1632
  };
1525
1633
 
1526
- export { BASE_COLOR, FloatingPhotoViewer, FloatingViewerInfoButton, LocationPickOnMap, MapillaryFeatureBar, PhotoDate, SELECTION_COLOR, StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, VIEW_SHAPE_FILL_OPACITY, ViewpointLayer, getViewpointSession, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useActiveDirectionKey, useAllProviderMapFeatures, useAllProviderPhotos, useArmedLocationOpenerId, useCanGoBack, useCanGoForward, useCurrentHistoryEntry, useLocationPickActions, useMapViewportBbox, useMapillaryImageDetections, useMapillaryMapFeatureImages, useMapillaryMapFeaturesNear, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences, useViewSuggestions, useViewpointLine, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel };
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 };
@@ -1,8 +1,11 @@
1
1
  import { type MapFeatureImages, type TargetImage } from '@osm-editor-kit/street-imagery';
2
+ import { type MapillaryFeatureBarLabels } from './featureBarLabels';
2
3
  export type MapillaryFeatureBarProps = {
3
4
  data: MapFeatureImages;
4
5
  shownImage: TargetImage | null;
5
6
  onShow: (image: TargetImage) => void;
7
+ /** Replace single texts, e.g. `name` to map values to your own sign catalogue. */
8
+ labels?: Partial<MapillaryFeatureBarLabels>;
6
9
  };
7
10
  /**
8
11
  * A Mapillary sign or object above its photo: what it is, when Mapillary saw it, and one button
@@ -10,4 +13,4 @@ export type MapillaryFeatureBarProps = {
10
13
  * that day's photos. Dates show month and year; the tooltip has the full date and the age.
11
14
  * Texts and dates follow the `StreetImageryLocaleProvider`.
12
15
  */
13
- export declare const MapillaryFeatureBar: ({ data, shownImage, onShow }: MapillaryFeatureBarProps) => import("react").JSX.Element;
16
+ export declare const MapillaryFeatureBar: ({ data, shownImage, onShow, labels: labelOverrides, }: MapillaryFeatureBarProps) => import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { type StreetImageryLocale } from '@osm-editor-kit/street-imagery';
2
+ /**
3
+ * Texts of `MapillaryFeatureBar`. Kept out of the package-wide messages, so the German sign names
4
+ * are only bundled by apps that show the feature bar.
5
+ */
6
+ export type MapillaryFeatureBarLabels = {
7
+ /** Name of a Mapillary value like `regulatory--turn-right-ahead--g1`. */
8
+ name: (value: string) => string;
9
+ /** Line below the name; `first` and `last` are the rendered dates. */
10
+ seen: string;
11
+ noPhotos: string;
12
+ daysSummary: (photos: number, days: number) => string;
13
+ /** Third tooltip line of a day button, below date and age. */
14
+ dayPhotos: (photos: number, canStep: boolean) => string;
15
+ };
16
+ export declare const MAPILLARY_FEATURE_BAR_LABELS: Record<StreetImageryLocale, MapillaryFeatureBarLabels>;
@@ -1,4 +1,4 @@
1
- import { useStreetImageryI18n } from './chunk-HA4GJWHI.js';
1
+ import { useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
2
2
  import { providerById, formatLocalDateTime, formatDate, formatRelativeDate } from '@osm-editor-kit/street-imagery';
3
3
  import { useRef, useEffect } from 'react';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -1,4 +1,4 @@
1
- import type { ViewSuggestion } from '@osm-editor-kit/street-imagery';
1
+ import type { PhotoCandidate, ViewSuggestion } from '@osm-editor-kit/street-imagery';
2
2
  import { type ReactNode } from 'react';
3
3
  /** Info button for `titleActions`; `label` is its tooltip and accessible name. */
4
4
  export declare const FloatingViewerInfoButton: ({ label, onClick, }: {
@@ -12,7 +12,13 @@ export type FloatingPhotoViewerProps = {
12
12
  /** Suggested views (viewpoint × direction); shown as chips. Empty hides the strip. */
13
13
  suggestions: ViewSuggestion[];
14
14
  activeDirectionKey: string | null;
15
- onSelectSuggestion: (suggestion: ViewSuggestion) => void;
15
+ /**
16
+ * A view button was clicked. `candidate` is the photo to show: the view's best one, or the next
17
+ * one when the active view is clicked again.
18
+ */
19
+ onSelectSuggestion: (suggestion: ViewSuggestion, candidate: PhotoCandidate) => void;
20
+ /** The photo in the viewer, so the active view can show which of its photos that is. */
21
+ shownPhotoId?: string | null;
16
22
  canGoBack: boolean;
17
23
  canGoForward: boolean;
18
24
  onBack: () => void;
@@ -51,4 +57,4 @@ export type FloatingPhotoViewerProps = {
51
57
  * Drag the header to move, drag the left edge to resize. Keys: Esc closes, [ and ] step history,
52
58
  * Alt + arrow left/right step along (`step`).
53
59
  */
54
- export declare const FloatingPhotoViewer: ({ title, titleActions, suggestions, activeDirectionKey, onSelectSuggestion, canGoBack, canGoForward, onBack, onForward, onClose, step, toolbar, status, children, footer, drawer, storageKey, }: FloatingPhotoViewerProps) => import("react").JSX.Element;
60
+ export declare const FloatingPhotoViewer: ({ title, titleActions, suggestions, activeDirectionKey, onSelectSuggestion, shownPhotoId, canGoBack, canGoForward, onBack, onForward, onClose, step, toolbar, status, children, footer, drawer, storageKey, }: FloatingPhotoViewerProps) => import("react").JSX.Element;
@@ -19,7 +19,10 @@ export declare const useCanGoForward: () => boolean;
19
19
  export declare const getViewpointSession: () => {
20
20
  actions: {
21
21
  open: (input: OpenInput) => void;
22
- /** Show a photo; truncates forward history. No-op when it is already the current photo. */
22
+ /**
23
+ * Show a photo; truncates forward history. For the photo that is shown already, only its
24
+ * direction changes.
25
+ */
23
26
  showPhoto: (entry: ViewpointHistoryEntry) => void;
24
27
  back: () => ViewpointHistoryEntry | null;
25
28
  forward: () => ViewpointHistoryEntry | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osm-editor-kit/street-imagery-react",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "React MapLibre layers and photo viewers for street-level imagery providers.",
5
5
  "homepage": "https://github.com/osmberlin/street-space-editor/tree/main/packages/street-imagery-react",
6
6
  "license": "MIT",
@@ -14,6 +14,7 @@
14
14
  "dist"
15
15
  ],
16
16
  "type": "module",
17
+ "sideEffects": false,
17
18
  "exports": {
18
19
  ".": {
19
20
  "types": "./dist/index.d.ts",
@@ -55,7 +56,7 @@
55
56
  "zustand": "^5.0.7"
56
57
  },
57
58
  "peerDependencies": {
58
- "@osm-editor-kit/street-imagery": "^0.1.0-alpha.5",
59
+ "@osm-editor-kit/street-imagery": "^0.1.0-alpha.6",
59
60
  "@panoramax/web-viewer": "^5",
60
61
  "@tanstack/react-query": "^5",
61
62
  "mapillary-js": "^4",