@osm-editor-kit/street-imagery-react 0.1.0-alpha.7 → 0.1.0-alpha.8

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 CHANGED
@@ -117,10 +117,49 @@ const { data: detections } = useMapillaryImageDetections(photo.photoId, {
117
117
  <StreetLevelImageryViewer photo={photo} outlines={detections?.map((d) => ({ id: d.id, outline: d.outline }))} … />
118
118
  ```
119
119
 
120
- - Map layers: `filter.mapFeatureValue` limits which signs/objects are drawn (`matchesAnyGroup(SIGN_GROUPS)`).
120
+ - Map layers: `filter.mapFeatureValue` limits which signs/objects are drawn: `signGroupFilter(['parking', 'bike'])` for sign groups, or `matchesAnyGroup(…)`.
121
121
  - Suggested views: `useViewSuggestions(viewpoints, { sources: [mapillaryPhotoSource, streetViewPhotoSource] })`.
122
122
  - `turnMapillaryViewerTo` / `setMapillaryViewerOutlines` work on a mapillary-js `Viewer` you own.
123
123
 
124
+ ## A selected sign: viewer, bar and map highlight
125
+
126
+ Keep the selected sign (`featureId`) and its shown photo in your own state or URL:
127
+
128
+ ```tsx
129
+ const { data, shownImage, firstImage } = useSelectedMapillaryFeature({
130
+ featureId,
131
+ shownPhotoId, // the photo the viewer shows now
132
+ minCapturedAt: fromMs, // optional: open the best photo from this time on
133
+ })
134
+ // Open `firstImage` first: showPhoto(targetImageToPhoto(firstImage))
135
+ <MapillaryFeatureBar data={data} shownImage={shownImage} onShow={(image) => showPhoto(targetImageToPhoto(image))} />
136
+ <StreetLevelImageryViewer lookAt={{ lngLat: data.feature.lngLat, outline: shownImage?.outline, value: data.feature.value }} … />
137
+ // On the map, after StreetLevelImagerySourcesAndLayers: the sign's halo and a line from the camera
138
+ <SelectedMapFeatureLayer data={data} shownImage={shownImage} providers={providers} bbox={bbox} zoom={zoom} />
139
+ ```
140
+
141
+ A click on a sign dot: `queryStreetImageryFeatures(event).find((hit) => hit.kind === 'mapFeature')?.featureId`.
142
+
143
+ ## Date filter with a slider
144
+
145
+ `PhotoDateRangeFilter` is the date filter of iD's photo panel as a controlled component: one slider with two handles on a scale that gives recent years more room. It works on plain `{ from?, to? }` days (`YYYY-MM-DD`), the same `DateRange` the layers take as `filter.date`.
146
+
147
+ ```tsx
148
+ const { photos } = useAllProviderPhotos(providers, bbox, zoom) // no date: all photos in view
149
+
150
+ <PhotoDateRangeFilter
151
+ value={date}
152
+ onChange={setDate} // on release of a handle, not while dragging
153
+ capturedAt={photos.map((photo) => photo.capturedAt)} // marks: where photos are
154
+ yearLines={[1, 2, 3, 4]} // lines with a label; a click shows the last N years
155
+ recommendedMaxAgeYears={2} // this line is red
156
+ markers={[{ date: '2024-04-01', label: 'Survey start' }]} // dashed lines for fixed dates
157
+ dateInputs="collapsed" // exact days: 'collapsed' (default), 'open' or 'none'
158
+ />
159
+ ```
160
+
161
+ 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`.
162
+
124
163
  ## Pick a place on the map to open elsewhere
125
164
 
126
165
  A button arms a location opener (see `@osm-editor-kit/street-imagery`), the next map click opens the clicked place in that service and disarms; Escape cancels.
@@ -18,6 +18,21 @@ var en = {
18
18
  nextPhotoOfView: (label) => `${label} \u2014 click for the next photo`
19
19
  },
20
20
  unknownDate: "Unknown date",
21
+ dateFilter: {
22
+ allDates: "All dates",
23
+ since: (date) => `Since ${date}`,
24
+ until: (date) => `Until ${date}`,
25
+ between: (from, to) => `${from} \u2013 ${to}`,
26
+ yearsShort: (years) => `${years} y`,
27
+ lastYears: (years) => years === 1 ? "Show the last year" : `Show the last ${years} years`,
28
+ recommendedMaxAge: (years) => `Recommended: photos of the last ${years === 1 ? "year" : `${years} years`}`,
29
+ fromHandle: "Oldest photos to show",
30
+ toHandle: "Newest photos to show",
31
+ exactDates: "Exact dates",
32
+ from: "From",
33
+ to: "To",
34
+ photoCount: (count) => count === 1 ? "1 photo" : `${count} photos`
35
+ },
21
36
  viewpointRole: {
22
37
  here: "Here",
23
38
  "line-start": "Start",
@@ -59,6 +74,21 @@ var de = {
59
74
  nextPhotoOfView: (label) => `${label} \u2014 klicken f\xFCr das n\xE4chste Foto`
60
75
  },
61
76
  unknownDate: "Datum unbekannt",
77
+ dateFilter: {
78
+ allDates: "Alle Daten",
79
+ since: (date) => `Seit ${date}`,
80
+ until: (date) => `Bis ${date}`,
81
+ between: (from, to) => `${from} \u2013 ${to}`,
82
+ yearsShort: (years) => `${years} J.`,
83
+ lastYears: (years) => years === 1 ? "Das letzte Jahr zeigen" : `Die letzten ${years} Jahre zeigen`,
84
+ recommendedMaxAge: (years) => `Empfohlen: Fotos ${years === 1 ? "des letzten Jahres" : `der letzten ${years} Jahre`}`,
85
+ fromHandle: "\xC4lteste Fotos, die gezeigt werden",
86
+ toHandle: "Neueste Fotos, die gezeigt werden",
87
+ exactDates: "Genaue Daten",
88
+ from: "Von",
89
+ to: "Bis",
90
+ photoCount: (count) => count === 1 ? "1 Foto" : `${count} Fotos`
91
+ },
62
92
  viewpointRole: {
63
93
  here: "Hier",
64
94
  "line-start": "Anfang",
@@ -95,6 +125,7 @@ var DEFAULT_I18N = {
95
125
  var I18nContext = createContext(DEFAULT_I18N);
96
126
  var mergeMessages = (base, overrides) => overrides ? {
97
127
  unknownDate: overrides.unknownDate ?? base.unknownDate,
128
+ dateFilter: { ...base.dateFilter, ...overrides.dateFilter },
98
129
  viewer: { ...base.viewer, ...overrides.viewer },
99
130
  viewpointRole: { ...base.viewpointRole, ...overrides.viewpointRole },
100
131
  compass: overrides.compass?.length === 8 ? overrides.compass : base.compass,
@@ -0,0 +1,41 @@
1
+ import { type DateRange } from '@osm-editor-kit/street-imagery';
2
+ /** A fixed date to show in the chart, e.g. the start of a project or of a survey. */
3
+ export type PhotoDateMarker = {
4
+ /** A day as `YYYY-MM-DD`, or a time (ms). */
5
+ date: string | number;
6
+ label: string;
7
+ /** CSS color of line and label. Default red. */
8
+ color?: string;
9
+ };
10
+ export type PhotoDateRangeFilterProps = {
11
+ /** The chosen range, days as `YYYY-MM-DD`. No `from` and no `to`: all dates. */
12
+ value: DateRange;
13
+ /** Called when a handle is released, a year label is clicked or a date input changes. */
14
+ onChange: (range: DateRange) => void;
15
+ /**
16
+ * Capture times (ms) of the photos to show as marks above the slider, usually all photos in
17
+ * view before the date filter. Without it there are no marks.
18
+ */
19
+ capturedAt?: Iterable<number | null | undefined>;
20
+ /** Lines with a label at these ages in years; a click on a label shows that many years. */
21
+ yearLines?: readonly number[];
22
+ /** Its line is red; added to `yearLines` when it is not one of them. */
23
+ recommendedMaxAgeYears?: number;
24
+ markers?: readonly PhotoDateMarker[];
25
+ /** Years the slider spans. Default 10. */
26
+ maxYears?: number;
27
+ /** Reference time (ms). Default: the time of the first render. */
28
+ now?: number;
29
+ /**
30
+ * The two date inputs for exact days, in sync with the slider: folded away (default), open,
31
+ * or left out.
32
+ */
33
+ dateInputs?: 'collapsed' | 'open' | 'none';
34
+ className?: string;
35
+ };
36
+ /**
37
+ * Date filter for photos after iD's photo age slider: one slider with two handles on a scale that
38
+ * gives recent years more room, marks that show how many photos there are at each time, lines at
39
+ * whole years, and optional fixed markers. Controlled: `value` in, `onChange` out.
40
+ */
41
+ export declare const PhotoDateRangeFilter: ({ value, onChange, capturedAt, yearLines, recommendedMaxAgeYears, markers, maxYears, now: nowProp, dateInputs, className, }: PhotoDateRangeFilterProps) => import("react").JSX.Element;
@@ -21,6 +21,24 @@ export type StreetImageryMessages = {
21
21
  /** Eight compass points, clockwise from north: N, NE, E, SE, S, SW, W, NW. */
22
22
  compass: readonly string[];
23
23
  unknownDate: string;
24
+ dateFilter: {
25
+ allDates: string;
26
+ /** e.g. "Since 5 Oct 2024". */
27
+ since: (date: string) => string;
28
+ until: (date: string) => string;
29
+ between: (from: string, to: string) => string;
30
+ /** Short label below a year line, e.g. "2 y". */
31
+ yearsShort: (years: number) => string;
32
+ /** Tooltip of a year label; a click shows the photos of the last `years` years. */
33
+ lastYears: (years: number) => string;
34
+ recommendedMaxAge: (years: number) => string;
35
+ fromHandle: string;
36
+ toHandle: string;
37
+ exactDates: string;
38
+ from: string;
39
+ to: string;
40
+ photoCount: (count: number) => string;
41
+ };
24
42
  /** e.g. "Start, looking along the street"; `role` is the viewpoint's label. */
25
43
  viewDirection: (role: string, kind: ViewDirection['kind']) => string;
26
44
  };
package/dist/index.d.ts CHANGED
@@ -21,9 +21,11 @@ export { mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo, ty
21
21
  export * from './mapillary/useMapillaryFeatures';
22
22
  export { LocationPickOnMap, type LocationPickOnMapProps } from './openers/LocationPickOnMap';
23
23
  export * from './openers/useLocationPickStore';
24
+ export { SelectedMapFeatureLayer, type SelectedMapFeatureLayerProps, } from './mapillary/SelectedMapFeatureLayer';
24
25
  export { MapillaryFeatureBar, type MapillaryFeatureBarProps } from './mapillary/MapillaryFeatureBar';
25
26
  export { MAPILLARY_FEATURE_BAR_LABELS, type MapillaryFeatureBarLabels, } from './mapillary/featureBarLabels';
26
27
  export { STREET_IMAGERY_MESSAGES, type StreetImageryMessages } from './i18n/messages';
27
28
  export { StreetImageryLocaleProvider, useStreetImageryI18n, type StreetImageryMessageOverrides, } from './i18n/StreetImageryLocaleProvider';
29
+ export { PhotoDateRangeFilter, type PhotoDateMarker, type PhotoDateRangeFilterProps, } from './filters/PhotoDateRangeFilter';
28
30
  export { PhotoDate } from './i18n/PhotoDate';
29
31
  export { PhotoCount } from './i18n/PhotoCount';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
2
- export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
1
+ import { useStreetImageryI18n } from './chunk-ZAIXGYAH.js';
2
+ export { STREET_IMAGERY_MESSAGES, StreetImageryLocaleProvider, useStreetImageryI18n } from './chunk-ZAIXGYAH.js';
3
3
  export { MapillaryPanel, mapillaryViewFor, setMapillaryViewerOutlines, turnMapillaryViewerTo } from './chunk-53RA4OSJ.js';
4
4
  export { PanoramaxPanel } from './chunk-EQ2JKLGT.js';
5
5
  export { useViewerActions, useViewerBearing, useViewerHfov, useViewerLngLat } from './chunk-25UGGXIE.js';
6
- import { distanceMeters, sequencesToFeatureCollection, emptyLineCollection, adapterById, providerById, providerCoversBbox, viewConeGeoJson, viewShapeRadiusMeters, setStreetImageryConfig, viewfieldLayerId, photoLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, formatLocalDateTime, formatMonth, formatRelativeDate, formatDateTooltip, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, fetchImageDetections, findLocationOpener, mapillaryValueName, mapillaryIconUrl, openLocationInNewTab, alignLineToPoints, coneRadiusMeters, mapFeaturesToFeatureCollection, emptyPointCollection, buildMapFeatureLayerFilter, featureSourceId, photosToFeatureCollection, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
6
+ import { distanceMeters, sequencesToFeatureCollection, emptyLineCollection, adapterById, providerById, providerCoversBbox, viewConeGeoJson, viewShapeRadiusMeters, setStreetImageryConfig, viewfieldLayerId, photoLayerId, featureLayerId, PROVIDER_IDS, mapFeatureMatchesDateRange, isBrowserAvailableProvider, photoMatchesFilters, resolvePhotoPanoramaUrl, photoFullUrlQueryKey, resolvePhotoThumbnailUrl, photoThumbnailQueryKey, formatLocalDateTime, formatMonth, formatRelativeDate, formatDateTooltip, mapillaryPhotoSource, buildViewSuggestions, yearsToMs, SIGN_TARGET, fetchMapFeatureImages, fetchMapillaryMapFeaturesInBbox, bboxAround, fetchImageDetections, bestTargetImage, findLocationOpener, MAP_FEATURE_COLOR, mapillaryValueName, mapillaryIconUrl, dateRangeToSliderPositions, sliderPositionsToDateRange, dateSliderBins, dateSliderPosition, yearsAgoMs, parseIsoDateStartMs, openLocationInNewTab, formatDate, isIsoDate, alignLineToPoints, coneRadiusMeters, mapFeaturesToFeatureCollection, emptyPointCollection, buildMapFeatureLayerFilter, featureSourceId, photosToFeatureCollection, photosToViewfieldsFeatureCollection, emptyPolygonCollection, buildPhotoLayerFilter, photoSourceId, viewfieldSourceId, sequenceSourceId, sequenceLayerId, viewfieldLineLayerId, VIEWFIELD_MIN_ZOOM } from '@osm-editor-kit/street-imagery';
7
7
  import { Source, Layer, useMap } from 'react-map-gl/maplibre';
8
8
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
9
9
  import { lazy, useEffect, Fragment as Fragment$1, Suspense, useState, useRef, useEffectEvent } from 'react';
@@ -1484,6 +1484,20 @@ var useMapillaryImageDetections = (imageId, { filter, filterKey } = {}) => useQu
1484
1484
  enabled: !!imageId,
1485
1485
  staleTime: STALE_TIME
1486
1486
  });
1487
+ var useSelectedMapillaryFeature = ({
1488
+ featureId,
1489
+ shownPhotoId,
1490
+ minCapturedAt,
1491
+ shape
1492
+ }) => {
1493
+ const { data, isLoading, isError } = useMapillaryMapFeatureImages(featureId, { shape });
1494
+ const shownImage = data?.images.find((image) => image.id === shownPhotoId) ?? null;
1495
+ const firstImage = data ? bestTargetImage(data.images, data.feature.lngLat, {
1496
+ minCapturedAt: minCapturedAt ?? void 0,
1497
+ shape
1498
+ }) ?? null : null;
1499
+ return { data: data ?? null, isLoading, isError, shownImage, firstImage };
1500
+ };
1487
1501
  var useLocationPickStore = create()((set) => ({
1488
1502
  armedOpenerId: null,
1489
1503
  actions: {
@@ -1535,6 +1549,70 @@ var LocationPickOnMap = ({ onPick }) => {
1535
1549
  );
1536
1550
  return null;
1537
1551
  };
1552
+ var SelectedMapFeatureLayer = ({
1553
+ data,
1554
+ shownImage,
1555
+ providers,
1556
+ bbox,
1557
+ zoom,
1558
+ color = MAP_FEATURE_COLOR
1559
+ }) => {
1560
+ const drawnFeatures = useAllProviderMapFeatures(providers, bbox, zoom);
1561
+ const target = data ? drawnFeatures.find((feature) => feature.featureId === data.feature.id)?.lngLat ?? data.feature.lngLat : null;
1562
+ const camera = shownImage?.lngLat;
1563
+ const points = {
1564
+ type: "FeatureCollection",
1565
+ features: target ? [{ type: "Feature", properties: {}, geometry: { type: "Point", coordinates: target } }] : []
1566
+ };
1567
+ const line = {
1568
+ type: "FeatureCollection",
1569
+ features: target && camera ? [
1570
+ {
1571
+ type: "Feature",
1572
+ properties: {},
1573
+ geometry: { type: "LineString", coordinates: [camera, target] }
1574
+ }
1575
+ ] : []
1576
+ };
1577
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1578
+ /* @__PURE__ */ jsx(Source, { data: line, id: "selected-map-feature-line", type: "geojson" }),
1579
+ /* @__PURE__ */ jsx(
1580
+ Layer,
1581
+ {
1582
+ id: "selected-map-feature-line",
1583
+ paint: { "line-color": SELECTION_COLOR, "line-width": 1.5, "line-dasharray": [2, 1.5] },
1584
+ source: "selected-map-feature-line",
1585
+ type: "line"
1586
+ }
1587
+ ),
1588
+ /* @__PURE__ */ jsx(Source, { data: points, id: "selected-map-feature-point", type: "geojson" }),
1589
+ /* @__PURE__ */ jsx(
1590
+ Layer,
1591
+ {
1592
+ id: "selected-map-feature-disk",
1593
+ paint: {
1594
+ "circle-radius": 17,
1595
+ "circle-color": color,
1596
+ "circle-opacity": VIEW_SHAPE_FILL_OPACITY
1597
+ },
1598
+ source: "selected-map-feature-point",
1599
+ type: "circle"
1600
+ }
1601
+ ),
1602
+ /* @__PURE__ */ jsx(
1603
+ Layer,
1604
+ {
1605
+ id: "selected-map-feature-dot",
1606
+ paint: {
1607
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 1.8, 14, 3.6, 18, 4.8],
1608
+ "circle-color": color
1609
+ },
1610
+ source: "selected-map-feature-point",
1611
+ type: "circle"
1612
+ }
1613
+ )
1614
+ ] });
1615
+ };
1538
1616
  var MAPILLARY_FEATURE_BAR_LABELS = {
1539
1617
  en: {
1540
1618
  name: (value) => mapillaryValueName(value, "en"),
@@ -1630,5 +1708,233 @@ ${labels.dayPhotos(day.images.length, active && day.images.length > 1)}`,
1630
1708
  )
1631
1709
  ] });
1632
1710
  };
1711
+ var DEFAULT_YEAR_LINES = [1, 2, 3, 4];
1712
+ var BINS = 60;
1713
+ var STEP = 1e-3;
1714
+ var CHART_HEIGHT_PX = 36;
1715
+ var RED = "#dc2626";
1716
+ var HANDLE_CLASS = "pointer-events-none absolute inset-0 m-0 h-full w-full cursor-pointer appearance-none bg-transparent focus:outline-none [&::-webkit-slider-runnable-track]:appearance-none [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-slate-800 [&::-webkit-slider-thumb]:shadow [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-slate-800 [&::-moz-range-thumb]:shadow focus-visible:[&::-webkit-slider-thumb]:ring-2 focus-visible:[&::-webkit-slider-thumb]:ring-sky-500 focus-visible:[&::-moz-range-thumb]:ring-2 focus-visible:[&::-moz-range-thumb]:ring-sky-500";
1717
+ var percent = (position) => `${position * 100}%`;
1718
+ var PhotoDateRangeFilter = ({
1719
+ value,
1720
+ onChange,
1721
+ capturedAt,
1722
+ yearLines = DEFAULT_YEAR_LINES,
1723
+ recommendedMaxAgeYears,
1724
+ markers = [],
1725
+ maxYears,
1726
+ now: nowProp,
1727
+ dateInputs = "collapsed",
1728
+ className
1729
+ }) => {
1730
+ const { locale, messages } = useStreetImageryI18n();
1731
+ const text = messages.dateFilter;
1732
+ const [startNow] = useState(() => Date.now());
1733
+ const now = nowProp ?? startNow;
1734
+ const scale = { now, maxYears };
1735
+ const [dragged, setDragged] = useState(null);
1736
+ const fromInput = useRef(null);
1737
+ const toInput = useRef(null);
1738
+ const positions = dragged ?? dateRangeToSliderPositions(value, scale);
1739
+ const shownRange = dragged ? sliderPositionsToDateRange(dragged, scale) : value;
1740
+ const readPositions = (moved) => {
1741
+ const from = Number(fromInput.current?.value ?? positions.from);
1742
+ const to = Number(toInput.current?.value ?? positions.to);
1743
+ return moved === "from" ? { from: Math.min(from, to), to } : { from, to: Math.max(from, to) };
1744
+ };
1745
+ const commitOnRelease = (moved) => (input) => {
1746
+ if (!input) {
1747
+ return void 0;
1748
+ }
1749
+ const commit = () => {
1750
+ setDragged(null);
1751
+ const next = sliderPositionsToDateRange(readPositions(moved), scale);
1752
+ const other = moved === "from" ? "to" : "from";
1753
+ onChange({ ...next, [other]: value[other] });
1754
+ };
1755
+ input.addEventListener("change", commit);
1756
+ return () => {
1757
+ input.removeEventListener("change", commit);
1758
+ };
1759
+ };
1760
+ const trackDrag = (moved, event) => {
1761
+ if (event.type === "input") {
1762
+ setDragged(readPositions(moved));
1763
+ }
1764
+ };
1765
+ const bins = capturedAt ? dateSliderBins(capturedAt, scale, BINS) : [];
1766
+ const maxCount = Math.max(1, ...bins);
1767
+ const years = [
1768
+ ...new Set(recommendedMaxAgeYears ? [...yearLines, recommendedMaxAgeYears] : yearLines)
1769
+ ].map((count) => ({ count, position: dateSliderPosition(yearsAgoMs(count, now), scale) })).filter(({ position }) => position > 0 && position < 1);
1770
+ const shownMarkers = markers.map((marker) => {
1771
+ const time = typeof marker.date === "number" ? marker.date : parseIsoDateStartMs(marker.date);
1772
+ return { ...marker, position: dateSliderPosition(time, scale) };
1773
+ }).filter(({ position }) => position > 0 && position < 1);
1774
+ const day = (isoDate) => formatDate(parseIsoDateStartMs(isoDate), locale);
1775
+ const rangeLabel = shownRange.from && shownRange.to ? text.between(day(shownRange.from), day(shownRange.to)) : shownRange.from ? text.since(day(shownRange.from)) : shownRange.to ? text.until(day(shownRange.to)) : text.allDates;
1776
+ const setYears = (count) => {
1777
+ onChange({ from: new Date(yearsAgoMs(count, now)).toISOString().slice(0, 10) });
1778
+ };
1779
+ const setDay = (key, input) => {
1780
+ if (input === "") {
1781
+ onChange({ ...value, [key]: void 0 });
1782
+ } else if (isIsoDate(input)) {
1783
+ onChange({ ...value, [key]: input });
1784
+ }
1785
+ };
1786
+ const isAllDates = !shownRange.from && !shownRange.to;
1787
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1788
+ /* @__PURE__ */ jsx("div", { className: "mx-2", children: /* @__PURE__ */ jsxs(
1789
+ "div",
1790
+ {
1791
+ className: "relative",
1792
+ style: { height: CHART_HEIGHT_PX, marginTop: shownMarkers.length > 0 ? 14 : 0 },
1793
+ children: [
1794
+ /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "absolute inset-0 flex items-end", children: bins.map((count, index) => {
1795
+ const start = index / BINS;
1796
+ const inRange = start + 1 / BINS > positions.from && start < positions.to;
1797
+ return /* @__PURE__ */ jsx(
1798
+ "div",
1799
+ {
1800
+ className: `flex-1 rounded-t-[1px] ${inRange ? "bg-slate-500" : "bg-slate-300"}`,
1801
+ style: {
1802
+ height: count === 0 ? 0 : Math.max(2, Math.sqrt(count / maxCount) * CHART_HEIGHT_PX)
1803
+ },
1804
+ title: count > 0 ? text.photoCount(count) : void 0
1805
+ },
1806
+ index
1807
+ );
1808
+ }) }),
1809
+ years.map(({ count, position }) => /* @__PURE__ */ jsx(
1810
+ "div",
1811
+ {
1812
+ "aria-hidden": true,
1813
+ className: "pointer-events-none absolute inset-y-0 w-px -translate-x-1/2",
1814
+ style: {
1815
+ left: percent(position),
1816
+ backgroundColor: count === recommendedMaxAgeYears ? RED : "rgb(148 163 184 / 0.7)"
1817
+ }
1818
+ },
1819
+ count
1820
+ )),
1821
+ shownMarkers.map((marker) => /* @__PURE__ */ jsx(
1822
+ "div",
1823
+ {
1824
+ className: "pointer-events-none absolute inset-y-0 w-0 border-l-2 border-dashed",
1825
+ style: { left: percent(marker.position), borderColor: marker.color ?? RED },
1826
+ children: /* @__PURE__ */ jsx(
1827
+ "span",
1828
+ {
1829
+ className: "absolute -top-3.5 -translate-x-1/2 text-[10px] leading-none whitespace-nowrap",
1830
+ style: { color: marker.color ?? RED },
1831
+ children: marker.label
1832
+ }
1833
+ )
1834
+ },
1835
+ `${marker.date}-${marker.label}`
1836
+ ))
1837
+ ]
1838
+ }
1839
+ ) }),
1840
+ /* @__PURE__ */ jsxs("div", { className: "relative h-4", children: [
1841
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-2 top-1/2 h-1 -translate-y-1/2 rounded-full bg-slate-200", children: /* @__PURE__ */ jsx(
1842
+ "div",
1843
+ {
1844
+ className: "absolute inset-y-0 rounded-full bg-slate-700",
1845
+ style: {
1846
+ left: percent(positions.from),
1847
+ right: percent(1 - positions.to)
1848
+ }
1849
+ }
1850
+ ) }),
1851
+ /* @__PURE__ */ jsx(
1852
+ "input",
1853
+ {
1854
+ "aria-label": text.fromHandle,
1855
+ "aria-valuetext": shownRange.from ? day(shownRange.from) : text.allDates,
1856
+ className: HANDLE_CLASS,
1857
+ max: 1,
1858
+ min: 0,
1859
+ onChange: (event) => {
1860
+ trackDrag("from", event.nativeEvent);
1861
+ },
1862
+ ref: (input) => {
1863
+ fromInput.current = input;
1864
+ return commitOnRelease("from")(input);
1865
+ },
1866
+ step: STEP,
1867
+ type: "range",
1868
+ value: positions.from
1869
+ }
1870
+ ),
1871
+ /* @__PURE__ */ jsx(
1872
+ "input",
1873
+ {
1874
+ "aria-label": text.toHandle,
1875
+ "aria-valuetext": shownRange.to ? day(shownRange.to) : text.allDates,
1876
+ className: HANDLE_CLASS,
1877
+ max: 1,
1878
+ min: 0,
1879
+ onChange: (event) => {
1880
+ trackDrag("to", event.nativeEvent);
1881
+ },
1882
+ ref: (input) => {
1883
+ toInput.current = input;
1884
+ return commitOnRelease("to")(input);
1885
+ },
1886
+ step: STEP,
1887
+ type: "range",
1888
+ value: positions.to
1889
+ }
1890
+ )
1891
+ ] }),
1892
+ /* @__PURE__ */ jsx("div", { className: "relative mx-2 h-4", children: years.map(({ count, position }) => /* @__PURE__ */ jsx(
1893
+ "button",
1894
+ {
1895
+ className: `absolute top-0 -translate-x-1/2 rounded px-0.5 text-[10px] leading-4 whitespace-nowrap hover:underline ${count === recommendedMaxAgeYears ? "font-semibold text-red-600" : "text-slate-500 hover:text-slate-900"}`,
1896
+ onClick: () => {
1897
+ setYears(count);
1898
+ },
1899
+ style: { left: percent(position) },
1900
+ title: count === recommendedMaxAgeYears ? text.recommendedMaxAge(count) : text.lastYears(count),
1901
+ type: "button",
1902
+ children: text.yearsShort(count)
1903
+ },
1904
+ count
1905
+ )) }),
1906
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-baseline justify-between gap-2 text-xs", children: [
1907
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-700", children: rangeLabel }),
1908
+ isAllDates ? null : /* @__PURE__ */ jsx(
1909
+ "button",
1910
+ {
1911
+ className: "shrink-0 font-medium text-slate-600 underline decoration-slate-300 underline-offset-2 hover:text-slate-900",
1912
+ onClick: () => {
1913
+ onChange({});
1914
+ },
1915
+ type: "button",
1916
+ children: text.allDates
1917
+ }
1918
+ )
1919
+ ] }),
1920
+ dateInputs === "none" ? null : /* @__PURE__ */ jsxs("details", { className: "mt-1 text-xs text-slate-600", open: dateInputs === "open" || void 0, children: [
1921
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer select-none hover:text-slate-900", children: text.exactDates }),
1922
+ /* @__PURE__ */ jsx("div", { className: "mt-1.5 grid grid-cols-2 gap-2", children: ["from", "to"].map((key) => /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1", children: [
1923
+ text[key],
1924
+ /* @__PURE__ */ jsx(
1925
+ "input",
1926
+ {
1927
+ className: "rounded-md border border-slate-200 px-2 py-1.5 text-sm text-slate-800",
1928
+ onChange: (event) => {
1929
+ setDay(key, event.target.value);
1930
+ },
1931
+ type: "date",
1932
+ value: value[key] ?? ""
1933
+ }
1934
+ )
1935
+ ] }, key)) })
1936
+ ] })
1937
+ ] });
1938
+ };
1633
1939
 
1634
- export { BASE_COLOR, FloatingPhotoViewer, FloatingViewerInfoButton, LocationPickOnMap, MAPILLARY_FEATURE_BAR_LABELS, MapillaryFeatureBar, PhotoCount, PhotoDate, SELECTION_COLOR, StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, VIEW_SHAPE_FILL_OPACITY, ViewpointLayer, getViewpointSession, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useActiveDirectionKey, useAllProviderMapFeatures, useAllProviderPhotos, useArmedLocationOpenerId, useCanGoBack, useCanGoForward, useCurrentHistoryEntry, useLocationPickActions, useMapViewportBbox, useMapillaryImageDetections, useMapillaryMapFeatureImages, useMapillaryMapFeaturesNear, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences, useViewSuggestions, useViewpointLine, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel };
1940
+ export { BASE_COLOR, FloatingPhotoViewer, FloatingViewerInfoButton, LocationPickOnMap, MAPILLARY_FEATURE_BAR_LABELS, MapillaryFeatureBar, PhotoCount, PhotoDate, PhotoDateRangeFilter, SELECTION_COLOR, SelectedMapFeatureLayer, StreetLevelImagerySelectionOverlay, StreetLevelImagerySourcesAndLayers, StreetLevelImageryViewCone, StreetLevelImageryViewer, VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, VIEW_SHAPE_FILL_OPACITY, ViewpointLayer, getViewpointSession, queryStreetImageryFeatures, resolveSelectedSequence, streetImageryInteractiveLayerIds, useActiveDirectionKey, useAllProviderMapFeatures, useAllProviderPhotos, useArmedLocationOpenerId, useCanGoBack, useCanGoForward, useCurrentHistoryEntry, useLocationPickActions, useMapViewportBbox, useMapillaryImageDetections, useMapillaryMapFeatureImages, useMapillaryMapFeaturesNear, usePhotoFullUrl, usePhotoThumbnail, useProviderMapFeatures, useProviderPhotos, useProviderSequences, useSelectedMapillaryFeature, useViewSuggestions, useViewpointLine, useViewpoints, viewDirectionKeyFromFeatures, viewSuggestionLabel };
@@ -0,0 +1,19 @@
1
+ import { type Bbox, type MapFeatureImages, type ProviderId, type TargetImage } from '@osm-editor-kit/street-imagery';
2
+ export type SelectedMapFeatureLayerProps = {
3
+ /** From `useSelectedMapillaryFeature`. Nothing is drawn without it. */
4
+ data: MapFeatureImages | null;
5
+ /** The shown photo of the feature: a dotted line runs from its camera to the feature. */
6
+ shownImage?: TargetImage | null;
7
+ /** The providers whose map features are drawn, to ring the dot that is on the map. */
8
+ providers: ProviderId[];
9
+ bbox: Bbox | null;
10
+ zoom: number;
11
+ /** Color of the dot and disk. Default: the map features' purple. */
12
+ color?: string;
13
+ };
14
+ /**
15
+ * The selected sign or object on the map: its dot on a light disk, and a dotted line to it from
16
+ * the camera position of the shown photo (Mapillary's computed position, which the feature was
17
+ * located from). Draw it after `StreetLevelImagerySourcesAndLayers`.
18
+ */
19
+ export declare const SelectedMapFeatureLayer: ({ data, shownImage, providers, bbox, zoom, color, }: SelectedMapFeatureLayerProps) => import("react").JSX.Element;
@@ -34,3 +34,21 @@ export declare const useMapillaryImageDetections: (imageId: string | null | unde
34
34
  filter?: (value: string) => boolean;
35
35
  filterKey?: string;
36
36
  }) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").MapillaryDetection[]>, Error>;
37
+ /**
38
+ * A selected Mapillary map feature (sign, object) for a host's feature + photo selection: all its
39
+ * photos by day, the photo that is shown (`shownPhotoId`), and the one to open first — the best of
40
+ * the newest day, from within `minCapturedAt` (ms) when set. Feed `data` and `shownImage` to
41
+ * `MapillaryFeatureBar` and `SelectedMapFeatureLayer`, and `shownImage.outline` to `lookAt`.
42
+ */
43
+ export declare const useSelectedMapillaryFeature: ({ featureId, shownPhotoId, minCapturedAt, shape, }: {
44
+ featureId: string | null | undefined;
45
+ shownPhotoId?: string | null;
46
+ minCapturedAt?: number | null;
47
+ shape?: TargetShape;
48
+ }) => {
49
+ data: NonNullable<NoInfer<import("@osm-editor-kit/street-imagery").MapFeatureImages | null>> | null;
50
+ isLoading: boolean;
51
+ isError: boolean;
52
+ shownImage: import("@osm-editor-kit/street-imagery").TargetImage | null;
53
+ firstImage: import("@osm-editor-kit/street-imagery").TargetImage | null;
54
+ };
@@ -1,4 +1,4 @@
1
- import { useStreetImageryI18n } from './chunk-EE7V3OIZ.js';
1
+ import { useStreetImageryI18n } from './chunk-ZAIXGYAH.js';
2
2
  import { providerById, formatLocalDateTime, formatDate, formatRelativeDate } from '@osm-editor-kit/street-imagery';
3
3
  import { useRef, useEffect } from 'react';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osm-editor-kit/street-imagery-react",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "React MapLibre layers and photo viewers for street-level imagery providers.",
5
5
  "homepage": "https://github.com/osmberlin/street-space-editor/tree/main/packages/street-imagery-react",
6
6
  "license": "MIT",
@@ -56,7 +56,7 @@
56
56
  "zustand": "^5.0.7"
57
57
  },
58
58
  "peerDependencies": {
59
- "@osm-editor-kit/street-imagery": "^0.1.0-alpha.6",
59
+ "@osm-editor-kit/street-imagery": "^0.1.0-alpha.7",
60
60
  "@panoramax/web-viewer": "^5",
61
61
  "@tanstack/react-query": "^5",
62
62
  "mapillary-js": "^4",