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

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 ADDED
@@ -0,0 +1,47 @@
1
+ # `@osm-editor-kit/street-imagery-react`
2
+
3
+ **Status:** First npm **alpha** (`0.1.0-alpha.0`). Publish with `bun run packages:release -- --publish-only` after `npm login`.
4
+
5
+ ## What it does
6
+
7
+ React MapLibre bindings for street-level imagery: map sources/layers, photo click handling, TanStack Query provider hooks, and lazy Mapillary/Panoramax viewer panels. Data adapters and GeoJSON helpers live in `@osm-editor-kit/street-imagery`.
8
+
9
+ ## Usage
10
+
11
+ ```tsx
12
+ import {
13
+ StreetLevelImagerySourcesAndLayers,
14
+ StreetLevelImageryViewer,
15
+ useMapViewportBbox,
16
+ queryStreetImageryFeatures,
17
+ streetImageryInteractiveLayerIds,
18
+ } from '@osm-editor-kit/street-imagery-react'
19
+ import { createStreetImageryConfig } from '@osm-editor-kit/street-imagery'
20
+
21
+ const bbox = useMapViewportBbox('main', map)
22
+
23
+ <StreetLevelImagerySourcesAndLayers
24
+ providers={['mapillary', 'panoramax']}
25
+ bbox={bbox}
26
+ zoom={map.zoom}
27
+ filter={{ photoTypes: ['flat', 'pano'] }}
28
+ options={{
29
+ config: createStreetImageryConfig({ mapillaryToken: '…' }),
30
+ showSequences: true,
31
+ showViewfields: true,
32
+ selectedPhoto,
33
+ photoCircleColor: '#3b82f6',
34
+ mapFeatureCircleColor: '#94a3b8',
35
+ }}
36
+ />
37
+
38
+ // interactiveLayerIds={streetImageryInteractiveLayerIds(providers)}
39
+ const hits = queryStreetImageryFeatures(event)
40
+
41
+ <StreetLevelImageryViewer photo={selectedPhoto} groupPhotos={sequencePhotos}
42
+ onPhotoSelected={setSelection} onEaseMapToPoint={(lng, lat) => map.easeTo({ center: [lng, lat] })} />
43
+ ```
44
+
45
+ **Mapillary:** `createStreetImageryConfig({ mapillaryToken })` or `setStreetImageryConfig` at boot (`@osm-editor-kit/street-imagery`). **Panoramax + Vite:** see `app/vite.config.ts` for the consuming-app setup.
46
+
47
+ Also: `useAllProviderPhotos`, `useProviderPhotos` / `useProviderSequences` / `useProviderMapFeatures`, `usePhotoThumbnails`, `resolveSelectedSequence`, `StreetLevelImageryViewCone`, `useViewerBearing` / `useViewerActions`, `MapillaryPanel`, `PanoramaxPanel`.
@@ -0,0 +1,2 @@
1
+ export { MapillaryPanel } from './chunk-BS6LPDMY.js';
2
+ import './chunk-25UGGXIE.js';
@@ -0,0 +1,2 @@
1
+ export { PanoramaxPanel } from './chunk-ZH3TPW4D.js';
2
+ import './chunk-25UGGXIE.js';
@@ -0,0 +1,9 @@
1
+ import type { Bbox, NormalizedPhoto, NormalizedSequence } from '@osm-editor-kit/street-imagery';
2
+ export type StreetLevelImagerySelectionOverlayProps = {
3
+ selectedPhoto?: NormalizedPhoto | null;
4
+ selectedSequence?: NormalizedSequence | null;
5
+ };
6
+ export declare const StreetLevelImagerySelectionOverlay: ({ selectedPhoto, selectedSequence, }: StreetLevelImagerySelectionOverlayProps) => import("react").JSX.Element | null;
7
+ /** Resolve sequence geometry for highlight when only photo + fetched sequences are available. */
8
+ export declare const resolveSelectedSequence: (selectedPhoto: NormalizedPhoto | null | undefined, sequences: NormalizedSequence[], sequenceId: string | null | undefined) => NormalizedSequence | null;
9
+ export type SequenceHighlightBbox = Bbox | null;
@@ -0,0 +1,33 @@
1
+ import { type StreetImageryConfig } from '@osm-editor-kit/street-imagery';
2
+ import { type DateRange, type PhotoTypeFilter } from '@osm-editor-kit/street-imagery';
3
+ import type { Bbox, NormalizedPhoto } from '@osm-editor-kit/street-imagery';
4
+ import { type ProviderId } from '@osm-editor-kit/street-imagery';
5
+ import type { DataDrivenPropertyValueSpecification } from 'maplibre-gl';
6
+ export type PhotoFilter = {
7
+ photoTypes?: PhotoTypeFilter[];
8
+ date?: DateRange;
9
+ };
10
+ export type StreetLevelImagerySourcesAndLayersProps = {
11
+ providers: ProviderId[];
12
+ filter?: PhotoFilter;
13
+ bbox: Bbox | null;
14
+ zoom: number;
15
+ options: {
16
+ config?: StreetImageryConfig;
17
+ showSequences?: boolean;
18
+ /** Always-on heading wedges / 360° disks for every photo. Default true. */
19
+ showViewfields?: boolean;
20
+ showViewCone?: boolean;
21
+ showSelectionHighlight?: boolean;
22
+ selectedPhoto?: NormalizedPhoto | null;
23
+ selectedSequenceId?: string | null;
24
+ viewerPov?: {
25
+ bearing?: number | null;
26
+ hfov?: number | null;
27
+ lngLat?: [number, number] | null;
28
+ } | null;
29
+ photoCircleColor: DataDrivenPropertyValueSpecification<string>;
30
+ mapFeatureCircleColor: DataDrivenPropertyValueSpecification<string>;
31
+ };
32
+ };
33
+ export declare const StreetLevelImagerySourcesAndLayers: ({ providers, filter, bbox, zoom, options, }: StreetLevelImagerySourcesAndLayersProps) => import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
+ export type StreetLevelImageryViewConeProps = {
3
+ selectedPhoto: NormalizedPhoto;
4
+ zoom: number;
5
+ viewerPov?: {
6
+ bearing?: number | null;
7
+ hfov?: number | null;
8
+ lngLat?: [number, number] | null;
9
+ } | null;
10
+ };
11
+ export declare const StreetLevelImageryViewCone: ({ selectedPhoto, zoom, viewerPov, }: StreetLevelImageryViewConeProps) => import("react").JSX.Element | null;
@@ -0,0 +1,10 @@
1
+ import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
+ import type { StreetImageryPhotoSelection } from './types';
3
+ type StreetLevelImageryViewerProps = {
4
+ photo: NormalizedPhoto;
5
+ groupPhotos: NormalizedPhoto[];
6
+ onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
7
+ onEaseMapToPoint: (lng: number, lat: number) => void;
8
+ };
9
+ export declare const StreetLevelImageryViewer: ({ photo, groupPhotos, onPhotoSelected, onEaseMapToPoint, }: StreetLevelImageryViewerProps) => import("react").JSX.Element | null;
10
+ export {};
@@ -0,0 +1,25 @@
1
+ import { create } from 'zustand';
2
+
3
+ // src/useViewerStore.ts
4
+ var initialState = {
5
+ bearing: null,
6
+ hfov: null,
7
+ lngLat: null
8
+ };
9
+ var useViewerStore = create()((set) => ({
10
+ ...initialState,
11
+ actions: {
12
+ setPov: (partial) => set((state) => ({
13
+ bearing: partial.bearing !== void 0 ? partial.bearing : state.bearing,
14
+ hfov: partial.hfov !== void 0 ? partial.hfov : state.hfov,
15
+ lngLat: partial.lngLat !== void 0 ? partial.lngLat : state.lngLat
16
+ })),
17
+ reset: () => set(initialState)
18
+ }
19
+ }));
20
+ var useViewerBearing = () => useViewerStore((state) => state.bearing);
21
+ var useViewerHfov = () => useViewerStore((state) => state.hfov);
22
+ var useViewerLngLat = () => useViewerStore((state) => state.lngLat);
23
+ var useViewerActions = () => useViewerStore((state) => state.actions);
24
+
25
+ export { useViewerActions, useViewerBearing, useViewerHfov, useViewerLngLat };
@@ -0,0 +1,143 @@
1
+ import { useViewerActions } from './chunk-25UGGXIE.js';
2
+ import { getStreetImageryConfig } from '@osm-editor-kit/street-imagery';
3
+ import 'mapillary-js/dist/mapillary.css';
4
+ import { Viewer } from 'mapillary-js';
5
+ import { useRef, useEffect } from 'react';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ var MapillaryPanel = ({
9
+ photo,
10
+ onPhotoSelected,
11
+ onEaseMapToPoint
12
+ }) => {
13
+ const containerRef = useRef(null);
14
+ const viewerRef = useRef(null);
15
+ const navigableRef = useRef(false);
16
+ const pendingImageIdRef = useRef(null);
17
+ const lastViewerPhotoIdRef = useRef(null);
18
+ const bearingRafRef = useRef(null);
19
+ const pendingBearingRef = useRef(null);
20
+ const actions = useViewerActions();
21
+ const initialPhotoIdRef = useRef(photo.photoId);
22
+ useEffect(
23
+ function resetViewerStoreOnProviderChange() {
24
+ return () => {
25
+ actions.reset();
26
+ };
27
+ },
28
+ [actions, photo.providerId]
29
+ );
30
+ useEffect(
31
+ function mountMapillaryViewer() {
32
+ const container = containerRef.current;
33
+ if (!container) {
34
+ return;
35
+ }
36
+ const viewer = new Viewer({
37
+ accessToken: getStreetImageryConfig().mapillaryToken,
38
+ container,
39
+ imageId: initialPhotoIdRef.current,
40
+ component: {
41
+ cover: false,
42
+ sequence: { visible: true }
43
+ }
44
+ });
45
+ viewerRef.current = viewer;
46
+ lastViewerPhotoIdRef.current = initialPhotoIdRef.current;
47
+ const flushPendingMove = () => {
48
+ const pendingId = pendingImageIdRef.current;
49
+ if (!pendingId) {
50
+ return;
51
+ }
52
+ pendingImageIdRef.current = null;
53
+ void viewer.moveTo(pendingId).catch(() => {
54
+ });
55
+ };
56
+ const onNavigable = (event) => {
57
+ navigableRef.current = event.navigable;
58
+ if (event.navigable) {
59
+ flushPendingMove();
60
+ }
61
+ };
62
+ const onImage = (event) => {
63
+ const { image } = event;
64
+ lastViewerPhotoIdRef.current = image.id;
65
+ onPhotoSelected({
66
+ provider: "mapillary",
67
+ sequenceId: image.sequenceId,
68
+ photoId: image.id
69
+ });
70
+ const position = image.lngLat ?? image.originalLngLat;
71
+ if (!position) {
72
+ return;
73
+ }
74
+ actions.setPov({ lngLat: [position.lng, position.lat] });
75
+ onEaseMapToPoint(position.lng, position.lat);
76
+ };
77
+ const flushBearing = () => {
78
+ bearingRafRef.current = null;
79
+ if (pendingBearingRef.current != null) {
80
+ actions.setPov({ bearing: pendingBearingRef.current });
81
+ pendingBearingRef.current = null;
82
+ }
83
+ void viewer.getFieldOfView().then((fov) => {
84
+ actions.setPov({ hfov: fov });
85
+ }).catch(() => {
86
+ });
87
+ };
88
+ const onBearing = (event) => {
89
+ pendingBearingRef.current = event.bearing;
90
+ if (bearingRafRef.current == null) {
91
+ bearingRafRef.current = requestAnimationFrame(flushBearing);
92
+ }
93
+ };
94
+ viewer.on("navigable", onNavigable);
95
+ viewer.on("image", onImage);
96
+ viewer.on("bearing", onBearing);
97
+ const resizeObserver = new ResizeObserver(() => {
98
+ viewer.resize();
99
+ });
100
+ resizeObserver.observe(container);
101
+ return () => {
102
+ if (bearingRafRef.current != null) {
103
+ cancelAnimationFrame(bearingRafRef.current);
104
+ bearingRafRef.current = null;
105
+ }
106
+ resizeObserver.disconnect();
107
+ viewer.off("navigable", onNavigable);
108
+ viewer.off("image", onImage);
109
+ viewer.off("bearing", onBearing);
110
+ viewer.remove();
111
+ viewerRef.current = null;
112
+ };
113
+ },
114
+ [actions, onEaseMapToPoint, onPhotoSelected]
115
+ );
116
+ useEffect(
117
+ function syncExternalPhotoSelection() {
118
+ const viewer = viewerRef.current;
119
+ if (!viewer || photo.photoId === lastViewerPhotoIdRef.current) {
120
+ return;
121
+ }
122
+ actions.reset();
123
+ lastViewerPhotoIdRef.current = photo.photoId;
124
+ if (navigableRef.current) {
125
+ void viewer.moveTo(photo.photoId).catch(() => {
126
+ });
127
+ } else {
128
+ pendingImageIdRef.current = photo.photoId;
129
+ }
130
+ },
131
+ [actions, photo.photoId]
132
+ );
133
+ return /* @__PURE__ */ jsx(
134
+ "div",
135
+ {
136
+ ref: containerRef,
137
+ className: "min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900",
138
+ style: { aspectRatio: "4 / 3" }
139
+ }
140
+ );
141
+ };
142
+
143
+ export { MapillaryPanel };
@@ -0,0 +1,165 @@
1
+ import { useViewerActions } from './chunk-25UGGXIE.js';
2
+ import '@panoramax/web-viewer';
3
+ import { getStreetImageryConfig } from '@osm-editor-kit/street-imagery';
4
+ import { useRef, useEffect } from 'react';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ var panoramaxApiEndpoint = () => `${getStreetImageryConfig().panoramaxApiBase}/api`;
8
+ var normalizeBearing = (degrees) => (degrees % 360 + 360) % 360;
9
+ var PanoramaxPanel = ({
10
+ photo,
11
+ onPhotoSelected,
12
+ onEaseMapToPoint
13
+ }) => {
14
+ const containerRef = useRef(null);
15
+ const viewerRef = useRef(null);
16
+ const lastViewerPhotoIdRef = useRef(null);
17
+ const readyRef = useRef(false);
18
+ const pendingSelectRef = useRef(null);
19
+ const photoRef = useRef(photo);
20
+ const bearingRafRef = useRef(null);
21
+ const pendingBearingRef = useRef(null);
22
+ const pendingHfovRef = useRef(null);
23
+ const actions = useViewerActions();
24
+ useEffect(
25
+ function syncPhotoRef() {
26
+ photoRef.current = photo;
27
+ },
28
+ [photo]
29
+ );
30
+ useEffect(
31
+ function resetViewerStoreOnProviderChange() {
32
+ return () => {
33
+ actions.reset();
34
+ };
35
+ },
36
+ [actions, photo.providerId]
37
+ );
38
+ useEffect(
39
+ function mountPanoramaxViewer() {
40
+ const viewer = viewerRef.current;
41
+ const container = containerRef.current;
42
+ if (!viewer || !container) {
43
+ return;
44
+ }
45
+ lastViewerPhotoIdRef.current = photoRef.current.photoId;
46
+ const flushBearing = () => {
47
+ bearingRafRef.current = null;
48
+ if (pendingBearingRef.current != null) {
49
+ actions.setPov({ bearing: pendingBearingRef.current });
50
+ pendingBearingRef.current = null;
51
+ }
52
+ if (pendingHfovRef.current != null) {
53
+ actions.setPov({ hfov: pendingHfovRef.current });
54
+ pendingHfovRef.current = null;
55
+ }
56
+ };
57
+ const onSelect = (event) => {
58
+ const { seqId, picId } = event.detail;
59
+ if (!picId) {
60
+ return;
61
+ }
62
+ lastViewerPhotoIdRef.current = picId;
63
+ const currentPhoto = photoRef.current;
64
+ const sequenceId = seqId ?? (picId === currentPhoto.photoId && currentPhoto.sequenceId ? currentPhoto.sequenceId : `photo:${picId}`);
65
+ onPhotoSelected({
66
+ provider: "panoramax",
67
+ sequenceId,
68
+ photoId: picId
69
+ });
70
+ };
71
+ const onPictureLoaded = (event) => {
72
+ const detail = event.detail;
73
+ if (detail.lon == null || detail.lat == null) {
74
+ return;
75
+ }
76
+ actions.setPov({ lngLat: [detail.lon, detail.lat] });
77
+ if (detail.x != null) {
78
+ actions.setPov({ bearing: normalizeBearing(detail.x) });
79
+ }
80
+ const psv = viewer.psv;
81
+ if (psv && detail.z != null) {
82
+ actions.setPov({ hfov: psv.dataHelper.zoomLevelToFov(detail.z) });
83
+ }
84
+ onEaseMapToPoint(detail.lon, detail.lat);
85
+ };
86
+ const onViewRotated = (event) => {
87
+ const detail = event.detail;
88
+ pendingBearingRef.current = normalizeBearing(detail.x);
89
+ const psv = viewer.psv;
90
+ if (psv && detail.z != null) {
91
+ pendingHfovRef.current = psv.dataHelper.zoomLevelToFov(detail.z);
92
+ }
93
+ if (bearingRafRef.current == null) {
94
+ bearingRafRef.current = requestAnimationFrame(flushBearing);
95
+ }
96
+ };
97
+ viewer.addEventListener("select", onSelect);
98
+ viewer.addEventListener("psv:picture-loaded", onPictureLoaded);
99
+ viewer.addEventListener("psv:view-rotated", onViewRotated);
100
+ const resizeObserver = new ResizeObserver(() => {
101
+ viewer.psv?.resize();
102
+ });
103
+ resizeObserver.observe(container);
104
+ readyRef.current = true;
105
+ const pending = pendingSelectRef.current;
106
+ if (pending) {
107
+ pendingSelectRef.current = null;
108
+ if (typeof viewer.select === "function") {
109
+ viewer.select(pending.sequenceId ?? null, pending.photoId);
110
+ }
111
+ }
112
+ return () => {
113
+ readyRef.current = false;
114
+ if (bearingRafRef.current != null) {
115
+ cancelAnimationFrame(bearingRafRef.current);
116
+ bearingRafRef.current = null;
117
+ }
118
+ resizeObserver.disconnect();
119
+ viewer.removeEventListener("select", onSelect);
120
+ viewer.removeEventListener("psv:picture-loaded", onPictureLoaded);
121
+ viewer.removeEventListener("psv:view-rotated", onViewRotated);
122
+ };
123
+ },
124
+ [actions, onEaseMapToPoint, onPhotoSelected]
125
+ );
126
+ useEffect(
127
+ function syncExternalPhotoSelection() {
128
+ const viewer = viewerRef.current;
129
+ if (!viewer || photo.photoId === lastViewerPhotoIdRef.current) {
130
+ return;
131
+ }
132
+ actions.reset();
133
+ lastViewerPhotoIdRef.current = photo.photoId;
134
+ if (!readyRef.current) {
135
+ pendingSelectRef.current = { sequenceId: photo.sequenceId, photoId: photo.photoId };
136
+ return;
137
+ }
138
+ if (typeof viewer.select === "function") {
139
+ viewer.select(photo.sequenceId ?? null, photo.photoId);
140
+ }
141
+ },
142
+ [actions, photo.photoId, photo.sequenceId]
143
+ );
144
+ return /* @__PURE__ */ jsx(
145
+ "div",
146
+ {
147
+ ref: containerRef,
148
+ className: "min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900",
149
+ style: { aspectRatio: "4 / 3" },
150
+ children: /* @__PURE__ */ jsx(
151
+ "pnx-photo-viewer",
152
+ {
153
+ ref: viewerRef,
154
+ className: "block h-full w-full",
155
+ endpoint: panoramaxApiEndpoint(),
156
+ "url-parameters": "false",
157
+ picture: photo.photoId,
158
+ sequence: photo.sequenceId ?? void 0
159
+ }
160
+ )
161
+ }
162
+ );
163
+ };
164
+
165
+ export { PanoramaxPanel };
@@ -0,0 +1,8 @@
1
+ import type { DateRange } from '@osm-editor-kit/street-imagery';
2
+ import type { Bbox, NormalizedMapFeature } from '@osm-editor-kit/street-imagery';
3
+ import { type ProviderId } from '@osm-editor-kit/street-imagery';
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,9 @@
1
+ import type { DateRange, PhotoTypeFilter } from '@osm-editor-kit/street-imagery';
2
+ import type { Bbox, NormalizedPhoto } from '@osm-editor-kit/street-imagery';
3
+ import { type ProviderId } from '@osm-editor-kit/street-imagery';
4
+ export type AllProviderPhotosResult = {
5
+ photos: NormalizedPhoto[];
6
+ isLoading: boolean;
7
+ isFetching: boolean;
8
+ };
9
+ export declare const useAllProviderPhotos: (providerIds: ProviderId[], bbox: Bbox | null, zoom: number, photoTypes?: PhotoTypeFilter[], date?: DateRange) => AllProviderPhotosResult;
@@ -0,0 +1,9 @@
1
+ import type { Bbox } from '@osm-editor-kit/street-imagery';
2
+ /**
3
+ * Live WGS84 bbox from a react-map-gl map instance.
4
+ * @param mapId - react-map-gl `<Map id=…>`
5
+ * @param viewportTrigger - optional value that changes when the camera moves
6
+ * (e.g. URL map param) so the bbox recomputes after pan/zoom without relying
7
+ * on MapLibre events.
8
+ */
9
+ export declare const useMapViewportBbox: (mapId: string, viewportTrigger?: unknown) => Bbox | null;
@@ -0,0 +1,3 @@
1
+ import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
+ export declare const usePhotoFullUrl: (photo: NormalizedPhoto | null) => import("@tanstack/react-query").UseQueryResult<string | null, Error>;
3
+ export declare const usePhotoThumbnail: (photo: NormalizedPhoto | null) => import("@tanstack/react-query").UseQueryResult<string | null, Error>;
@@ -0,0 +1,5 @@
1
+ import type { Bbox } from '@osm-editor-kit/street-imagery';
2
+ import { type ProviderId } from '@osm-editor-kit/street-imagery';
3
+ export declare const useProviderPhotos: (providerId: ProviderId, bbox: Bbox | null, zoom: number) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedPhoto[]>, Error>;
4
+ export declare const useProviderSequences: (providerId: ProviderId, bbox: Bbox | null, zoom: number) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedSequence[]>, Error>;
5
+ export declare const useProviderMapFeatures: (providerId: ProviderId, bbox: Bbox | null, zoom: number) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedMapFeature[]>, Error>;
@@ -0,0 +1,14 @@
1
+ export { StreetLevelImagerySelectionOverlay, resolveSelectedSequence, } from './StreetLevelImagerySelectionOverlay';
2
+ export { StreetLevelImagerySourcesAndLayers, type PhotoFilter, } from './StreetLevelImagerySourcesAndLayers';
3
+ export { StreetLevelImageryViewCone } from './StreetLevelImageryViewCone';
4
+ export { StreetLevelImageryViewer } from './StreetLevelImageryViewer';
5
+ export type { StreetImageryPhotoSelection } from './types';
6
+ export { queryStreetImageryFeatures, streetImageryInteractiveLayerIds, type StreetImageryClickFeature, } from './streetImageryClick';
7
+ export * from './hooks/useAllProviderMapFeatures';
8
+ export * from './hooks/useAllProviderPhotos';
9
+ export * from './hooks/useMapViewportBbox';
10
+ export * from './hooks/useProviderData';
11
+ export * from './hooks/usePhotoThumbnails';
12
+ export { MapillaryPanel } from './panels/MapillaryPanel';
13
+ export { PanoramaxPanel } from './panels/PanoramaxPanel';
14
+ export * from './useViewerStore';
package/dist/index.js ADDED
@@ -0,0 +1,659 @@
1
+ export { MapillaryPanel } from './chunk-BS6LPDMY.js';
2
+ export { PanoramaxPanel } from './chunk-ZH3TPW4D.js';
3
+ export { useViewerActions, useViewerBearing, useViewerHfov, useViewerLngLat } from './chunk-25UGGXIE.js';
4
+ import { photosToFeatureCollection, emptyPointCollection, sequencesToFeatureCollection, emptyLineCollection, adapterById, viewConeGeoJson, coneRadiusMeters, setStreetImageryConfig, photoLayerId, viewfieldLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, mapFeaturesToFeatureCollection, buildMapFeatureLayerFilter, featureSourceId, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId } from '@osm-editor-kit/street-imagery';
5
+ import { Source, Layer, useMap } from 'react-map-gl/maplibre';
6
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
7
+ import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState } from 'react';
8
+ import { useQuery } from '@tanstack/react-query';
9
+
10
+ var HIGHLIGHT_SOURCE_ID = "selection-highlight";
11
+ var HIGHLIGHT_LAYER_ID = "selection-highlight-layer";
12
+ var SEQUENCE_HIGHLIGHT_SOURCE_ID = "sequence-highlight";
13
+ var SEQUENCE_HIGHLIGHT_LAYER_ID = "sequence-highlight-layer";
14
+ var StreetLevelImagerySelectionOverlay = ({
15
+ selectedPhoto,
16
+ selectedSequence
17
+ }) => {
18
+ const highlightCollection = selectedPhoto ? photosToFeatureCollection([selectedPhoto]) : emptyPointCollection();
19
+ const sequenceHighlightCollection = selectedSequence ? sequencesToFeatureCollection([selectedSequence]) : emptyLineCollection();
20
+ if (!selectedPhoto && sequenceHighlightCollection.features.length === 0) {
21
+ return null;
22
+ }
23
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
24
+ selectedPhoto ? /* @__PURE__ */ jsxs(Fragment, { children: [
25
+ /* @__PURE__ */ jsx(Source, { id: HIGHLIGHT_SOURCE_ID, type: "geojson", data: highlightCollection }),
26
+ /* @__PURE__ */ jsx(
27
+ Layer,
28
+ {
29
+ id: HIGHLIGHT_LAYER_ID,
30
+ type: "circle",
31
+ source: HIGHLIGHT_SOURCE_ID,
32
+ paint: {
33
+ "circle-radius": 10,
34
+ "circle-color": "#ffffff",
35
+ "circle-stroke-width": 3,
36
+ "circle-stroke-color": "#0f172a"
37
+ }
38
+ }
39
+ )
40
+ ] }) : null,
41
+ sequenceHighlightCollection.features.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
42
+ /* @__PURE__ */ jsx(
43
+ Source,
44
+ {
45
+ id: SEQUENCE_HIGHLIGHT_SOURCE_ID,
46
+ type: "geojson",
47
+ data: sequenceHighlightCollection
48
+ }
49
+ ),
50
+ /* @__PURE__ */ jsx(
51
+ Layer,
52
+ {
53
+ id: SEQUENCE_HIGHLIGHT_LAYER_ID,
54
+ type: "line",
55
+ source: SEQUENCE_HIGHLIGHT_SOURCE_ID,
56
+ paint: {
57
+ "line-color": "#0f172a",
58
+ "line-width": 4,
59
+ "line-opacity": 0.75
60
+ }
61
+ }
62
+ )
63
+ ] }) : null
64
+ ] });
65
+ };
66
+ var resolveSelectedSequence = (selectedPhoto, sequences, sequenceId) => {
67
+ if (!selectedPhoto || !sequenceId) {
68
+ return null;
69
+ }
70
+ return sequences.find(
71
+ (sequence) => sequence.providerId === selectedPhoto.providerId && sequence.sequenceId === sequenceId
72
+ ) ?? null;
73
+ };
74
+ var bboxKey = (bbox) => bbox ? `${bbox[0]},${bbox[1]},${bbox[2]},${bbox[3]}` : "none";
75
+ var withinBbox = (bbox) => (item) => {
76
+ const [lng, lat] = item.lngLat;
77
+ return lng >= bbox[0] && lng <= bbox[2] && lat >= bbox[1] && lat <= bbox[3];
78
+ };
79
+ var useProviderPhotos = (providerId, bbox, zoom) => {
80
+ const adapter = adapterById[providerId];
81
+ const enabled = adapter.kind === "photo" && bbox != null && zoom >= adapter.minZoom && adapter.fetchPhotos != null;
82
+ return useQuery({
83
+ queryKey: ["provider-photos", providerId, bboxKey(bbox), zoom],
84
+ queryFn: async ({ signal }) => {
85
+ const photos = await adapter.fetchPhotos(bbox, zoom, signal);
86
+ return photos.filter(withinBbox(bbox));
87
+ },
88
+ enabled,
89
+ placeholderData: (previous) => previous
90
+ });
91
+ };
92
+ var useProviderSequences = (providerId, bbox, zoom) => {
93
+ const adapter = adapterById[providerId];
94
+ const sequencesMinZoom = adapter.sequencesMinZoom ?? adapter.minZoom;
95
+ const enabled = adapter.kind === "photo" && bbox != null && zoom >= sequencesMinZoom && adapter.fetchSequences != null;
96
+ return useQuery({
97
+ queryKey: ["provider-sequences", providerId, bboxKey(bbox), zoom],
98
+ queryFn: ({ signal }) => adapter.fetchSequences?.(bbox, zoom, signal) ?? [],
99
+ enabled,
100
+ placeholderData: (previous) => previous
101
+ });
102
+ };
103
+ var useProviderMapFeatures = (providerId, bbox, zoom) => {
104
+ const adapter = adapterById[providerId];
105
+ const enabled = adapter.kind === "mapFeature" && bbox != null && zoom >= adapter.minZoom && adapter.fetchMapFeatures != null;
106
+ return useQuery({
107
+ queryKey: ["provider-map-features", providerId, bboxKey(bbox), zoom],
108
+ queryFn: async ({ signal }) => {
109
+ const features = await adapter.fetchMapFeatures(bbox, zoom, signal);
110
+ return features.filter(withinBbox(bbox));
111
+ },
112
+ enabled,
113
+ placeholderData: (previous) => previous
114
+ });
115
+ };
116
+ var CONE_SOURCE_ID = "view-direction-cone";
117
+ var CONE_FILL_LAYER_ID = "view-direction-cone-fill";
118
+ var CONE_LINE_LAYER_ID = "view-direction-cone-line";
119
+ var INTERACTIVE_PANO_PROVIDERS = /* @__PURE__ */ new Set([
120
+ "mapillary",
121
+ "panoramax",
122
+ "streetside",
123
+ "kartaview",
124
+ "mapilio",
125
+ "vegbilder"
126
+ ]);
127
+ var StreetLevelImageryViewCone = ({
128
+ selectedPhoto,
129
+ zoom,
130
+ viewerPov
131
+ }) => {
132
+ const apex = viewerPov?.lngLat ?? selectedPhoto.lngLat;
133
+ const isPano = selectedPhoto.isPano === true;
134
+ const hasLiveBearing = isPano && INTERACTIVE_PANO_PROVIDERS.has(selectedPhoto.providerId);
135
+ let bearing = null;
136
+ let fov = 30;
137
+ if (isPano) {
138
+ bearing = hasLiveBearing ? viewerPov?.bearing ?? selectedPhoto.heading : selectedPhoto.heading;
139
+ fov = hasLiveBearing ? viewerPov?.hfov ?? 60 : 60;
140
+ } else if (selectedPhoto.providerId === "panoramax") {
141
+ bearing = viewerPov?.bearing ?? selectedPhoto.heading;
142
+ fov = 30;
143
+ } else {
144
+ bearing = selectedPhoto.heading;
145
+ fov = 30;
146
+ }
147
+ if (bearing == null) {
148
+ return null;
149
+ }
150
+ const coneFeature = viewConeGeoJson(apex, bearing, fov, coneRadiusMeters(zoom));
151
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
152
+ /* @__PURE__ */ jsx(Source, { id: CONE_SOURCE_ID, type: "geojson", data: coneFeature }),
153
+ /* @__PURE__ */ jsx(
154
+ Layer,
155
+ {
156
+ id: CONE_FILL_LAYER_ID,
157
+ type: "fill",
158
+ source: CONE_SOURCE_ID,
159
+ paint: {
160
+ "fill-color": "#0f172a",
161
+ "fill-opacity": 0.15
162
+ }
163
+ }
164
+ ),
165
+ /* @__PURE__ */ jsx(
166
+ Layer,
167
+ {
168
+ id: CONE_LINE_LAYER_ID,
169
+ type: "line",
170
+ source: CONE_SOURCE_ID,
171
+ paint: {
172
+ "line-color": "#0f172a",
173
+ "line-width": 1.5,
174
+ "line-opacity": 0.5
175
+ }
176
+ }
177
+ )
178
+ ] });
179
+ };
180
+ var CIRCLE_RADIUS = [
181
+ "interpolate",
182
+ ["linear"],
183
+ ["zoom"],
184
+ 10,
185
+ 2,
186
+ 14,
187
+ 4,
188
+ 18,
189
+ 6
190
+ ];
191
+ var FEATURE_CIRCLE_RADIUS = [
192
+ "interpolate",
193
+ ["linear"],
194
+ ["zoom"],
195
+ 10,
196
+ 1.5,
197
+ 14,
198
+ 3,
199
+ 18,
200
+ 4
201
+ ];
202
+ var PHOTO_SORT_KEY = ["coalesce", ["get", "capturedAt"], 0];
203
+ var FEATURE_SORT_KEY = ["coalesce", ["get", "lastSeenAt"], 0];
204
+ var PhotoProviderLayer = ({
205
+ providerId,
206
+ bbox,
207
+ zoom,
208
+ filter,
209
+ showSequences,
210
+ showViewfields,
211
+ photoCircleColor
212
+ }) => {
213
+ const adapter = adapterById[providerId];
214
+ const { data: photos = [] } = useProviderPhotos(providerId, bbox, zoom);
215
+ const { data: sequences = [] } = useProviderSequences(providerId, bbox, zoom);
216
+ const visiblePhotos = zoom >= adapter.minZoom ? photos.filter((photo) => photoMatchesFilters(photo, filter?.photoTypes, filter?.date)) : [];
217
+ const photoCollection = zoom >= adapter.minZoom ? photosToFeatureCollection(photos) : emptyPointCollection();
218
+ const viewfieldCollection = showViewfields ? photosToViewfieldsFeatureCollection(visiblePhotos, zoom) : emptyPolygonCollection();
219
+ const photoFilter = buildPhotoLayerFilter(filter?.photoTypes, filter?.date);
220
+ const photoSrcId = photoSourceId(providerId);
221
+ const viewfieldSrcId = viewfieldSourceId(providerId);
222
+ const sequenceCapturedAtById = /* @__PURE__ */ new Map();
223
+ for (const photo of visiblePhotos) {
224
+ if (photo.sequenceId == null || photo.capturedAt == null) continue;
225
+ const prev = sequenceCapturedAtById.get(photo.sequenceId);
226
+ if (prev == null || photo.capturedAt > prev) {
227
+ sequenceCapturedAtById.set(photo.sequenceId, photo.capturedAt);
228
+ }
229
+ }
230
+ const filteredSequences = showSequences && zoom >= (adapter.sequencesMinZoom ?? adapter.minZoom) ? sequences.map((sequence) => {
231
+ if (sequence.capturedAt != null) return sequence;
232
+ const fromPhotos = sequenceCapturedAtById.get(sequence.sequenceId);
233
+ return fromPhotos != null ? { ...sequence, capturedAt: fromPhotos } : sequence;
234
+ }).filter((sequence) => {
235
+ const asPhoto = {
236
+ providerId: sequence.providerId,
237
+ photoId: sequence.sequenceId,
238
+ sequenceId: sequence.sequenceId,
239
+ capturedAt: sequence.capturedAt,
240
+ isPano: sequence.isPano,
241
+ heading: null,
242
+ lngLat: [0, 0]
243
+ };
244
+ return photoMatchesFilters(asPhoto, filter?.photoTypes, filter?.date);
245
+ }) : [];
246
+ const sequenceCollection = filteredSequences.length > 0 ? sequencesToFeatureCollection(filteredSequences) : emptyLineCollection();
247
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
248
+ adapter.fetchSequences && showSequences ? /* @__PURE__ */ jsxs(Fragment, { children: [
249
+ /* @__PURE__ */ jsx(
250
+ Source,
251
+ {
252
+ id: sequenceSourceId(providerId),
253
+ type: "geojson",
254
+ data: sequenceCollection
255
+ },
256
+ sequenceSourceId(providerId)
257
+ ),
258
+ /* @__PURE__ */ jsx(
259
+ Layer,
260
+ {
261
+ id: sequenceLayerId(providerId),
262
+ type: "line",
263
+ source: sequenceSourceId(providerId),
264
+ paint: {
265
+ "line-color": adapter.color,
266
+ "line-width": 2,
267
+ "line-opacity": 0.35
268
+ }
269
+ }
270
+ )
271
+ ] }) : null,
272
+ showViewfields ? /* @__PURE__ */ jsxs(Fragment, { children: [
273
+ /* @__PURE__ */ jsx(
274
+ Source,
275
+ {
276
+ id: viewfieldSrcId,
277
+ type: "geojson",
278
+ data: viewfieldCollection
279
+ },
280
+ viewfieldSrcId
281
+ ),
282
+ /* @__PURE__ */ jsx(
283
+ Layer,
284
+ {
285
+ id: viewfieldLayerId(providerId),
286
+ type: "fill",
287
+ source: viewfieldSrcId,
288
+ paint: {
289
+ "fill-color": photoCircleColor,
290
+ "fill-opacity": 0.22
291
+ }
292
+ }
293
+ ),
294
+ /* @__PURE__ */ jsx(
295
+ Layer,
296
+ {
297
+ id: viewfieldLineLayerId(providerId),
298
+ type: "line",
299
+ source: viewfieldSrcId,
300
+ paint: {
301
+ "line-color": photoCircleColor,
302
+ "line-width": 1,
303
+ "line-opacity": 0.45
304
+ }
305
+ }
306
+ )
307
+ ] }) : null,
308
+ /* @__PURE__ */ jsx(
309
+ Source,
310
+ {
311
+ id: photoSrcId,
312
+ type: "geojson",
313
+ data: photoCollection,
314
+ promoteId: "photoId"
315
+ },
316
+ photoSrcId
317
+ ),
318
+ /* @__PURE__ */ jsx(
319
+ Layer,
320
+ {
321
+ id: photoLayerId(providerId),
322
+ type: "circle",
323
+ source: photoSrcId,
324
+ filter: photoFilter,
325
+ layout: { "circle-sort-key": PHOTO_SORT_KEY },
326
+ paint: {
327
+ "circle-radius": CIRCLE_RADIUS,
328
+ "circle-color": photoCircleColor,
329
+ "circle-stroke-width": 1,
330
+ "circle-stroke-color": "#ffffff"
331
+ }
332
+ }
333
+ )
334
+ ] });
335
+ };
336
+ var MapFeatureProviderLayer = ({
337
+ providerId,
338
+ bbox,
339
+ zoom,
340
+ filter,
341
+ mapFeatureCircleColor
342
+ }) => {
343
+ const adapter = adapterById[providerId];
344
+ const { data: features = [] } = useProviderMapFeatures(providerId, bbox, zoom);
345
+ const featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(features) : emptyPointCollection();
346
+ const featureFilter = buildMapFeatureLayerFilter(filter?.date);
347
+ const featureSrcId = featureSourceId(providerId);
348
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
349
+ /* @__PURE__ */ jsx(
350
+ Source,
351
+ {
352
+ id: featureSrcId,
353
+ type: "geojson",
354
+ data: featureCollection,
355
+ promoteId: "featureId"
356
+ },
357
+ featureSrcId
358
+ ),
359
+ /* @__PURE__ */ jsx(
360
+ Layer,
361
+ {
362
+ id: featureLayerId(providerId),
363
+ type: "circle",
364
+ source: featureSrcId,
365
+ filter: featureFilter,
366
+ layout: { "circle-sort-key": FEATURE_SORT_KEY },
367
+ paint: {
368
+ "circle-radius": FEATURE_CIRCLE_RADIUS,
369
+ "circle-color": mapFeatureCircleColor,
370
+ "circle-stroke-width": 1,
371
+ "circle-stroke-color": "#ffffff"
372
+ }
373
+ }
374
+ )
375
+ ] });
376
+ };
377
+ var ProviderLayer = (props) => {
378
+ const adapter = adapterById[props.providerId];
379
+ return adapter.kind === "mapFeature" ? /* @__PURE__ */ jsx(MapFeatureProviderLayer, { ...props }) : /* @__PURE__ */ jsx(PhotoProviderLayer, { ...props });
380
+ };
381
+ var StreetLevelImagerySourcesAndLayers = ({
382
+ providers,
383
+ filter,
384
+ bbox,
385
+ zoom,
386
+ options
387
+ }) => {
388
+ const {
389
+ config,
390
+ showSequences = true,
391
+ showViewfields = true,
392
+ showViewCone = false,
393
+ showSelectionHighlight = false,
394
+ selectedPhoto,
395
+ selectedSequenceId,
396
+ viewerPov,
397
+ photoCircleColor,
398
+ mapFeatureCircleColor
399
+ } = options;
400
+ useEffect(
401
+ function applyStreetImageryConfig() {
402
+ if (config) {
403
+ setStreetImageryConfig(config);
404
+ }
405
+ },
406
+ [config]
407
+ );
408
+ const selectedProviderId = selectedPhoto?.providerId ?? null;
409
+ const { data: sequences = [] } = useProviderSequences(
410
+ selectedProviderId ?? "mapillary",
411
+ showSelectionHighlight && selectedProviderId ? bbox : null,
412
+ zoom
413
+ );
414
+ const selectedSequence = resolveSelectedSequence(
415
+ selectedPhoto,
416
+ sequences,
417
+ selectedSequenceId ?? selectedPhoto?.sequenceId
418
+ );
419
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
420
+ providers.map((providerId) => /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(
421
+ ProviderLayer,
422
+ {
423
+ bbox,
424
+ filter,
425
+ mapFeatureCircleColor,
426
+ photoCircleColor,
427
+ providerId,
428
+ showSequences,
429
+ showViewfields,
430
+ zoom
431
+ }
432
+ ) }, providerId)),
433
+ showViewCone && selectedPhoto ? /* @__PURE__ */ jsx(
434
+ StreetLevelImageryViewCone,
435
+ {
436
+ selectedPhoto,
437
+ viewerPov,
438
+ zoom
439
+ }
440
+ ) : null,
441
+ showSelectionHighlight ? /* @__PURE__ */ jsx(
442
+ StreetLevelImagerySelectionOverlay,
443
+ {
444
+ selectedPhoto,
445
+ selectedSequence
446
+ }
447
+ ) : null
448
+ ] });
449
+ };
450
+ var MapillaryPanel2 = lazy(
451
+ () => import('./MapillaryPanel-QX6NESX7.js').then((module) => ({
452
+ default: module.MapillaryPanel
453
+ }))
454
+ );
455
+ var PanoramaxPanel2 = lazy(
456
+ () => import('./PanoramaxPanel-J6ANR6LB.js').then((module) => ({
457
+ default: module.PanoramaxPanel
458
+ }))
459
+ );
460
+ var ViewerPanelPlaceholder = () => /* @__PURE__ */ jsx("div", { className: "flex min-h-48 animate-pulse items-center justify-center rounded-lg border border-slate-200 bg-slate-100", children: /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: "Loading viewer\u2026" }) });
461
+ var StreetLevelImageryViewer = ({
462
+ photo,
463
+ groupPhotos,
464
+ onPhotoSelected,
465
+ onEaseMapToPoint
466
+ }) => {
467
+ if (photo.providerId === "mapillary") {
468
+ return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewerPanelPlaceholder, {}), children: /* @__PURE__ */ jsx(
469
+ MapillaryPanel2,
470
+ {
471
+ groupPhotos,
472
+ onEaseMapToPoint,
473
+ onPhotoSelected,
474
+ photo
475
+ }
476
+ ) });
477
+ }
478
+ if (photo.providerId === "panoramax") {
479
+ return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewerPanelPlaceholder, {}), children: /* @__PURE__ */ jsx(
480
+ PanoramaxPanel2,
481
+ {
482
+ groupPhotos,
483
+ onEaseMapToPoint,
484
+ onPhotoSelected,
485
+ photo
486
+ }
487
+ ) });
488
+ }
489
+ return null;
490
+ };
491
+ var streetImageryInteractiveLayerIds = (providers) => [
492
+ ...providers.map((providerId) => photoLayerId(providerId)),
493
+ ...providers.map((providerId) => viewfieldLayerId(providerId)),
494
+ ...providers.map((providerId) => featureLayerId(providerId))
495
+ ];
496
+ var parseProviderIdFromLayerId = (layerId) => {
497
+ const photoMatch = layerId.match(/^photos-(.+)$/);
498
+ if (photoMatch) {
499
+ return photoMatch[1];
500
+ }
501
+ const viewfieldMatch = layerId.match(/^viewfields-(.+)$/);
502
+ if (viewfieldMatch) {
503
+ return viewfieldMatch[1];
504
+ }
505
+ const featureMatch = layerId.match(/^features-(.+)$/);
506
+ if (featureMatch) {
507
+ return featureMatch[1];
508
+ }
509
+ return null;
510
+ };
511
+ var queryStreetImageryFeatures = (event) => {
512
+ const features = event.features ?? [];
513
+ const results = [];
514
+ const seenPhotoKeys = /* @__PURE__ */ new Set();
515
+ for (const feature of features) {
516
+ const layerId = feature.layer?.id;
517
+ if (!layerId) {
518
+ continue;
519
+ }
520
+ const providerId = parseProviderIdFromLayerId(layerId);
521
+ if (!providerId) {
522
+ continue;
523
+ }
524
+ const props = feature.properties ?? {};
525
+ if (layerId.startsWith("photos-") || layerId.startsWith("viewfields-")) {
526
+ const photoId = props.photoId;
527
+ if (photoId == null) {
528
+ continue;
529
+ }
530
+ const photoIdStr = String(photoId);
531
+ const dedupeKey = `${providerId}:${photoIdStr}`;
532
+ if (seenPhotoKeys.has(dedupeKey)) {
533
+ continue;
534
+ }
535
+ seenPhotoKeys.add(dedupeKey);
536
+ results.push({
537
+ providerId,
538
+ kind: "photo",
539
+ photoId: photoIdStr,
540
+ sequenceId: props.sequenceId != null ? String(props.sequenceId) : void 0
541
+ });
542
+ continue;
543
+ }
544
+ if (layerId.startsWith("features-")) {
545
+ const featureId = props.featureId;
546
+ if (featureId == null) {
547
+ continue;
548
+ }
549
+ results.push({
550
+ providerId,
551
+ kind: "mapFeature",
552
+ featureId: String(featureId)
553
+ });
554
+ }
555
+ }
556
+ return results;
557
+ };
558
+ var useProviderMapFeaturesMaybe = (providerId, enabled, bbox, zoom) => useProviderMapFeatures(providerId, enabled ? bbox : null, zoom);
559
+ var useAllProviderMapFeatures = (providerIds, bbox, zoom, date) => {
560
+ const enabled = new Set(providerIds);
561
+ const queries = PROVIDER_IDS.map((providerId) => ({
562
+ providerId,
563
+ query: useProviderMapFeaturesMaybe(providerId, enabled.has(providerId), bbox, zoom)
564
+ }));
565
+ return queries.flatMap(({ query }) => {
566
+ const data = query.data ?? [];
567
+ return data.filter((feature) => mapFeatureMatchesDateRange(feature, date));
568
+ });
569
+ };
570
+ var useAllProviderMapFeaturesLoading = (providerIds, bbox, zoom) => {
571
+ const enabled = new Set(providerIds);
572
+ const queries = PROVIDER_IDS.map((providerId) => ({
573
+ providerId,
574
+ query: useProviderMapFeaturesMaybe(providerId, enabled.has(providerId), bbox, zoom)
575
+ }));
576
+ const activeQueries = queries.filter(({ providerId }) => enabled.has(providerId));
577
+ return {
578
+ isLoading: activeQueries.some(({ query }) => query.isLoading),
579
+ isFetching: activeQueries.some(({ query }) => query.isFetching)
580
+ };
581
+ };
582
+ var useProviderPhotosMaybe = (providerId, enabled, bbox, zoom) => useProviderPhotos(providerId, enabled ? bbox : null, zoom);
583
+ var useAllProviderPhotos = (providerIds, bbox, zoom, photoTypes, date) => {
584
+ const enabled = new Set(providerIds);
585
+ const queries = PROVIDER_IDS.map((providerId) => ({
586
+ providerId,
587
+ query: useProviderPhotosMaybe(
588
+ providerId,
589
+ enabled.has(providerId) && isBrowserAvailableProvider(providerId),
590
+ bbox,
591
+ zoom
592
+ )
593
+ }));
594
+ const photos = queries.flatMap(({ query }) => {
595
+ const data = query.data ?? [];
596
+ return data.filter((photo) => photoMatchesFilters(photo, photoTypes, date));
597
+ });
598
+ const activeQueries = queries.filter(({ providerId }) => enabled.has(providerId));
599
+ const isLoading = activeQueries.some(({ query }) => query.isLoading);
600
+ const isFetching = activeQueries.some(({ query }) => query.isFetching);
601
+ return { photos, isLoading, isFetching };
602
+ };
603
+ var useMapViewportBbox = (mapId, viewportTrigger) => {
604
+ const { [mapId]: mapRef } = useMap();
605
+ const [mapLoaded, setMapLoaded] = useState(false);
606
+ useEffect(
607
+ function subscribeMapLoad() {
608
+ if (!mapRef) {
609
+ setMapLoaded(false);
610
+ return;
611
+ }
612
+ const map = mapRef.getMap();
613
+ if (map.loaded()) {
614
+ setMapLoaded(true);
615
+ return;
616
+ }
617
+ const onLoad = () => {
618
+ setMapLoaded(true);
619
+ };
620
+ void map.once("load", onLoad);
621
+ return () => {
622
+ map.off("load", onLoad);
623
+ };
624
+ },
625
+ [mapRef]
626
+ );
627
+ if (!mapRef || !mapLoaded) {
628
+ return null;
629
+ }
630
+ const bounds = mapRef.getBounds();
631
+ if (!bounds) {
632
+ return null;
633
+ }
634
+ return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
635
+ };
636
+ var usePhotoFullUrl = (photo) => useQuery({
637
+ queryKey: photo ? photoFullUrlQueryKey(photo) : ["photo-full-url", "none"],
638
+ queryFn: () => {
639
+ if (!photo) {
640
+ return null;
641
+ }
642
+ return resolvePhotoPanoramaUrl(photo);
643
+ },
644
+ enabled: photo != null,
645
+ staleTime: 24 * 60 * 60 * 1e3
646
+ });
647
+ var usePhotoThumbnail = (photo) => useQuery({
648
+ queryKey: photo ? photoThumbnailQueryKey(photo) : ["photo-thumbnail", "none"],
649
+ queryFn: () => {
650
+ if (!photo) {
651
+ return null;
652
+ }
653
+ return resolvePhotoThumbnailUrl(photo);
654
+ },
655
+ enabled: photo != null,
656
+ staleTime: 24 * 60 * 60 * 1e3
657
+ });
658
+
659
+ export { StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useAllProviderMapFeatures, useAllProviderMapFeaturesLoading, useAllProviderPhotos, useMapViewportBbox, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences };
@@ -0,0 +1,11 @@
1
+ import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
+ import 'mapillary-js/dist/mapillary.css';
3
+ import type { StreetImageryPhotoSelection } from '../types';
4
+ type MapillaryPanelProps = {
5
+ photo: NormalizedPhoto;
6
+ groupPhotos: NormalizedPhoto[];
7
+ onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
8
+ onEaseMapToPoint: (lng: number, lat: number) => void;
9
+ };
10
+ export declare const MapillaryPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, }: MapillaryPanelProps) => import("react").JSX.Element;
11
+ export {};
@@ -0,0 +1,11 @@
1
+ import '@panoramax/web-viewer';
2
+ import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
3
+ import type { StreetImageryPhotoSelection } from '../types';
4
+ type PanoramaxPanelProps = {
5
+ photo: NormalizedPhoto;
6
+ groupPhotos: NormalizedPhoto[];
7
+ onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
8
+ onEaseMapToPoint: (lng: number, lat: number) => void;
9
+ };
10
+ export declare const PanoramaxPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, }: PanoramaxPanelProps) => import("react").JSX.Element;
11
+ export {};
@@ -0,0 +1,13 @@
1
+ import { type ProviderId } from '@osm-editor-kit/street-imagery';
2
+ import type { MapGeoJSONFeature } from 'maplibre-gl';
3
+ export type StreetImageryClickFeature = {
4
+ providerId: ProviderId;
5
+ kind: 'photo' | 'mapFeature';
6
+ photoId?: string;
7
+ featureId?: string;
8
+ sequenceId?: string;
9
+ };
10
+ export declare const streetImageryInteractiveLayerIds: (providers: ProviderId[]) => string[];
11
+ export declare const queryStreetImageryFeatures: (event: {
12
+ features?: MapGeoJSONFeature[] | null;
13
+ }) => StreetImageryClickFeature[];
@@ -0,0 +1,6 @@
1
+ import type { ProviderId } from '@osm-editor-kit/street-imagery';
2
+ export type StreetImageryPhotoSelection = {
3
+ provider: ProviderId;
4
+ sequenceId: string;
5
+ photoId: string;
6
+ };
@@ -0,0 +1,13 @@
1
+ type ViewerPovUpdate = {
2
+ bearing?: number | null;
3
+ hfov?: number | null;
4
+ lngLat?: [number, number] | null;
5
+ };
6
+ export declare const useViewerBearing: () => number | null;
7
+ export declare const useViewerHfov: () => number | null;
8
+ export declare const useViewerLngLat: () => [number, number] | null;
9
+ export declare const useViewerActions: () => {
10
+ setPov: (partial: ViewerPovUpdate) => void;
11
+ reset: () => void;
12
+ };
13
+ export {};
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@osm-editor-kit/street-imagery-react",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "React MapLibre layers and photo viewers for street-level imagery providers.",
5
+ "license": "MIT",
6
+ "author": "Tobias Jordans",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/osmberlin/street-space-editor.git",
10
+ "directory": "packages/street-imagery-react"
11
+ },
12
+ "type": "module",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "tag": "alpha"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup && rm -f .tsbuildinfo && tsc -p tsconfig.build.json",
29
+ "type-check": "tsc --noEmit",
30
+ "test-run": "bun test --pass-with-no-tests src",
31
+ "prepublishOnly": "bun run build && bun ../../scripts/package-exports-for-publish.ts",
32
+ "postpublish": "bun ../../scripts/package-exports-restore.ts",
33
+ "prepack": "bun ../../scripts/package-exports-for-publish.ts",
34
+ "postpack": "bun ../../scripts/package-exports-restore.ts"
35
+ },
36
+ "devDependencies": {
37
+ "@panoramax/web-viewer": "^5",
38
+ "@tanstack/react-query": "^5.85.5",
39
+ "@types/geojson": "^7946.0.16",
40
+ "@types/react": "^19.1.10",
41
+ "@types/react-dom": "^19.1.7",
42
+ "bun-types": "^1.2.20",
43
+ "mapillary-js": "^4.1.2",
44
+ "maplibre-gl": "^5.7.1",
45
+ "react": "^19.1.1",
46
+ "react-dom": "^19.1.1",
47
+ "react-map-gl": "^8.0.4",
48
+ "tsup": "^8.5.1",
49
+ "typescript": "^7.0.0",
50
+ "zustand": "^5.0.7"
51
+ },
52
+ "peerDependencies": {
53
+ "@osm-editor-kit/street-imagery": "workspace:*",
54
+ "@panoramax/web-viewer": "^5",
55
+ "@tanstack/react-query": "^5",
56
+ "mapillary-js": "^4",
57
+ "maplibre-gl": "^5",
58
+ "react": "^19",
59
+ "react-dom": "^19",
60
+ "react-map-gl": "^8",
61
+ "zustand": "^5"
62
+ },
63
+ "publishExports": {
64
+ ".": {
65
+ "types": "./dist/index.d.ts",
66
+ "import": "./dist/index.js"
67
+ },
68
+ "./package.json": "./package.json"
69
+ }
70
+ }