@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/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
- export { MapillaryPanel } from './chunk-BS6LPDMY.js';
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 photoCollection = zoom >= adapter.minZoom ? photosToFeatureCollection(photos) : emptyPointCollection();
218
- const viewfieldCollection = showViewfields ? photosToViewfieldsFeatureCollection(visiblePhotos, zoom) : emptyPolygonCollection();
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 featureCollection = zoom >= adapter.minZoom ? mapFeaturesToFeatureCollection(features) : emptyPointCollection();
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-QX6NESX7.js').then((module) => ({
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
- export { StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useAllProviderMapFeatures, useAllProviderMapFeaturesLoading, useAllProviderPhotos, useMapViewportBbox, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences };
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>;