@osm-editor-kit/street-imagery-react 0.1.0-alpha.0 → 0.1.0-alpha.10

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.
Files changed (52) hide show
  1. package/README.md +186 -4
  2. package/dist/MapillaryPanel-FAXVZH37.js +2 -0
  3. package/dist/PanoramaxPanel-TWKTSOXM.js +2 -0
  4. package/dist/StreetLevelImagerySelectionOverlay.d.ts +22 -3
  5. package/dist/StreetLevelImagerySourcesAndLayers.d.ts +24 -1
  6. package/dist/StreetLevelImageryViewCone.d.ts +13 -1
  7. package/dist/StreetLevelImageryViewer.d.ts +23 -1
  8. package/dist/chunk-6EMFFIL2.js +147 -0
  9. package/dist/chunk-SGIB7AVC.js +309 -0
  10. package/dist/chunk-XQXYV6SR.js +466 -0
  11. package/dist/filters/PhotoDateRangeFilter.d.ts +46 -0
  12. package/dist/hooks/useAllProviderMapFeatures.d.ts +0 -4
  13. package/dist/hooks/useProviderData.d.ts +8 -3
  14. package/dist/i18n/PhotoCount.d.ts +12 -0
  15. package/dist/i18n/PhotoDate.d.ts +16 -0
  16. package/dist/i18n/StreetImageryLocaleProvider.d.ts +21 -0
  17. package/dist/i18n/messages.d.ts +45 -0
  18. package/dist/index.d.ts +19 -4
  19. package/dist/index.js +1542 -141
  20. package/dist/mapillary/MapillaryFeatureBar.d.ts +16 -0
  21. package/dist/mapillary/SelectedMapFeatureLayer.d.ts +19 -0
  22. package/dist/mapillary/featureBarLabels.d.ts +16 -0
  23. package/dist/mapillary/useMapillaryFeatures.d.ts +54 -0
  24. package/dist/openers/LocationPickOnMap.d.ts +10 -0
  25. package/dist/openers/useLocationPickStore.d.ts +9 -0
  26. package/dist/panels/MapillaryPanel.d.ts +19 -1
  27. package/dist/panels/PanoramaxPanel.d.ts +14 -4
  28. package/dist/panels/PanoramaxWebViewerPanel.d.ts +20 -0
  29. package/dist/panels/mapillaryLookAt.d.ts +41 -0
  30. package/dist/panels/panoramaxDataProvider.d.ts +44 -0
  31. package/dist/panels/panoramaxItem.d.ts +96 -0
  32. package/dist/panels/panoramaxLimitPanning.d.ts +54 -0
  33. package/dist/panels/panoramaxPhotoFromMetadata.d.ts +8 -0
  34. package/dist/panoramax-web-viewer/index.d.ts +1 -0
  35. package/dist/panoramax-web-viewer.js +314 -0
  36. package/dist/photo-details/PhotoDetailsDialog.d.ts +21 -0
  37. package/dist/photo-details/index.d.ts +6 -0
  38. package/dist/photo-details/labels.d.ts +33 -0
  39. package/dist/photo-details.js +235 -0
  40. package/dist/streetImageryClick.d.ts +4 -0
  41. package/dist/viewer-panels/index.d.ts +5 -0
  42. package/dist/viewer-panels.js +3 -0
  43. package/dist/viewpoints/FloatingPhotoViewer.d.ts +73 -0
  44. package/dist/viewpoints/ViewpointLayer.d.ts +17 -0
  45. package/dist/viewpoints/useViewSuggestions.d.ts +23 -0
  46. package/dist/viewpoints/useViewpointSessionStore.d.ts +35 -0
  47. package/dist/viewpoints/viewDirectionLabels.d.ts +4 -0
  48. package/package.json +37 -6
  49. package/dist/MapillaryPanel-QX6NESX7.js +0 -2
  50. package/dist/PanoramaxPanel-J6ANR6LB.js +0 -2
  51. package/dist/chunk-BS6LPDMY.js +0 -143
  52. package/dist/chunk-ZH3TPW4D.js +0 -165
@@ -0,0 +1,466 @@
1
+ import { useViewerActions } from './chunk-25UGGXIE.js';
2
+ import { getStreetImageryConfig, providerExternalLink } from '@osm-editor-kit/street-imagery';
3
+ import 'mapillary-js/dist/mapillary.css';
4
+ import { DataProviderBase, S2GeometryProvider, Viewer } from 'mapillary-js';
5
+ import { useRef, useState, useEffectEvent, useEffect } from 'react';
6
+ import { jsxs, jsx } from 'react/jsx-runtime';
7
+
8
+ // src/panels/panoramaxItem.ts
9
+ var DEFAULT_FIELD_OF_VIEW_DEG = 70;
10
+ var toMs = (iso) => {
11
+ const ms = iso ? Date.parse(iso) : Number.NaN;
12
+ return Number.isNaN(ms) ? null : ms;
13
+ };
14
+ var orientation = (item) => item.properties?.["pers:interior_orientation"];
15
+ var isPanoramaxItemSpherical = (item) => orientation(item)?.field_of_view === 360;
16
+ var panoramaxItemSize = (item) => orientation(item)?.sensor_array_dimensions ?? (isPanoramaxItemSpherical(item) ? [2, 1] : [4, 3]);
17
+ var panoramaxItemNeighbour = (item, rel) => item.links?.find((link) => link.rel === rel)?.id ?? null;
18
+ var SD_WIDTH = 2048;
19
+ var panoramaxItemImageUrl = (item) => {
20
+ const { hd, sd, thumb } = item.assets ?? {};
21
+ return sd?.href ?? hd?.href ?? thumb?.href ?? "";
22
+ };
23
+ var TILE_SIZE = 1024;
24
+ var tileSpan = ([width, height], z) => TILE_SIZE * 2 ** (Math.ceil(Math.log2(Math.max(width, height))) - z);
25
+ var panoramaxViewerSize = (item) => {
26
+ const [width, height] = panoramaxItemSize(item);
27
+ const scale = 2 ** Math.ceil(Math.log2(Math.max(width, height))) / Math.max(width, height);
28
+ return [Math.round(width * scale), Math.round(height * scale)];
29
+ };
30
+ var panoramaxLevelNeedsOriginal = (size, z) => size[0] / (tileSpan(size, z) / TILE_SIZE) > SD_WIDTH;
31
+ var panoramaxTilesOfLevel = (size, z) => {
32
+ const [width, height] = size;
33
+ const span = tileSpan(size, z);
34
+ const tiles = [];
35
+ for (let y = 0; y < Math.ceil(height / span); y++) {
36
+ for (let x = 0; x < Math.ceil(width / span); x++) {
37
+ tiles.push({ x, y });
38
+ }
39
+ }
40
+ return tiles;
41
+ };
42
+ var panoramaxTileRect = (size, tile, original) => {
43
+ const [width, height] = size;
44
+ const span = tileSpan(size, tile.z);
45
+ const x = tile.x * span;
46
+ const y = tile.y * span;
47
+ const cutWidth = Math.min(span, width - x);
48
+ const cutHeight = Math.min(span, height - y);
49
+ const scaleX = original.width / width;
50
+ const scaleY = original.height / height;
51
+ return {
52
+ x: x * scaleX,
53
+ y: y * scaleY,
54
+ width: cutWidth * scaleX,
55
+ height: cutHeight * scaleY,
56
+ outWidth: Math.max(1, Math.round(cutWidth / span * TILE_SIZE)),
57
+ outHeight: Math.max(1, Math.round(cutHeight / span * TILE_SIZE))
58
+ };
59
+ };
60
+ var panoramaxItemFocal = (item) => {
61
+ const fieldOfView = orientation(item)?.field_of_view || DEFAULT_FIELD_OF_VIEW_DEG;
62
+ const [width, height] = panoramaxItemSize(item);
63
+ const focalByWidth = 0.5 / Math.tan(Math.min(fieldOfView, 170) * Math.PI / 360);
64
+ return focalByWidth * (width / Math.max(width, height));
65
+ };
66
+ var creatorOf = (item) => item.properties?.["geovisio:producer"] ?? item.providers?.at(-1)?.name;
67
+ var panoramaxPhotoFromItem = (item) => {
68
+ const properties = item.properties ?? {};
69
+ const camera = orientation(item);
70
+ const cameraName = [camera?.camera_manufacturer, camera?.camera_model].filter(Boolean).join(" ");
71
+ const isPano = isPanoramaxItemSpherical(item);
72
+ const creatorName = creatorOf(item);
73
+ const uploadedAt = toMs(properties.created);
74
+ const licenseUrl = item.links?.find((link) => link.rel === "license")?.href;
75
+ const instance = item.links?.find((link) => link.rel === "via")?.instance_name;
76
+ const exifEntries = Object.entries(properties.exif ?? {}).filter(([, value]) => typeof value === "string" || typeof value === "number").map(([key, value]) => [key, String(value)]);
77
+ const details = {
78
+ ...properties.license ? { license: properties.license } : {},
79
+ ...licenseUrl ? { licenseUrl } : {},
80
+ ...properties.datetimetz ? { capturedAtLocal: properties.datetimetz } : {},
81
+ ...uploadedAt != null ? { uploadedAt } : {},
82
+ ...cameraName ? { camera: cameraName } : {},
83
+ ...!isPano && camera?.field_of_view ? { fieldOfViewDeg: camera.field_of_view } : {},
84
+ ...properties["quality:horizontal_accuracy"] != null ? { positionAccuracyMeters: properties["quality:horizontal_accuracy"] } : {},
85
+ ...instance ? { instance } : {},
86
+ ...camera?.focal_length != null ? { focalLengthMm: camera.focal_length } : {},
87
+ ...properties["original_file:name"] ? { originalFileName: properties["original_file:name"] } : {},
88
+ ...properties["original_file:size"] != null ? { originalFileSizeBytes: properties["original_file:size"] } : {},
89
+ ...properties["geovisio:rank_in_collection"] != null ? { rankInSequence: properties["geovisio:rank_in_collection"] } : {},
90
+ ...exifEntries.length > 0 ? { exif: Object.fromEntries(exifEntries) } : {}
91
+ };
92
+ return {
93
+ providerId: "panoramax",
94
+ photoId: item.id,
95
+ sequenceId: item.collection ?? null,
96
+ capturedAt: toMs(properties.datetime),
97
+ isPano,
98
+ heading: properties["view:azimuth"] ?? null,
99
+ lngLat: item.geometry.coordinates,
100
+ ...creatorName ? { creatorName } : {},
101
+ details
102
+ };
103
+ };
104
+
105
+ // src/panels/panoramaxDataProvider.ts
106
+ var IDS_PER_REQUEST = 40;
107
+ var SEQUENCE_PREFIX = "panoramax-around:";
108
+ var fetchFile = async (url) => {
109
+ const response = await fetch(url);
110
+ if (!response.ok) {
111
+ throw new Error(`Panoramax picture file: HTTP ${response.status}`);
112
+ }
113
+ return response;
114
+ };
115
+ var TILE_PREFIX = "panoramax-tile:";
116
+ var PanoramaxDataProvider = class extends DataProviderBase {
117
+ endpoint;
118
+ items = /* @__PURE__ */ new Map();
119
+ itemRequests = /* @__PURE__ */ new Map();
120
+ imageBuffers = /* @__PURE__ */ new Map();
121
+ pictures = /* @__PURE__ */ new Map();
122
+ constructor({ endpoint }) {
123
+ super(new S2GeometryProvider());
124
+ this.endpoint = endpoint.replace(/\/$/, "");
125
+ }
126
+ /** The Panoramax data of a picture the viewer has loaded. */
127
+ getItem(imageId) {
128
+ return this.items.get(imageId);
129
+ }
130
+ // Only the arrows to pictures nearby need these, and those need a 3D reconstruction.
131
+ getCoreImages(cellId) {
132
+ return Promise.resolve({ cell_id: cellId, images: [] });
133
+ }
134
+ async getImages(imageIds) {
135
+ const items = await this.loadItems(imageIds);
136
+ return items.map((item) => ({ node: this.toImageEnt(item), node_id: item.id }));
137
+ }
138
+ getSpatialImages(imageIds) {
139
+ return this.getImages(imageIds);
140
+ }
141
+ async getSequence(sequenceId) {
142
+ const imageId = sequenceId.slice(SEQUENCE_PREFIX.length);
143
+ const [item] = await this.loadItems([imageId]);
144
+ if (!item) {
145
+ throw new Error(`Panoramax picture ${imageId} not found`);
146
+ }
147
+ const around = [
148
+ panoramaxItemNeighbour(item, "prev"),
149
+ item.id,
150
+ panoramaxItemNeighbour(item, "next")
151
+ ];
152
+ return { id: sequenceId, image_ids: around.filter((id) => id != null) };
153
+ }
154
+ // The viewer asks for the same file more than once while it starts; one download serves all.
155
+ getImageBuffer(url) {
156
+ const running = this.imageBuffers.get(url);
157
+ if (running) {
158
+ return running;
159
+ }
160
+ const load = url.startsWith(TILE_PREFIX) ? this.cutTile(url) : fetchFile(url).then((response) => response.arrayBuffer());
161
+ const request = load.finally(() => this.imageBuffers.delete(url));
162
+ this.imageBuffers.set(url, request);
163
+ return request;
164
+ }
165
+ async getImageTiles({
166
+ imageId,
167
+ z
168
+ }) {
169
+ const [item] = await this.loadItems([imageId]);
170
+ const tiles = item ? panoramaxTilesOfLevel(panoramaxViewerSize(item), z) : [];
171
+ return {
172
+ node_id: imageId,
173
+ node: tiles.map(({ x, y }) => ({ url: `${TILE_PREFIX}${imageId}/${z}/${x}/${y}`, x, y, z }))
174
+ };
175
+ }
176
+ async cutTile(url) {
177
+ const [imageId = "", z, x, y] = url.slice(TILE_PREFIX.length).split("/");
178
+ const item = this.items.get(imageId);
179
+ if (!item) {
180
+ throw new Error(`Panoramax picture ${imageId} not loaded`);
181
+ }
182
+ const size = panoramaxViewerSize(item);
183
+ const { hd, sd } = item.assets ?? {};
184
+ const file = (panoramaxLevelNeedsOriginal(size, Number(z)) ? hd : sd) ?? hd ?? sd;
185
+ const original = await this.loadPicture(file?.href ?? "");
186
+ const rect = panoramaxTileRect(
187
+ size,
188
+ { x: Number(x), y: Number(y), z: Number(z) },
189
+ { width: original.width, height: original.height }
190
+ );
191
+ const canvas = new OffscreenCanvas(rect.outWidth, rect.outHeight);
192
+ canvas.getContext("2d")?.drawImage(
193
+ original,
194
+ rect.x,
195
+ rect.y,
196
+ rect.width,
197
+ rect.height,
198
+ 0,
199
+ 0,
200
+ rect.outWidth,
201
+ rect.outHeight
202
+ );
203
+ const blob = await canvas.convertToBlob({ type: "image/jpeg", quality: 0.9 });
204
+ return blob.arrayBuffer();
205
+ }
206
+ loadPicture(url) {
207
+ const known = this.pictures.get(url);
208
+ if (known) {
209
+ return known;
210
+ }
211
+ const picture = fetchFile(url).then((response) => response.blob()).then((blob) => createImageBitmap(blob));
212
+ this.pictures.set(url, picture);
213
+ picture.catch(() => this.pictures.delete(url));
214
+ for (const [oldUrl, old] of [...this.pictures].slice(0, -3)) {
215
+ this.pictures.delete(oldUrl);
216
+ void old.then((bitmap) => bitmap.close()).catch(() => {
217
+ });
218
+ }
219
+ return picture;
220
+ }
221
+ getMesh() {
222
+ return Promise.resolve({ faces: [], vertices: [] });
223
+ }
224
+ getCluster(url) {
225
+ return Promise.resolve({ id: url, points: {}, reference: { lat: 0, lng: 0, alt: 0 } });
226
+ }
227
+ async loadItems(imageIds) {
228
+ const missing = [
229
+ ...new Set(imageIds.filter((id) => !this.items.has(id) && !this.itemRequests.has(id)))
230
+ ];
231
+ for (let start = 0; start < missing.length; start += IDS_PER_REQUEST) {
232
+ const ids = missing.slice(start, start + IDS_PER_REQUEST);
233
+ const request = fetch(`${this.endpoint}/search?ids=${ids.join(",")}&limit=${ids.length}`).then((response) => {
234
+ if (!response.ok) {
235
+ throw new Error(`Panoramax search: HTTP ${response.status}`);
236
+ }
237
+ return response.json();
238
+ }).then((result) => {
239
+ for (const item of result.features ?? []) {
240
+ this.items.set(item.id, item);
241
+ }
242
+ }).finally(() => {
243
+ for (const id of ids) {
244
+ this.itemRequests.delete(id);
245
+ }
246
+ });
247
+ for (const id of ids) {
248
+ this.itemRequests.set(id, request);
249
+ }
250
+ }
251
+ await Promise.all(imageIds.map((id) => this.itemRequests.get(id)));
252
+ return imageIds.flatMap((id) => this.items.get(id) ?? []);
253
+ }
254
+ toImageEnt(item) {
255
+ const [lng, lat] = item.geometry.coordinates;
256
+ const [width, height] = panoramaxViewerSize(item);
257
+ const spherical = isPanoramaxItemSpherical(item);
258
+ const properties = item.properties ?? {};
259
+ const none = { id: "", url: "" };
260
+ return {
261
+ id: item.id,
262
+ geometry: { lat, lng },
263
+ sequence: { id: `${SEQUENCE_PREFIX}${item.id}` },
264
+ altitude: 0,
265
+ camera_type: spherical ? "spherical" : "perspective",
266
+ camera_parameters: spherical ? [] : [panoramaxItemFocal(item), 0, 0],
267
+ captured_at: Date.parse(properties.datetime ?? "") || 0,
268
+ compass_angle: properties["view:azimuth"] ?? 0,
269
+ cluster: none,
270
+ creator: { id: "", username: properties["geovisio:producer"] ?? "" },
271
+ // Panoramax serves its pictures upright.
272
+ exif_orientation: 1,
273
+ width,
274
+ height,
275
+ mesh: none,
276
+ owner: { id: "" },
277
+ thumb: {
278
+ id: item.id,
279
+ url: panoramaxItemImageUrl(item)
280
+ }
281
+ };
282
+ }
283
+ };
284
+ var PanoramaxPanel = ({
285
+ photo,
286
+ onPhotoSelected,
287
+ onEaseMapToPoint,
288
+ onViewerPhoto,
289
+ hideAttribution = false
290
+ }) => {
291
+ const containerRef = useRef(null);
292
+ const viewerRef = useRef(null);
293
+ const navigableRef = useRef(false);
294
+ const pendingImageIdRef = useRef(null);
295
+ const lastViewerPhotoIdRef = useRef(null);
296
+ const bearingRafRef = useRef(null);
297
+ const pendingBearingRef = useRef(null);
298
+ const actions = useViewerActions();
299
+ const initialPhotoIdRef = useRef(photo.photoId);
300
+ const [shownPhoto, setShownPhoto] = useState(null);
301
+ const emitPhotoSelected = useEffectEvent(onPhotoSelected);
302
+ const emitEaseMapToPoint = useEffectEvent(onEaseMapToPoint);
303
+ const emitViewerPhoto = useEffectEvent(
304
+ (viewerPhoto) => onViewerPhoto?.(viewerPhoto)
305
+ );
306
+ useEffect(
307
+ function resetViewerStoreOnProviderChange() {
308
+ return () => {
309
+ actions.reset();
310
+ };
311
+ },
312
+ [actions, photo.providerId]
313
+ );
314
+ useEffect(
315
+ function mountViewer() {
316
+ const container = containerRef.current;
317
+ if (!container) {
318
+ return;
319
+ }
320
+ const dataProvider = new PanoramaxDataProvider({
321
+ endpoint: `${getStreetImageryConfig().panoramaxApiBase}/api`
322
+ });
323
+ const viewer = new Viewer({
324
+ container,
325
+ dataProvider,
326
+ imageId: initialPhotoIdRef.current,
327
+ component: {
328
+ // The attribution of the viewer links to mapillary.com.
329
+ attribution: false,
330
+ // Load only the direct neighbours ahead.
331
+ cache: { depth: { sequence: 1, spherical: 0, step: 0, turn: 0 } },
332
+ cover: false,
333
+ sequence: { visible: true }
334
+ }
335
+ });
336
+ viewerRef.current = viewer;
337
+ lastViewerPhotoIdRef.current = initialPhotoIdRef.current;
338
+ const flushPendingMove = () => {
339
+ const pendingId = pendingImageIdRef.current;
340
+ if (!pendingId) {
341
+ return;
342
+ }
343
+ pendingImageIdRef.current = null;
344
+ void viewer.moveTo(pendingId).catch(() => {
345
+ });
346
+ };
347
+ const onNavigable = (event) => {
348
+ navigableRef.current = event.navigable;
349
+ if (event.navigable) {
350
+ flushPendingMove();
351
+ }
352
+ };
353
+ const onImage = (event) => {
354
+ const { image } = event;
355
+ lastViewerPhotoIdRef.current = image.id;
356
+ const item = dataProvider.getItem(image.id);
357
+ if (!item) {
358
+ return;
359
+ }
360
+ const viewerPhoto = panoramaxPhotoFromItem(item);
361
+ emitPhotoSelected({
362
+ provider: "panoramax",
363
+ sequenceId: viewerPhoto.sequenceId ?? `photo:${image.id}`,
364
+ photoId: image.id
365
+ });
366
+ setShownPhoto(viewerPhoto);
367
+ emitViewerPhoto(viewerPhoto);
368
+ actions.setPov({
369
+ lngLat: viewerPhoto.lngLat,
370
+ ...!viewerPhoto.isPano && viewerPhoto.heading != null ? { bearing: viewerPhoto.heading } : {}
371
+ });
372
+ emitEaseMapToPoint(...viewerPhoto.lngLat);
373
+ };
374
+ const flushBearing = () => {
375
+ bearingRafRef.current = null;
376
+ if (pendingBearingRef.current != null) {
377
+ actions.setPov({ bearing: pendingBearingRef.current });
378
+ pendingBearingRef.current = null;
379
+ }
380
+ void viewer.getFieldOfView().then((fov) => {
381
+ actions.setPov({ hfov: fov });
382
+ }).catch(() => {
383
+ });
384
+ };
385
+ const onBearing = (event) => {
386
+ pendingBearingRef.current = event.bearing;
387
+ if (bearingRafRef.current == null) {
388
+ bearingRafRef.current = requestAnimationFrame(flushBearing);
389
+ }
390
+ };
391
+ viewer.on("navigable", onNavigable);
392
+ viewer.on("image", onImage);
393
+ viewer.on("bearing", onBearing);
394
+ const resizeObserver = new ResizeObserver(() => {
395
+ viewer.resize();
396
+ });
397
+ resizeObserver.observe(container);
398
+ return () => {
399
+ if (bearingRafRef.current != null) {
400
+ cancelAnimationFrame(bearingRafRef.current);
401
+ bearingRafRef.current = null;
402
+ }
403
+ resizeObserver.disconnect();
404
+ viewer.off("navigable", onNavigable);
405
+ viewer.off("image", onImage);
406
+ viewer.off("bearing", onBearing);
407
+ viewer.remove();
408
+ viewerRef.current = null;
409
+ };
410
+ },
411
+ [actions]
412
+ );
413
+ useEffect(
414
+ function syncExternalPhotoSelection() {
415
+ const viewer = viewerRef.current;
416
+ if (!viewer || photo.photoId === lastViewerPhotoIdRef.current) {
417
+ return;
418
+ }
419
+ actions.reset();
420
+ lastViewerPhotoIdRef.current = photo.photoId;
421
+ if (navigableRef.current) {
422
+ void viewer.moveTo(photo.photoId).catch(() => {
423
+ });
424
+ } else {
425
+ pendingImageIdRef.current = photo.photoId;
426
+ }
427
+ },
428
+ [actions, photo.photoId]
429
+ );
430
+ const attribution = hideAttribution ? null : shownPhoto;
431
+ const license = attribution?.details?.license;
432
+ return /* @__PURE__ */ jsxs(
433
+ "div",
434
+ {
435
+ className: "relative min-h-48 overflow-hidden rounded-lg border border-slate-200 bg-slate-900",
436
+ style: { aspectRatio: "4 / 3" },
437
+ children: [
438
+ /* @__PURE__ */ jsx("div", { ref: containerRef, className: "h-full w-full" }),
439
+ attribution ? /* @__PURE__ */ jsxs("div", { className: "absolute right-0 bottom-0 flex gap-1.5 rounded-tl bg-black/50 px-1.5 py-0.5 text-[11px] leading-4 text-white", children: [
440
+ /* @__PURE__ */ jsx(
441
+ "a",
442
+ {
443
+ className: "underline-offset-2 hover:underline",
444
+ href: providerExternalLink(attribution),
445
+ rel: "noreferrer",
446
+ target: "_blank",
447
+ children: ["Panoramax", attribution.creatorName].filter(Boolean).join(" \xB7 ")
448
+ }
449
+ ),
450
+ license ? attribution.details?.licenseUrl ? /* @__PURE__ */ jsx(
451
+ "a",
452
+ {
453
+ className: "underline-offset-2 hover:underline",
454
+ href: attribution.details.licenseUrl,
455
+ rel: "noreferrer",
456
+ target: "_blank",
457
+ children: license
458
+ }
459
+ ) : /* @__PURE__ */ jsx("span", { children: license }) : null
460
+ ] }) : null
461
+ ]
462
+ }
463
+ );
464
+ };
465
+
466
+ export { PanoramaxDataProvider, PanoramaxPanel, panoramaxPhotoFromItem };
@@ -0,0 +1,46 @@
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
+ /**
35
+ * The "All dates" action next to the chosen range, which removes both limits. Default true;
36
+ * without it the user moves both handles to the ends.
37
+ */
38
+ allDatesAction?: boolean;
39
+ className?: string;
40
+ };
41
+ /**
42
+ * Date filter for photos after iD's photo age slider: one slider with two handles on a scale that
43
+ * gives recent years more room, marks that show how many photos there are at each time, lines at
44
+ * whole years, and optional fixed markers. Controlled: `value` in, `onChange` out.
45
+ */
46
+ export declare const PhotoDateRangeFilter: ({ value, onChange, capturedAt, yearLines, recommendedMaxAgeYears, markers, maxYears, now: nowProp, dateInputs, className, allDatesAction, }: PhotoDateRangeFilterProps) => import("react").JSX.Element;
@@ -2,7 +2,3 @@ import type { DateRange } from '@osm-editor-kit/street-imagery';
2
2
  import type { Bbox, NormalizedMapFeature } from '@osm-editor-kit/street-imagery';
3
3
  import { type ProviderId } from '@osm-editor-kit/street-imagery';
4
4
  export declare const useAllProviderMapFeatures: (providerIds: ProviderId[], bbox: Bbox | null, zoom: number, date?: DateRange) => NormalizedMapFeature[];
5
- export declare const useAllProviderMapFeaturesLoading: (providerIds: ProviderId[], bbox: Bbox | null, zoom: number) => {
6
- isLoading: boolean;
7
- isFetching: boolean;
8
- };
@@ -1,5 +1,10 @@
1
1
  import type { Bbox } from '@osm-editor-kit/street-imagery';
2
2
  import { type ProviderId } from '@osm-editor-kit/street-imagery';
3
- export declare const useProviderPhotos: (providerId: ProviderId, bbox: Bbox | null, zoom: number) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedPhoto[]>, Error>;
4
- export declare const useProviderSequences: (providerId: ProviderId, bbox: Bbox | null, zoom: number) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedSequence[]>, Error>;
5
- export declare const useProviderMapFeatures: (providerId: ProviderId, bbox: Bbox | null, zoom: number) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedMapFeature[]>, Error>;
3
+ /** Host limit on top of the provider's own minimum zooms: below it nothing is requested. */
4
+ type ProviderDataOptions = {
5
+ minZoom?: number;
6
+ };
7
+ export declare const useProviderPhotos: (providerId: ProviderId, bbox: Bbox | null, zoom: number, { minZoom }?: ProviderDataOptions) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedPhoto[]>, Error>;
8
+ export declare const useProviderSequences: (providerId: ProviderId, bbox: Bbox | null, zoom: number, { minZoom }?: ProviderDataOptions) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedSequence[]>, Error>;
9
+ export declare const useProviderMapFeatures: (providerId: ProviderId, bbox: Bbox | null, zoom: number, { minZoom }?: ProviderDataOptions) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@osm-editor-kit/street-imagery").NormalizedMapFeature[]>, Error>;
10
+ export {};
@@ -0,0 +1,12 @@
1
+ type PhotoCountProps = {
2
+ /** Position of the shown photo, starting at 1; leave out to show only the total. */
3
+ index?: number;
4
+ total: number;
5
+ className?: string;
6
+ };
7
+ /**
8
+ * "1/5" behind a small picture icon: this button has several photos and a click on it shows the
9
+ * next one.
10
+ */
11
+ export declare const PhotoCount: ({ index, total, className }: PhotoCountProps) => import("react").JSX.Element;
12
+ export {};
@@ -0,0 +1,16 @@
1
+ type PhotoDateProps = {
2
+ /** Capture time (ms); `null` shows "Unknown date". */
3
+ timestamp: number | null | undefined;
4
+ className?: string;
5
+ /**
6
+ * Capture time with the camera's UTC offset (`2025-03-08T15:40:35+01:00`). The tooltip then
7
+ * shows date and time as the camera's clock had it, instead of the date alone.
8
+ */
9
+ localDateTime?: string;
10
+ };
11
+ /**
12
+ * A date as month and year ("Aug 2026"). The tooltip has the full date and, on a second line,
13
+ * how long ago that is.
14
+ */
15
+ export declare const PhotoDate: ({ timestamp, className, localDateTime }: PhotoDateProps) => import("react").JSX.Element;
16
+ export {};
@@ -0,0 +1,21 @@
1
+ import { type StreetImageryLocale } from '@osm-editor-kit/street-imagery';
2
+ import { type ReactNode } from 'react';
3
+ import { type StreetImageryMessages } from './messages';
4
+ type StreetImageryI18n = {
5
+ locale: StreetImageryLocale;
6
+ messages: StreetImageryMessages;
7
+ };
8
+ /** Overrides per group, e.g. `{ feature: { name: germanSignName } }`. */
9
+ export type StreetImageryMessageOverrides = {
10
+ [Group in keyof StreetImageryMessages]?: StreetImageryMessages[Group] extends object ? StreetImageryMessages[Group] extends ((...args: never[]) => unknown) | readonly unknown[] ? StreetImageryMessages[Group] : Partial<StreetImageryMessages[Group]> : StreetImageryMessages[Group];
11
+ };
12
+ type StreetImageryLocaleProviderProps = {
13
+ /** Language of the package's texts and date formats. Without a provider: English. */
14
+ locale: StreetImageryLocale;
15
+ messages?: StreetImageryMessageOverrides;
16
+ children: ReactNode;
17
+ };
18
+ export declare const StreetImageryLocaleProvider: ({ locale, messages, children, }: StreetImageryLocaleProviderProps) => import("react").JSX.Element;
19
+ /** Locale and texts of the nearest `StreetImageryLocaleProvider`. */
20
+ export declare const useStreetImageryI18n: () => StreetImageryI18n;
21
+ export {};
@@ -0,0 +1,45 @@
1
+ import { type StreetImageryLocale, type ViewDirection, type ViewpointRole } from '@osm-editor-kit/street-imagery';
2
+ /**
3
+ * Every text the package's components show. Texts with values are plain functions, so a host app
4
+ * can override single entries from a constant or from its own i18n library.
5
+ */
6
+ export type StreetImageryMessages = {
7
+ viewer: {
8
+ regionLabel: string;
9
+ loading: string;
10
+ backInHistory: string;
11
+ forwardInHistory: string;
12
+ minimize: string;
13
+ close: string;
14
+ suggestedViews: string;
15
+ suggestedViewsHint: string;
16
+ noPhoto: (label: string) => string;
17
+ noMatchingPhoto: (label: string) => string;
18
+ nextPhotoOfView: (label: string) => string;
19
+ };
20
+ viewpointRole: Record<ViewpointRole, string>;
21
+ /** Eight compass points, clockwise from north: N, NE, E, SE, S, SW, W, NW. */
22
+ compass: readonly string[];
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
+ };
42
+ /** e.g. "Start, looking along the street"; `role` is the viewpoint's label. */
43
+ viewDirection: (role: string, kind: ViewDirection['kind']) => string;
44
+ };
45
+ export declare const STREET_IMAGERY_MESSAGES: Record<StreetImageryLocale, StreetImageryMessages>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { StreetLevelImagerySelectionOverlay, resolveSelectedSequence, } from './StreetLevelImagerySelectionOverlay';
1
+ export { BASE_COLOR, SELECTION_COLOR, StreetLevelImagerySelectionOverlay, resolveSelectedSequence, } from './StreetLevelImagerySelectionOverlay';
2
2
  export { StreetLevelImagerySourcesAndLayers, type PhotoFilter, } from './StreetLevelImagerySourcesAndLayers';
3
- export { StreetLevelImageryViewCone } from './StreetLevelImageryViewCone';
3
+ export { StreetLevelImageryViewCone, VIEW_SHAPE_FILL_OPACITY } from './StreetLevelImageryViewCone';
4
4
  export { StreetLevelImageryViewer } from './StreetLevelImageryViewer';
5
5
  export type { StreetImageryPhotoSelection } from './types';
6
6
  export { queryStreetImageryFeatures, streetImageryInteractiveLayerIds, type StreetImageryClickFeature, } from './streetImageryClick';
@@ -9,6 +9,21 @@ export * from './hooks/useAllProviderPhotos';
9
9
  export * from './hooks/useMapViewportBbox';
10
10
  export * from './hooks/useProviderData';
11
11
  export * from './hooks/usePhotoThumbnails';
12
- export { MapillaryPanel } from './panels/MapillaryPanel';
13
- export { PanoramaxPanel } from './panels/PanoramaxPanel';
14
12
  export * from './useViewerStore';
13
+ export { FloatingPhotoViewer, FloatingViewerInfoButton, type FloatingPhotoViewerProps, } from './viewpoints/FloatingPhotoViewer';
14
+ export { VIEWPOINT_DEFAULT_COLOR, VIEWPOINT_DIRECTION_LAYER_ID, ViewpointLayer, viewDirectionKeyFromFeatures, type ViewpointLayerProps, } from './viewpoints/ViewpointLayer';
15
+ export { viewSuggestionLabel } from './viewpoints/viewDirectionLabels';
16
+ export * from './viewpoints/useViewpointSessionStore';
17
+ export * from './viewpoints/useViewSuggestions';
18
+ export type { MapillaryLookAt, MapillaryViewerOutline } from './panels/mapillaryLookAt';
19
+ export * from './mapillary/useMapillaryFeatures';
20
+ export { LocationPickOnMap, type LocationPickOnMapProps } from './openers/LocationPickOnMap';
21
+ export * from './openers/useLocationPickStore';
22
+ export { SelectedMapFeatureLayer, type SelectedMapFeatureLayerProps, } from './mapillary/SelectedMapFeatureLayer';
23
+ export { MapillaryFeatureBar, type MapillaryFeatureBarProps } from './mapillary/MapillaryFeatureBar';
24
+ export { MAPILLARY_FEATURE_BAR_LABELS, type MapillaryFeatureBarLabels, } from './mapillary/featureBarLabels';
25
+ export { STREET_IMAGERY_MESSAGES, type StreetImageryMessages } from './i18n/messages';
26
+ export { StreetImageryLocaleProvider, useStreetImageryI18n, type StreetImageryMessageOverrides, } from './i18n/StreetImageryLocaleProvider';
27
+ export { PhotoDateRangeFilter, type PhotoDateMarker, type PhotoDateRangeFilterProps, } from './filters/PhotoDateRangeFilter';
28
+ export { PhotoDate } from './i18n/PhotoDate';
29
+ export { PhotoCount } from './i18n/PhotoCount';