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

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.
@@ -1,11 +1,23 @@
1
1
  import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
2
2
  import 'mapillary-js/dist/mapillary.css';
3
3
  import type { StreetImageryPhotoSelection } from '../types';
4
+ import { type MapillaryLookAt, type MapillaryViewerOutline } from './mapillaryLookAt';
4
5
  type MapillaryPanelProps = {
5
6
  photo: NormalizedPhoto;
6
7
  groupPhotos: NormalizedPhoto[];
7
8
  onPhotoSelected: (selection: StreetImageryPhotoSelection) => void;
8
9
  onEaseMapToPoint: (lng: number, lat: number) => void;
10
+ /** Full photo data for each image the viewer shows (incl. native prev/next navigation). */
11
+ onViewerPhoto?: (photo: NormalizedPhoto) => void;
12
+ /** 360° photos: turn the view to this map bearing when `photo` opens (e.g. a suggested view). */
13
+ lookAtBearing?: number | null;
14
+ /**
15
+ * Turn (and zoom) the view to a place when `photo` opens: a sign, a traffic light, a junction.
16
+ * Works for 360° and flat photos; wins over `lookAtBearing`.
17
+ */
18
+ lookAt?: MapillaryLookAt | null;
19
+ /** Outlines to draw in the shown image, e.g. the detections you care about. */
20
+ outlines?: MapillaryViewerOutline[];
9
21
  };
10
- export declare const MapillaryPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, }: MapillaryPanelProps) => import("react").JSX.Element;
22
+ export declare const MapillaryPanel: ({ photo, onPhotoSelected, onEaseMapToPoint, onViewerPhoto, lookAtBearing, lookAt, outlines, }: MapillaryPanelProps) => import("react").JSX.Element;
11
23
  export {};
@@ -0,0 +1,41 @@
1
+ import { type LngLat, type TargetShape, type ViewTarget } from '@osm-editor-kit/street-imagery';
2
+ import { type Image, type Viewer } from 'mapillary-js';
3
+ /** A place the Mapillary viewer should turn to: a sign, a traffic light, a junction … */
4
+ export type MapillaryLookAt = {
5
+ lngLat: LngLat;
6
+ /** Size and height of the target; decides zoom and vertical position. Default: a traffic sign. */
7
+ shape?: TargetShape;
8
+ /** The target's outline in the shown image, if known; it is more exact than the location. */
9
+ outline?: [number, number][];
10
+ /**
11
+ * Mapillary value of the target (`regulatory--bicycles-only--g1`). Without an `outline`, the
12
+ * image's own detection of this value is used: Mapillary links a map feature to few of its
13
+ * images, but almost every image has a detection of it. With several, the one nearest to the
14
+ * computed position wins (the computed position alone can be far off in 360° images).
15
+ */
16
+ value?: string;
17
+ /** Label for the outline drawn around the target. */
18
+ label?: string;
19
+ };
20
+ export type MapillaryViewerOutline = {
21
+ id: string;
22
+ /** Image ("basic") coordinates, `[0, 0]` top left to `[1, 1]` bottom right. */
23
+ outline: [number, number][];
24
+ label?: string;
25
+ /** CSS color number, e.g. `0xffff00`. Default yellow. */
26
+ color?: number;
27
+ };
28
+ /**
29
+ * Where the target is in the image: from its outline, else computed from the locations
30
+ * (360°: the viewer's own projection; flat: a pinhole model). `undefined` when a flat image does
31
+ * not show the target.
32
+ */
33
+ export declare const mapillaryViewFor: (viewer: Viewer, image: Image, { lngLat, shape, outline }: MapillaryLookAt) => Promise<ViewTarget | undefined>;
34
+ /**
35
+ * Turns the viewer to the target. Returns the outline it aimed at (given, or the image's own
36
+ * detection of `value`), or `null` when it aimed by location; `false` when the shown (flat) image
37
+ * does not show the target.
38
+ */
39
+ export declare const turnMapillaryViewerTo: (viewer: Viewer, image: Image, lookAt: MapillaryLookAt) => Promise<MapillaryViewerOutline | null | false>;
40
+ /** Replaces the outlines drawn in the viewer (needs the viewer's `tag` component). */
41
+ export declare const setMapillaryViewerOutlines: (viewer: Viewer, outlines: readonly MapillaryViewerOutline[]) => void;
@@ -0,0 +1,31 @@
1
+ import type { ViewSuggestion } from '@osm-editor-kit/street-imagery';
2
+ import { type ReactNode } from 'react';
3
+ export type FloatingPhotoViewerProps = {
4
+ title: ReactNode;
5
+ /** Suggested views (viewpoint × direction); shown as chips. Empty hides the strip. */
6
+ suggestions: ViewSuggestion[];
7
+ activeDirectionKey: string | null;
8
+ onSelectSuggestion: (suggestion: ViewSuggestion) => void;
9
+ canGoBack: boolean;
10
+ canGoForward: boolean;
11
+ onBack: () => void;
12
+ onForward: () => void;
13
+ onClose: () => void;
14
+ /** Short status line, e.g. "Loading…" or "No photos from the last 2 years". */
15
+ status?: ReactNode;
16
+ /** Viewer body (provider panel). */
17
+ children?: ReactNode;
18
+ footer?: ReactNode;
19
+ /** Collapsible list below the footer, e.g. other photos near the click. */
20
+ drawer?: {
21
+ label: ReactNode;
22
+ content: ReactNode;
23
+ };
24
+ /** localStorage key for position, width and minimized state. */
25
+ storageKey?: string;
26
+ };
27
+ /**
28
+ * Photo viewer box floating over the map. Render inside a positioned (relative) map container.
29
+ * Drag the header to move, drag the left edge to resize. Keys: Esc closes, [ and ] step history.
30
+ */
31
+ export declare const FloatingPhotoViewer: ({ title, suggestions, activeDirectionKey, onSelectSuggestion, canGoBack, canGoForward, onBack, onForward, onClose, status, children, footer, drawer, storageKey, }: FloatingPhotoViewerProps) => import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { type LngLat, type ViewSuggestion, type Viewpoint } from '@osm-editor-kit/street-imagery';
2
+ import type { MapGeoJSONFeature } from 'maplibre-gl';
3
+ /** Add to `interactiveLayerIds` to make view directions clickable; see `viewDirectionKeyFromFeatures`. */
4
+ export declare const VIEWPOINT_DIRECTION_LAYER_ID = "viewpoint-direction-fill";
5
+ export declare const VIEWPOINT_DEFAULT_COLOR = "#c026d3";
6
+ export type ViewpointLayerProps = {
7
+ viewpoints: Viewpoint[];
8
+ suggestions: ViewSuggestion[];
9
+ activeDirectionKey: string | null;
10
+ zoom: number;
11
+ /** Clicked line, drawn below the viewpoints. */
12
+ line?: LngLat[] | null;
13
+ color?: string;
14
+ };
15
+ export declare const ViewpointLayer: ({ viewpoints, suggestions, activeDirectionKey, zoom, line, color, }: ViewpointLayerProps) => import("react").JSX.Element;
16
+ /** Direction key of the top-most clicked view direction, if any. */
17
+ export declare const viewDirectionKeyFromFeatures: (features: MapGeoJSONFeature[] | undefined) => string | null;
@@ -0,0 +1,23 @@
1
+ import { type NormalizedPhoto, type PhotoTypeFilter, type ViewSuggestion, type ViewpointPhotoSource, type Viewpoint } from '@osm-editor-kit/street-imagery';
2
+ export type ViewSuggestionsOptions = {
3
+ /** Only photos from the last N years. */
4
+ maxAgeYears?: number;
5
+ photoTypes?: PhotoTypeFilter[];
6
+ /** Candidates kept per direction. */
7
+ limit?: number;
8
+ /** Extra app filter (e.g. date range), applied before ranking. */
9
+ filterPhoto?: (photo: NormalizedPhoto) => boolean;
10
+ /**
11
+ * Where photos come from; default Mapillary. Pass several to rank across providers, e.g.
12
+ * `[mapillaryPhotoSource, streetViewPhotoSource]` or your own (Infra3D).
13
+ */
14
+ sources?: ViewpointPhotoSource[];
15
+ enabled?: boolean;
16
+ };
17
+ export type ViewSuggestionsResult = {
18
+ suggestions: ViewSuggestion[];
19
+ isLoading: boolean;
20
+ isError: boolean;
21
+ };
22
+ /** Photos near each viewpoint from all `sources`, ranked per view direction. */
23
+ export declare const useViewSuggestions: (viewpoints: Viewpoint[], { maxAgeYears, photoTypes, limit, filterPhoto, sources, enabled, }?: ViewSuggestionsOptions) => ViewSuggestionsResult;
@@ -0,0 +1,39 @@
1
+ import type { LngLat, NormalizedPhoto, Viewpoint } from '@osm-editor-kit/street-imagery';
2
+ export type ViewpointHistoryEntry = {
3
+ photo: NormalizedPhoto;
4
+ /** Suggested view the photo came from; `null` for photos picked by hand. */
5
+ directionKey: string | null;
6
+ };
7
+ type OpenInput = {
8
+ viewpoints: Viewpoint[];
9
+ /** Clicked line, drawn as context on the map. */
10
+ line?: LngLat[] | null;
11
+ };
12
+ export declare const useViewpointSessionId: () => number;
13
+ export declare const useViewpoints: () => Viewpoint[];
14
+ export declare const useViewpointLine: () => LngLat[] | null;
15
+ export declare const useActiveDirectionKey: () => string | null;
16
+ export declare const useCurrentHistoryEntry: () => ViewpointHistoryEntry;
17
+ export declare const useCanGoBack: () => boolean;
18
+ export declare const useCanGoForward: () => boolean;
19
+ export declare const useViewpointSessionActions: () => {
20
+ open: (input: OpenInput) => void;
21
+ /** Show a photo; truncates forward history. No-op when it is already the current photo. */
22
+ showPhoto: (entry: ViewpointHistoryEntry) => void;
23
+ back: () => ViewpointHistoryEntry | null;
24
+ forward: () => ViewpointHistoryEntry | null;
25
+ close: () => void;
26
+ };
27
+ /** Actions + current entry outside React (event handlers, tests). */
28
+ export declare const getViewpointSession: () => {
29
+ actions: {
30
+ open: (input: OpenInput) => void;
31
+ /** Show a photo; truncates forward history. No-op when it is already the current photo. */
32
+ showPhoto: (entry: ViewpointHistoryEntry) => void;
33
+ back: () => ViewpointHistoryEntry | null;
34
+ forward: () => ViewpointHistoryEntry | null;
35
+ close: () => void;
36
+ };
37
+ current: ViewpointHistoryEntry;
38
+ };
39
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { ViewSuggestion, ViewpointRole } from '@osm-editor-kit/street-imagery';
2
+ export declare const viewpointRoleLabel: (role: ViewpointRole) => string;
3
+ /** Short human label, e.g. "Start → forward", "Here · N", "Junction · in". */
4
+ export declare const viewSuggestionLabel: ({ viewpoint, direction }: ViewSuggestion) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osm-editor-kit/street-imagery-react",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.5",
4
4
  "description": "React MapLibre layers and photo viewers for street-level imagery providers.",
5
5
  "homepage": "https://github.com/osmberlin/street-space-editor/tree/main/packages/street-imagery-react",
6
6
  "license": "MIT",
@@ -45,13 +45,13 @@
45
45
  "maplibre-gl": "^6.7.0",
46
46
  "react": "^19.1.1",
47
47
  "react-dom": "^19.1.1",
48
- "react-map-gl": "^8.0.4",
48
+ "react-map-gl": "^8.1.3",
49
49
  "tsup": "^8.5.1",
50
50
  "typescript": "^7.0.0",
51
51
  "zustand": "^5.0.7"
52
52
  },
53
53
  "peerDependencies": {
54
- "@osm-editor-kit/street-imagery": "^0.1.0-alpha.2",
54
+ "@osm-editor-kit/street-imagery": "^0.1.0-alpha.4",
55
55
  "@panoramax/web-viewer": "^5",
56
56
  "@tanstack/react-query": "^5",
57
57
  "mapillary-js": "^4",
@@ -1,2 +0,0 @@
1
- export { MapillaryPanel } from './chunk-BS6LPDMY.js';
2
- import './chunk-25UGGXIE.js';
@@ -1,143 +0,0 @@
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 };