@osm-editor-kit/street-imagery-react 0.1.0-alpha.13 → 0.1.0-alpha.15

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tobias Jordans / OSM Editor Kit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -75,7 +75,7 @@ import { PanoramaxWebViewerPanel } from '@osm-editor-kit/street-imagery-react/pa
75
75
  <StreetLevelImageryViewer panoramaxPanel={PanoramaxWebViewerPanel} … />
76
76
  ```
77
77
 
78
- Only then you need `@panoramax/web-viewer` (an optional peer dependency) and its Vite setup: see `vite.config.ts` of [street-level-imagery-provider-overview](https://github.com/osmberlin/street-level-imagery-provider-overview).
78
+ Only then you need `@panoramax/web-viewer` (an optional peer dependency) and its Vite setup: see [`vite.config.ts` of the preview](https://github.com/osm-editor-kit/street-imagery/blob/main/apps/preview/vite.config.ts).
79
79
 
80
80
  ## Viewpoints and floating viewer
81
81
 
@@ -106,6 +106,58 @@ const { suggestions } = useViewSuggestions(viewpoints, { maxAgeYears: 2 })
106
106
  Render `FloatingPhotoViewer` inside a positioned map container. Photos from suggestions open in
107
107
  `StreetLevelImageryViewer` with `lookAtBearing={suggestion.direction.bearing}`.
108
108
 
109
+ A host without suggested views leaves out `suggestions`, `activeDirectionKey` and `onSelectSuggestion`; the chips are then not shown. Its own buttons and links go in `titleActions`. They keep their size next to a long title and do not start a drag.
110
+
111
+ For a free window over the whole page, use the viewport as container and let the box leave it until a share of it shows:
112
+
113
+ ```tsx
114
+ <FloatingPhotoViewer position="fixed" minVisibleShare={0.2} onClose={close} title="Photo">
115
+ {viewer}
116
+ </FloatingPhotoViewer>
117
+ ```
118
+
119
+ `position="fixed"` (default `absolute`) needs an ancestor without `transform` or `filter`, and a stacking order that puts the box above the page. `minVisibleShare` (0–1) never lets the box go over the top edge, because the header is what moves it. The limit also holds when the box or the container changes size, and a minimized viewer stays within reach. Without `minVisibleShare` the box keeps 8px from the right and bottom edge.
120
+
121
+ To ask "who sees this spot" instead of "what looks north here", use `usePhotosLookingAt`: flat photos within `maxDistanceMeters` (50) whose heading points at the place within `maxFlatAngleDeg` (35), and 360° photos within the distance, best first. Each candidate has `bearingToPlace`.
122
+
123
+ ```tsx
124
+ const [place, setPlace] = useState<LngLat | null>(null) // set on map click
125
+ const [index, setIndex] = useState(0)
126
+ const { candidates } = usePhotosLookingAt(place, { maxAgeYears: 2, enabled: place != null })
127
+ const candidate = candidates[index]
128
+
129
+ {candidate ? (
130
+ <FloatingPhotoViewer
131
+ title="Photos of this place"
132
+ onClose={() => setPlace(null)}
133
+ step={{
134
+ onPrevious: index > 0 ? () => setIndex(index - 1) : undefined,
135
+ onNext: index < candidates.length - 1 ? () => setIndex(index + 1) : undefined,
136
+ previousLabel: 'Previous photo',
137
+ nextLabel: 'Next photo',
138
+ }}
139
+ >
140
+ <StreetLevelImageryViewer
141
+ photo={candidate.photo}
142
+ lookAtBearing={candidate.bearingToPlace}
143
+ … />
144
+ </FloatingPhotoViewer>
145
+ ) : null}
146
+ ```
147
+
148
+ The list needs no chips: `step` gives the `<` `>` buttons and Alt + arrow keys. `lookAtBearing` turns Mapillary 360° photos only. The pure ranking is `rankPhotosLookingAt(place, photos, options)` in the core package.
149
+
150
+ For a link that opens the same photo with the same zoom and pan, keep the view the viewer reports and pass it to `providerExternalLink`:
151
+
152
+ ```tsx
153
+ const [view, setView] = useState<PhotoView | null>(null)
154
+
155
+ <StreetLevelImageryViewer onViewerView={setView} … />
156
+ <a href={providerExternalLink(photo, view)}>Open on Mapillary</a>
157
+ ```
158
+
159
+ `onViewerView` runs when the view has stopped moving and when the photo changes, not during a drag, for Mapillary (`x`, `y`, `zoom`) and Panoramax (`heading`, `pitch`, `zoom`). A view of another photo is ignored by the link. What a link shows also depends on the size of the window it opens in. Panoramax in the default panel: the viewers differ, so direction and height of the view carry over, not every pixel.
160
+
109
161
  **Tailwind:** components use Tailwind classes. Tailwind does not scan `node_modules`, so add to your CSS:
110
162
 
111
163
  ```css
@@ -181,7 +233,7 @@ const { photos } = useAllProviderPhotos(providers, bbox, zoom) // no date: all p
181
233
  />
182
234
  ```
183
235
 
184
- A handle at its end means no limit, so the left end also shows photos older than the slider spans (`maxYears`, default 10). The scale itself is in the core package (`dateSliderPosition`, `dateSliderBins`, …) for a host that draws its own chart. The texts are in `messages.dateFilter`.
236
+ A click or tap on the chart moves the nearer handle to that exact place, and a press on the chart can go on as a drag. Handles on the same spot come apart in the direction of the drag. Handles stay separate targets for mouse and touch, at least 44px high and wide, and a vertical swipe on a phone scrolls the page. A handle at its end means no limit, so the left end also shows photos older than the slider spans (`maxYears`, default 10). The scale itself is in the core package (`dateSliderPosition`, `dateSliderBins`, …) for a host that draws its own chart. The texts are in `messages.dateFilter`.
185
237
 
186
238
  ## Pick a place on the map to open elsewhere
187
239
 
@@ -0,0 +1,3 @@
1
+ export { MapillaryPanel } from './chunk-LZR5SZDS.js';
2
+ import './chunk-XLBFXACK.js';
3
+ import './chunk-GYUS2JVV.js';
@@ -0,0 +1,3 @@
1
+ export { PanoramaxPanel } from './chunk-R6BDVYWM.js';
2
+ import './chunk-XLBFXACK.js';
3
+ import './chunk-GYUS2JVV.js';
@@ -1,4 +1,4 @@
1
- import type { NormalizedPhoto } from '@osm-editor-kit/street-imagery';
1
+ import type { NormalizedPhoto, PhotoView } from '@osm-editor-kit/street-imagery';
2
2
  import { type ComponentType } from 'react';
3
3
  import type { MapillaryLookAt, MapillaryViewerOutline } from './panels/mapillaryLookAt';
4
4
  import type { PanoramaxPanelProps } from './panels/PanoramaxPanel';
@@ -10,6 +10,12 @@ type StreetLevelImageryViewerProps = {
10
10
  onEaseMapToPoint: (lng: number, lat: number) => void;
11
11
  /** Mapillary, Panoramax: full photo data (creator, licence …) for each image the viewer shows. */
12
12
  onViewerPhoto?: (photo: NormalizedPhoto) => void;
13
+ /**
14
+ * Mapillary, Panoramax: the view inside the shown photo (pan and zoom), after it has stopped
15
+ * moving and when the photo changes. Pass it to `providerExternalLink` for a link that opens the
16
+ * same view on the provider's site.
17
+ */
18
+ onViewerView?: (view: PhotoView) => void;
13
19
  /** Mapillary 360° photos: initial view bearing (e.g. a suggested view direction). */
14
20
  lookAtBearing?: number | null;
15
21
  /** Mapillary: turn and zoom to a place (sign, traffic light, junction); wins over `lookAtBearing`. */
@@ -28,5 +34,5 @@ type StreetLevelImageryViewerProps = {
28
34
  */
29
35
  panoramaxPanel?: ComponentType<PanoramaxPanelProps>;
30
36
  };
31
- export declare const StreetLevelImageryViewer: ({ photo, groupPhotos, onPhotoSelected, onEaseMapToPoint, onViewerPhoto, lookAtBearing, lookAt, outlines, hideAttribution, panoramaxPanel: PanoramaxPanelOverride, }: StreetLevelImageryViewerProps) => import("react").JSX.Element | null;
37
+ export declare const StreetLevelImageryViewer: ({ photo, groupPhotos, onPhotoSelected, onEaseMapToPoint, onViewerPhoto, onViewerView, lookAtBearing, lookAt, outlines, hideAttribution, panoramaxPanel: PanoramaxPanelOverride, }: StreetLevelImageryViewerProps) => import("react").JSX.Element | null;
32
38
  export {};
@@ -1,3 +1,4 @@
1
+ import { watchMapillaryJsView } from './chunk-XLBFXACK.js';
1
2
  import { useViewerActions } from './chunk-GYUS2JVV.js';
2
3
  import { c } from 'react/compiler-runtime';
3
4
  import { viewFromOutline, SIGN_TARGET, viewFromFlatCamera, cameraPitch, panoX, bearingDeg, viewFromLocation, getStreetImageryConfig, fetchImageDetections } from '@osm-editor-kit/street-imagery';
@@ -112,12 +113,13 @@ var setMapillaryViewerOutlines = (viewer, outlines) => {
112
113
  };
113
114
  var panoCenterX = (bearing, compassAngle) => ((0.5 + (bearing - compassAngle) / 360) % 1 + 1) % 1;
114
115
  var MapillaryPanel = (t0) => {
115
- const $ = c(41);
116
+ const $ = c(49);
116
117
  const {
117
118
  photo,
118
119
  onPhotoSelected,
119
120
  onEaseMapToPoint,
120
121
  onViewerPhoto,
122
+ onViewerView,
121
123
  lookAtBearing,
122
124
  lookAt,
123
125
  outlines,
@@ -340,99 +342,147 @@ var MapillaryPanel = (t0) => {
340
342
  t8 = $[22];
341
343
  }
342
344
  useEffect(t7, t8);
343
- let t10;
344
345
  let t9;
345
- if ($[23] !== actions || $[24] !== photo.photoId) {
346
- t9 = function syncExternalPhotoSelection() {
346
+ if ($[23] !== onViewerView) {
347
+ t9 = (view) => onViewerView?.(view);
348
+ $[23] = onViewerView;
349
+ $[24] = t9;
350
+ } else {
351
+ t9 = $[24];
352
+ }
353
+ const emitViewerView = useEffectEvent(t9);
354
+ const reportsView = onViewerView !== void 0;
355
+ let t10;
356
+ if ($[25] !== emitViewerView || $[26] !== reportsView) {
357
+ t10 = function reportViewInPhoto() {
347
358
  const viewer_2 = viewerRef.current;
348
- if (!viewer_2 || photo.photoId === lastViewerPhotoIdRef.current) {
359
+ if (!viewer_2 || !reportsView) {
360
+ return;
361
+ }
362
+ const read = async () => {
363
+ const [image_1, t112, zoom] = await Promise.all([viewer_2.getImage(), viewer_2.getCenter(), viewer_2.getZoom()]);
364
+ const [t122, t132] = t112;
365
+ const x = t122 === void 0 ? 0.5 : t122;
366
+ const y = t132 === void 0 ? 0.5 : t132;
367
+ return {
368
+ providerId: "mapillary",
369
+ photoId: image_1.id,
370
+ x,
371
+ y,
372
+ zoom
373
+ };
374
+ };
375
+ return watchMapillaryJsView(viewer_2, read, (view_0) => emitViewerView(view_0));
376
+ };
377
+ $[25] = emitViewerView;
378
+ $[26] = reportsView;
379
+ $[27] = t10;
380
+ } else {
381
+ t10 = $[27];
382
+ }
383
+ let t11;
384
+ if ($[28] !== actions || $[29] !== reportsView) {
385
+ t11 = [actions, reportsView];
386
+ $[28] = actions;
387
+ $[29] = reportsView;
388
+ $[30] = t11;
389
+ } else {
390
+ t11 = $[30];
391
+ }
392
+ useEffect(t10, t11);
393
+ let t12;
394
+ let t13;
395
+ if ($[31] !== actions || $[32] !== photo.photoId) {
396
+ t12 = function syncExternalPhotoSelection() {
397
+ const viewer_3 = viewerRef.current;
398
+ if (!viewer_3 || photo.photoId === lastViewerPhotoIdRef.current) {
349
399
  return;
350
400
  }
351
401
  actions.reset();
352
402
  lastViewerPhotoIdRef.current = photo.photoId;
353
403
  if (navigableRef.current) {
354
- viewer_2.moveTo(photo.photoId).catch(_temp4);
404
+ viewer_3.moveTo(photo.photoId).catch(_temp4);
355
405
  } else {
356
406
  pendingImageIdRef.current = photo.photoId;
357
407
  }
358
408
  };
359
- t10 = [actions, photo.photoId];
360
- $[23] = actions;
361
- $[24] = photo.photoId;
362
- $[25] = t10;
363
- $[26] = t9;
409
+ t13 = [actions, photo.photoId];
410
+ $[31] = actions;
411
+ $[32] = photo.photoId;
412
+ $[33] = t12;
413
+ $[34] = t13;
364
414
  } else {
365
- t10 = $[25];
366
- t9 = $[26];
415
+ t12 = $[33];
416
+ t13 = $[34];
367
417
  }
368
- useEffect(t9, t10);
369
- let t11;
370
- if ($[27] !== lookAt || $[28] !== lookAtBearing) {
371
- t11 = lookAt ? `${lookAt.lngLat.join(",")}|${lookAt.outline?.length ?? 0}|${lookAt.value ?? ""}` : String(lookAtBearing);
372
- $[27] = lookAt;
373
- $[28] = lookAtBearing;
374
- $[29] = t11;
418
+ useEffect(t12, t13);
419
+ let t14;
420
+ if ($[35] !== lookAt || $[36] !== lookAtBearing) {
421
+ t14 = lookAt ? `${lookAt.lngLat.join(",")}|${lookAt.outline?.length ?? 0}|${lookAt.value ?? ""}` : String(lookAtBearing);
422
+ $[35] = lookAt;
423
+ $[36] = lookAtBearing;
424
+ $[37] = t14;
375
425
  } else {
376
- t11 = $[29];
426
+ t14 = $[37];
377
427
  }
378
- const lookAtKey = t11;
379
- let t12;
380
- if ($[30] !== photo.photoId || $[31] !== turnToWantedBearing) {
381
- t12 = function turnToTargetOnChange() {
382
- const viewer_3 = viewerRef.current;
383
- if (!viewer_3 || lastViewerPhotoIdRef.current !== photo.photoId) {
428
+ const lookAtKey = t14;
429
+ let t15;
430
+ if ($[38] !== photo.photoId || $[39] !== turnToWantedBearing) {
431
+ t15 = function turnToTargetOnChange() {
432
+ const viewer_4 = viewerRef.current;
433
+ if (!viewer_4 || lastViewerPhotoIdRef.current !== photo.photoId) {
384
434
  return;
385
435
  }
386
- viewer_3.getImage().then((image_1) => turnToWantedBearing(viewer_3, image_1)).catch(_temp5);
436
+ viewer_4.getImage().then((image_2) => turnToWantedBearing(viewer_4, image_2)).catch(_temp5);
387
437
  };
388
- $[30] = photo.photoId;
389
- $[31] = turnToWantedBearing;
390
- $[32] = t12;
438
+ $[38] = photo.photoId;
439
+ $[39] = turnToWantedBearing;
440
+ $[40] = t15;
391
441
  } else {
392
- t12 = $[32];
442
+ t15 = $[40];
393
443
  }
394
- let t13;
395
- if ($[33] !== lookAtKey || $[34] !== photo.photoId) {
396
- t13 = [lookAtKey, photo.photoId];
397
- $[33] = lookAtKey;
398
- $[34] = photo.photoId;
399
- $[35] = t13;
444
+ let t16;
445
+ if ($[41] !== lookAtKey || $[42] !== photo.photoId) {
446
+ t16 = [lookAtKey, photo.photoId];
447
+ $[41] = lookAtKey;
448
+ $[42] = photo.photoId;
449
+ $[43] = t16;
400
450
  } else {
401
- t13 = $[35];
451
+ t16 = $[43];
402
452
  }
403
- useEffect(t12, t13);
404
- let t14;
405
- if ($[36] !== drawOutlines) {
406
- t14 = function redrawOutlinesOnChange() {
407
- const viewer_4 = viewerRef.current;
408
- if (viewer_4 && navigableRef.current) {
409
- drawOutlines(viewer_4);
453
+ useEffect(t15, t16);
454
+ let t17;
455
+ if ($[44] !== drawOutlines) {
456
+ t17 = function redrawOutlinesOnChange() {
457
+ const viewer_5 = viewerRef.current;
458
+ if (viewer_5 && navigableRef.current) {
459
+ drawOutlines(viewer_5);
410
460
  }
411
461
  };
412
- $[36] = drawOutlines;
413
- $[37] = t14;
462
+ $[44] = drawOutlines;
463
+ $[45] = t17;
414
464
  } else {
415
- t14 = $[37];
465
+ t17 = $[45];
416
466
  }
417
- let t15;
418
- if ($[38] !== outlines) {
419
- t15 = [outlines];
420
- $[38] = outlines;
421
- $[39] = t15;
467
+ let t18;
468
+ if ($[46] !== outlines) {
469
+ t18 = [outlines];
470
+ $[46] = outlines;
471
+ $[47] = t18;
422
472
  } else {
423
- t15 = $[39];
473
+ t18 = $[47];
424
474
  }
425
- useEffect(t14, t15);
426
- let t16;
427
- if ($[40] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
428
- t16 = /* @__PURE__ */ jsx("div", { ref: containerRef, className: "min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900", style: {
475
+ useEffect(t17, t18);
476
+ let t19;
477
+ if ($[48] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
478
+ t19 = /* @__PURE__ */ jsx("div", { ref: containerRef, className: "min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900", style: {
429
479
  aspectRatio: "4 / 3"
430
480
  } });
431
- $[40] = t16;
481
+ $[48] = t19;
432
482
  } else {
433
- t16 = $[40];
483
+ t19 = $[48];
434
484
  }
435
- return t16;
485
+ return t19;
436
486
  };
437
487
  function _temp() {
438
488
  }
@@ -1,3 +1,4 @@
1
+ import { watchMapillaryJsView, panoramaxZoomLevel } from './chunk-XLBFXACK.js';
1
2
  import { useViewerActions } from './chunk-GYUS2JVV.js';
2
3
  import { c } from 'react/compiler-runtime';
3
4
  import { providerExternalLink, getStreetImageryConfig } from '@osm-editor-kit/street-imagery';
@@ -14,6 +15,7 @@ var toMs = (iso) => {
14
15
  };
15
16
  var orientation = (item) => item.properties?.["pers:interior_orientation"];
16
17
  var isPanoramaxItemSpherical = (item) => orientation(item)?.field_of_view === 360;
18
+ var panoramaxItemFieldOfView = (item) => orientation(item)?.field_of_view ?? DEFAULT_FIELD_OF_VIEW_DEG;
17
19
  var panoramaxItemSize = (item) => orientation(item)?.sensor_array_dimensions ?? (isPanoramaxItemSpherical(item) ? [2, 1] : [4, 3]);
18
20
  var panoramaxItemNeighbour = (item, rel) => item.links?.find((link) => link.rel === rel)?.id ?? null;
19
21
  var SD_WIDTH = 2048;
@@ -307,7 +309,7 @@ var PanoramaxDataProvider = class extends DataProviderBase {
307
309
  this.itemRequests.set(id, request);
308
310
  }
309
311
  }
310
- await Promise.all(imageIds.map((id) => this.itemRequests.get(id)));
312
+ await Promise.all(imageIds.flatMap((id) => this.itemRequests.get(id) ?? []));
311
313
  return imageIds.flatMap((id) => this.items.get(id) ?? []);
312
314
  }
313
315
  toImageEnt(item) {
@@ -354,17 +356,19 @@ var PanoramaxDataProvider = class extends DataProviderBase {
354
356
  }
355
357
  };
356
358
  var PanoramaxPanel = (t0) => {
357
- const $ = c(25);
359
+ const $ = c(33);
358
360
  const {
359
361
  photo,
360
362
  onPhotoSelected,
361
363
  onEaseMapToPoint,
362
364
  onViewerPhoto,
365
+ onViewerView,
363
366
  hideAttribution: t1
364
367
  } = t0;
365
368
  const hideAttribution = t1 === void 0 ? false : t1;
366
369
  const containerRef = useRef(null);
367
370
  const viewerRef = useRef(null);
371
+ const dataProviderRef = useRef(null);
368
372
  const navigableRef = useRef(false);
369
373
  const pendingImageIdRef = useRef(null);
370
374
  const lastViewerPhotoIdRef = useRef(null);
@@ -416,6 +420,7 @@ var PanoramaxPanel = (t0) => {
416
420
  const dataProvider = new PanoramaxDataProvider({
417
421
  endpoint: `${getStreetImageryConfig().panoramaxApiBase}/api`
418
422
  });
423
+ dataProviderRef.current = dataProvider;
419
424
  const viewer = new Viewer({
420
425
  container,
421
426
  dataProvider,
@@ -535,70 +540,120 @@ var PanoramaxPanel = (t0) => {
535
540
  }
536
541
  useEffect(t5, t6);
537
542
  let t7;
543
+ if ($[14] !== onViewerView) {
544
+ t7 = (view) => onViewerView?.(view);
545
+ $[14] = onViewerView;
546
+ $[15] = t7;
547
+ } else {
548
+ t7 = $[15];
549
+ }
550
+ const emitViewerView = useEffectEvent(t7);
551
+ const reportsView = onViewerView !== void 0;
538
552
  let t8;
539
- if ($[14] !== actions || $[15] !== photo.photoId) {
540
- t7 = function syncExternalPhotoSelection() {
553
+ if ($[16] !== emitViewerView || $[17] !== reportsView) {
554
+ t8 = function reportViewInPhoto() {
541
555
  const viewer_0 = viewerRef.current;
542
- if (!viewer_0 || photo.photoId === lastViewerPhotoIdRef.current) {
556
+ if (!viewer_0 || !reportsView) {
557
+ return;
558
+ }
559
+ const read = async () => {
560
+ const [image_0, t92, fov_0] = await Promise.all([viewer_0.getImage(), viewer_0.getPointOfView(), viewer_0.getFieldOfView()]);
561
+ const {
562
+ bearing,
563
+ tilt
564
+ } = t92;
565
+ const item_0 = dataProviderRef.current?.getItem(image_0.id);
566
+ return item_0 ? {
567
+ providerId: "panoramax",
568
+ photoId: image_0.id,
569
+ heading: bearing,
570
+ pitch: tilt,
571
+ zoom: panoramaxZoomLevel(fov_0, panoramaxItemFieldOfView(item_0))
572
+ } : null;
573
+ };
574
+ return watchMapillaryJsView(viewer_0, read, (view_0) => emitViewerView(view_0));
575
+ };
576
+ $[16] = emitViewerView;
577
+ $[17] = reportsView;
578
+ $[18] = t8;
579
+ } else {
580
+ t8 = $[18];
581
+ }
582
+ let t9;
583
+ if ($[19] !== actions || $[20] !== reportsView) {
584
+ t9 = [actions, reportsView];
585
+ $[19] = actions;
586
+ $[20] = reportsView;
587
+ $[21] = t9;
588
+ } else {
589
+ t9 = $[21];
590
+ }
591
+ useEffect(t8, t9);
592
+ let t10;
593
+ let t11;
594
+ if ($[22] !== actions || $[23] !== photo.photoId) {
595
+ t10 = function syncExternalPhotoSelection() {
596
+ const viewer_1 = viewerRef.current;
597
+ if (!viewer_1 || photo.photoId === lastViewerPhotoIdRef.current) {
543
598
  return;
544
599
  }
545
600
  actions.reset();
546
601
  lastViewerPhotoIdRef.current = photo.photoId;
547
602
  if (navigableRef.current) {
548
- viewer_0.moveTo(photo.photoId).catch(_temp3);
603
+ viewer_1.moveTo(photo.photoId).catch(_temp3);
549
604
  } else {
550
605
  pendingImageIdRef.current = photo.photoId;
551
606
  }
552
607
  };
553
- t8 = [actions, photo.photoId];
554
- $[14] = actions;
555
- $[15] = photo.photoId;
556
- $[16] = t7;
557
- $[17] = t8;
608
+ t11 = [actions, photo.photoId];
609
+ $[22] = actions;
610
+ $[23] = photo.photoId;
611
+ $[24] = t10;
612
+ $[25] = t11;
558
613
  } else {
559
- t7 = $[16];
560
- t8 = $[17];
614
+ t10 = $[24];
615
+ t11 = $[25];
561
616
  }
562
- useEffect(t7, t8);
617
+ useEffect(t10, t11);
563
618
  const attribution = hideAttribution ? null : shownPhoto;
564
619
  const license = attribution?.details?.license;
565
- let t10;
566
- let t9;
567
- if ($[18] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
568
- t9 = {
620
+ let t12;
621
+ let t13;
622
+ if ($[26] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
623
+ t12 = {
569
624
  aspectRatio: "4 / 3"
570
625
  };
571
- t10 = /* @__PURE__ */ jsx("div", { ref: containerRef, className: "h-full w-full" });
572
- $[18] = t10;
573
- $[19] = t9;
626
+ t13 = /* @__PURE__ */ jsx("div", { ref: containerRef, className: "h-full w-full" });
627
+ $[26] = t12;
628
+ $[27] = t13;
574
629
  } else {
575
- t10 = $[18];
576
- t9 = $[19];
630
+ t12 = $[26];
631
+ t13 = $[27];
577
632
  }
578
- let t11;
579
- if ($[20] !== attribution || $[21] !== license) {
580
- t11 = attribution ? /* @__PURE__ */ jsxs("div", { className: "absolute right-0 bottom-0 flex gap-1.5 rounded-tl bg-black/50 px-1.5 py-0.5 text-[11px] leading-4 text-white", children: [
633
+ let t14;
634
+ if ($[28] !== attribution || $[29] !== license) {
635
+ t14 = attribution ? /* @__PURE__ */ jsxs("div", { className: "absolute right-0 bottom-0 flex gap-1.5 rounded-tl bg-black/50 px-1.5 py-0.5 text-[11px] leading-4 text-white", children: [
581
636
  /* @__PURE__ */ jsx("a", { className: "underline-offset-2 hover:underline", href: providerExternalLink(attribution), rel: "noreferrer", target: "_blank", children: ["Panoramax", attribution.creatorName].filter(Boolean).join(" \xB7 ") }),
582
637
  license ? attribution.details?.licenseUrl ? /* @__PURE__ */ jsx("a", { className: "underline-offset-2 hover:underline", href: attribution.details.licenseUrl, rel: "noreferrer", target: "_blank", children: license }) : /* @__PURE__ */ jsx("span", { children: license }) : null
583
638
  ] }) : null;
584
- $[20] = attribution;
585
- $[21] = license;
586
- $[22] = t11;
639
+ $[28] = attribution;
640
+ $[29] = license;
641
+ $[30] = t14;
587
642
  } else {
588
- t11 = $[22];
643
+ t14 = $[30];
589
644
  }
590
- let t12;
591
- if ($[23] !== t11) {
592
- t12 = /* @__PURE__ */ jsxs("div", { className: "relative min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900", style: t9, children: [
593
- t10,
594
- t11
645
+ let t15;
646
+ if ($[31] !== t14) {
647
+ t15 = /* @__PURE__ */ jsxs("div", { className: "relative min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900", style: t12, children: [
648
+ t13,
649
+ t14
595
650
  ] });
596
- $[23] = t11;
597
- $[24] = t12;
651
+ $[31] = t14;
652
+ $[32] = t15;
598
653
  } else {
599
- t12 = $[24];
654
+ t15 = $[32];
600
655
  }
601
- return t12;
656
+ return t15;
602
657
  };
603
658
  function _temp() {
604
659
  }
@@ -0,0 +1,56 @@
1
+ // src/panels/photoView.ts
2
+ var REPORT_DELAY_MS = 200;
3
+ var PANORAMAX_MIN_FOV = 5;
4
+ var PANORAMAX_MAX_FOV = 90;
5
+ var tanOfHalf = (degrees) => Math.tan(degrees * Math.PI / 360);
6
+ var panoramaxZoomLevel = (verticalFov, horizontalFovOfPicture) => {
7
+ const maxFov = Math.min(horizontalFovOfPicture * 3 / 4, PANORAMAX_MAX_FOV);
8
+ const fov = horizontalFovOfPicture >= 360 ? verticalFov : tanOfHalf(verticalFov) / tanOfHalf(horizontalFovOfPicture) * horizontalFovOfPicture;
9
+ const level = (maxFov - fov) / (maxFov - PANORAMAX_MIN_FOV) * 100;
10
+ return Math.min(100, Math.max(0, level));
11
+ };
12
+ var viewKey = (view) => JSON.stringify(view, (_, value) => typeof value === "number" ? Number(value.toFixed(4)) : value);
13
+ var createPhotoViewReporter = (read, report, delayMs = REPORT_DELAY_MS) => {
14
+ let timer;
15
+ let last = null;
16
+ let stopped = false;
17
+ const send = () => {
18
+ void read().then((view) => {
19
+ if (view && !stopped && viewKey(view) !== last) {
20
+ last = viewKey(view);
21
+ report(view);
22
+ }
23
+ }).catch(() => {
24
+ });
25
+ };
26
+ return {
27
+ viewChanged: () => {
28
+ clearTimeout(timer);
29
+ timer = setTimeout(send, delayMs);
30
+ },
31
+ stop: () => {
32
+ stopped = true;
33
+ clearTimeout(timer);
34
+ }
35
+ };
36
+ };
37
+ var watchMapillaryJsView = (viewer, read, report) => {
38
+ const reporter = createPhotoViewReporter(read, report);
39
+ const {
40
+ viewChanged
41
+ } = reporter;
42
+ viewer.on("image", viewChanged);
43
+ viewer.on("pov", viewChanged);
44
+ viewer.on("fov", viewChanged);
45
+ viewer.on("moveend", viewChanged);
46
+ viewChanged();
47
+ return () => {
48
+ reporter.stop();
49
+ viewer.off("image", viewChanged);
50
+ viewer.off("pov", viewChanged);
51
+ viewer.off("fov", viewChanged);
52
+ viewer.off("moveend", viewChanged);
53
+ };
54
+ };
55
+
56
+ export { createPhotoViewReporter, panoramaxZoomLevel, watchMapillaryJsView };
@@ -10,7 +10,10 @@ export type PhotoDateMarker = {
10
10
  export type PhotoDateRangeFilterProps = {
11
11
  /** The chosen range, days as `YYYY-MM-DD`. No `from` and no `to`: all dates. */
12
12
  value: DateRange;
13
- /** Called when a handle is released, a year label is clicked or a date input changes. */
13
+ /**
14
+ * Called when a handle is released, the chart is clicked, a year label is clicked or a date
15
+ * input changes.
16
+ */
14
17
  onChange: (range: DateRange) => void;
15
18
  /**
16
19
  * Capture times (ms) of the photos to show as marks above the slider, usually all photos in
@@ -0,0 +1,21 @@
1
+ import type { DateSliderPositions } from '@osm-editor-kit/street-imagery';
2
+ export type DateSliderHandle = 'from' | 'to';
3
+ /** Size of one step of the slider, in positions: about a day to a month, depending on the age. */
4
+ export declare const DATE_SLIDER_STEP = 0.001;
5
+ /** The handles as they show. */
6
+ export declare const HANDLE_PX = 16;
7
+ /** Position of a point `leftPx` from the start of the track, in steps like the native handles. */
8
+ export declare const positionOnTrack: (leftPx: number, widthPx: number) => number;
9
+ /**
10
+ * The handle a press takes. `null` when the handles overlap under the press: they can only move
11
+ * apart, so the direction of the drag decides (`handleOfDrag`), also when both are at one end.
12
+ */
13
+ export declare const pressedHandle: (positions: DateSliderPositions, press: number, widthPx: number) => DateSliderHandle | null;
14
+ export declare const handleOfDrag: (deltaPx: number) => DateSliderHandle;
15
+ /** A drag keeps the distance between a grabbed handle and the pointer: the handle does not jump. */
16
+ export declare const dragPositions: (start: DateSliderPositions, press: number, handle: DateSliderHandle, deltaPx: number, widthPx: number) => DateSliderPositions;
17
+ /** A click or tap moves the nearer handle there, or does nothing on a handle itself. */
18
+ export declare const tapPositions: (start: DateSliderPositions, press: number, widthPx: number) => {
19
+ handle: DateSliderHandle;
20
+ positions: DateSliderPositions | null;
21
+ };