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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +113 -0
  2. package/dist/MapillaryPanel-N45NQY4C.js +2 -0
  3. package/dist/PanoramaxPanel-MFI4L67Z.js +2 -0
  4. package/dist/StreetLevelImagerySelectionOverlay.d.ts +20 -3
  5. package/dist/StreetLevelImagerySourcesAndLayers.d.ts +14 -1
  6. package/dist/StreetLevelImageryViewCone.d.ts +11 -1
  7. package/dist/StreetLevelImageryViewer.d.ts +15 -1
  8. package/dist/chunk-53RA4OSJ.js +304 -0
  9. package/dist/chunk-EQ2JKLGT.js +311 -0
  10. package/dist/chunk-HA4GJWHI.js +120 -0
  11. package/dist/hooks/useAllProviderMapFeatures.d.ts +0 -4
  12. package/dist/i18n/PhotoDate.d.ts +16 -0
  13. package/dist/i18n/StreetImageryLocaleProvider.d.ts +21 -0
  14. package/dist/i18n/messages.d.ts +35 -0
  15. package/dist/index.d.ts +15 -2
  16. package/dist/index.js +984 -117
  17. package/dist/mapillary/MapillaryFeatureBar.d.ts +13 -0
  18. package/dist/mapillary/useMapillaryFeatures.d.ts +36 -0
  19. package/dist/openers/LocationPickOnMap.d.ts +10 -0
  20. package/dist/openers/useLocationPickStore.d.ts +9 -0
  21. package/dist/panels/MapillaryPanel.d.ts +19 -1
  22. package/dist/panels/PanoramaxPanel.d.ts +8 -1
  23. package/dist/panels/mapillaryLookAt.d.ts +41 -0
  24. package/dist/panels/panoramaxLimitPanning.d.ts +54 -0
  25. package/dist/panels/panoramaxPhotoFromMetadata.d.ts +8 -0
  26. package/dist/photo-details/PhotoDetailsDialog.d.ts +21 -0
  27. package/dist/photo-details/index.d.ts +6 -0
  28. package/dist/photo-details/labels.d.ts +33 -0
  29. package/dist/photo-details.js +235 -0
  30. package/dist/viewpoints/FloatingPhotoViewer.d.ts +54 -0
  31. package/dist/viewpoints/ViewpointLayer.d.ts +17 -0
  32. package/dist/viewpoints/useViewSuggestions.d.ts +23 -0
  33. package/dist/viewpoints/useViewpointSessionStore.d.ts +32 -0
  34. package/dist/viewpoints/viewDirectionLabels.d.ts +4 -0
  35. package/package.json +11 -3
  36. package/dist/MapillaryPanel-QX6NESX7.js +0 -2
  37. package/dist/PanoramaxPanel-J6ANR6LB.js +0 -2
  38. package/dist/chunk-BS6LPDMY.js +0 -143
  39. package/dist/chunk-ZH3TPW4D.js +0 -165
@@ -0,0 +1,13 @@
1
+ import { type MapFeatureImages, type TargetImage } from '@osm-editor-kit/street-imagery';
2
+ export type MapillaryFeatureBarProps = {
3
+ data: MapFeatureImages;
4
+ shownImage: TargetImage | null;
5
+ onShow: (image: TargetImage) => void;
6
+ };
7
+ /**
8
+ * A Mapillary sign or object above its photo: what it is, when Mapillary saw it, and one button
9
+ * per capture day (newest first). The shown day is highlighted; clicking it again steps through
10
+ * that day's photos. Dates show month and year; the tooltip has the full date and the age.
11
+ * Texts and dates follow the `StreetImageryLocaleProvider`.
12
+ */
13
+ export declare const MapillaryFeatureBar: ({ data, shownImage, onShow }: MapillaryFeatureBarProps) => import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { type LngLat, type TargetShape } from '@osm-editor-kit/street-imagery';
2
+ /**
3
+ * A Mapillary map feature (sign, traffic light, marking) with all images that show it, grouped by
4
+ * day, and the outlines Mapillary linked to it. Feed `days[0].best` into the viewer with
5
+ * `lookAt={{ lngLat: feature.lngLat, outline: image.outline }}`.
6
+ */
7
+ export declare const useMapillaryMapFeatureImages: (featureId: string | null | undefined, { shape }?: {
8
+ shape?: TargetShape;
9
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").MapFeatureImages | null>, Error>;
10
+ /**
11
+ * Map features within `radiusMeters` of a point (Graph API), nearest first, e.g. the traffic
12
+ * lights and markings at a junction. `objectValues` accepts wildcards (`object--traffic-light--*`).
13
+ */
14
+ export declare const useMapillaryMapFeaturesNear: (lngLat: LngLat | null | undefined, { radiusMeters, objectValues, enabled, }?: {
15
+ radiusMeters?: number;
16
+ objectValues?: string[];
17
+ enabled?: boolean;
18
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<{
19
+ id: string;
20
+ value: string;
21
+ lngLat: LngLat;
22
+ firstSeenAt: number | null;
23
+ lastSeenAt: number | null;
24
+ facing: number | null;
25
+ imageIds: string[];
26
+ distanceMeters: number;
27
+ }[]>, Error>;
28
+ /**
29
+ * Detection outlines of one image, for `outlines` of the Mapillary viewer. An image has ~500
30
+ * detections; pass `filter` (e.g. `matchesAnyGroup(JUNCTION_DETECTION_GROUPS)`) and name it with
31
+ * `filterKey` so the cache can tell filters apart.
32
+ */
33
+ export declare const useMapillaryImageDetections: (imageId: string | null | undefined, { filter, filterKey }?: {
34
+ filter?: (value: string) => boolean;
35
+ filterKey?: string;
36
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").MapillaryDetection[]>, Error>;
@@ -0,0 +1,10 @@
1
+ import { type LocationOpener, type OpenTarget } from '@osm-editor-kit/street-imagery';
2
+ export type LocationPickOnMapProps = {
3
+ /** Called after the place was opened, e.g. to mark it on the map. */
4
+ onPick?: (target: OpenTarget, opener: LocationOpener) => void;
5
+ };
6
+ /**
7
+ * Render inside `<Map>`. While a location opener is armed, the next map click opens the clicked
8
+ * place in a new tab and disarms; Escape disarms. The map's own click handlers still run.
9
+ */
10
+ export declare const LocationPickOnMap: ({ onPick }: LocationPickOnMapProps) => null;
@@ -0,0 +1,9 @@
1
+ import type { LocationOpenerId } from '@osm-editor-kit/street-imagery';
2
+ /** The opener waiting for a map click, or `null`. */
3
+ export declare const useArmedLocationOpenerId: () => LocationOpenerId | null;
4
+ export declare const useLocationPickActions: () => {
5
+ arm: (openerId: LocationOpenerId) => void;
6
+ disarm: () => void;
7
+ /** Arm, or disarm when this opener is armed already. */
8
+ toggle: (openerId: LocationOpenerId) => void;
9
+ };
@@ -1,11 +1,29 @@
1
1
  import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
2
  import 'mapillary-js/dist/mapillary.css';
3
3
  import type { StreetImageryPhotoSelection } from '../types';
4
+ import { type MapillaryLookAt, type MapillaryViewerOutline } from './mapillaryLookAt';
4
5
  type MapillaryPanelProps = {
5
6
  photo: NormalizedPhoto;
6
7
  groupPhotos: NormalizedPhoto[];
7
8
  onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
8
9
  onEaseMapToPoint: (lng: number, lat: number) => void;
10
+ /** Full photo data for each image the viewer shows (incl. native prev/next navigation). */
11
+ onViewerPhoto?: (photo: NormalizedPhoto) => void;
12
+ /** 360° photos: turn the view to this map bearing when `photo` opens (e.g. a suggested view). */
13
+ lookAtBearing?: number | null;
14
+ /**
15
+ * Turn (and zoom) the view to a place when `photo` opens: a sign, a traffic light, a junction.
16
+ * Works for 360° and flat photos; wins over `lookAtBearing`.
17
+ */
18
+ lookAt?: MapillaryLookAt | null;
19
+ /** Outlines to draw in the shown image, e.g. the detections you care about. */
20
+ outlines?: MapillaryViewerOutline[];
21
+ /**
22
+ * Hide the viewer's own attribution (creator and year). The host must then show the creator
23
+ * (`creatorName` of `onViewerPhoto`) and a link to the image on Mapillary itself; the images
24
+ * are CC BY-SA.
25
+ */
26
+ hideAttribution?: boolean;
9
27
  };
10
- export declare const MapillaryPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, }: MapillaryPanelProps) => import("react").JSX.Element;
28
+ export declare const MapillaryPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, onViewerPhoto, lookAtBearing, lookAt, outlines, hideAttribution, }: MapillaryPanelProps) => import("react").JSX.Element;
11
29
  export {};
@@ -6,6 +6,13 @@ type PanoramaxPanelProps = {
6
6
  groupPhotos: NormalizedPhoto[];
7
7
  onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
8
8
  onEaseMapToPoint: (lng: number, lat: number) => void;
9
+ /** The shown picture with creator, licence, local capture time and camera. */
10
+ onViewerPhoto?: (photo: NormalizedPhoto) => void;
11
+ /**
12
+ * Hide the viewer's own legend (a bottom drawer in narrow containers). The host must then show
13
+ * creator and licence itself, from `onViewerPhoto`.
14
+ */
15
+ hideLegend?: boolean;
9
16
  };
10
- export declare const PanoramaxPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, }: PanoramaxPanelProps) => import("react").JSX.Element;
17
+ export declare const PanoramaxPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, onViewerPhoto, hideLegend, }: PanoramaxPanelProps) => import("react").JSX.Element;
11
18
  export {};
@@ -0,0 +1,41 @@
1
+ import { type LngLat, type TargetShape, type ViewTarget } from '@osm-editor-kit/street-imagery';
2
+ import { type Image, type Viewer } from 'mapillary-js';
3
+ /** A place the Mapillary viewer should turn to: a sign, a traffic light, a junction … */
4
+ export type MapillaryLookAt = {
5
+ lngLat: LngLat;
6
+ /** Size and height of the target; decides zoom and vertical position. Default: a traffic sign. */
7
+ shape?: TargetShape;
8
+ /** The target's outline in the shown image, if known; it is more exact than the location. */
9
+ outline?: [number, number][];
10
+ /**
11
+ * Mapillary value of the target (`regulatory--bicycles-only--g1`). Without an `outline`, the
12
+ * image's own detection of this value is used: Mapillary links a map feature to few of its
13
+ * images, but almost every image has a detection of it. With several, the one nearest to the
14
+ * computed position wins (the computed position alone can be far off in 360° images).
15
+ */
16
+ value?: string;
17
+ /** Label for the outline drawn around the target. */
18
+ label?: string;
19
+ };
20
+ export type MapillaryViewerOutline = {
21
+ id: string;
22
+ /** Image ("basic") coordinates, `[0, 0]` top left to `[1, 1]` bottom right. */
23
+ outline: [number, number][];
24
+ label?: string;
25
+ /** CSS color number, e.g. `0xffff00`. Default yellow. */
26
+ color?: number;
27
+ };
28
+ /**
29
+ * Where the target is in the image: from its outline, else computed from the locations
30
+ * (360°: the viewer's own projection; flat: a pinhole model). `undefined` when a flat image does
31
+ * not show the target.
32
+ */
33
+ export declare const mapillaryViewFor: (viewer: Viewer, image: Image, { lngLat, shape, outline }: MapillaryLookAt) => Promise<ViewTarget | undefined>;
34
+ /**
35
+ * Turns the viewer to the target. Returns the outline it aimed at (given, or the image's own
36
+ * detection of `value`), or `null` when it aimed by location; `false` when the shown (flat) image
37
+ * does not show the target.
38
+ */
39
+ export declare const turnMapillaryViewerTo: (viewer: Viewer, image: Image, lookAt: MapillaryLookAt) => Promise<MapillaryViewerOutline | null | false>;
40
+ /** Replaces the outlines drawn in the viewer (needs the viewer's `tag` component). */
41
+ export declare const setMapillaryViewerOutlines: (viewer: Viewer, outlines: readonly MapillaryViewerOutline[]) => void;
@@ -0,0 +1,54 @@
1
+ /** The parts of Photo Sphere Viewer (inside the Panoramax viewer) that limiting the view needs. */
2
+ export type PsvForPanLimit = {
3
+ state: {
4
+ hFov: number;
5
+ vFov: number;
6
+ textureData?: {
7
+ panoData?: {
8
+ baseData?: {
9
+ fullWidth: number;
10
+ fullHeight: number;
11
+ croppedWidth: number;
12
+ croppedHeight: number;
13
+ croppedX: number;
14
+ croppedY: number;
15
+ };
16
+ };
17
+ };
18
+ };
19
+ dataHelper: {
20
+ fovToZoomLevel(fov: number): number;
21
+ };
22
+ getZoomLevel(): number;
23
+ zoom(level: number): void;
24
+ getPosition(): {
25
+ yaw: number;
26
+ pitch: number;
27
+ };
28
+ rotate(position: {
29
+ yaw: number;
30
+ pitch: number;
31
+ }): void;
32
+ addEventListener(type: string, listener: (event: Event) => void): void;
33
+ removeEventListener(type: string, listener: (event: Event) => void): void;
34
+ };
35
+ type ViewPosition = {
36
+ yaw: number;
37
+ pitch: number;
38
+ };
39
+ /**
40
+ * The view position moved back so the image stays in view. Flat photos are shown as a small
41
+ * patch of a sphere; outside the patch there is only black. 360° photos have no limit.
42
+ */
43
+ export declare const limitToImage: (psv: PsvForPanLimit, position: ViewPosition) => ViewPosition;
44
+ /**
45
+ * The widest zoom level for a flat photo: the image with a small margin around it (in the
46
+ * tighter direction). `null` when there is no limit.
47
+ */
48
+ export declare const widestZoomLevel: (psv: PsvForPanLimit) => number | null;
49
+ /**
50
+ * Keep the view inside the image while the user pans and zooms. Returns a function that removes
51
+ * the listeners again.
52
+ */
53
+ export declare const limitPanningToImage: (psv: PsvForPanLimit) => (() => void);
54
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
+ import type { PnxPictureMetadata } from './panoramax-photo-viewer.d';
3
+ /**
4
+ * The shown picture as a photo with everything the Panoramax viewer knows about it: creator,
5
+ * licence, local capture time, camera. The viewer's own legend holds the same; hosts show it in
6
+ * their own layout instead.
7
+ */
8
+ export declare const panoramaxPhotoFromMetadata: (metadata: PnxPictureMetadata) => NormalizedPhoto;
@@ -0,0 +1,21 @@
1
+ import { type NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
+ import { type PhotoDetailsLabels } from './labels';
3
+ export type PhotoDetailsDialogProps = {
4
+ /** The photo with its `details`, as a viewer's `onViewerPhoto` gives it. */
5
+ photo: NormalizedPhoto;
6
+ open: boolean;
7
+ onClose: () => void;
8
+ /** Link to the photo on the provider's site, shown at the photo id. */
9
+ externalUrl?: string;
10
+ /** Replace single texts; the rest follows the `StreetImageryLocaleProvider`'s language. */
11
+ labels?: Partial<PhotoDetailsLabels>;
12
+ };
13
+ /**
14
+ * Everything known about a photo, as a modal: capture, camera, position, source and raw EXIF.
15
+ * Replaces the providers' own legends (Panoramax's drawer), which do not fit a small viewer box.
16
+ * A native `<dialog>`: Escape and a click on the backdrop close it.
17
+ *
18
+ * Lives in its own entry (`@osm-editor-kit/street-imagery-react/photo-details`), so apps that do
19
+ * not import it do not bundle it.
20
+ */
21
+ export declare const PhotoDetailsDialog: ({ photo, open, onClose, externalUrl, labels, }: PhotoDetailsDialogProps) => import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Optional entry: the photo details dialog. Import it from
3
+ * `@osm-editor-kit/street-imagery-react/photo-details`; apps that do not, do not bundle it.
4
+ */
5
+ export { PhotoDetailsDialog, type PhotoDetailsDialogProps } from './PhotoDetailsDialog';
6
+ export { PHOTO_DETAILS_LABELS, type PhotoDetailsLabels } from './labels';
@@ -0,0 +1,33 @@
1
+ import type { StreetImageryLocale } from '@osm-editor-kit/street-imagery';
2
+ /** Texts of the photo details dialog and of the button that opens it. */
3
+ export type PhotoDetailsLabels = {
4
+ open: string;
5
+ title: string;
6
+ close: string;
7
+ sectionCapture: string;
8
+ sectionCamera: string;
9
+ sectionPosition: string;
10
+ sectionSource: string;
11
+ creator: string;
12
+ contact: string;
13
+ capturedAt: string;
14
+ uploadedAt: string;
15
+ license: string;
16
+ type: string;
17
+ pano: string;
18
+ flat: string;
19
+ camera: string;
20
+ focalLength: string;
21
+ fieldOfView: string;
22
+ coordinates: string;
23
+ heading: string;
24
+ accuracy: string;
25
+ provider: string;
26
+ instance: string;
27
+ photoId: string;
28
+ sequence: string;
29
+ rankInSequence: string;
30
+ file: string;
31
+ exif: (count: number) => string;
32
+ };
33
+ export declare const PHOTO_DETAILS_LABELS: Record<StreetImageryLocale, PhotoDetailsLabels>;
@@ -0,0 +1,235 @@
1
+ import { useStreetImageryI18n } from './chunk-HA4GJWHI.js';
2
+ import { providerById, formatLocalDateTime, formatDate, formatRelativeDate } from '@osm-editor-kit/street-imagery';
3
+ import { useRef, useEffect } from 'react';
4
+ import { jsx, jsxs } from 'react/jsx-runtime';
5
+
6
+ // src/photo-details/labels.ts
7
+ var PHOTO_DETAILS_LABELS = {
8
+ en: {
9
+ open: "Details of this photo",
10
+ title: "Photo details",
11
+ close: "Close",
12
+ sectionCapture: "Capture",
13
+ sectionCamera: "Camera",
14
+ sectionPosition: "Position",
15
+ sectionSource: "Source",
16
+ creator: "Creator",
17
+ contact: "Contact",
18
+ capturedAt: "Captured",
19
+ uploadedAt: "Uploaded",
20
+ license: "Licence",
21
+ type: "Type",
22
+ pano: "360\xB0 panorama",
23
+ flat: "Flat photo",
24
+ camera: "Camera",
25
+ focalLength: "Focal length",
26
+ fieldOfView: "Field of view",
27
+ coordinates: "Coordinates",
28
+ heading: "Heading",
29
+ accuracy: "Accuracy",
30
+ provider: "Provider",
31
+ instance: "Instance",
32
+ photoId: "Photo ID",
33
+ sequence: "Sequence",
34
+ rankInSequence: "Position in sequence",
35
+ file: "File",
36
+ exif: (count) => `EXIF (${count} tags)`
37
+ },
38
+ de: {
39
+ open: "Details zu diesem Foto",
40
+ title: "Fotodetails",
41
+ close: "Schlie\xDFen",
42
+ sectionCapture: "Aufnahme",
43
+ sectionCamera: "Kamera",
44
+ sectionPosition: "Position",
45
+ sectionSource: "Quelle",
46
+ creator: "Urheber:in",
47
+ contact: "Kontakt",
48
+ capturedAt: "Aufgenommen",
49
+ uploadedAt: "Hochgeladen",
50
+ license: "Lizenz",
51
+ type: "Typ",
52
+ pano: "360\xB0-Panorama",
53
+ flat: "Normales Foto",
54
+ camera: "Kamera",
55
+ focalLength: "Brennweite",
56
+ fieldOfView: "Bildwinkel",
57
+ coordinates: "Koordinaten",
58
+ heading: "Blickrichtung",
59
+ accuracy: "Genauigkeit",
60
+ provider: "Anbieter",
61
+ instance: "Instanz",
62
+ photoId: "Foto-ID",
63
+ sequence: "Sequenz",
64
+ rankInSequence: "Position in der Sequenz",
65
+ file: "Datei",
66
+ exif: (count) => `EXIF (${count} Eintr\xE4ge)`
67
+ }
68
+ };
69
+ var formatBytes = (bytes) => bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} kB`;
70
+ var LINK_CLASS = "text-zinc-950 underline decoration-zinc-950/30 hover:decoration-zinc-950";
71
+ var Section = ({ title, rows }) => {
72
+ const filled = rows.filter(([, value]) => value != null && value !== "");
73
+ if (filled.length === 0) {
74
+ return null;
75
+ }
76
+ return /* @__PURE__ */ jsxs("section", { className: "mt-6 first:mt-0", children: [
77
+ /* @__PURE__ */ jsx("h3", { className: "text-xs/5 font-semibold tracking-wide text-zinc-500 uppercase", children: title }),
78
+ /* @__PURE__ */ jsx("dl", { className: "mt-1 grid grid-cols-[minmax(7rem,35%)_1fr] text-sm/6", children: filled.map(([term, value]) => /* @__PURE__ */ jsxs("div", { className: "col-span-2 grid grid-cols-subgrid border-t border-zinc-950/5", children: [
79
+ /* @__PURE__ */ jsx("dt", { className: "py-2 pr-3 text-zinc-500", children: term }),
80
+ /* @__PURE__ */ jsx("dd", { className: "min-w-0 py-2 wrap-break-word text-zinc-950", children: value })
81
+ ] }, term)) })
82
+ ] });
83
+ };
84
+ var PhotoDetailsDialog = ({
85
+ photo,
86
+ open,
87
+ onClose,
88
+ externalUrl,
89
+ labels
90
+ }) => {
91
+ const { locale } = useStreetImageryI18n();
92
+ const t = { ...PHOTO_DETAILS_LABELS[locale], ...labels };
93
+ const dialogRef = useRef(null);
94
+ useEffect(
95
+ function syncOpenState() {
96
+ const dialog = dialogRef.current;
97
+ if (!dialog) {
98
+ return;
99
+ }
100
+ if (open && !dialog.open) {
101
+ dialog.showModal();
102
+ dialog.firstElementChild?.scrollTo({ top: 0 });
103
+ } else if (!open && dialog.open) {
104
+ dialog.close();
105
+ }
106
+ },
107
+ [open]
108
+ );
109
+ const details = photo.details ?? {};
110
+ const provider = providerById[photo.providerId];
111
+ const capturedAt = (details.capturedAtLocal ? formatLocalDateTime(details.capturedAtLocal, locale) : null) ?? (photo.capturedAt != null ? formatDate(photo.capturedAt, locale) : null);
112
+ const contact = details.creatorContact?.includes("@") ? /* @__PURE__ */ jsx("a", { className: LINK_CLASS, href: `mailto:${details.creatorContact}`, children: details.creatorContact }) : details.creatorContact;
113
+ const license = details.license && details.licenseUrl ? /* @__PURE__ */ jsx("a", { className: LINK_CLASS, href: details.licenseUrl, rel: "noreferrer", target: "_blank", children: details.license }) : details.license;
114
+ const photoId = externalUrl ? /* @__PURE__ */ jsx("a", { className: LINK_CLASS, href: externalUrl, rel: "noreferrer", target: "_blank", children: photo.photoId }) : photo.photoId;
115
+ const exif = Object.entries(details.exif ?? {}).sort(([a], [b]) => a.localeCompare(b));
116
+ return /* @__PURE__ */ jsx(
117
+ "dialog",
118
+ {
119
+ "aria-labelledby": "photo-details-title",
120
+ className: "m-auto w-full max-w-lg rounded-2xl bg-white p-0 shadow-lg ring-1 ring-zinc-950/10 backdrop:bg-zinc-950/25",
121
+ ref: dialogRef,
122
+ onClick: (event) => {
123
+ if (event.target === event.currentTarget) {
124
+ onClose();
125
+ }
126
+ },
127
+ onClose,
128
+ onKeyDown: (event) => {
129
+ if (event.key === "Escape") {
130
+ event.stopPropagation();
131
+ }
132
+ },
133
+ children: /* @__PURE__ */ jsxs("div", { className: "max-h-[85vh] overflow-y-auto p-6 sm:p-8", children: [
134
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
135
+ /* @__PURE__ */ jsxs("h2", { className: "text-base/6 font-semibold text-zinc-950", id: "photo-details-title", children: [
136
+ t.title,
137
+ /* @__PURE__ */ jsx("span", { className: "ml-2 font-normal text-zinc-500", children: provider.label })
138
+ ] }),
139
+ /* @__PURE__ */ jsx(
140
+ "button",
141
+ {
142
+ className: "-m-1 rounded-md p-1 text-sm/6 font-medium text-zinc-500 hover:bg-zinc-950/5 hover:text-zinc-950",
143
+ type: "button",
144
+ onClick: onClose,
145
+ children: t.close
146
+ }
147
+ )
148
+ ] }),
149
+ /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
150
+ /* @__PURE__ */ jsx(
151
+ Section,
152
+ {
153
+ rows: [
154
+ [t.creator, photo.creatorName],
155
+ [t.contact, contact],
156
+ [
157
+ t.capturedAt,
158
+ capturedAt && photo.capturedAt != null ? `${capturedAt} \xB7 ${formatRelativeDate(photo.capturedAt, locale)}` : capturedAt
159
+ ],
160
+ [
161
+ t.uploadedAt,
162
+ details.uploadedAt != null ? formatDate(details.uploadedAt, locale) : null
163
+ ],
164
+ [t.license, license]
165
+ ],
166
+ title: t.sectionCapture
167
+ }
168
+ ),
169
+ /* @__PURE__ */ jsx(
170
+ Section,
171
+ {
172
+ rows: [
173
+ [t.type, photo.isPano == null ? null : photo.isPano ? t.pano : t.flat],
174
+ [t.camera, details.camera],
175
+ [t.focalLength, details.focalLengthMm != null ? `${details.focalLengthMm} mm` : null],
176
+ [
177
+ t.fieldOfView,
178
+ details.fieldOfViewDeg != null ? `${Math.round(details.fieldOfViewDeg)}\xB0` : null
179
+ ]
180
+ ],
181
+ title: t.sectionCamera
182
+ }
183
+ ),
184
+ /* @__PURE__ */ jsx(
185
+ Section,
186
+ {
187
+ rows: [
188
+ [t.coordinates, `${photo.lngLat[1].toFixed(6)}, ${photo.lngLat[0].toFixed(6)}`],
189
+ [t.heading, photo.heading != null ? `${Math.round(photo.heading)}\xB0` : null],
190
+ [
191
+ t.accuracy,
192
+ details.positionAccuracyMeters != null ? `\xB1 ${details.positionAccuracyMeters} m` : null
193
+ ]
194
+ ],
195
+ title: t.sectionPosition
196
+ }
197
+ ),
198
+ /* @__PURE__ */ jsx(
199
+ Section,
200
+ {
201
+ rows: [
202
+ [t.provider, provider.label],
203
+ [t.instance, details.instance],
204
+ [t.photoId, photoId],
205
+ [t.sequence, photo.sequenceId],
206
+ [t.rankInSequence, details.rankInSequence],
207
+ [
208
+ t.file,
209
+ details.originalFileName ? `${details.originalFileName}${details.originalFileSizeBytes != null ? ` (${formatBytes(details.originalFileSizeBytes)})` : ""}` : null
210
+ ]
211
+ ],
212
+ title: t.sectionSource
213
+ }
214
+ ),
215
+ exif.length > 0 ? /* @__PURE__ */ jsxs("details", { className: "mt-6 border-t border-zinc-950/5 pt-3", children: [
216
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer text-xs/5 font-semibold tracking-wide text-zinc-500 uppercase hover:text-zinc-950", children: t.exif(exif.length) }),
217
+ /* @__PURE__ */ jsx("dl", { className: "mt-2 grid grid-cols-[minmax(7rem,45%)_1fr] text-xs/5", children: exif.map(([key, value]) => /* @__PURE__ */ jsxs(
218
+ "div",
219
+ {
220
+ className: "col-span-2 grid grid-cols-subgrid border-t border-zinc-950/5",
221
+ children: [
222
+ /* @__PURE__ */ jsx("dt", { className: "py-1 pr-3 wrap-break-word text-zinc-500", children: key }),
223
+ /* @__PURE__ */ jsx("dd", { className: "min-w-0 py-1 wrap-break-word text-zinc-950", children: value })
224
+ ]
225
+ },
226
+ key
227
+ )) })
228
+ ] }) : null
229
+ ] })
230
+ ] })
231
+ }
232
+ );
233
+ };
234
+
235
+ export { PHOTO_DETAILS_LABELS, PhotoDetailsDialog };
@@ -0,0 +1,54 @@
1
+ import type { ViewSuggestion } from '@osm-editor-kit/street-imagery';
2
+ import { type ReactNode } from 'react';
3
+ /** Info button for `titleActions`; `label` is its tooltip and accessible name. */
4
+ export declare const FloatingViewerInfoButton: ({ label, onClick, }: {
5
+ label: string;
6
+ onClick: () => void;
7
+ }) => import("react").JSX.Element;
8
+ export type FloatingPhotoViewerProps = {
9
+ title: ReactNode;
10
+ /** Buttons right after the title, e.g. `<FloatingViewerInfoButton>` for the photo's details. */
11
+ titleActions?: ReactNode;
12
+ /** Suggested views (viewpoint × direction); shown as chips. Empty hides the strip. */
13
+ suggestions: ViewSuggestion[];
14
+ activeDirectionKey: string | null;
15
+ onSelectSuggestion: (suggestion: ViewSuggestion) => void;
16
+ canGoBack: boolean;
17
+ canGoForward: boolean;
18
+ onBack: () => void;
19
+ onForward: () => void;
20
+ onClose: () => void;
21
+ /**
22
+ * Step to the neighbouring photo, e.g. along the clicked street: the `<` `>` buttons at the
23
+ * left of the header. A missing handler disables its button.
24
+ */
25
+ step?: {
26
+ onPrevious?: () => void;
27
+ onNext?: () => void;
28
+ previousLabel: string;
29
+ nextLabel: string;
30
+ };
31
+ /**
32
+ * Bar below the header, in place of the suggested-view chips; e.g. the map feature the photos
33
+ * belong to, with a button per capture day.
34
+ */
35
+ toolbar?: ReactNode;
36
+ /** Short status line, e.g. "Loading…" or "No photos from the last 2 years". */
37
+ status?: ReactNode;
38
+ /** Viewer body (provider panel). */
39
+ children?: ReactNode;
40
+ footer?: ReactNode;
41
+ /** Collapsible list below the footer, e.g. other photos near the click. */
42
+ drawer?: {
43
+ label: ReactNode;
44
+ content: ReactNode;
45
+ };
46
+ /** localStorage key for position, width and minimized state. */
47
+ storageKey?: string;
48
+ };
49
+ /**
50
+ * Photo viewer box floating over the map. Render inside a positioned (relative) map container.
51
+ * Drag the header to move, drag the left edge to resize. Keys: Esc closes, [ and ] step history,
52
+ * Alt + arrow left/right step along (`step`).
53
+ */
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;
@@ -0,0 +1,17 @@
1
+ import { type LngLat, type ViewSuggestion, type Viewpoint } from '@osm-editor-kit/street-imagery';
2
+ import type { MapGeoJSONFeature } from 'maplibre-gl';
3
+ /** Add to `interactiveLayerIds` to make view directions clickable; see `viewDirectionKeyFromFeatures`. */
4
+ export declare const VIEWPOINT_DIRECTION_LAYER_ID = "viewpoint-direction-fill";
5
+ export declare const VIEWPOINT_DEFAULT_COLOR = "#c026d3";
6
+ export type ViewpointLayerProps = {
7
+ viewpoints: Viewpoint[];
8
+ suggestions: ViewSuggestion[];
9
+ activeDirectionKey: string | null;
10
+ zoom: number;
11
+ /** Clicked line, drawn below the viewpoints. */
12
+ line?: LngLat[] | null;
13
+ color?: string;
14
+ };
15
+ export declare const ViewpointLayer: ({ viewpoints, suggestions, activeDirectionKey, zoom, line, color, }: ViewpointLayerProps) => import("react").JSX.Element;
16
+ /** Direction key of the top-most clicked view direction, if any. */
17
+ export declare const viewDirectionKeyFromFeatures: (features: MapGeoJSONFeature[] | undefined) => string | null;
@@ -0,0 +1,23 @@
1
+ import { type NormalizedPhoto, type PhotoTypeFilter, type ViewSuggestion, type ViewpointPhotoSource, type Viewpoint } from '@osm-editor-kit/street-imagery';
2
+ export type ViewSuggestionsOptions = {
3
+ /** Only photos from the last N years. */
4
+ maxAgeYears?: number;
5
+ photoTypes?: PhotoTypeFilter[];
6
+ /** Candidates kept per direction. */
7
+ limit?: number;
8
+ /** Extra app filter (e.g. date range), applied before ranking. */
9
+ filterPhoto?: (photo: NormalizedPhoto) => boolean;
10
+ /**
11
+ * Where photos come from; default Mapillary. Pass several to rank across providers, e.g.
12
+ * `[mapillaryPhotoSource, streetViewPhotoSource]` or your own (Infra3D).
13
+ */
14
+ sources?: ViewpointPhotoSource[];
15
+ enabled?: boolean;
16
+ };
17
+ export type ViewSuggestionsResult = {
18
+ suggestions: ViewSuggestion[];
19
+ isLoading: boolean;
20
+ isError: boolean;
21
+ };
22
+ /** Photos near each viewpoint from all `sources`, ranked per view direction. */
23
+ export declare const useViewSuggestions: (viewpoints: Viewpoint[], { maxAgeYears, photoTypes, limit, filterPhoto, sources, enabled, }?: ViewSuggestionsOptions) => ViewSuggestionsResult;
@@ -0,0 +1,32 @@
1
+ import type { LngLat, NormalizedPhoto, Viewpoint } from '@osm-editor-kit/street-imagery';
2
+ export type ViewpointHistoryEntry = {
3
+ photo: NormalizedPhoto;
4
+ /** Suggested view the photo came from; `null` for photos picked by hand. */
5
+ directionKey: string | null;
6
+ };
7
+ type OpenInput = {
8
+ viewpoints: Viewpoint[];
9
+ /** Clicked line, drawn as context on the map. */
10
+ line?: LngLat[] | null;
11
+ };
12
+ export declare const useViewpoints: () => Viewpoint[];
13
+ export declare const useViewpointLine: () => LngLat[] | null;
14
+ export declare const useActiveDirectionKey: () => string | null;
15
+ export declare const useCurrentHistoryEntry: () => ViewpointHistoryEntry;
16
+ export declare const useCanGoBack: () => boolean;
17
+ export declare const useCanGoForward: () => boolean;
18
+ /** Actions + current entry outside React (event handlers, tests). */
19
+ export declare const getViewpointSession: () => {
20
+ actions: {
21
+ open: (input: OpenInput) => void;
22
+ /** Show a photo; truncates forward history. No-op when it is already the current photo. */
23
+ showPhoto: (entry: ViewpointHistoryEntry) => void;
24
+ back: () => ViewpointHistoryEntry | null;
25
+ forward: () => ViewpointHistoryEntry | null;
26
+ close: () => void;
27
+ /** Like `close`, and forget the history too: the viewer was closed. */
28
+ reset: () => void;
29
+ };
30
+ current: ViewpointHistoryEntry;
31
+ };
32
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { ViewSuggestion } from '@osm-editor-kit/street-imagery';
2
+ import type { StreetImageryMessages } from '../i18n/messages';
3
+ /** Label of a suggested view, e.g. "Start, looking along the street". */
4
+ export declare const viewSuggestionLabel: ({ viewpoint, direction }: ViewSuggestion, messages: StreetImageryMessages) => string;