@osm-editor-kit/street-imagery-react 0.1.0-alpha.2 → 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.
- package/README.md +68 -0
- package/dist/MapillaryPanel-I5GKT6YT.js +2 -0
- package/dist/StreetLevelImagerySourcesAndLayers.d.ts +6 -1
- package/dist/StreetLevelImageryViewer.d.ts +10 -1
- package/dist/chunk-72HJV6II.js +300 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +574 -10
- package/dist/mapillary/useMapillaryFeatures.d.ts +36 -0
- package/dist/panels/MapillaryPanel.d.ts +13 -1
- package/dist/panels/mapillaryLookAt.d.ts +41 -0
- package/dist/viewpoints/FloatingPhotoViewer.d.ts +31 -0
- package/dist/viewpoints/ViewpointLayer.d.ts +17 -0
- package/dist/viewpoints/useViewSuggestions.d.ts +23 -0
- package/dist/viewpoints/useViewpointSessionStore.d.ts +39 -0
- package/dist/viewpoints/viewDirectionLabels.d.ts +4 -0
- package/package.json +3 -3
- package/dist/MapillaryPanel-QX6NESX7.js +0 -2
- package/dist/chunk-BS6LPDMY.js +0 -143
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
export { MapillaryPanel } from './chunk-
|
|
1
|
+
export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo } from './chunk-72HJV6II.js';
|
|
2
2
|
export { PanoramaxPanel } from './chunk-ZH3TPW4D.js';
|
|
3
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';
|
|
4
|
+
import { photosToFeatureCollection, emptyPointCollection, sequencesToFeatureCollection, emptyLineCollection, adapterById, viewConeGeoJson, coneRadiusMeters, setStreetImageryConfig, photoLayerId, viewfieldLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, distanceMeters, fetchImageDetections, mapFeaturesToFeatureCollection, buildMapFeatureLayerFilter, featureSourceId, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
|
|
5
5
|
import { Source, Layer, useMap } from 'react-map-gl/maplibre';
|
|
6
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';
|
|
7
|
+
import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState, useRef, useEffectEvent } from 'react';
|
|
8
|
+
import { useQuery, useQueries } from '@tanstack/react-query';
|
|
9
|
+
import { create } from 'zustand';
|
|
9
10
|
|
|
10
11
|
var HIGHLIGHT_SOURCE_ID = "selection-highlight";
|
|
11
12
|
var HIGHLIGHT_LAYER_ID = "selection-highlight-layer";
|
|
@@ -199,6 +200,12 @@ var FEATURE_CIRCLE_RADIUS = [
|
|
|
199
200
|
18,
|
|
200
201
|
4
|
|
201
202
|
];
|
|
203
|
+
var padBbox = ([west, south, east, north], factor) => {
|
|
204
|
+
const dx = (east - west) * factor;
|
|
205
|
+
const dy = (north - south) * factor;
|
|
206
|
+
return [west - dx, south - dy, east + dx, north + dy];
|
|
207
|
+
};
|
|
208
|
+
var lngLatInBbox = ([lng, lat], [west, south, east, north]) => lng >= west && lng <= east && lat >= south && lat <= north;
|
|
202
209
|
var PHOTO_SORT_KEY = ["coalesce", ["get", "capturedAt"], 0];
|
|
203
210
|
var FEATURE_SORT_KEY = ["coalesce", ["get", "lastSeenAt"], 0];
|
|
204
211
|
var PhotoProviderLayer = ({
|
|
@@ -214,8 +221,12 @@ var PhotoProviderLayer = ({
|
|
|
214
221
|
const { data: photos = [] } = useProviderPhotos(providerId, bbox, zoom);
|
|
215
222
|
const { data: sequences = [] } = useProviderSequences(providerId, bbox, zoom);
|
|
216
223
|
const visiblePhotos = zoom >= adapter.minZoom ? photos.filter((photo) => photoMatchesFilters(photo, filter?.photoTypes, filter?.date)) : [];
|
|
217
|
-
const
|
|
218
|
-
const
|
|
224
|
+
const drawBbox = bbox ? padBbox(bbox, 0.25) : null;
|
|
225
|
+
const photoCollection = zoom >= adapter.minZoom ? photosToFeatureCollection(
|
|
226
|
+
drawBbox ? visiblePhotos.filter((photo) => lngLatInBbox(photo.lngLat, drawBbox)) : visiblePhotos
|
|
227
|
+
) : emptyPointCollection();
|
|
228
|
+
const viewfieldsActive = showViewfields && zoom >= VIEWFIELD_MIN_ZOOM;
|
|
229
|
+
const viewfieldCollection = viewfieldsActive ? photosToViewfieldsFeatureCollection(visiblePhotos, zoom, { bbox }) : emptyPolygonCollection();
|
|
219
230
|
const photoFilter = buildPhotoLayerFilter(filter?.photoTypes, filter?.date);
|
|
220
231
|
const photoSrcId = photoSourceId(providerId);
|
|
221
232
|
const viewfieldSrcId = viewfieldSourceId(providerId);
|
|
@@ -342,7 +353,10 @@ var MapFeatureProviderLayer = ({
|
|
|
342
353
|
}) => {
|
|
343
354
|
const adapter = adapterById[providerId];
|
|
344
355
|
const { data: features = [] } = useProviderMapFeatures(providerId, bbox, zoom);
|
|
345
|
-
const
|
|
356
|
+
const valueFilter = filter?.mapFeatureValue;
|
|
357
|
+
const featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(
|
|
358
|
+
valueFilter ? features.filter((feature) => valueFilter(feature.value)) : features
|
|
359
|
+
) : emptyPointCollection();
|
|
346
360
|
const featureFilter = buildMapFeatureLayerFilter(filter?.date);
|
|
347
361
|
const featureSrcId = featureSourceId(providerId);
|
|
348
362
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -448,7 +462,7 @@ var StreetLevelImagerySourcesAndLayers = ({
|
|
|
448
462
|
] });
|
|
449
463
|
};
|
|
450
464
|
var MapillaryPanel2 = lazy(
|
|
451
|
-
() => import('./MapillaryPanel-
|
|
465
|
+
() => import('./MapillaryPanel-I5GKT6YT.js').then((module) => ({
|
|
452
466
|
default: module.MapillaryPanel
|
|
453
467
|
}))
|
|
454
468
|
);
|
|
@@ -462,7 +476,11 @@ var StreetLevelImageryViewer = ({
|
|
|
462
476
|
photo,
|
|
463
477
|
groupPhotos,
|
|
464
478
|
onPhotoSelected,
|
|
465
|
-
onEaseMapToPoint
|
|
479
|
+
onEaseMapToPoint,
|
|
480
|
+
onViewerPhoto,
|
|
481
|
+
lookAtBearing,
|
|
482
|
+
lookAt,
|
|
483
|
+
outlines
|
|
466
484
|
}) => {
|
|
467
485
|
if (photo.providerId === "mapillary") {
|
|
468
486
|
return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewerPanelPlaceholder, {}), children: /* @__PURE__ */ jsx(
|
|
@@ -471,6 +489,10 @@ var StreetLevelImageryViewer = ({
|
|
|
471
489
|
groupPhotos,
|
|
472
490
|
onEaseMapToPoint,
|
|
473
491
|
onPhotoSelected,
|
|
492
|
+
lookAt,
|
|
493
|
+
lookAtBearing,
|
|
494
|
+
outlines,
|
|
495
|
+
onViewerPhoto,
|
|
474
496
|
photo
|
|
475
497
|
}
|
|
476
498
|
) });
|
|
@@ -656,4 +678,546 @@ var usePhotoThumbnail = (photo) => useQuery({
|
|
|
656
678
|
staleTime: 24 * 60 * 60 * 1e3
|
|
657
679
|
});
|
|
658
680
|
|
|
659
|
-
|
|
681
|
+
// src/viewpoints/viewDirectionLabels.ts
|
|
682
|
+
var ROLE_LABELS = {
|
|
683
|
+
here: "Here",
|
|
684
|
+
"line-start": "Start",
|
|
685
|
+
"line-end": "End",
|
|
686
|
+
"into-node": "Junction",
|
|
687
|
+
custom: "View"
|
|
688
|
+
};
|
|
689
|
+
var viewpointRoleLabel = (role) => ROLE_LABELS[role];
|
|
690
|
+
var viewSuggestionLabel = ({ viewpoint, direction }) => {
|
|
691
|
+
const role = viewpoint.label ?? viewpointRoleLabel(viewpoint.role);
|
|
692
|
+
switch (direction.kind) {
|
|
693
|
+
case "forward":
|
|
694
|
+
return `${role}, looking along the street`;
|
|
695
|
+
case "back":
|
|
696
|
+
return `${role}, looking back along the street`;
|
|
697
|
+
case "into":
|
|
698
|
+
return `${role}, looking into the junction`;
|
|
699
|
+
case "N":
|
|
700
|
+
case "E":
|
|
701
|
+
case "S":
|
|
702
|
+
case "W":
|
|
703
|
+
return `${role}, looking ${direction.kind}`;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
var DEFAULT_LAYOUT = { right: 16, bottom: 32, width: 420, minimized: false };
|
|
707
|
+
var MIN_WIDTH = 280;
|
|
708
|
+
var EDGE = 8;
|
|
709
|
+
var readLayout = (storageKey) => {
|
|
710
|
+
try {
|
|
711
|
+
const raw = localStorage.getItem(storageKey);
|
|
712
|
+
return raw ? { ...DEFAULT_LAYOUT, ...JSON.parse(raw) } : DEFAULT_LAYOUT;
|
|
713
|
+
} catch {
|
|
714
|
+
return DEFAULT_LAYOUT;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
var writeLayout = (storageKey, layout) => {
|
|
718
|
+
try {
|
|
719
|
+
localStorage.setItem(storageKey, JSON.stringify(layout));
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
var Icon = ({ path, className = "size-4" }) => /* @__PURE__ */ jsx(
|
|
724
|
+
"svg",
|
|
725
|
+
{
|
|
726
|
+
"aria-hidden": true,
|
|
727
|
+
className,
|
|
728
|
+
fill: "none",
|
|
729
|
+
stroke: "currentColor",
|
|
730
|
+
strokeLinecap: "round",
|
|
731
|
+
strokeLinejoin: "round",
|
|
732
|
+
strokeWidth: 1.75,
|
|
733
|
+
viewBox: "0 0 24 24",
|
|
734
|
+
children: /* @__PURE__ */ jsx("path", { d: path })
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
var ICONS = {
|
|
738
|
+
back: "M15 18l-6-6 6-6",
|
|
739
|
+
forward: "M9 18l6-6-6-6",
|
|
740
|
+
close: "M18 6 6 18M6 6l12 12",
|
|
741
|
+
minimize: "M5 12h14",
|
|
742
|
+
expand: "M4 14v6h6M20 10V4h-6M14 10l6-6M10 14l-6 6",
|
|
743
|
+
arrow: "M12 20V5M6 11l6-6 6 6"
|
|
744
|
+
};
|
|
745
|
+
var ToolbarButton = ({
|
|
746
|
+
label,
|
|
747
|
+
onClick,
|
|
748
|
+
disabled,
|
|
749
|
+
children
|
|
750
|
+
}) => /* @__PURE__ */ jsx(
|
|
751
|
+
"button",
|
|
752
|
+
{
|
|
753
|
+
"aria-label": label,
|
|
754
|
+
className: "inline-flex size-7 items-center justify-center rounded-md text-slate-600 hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent",
|
|
755
|
+
disabled,
|
|
756
|
+
onClick,
|
|
757
|
+
title: label,
|
|
758
|
+
type: "button",
|
|
759
|
+
children
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
var formatShortDate = (capturedAt) => capturedAt == null ? "\u2014" : new Intl.DateTimeFormat("en", { year: "2-digit", month: "short" }).format(capturedAt);
|
|
763
|
+
var SuggestionChip = ({
|
|
764
|
+
suggestion,
|
|
765
|
+
active,
|
|
766
|
+
onSelect
|
|
767
|
+
}) => {
|
|
768
|
+
const best = suggestion.candidates[0];
|
|
769
|
+
const label = viewSuggestionLabel(suggestion);
|
|
770
|
+
return /* @__PURE__ */ jsxs(
|
|
771
|
+
"button",
|
|
772
|
+
{
|
|
773
|
+
"aria-label": best ? label : `${label} (no photo)`,
|
|
774
|
+
"aria-pressed": active,
|
|
775
|
+
className: [
|
|
776
|
+
"inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs whitespace-nowrap",
|
|
777
|
+
active ? "border-fuchsia-600 bg-fuchsia-600 text-white" : best ? "border-slate-300 bg-white text-slate-700 hover:border-fuchsia-500" : "border-dashed border-slate-200 bg-white text-slate-400"
|
|
778
|
+
].join(" "),
|
|
779
|
+
disabled: !best,
|
|
780
|
+
onClick: onSelect,
|
|
781
|
+
title: best ? label : `${label}: no matching photo`,
|
|
782
|
+
type: "button",
|
|
783
|
+
children: [
|
|
784
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: viewpointRoleLabel(suggestion.viewpoint.role) }),
|
|
785
|
+
/* @__PURE__ */ jsx(
|
|
786
|
+
"span",
|
|
787
|
+
{
|
|
788
|
+
"aria-hidden": true,
|
|
789
|
+
className: "inline-flex",
|
|
790
|
+
style: { transform: `rotate(${suggestion.direction.bearing}deg)` },
|
|
791
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "size-3", path: ICONS.arrow })
|
|
792
|
+
}
|
|
793
|
+
),
|
|
794
|
+
/* @__PURE__ */ jsx("span", { className: active ? "text-fuchsia-100" : "text-slate-400", children: formatShortDate(best?.photo.capturedAt) })
|
|
795
|
+
]
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
};
|
|
799
|
+
var FloatingPhotoViewer = ({
|
|
800
|
+
title,
|
|
801
|
+
suggestions,
|
|
802
|
+
activeDirectionKey,
|
|
803
|
+
onSelectSuggestion,
|
|
804
|
+
canGoBack,
|
|
805
|
+
canGoForward,
|
|
806
|
+
onBack,
|
|
807
|
+
onForward,
|
|
808
|
+
onClose,
|
|
809
|
+
status,
|
|
810
|
+
children,
|
|
811
|
+
footer,
|
|
812
|
+
drawer,
|
|
813
|
+
storageKey = "street-imagery:floating-viewer"
|
|
814
|
+
}) => {
|
|
815
|
+
const [layout, setLayout] = useState(() => readLayout(storageKey));
|
|
816
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
817
|
+
const dragRef = useRef(
|
|
818
|
+
null
|
|
819
|
+
);
|
|
820
|
+
const updateLayout = (next) => {
|
|
821
|
+
setLayout(next);
|
|
822
|
+
writeLayout(storageKey, next);
|
|
823
|
+
};
|
|
824
|
+
const onShortcut = useEffectEvent((event) => {
|
|
825
|
+
const target = event.target;
|
|
826
|
+
if (target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"]')) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (event.key === "Escape") {
|
|
830
|
+
onClose();
|
|
831
|
+
} else if (event.key === "[" && canGoBack) {
|
|
832
|
+
onBack();
|
|
833
|
+
} else if (event.key === "]" && canGoForward) {
|
|
834
|
+
onForward();
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
useEffect(function subscribeKeyboardShortcuts() {
|
|
838
|
+
window.addEventListener("keydown", onShortcut);
|
|
839
|
+
return () => window.removeEventListener("keydown", onShortcut);
|
|
840
|
+
}, []);
|
|
841
|
+
const startDrag = (event, mode) => {
|
|
842
|
+
if (event.button !== 0 || event.target.closest("button")) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
846
|
+
dragRef.current = { x: event.clientX, y: event.clientY, layout, mode };
|
|
847
|
+
};
|
|
848
|
+
const onDrag = (event) => {
|
|
849
|
+
const drag = dragRef.current;
|
|
850
|
+
if (!drag) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const dx = event.clientX - drag.x;
|
|
854
|
+
const dy = event.clientY - drag.y;
|
|
855
|
+
setLayout(
|
|
856
|
+
drag.mode === "move" ? {
|
|
857
|
+
...drag.layout,
|
|
858
|
+
right: Math.max(EDGE, drag.layout.right - dx),
|
|
859
|
+
bottom: Math.max(EDGE, drag.layout.bottom - dy)
|
|
860
|
+
} : { ...drag.layout, width: Math.max(MIN_WIDTH, drag.layout.width - dx) }
|
|
861
|
+
);
|
|
862
|
+
};
|
|
863
|
+
const endDrag = () => {
|
|
864
|
+
if (dragRef.current) {
|
|
865
|
+
dragRef.current = null;
|
|
866
|
+
writeLayout(storageKey, layout);
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
const dragHandlers = { onPointerMove: onDrag, onPointerUp: endDrag, onPointerCancel: endDrag };
|
|
870
|
+
if (layout.minimized) {
|
|
871
|
+
return /* @__PURE__ */ jsxs(
|
|
872
|
+
"button",
|
|
873
|
+
{
|
|
874
|
+
className: "absolute z-10 inline-flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-sm font-medium text-slate-800 shadow-lg ring-1 ring-slate-200 hover:bg-slate-50",
|
|
875
|
+
onClick: () => updateLayout({ ...layout, minimized: false }),
|
|
876
|
+
style: { right: layout.right, bottom: layout.bottom },
|
|
877
|
+
type: "button",
|
|
878
|
+
children: [
|
|
879
|
+
/* @__PURE__ */ jsx(Icon, { path: ICONS.expand }),
|
|
880
|
+
title
|
|
881
|
+
]
|
|
882
|
+
}
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return /* @__PURE__ */ jsxs(
|
|
886
|
+
"section",
|
|
887
|
+
{
|
|
888
|
+
"aria-label": "Photo viewer",
|
|
889
|
+
className: "absolute z-10 flex max-h-[calc(100%-1rem)] flex-col overflow-hidden rounded-xl bg-white shadow-xl ring-1 ring-slate-200",
|
|
890
|
+
style: {
|
|
891
|
+
right: layout.right,
|
|
892
|
+
bottom: layout.bottom,
|
|
893
|
+
width: `min(${layout.width}px, calc(100% - 1rem))`
|
|
894
|
+
},
|
|
895
|
+
children: [
|
|
896
|
+
/* @__PURE__ */ jsx(
|
|
897
|
+
"div",
|
|
898
|
+
{
|
|
899
|
+
"aria-hidden": true,
|
|
900
|
+
className: "absolute inset-y-0 left-0 z-10 w-1.5 cursor-ew-resize hover:bg-fuchsia-500/20",
|
|
901
|
+
onPointerDown: (event) => startDrag(event, "resize"),
|
|
902
|
+
...dragHandlers
|
|
903
|
+
}
|
|
904
|
+
),
|
|
905
|
+
/* @__PURE__ */ jsxs(
|
|
906
|
+
"header",
|
|
907
|
+
{
|
|
908
|
+
className: "flex cursor-move touch-none items-center gap-1 border-b border-slate-100 px-2 py-1.5 select-none",
|
|
909
|
+
onPointerDown: (event) => startDrag(event, "move"),
|
|
910
|
+
...dragHandlers,
|
|
911
|
+
children: [
|
|
912
|
+
/* @__PURE__ */ jsx(ToolbarButton, { disabled: !canGoBack, label: "Previous photo in history ([)", onClick: onBack, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.back }) }),
|
|
913
|
+
/* @__PURE__ */ jsx(
|
|
914
|
+
ToolbarButton,
|
|
915
|
+
{
|
|
916
|
+
disabled: !canGoForward,
|
|
917
|
+
label: "Next photo in history (])",
|
|
918
|
+
onClick: onForward,
|
|
919
|
+
children: /* @__PURE__ */ jsx(Icon, { path: ICONS.forward })
|
|
920
|
+
}
|
|
921
|
+
),
|
|
922
|
+
/* @__PURE__ */ jsx("h2", { className: "min-w-0 flex-1 truncate px-1 text-sm font-semibold text-slate-800", children: title }),
|
|
923
|
+
/* @__PURE__ */ jsx(
|
|
924
|
+
ToolbarButton,
|
|
925
|
+
{
|
|
926
|
+
label: "Minimize",
|
|
927
|
+
onClick: () => updateLayout({ ...layout, minimized: true }),
|
|
928
|
+
children: /* @__PURE__ */ jsx(Icon, { path: ICONS.minimize })
|
|
929
|
+
}
|
|
930
|
+
),
|
|
931
|
+
/* @__PURE__ */ jsx(ToolbarButton, { label: "Close (Esc)", onClick: onClose, children: /* @__PURE__ */ jsx(Icon, { path: ICONS.close }) })
|
|
932
|
+
]
|
|
933
|
+
}
|
|
934
|
+
),
|
|
935
|
+
suggestions.length > 0 ? /* @__PURE__ */ jsx(
|
|
936
|
+
"nav",
|
|
937
|
+
{
|
|
938
|
+
"aria-label": "Suggested views",
|
|
939
|
+
className: "flex gap-1 overflow-x-auto border-b border-slate-100 px-2 py-1.5",
|
|
940
|
+
children: suggestions.map((suggestion) => /* @__PURE__ */ jsx(
|
|
941
|
+
SuggestionChip,
|
|
942
|
+
{
|
|
943
|
+
active: suggestion.direction.key === activeDirectionKey,
|
|
944
|
+
onSelect: () => onSelectSuggestion(suggestion),
|
|
945
|
+
suggestion
|
|
946
|
+
},
|
|
947
|
+
suggestion.direction.key
|
|
948
|
+
))
|
|
949
|
+
}
|
|
950
|
+
) : null,
|
|
951
|
+
/* @__PURE__ */ jsxs("div", { className: "min-h-0 overflow-y-auto", children: [
|
|
952
|
+
status ? /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-xs text-slate-500", role: "status", children: status }) : null,
|
|
953
|
+
children,
|
|
954
|
+
footer ? /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-xs text-slate-600", children: footer }) : null,
|
|
955
|
+
drawer ? /* @__PURE__ */ jsxs("div", { className: "border-t border-slate-100", children: [
|
|
956
|
+
/* @__PURE__ */ jsxs(
|
|
957
|
+
"button",
|
|
958
|
+
{
|
|
959
|
+
"aria-expanded": drawerOpen,
|
|
960
|
+
className: "flex w-full items-center justify-between px-3 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50",
|
|
961
|
+
onClick: () => setDrawerOpen((open) => !open),
|
|
962
|
+
type: "button",
|
|
963
|
+
children: [
|
|
964
|
+
drawer.label,
|
|
965
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: drawerOpen ? "inline-flex rotate-90" : "inline-flex", children: /* @__PURE__ */ jsx(Icon, { className: "size-3.5", path: ICONS.forward }) })
|
|
966
|
+
]
|
|
967
|
+
}
|
|
968
|
+
),
|
|
969
|
+
drawerOpen ? /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: drawer.content }) : null
|
|
970
|
+
] }) : null
|
|
971
|
+
] })
|
|
972
|
+
]
|
|
973
|
+
}
|
|
974
|
+
);
|
|
975
|
+
};
|
|
976
|
+
var SOURCE_DIRECTIONS = "viewpoint-directions";
|
|
977
|
+
var SOURCE_POINTS = "viewpoint-points";
|
|
978
|
+
var SOURCE_LINE = "viewpoint-line";
|
|
979
|
+
var VIEWPOINT_DIRECTION_LAYER_ID = "viewpoint-direction-fill";
|
|
980
|
+
var VIEWPOINT_DEFAULT_COLOR = "#c026d3";
|
|
981
|
+
var directionLengthMeters = (zoom) => Math.max(8, coneRadiusMeters(zoom) * 3);
|
|
982
|
+
var ViewpointLayer = ({
|
|
983
|
+
viewpoints,
|
|
984
|
+
suggestions,
|
|
985
|
+
activeDirectionKey,
|
|
986
|
+
zoom,
|
|
987
|
+
line,
|
|
988
|
+
color = VIEWPOINT_DEFAULT_COLOR
|
|
989
|
+
}) => {
|
|
990
|
+
const length = directionLengthMeters(zoom);
|
|
991
|
+
const directions = {
|
|
992
|
+
type: "FeatureCollection",
|
|
993
|
+
features: suggestions.map(({ viewpoint, direction, candidates }) => ({
|
|
994
|
+
...viewConeGeoJson(viewpoint.lngLat, direction.bearing, 40, length),
|
|
995
|
+
properties: {
|
|
996
|
+
key: direction.key,
|
|
997
|
+
active: direction.key === activeDirectionKey,
|
|
998
|
+
hasPhoto: candidates.length > 0
|
|
999
|
+
}
|
|
1000
|
+
}))
|
|
1001
|
+
};
|
|
1002
|
+
const points = {
|
|
1003
|
+
type: "FeatureCollection",
|
|
1004
|
+
features: viewpoints.map(
|
|
1005
|
+
(viewpoint) => ({
|
|
1006
|
+
type: "Feature",
|
|
1007
|
+
properties: { id: viewpoint.id },
|
|
1008
|
+
geometry: { type: "Point", coordinates: viewpoint.lngLat }
|
|
1009
|
+
})
|
|
1010
|
+
)
|
|
1011
|
+
};
|
|
1012
|
+
const lineFeature = {
|
|
1013
|
+
type: "FeatureCollection",
|
|
1014
|
+
features: line && line.length > 1 ? [{ type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: line } }] : []
|
|
1015
|
+
};
|
|
1016
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1017
|
+
/* @__PURE__ */ jsx(Source, { id: SOURCE_LINE, type: "geojson", data: lineFeature }),
|
|
1018
|
+
/* @__PURE__ */ jsx(
|
|
1019
|
+
Layer,
|
|
1020
|
+
{
|
|
1021
|
+
id: "viewpoint-line",
|
|
1022
|
+
type: "line",
|
|
1023
|
+
source: SOURCE_LINE,
|
|
1024
|
+
layout: { "line-cap": "round", "line-join": "round" },
|
|
1025
|
+
paint: { "line-color": color, "line-width": 5, "line-opacity": 0.5 }
|
|
1026
|
+
}
|
|
1027
|
+
),
|
|
1028
|
+
/* @__PURE__ */ jsx(Source, { id: SOURCE_DIRECTIONS, type: "geojson", data: directions }),
|
|
1029
|
+
/* @__PURE__ */ jsx(
|
|
1030
|
+
Layer,
|
|
1031
|
+
{
|
|
1032
|
+
id: VIEWPOINT_DIRECTION_LAYER_ID,
|
|
1033
|
+
type: "fill",
|
|
1034
|
+
source: SOURCE_DIRECTIONS,
|
|
1035
|
+
paint: {
|
|
1036
|
+
"fill-color": color,
|
|
1037
|
+
"fill-opacity": ["case", ["get", "active"], 0.55, ["get", "hasPhoto"], 0.18, 0.04]
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
),
|
|
1041
|
+
/* @__PURE__ */ jsx(
|
|
1042
|
+
Layer,
|
|
1043
|
+
{
|
|
1044
|
+
id: "viewpoint-direction-line",
|
|
1045
|
+
type: "line",
|
|
1046
|
+
source: SOURCE_DIRECTIONS,
|
|
1047
|
+
paint: {
|
|
1048
|
+
"line-color": color,
|
|
1049
|
+
"line-width": ["case", ["get", "active"], 2, 1],
|
|
1050
|
+
"line-dasharray": [2, 1.5],
|
|
1051
|
+
"line-opacity": ["case", ["get", "hasPhoto"], 0.9, 0.35]
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
),
|
|
1055
|
+
/* @__PURE__ */ jsx(Source, { id: SOURCE_POINTS, type: "geojson", data: points }),
|
|
1056
|
+
/* @__PURE__ */ jsx(
|
|
1057
|
+
Layer,
|
|
1058
|
+
{
|
|
1059
|
+
id: "viewpoint-point",
|
|
1060
|
+
type: "circle",
|
|
1061
|
+
source: SOURCE_POINTS,
|
|
1062
|
+
paint: {
|
|
1063
|
+
"circle-radius": 5,
|
|
1064
|
+
"circle-color": "#ffffff",
|
|
1065
|
+
"circle-stroke-color": color,
|
|
1066
|
+
"circle-stroke-width": 2.5
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
] });
|
|
1071
|
+
};
|
|
1072
|
+
var viewDirectionKeyFromFeatures = (features) => {
|
|
1073
|
+
const hit = features?.find((feature) => feature.layer?.id === VIEWPOINT_DIRECTION_LAYER_ID);
|
|
1074
|
+
const key = hit?.properties?.key;
|
|
1075
|
+
return typeof key === "string" ? key : null;
|
|
1076
|
+
};
|
|
1077
|
+
var samePhoto = (a, b) => a != null && a.providerId === b.providerId && a.photoId === b.photoId;
|
|
1078
|
+
var useViewpointSessionStore = create()((set, get) => ({
|
|
1079
|
+
sessionId: 0,
|
|
1080
|
+
viewpoints: [],
|
|
1081
|
+
line: null,
|
|
1082
|
+
activeDirectionKey: null,
|
|
1083
|
+
history: [],
|
|
1084
|
+
historyIndex: -1,
|
|
1085
|
+
actions: {
|
|
1086
|
+
open: ({ viewpoints, line = null }) => set((state) => ({
|
|
1087
|
+
sessionId: state.sessionId + 1,
|
|
1088
|
+
viewpoints,
|
|
1089
|
+
line,
|
|
1090
|
+
activeDirectionKey: null
|
|
1091
|
+
})),
|
|
1092
|
+
showPhoto: (entry) => set((state) => {
|
|
1093
|
+
const current = state.history[state.historyIndex];
|
|
1094
|
+
if (samePhoto(current?.photo, entry.photo)) {
|
|
1095
|
+
return entry.directionKey != null && entry.directionKey !== state.activeDirectionKey ? { activeDirectionKey: entry.directionKey } : state;
|
|
1096
|
+
}
|
|
1097
|
+
const history = [...state.history.slice(0, state.historyIndex + 1), entry].slice(
|
|
1098
|
+
-100
|
|
1099
|
+
);
|
|
1100
|
+
return {
|
|
1101
|
+
history,
|
|
1102
|
+
historyIndex: history.length - 1,
|
|
1103
|
+
activeDirectionKey: entry.directionKey ?? state.activeDirectionKey
|
|
1104
|
+
};
|
|
1105
|
+
}),
|
|
1106
|
+
back: () => {
|
|
1107
|
+
const { history, historyIndex } = get();
|
|
1108
|
+
const entry = history[historyIndex - 1];
|
|
1109
|
+
if (!entry) {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
set({ historyIndex: historyIndex - 1, activeDirectionKey: entry.directionKey });
|
|
1113
|
+
return entry;
|
|
1114
|
+
},
|
|
1115
|
+
forward: () => {
|
|
1116
|
+
const { history, historyIndex } = get();
|
|
1117
|
+
const entry = history[historyIndex + 1];
|
|
1118
|
+
if (!entry) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
set({ historyIndex: historyIndex + 1, activeDirectionKey: entry.directionKey });
|
|
1122
|
+
return entry;
|
|
1123
|
+
},
|
|
1124
|
+
close: () => set({ viewpoints: [], line: null, activeDirectionKey: null })
|
|
1125
|
+
}
|
|
1126
|
+
}));
|
|
1127
|
+
var useViewpointSessionId = () => useViewpointSessionStore((state) => state.sessionId);
|
|
1128
|
+
var useViewpoints = () => useViewpointSessionStore((state) => state.viewpoints);
|
|
1129
|
+
var useViewpointLine = () => useViewpointSessionStore((state) => state.line);
|
|
1130
|
+
var useActiveDirectionKey = () => useViewpointSessionStore((state) => state.activeDirectionKey);
|
|
1131
|
+
var useCurrentHistoryEntry = () => useViewpointSessionStore((state) => state.history[state.historyIndex] ?? null);
|
|
1132
|
+
var useCanGoBack = () => useViewpointSessionStore((state) => state.historyIndex > 0);
|
|
1133
|
+
var useCanGoForward = () => useViewpointSessionStore((state) => state.historyIndex < state.history.length - 1);
|
|
1134
|
+
var useViewpointSessionActions = () => useViewpointSessionStore((state) => state.actions);
|
|
1135
|
+
var getViewpointSession = () => {
|
|
1136
|
+
const { actions, history, historyIndex } = useViewpointSessionStore.getState();
|
|
1137
|
+
return { actions, current: history[historyIndex] ?? null };
|
|
1138
|
+
};
|
|
1139
|
+
var DEFAULT_SOURCES = [mapillaryPhotoSource];
|
|
1140
|
+
var useViewSuggestions = (viewpoints, {
|
|
1141
|
+
maxAgeYears,
|
|
1142
|
+
photoTypes,
|
|
1143
|
+
limit,
|
|
1144
|
+
filterPhoto,
|
|
1145
|
+
sources = DEFAULT_SOURCES,
|
|
1146
|
+
enabled = true
|
|
1147
|
+
} = {}) => {
|
|
1148
|
+
const [now] = useState(() => Date.now());
|
|
1149
|
+
const requests = viewpoints.flatMap(
|
|
1150
|
+
(viewpoint) => sources.map((source) => ({ viewpoint, source }))
|
|
1151
|
+
);
|
|
1152
|
+
const queries = useQueries({
|
|
1153
|
+
queries: requests.map(({ viewpoint, source }) => ({
|
|
1154
|
+
queryKey: [
|
|
1155
|
+
"street-imagery",
|
|
1156
|
+
"photos-near",
|
|
1157
|
+
source.id,
|
|
1158
|
+
viewpoint.lngLat[0],
|
|
1159
|
+
viewpoint.lngLat[1]
|
|
1160
|
+
],
|
|
1161
|
+
queryFn: ({ signal }) => source.fetchNear(viewpoint.lngLat, signal),
|
|
1162
|
+
enabled,
|
|
1163
|
+
staleTime: 10 * 60 * 1e3
|
|
1164
|
+
}))
|
|
1165
|
+
});
|
|
1166
|
+
const photosByViewpointId = /* @__PURE__ */ new Map();
|
|
1167
|
+
requests.forEach(({ viewpoint }, index) => {
|
|
1168
|
+
const photos = queries[index]?.data ?? [];
|
|
1169
|
+
const kept = filterPhoto ? photos.filter(filterPhoto) : photos;
|
|
1170
|
+
photosByViewpointId.set(viewpoint.id, [
|
|
1171
|
+
...photosByViewpointId.get(viewpoint.id) ?? [],
|
|
1172
|
+
...kept
|
|
1173
|
+
]);
|
|
1174
|
+
});
|
|
1175
|
+
const suggestions = buildViewSuggestions(
|
|
1176
|
+
viewpoints,
|
|
1177
|
+
(viewpoint) => photosByViewpointId.get(viewpoint.id) ?? [],
|
|
1178
|
+
{
|
|
1179
|
+
now,
|
|
1180
|
+
maxAgeMs: maxAgeYears == null ? void 0 : yearsToMs(maxAgeYears),
|
|
1181
|
+
photoTypes,
|
|
1182
|
+
limit
|
|
1183
|
+
}
|
|
1184
|
+
);
|
|
1185
|
+
return {
|
|
1186
|
+
suggestions,
|
|
1187
|
+
isLoading: enabled && queries.some((query) => query.isPending),
|
|
1188
|
+
// An error only when every source failed; one failing provider still leaves suggestions.
|
|
1189
|
+
isError: queries.length > 0 && queries.every((query) => query.isError)
|
|
1190
|
+
};
|
|
1191
|
+
};
|
|
1192
|
+
var STALE_TIME = 10 * 60 * 1e3;
|
|
1193
|
+
var useMapillaryMapFeatureImages = (featureId, { shape = SIGN_TARGET } = {}) => useQuery({
|
|
1194
|
+
queryKey: ["street-imagery", "mapillary-map-feature", featureId, shape],
|
|
1195
|
+
queryFn: ({ signal }) => fetchMapFeatureImages(featureId, { shape, signal }),
|
|
1196
|
+
enabled: !!featureId,
|
|
1197
|
+
staleTime: STALE_TIME
|
|
1198
|
+
});
|
|
1199
|
+
var useMapillaryMapFeaturesNear = (lngLat, {
|
|
1200
|
+
radiusMeters = 40,
|
|
1201
|
+
objectValues,
|
|
1202
|
+
enabled = true
|
|
1203
|
+
} = {}) => useQuery({
|
|
1204
|
+
queryKey: ["street-imagery", "mapillary-map-features-near", lngLat, radiusMeters, objectValues],
|
|
1205
|
+
queryFn: async ({ signal }) => {
|
|
1206
|
+
const center = lngLat;
|
|
1207
|
+
const features = await fetchMapillaryMapFeaturesInBbox(bboxAround(center, radiusMeters), {
|
|
1208
|
+
objectValues,
|
|
1209
|
+
signal
|
|
1210
|
+
});
|
|
1211
|
+
return features.map((feature) => ({ ...feature, distanceMeters: distanceMeters(center, feature.lngLat) })).filter((feature) => feature.distanceMeters <= radiusMeters).sort((a, b) => a.distanceMeters - b.distanceMeters);
|
|
1212
|
+
},
|
|
1213
|
+
enabled: enabled && !!lngLat,
|
|
1214
|
+
staleTime: STALE_TIME
|
|
1215
|
+
});
|
|
1216
|
+
var useMapillaryImageDetections = (imageId, { filter, filterKey } = {}) => useQuery({
|
|
1217
|
+
queryKey: ["street-imagery", "mapillary-detections", imageId, filterKey ?? null],
|
|
1218
|
+
queryFn: ({ signal }) => fetchImageDetections(imageId, { filter, signal }),
|
|
1219
|
+
enabled: !!imageId,
|
|
1220
|
+
staleTime: STALE_TIME
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
export { FloatingPhotoViewer, StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, ViewpointLayer, getViewpointSession, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useActiveDirectionKey, useAllProviderMapFeatures, useAllProviderMapFeaturesLoading, useAllProviderPhotos, useCanGoBack, useCanGoForward, useCurrentHistoryEntry, useMapViewportBbox, useMapillaryImageDetections, useMapillaryMapFeatureImages, useMapillaryMapFeaturesNear, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences, useViewSuggestions, useViewpointLine, useViewpointSessionActions, useViewpointSessionId, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel, viewpointRoleLabel };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type LngLat, type TargetShape } from '@osm-editor-kit/street-imagery';
|
|
2
|
+
/**
|
|
3
|
+
* A Mapillary map feature (sign, traffic light, marking) with all images that show it, grouped by
|
|
4
|
+
* day, and the outlines Mapillary linked to it. Feed `days[0].best` into the viewer with
|
|
5
|
+
* `lookAt={{ lngLat: feature.lngLat, outline: image.outline }}`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const useMapillaryMapFeatureImages: (featureId: string | null | undefined, { shape }?: {
|
|
8
|
+
shape?: TargetShape;
|
|
9
|
+
}) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").MapFeatureImages | null>, Error>;
|
|
10
|
+
/**
|
|
11
|
+
* Map features within `radiusMeters` of a point (Graph API), nearest first, e.g. the traffic
|
|
12
|
+
* lights and markings at a junction. `objectValues` accepts wildcards (`object--traffic-light--*`).
|
|
13
|
+
*/
|
|
14
|
+
export declare const useMapillaryMapFeaturesNear: (lngLat: LngLat | null | undefined, { radiusMeters, objectValues, enabled, }?: {
|
|
15
|
+
radiusMeters?: number;
|
|
16
|
+
objectValues?: string[];
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
}) => import("@tanstack/react-query").UseQueryResult<NoInfer<{
|
|
19
|
+
id: string;
|
|
20
|
+
value: string;
|
|
21
|
+
lngLat: LngLat;
|
|
22
|
+
firstSeenAt: number | null;
|
|
23
|
+
lastSeenAt: number | null;
|
|
24
|
+
facing: number | null;
|
|
25
|
+
imageIds: string[];
|
|
26
|
+
distanceMeters: number;
|
|
27
|
+
}[]>, Error>;
|
|
28
|
+
/**
|
|
29
|
+
* Detection outlines of one image, for `outlines` of the Mapillary viewer. An image has ~500
|
|
30
|
+
* detections; pass `filter` (e.g. `matchesAnyGroup(JUNCTION_DETECTION_GROUPS)`) and name it with
|
|
31
|
+
* `filterKey` so the cache can tell filters apart.
|
|
32
|
+
*/
|
|
33
|
+
export declare const useMapillaryImageDetections: (imageId: string | null | undefined, { filter, filterKey }?: {
|
|
34
|
+
filter?: (value: string) => boolean;
|
|
35
|
+
filterKey?: string;
|
|
36
|
+
}) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").MapillaryDetection[]>, Error>;
|