@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
package/README.md CHANGED
@@ -12,6 +12,7 @@ React + MapLibre UI on top of `@osm-editor-kit/street-imagery`:
12
12
  - TanStack Query hooks to load provider data for the viewport
13
13
  - Lazy Mapillary and Panoramax viewer panels
14
14
  - Helpers for selection, thumbnails, and easing the map to a photo
15
+ - Viewpoints + floating photo viewer: suggested views per direction, history, map layer
15
16
 
16
17
  ## Usage
17
18
 
@@ -52,3 +53,115 @@ const hits = queryStreetImageryFeatures(event)
52
53
  **Mapillary:** `createStreetImageryConfig({ mapillaryToken })` or `setStreetImageryConfig` at boot (`@osm-editor-kit/street-imagery`). **Panoramax + Vite:** see `app/vite.config.ts` for the consuming-app setup.
53
54
 
54
55
  Also: `useAllProviderPhotos`, `useProviderPhotos` / `useProviderSequences` / `useProviderMapFeatures`, `usePhotoThumbnails`, `resolveSelectedSequence`, `StreetLevelImageryViewCone`, `useViewerBearing` / `useViewerActions`, `MapillaryPanel`, `PanoramaxPanel`.
56
+
57
+ ## Viewpoints and floating viewer
58
+
59
+ Click → viewpoints → ranked Mapillary photos per view direction → floating viewer with chips and history.
60
+
61
+ ```tsx
62
+ import { viewpointFromPoint, viewpointsFromLine } from '@osm-editor-kit/street-imagery'
63
+ import {
64
+ FloatingPhotoViewer,
65
+ getViewpointSession,
66
+ useViewpoints,
67
+ useViewSuggestions,
68
+ ViewpointLayer,
69
+ VIEWPOINT_DIRECTION_LAYER_ID,
70
+ viewDirectionKeyFromFeatures,
71
+ } from '@osm-editor-kit/street-imagery-react'
72
+
73
+ // On map click (add VIEWPOINT_DIRECTION_LAYER_ID to interactiveLayerIds):
74
+ getViewpointSession().actions.open({ viewpoints: [viewpointFromPoint([lng, lat])] })
75
+ // or for a clicked line: open({ viewpoints: viewpointsFromLine(line, [lng, lat]), line })
76
+
77
+ const viewpoints = useViewpoints()
78
+ const { suggestions } = useViewSuggestions(viewpoints, { maxAgeYears: 2 })
79
+ // <ViewpointLayer viewpoints={viewpoints} suggestions={suggestions} activeDirectionKey={…} zoom={zoom} />
80
+ // <FloatingPhotoViewer suggestions={suggestions} … >{viewer}</FloatingPhotoViewer>
81
+ ```
82
+
83
+ Render `FloatingPhotoViewer` inside a positioned map container. Photos from suggestions open in
84
+ `StreetLevelImageryViewer` with `lookAtBearing={suggestion.direction.bearing}`.
85
+
86
+ **Tailwind:** components use Tailwind classes. Tailwind does not scan `node_modules`, so add to your CSS:
87
+
88
+ ```css
89
+ @source '../node_modules/@osm-editor-kit/street-imagery-react';
90
+ ```
91
+
92
+ ## Look at a place, outline detections
93
+
94
+ ```tsx
95
+ import { JUNCTION_DETECTION_GROUPS, matchesAnyGroup, PLACE_TARGET } from '@osm-editor-kit/street-imagery'
96
+ import {
97
+ StreetLevelImageryViewer,
98
+ useMapillaryImageDetections,
99
+ useMapillaryMapFeatureImages,
100
+ useMapillaryMapFeaturesNear,
101
+ } from '@osm-editor-kit/street-imagery-react'
102
+
103
+ // Turn any Mapillary photo (360° or flat) towards a junction:
104
+ <StreetLevelImageryViewer photo={photo} lookAt={{ lngLat: node, shape: PLACE_TARGET }} … />
105
+
106
+ // Turn to a detected sign or object and outline it. `value` lets the viewer find the image's own
107
+ // detection when Mapillary linked no outline for this image:
108
+ const { data } = useMapillaryMapFeatureImages(featureId)
109
+ <StreetLevelImageryViewer photo={…} lookAt={{ lngLat: data.feature.lngLat, value: data.feature.value }} … />
110
+
111
+ // Features around a point, and only the outlines you care about in the shown image:
112
+ const { data: features } = useMapillaryMapFeaturesNear(node, { radiusMeters: 40 })
113
+ const { data: detections } = useMapillaryImageDetections(photo.photoId, {
114
+ filter: matchesAnyGroup(JUNCTION_DETECTION_GROUPS),
115
+ filterKey: 'junction',
116
+ })
117
+ <StreetLevelImageryViewer photo={photo} outlines={detections?.map((d) => ({ id: d.id, outline: d.outline }))} … />
118
+ ```
119
+
120
+ - Map layers: `filter.mapFeatureValue` limits which signs/objects are drawn (`matchesAnyGroup(SIGN_GROUPS)`).
121
+ - Suggested views: `useViewSuggestions(viewpoints, { sources: [mapillaryPhotoSource, streetViewPhotoSource] })`.
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).
@@ -0,0 +1,2 @@
1
+ export { MapillaryPanel } from './chunk-53RA4OSJ.js';
2
+ import './chunk-25UGGXIE.js';
@@ -0,0 +1,2 @@
1
+ export { PanoramaxPanel } from './chunk-EQ2JKLGT.js';
2
+ import './chunk-25UGGXIE.js';
@@ -1,9 +1,26 @@
1
- import type { Bbox, NormalizedPhoto, NormalizedSequence } from '@osm-editor-kit/street-imagery';
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
- export declare const StreetLevelImagerySelectionOverlay: ({ selectedPhoto, selectedSequence, }: StreetLevelImagerySelectionOverlayProps) => import("react").JSX.Element | null;
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;
@@ -6,6 +6,11 @@ import type { DataDrivenPropertyValueSpecification } from 'maplibre-gl';
6
6
  export type PhotoFilter = {
7
7
  photoTypes?: PhotoTypeFilter[];
8
8
  date?: DateRange;
9
+ /**
10
+ * Which map features (signs, objects) to draw, by Mapillary value. Use `matchesAnyGroup(...)`
11
+ * for groups like traffic lights or bike signs. Keep the function identity stable.
12
+ */
13
+ mapFeatureValue?: (value: string) => boolean;
9
14
  };
10
15
  export type StreetLevelImagerySourcesAndLayersProps = {
11
16
  providers: ProviderId[];
@@ -15,7 +20,7 @@ export type StreetLevelImagerySourcesAndLayersProps = {
15
20
  options: {
16
21
  config?: StreetImageryConfig;
17
22
  showSequences?: boolean;
18
- /** Always-on heading wedges / 360° disks for every photo. Default true. */
23
+ /** Heading wedges / 360° disks per photo, from zoom 17, capped for dense areas. Default true. */
19
24
  showViewfields?: boolean;
20
25
  showViewCone?: boolean;
21
26
  showSelectionHighlight?: boolean;
@@ -26,7 +31,15 @@ export type StreetLevelImagerySourcesAndLayersProps = {
26
31
  hfov?: number | null;
27
32
  lngLat?: [number, number] | null;
28
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
+ */
29
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;
30
43
  mapFeatureCircleColor: DataDrivenPropertyValueSpecification<string>;
31
44
  };
32
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;
@@ -1,10 +1,24 @@
1
1
  import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
+ import type { MapillaryLookAt, MapillaryViewerOutline } from './panels/mapillaryLookAt';
2
3
  import type { StreetImageryPhotoSelection } from './types';
3
4
  type StreetLevelImageryViewerProps = {
4
5
  photo: NormalizedPhoto;
5
6
  groupPhotos: NormalizedPhoto[];
6
7
  onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
7
8
  onEaseMapToPoint: (lng: number, lat: number) => void;
9
+ /** Mapillary, Panoramax: full photo data (creator, licence …) for each image the viewer shows. */
10
+ onViewerPhoto?: (photo: NormalizedPhoto) => void;
11
+ /** Mapillary 360° photos: initial view bearing (e.g. a suggested view direction). */
12
+ lookAtBearing?: number | null;
13
+ /** Mapillary: turn and zoom to a place (sign, traffic light, junction); wins over `lookAtBearing`. */
14
+ lookAt?: MapillaryLookAt | null;
15
+ /** Mapillary: outlines to draw in the shown image. */
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;
8
22
  };
9
- export declare const StreetLevelImageryViewer: ({ photo, groupPhotos, onPhotoSelected, onEaseMapToPoint, }: 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;
10
24
  export {};
@@ -0,0 +1,304 @@
1
+ import { useViewerActions } from './chunk-25UGGXIE.js';
2
+ import { viewFromOutline, SIGN_TARGET, viewFromFlatCamera, cameraPitch, panoX, bearingDeg, viewFromLocation, getStreetImageryConfig, fetchImageDetections } from '@osm-editor-kit/street-imagery';
3
+ import 'mapillary-js/dist/mapillary.css';
4
+ import { OutlineTag, PolygonGeometry, Viewer } from 'mapillary-js';
5
+ import { useRef, useEffectEvent, useEffect } from 'react';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ var isSpherical = (image) => image.cameraType === "spherical" || image.cameraType === "equirectangular";
9
+ var mapillaryViewFor = async (viewer, image, { lngLat, shape = SIGN_TARGET, outline }) => {
10
+ const spherical = isSpherical(image);
11
+ if (outline?.length) {
12
+ return viewFromOutline(outline, spherical, shape.maxZoom);
13
+ }
14
+ const position = image.lngLat ?? image.originalLngLat;
15
+ const imageLngLat = [position.lng, position.lat];
16
+ const compass = image.computedCompassAngle ?? image.compassAngle;
17
+ if (!spherical) {
18
+ const focal = image.cameraParameters?.[0];
19
+ return focal ? viewFromFlatCamera(
20
+ {
21
+ focal,
22
+ width: image.width,
23
+ height: image.height,
24
+ compassAngle: compass,
25
+ pitch: cameraPitch(image.rotation)
26
+ },
27
+ imageLngLat,
28
+ lngLat,
29
+ shape
30
+ ) : void 0;
31
+ }
32
+ let x = panoX(bearingDeg(imageLngLat, lngLat), compass);
33
+ try {
34
+ viewer.setCenter([x, 0.5]);
35
+ const pixel = await viewer.project({ lng: lngLat[0], lat: lngLat[1] });
36
+ const basic = pixel ? await viewer.unprojectToBasic(pixel) : null;
37
+ const projected = basic?.[0];
38
+ if (projected != null && projected >= 0 && projected <= 1) {
39
+ x = projected;
40
+ }
41
+ } catch {
42
+ }
43
+ return viewFromLocation(x, { lngLat: imageLngLat, isPano: true }, lngLat, shape);
44
+ };
45
+ var xDistance = (a, b, spherical) => {
46
+ const distance = Math.abs(a - b);
47
+ return spherical ? Math.min(distance, 1 - distance) : distance;
48
+ };
49
+ var findOwnDetection = async (image, value, estimate) => {
50
+ const spherical = isSpherical(image);
51
+ const candidates = await fetchImageDetections(image.id, { filter: (v) => v === value }).catch(
52
+ () => []
53
+ );
54
+ const centerX = (detection) => viewFromOutline(detection.outline, spherical)?.center[0] ?? 0.5;
55
+ return candidates.sort(
56
+ (a, b) => estimate ? xDistance(centerX(a), estimate.center[0], spherical) - xDistance(centerX(b), estimate.center[0], spherical) : 0
57
+ )[0];
58
+ };
59
+ var turnMapillaryViewerTo = async (viewer, image, lookAt) => {
60
+ let outline = lookAt.outline?.length ? lookAt.outline : void 0;
61
+ let outlineId = `look-at-${image.id}`;
62
+ let view;
63
+ if (!outline) {
64
+ view = await mapillaryViewFor(viewer, image, lookAt);
65
+ const own = lookAt.value ? await findOwnDetection(image, lookAt.value, view) : void 0;
66
+ if (own) {
67
+ outline = own.outline;
68
+ outlineId = own.id;
69
+ }
70
+ }
71
+ if (outline) {
72
+ view = viewFromOutline(outline, isSpherical(image), (lookAt.shape ?? SIGN_TARGET).maxZoom);
73
+ }
74
+ if (!view) {
75
+ return false;
76
+ }
77
+ viewer.setCenter(view.center);
78
+ viewer.setZoom(view.zoom);
79
+ return outline ? { id: outlineId, outline, label: lookAt.label } : null;
80
+ };
81
+ var setMapillaryViewerOutlines = (viewer, outlines) => {
82
+ const tags = viewer.getComponent("tag");
83
+ tags.removeAll();
84
+ tags.add(
85
+ outlines.filter(({ outline }) => outline.length >= 3).map(({ id, outline, label, color = 16776960 }) => {
86
+ const first = outline[0];
87
+ const last = outline[outline.length - 1];
88
+ const closed = first[0] === last[0] && first[1] === last[1] ? outline : [...outline, first];
89
+ return new OutlineTag(id, new PolygonGeometry(closed), {
90
+ text: label,
91
+ textColor: color,
92
+ lineColor: color,
93
+ lineWidth: 2,
94
+ fillColor: color,
95
+ fillOpacity: 0.3
96
+ });
97
+ })
98
+ );
99
+ };
100
+ var panoCenterX = (bearing, compassAngle) => ((0.5 + (bearing - compassAngle) / 360) % 1 + 1) % 1;
101
+ var MapillaryPanel = ({
102
+ photo,
103
+ onPhotoSelected,
104
+ onEaseMapToPoint,
105
+ onViewerPhoto,
106
+ lookAtBearing,
107
+ lookAt,
108
+ outlines,
109
+ hideAttribution = false
110
+ }) => {
111
+ const containerRef = useRef(null);
112
+ const viewerRef = useRef(null);
113
+ const navigableRef = useRef(false);
114
+ const pendingImageIdRef = useRef(null);
115
+ const lastViewerPhotoIdRef = useRef(null);
116
+ const bearingRafRef = useRef(null);
117
+ const pendingBearingRef = useRef(null);
118
+ const actions = useViewerActions();
119
+ const initialPhotoIdRef = useRef(photo.photoId);
120
+ const initialHideAttributionRef = useRef(hideAttribution);
121
+ const emitPhotoSelected = useEffectEvent(onPhotoSelected);
122
+ const emitEaseMapToPoint = useEffectEvent(onEaseMapToPoint);
123
+ const turnToWantedBearing = useEffectEvent((viewer, image) => {
124
+ if (image.id !== photo.photoId) {
125
+ return;
126
+ }
127
+ if (lookAt) {
128
+ void turnMapillaryViewerTo(viewer, image, lookAt).then((aimedAt) => {
129
+ if (aimedAt && !outlines && lastViewerPhotoIdRef.current === image.id) {
130
+ setMapillaryViewerOutlines(viewer, [aimedAt]);
131
+ }
132
+ }).catch(() => {
133
+ });
134
+ return;
135
+ }
136
+ const spherical = image.cameraType === "spherical" || image.cameraType === "equirectangular";
137
+ const compass = image.computedCompassAngle ?? image.compassAngle;
138
+ if (lookAtBearing != null && spherical && compass != null) {
139
+ viewer.setCenter([panoCenterX(lookAtBearing, compass), 0.5]);
140
+ }
141
+ });
142
+ const drawOutlines = useEffectEvent((viewer) => {
143
+ setMapillaryViewerOutlines(viewer, outlines ?? []);
144
+ });
145
+ const emitViewerPhoto = useEffectEvent(
146
+ (viewerPhoto) => onViewerPhoto?.(viewerPhoto)
147
+ );
148
+ useEffect(
149
+ function resetViewerStoreOnProviderChange() {
150
+ return () => {
151
+ actions.reset();
152
+ };
153
+ },
154
+ [actions, photo.providerId]
155
+ );
156
+ useEffect(
157
+ function mountMapillaryViewer() {
158
+ const container = containerRef.current;
159
+ if (!container) {
160
+ return;
161
+ }
162
+ const viewer = new Viewer({
163
+ accessToken: getStreetImageryConfig().mapillaryToken,
164
+ container,
165
+ imageId: initialPhotoIdRef.current,
166
+ component: {
167
+ attribution: !initialHideAttributionRef.current,
168
+ cover: false,
169
+ sequence: { visible: true },
170
+ tag: true
171
+ }
172
+ });
173
+ viewerRef.current = viewer;
174
+ lastViewerPhotoIdRef.current = initialPhotoIdRef.current;
175
+ const flushPendingMove = () => {
176
+ const pendingId = pendingImageIdRef.current;
177
+ if (!pendingId) {
178
+ return;
179
+ }
180
+ pendingImageIdRef.current = null;
181
+ void viewer.moveTo(pendingId).catch(() => {
182
+ });
183
+ };
184
+ const onNavigable = (event) => {
185
+ navigableRef.current = event.navigable;
186
+ if (event.navigable) {
187
+ flushPendingMove();
188
+ }
189
+ };
190
+ const onImage = (event) => {
191
+ const { image } = event;
192
+ lastViewerPhotoIdRef.current = image.id;
193
+ turnToWantedBearing(viewer, image);
194
+ drawOutlines(viewer);
195
+ emitPhotoSelected({
196
+ provider: "mapillary",
197
+ sequenceId: image.sequenceId,
198
+ photoId: image.id
199
+ });
200
+ const position = image.lngLat ?? image.originalLngLat;
201
+ if (!position) {
202
+ return;
203
+ }
204
+ emitViewerPhoto({
205
+ providerId: "mapillary",
206
+ photoId: image.id,
207
+ sequenceId: image.sequenceId,
208
+ capturedAt: image.capturedAt,
209
+ isPano: image.cameraType === "spherical" || image.cameraType === "equirectangular",
210
+ heading: image.computedCompassAngle ?? image.compassAngle,
211
+ lngLat: [position.lng, position.lat],
212
+ ...image.creatorUsername ? { creatorName: image.creatorUsername } : {}
213
+ });
214
+ actions.setPov({ lngLat: [position.lng, position.lat] });
215
+ emitEaseMapToPoint(position.lng, position.lat);
216
+ };
217
+ const flushBearing = () => {
218
+ bearingRafRef.current = null;
219
+ if (pendingBearingRef.current != null) {
220
+ actions.setPov({ bearing: pendingBearingRef.current });
221
+ pendingBearingRef.current = null;
222
+ }
223
+ void viewer.getFieldOfView().then((fov) => {
224
+ actions.setPov({ hfov: fov });
225
+ }).catch(() => {
226
+ });
227
+ };
228
+ const onBearing = (event) => {
229
+ pendingBearingRef.current = event.bearing;
230
+ if (bearingRafRef.current == null) {
231
+ bearingRafRef.current = requestAnimationFrame(flushBearing);
232
+ }
233
+ };
234
+ viewer.on("navigable", onNavigable);
235
+ viewer.on("image", onImage);
236
+ viewer.on("bearing", onBearing);
237
+ const resizeObserver = new ResizeObserver(() => {
238
+ viewer.resize();
239
+ });
240
+ resizeObserver.observe(container);
241
+ return () => {
242
+ if (bearingRafRef.current != null) {
243
+ cancelAnimationFrame(bearingRafRef.current);
244
+ bearingRafRef.current = null;
245
+ }
246
+ resizeObserver.disconnect();
247
+ viewer.off("navigable", onNavigable);
248
+ viewer.off("image", onImage);
249
+ viewer.off("bearing", onBearing);
250
+ viewer.remove();
251
+ viewerRef.current = null;
252
+ };
253
+ },
254
+ [actions]
255
+ );
256
+ useEffect(
257
+ function syncExternalPhotoSelection() {
258
+ const viewer = viewerRef.current;
259
+ if (!viewer || photo.photoId === lastViewerPhotoIdRef.current) {
260
+ return;
261
+ }
262
+ actions.reset();
263
+ lastViewerPhotoIdRef.current = photo.photoId;
264
+ if (navigableRef.current) {
265
+ void viewer.moveTo(photo.photoId).catch(() => {
266
+ });
267
+ } else {
268
+ pendingImageIdRef.current = photo.photoId;
269
+ }
270
+ },
271
+ [actions, photo.photoId]
272
+ );
273
+ const lookAtKey = lookAt ? `${lookAt.lngLat.join(",")}|${lookAt.outline?.length ?? 0}|${lookAt.value ?? ""}` : String(lookAtBearing);
274
+ useEffect(
275
+ function turnToTargetOnChange() {
276
+ const viewer = viewerRef.current;
277
+ if (!viewer || lastViewerPhotoIdRef.current !== photo.photoId) {
278
+ return;
279
+ }
280
+ void viewer.getImage().then((image) => turnToWantedBearing(viewer, image)).catch(() => {
281
+ });
282
+ },
283
+ [lookAtKey, photo.photoId]
284
+ );
285
+ useEffect(
286
+ function redrawOutlinesOnChange() {
287
+ const viewer = viewerRef.current;
288
+ if (viewer && navigableRef.current) {
289
+ drawOutlines(viewer);
290
+ }
291
+ },
292
+ [outlines]
293
+ );
294
+ return /* @__PURE__ */ jsx(
295
+ "div",
296
+ {
297
+ ref: containerRef,
298
+ className: "min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900",
299
+ style: { aspectRatio: "4 / 3" }
300
+ }
301
+ );
302
+ };
303
+
304
+ export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo };