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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +186 -4
  2. package/dist/MapillaryPanel-FAXVZH37.js +2 -0
  3. package/dist/PanoramaxPanel-TWKTSOXM.js +2 -0
  4. package/dist/StreetLevelImagerySelectionOverlay.d.ts +22 -3
  5. package/dist/StreetLevelImagerySourcesAndLayers.d.ts +24 -1
  6. package/dist/StreetLevelImageryViewCone.d.ts +13 -1
  7. package/dist/StreetLevelImageryViewer.d.ts +23 -1
  8. package/dist/chunk-6EMFFIL2.js +147 -0
  9. package/dist/chunk-SGIB7AVC.js +309 -0
  10. package/dist/chunk-XQXYV6SR.js +466 -0
  11. package/dist/filters/PhotoDateRangeFilter.d.ts +46 -0
  12. package/dist/hooks/useAllProviderMapFeatures.d.ts +0 -4
  13. package/dist/hooks/useProviderData.d.ts +8 -3
  14. package/dist/i18n/PhotoCount.d.ts +12 -0
  15. package/dist/i18n/PhotoDate.d.ts +16 -0
  16. package/dist/i18n/StreetImageryLocaleProvider.d.ts +21 -0
  17. package/dist/i18n/messages.d.ts +45 -0
  18. package/dist/index.d.ts +19 -4
  19. package/dist/index.js +1542 -141
  20. package/dist/mapillary/MapillaryFeatureBar.d.ts +16 -0
  21. package/dist/mapillary/SelectedMapFeatureLayer.d.ts +19 -0
  22. package/dist/mapillary/featureBarLabels.d.ts +16 -0
  23. package/dist/mapillary/useMapillaryFeatures.d.ts +54 -0
  24. package/dist/openers/LocationPickOnMap.d.ts +10 -0
  25. package/dist/openers/useLocationPickStore.d.ts +9 -0
  26. package/dist/panels/MapillaryPanel.d.ts +19 -1
  27. package/dist/panels/PanoramaxPanel.d.ts +14 -4
  28. package/dist/panels/PanoramaxWebViewerPanel.d.ts +20 -0
  29. package/dist/panels/mapillaryLookAt.d.ts +41 -0
  30. package/dist/panels/panoramaxDataProvider.d.ts +44 -0
  31. package/dist/panels/panoramaxItem.d.ts +96 -0
  32. package/dist/panels/panoramaxLimitPanning.d.ts +54 -0
  33. package/dist/panels/panoramaxPhotoFromMetadata.d.ts +8 -0
  34. package/dist/panoramax-web-viewer/index.d.ts +1 -0
  35. package/dist/panoramax-web-viewer.js +314 -0
  36. package/dist/photo-details/PhotoDetailsDialog.d.ts +21 -0
  37. package/dist/photo-details/index.d.ts +6 -0
  38. package/dist/photo-details/labels.d.ts +33 -0
  39. package/dist/photo-details.js +235 -0
  40. package/dist/streetImageryClick.d.ts +4 -0
  41. package/dist/viewer-panels/index.d.ts +5 -0
  42. package/dist/viewer-panels.js +3 -0
  43. package/dist/viewpoints/FloatingPhotoViewer.d.ts +73 -0
  44. package/dist/viewpoints/ViewpointLayer.d.ts +17 -0
  45. package/dist/viewpoints/useViewSuggestions.d.ts +23 -0
  46. package/dist/viewpoints/useViewpointSessionStore.d.ts +35 -0
  47. package/dist/viewpoints/viewDirectionLabels.d.ts +4 -0
  48. package/package.json +37 -6
  49. package/dist/MapillaryPanel-QX6NESX7.js +0 -2
  50. package/dist/PanoramaxPanel-J6ANR6LB.js +0 -2
  51. package/dist/chunk-BS6LPDMY.js +0 -143
  52. package/dist/chunk-ZH3TPW4D.js +0 -165
@@ -0,0 +1,314 @@
1
+ import { useViewerActions } from './chunk-25UGGXIE.js';
2
+ import '@panoramax/web-viewer';
3
+ import { getStreetImageryConfig } from '@osm-editor-kit/street-imagery';
4
+ import { useRef, useEffectEvent, useEffect } from 'react';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ // src/panels/panoramaxLimitPanning.ts
8
+ var toRad = (degrees) => degrees * Math.PI / 180;
9
+ var MIN_VISIBLE_SHARE = 0.3;
10
+ var MIN_IMAGE_SHARE_OF_VIEW = 0.8;
11
+ var keepInView = (centre, imageMin, imageMax, halfView) => {
12
+ const keep = MIN_VISIBLE_SHARE * Math.min(imageMax - imageMin, 2 * halfView);
13
+ return Math.max(imageMin - halfView + keep, Math.min(imageMax + halfView - keep, centre));
14
+ };
15
+ var limitToImage = (psv, position) => {
16
+ const crop = psv.state.textureData?.panoData?.baseData;
17
+ if (!crop || !crop.fullWidth || !crop.fullHeight) {
18
+ return position;
19
+ }
20
+ const halfH = toRad(psv.state.hFov) / 2;
21
+ const halfV = toRad(psv.state.vFov) / 2;
22
+ let { yaw, pitch } = position;
23
+ if (crop.croppedWidth < crop.fullWidth) {
24
+ const left = crop.croppedX / crop.fullWidth * 2 * Math.PI - Math.PI;
25
+ const right = (crop.croppedX + crop.croppedWidth) / crop.fullWidth * 2 * Math.PI - Math.PI;
26
+ const centred = yaw > Math.PI ? yaw - 2 * Math.PI : yaw;
27
+ yaw = keepInView(centred, left, right, halfH);
28
+ }
29
+ if (crop.croppedHeight < crop.fullHeight) {
30
+ const top = Math.PI / 2 - crop.croppedY / crop.fullHeight * Math.PI;
31
+ const bottom = Math.PI / 2 - (crop.croppedY + crop.croppedHeight) / crop.fullHeight * Math.PI;
32
+ pitch = keepInView(pitch, bottom, top, halfV);
33
+ }
34
+ return { yaw, pitch };
35
+ };
36
+ var toDeg = (radians) => radians * 180 / Math.PI;
37
+ var widestZoomLevel = (psv) => {
38
+ const crop = psv.state.textureData?.panoData?.baseData;
39
+ if (!crop || !crop.fullWidth || !crop.fullHeight) {
40
+ return null;
41
+ }
42
+ const flatWide = crop.croppedWidth < crop.fullWidth;
43
+ const flatHigh = crop.croppedHeight < crop.fullHeight;
44
+ if (!flatWide && !flatHigh) {
45
+ return null;
46
+ }
47
+ const imageV = crop.croppedHeight / crop.fullHeight * 180;
48
+ const imageH = crop.croppedWidth / crop.fullWidth * 360;
49
+ const aspect = Math.tan(toRad(psv.state.hFov) / 2) / Math.tan(toRad(psv.state.vFov) / 2);
50
+ const vForImageWidth = toDeg(2 * Math.atan(Math.tan(toRad(Math.min(imageH, 179)) / 2) / aspect));
51
+ const widestV = Math.min(flatHigh ? imageV : 180, flatWide ? vForImageWidth : 180);
52
+ return psv.dataHelper.fovToZoomLevel(Math.min(widestV / MIN_IMAGE_SHARE_OF_VIEW, 179));
53
+ };
54
+ var limitPanningToImage = (psv) => {
55
+ const onMove = (event) => {
56
+ const move = event;
57
+ if (move.position) {
58
+ move.position = limitToImage(psv, move.position);
59
+ }
60
+ };
61
+ const onZoomOrLoad = () => {
62
+ const widest = widestZoomLevel(psv);
63
+ if (widest != null && psv.getZoomLevel() < widest - 0.01) {
64
+ psv.zoom(widest);
65
+ return;
66
+ }
67
+ const current = psv.getPosition();
68
+ const limited = limitToImage(psv, current);
69
+ if (Math.abs(limited.yaw - current.yaw) > 1e-4 || Math.abs(limited.pitch - current.pitch) > 1e-4) {
70
+ psv.rotate(limited);
71
+ }
72
+ };
73
+ psv.addEventListener("before-rotate", onMove);
74
+ psv.addEventListener("before-animate", onMove);
75
+ psv.addEventListener("zoom-updated", onZoomOrLoad);
76
+ psv.addEventListener("panorama-loaded", onZoomOrLoad);
77
+ return () => {
78
+ psv.removeEventListener("before-rotate", onMove);
79
+ psv.removeEventListener("before-animate", onMove);
80
+ psv.removeEventListener("zoom-updated", onZoomOrLoad);
81
+ psv.removeEventListener("panorama-loaded", onZoomOrLoad);
82
+ };
83
+ };
84
+
85
+ // src/panels/panoramaxPhotoFromMetadata.ts
86
+ var toMs = (iso) => {
87
+ const ms = iso ? Date.parse(iso) : Number.NaN;
88
+ return Number.isNaN(ms) ? null : ms;
89
+ };
90
+ var panoramaxPhotoFromMetadata = (metadata) => {
91
+ const properties = metadata.properties ?? {};
92
+ const producers = metadata.caption?.producer ?? [];
93
+ const creatorName = producers.find((entry) => !entry.includes("@")) ?? producers.at(-1);
94
+ const creatorContact = producers.find((entry) => entry.includes("@"));
95
+ const camera = properties["pers:interior_orientation"];
96
+ const cameraName = [camera?.camera_manufacturer, camera?.camera_model].filter(Boolean).join(" ");
97
+ const crop = metadata.panorama?.cropData;
98
+ const isFlat = crop?.croppedWidth != null && crop.fullWidth != null && crop.croppedWidth < crop.fullWidth;
99
+ const uploadedAt = toMs(properties.created);
100
+ const exifEntries = Object.entries(properties.exif ?? {}).filter(([, value]) => typeof value === "string" || typeof value === "number").map(([key, value]) => [key, String(value)]);
101
+ const exif = exifEntries.length > 0 ? Object.fromEntries(exifEntries) : null;
102
+ const details = {
103
+ ...creatorContact && creatorContact !== creatorName ? { creatorContact } : {},
104
+ ...properties.license ? { license: properties.license } : {},
105
+ ...metadata.origLinks?.find((link) => link.rel === "license")?.href ? { licenseUrl: metadata.origLinks.find((link) => link.rel === "license")?.href } : {},
106
+ ...properties.datetimetz ? { capturedAtLocal: properties.datetimetz } : {},
107
+ ...uploadedAt != null ? { uploadedAt } : {},
108
+ ...cameraName ? { camera: cameraName } : {},
109
+ ...isFlat && metadata.horizontalFov ? { fieldOfViewDeg: metadata.horizontalFov } : {},
110
+ ...properties["quality:horizontal_accuracy"] != null ? { positionAccuracyMeters: properties["quality:horizontal_accuracy"] } : {},
111
+ ...metadata.origInstance?.instance_name ? { instance: metadata.origInstance.instance_name } : {},
112
+ ...camera?.focal_length != null ? { focalLengthMm: camera.focal_length } : {},
113
+ ...properties["original_file:name"] ? { originalFileName: properties["original_file:name"] } : {},
114
+ ...properties["original_file:size"] != null ? { originalFileSizeBytes: properties["original_file:size"] } : {},
115
+ ...properties["geovisio:rank_in_collection"] != null ? { rankInSequence: properties["geovisio:rank_in_collection"] } : {},
116
+ ...exif ? { exif } : {}
117
+ };
118
+ return {
119
+ providerId: "panoramax",
120
+ photoId: metadata.id,
121
+ sequenceId: metadata.sequence?.id ?? null,
122
+ capturedAt: toMs(properties.datetime),
123
+ isPano: !isFlat,
124
+ heading: properties["view:azimuth"] ?? null,
125
+ lngLat: metadata.gps,
126
+ ...creatorName ? { creatorName } : {},
127
+ details
128
+ };
129
+ };
130
+ var panoramaxApiEndpoint = () => `${getStreetImageryConfig().panoramaxApiBase}/api`;
131
+ var HIDE_LEGEND_CSS = "pnx-photo-viewer pnx-bottom-drawer, pnx-photo-viewer pnx-picture-legend { display: none !important; }";
132
+ var normalizeBearing = (degrees) => (degrees % 360 + 360) % 360;
133
+ var PanoramaxWebViewerPanel = ({
134
+ photo,
135
+ onPhotoSelected,
136
+ onEaseMapToPoint,
137
+ onViewerPhoto,
138
+ hideLegend: hideLegendProp = false,
139
+ hideAttribution = false
140
+ }) => {
141
+ const hideLegend = hideLegendProp || hideAttribution;
142
+ const containerRef = useRef(null);
143
+ const viewerRef = useRef(null);
144
+ const lastViewerPhotoIdRef = useRef(null);
145
+ const readyRef = useRef(false);
146
+ const pendingSelectRef = useRef(null);
147
+ const photoRef = useRef(photo);
148
+ const bearingRafRef = useRef(null);
149
+ const pendingBearingRef = useRef(null);
150
+ const pendingHfovRef = useRef(null);
151
+ const actions = useViewerActions();
152
+ const emitViewerPhoto = useEffectEvent(
153
+ (viewerPhoto) => onViewerPhoto?.(viewerPhoto)
154
+ );
155
+ useEffect(
156
+ function syncPhotoRef() {
157
+ photoRef.current = photo;
158
+ },
159
+ [photo]
160
+ );
161
+ useEffect(
162
+ function resetViewerStoreOnProviderChange() {
163
+ return () => {
164
+ actions.reset();
165
+ };
166
+ },
167
+ [actions, photo.providerId]
168
+ );
169
+ useEffect(
170
+ function mountPanoramaxViewer() {
171
+ const viewer = viewerRef.current;
172
+ const container = containerRef.current;
173
+ if (!viewer || !container) {
174
+ return;
175
+ }
176
+ lastViewerPhotoIdRef.current = photoRef.current.photoId;
177
+ const flushBearing = () => {
178
+ bearingRafRef.current = null;
179
+ if (pendingBearingRef.current != null) {
180
+ actions.setPov({ bearing: pendingBearingRef.current });
181
+ pendingBearingRef.current = null;
182
+ }
183
+ if (pendingHfovRef.current != null) {
184
+ actions.setPov({ hfov: pendingHfovRef.current });
185
+ pendingHfovRef.current = null;
186
+ }
187
+ };
188
+ const onSelect = (event) => {
189
+ const { seqId, picId } = event.detail;
190
+ if (!picId) {
191
+ return;
192
+ }
193
+ lastViewerPhotoIdRef.current = picId;
194
+ const currentPhoto = photoRef.current;
195
+ const sequenceId = seqId ?? (picId === currentPhoto.photoId && currentPhoto.sequenceId ? currentPhoto.sequenceId : `photo:${picId}`);
196
+ onPhotoSelected({
197
+ provider: "panoramax",
198
+ sequenceId,
199
+ photoId: picId
200
+ });
201
+ };
202
+ const onPictureLoaded = (event) => {
203
+ const detail = event.detail;
204
+ const metadata = viewer.psv?.getPictureMetadata();
205
+ if (metadata) {
206
+ emitViewerPhoto(panoramaxPhotoFromMetadata(metadata));
207
+ }
208
+ if (detail.lon == null || detail.lat == null) {
209
+ return;
210
+ }
211
+ actions.setPov({ lngLat: [detail.lon, detail.lat] });
212
+ if (detail.x != null) {
213
+ actions.setPov({ bearing: normalizeBearing(detail.x) });
214
+ }
215
+ const psv = viewer.psv;
216
+ if (psv && detail.z != null) {
217
+ actions.setPov({ hfov: psv.dataHelper.zoomLevelToFov(detail.z) });
218
+ }
219
+ onEaseMapToPoint(detail.lon, detail.lat);
220
+ };
221
+ const onViewRotated = (event) => {
222
+ const detail = event.detail;
223
+ pendingBearingRef.current = normalizeBearing(detail.x);
224
+ const psv = viewer.psv;
225
+ if (psv && detail.z != null) {
226
+ pendingHfovRef.current = psv.dataHelper.zoomLevelToFov(detail.z);
227
+ }
228
+ if (bearingRafRef.current == null) {
229
+ bearingRafRef.current = requestAnimationFrame(flushBearing);
230
+ }
231
+ };
232
+ viewer.addEventListener("select", onSelect);
233
+ viewer.addEventListener("psv:picture-loaded", onPictureLoaded);
234
+ viewer.addEventListener("psv:view-rotated", onViewRotated);
235
+ const resizeObserver = new ResizeObserver(() => {
236
+ viewer.psv?.resize();
237
+ });
238
+ resizeObserver.observe(container);
239
+ let stopLimit = null;
240
+ const startLimit = () => {
241
+ if (!stopLimit && viewer.psv) {
242
+ stopLimit = limitPanningToImage(viewer.psv);
243
+ }
244
+ };
245
+ startLimit();
246
+ viewer.addEventListener("psv:picture-loaded", startLimit);
247
+ readyRef.current = true;
248
+ const pending = pendingSelectRef.current;
249
+ if (pending) {
250
+ pendingSelectRef.current = null;
251
+ if (typeof viewer.select === "function") {
252
+ viewer.select(pending.sequenceId ?? null, pending.photoId);
253
+ }
254
+ }
255
+ return () => {
256
+ readyRef.current = false;
257
+ if (bearingRafRef.current != null) {
258
+ cancelAnimationFrame(bearingRafRef.current);
259
+ bearingRafRef.current = null;
260
+ }
261
+ resizeObserver.disconnect();
262
+ viewer.removeEventListener("select", onSelect);
263
+ viewer.removeEventListener("psv:picture-loaded", onPictureLoaded);
264
+ viewer.removeEventListener("psv:view-rotated", onViewRotated);
265
+ viewer.removeEventListener("psv:picture-loaded", startLimit);
266
+ stopLimit?.();
267
+ };
268
+ },
269
+ [actions, onEaseMapToPoint, onPhotoSelected]
270
+ );
271
+ useEffect(
272
+ function syncExternalPhotoSelection() {
273
+ const viewer = viewerRef.current;
274
+ if (!viewer || photo.photoId === lastViewerPhotoIdRef.current) {
275
+ return;
276
+ }
277
+ actions.reset();
278
+ lastViewerPhotoIdRef.current = photo.photoId;
279
+ if (!readyRef.current) {
280
+ pendingSelectRef.current = { sequenceId: photo.sequenceId, photoId: photo.photoId };
281
+ return;
282
+ }
283
+ if (typeof viewer.select === "function") {
284
+ viewer.select(photo.sequenceId ?? null, photo.photoId);
285
+ }
286
+ },
287
+ [actions, photo.photoId, photo.sequenceId]
288
+ );
289
+ return /* @__PURE__ */ jsxs(
290
+ "div",
291
+ {
292
+ ref: containerRef,
293
+ className: "min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900",
294
+ style: { aspectRatio: "4 / 3" },
295
+ children: [
296
+ hideLegend ? /* @__PURE__ */ jsx("style", { children: HIDE_LEGEND_CSS }) : null,
297
+ /* @__PURE__ */ jsx(
298
+ "pnx-photo-viewer",
299
+ {
300
+ ref: viewerRef,
301
+ className: "block h-full w-full",
302
+ endpoint: panoramaxApiEndpoint(),
303
+ "url-parameters": "false",
304
+ widgets: hideLegend ? "false" : void 0,
305
+ picture: photo.photoId,
306
+ sequence: photo.sequenceId ?? void 0
307
+ }
308
+ )
309
+ ]
310
+ }
311
+ );
312
+ };
313
+
314
+ export { PanoramaxWebViewerPanel };
@@ -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-6EMFFIL2.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 };
@@ -7,6 +7,10 @@ export type StreetImageryClickFeature = {
7
7
  featureId?: string;
8
8
  sequenceId?: string;
9
9
  };
10
+ /**
11
+ * Layers to pass as `interactiveLayerIds`. A click returns the features of all layers it hits, the
12
+ * top one first: the photo dot itself, then view shapes, then the larger invisible targets.
13
+ */
10
14
  export declare const streetImageryInteractiveLayerIds: (providers: ProviderId[]) => string[];
11
15
  export declare const queryStreetImageryFeatures: (event: {
12
16
  features?: MapGeoJSONFeature[] | null;
@@ -0,0 +1,5 @@
1
+ export { MapillaryPanel } from '../panels/MapillaryPanel';
2
+ export { PanoramaxPanel, type PanoramaxPanelProps } from '../panels/PanoramaxPanel';
3
+ export { PanoramaxDataProvider, type PanoramaxDataProviderOptions, } from '../panels/panoramaxDataProvider';
4
+ export { panoramaxPhotoFromItem, type PanoramaxItem } from '../panels/panoramaxItem';
5
+ export { mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo, } from '../panels/mapillaryLookAt';
@@ -0,0 +1,3 @@
1
+ export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo } from './chunk-SGIB7AVC.js';
2
+ export { PanoramaxDataProvider, PanoramaxPanel, panoramaxPhotoFromItem } from './chunk-XQXYV6SR.js';
3
+ import './chunk-25UGGXIE.js';