@osm-editor-kit/street-imagery-react 0.1.0-alpha.5 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/MapillaryPanel-N45NQY4C.js +2 -0
- package/dist/PanoramaxPanel-MFI4L67Z.js +2 -0
- package/dist/StreetLevelImagerySelectionOverlay.d.ts +20 -3
- package/dist/StreetLevelImagerySourcesAndLayers.d.ts +9 -1
- package/dist/StreetLevelImageryViewCone.d.ts +11 -1
- package/dist/StreetLevelImageryViewer.d.ts +7 -2
- package/dist/{chunk-72HJV6II.js → chunk-53RA4OSJ.js} +6 -2
- package/dist/chunk-EE7V3OIZ.js +116 -0
- package/dist/chunk-EQ2JKLGT.js +311 -0
- package/dist/hooks/useAllProviderMapFeatures.d.ts +0 -4
- package/dist/i18n/PhotoCount.d.ts +12 -0
- package/dist/i18n/PhotoDate.d.ts +16 -0
- package/dist/i18n/StreetImageryLocaleProvider.d.ts +21 -0
- package/dist/i18n/messages.d.ts +27 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +593 -182
- package/dist/mapillary/MapillaryFeatureBar.d.ts +16 -0
- package/dist/mapillary/featureBarLabels.d.ts +16 -0
- package/dist/openers/LocationPickOnMap.d.ts +10 -0
- package/dist/openers/useLocationPickStore.d.ts +9 -0
- package/dist/panels/MapillaryPanel.d.ts +7 -1
- package/dist/panels/PanoramaxPanel.d.ts +8 -1
- package/dist/panels/panoramaxLimitPanning.d.ts +54 -0
- package/dist/panels/panoramaxPhotoFromMetadata.d.ts +8 -0
- package/dist/photo-details/PhotoDetailsDialog.d.ts +21 -0
- package/dist/photo-details/index.d.ts +6 -0
- package/dist/photo-details/labels.d.ts +33 -0
- package/dist/photo-details.js +235 -0
- package/dist/viewpoints/FloatingPhotoViewer.d.ts +33 -4
- package/dist/viewpoints/useViewpointSessionStore.d.ts +6 -10
- package/dist/viewpoints/viewDirectionLabels.d.ts +4 -4
- package/package.json +11 -2
- package/dist/MapillaryPanel-I5GKT6YT.js +0 -2
- package/dist/PanoramaxPanel-J6ANR6LB.js +0 -2
- package/dist/chunk-ZH3TPW4D.js +0 -165
package/README.md
CHANGED
|
@@ -120,3 +120,48 @@ const { data: detections } = useMapillaryImageDetections(photo.photoId, {
|
|
|
120
120
|
- Map layers: `filter.mapFeatureValue` limits which signs/objects are drawn (`matchesAnyGroup(SIGN_GROUPS)`).
|
|
121
121
|
- Suggested views: `useViewSuggestions(viewpoints, { sources: [mapillaryPhotoSource, streetViewPhotoSource] })`.
|
|
122
122
|
- `turnMapillaryViewerTo` / `setMapillaryViewerOutlines` work on a mapillary-js `Viewer` you own.
|
|
123
|
+
|
|
124
|
+
## Pick a place on the map to open elsewhere
|
|
125
|
+
|
|
126
|
+
A button arms a location opener (see `@osm-editor-kit/street-imagery`), the next map click opens the clicked place in that service and disarms; Escape cancels.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import {
|
|
130
|
+
LocationPickOnMap,
|
|
131
|
+
useArmedLocationOpenerId,
|
|
132
|
+
useLocationPickActions,
|
|
133
|
+
} from '@osm-editor-kit/street-imagery-react'
|
|
134
|
+
|
|
135
|
+
const armed = useArmedLocationOpenerId() // show it: pressed button, crosshair cursor, a hint on the map
|
|
136
|
+
const { toggle, disarm } = useLocationPickActions()
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
getLocationOpeners().map((opener) => (
|
|
140
|
+
<button key={opener.id} aria-pressed={armed === opener.id} onClick={() => toggle(opener.id)}>
|
|
141
|
+
Open in {opener.label}
|
|
142
|
+
</button>
|
|
143
|
+
))
|
|
144
|
+
}
|
|
145
|
+
;<Map cursor={armed ? 'crosshair' : undefined}>
|
|
146
|
+
<LocationPickOnMap />
|
|
147
|
+
</Map>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The map's own click handlers still run; skip them while `armed` is set if the click should only open the place. When the place is known already (a selected feature), call `openLocationInNewTab` directly instead.
|
|
151
|
+
|
|
152
|
+
## Photo details dialog (optional)
|
|
153
|
+
|
|
154
|
+
A modal with everything known about the shown photo: capture, camera, position, source and raw EXIF. It lives in its own entry, so apps that do not import it do not bundle it.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { FloatingViewerInfoButton } from '@osm-editor-kit/street-imagery-react'
|
|
158
|
+
import { PHOTO_DETAILS_LABELS, PhotoDetailsDialog } from '@osm-editor-kit/street-imagery-react/photo-details'
|
|
159
|
+
|
|
160
|
+
<FloatingPhotoViewer
|
|
161
|
+
titleActions={<FloatingViewerInfoButton label={PHOTO_DETAILS_LABELS.en.open} onClick={() => setOpen(true)} />}
|
|
162
|
+
…
|
|
163
|
+
/>
|
|
164
|
+
<PhotoDetailsDialog open={open} photo={viewerPhoto} onClose={() => setOpen(false)} />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`viewerPhoto` is what `onViewerPhoto` of the viewer gives (Mapillary, Panoramax).
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NormalizedPhoto, NormalizedSequence } from '@osm-editor-kit/street-imagery';
|
|
2
|
+
/** Lines and photo dots: near black. The shown photo's line is thicker, its marker larger. */
|
|
3
|
+
export declare const BASE_COLOR = "#171717";
|
|
4
|
+
/** The shown photo: fill of its marker and camera pin (black outline), and the line between. */
|
|
5
|
+
export declare const SELECTION_COLOR = "#f97316";
|
|
6
|
+
/** Width of the shown photo's sequence line; other lines are 1.25. */
|
|
7
|
+
export declare const ACTIVE_LINE_WIDTH = 2.5;
|
|
2
8
|
export type StreetLevelImagerySelectionOverlayProps = {
|
|
3
9
|
selectedPhoto?: NormalizedPhoto | null;
|
|
4
10
|
selectedSequence?: NormalizedSequence | null;
|
|
11
|
+
/**
|
|
12
|
+
* Where the viewer says the camera was (Mapillary: the computed position, which the view cone
|
|
13
|
+
* starts at). When it differs from the photo dot, a pin marks it and a line connects both.
|
|
14
|
+
*/
|
|
15
|
+
cameraLngLat?: [number, number] | null;
|
|
16
|
+
/** Layer to draw the sequence line below, so photo dots stay on top of it. */
|
|
17
|
+
sequenceBeforeId?: string;
|
|
5
18
|
};
|
|
6
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The shown photo on the map, bottom to top: its sequence line (only passed when the provider's
|
|
21
|
+
* own layers are off; else that layer marks it), a filled marker on its photo dot, and — when the camera position differs from the
|
|
22
|
+
* dot — a dashed connector and a pin at the camera position.
|
|
23
|
+
*/
|
|
24
|
+
export declare const StreetLevelImagerySelectionOverlay: ({ selectedPhoto, selectedSequence, cameraLngLat, sequenceBeforeId, }: StreetLevelImagerySelectionOverlayProps) => import("react").JSX.Element | null;
|
|
7
25
|
/** Resolve sequence geometry for highlight when only photo + fetched sequences are available. */
|
|
8
26
|
export declare const resolveSelectedSequence: (selectedPhoto: NormalizedPhoto | null | undefined, sequences: NormalizedSequence[], sequenceId: string | null | undefined) => NormalizedSequence | null;
|
|
9
|
-
export type SequenceHighlightBbox = Bbox | null;
|
|
@@ -20,7 +20,7 @@ export type StreetLevelImagerySourcesAndLayersProps = {
|
|
|
20
20
|
options: {
|
|
21
21
|
config?: StreetImageryConfig;
|
|
22
22
|
showSequences?: boolean;
|
|
23
|
-
/** Heading wedges / 360° disks per photo, from zoom
|
|
23
|
+
/** Heading wedges / 360° disks per photo, from zoom 17, capped for dense areas. Default true. */
|
|
24
24
|
showViewfields?: boolean;
|
|
25
25
|
showViewCone?: boolean;
|
|
26
26
|
showSelectionHighlight?: boolean;
|
|
@@ -31,7 +31,15 @@ export type StreetLevelImagerySourcesAndLayersProps = {
|
|
|
31
31
|
hfov?: number | null;
|
|
32
32
|
lngLat?: [number, number] | null;
|
|
33
33
|
} | null;
|
|
34
|
+
/**
|
|
35
|
+
* Style colour of a photo (by type, age …). It colours the view-direction shapes; photo dots
|
|
36
|
+
* and sequence lines are black.
|
|
37
|
+
*/
|
|
34
38
|
photoCircleColor: DataDrivenPropertyValueSpecification<string>;
|
|
39
|
+
/** Colour of the shown photo's view cone. Default: the selection colour (orange). */
|
|
40
|
+
viewConeColor?: DataDrivenPropertyValueSpecification<string>;
|
|
41
|
+
/** Length of the shown photo's view cone, as a multiple of the per-photo shapes. Default 2.5. */
|
|
42
|
+
viewConeScale?: number;
|
|
35
43
|
mapFeatureCircleColor: DataDrivenPropertyValueSpecification<string>;
|
|
36
44
|
};
|
|
37
45
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
|
|
2
|
+
import type { DataDrivenPropertyValueSpecification } from 'maplibre-gl';
|
|
3
|
+
/** Fill of view-direction shapes: the 360° disks, the flat wedges and the shown photo's cone. */
|
|
4
|
+
export declare const VIEW_SHAPE_FILL_OPACITY = 0.25;
|
|
2
5
|
export type StreetLevelImageryViewConeProps = {
|
|
3
6
|
selectedPhoto: NormalizedPhoto;
|
|
4
7
|
zoom: number;
|
|
@@ -7,5 +10,12 @@ export type StreetLevelImageryViewConeProps = {
|
|
|
7
10
|
hfov?: number | null;
|
|
8
11
|
lngLat?: [number, number] | null;
|
|
9
12
|
} | null;
|
|
13
|
+
/**
|
|
14
|
+
* Fill colour; an expression may read the photo's `isPano` and `capturedAt`, so the cone gets
|
|
15
|
+
* the same colour as the photo's own view-direction shape.
|
|
16
|
+
*/
|
|
17
|
+
color?: DataDrivenPropertyValueSpecification<string>;
|
|
18
|
+
/** Cone length as a multiple of the per-photo view-direction shapes. Default 2.5. */
|
|
19
|
+
scale?: number;
|
|
10
20
|
};
|
|
11
|
-
export declare const StreetLevelImageryViewCone: ({ selectedPhoto, zoom, viewerPov, }: StreetLevelImageryViewConeProps) => import("react").JSX.Element | null;
|
|
21
|
+
export declare const StreetLevelImageryViewCone: ({ selectedPhoto, zoom, viewerPov, color, scale, }: StreetLevelImageryViewConeProps) => import("react").JSX.Element | null;
|
|
@@ -6,7 +6,7 @@ type StreetLevelImageryViewerProps = {
|
|
|
6
6
|
groupPhotos: NormalizedPhoto[];
|
|
7
7
|
onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
|
|
8
8
|
onEaseMapToPoint: (lng: number, lat: number) => void;
|
|
9
|
-
/** Mapillary
|
|
9
|
+
/** Mapillary, Panoramax: full photo data (creator, licence …) for each image the viewer shows. */
|
|
10
10
|
onViewerPhoto?: (photo: NormalizedPhoto) => void;
|
|
11
11
|
/** Mapillary 360° photos: initial view bearing (e.g. a suggested view direction). */
|
|
12
12
|
lookAtBearing?: number | null;
|
|
@@ -14,6 +14,11 @@ type StreetLevelImageryViewerProps = {
|
|
|
14
14
|
lookAt?: MapillaryLookAt | null;
|
|
15
15
|
/** Mapillary: outlines to draw in the shown image. */
|
|
16
16
|
outlines?: MapillaryViewerOutline[];
|
|
17
|
+
/**
|
|
18
|
+
* Hide the viewer's own attribution (Mapillary) or legend (Panoramax); the host must then show
|
|
19
|
+
* creator and licence itself, from `onViewerPhoto`.
|
|
20
|
+
*/
|
|
21
|
+
hideAttribution?: boolean;
|
|
17
22
|
};
|
|
18
|
-
export declare const StreetLevelImageryViewer: ({ photo, groupPhotos, onPhotoSelected, onEaseMapToPoint, onViewerPhoto, lookAtBearing, lookAt, outlines, }: StreetLevelImageryViewerProps) => import("react").JSX.Element | null;
|
|
23
|
+
export declare const StreetLevelImageryViewer: ({ photo, groupPhotos, onPhotoSelected, onEaseMapToPoint, onViewerPhoto, lookAtBearing, lookAt, outlines, hideAttribution, }: StreetLevelImageryViewerProps) => import("react").JSX.Element | null;
|
|
19
24
|
export {};
|
|
@@ -105,7 +105,8 @@ var MapillaryPanel = ({
|
|
|
105
105
|
onViewerPhoto,
|
|
106
106
|
lookAtBearing,
|
|
107
107
|
lookAt,
|
|
108
|
-
outlines
|
|
108
|
+
outlines,
|
|
109
|
+
hideAttribution = false
|
|
109
110
|
}) => {
|
|
110
111
|
const containerRef = useRef(null);
|
|
111
112
|
const viewerRef = useRef(null);
|
|
@@ -116,6 +117,7 @@ var MapillaryPanel = ({
|
|
|
116
117
|
const pendingBearingRef = useRef(null);
|
|
117
118
|
const actions = useViewerActions();
|
|
118
119
|
const initialPhotoIdRef = useRef(photo.photoId);
|
|
120
|
+
const initialHideAttributionRef = useRef(hideAttribution);
|
|
119
121
|
const emitPhotoSelected = useEffectEvent(onPhotoSelected);
|
|
120
122
|
const emitEaseMapToPoint = useEffectEvent(onEaseMapToPoint);
|
|
121
123
|
const turnToWantedBearing = useEffectEvent((viewer, image) => {
|
|
@@ -162,6 +164,7 @@ var MapillaryPanel = ({
|
|
|
162
164
|
container,
|
|
163
165
|
imageId: initialPhotoIdRef.current,
|
|
164
166
|
component: {
|
|
167
|
+
attribution: !initialHideAttributionRef.current,
|
|
165
168
|
cover: false,
|
|
166
169
|
sequence: { visible: true },
|
|
167
170
|
tag: true
|
|
@@ -205,7 +208,8 @@ var MapillaryPanel = ({
|
|
|
205
208
|
capturedAt: image.capturedAt,
|
|
206
209
|
isPano: image.cameraType === "spherical" || image.cameraType === "equirectangular",
|
|
207
210
|
heading: image.computedCompassAngle ?? image.compassAngle,
|
|
208
|
-
lngLat: [position.lng, position.lat]
|
|
211
|
+
lngLat: [position.lng, position.lat],
|
|
212
|
+
...image.creatorUsername ? { creatorName: image.creatorUsername } : {}
|
|
209
213
|
});
|
|
210
214
|
actions.setPov({ lngLat: [position.lng, position.lat] });
|
|
211
215
|
emitEaseMapToPoint(position.lng, position.lat);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { DEFAULT_STREET_IMAGERY_LOCALE } from '@osm-editor-kit/street-imagery';
|
|
2
|
+
import { createContext, use } from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/i18n/messages.ts
|
|
6
|
+
var en = {
|
|
7
|
+
viewer: {
|
|
8
|
+
regionLabel: "Photo viewer",
|
|
9
|
+
loading: "Loading viewer\u2026",
|
|
10
|
+
backInHistory: "Back in history ([)",
|
|
11
|
+
forwardInHistory: "Forward in history (])",
|
|
12
|
+
minimize: "Minimize",
|
|
13
|
+
close: "Close (Esc)",
|
|
14
|
+
suggestedViews: "Suggested views",
|
|
15
|
+
suggestedViewsHint: "Suggested views: where the photo is taken from, and which way it looks",
|
|
16
|
+
noPhoto: (label) => `${label} (no photo)`,
|
|
17
|
+
noMatchingPhoto: (label) => `${label}: no matching photo`,
|
|
18
|
+
nextPhotoOfView: (label) => `${label} \u2014 click for the next photo`
|
|
19
|
+
},
|
|
20
|
+
unknownDate: "Unknown date",
|
|
21
|
+
viewpointRole: {
|
|
22
|
+
here: "Here",
|
|
23
|
+
"line-start": "Start",
|
|
24
|
+
"line-end": "End",
|
|
25
|
+
"into-node": "Junction",
|
|
26
|
+
custom: "View"
|
|
27
|
+
},
|
|
28
|
+
compass: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"],
|
|
29
|
+
viewDirection: (role, kind) => {
|
|
30
|
+
switch (kind) {
|
|
31
|
+
case "forward":
|
|
32
|
+
return `${role}, looking along the street`;
|
|
33
|
+
case "back":
|
|
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`;
|
|
39
|
+
case "into":
|
|
40
|
+
return `${role}, looking into the junction`;
|
|
41
|
+
default:
|
|
42
|
+
return `${role}, looking ${kind}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var COMPASS_DE = { N: "Norden", E: "Osten", S: "S\xFCden", W: "Westen" };
|
|
47
|
+
var de = {
|
|
48
|
+
viewer: {
|
|
49
|
+
regionLabel: "Fotoansicht",
|
|
50
|
+
loading: "Ansicht wird geladen\u2026",
|
|
51
|
+
backInHistory: "Zur\xFCck im Verlauf ([)",
|
|
52
|
+
forwardInHistory: "Vor im Verlauf (])",
|
|
53
|
+
minimize: "Minimieren",
|
|
54
|
+
close: "Schlie\xDFen (Esc)",
|
|
55
|
+
suggestedViews: "Vorgeschlagene Blicke",
|
|
56
|
+
suggestedViewsHint: "Vorgeschlagene Blicke: von wo das Foto aufgenommen ist und wohin es schaut",
|
|
57
|
+
noPhoto: (label) => `${label} (kein Foto)`,
|
|
58
|
+
noMatchingPhoto: (label) => `${label}: kein passendes Foto`,
|
|
59
|
+
nextPhotoOfView: (label) => `${label} \u2014 klicken f\xFCr das n\xE4chste Foto`
|
|
60
|
+
},
|
|
61
|
+
unknownDate: "Datum unbekannt",
|
|
62
|
+
viewpointRole: {
|
|
63
|
+
here: "Hier",
|
|
64
|
+
"line-start": "Anfang",
|
|
65
|
+
"line-end": "Ende",
|
|
66
|
+
"into-node": "Knoten",
|
|
67
|
+
custom: "Blick"
|
|
68
|
+
},
|
|
69
|
+
compass: ["N", "NO", "O", "SO", "S", "SW", "W", "NW"],
|
|
70
|
+
viewDirection: (role, kind) => {
|
|
71
|
+
switch (kind) {
|
|
72
|
+
case "forward":
|
|
73
|
+
return `${role}, Blick entlang der Stra\xDFe`;
|
|
74
|
+
case "back":
|
|
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`;
|
|
80
|
+
case "into":
|
|
81
|
+
return `${role}, Blick in den Knoten`;
|
|
82
|
+
default:
|
|
83
|
+
return `${role}, Blick nach ${COMPASS_DE[kind] ?? kind}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var STREET_IMAGERY_MESSAGES = {
|
|
88
|
+
en,
|
|
89
|
+
de
|
|
90
|
+
};
|
|
91
|
+
var DEFAULT_I18N = {
|
|
92
|
+
locale: DEFAULT_STREET_IMAGERY_LOCALE,
|
|
93
|
+
messages: STREET_IMAGERY_MESSAGES[DEFAULT_STREET_IMAGERY_LOCALE]
|
|
94
|
+
};
|
|
95
|
+
var I18nContext = createContext(DEFAULT_I18N);
|
|
96
|
+
var mergeMessages = (base, overrides) => overrides ? {
|
|
97
|
+
unknownDate: overrides.unknownDate ?? base.unknownDate,
|
|
98
|
+
viewer: { ...base.viewer, ...overrides.viewer },
|
|
99
|
+
viewpointRole: { ...base.viewpointRole, ...overrides.viewpointRole },
|
|
100
|
+
compass: overrides.compass?.length === 8 ? overrides.compass : base.compass,
|
|
101
|
+
viewDirection: overrides.viewDirection ?? base.viewDirection
|
|
102
|
+
} : base;
|
|
103
|
+
var StreetImageryLocaleProvider = ({
|
|
104
|
+
locale,
|
|
105
|
+
messages,
|
|
106
|
+
children
|
|
107
|
+
}) => /* @__PURE__ */ jsx(
|
|
108
|
+
I18nContext,
|
|
109
|
+
{
|
|
110
|
+
value: { locale, messages: mergeMessages(STREET_IMAGERY_MESSAGES[locale], messages) },
|
|
111
|
+
children
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
var useStreetImageryI18n = () => use(I18nContext);
|
|
115
|
+
|
|
116
|
+
export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n };
|
|
@@ -0,0 +1,311 @@
|
|
|
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 PanoramaxPanel = ({
|
|
134
|
+
photo,
|
|
135
|
+
onPhotoSelected,
|
|
136
|
+
onEaseMapToPoint,
|
|
137
|
+
onViewerPhoto,
|
|
138
|
+
hideLegend = false
|
|
139
|
+
}) => {
|
|
140
|
+
const containerRef = useRef(null);
|
|
141
|
+
const viewerRef = useRef(null);
|
|
142
|
+
const lastViewerPhotoIdRef = useRef(null);
|
|
143
|
+
const readyRef = useRef(false);
|
|
144
|
+
const pendingSelectRef = useRef(null);
|
|
145
|
+
const photoRef = useRef(photo);
|
|
146
|
+
const bearingRafRef = useRef(null);
|
|
147
|
+
const pendingBearingRef = useRef(null);
|
|
148
|
+
const pendingHfovRef = useRef(null);
|
|
149
|
+
const actions = useViewerActions();
|
|
150
|
+
const emitViewerPhoto = useEffectEvent(
|
|
151
|
+
(viewerPhoto) => onViewerPhoto?.(viewerPhoto)
|
|
152
|
+
);
|
|
153
|
+
useEffect(
|
|
154
|
+
function syncPhotoRef() {
|
|
155
|
+
photoRef.current = photo;
|
|
156
|
+
},
|
|
157
|
+
[photo]
|
|
158
|
+
);
|
|
159
|
+
useEffect(
|
|
160
|
+
function resetViewerStoreOnProviderChange() {
|
|
161
|
+
return () => {
|
|
162
|
+
actions.reset();
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
[actions, photo.providerId]
|
|
166
|
+
);
|
|
167
|
+
useEffect(
|
|
168
|
+
function mountPanoramaxViewer() {
|
|
169
|
+
const viewer = viewerRef.current;
|
|
170
|
+
const container = containerRef.current;
|
|
171
|
+
if (!viewer || !container) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
lastViewerPhotoIdRef.current = photoRef.current.photoId;
|
|
175
|
+
const flushBearing = () => {
|
|
176
|
+
bearingRafRef.current = null;
|
|
177
|
+
if (pendingBearingRef.current != null) {
|
|
178
|
+
actions.setPov({ bearing: pendingBearingRef.current });
|
|
179
|
+
pendingBearingRef.current = null;
|
|
180
|
+
}
|
|
181
|
+
if (pendingHfovRef.current != null) {
|
|
182
|
+
actions.setPov({ hfov: pendingHfovRef.current });
|
|
183
|
+
pendingHfovRef.current = null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const onSelect = (event) => {
|
|
187
|
+
const { seqId, picId } = event.detail;
|
|
188
|
+
if (!picId) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
lastViewerPhotoIdRef.current = picId;
|
|
192
|
+
const currentPhoto = photoRef.current;
|
|
193
|
+
const sequenceId = seqId ?? (picId === currentPhoto.photoId && currentPhoto.sequenceId ? currentPhoto.sequenceId : `photo:${picId}`);
|
|
194
|
+
onPhotoSelected({
|
|
195
|
+
provider: "panoramax",
|
|
196
|
+
sequenceId,
|
|
197
|
+
photoId: picId
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
const onPictureLoaded = (event) => {
|
|
201
|
+
const detail = event.detail;
|
|
202
|
+
const metadata = viewer.psv?.getPictureMetadata();
|
|
203
|
+
if (metadata) {
|
|
204
|
+
emitViewerPhoto(panoramaxPhotoFromMetadata(metadata));
|
|
205
|
+
}
|
|
206
|
+
if (detail.lon == null || detail.lat == null) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
actions.setPov({ lngLat: [detail.lon, detail.lat] });
|
|
210
|
+
if (detail.x != null) {
|
|
211
|
+
actions.setPov({ bearing: normalizeBearing(detail.x) });
|
|
212
|
+
}
|
|
213
|
+
const psv = viewer.psv;
|
|
214
|
+
if (psv && detail.z != null) {
|
|
215
|
+
actions.setPov({ hfov: psv.dataHelper.zoomLevelToFov(detail.z) });
|
|
216
|
+
}
|
|
217
|
+
onEaseMapToPoint(detail.lon, detail.lat);
|
|
218
|
+
};
|
|
219
|
+
const onViewRotated = (event) => {
|
|
220
|
+
const detail = event.detail;
|
|
221
|
+
pendingBearingRef.current = normalizeBearing(detail.x);
|
|
222
|
+
const psv = viewer.psv;
|
|
223
|
+
if (psv && detail.z != null) {
|
|
224
|
+
pendingHfovRef.current = psv.dataHelper.zoomLevelToFov(detail.z);
|
|
225
|
+
}
|
|
226
|
+
if (bearingRafRef.current == null) {
|
|
227
|
+
bearingRafRef.current = requestAnimationFrame(flushBearing);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
viewer.addEventListener("select", onSelect);
|
|
231
|
+
viewer.addEventListener("psv:picture-loaded", onPictureLoaded);
|
|
232
|
+
viewer.addEventListener("psv:view-rotated", onViewRotated);
|
|
233
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
234
|
+
viewer.psv?.resize();
|
|
235
|
+
});
|
|
236
|
+
resizeObserver.observe(container);
|
|
237
|
+
let stopLimit = null;
|
|
238
|
+
const startLimit = () => {
|
|
239
|
+
if (!stopLimit && viewer.psv) {
|
|
240
|
+
stopLimit = limitPanningToImage(viewer.psv);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
startLimit();
|
|
244
|
+
viewer.addEventListener("psv:picture-loaded", startLimit);
|
|
245
|
+
readyRef.current = true;
|
|
246
|
+
const pending = pendingSelectRef.current;
|
|
247
|
+
if (pending) {
|
|
248
|
+
pendingSelectRef.current = null;
|
|
249
|
+
if (typeof viewer.select === "function") {
|
|
250
|
+
viewer.select(pending.sequenceId ?? null, pending.photoId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return () => {
|
|
254
|
+
readyRef.current = false;
|
|
255
|
+
if (bearingRafRef.current != null) {
|
|
256
|
+
cancelAnimationFrame(bearingRafRef.current);
|
|
257
|
+
bearingRafRef.current = null;
|
|
258
|
+
}
|
|
259
|
+
resizeObserver.disconnect();
|
|
260
|
+
viewer.removeEventListener("select", onSelect);
|
|
261
|
+
viewer.removeEventListener("psv:picture-loaded", onPictureLoaded);
|
|
262
|
+
viewer.removeEventListener("psv:view-rotated", onViewRotated);
|
|
263
|
+
viewer.removeEventListener("psv:picture-loaded", startLimit);
|
|
264
|
+
stopLimit?.();
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
[actions, onEaseMapToPoint, onPhotoSelected]
|
|
268
|
+
);
|
|
269
|
+
useEffect(
|
|
270
|
+
function syncExternalPhotoSelection() {
|
|
271
|
+
const viewer = viewerRef.current;
|
|
272
|
+
if (!viewer || photo.photoId === lastViewerPhotoIdRef.current) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
actions.reset();
|
|
276
|
+
lastViewerPhotoIdRef.current = photo.photoId;
|
|
277
|
+
if (!readyRef.current) {
|
|
278
|
+
pendingSelectRef.current = { sequenceId: photo.sequenceId, photoId: photo.photoId };
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (typeof viewer.select === "function") {
|
|
282
|
+
viewer.select(photo.sequenceId ?? null, photo.photoId);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
[actions, photo.photoId, photo.sequenceId]
|
|
286
|
+
);
|
|
287
|
+
return /* @__PURE__ */ jsxs(
|
|
288
|
+
"div",
|
|
289
|
+
{
|
|
290
|
+
ref: containerRef,
|
|
291
|
+
className: "min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900",
|
|
292
|
+
style: { aspectRatio: "4 / 3" },
|
|
293
|
+
children: [
|
|
294
|
+
hideLegend ? /* @__PURE__ */ jsx("style", { children: HIDE_LEGEND_CSS }) : null,
|
|
295
|
+
/* @__PURE__ */ jsx(
|
|
296
|
+
"pnx-photo-viewer",
|
|
297
|
+
{
|
|
298
|
+
ref: viewerRef,
|
|
299
|
+
className: "block h-full w-full",
|
|
300
|
+
endpoint: panoramaxApiEndpoint(),
|
|
301
|
+
"url-parameters": "false",
|
|
302
|
+
picture: photo.photoId,
|
|
303
|
+
sequence: photo.sequenceId ?? void 0
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export { PanoramaxPanel };
|
|
@@ -2,7 +2,3 @@ import type { DateRange } from '@osm-editor-kit/street-imagery';
|
|
|
2
2
|
import type { Bbox, NormalizedMapFeature } from '@osm-editor-kit/street-imagery';
|
|
3
3
|
import { type ProviderId } from '@osm-editor-kit/street-imagery';
|
|
4
4
|
export declare const useAllProviderMapFeatures: (providerIds: ProviderId[], bbox: Bbox | null, zoom: number, date?: DateRange) => NormalizedMapFeature[];
|
|
5
|
-
export declare const useAllProviderMapFeaturesLoading: (providerIds: ProviderId[], bbox: Bbox | null, zoom: number) => {
|
|
6
|
-
isLoading: boolean;
|
|
7
|
-
isFetching: boolean;
|
|
8
|
-
};
|
|
@@ -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 {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type PhotoDateProps = {
|
|
2
|
+
/** Capture time (ms); `null` shows "Unknown date". */
|
|
3
|
+
timestamp: number | null | undefined;
|
|
4
|
+
className?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Capture time with the camera's UTC offset (`2025-03-08T15:40:35+01:00`). The tooltip then
|
|
7
|
+
* shows date and time as the camera's clock had it, instead of the date alone.
|
|
8
|
+
*/
|
|
9
|
+
localDateTime?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A date as month and year ("Aug 2026"). The tooltip has the full date and, on a second line,
|
|
13
|
+
* how long ago that is.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PhotoDate: ({ timestamp, className, localDateTime }: PhotoDateProps) => import("react").JSX.Element;
|
|
16
|
+
export {};
|