@osm-editor-kit/street-imagery 0.1.0-alpha.0
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 +29 -0
- package/dist/config.d.ts +10 -0
- package/dist/data/geojson.d.ts +7 -0
- package/dist/filters/searchFilters.d.ts +19 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1815 -0
- package/dist/map/viewCone.d.ts +4 -0
- package/dist/map/viewfields.d.ts +24 -0
- package/dist/providers/adapters/kartaview.d.ts +18 -0
- package/dist/providers/adapters/lookaround.d.ts +17 -0
- package/dist/providers/adapters/mapilio.d.ts +7 -0
- package/dist/providers/adapters/mapillary-map-features.d.ts +4 -0
- package/dist/providers/adapters/mapillary-signs.d.ts +4 -0
- package/dist/providers/adapters/mapillary.d.ts +5 -0
- package/dist/providers/adapters/panoramax.d.ts +6 -0
- package/dist/providers/adapters/streetside.d.ts +16 -0
- package/dist/providers/adapters/streetview.d.ts +16 -0
- package/dist/providers/adapters/vegbilder.d.ts +14 -0
- package/dist/providers/fetchMvt.d.ts +5 -0
- package/dist/providers/mapillaryShared.d.ts +6 -0
- package/dist/providers/model.d.ts +67 -0
- package/dist/providers/registry.d.ts +29 -0
- package/dist/providers/tileCache.d.ts +9 -0
- package/dist/providers/tileMath.d.ts +16 -0
- package/dist/viewer/clickRadius.d.ts +4 -0
- package/dist/viewer/externalLinks.d.ts +5 -0
- package/dist/viewer/groupClickedPhotos.d.ts +20 -0
- package/dist/viewer/photoThumbnails.d.ts +17 -0
- package/dist/viewer/streetsidePreview.d.ts +8 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1815 @@
|
|
|
1
|
+
import { VectorTile } from '@mapbox/vector-tile';
|
|
2
|
+
import { PbfReader } from 'pbf';
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
var DEFAULT_PANORAMAX_API_BASE = "https://api.panoramax.xyz";
|
|
6
|
+
var activeConfig = null;
|
|
7
|
+
var createStreetImageryConfig = (input) => ({
|
|
8
|
+
mapillaryToken: input.mapillaryToken,
|
|
9
|
+
panoramaxApiBase: (input.panoramaxApiBase ?? DEFAULT_PANORAMAX_API_BASE).replace(/\/$/, "")
|
|
10
|
+
});
|
|
11
|
+
var setStreetImageryConfig = (config) => {
|
|
12
|
+
activeConfig = config;
|
|
13
|
+
};
|
|
14
|
+
var getStreetImageryConfig = () => {
|
|
15
|
+
if (!activeConfig) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Street imagery config is not set. Call setStreetImageryConfig(createStreetImageryConfig(...)) before use."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return activeConfig;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/data/geojson.ts
|
|
24
|
+
var photosToFeatureCollection = (photos) => ({
|
|
25
|
+
type: "FeatureCollection",
|
|
26
|
+
features: photos.map((photo) => ({
|
|
27
|
+
type: "Feature",
|
|
28
|
+
geometry: {
|
|
29
|
+
type: "Point",
|
|
30
|
+
coordinates: photo.lngLat
|
|
31
|
+
},
|
|
32
|
+
properties: {
|
|
33
|
+
providerId: photo.providerId,
|
|
34
|
+
photoId: photo.photoId,
|
|
35
|
+
sequenceId: photo.sequenceId,
|
|
36
|
+
capturedAt: photo.capturedAt,
|
|
37
|
+
isPano: photo.isPano,
|
|
38
|
+
heading: photo.heading
|
|
39
|
+
}
|
|
40
|
+
}))
|
|
41
|
+
});
|
|
42
|
+
var mapFeaturesToFeatureCollection = (features) => ({
|
|
43
|
+
type: "FeatureCollection",
|
|
44
|
+
features: features.map((feature) => ({
|
|
45
|
+
type: "Feature",
|
|
46
|
+
geometry: {
|
|
47
|
+
type: "Point",
|
|
48
|
+
coordinates: feature.lngLat
|
|
49
|
+
},
|
|
50
|
+
properties: {
|
|
51
|
+
providerId: feature.providerId,
|
|
52
|
+
featureId: feature.featureId,
|
|
53
|
+
value: feature.value,
|
|
54
|
+
firstSeenAt: feature.firstSeenAt,
|
|
55
|
+
lastSeenAt: feature.lastSeenAt
|
|
56
|
+
}
|
|
57
|
+
}))
|
|
58
|
+
});
|
|
59
|
+
var sequencesToFeatureCollection = (sequences) => ({
|
|
60
|
+
type: "FeatureCollection",
|
|
61
|
+
features: sequences.map((sequence) => ({
|
|
62
|
+
type: "Feature",
|
|
63
|
+
geometry: sequence.geometry,
|
|
64
|
+
properties: {
|
|
65
|
+
providerId: sequence.providerId,
|
|
66
|
+
sequenceId: sequence.sequenceId,
|
|
67
|
+
capturedAt: sequence.capturedAt,
|
|
68
|
+
isPano: sequence.isPano
|
|
69
|
+
}
|
|
70
|
+
}))
|
|
71
|
+
});
|
|
72
|
+
var emptyPointCollection = () => ({
|
|
73
|
+
type: "FeatureCollection",
|
|
74
|
+
features: []
|
|
75
|
+
});
|
|
76
|
+
var emptyLineCollection = () => ({
|
|
77
|
+
type: "FeatureCollection",
|
|
78
|
+
features: []
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/filters/searchFilters.ts
|
|
82
|
+
var DAY_MS = 864e5;
|
|
83
|
+
var parseIsoDateStartMs = (isoDate) => {
|
|
84
|
+
const [year, month, day] = isoDate.split("-").map(Number);
|
|
85
|
+
return Date.UTC(year, month - 1, day);
|
|
86
|
+
};
|
|
87
|
+
var parseIsoDateEndMs = (isoDate) => parseIsoDateStartMs(isoDate) + DAY_MS - 1;
|
|
88
|
+
var normalizeDateRange = (date) => {
|
|
89
|
+
if (!date?.from && !date?.to) {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
const { from, to } = date;
|
|
93
|
+
if (from && to && from > to) {
|
|
94
|
+
return { from: to, to: from };
|
|
95
|
+
}
|
|
96
|
+
return date;
|
|
97
|
+
};
|
|
98
|
+
var photoTypesFilter = (photoTypes) => {
|
|
99
|
+
if (!photoTypes || photoTypes.length === 0) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
const unique = [...new Set(photoTypes)];
|
|
103
|
+
if (unique.length >= 2) {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
return unique;
|
|
107
|
+
};
|
|
108
|
+
var photoMatchesPhotoTypes = (photo, photoTypes) => {
|
|
109
|
+
const active = photoTypesFilter(photoTypes);
|
|
110
|
+
if (!active) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
const [onlyType] = active;
|
|
114
|
+
if (onlyType === "flat") {
|
|
115
|
+
return photo.isPano === false;
|
|
116
|
+
}
|
|
117
|
+
return photo.isPano === true;
|
|
118
|
+
};
|
|
119
|
+
var photoMatchesDateRange = (photo, date) => {
|
|
120
|
+
const range = normalizeDateRange(date);
|
|
121
|
+
if (!range) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const capturedAt = photo.capturedAt;
|
|
125
|
+
if (capturedAt == null) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (range.from && capturedAt < parseIsoDateStartMs(range.from)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (range.to && capturedAt > parseIsoDateEndMs(range.to)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
136
|
+
var photoMatchesFilters = (photo, photoTypes, date) => photoMatchesPhotoTypes(photo, photoTypes) && photoMatchesDateRange(photo, date);
|
|
137
|
+
var mapFeatureMatchesDateRange = (feature, date) => {
|
|
138
|
+
const range = normalizeDateRange(date);
|
|
139
|
+
if (!range) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (range.from) {
|
|
143
|
+
const lastSeenAt = feature.lastSeenAt;
|
|
144
|
+
if (lastSeenAt == null || lastSeenAt < parseIsoDateStartMs(range.from)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (range.to) {
|
|
149
|
+
const firstSeenAt = feature.firstSeenAt;
|
|
150
|
+
if (firstSeenAt == null || firstSeenAt > parseIsoDateEndMs(range.to)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
};
|
|
156
|
+
var photoTypeLayerFilter = (photoTypes) => {
|
|
157
|
+
const active = photoTypesFilter(photoTypes);
|
|
158
|
+
if (!active) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const [onlyType] = active;
|
|
162
|
+
return onlyType === "flat" ? ["==", ["get", "isPano"], false] : ["==", ["get", "isPano"], true];
|
|
163
|
+
};
|
|
164
|
+
var photoDateLayerFilter = (date) => {
|
|
165
|
+
const range = normalizeDateRange(date);
|
|
166
|
+
if (!range) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const clauses = [];
|
|
170
|
+
if (range.from) {
|
|
171
|
+
clauses.push([">=", ["get", "capturedAt"], parseIsoDateStartMs(range.from)]);
|
|
172
|
+
}
|
|
173
|
+
if (range.to) {
|
|
174
|
+
clauses.push(["<=", ["get", "capturedAt"], parseIsoDateEndMs(range.to)]);
|
|
175
|
+
}
|
|
176
|
+
if (clauses.length === 0) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return clauses.length === 1 ? clauses[0] : ["all", ...clauses];
|
|
180
|
+
};
|
|
181
|
+
var MATCH_ALL = ["boolean", true];
|
|
182
|
+
var buildPhotoLayerFilter = (photoTypes, date) => {
|
|
183
|
+
const clauses = [photoTypeLayerFilter(photoTypes), photoDateLayerFilter(date)].filter(
|
|
184
|
+
(clause) => clause != null
|
|
185
|
+
);
|
|
186
|
+
if (clauses.length === 0) {
|
|
187
|
+
return MATCH_ALL;
|
|
188
|
+
}
|
|
189
|
+
return clauses.length === 1 ? clauses[0] : ["all", ...clauses];
|
|
190
|
+
};
|
|
191
|
+
var buildMapFeatureLayerFilter = (date) => {
|
|
192
|
+
const range = normalizeDateRange(date);
|
|
193
|
+
if (!range) {
|
|
194
|
+
return MATCH_ALL;
|
|
195
|
+
}
|
|
196
|
+
const clauses = [];
|
|
197
|
+
if (range.from) {
|
|
198
|
+
clauses.push([">=", ["get", "lastSeenAt"], parseIsoDateStartMs(range.from)]);
|
|
199
|
+
}
|
|
200
|
+
if (range.to) {
|
|
201
|
+
clauses.push(["<=", ["get", "firstSeenAt"], parseIsoDateEndMs(range.to)]);
|
|
202
|
+
}
|
|
203
|
+
if (clauses.length === 0) {
|
|
204
|
+
return MATCH_ALL;
|
|
205
|
+
}
|
|
206
|
+
return clauses.length === 1 ? clauses[0] : ["all", ...clauses];
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// src/map/viewCone.ts
|
|
210
|
+
var ARC_SEGMENTS = 24;
|
|
211
|
+
var METERS_PER_DEGREE_LAT = 111320;
|
|
212
|
+
var normalizeBearing = (bearingDeg) => (bearingDeg % 360 + 360) % 360;
|
|
213
|
+
var offsetMeters = (lng, lat, bearingDeg, distanceMeters) => {
|
|
214
|
+
const bearingRad = bearingDeg * Math.PI / 180;
|
|
215
|
+
const northMeters = Math.cos(bearingRad) * distanceMeters;
|
|
216
|
+
const eastMeters = Math.sin(bearingRad) * distanceMeters;
|
|
217
|
+
const cosLat = Math.cos(lat * Math.PI / 180);
|
|
218
|
+
const metersPerDegreeLng = METERS_PER_DEGREE_LAT * Math.max(cosLat, 1e-6);
|
|
219
|
+
return [lng + eastMeters / metersPerDegreeLng, lat + northMeters / METERS_PER_DEGREE_LAT];
|
|
220
|
+
};
|
|
221
|
+
var coneRadiusMeters = (zoom) => 50 * 2 ** (14 - zoom);
|
|
222
|
+
var viewConeGeoJson = (lngLat, bearingDeg, fovDeg, radiusMeters) => {
|
|
223
|
+
const [lng, lat] = lngLat;
|
|
224
|
+
const centerBearing = normalizeBearing(bearingDeg);
|
|
225
|
+
const halfFov = Math.max(fovDeg, 0) / 2;
|
|
226
|
+
const startBearing = centerBearing - halfFov;
|
|
227
|
+
const coordinates = [[lng, lat]];
|
|
228
|
+
for (let index = 0; index <= ARC_SEGMENTS; index += 1) {
|
|
229
|
+
const segmentBearing = startBearing + index / ARC_SEGMENTS * fovDeg;
|
|
230
|
+
coordinates.push(offsetMeters(lng, lat, segmentBearing, radiusMeters));
|
|
231
|
+
}
|
|
232
|
+
coordinates.push([lng, lat]);
|
|
233
|
+
return {
|
|
234
|
+
type: "Feature",
|
|
235
|
+
properties: {},
|
|
236
|
+
geometry: {
|
|
237
|
+
type: "Polygon",
|
|
238
|
+
coordinates: [coordinates]
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/map/viewfields.ts
|
|
244
|
+
var FLAT_VIEWFIELD_FOV_DEG = 55;
|
|
245
|
+
var PANO_VIEWFIELD_FOV_DEG = 360;
|
|
246
|
+
var METERS_PER_DEGREE_LAT2 = 111320;
|
|
247
|
+
var normalizeBearing2 = (bearingDeg) => (bearingDeg % 360 + 360) % 360;
|
|
248
|
+
var offsetMeters2 = (lng, lat, bearingDeg, distanceMeters) => {
|
|
249
|
+
const bearingRad = bearingDeg * Math.PI / 180;
|
|
250
|
+
const northMeters = Math.cos(bearingRad) * distanceMeters;
|
|
251
|
+
const eastMeters = Math.sin(bearingRad) * distanceMeters;
|
|
252
|
+
const cosLat = Math.cos(lat * Math.PI / 180);
|
|
253
|
+
const metersPerDegreeLng = METERS_PER_DEGREE_LAT2 * Math.max(cosLat, 1e-6);
|
|
254
|
+
return [lng + eastMeters / metersPerDegreeLng, lat + northMeters / METERS_PER_DEGREE_LAT2];
|
|
255
|
+
};
|
|
256
|
+
var flatViewfieldTriangle = (lngLat, bearingDeg, fovDeg, radiusMeters) => {
|
|
257
|
+
const [lng, lat] = lngLat;
|
|
258
|
+
const center = normalizeBearing2(bearingDeg);
|
|
259
|
+
const half = Math.max(fovDeg, 0) / 2;
|
|
260
|
+
const left = offsetMeters2(lng, lat, center - half, radiusMeters);
|
|
261
|
+
const right = offsetMeters2(lng, lat, center + half, radiusMeters);
|
|
262
|
+
return {
|
|
263
|
+
type: "Feature",
|
|
264
|
+
properties: {},
|
|
265
|
+
geometry: {
|
|
266
|
+
type: "Polygon",
|
|
267
|
+
coordinates: [[[lng, lat], left, right, [lng, lat]]]
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
var photosToViewfieldsFeatureCollection = (photos, zoom) => {
|
|
272
|
+
const radius = coneRadiusMeters(zoom);
|
|
273
|
+
const features = [];
|
|
274
|
+
for (const photo of photos) {
|
|
275
|
+
const isPano = photo.isPano === true;
|
|
276
|
+
if (!isPano && photo.heading == null) continue;
|
|
277
|
+
const props = {
|
|
278
|
+
providerId: photo.providerId,
|
|
279
|
+
photoId: photo.photoId,
|
|
280
|
+
sequenceId: photo.sequenceId,
|
|
281
|
+
capturedAt: photo.capturedAt,
|
|
282
|
+
isPano: photo.isPano,
|
|
283
|
+
heading: photo.heading
|
|
284
|
+
};
|
|
285
|
+
if (isPano) {
|
|
286
|
+
const bearing = photo.heading ?? 0;
|
|
287
|
+
const disk = viewConeGeoJson(photo.lngLat, bearing, PANO_VIEWFIELD_FOV_DEG, radius);
|
|
288
|
+
features.push({ ...disk, properties: props });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const triangle = flatViewfieldTriangle(
|
|
292
|
+
photo.lngLat,
|
|
293
|
+
photo.heading,
|
|
294
|
+
FLAT_VIEWFIELD_FOV_DEG,
|
|
295
|
+
radius
|
|
296
|
+
);
|
|
297
|
+
features.push({ ...triangle, properties: props });
|
|
298
|
+
}
|
|
299
|
+
return { type: "FeatureCollection", features };
|
|
300
|
+
};
|
|
301
|
+
var emptyPolygonCollection = () => ({
|
|
302
|
+
type: "FeatureCollection",
|
|
303
|
+
features: []
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// src/providers/model.ts
|
|
307
|
+
var PROVIDER_IDS = [
|
|
308
|
+
"mapillary",
|
|
309
|
+
"panoramax",
|
|
310
|
+
"kartaview",
|
|
311
|
+
"mapilio",
|
|
312
|
+
"streetside",
|
|
313
|
+
"vegbilder",
|
|
314
|
+
"streetview",
|
|
315
|
+
"lookaround",
|
|
316
|
+
"mapillary-signs",
|
|
317
|
+
"mapillary-map-features"
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
// src/providers/tileCache.ts
|
|
321
|
+
var MAX_RESOLVED_ENTRIES = 200;
|
|
322
|
+
var pendingTiles = /* @__PURE__ */ new Map();
|
|
323
|
+
var resolvedTiles = /* @__PURE__ */ new Map();
|
|
324
|
+
var getTileCacheKey = (providerId, tile) => `${providerId}:${tile.z}:${tile.x}:${tile.y}`;
|
|
325
|
+
var subscribe = (key, entry, signal) => {
|
|
326
|
+
entry.activeSubscribers += 1;
|
|
327
|
+
const onAbort = () => {
|
|
328
|
+
entry.activeSubscribers -= 1;
|
|
329
|
+
if (entry.activeSubscribers === 0 && !entry.settled) {
|
|
330
|
+
entry.controller.abort();
|
|
331
|
+
pendingTiles.delete(key);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
if (signal.aborted) {
|
|
335
|
+
onAbort();
|
|
336
|
+
} else {
|
|
337
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
var touchResolved = (key, value) => {
|
|
341
|
+
resolvedTiles.delete(key);
|
|
342
|
+
resolvedTiles.set(key, value);
|
|
343
|
+
while (resolvedTiles.size > MAX_RESOLVED_ENTRIES) {
|
|
344
|
+
const oldestKey = resolvedTiles.keys().next().value;
|
|
345
|
+
if (oldestKey === void 0) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
resolvedTiles.delete(oldestKey);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var fetchTileCached = async (key, fetcher, signal) => {
|
|
352
|
+
if (resolvedTiles.has(key)) {
|
|
353
|
+
const value = resolvedTiles.get(key);
|
|
354
|
+
touchResolved(key, value);
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
const existing = pendingTiles.get(key);
|
|
358
|
+
if (existing) {
|
|
359
|
+
subscribe(key, existing, signal);
|
|
360
|
+
return existing.promise;
|
|
361
|
+
}
|
|
362
|
+
const controller = new AbortController();
|
|
363
|
+
const entry = {
|
|
364
|
+
promise: void 0,
|
|
365
|
+
controller,
|
|
366
|
+
activeSubscribers: 0,
|
|
367
|
+
settled: false
|
|
368
|
+
};
|
|
369
|
+
entry.promise = fetcher(controller.signal).then(
|
|
370
|
+
(value) => {
|
|
371
|
+
entry.settled = true;
|
|
372
|
+
if (pendingTiles.get(key) === entry) {
|
|
373
|
+
pendingTiles.delete(key);
|
|
374
|
+
touchResolved(key, value);
|
|
375
|
+
}
|
|
376
|
+
return value;
|
|
377
|
+
},
|
|
378
|
+
(error) => {
|
|
379
|
+
entry.settled = true;
|
|
380
|
+
if (pendingTiles.get(key) === entry) {
|
|
381
|
+
pendingTiles.delete(key);
|
|
382
|
+
}
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
pendingTiles.set(key, entry);
|
|
387
|
+
subscribe(key, entry, signal);
|
|
388
|
+
return entry.promise;
|
|
389
|
+
};
|
|
390
|
+
var collectSettledTiles = async (promises) => {
|
|
391
|
+
const results = await Promise.allSettled(promises);
|
|
392
|
+
const fulfilled = results.filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
393
|
+
if (results.length > 0 && fulfilled.length === 0) {
|
|
394
|
+
const firstRejected = results.find(
|
|
395
|
+
(result) => result.status === "rejected"
|
|
396
|
+
);
|
|
397
|
+
throw firstRejected?.reason ?? new Error("All tile fetches failed");
|
|
398
|
+
}
|
|
399
|
+
return fulfilled;
|
|
400
|
+
};
|
|
401
|
+
var clearTileCache = () => {
|
|
402
|
+
pendingTiles.clear();
|
|
403
|
+
resolvedTiles.clear();
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/providers/tileMath.ts
|
|
407
|
+
var TILE_SIZE = 256;
|
|
408
|
+
var lonToTileX = (lon, z) => Math.floor((lon + 180) / 360 * 2 ** z);
|
|
409
|
+
var latToTileY = (lat, z) => {
|
|
410
|
+
const latRad = lat * Math.PI / 180;
|
|
411
|
+
return Math.floor(
|
|
412
|
+
(1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * 2 ** z
|
|
413
|
+
);
|
|
414
|
+
};
|
|
415
|
+
var tileToLon = (x, z) => x / 2 ** z * 360 - 180;
|
|
416
|
+
var tileToLat = (y, z) => {
|
|
417
|
+
const n = Math.PI - 2 * Math.PI * y / 2 ** z;
|
|
418
|
+
return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
|
|
419
|
+
};
|
|
420
|
+
var tileBbox = (tile) => {
|
|
421
|
+
const west = tileToLon(tile.x, tile.z);
|
|
422
|
+
const east = tileToLon(tile.x + 1, tile.z);
|
|
423
|
+
const north = tileToLat(tile.y, tile.z);
|
|
424
|
+
const south = tileToLat(tile.y + 1, tile.z);
|
|
425
|
+
return [west, south, east, north];
|
|
426
|
+
};
|
|
427
|
+
var clampZoom = (zoom, min, max) => Math.min(max, Math.max(min, Math.floor(zoom)));
|
|
428
|
+
var isNullIslandTile = (tile) => {
|
|
429
|
+
const [west, south, east, north] = tileBbox(tile);
|
|
430
|
+
const centerLon = (west + east) / 2;
|
|
431
|
+
const centerLat = (south + north) / 2;
|
|
432
|
+
return Math.abs(centerLon) < 1 && Math.abs(centerLat) < 1;
|
|
433
|
+
};
|
|
434
|
+
var tilesForBbox = (bbox, z, options) => {
|
|
435
|
+
const [west, south, east, north] = bbox;
|
|
436
|
+
const tileCount = 2 ** z;
|
|
437
|
+
let minX = lonToTileX(west, z);
|
|
438
|
+
let maxX = lonToTileX(east, z);
|
|
439
|
+
if (minX > maxX || maxX - minX + 1 >= tileCount) {
|
|
440
|
+
minX = 0;
|
|
441
|
+
maxX = tileCount - 1;
|
|
442
|
+
}
|
|
443
|
+
const minY = Math.max(0, Math.min(tileCount - 1, latToTileY(north, z)));
|
|
444
|
+
const maxY = Math.max(0, Math.min(tileCount - 1, latToTileY(south, z)));
|
|
445
|
+
const tiles = [];
|
|
446
|
+
for (let x = minX; x <= maxX; x += 1) {
|
|
447
|
+
const wrappedX = (x % tileCount + tileCount) % tileCount;
|
|
448
|
+
for (let y = minY; y <= maxY; y += 1) {
|
|
449
|
+
const tile = { z, x: wrappedX, y };
|
|
450
|
+
if (options?.skipNullIsland && isNullIslandTile(tile)) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
tiles.push(tile);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return tiles;
|
|
457
|
+
};
|
|
458
|
+
var bboxIntersects = (a, b) => a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
|
|
459
|
+
var tilePixelSizeAtZoom = (zoom) => TILE_SIZE * 2 ** zoom;
|
|
460
|
+
|
|
461
|
+
// src/providers/adapters/kartaview.ts
|
|
462
|
+
var API_URL = "https://kartaview.org/1.0/list/nearby-photos/";
|
|
463
|
+
var TILE_ZOOM = 14;
|
|
464
|
+
var RESULTS_PER_PAGE = 1e3;
|
|
465
|
+
var kartaviewImageUrl = (imagePath) => {
|
|
466
|
+
const normalizedPath = imagePath.replace(/^\//, "");
|
|
467
|
+
const storageMatch = normalizedPath.match(/^storage(\d+)\/(.*)$/);
|
|
468
|
+
const storageUrl = storageMatch ? `https://storage${storageMatch[1]}.openstreetcam.org/${storageMatch[2]}` : `https://kartaview.org/${normalizedPath}`;
|
|
469
|
+
return `https://cdn.kartaview.org/pr:sharp/${btoa(storageUrl).replace(/=+$/, "")}`;
|
|
470
|
+
};
|
|
471
|
+
var maxPageAtZoom = (z) => {
|
|
472
|
+
if (z < 15) {
|
|
473
|
+
return 2;
|
|
474
|
+
}
|
|
475
|
+
if (z === 15) {
|
|
476
|
+
return 5;
|
|
477
|
+
}
|
|
478
|
+
if (z === 16) {
|
|
479
|
+
return 10;
|
|
480
|
+
}
|
|
481
|
+
if (z === 17) {
|
|
482
|
+
return 20;
|
|
483
|
+
}
|
|
484
|
+
if (z === 18) {
|
|
485
|
+
return 40;
|
|
486
|
+
}
|
|
487
|
+
return 80;
|
|
488
|
+
};
|
|
489
|
+
var parseKartaviewDate = (value) => {
|
|
490
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
const parsed = Date.parse(value);
|
|
494
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
495
|
+
};
|
|
496
|
+
var normalizeKartaviewItem = (item) => {
|
|
497
|
+
const id = item.id;
|
|
498
|
+
if (id == null) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
const lng = +(item.lng ?? Number.NaN);
|
|
502
|
+
const lat = +(item.lat ?? Number.NaN);
|
|
503
|
+
if (Number.isNaN(lng) || Number.isNaN(lat)) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
const headingRaw = item.heading;
|
|
507
|
+
const heading = headingRaw == null ? null : typeof headingRaw === "number" ? headingRaw : Number.parseFloat(String(headingRaw));
|
|
508
|
+
const sequenceIndexRaw = item.sequence_index;
|
|
509
|
+
const sequenceIndex = sequenceIndexRaw == null ? void 0 : typeof sequenceIndexRaw === "number" ? sequenceIndexRaw : Number.parseInt(String(sequenceIndexRaw), 10);
|
|
510
|
+
const thumbPath = item.lth_name ?? item.name;
|
|
511
|
+
const thumbUrl = typeof thumbPath === "string" && thumbPath.length > 0 ? kartaviewImageUrl(thumbPath) : void 0;
|
|
512
|
+
const fullUrl = typeof item.name === "string" && item.name.length > 0 ? kartaviewImageUrl(item.name) : void 0;
|
|
513
|
+
return {
|
|
514
|
+
photoId: String(id),
|
|
515
|
+
sequenceId: item.sequence_id != null ? String(item.sequence_id) : null,
|
|
516
|
+
sequenceIndex: sequenceIndex != null && !Number.isNaN(sequenceIndex) ? sequenceIndex : void 0,
|
|
517
|
+
capturedAt: parseKartaviewDate(item.shot_date ?? item.date_added),
|
|
518
|
+
isPano: null,
|
|
519
|
+
heading: heading != null && !Number.isNaN(heading) ? heading : null,
|
|
520
|
+
lngLat: [lng, lat],
|
|
521
|
+
thumbUrl,
|
|
522
|
+
fullUrl
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
var MAX_RADIUS_METERS = 1400;
|
|
526
|
+
var tileCenterAndRadius = (tile) => {
|
|
527
|
+
const [west, south, east, north] = tileBbox(tile);
|
|
528
|
+
const lng = (west + east) / 2;
|
|
529
|
+
const lat = (south + north) / 2;
|
|
530
|
+
const metersPerDegreeLat = 111320;
|
|
531
|
+
const halfHeight = (north - south) / 2 * metersPerDegreeLat;
|
|
532
|
+
const halfWidth = (east - west) / 2 * metersPerDegreeLat * Math.cos(lat * Math.PI / 180);
|
|
533
|
+
const radius = Math.ceil(Math.hypot(halfWidth, halfHeight));
|
|
534
|
+
return { lng, lat, radius: Math.min(radius, MAX_RADIUS_METERS) };
|
|
535
|
+
};
|
|
536
|
+
var fetchKartaviewTilePhotos = async (tile, maxPages, signal) => {
|
|
537
|
+
const [west, south, east, north] = tileBbox(tile);
|
|
538
|
+
const { lng, lat, radius } = tileCenterAndRadius(tile);
|
|
539
|
+
const photos = [];
|
|
540
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
541
|
+
const body = new URLSearchParams({
|
|
542
|
+
ipp: String(RESULTS_PER_PAGE),
|
|
543
|
+
page: String(page),
|
|
544
|
+
coordinate: `${lat},${lng}`,
|
|
545
|
+
radius: String(radius)
|
|
546
|
+
});
|
|
547
|
+
const response = await fetch(API_URL, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
signal,
|
|
550
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
551
|
+
body: body.toString()
|
|
552
|
+
});
|
|
553
|
+
if (!response.ok) {
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
const data = await response.json();
|
|
557
|
+
if (data.status?.httpCode !== 200) {
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
const items = data.currentPageItems ?? [];
|
|
561
|
+
for (const item of items) {
|
|
562
|
+
const normalized = normalizeKartaviewItem(item);
|
|
563
|
+
if (!normalized) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const [photoLng, photoLat] = normalized.lngLat;
|
|
567
|
+
if (photoLng < west || photoLng > east || photoLat < south || photoLat > north) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
photos.push({ providerId: "kartaview", ...normalized });
|
|
571
|
+
}
|
|
572
|
+
if (items.length < RESULTS_PER_PAGE) {
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return photos;
|
|
577
|
+
};
|
|
578
|
+
var fetchKartaviewTile = async (tile, maxPages, signal) => {
|
|
579
|
+
const key = getTileCacheKey(`kartaview:${maxPages}`, tile);
|
|
580
|
+
return fetchTileCached(
|
|
581
|
+
key,
|
|
582
|
+
(innerSignal) => fetchKartaviewTilePhotos(tile, maxPages, innerSignal),
|
|
583
|
+
signal
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
var fetchPhotos = async (bbox, zoom, signal) => {
|
|
587
|
+
const maxPages = Math.min(maxPageAtZoom(zoom), 5);
|
|
588
|
+
const tiles = tilesForBbox(bbox, TILE_ZOOM, { skipNullIsland: true });
|
|
589
|
+
const tileResults = await collectSettledTiles(
|
|
590
|
+
tiles.map((tile) => fetchKartaviewTile(tile, maxPages, signal))
|
|
591
|
+
);
|
|
592
|
+
return tileResults.flat();
|
|
593
|
+
};
|
|
594
|
+
var kartaviewAdapter = {
|
|
595
|
+
id: "kartaview",
|
|
596
|
+
kind: "photo",
|
|
597
|
+
label: "KartaView",
|
|
598
|
+
color: "#2563EB",
|
|
599
|
+
minZoom: 12,
|
|
600
|
+
fetchPhotos
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// src/providers/adapters/lookaround.ts
|
|
604
|
+
var DEFAULT_LOOK_AROUND_ALTITUDE_M = 50;
|
|
605
|
+
var bytesToBase64 = (bytes) => {
|
|
606
|
+
let binary = "";
|
|
607
|
+
for (const byte of bytes) {
|
|
608
|
+
binary += String.fromCharCode(byte);
|
|
609
|
+
}
|
|
610
|
+
return btoa(binary);
|
|
611
|
+
};
|
|
612
|
+
var encodeLookAroundMvs = (lat, lng, altitudeMeters = DEFAULT_LOOK_AROUND_ALTITUDE_M) => {
|
|
613
|
+
const inner = new Uint8Array(27);
|
|
614
|
+
const view = new DataView(inner.buffer);
|
|
615
|
+
let offset = 0;
|
|
616
|
+
for (const [field, value] of [
|
|
617
|
+
[1, lat],
|
|
618
|
+
[2, lng],
|
|
619
|
+
[3, altitudeMeters]
|
|
620
|
+
]) {
|
|
621
|
+
inner[offset] = field << 3 | 1;
|
|
622
|
+
offset += 1;
|
|
623
|
+
view.setFloat64(offset, value, true);
|
|
624
|
+
offset += 8;
|
|
625
|
+
}
|
|
626
|
+
const raw = new Uint8Array(2 + inner.length);
|
|
627
|
+
raw[0] = 10;
|
|
628
|
+
raw[1] = inner.length;
|
|
629
|
+
raw.set(inner, 2);
|
|
630
|
+
return bytesToBase64(raw);
|
|
631
|
+
};
|
|
632
|
+
var lookAroundDeepLink = (lat, lng) => {
|
|
633
|
+
const params = new URLSearchParams({
|
|
634
|
+
coordinate: `${lat},${lng}`,
|
|
635
|
+
_mvs: encodeLookAroundMvs(lat, lng)
|
|
636
|
+
});
|
|
637
|
+
return `https://maps.apple.com/look-around?${params.toString()}`;
|
|
638
|
+
};
|
|
639
|
+
var lookaroundAdapter = {
|
|
640
|
+
id: "lookaround",
|
|
641
|
+
kind: "photo",
|
|
642
|
+
label: "Apple Look Around",
|
|
643
|
+
color: "#007AFF",
|
|
644
|
+
minZoom: 0,
|
|
645
|
+
defaultEnabled: false
|
|
646
|
+
};
|
|
647
|
+
var pointLngLat = (feature) => {
|
|
648
|
+
if (feature.geometry.type !== "Point") {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
const [lng, lat] = feature.geometry.coordinates;
|
|
652
|
+
if (lng === void 0 || lat === void 0) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
return [lng, lat];
|
|
656
|
+
};
|
|
657
|
+
var fetchMvt = async (url, tile, signal) => {
|
|
658
|
+
const response = await fetch(url, { signal });
|
|
659
|
+
if (!response.ok) {
|
|
660
|
+
throw new Error(`MVT fetch failed (${response.status}): ${url}`);
|
|
661
|
+
}
|
|
662
|
+
const buffer = await response.arrayBuffer();
|
|
663
|
+
if (buffer.byteLength === 0) {
|
|
664
|
+
return {};
|
|
665
|
+
}
|
|
666
|
+
const vectorTile = new VectorTile(new PbfReader(buffer));
|
|
667
|
+
const layers = {};
|
|
668
|
+
for (const layerName of Object.keys(vectorTile.layers)) {
|
|
669
|
+
const layer = vectorTile.layers[layerName];
|
|
670
|
+
if (!layer) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const features = [];
|
|
674
|
+
for (let i = 0; i < layer.length; i += 1) {
|
|
675
|
+
const feature = layer.feature(i);
|
|
676
|
+
features.push(feature.toGeoJSON(tile.x, tile.y, tile.z));
|
|
677
|
+
}
|
|
678
|
+
layers[layerName] = features;
|
|
679
|
+
}
|
|
680
|
+
return layers;
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/providers/adapters/mapilio.ts
|
|
684
|
+
var TILE_ZOOM2 = 14;
|
|
685
|
+
var POINTS_LAYER = "mapilio:map_points";
|
|
686
|
+
var ROADS_LAYER = "mapilio:map_roads_line";
|
|
687
|
+
var wmtsUrl = (layer, tile) => `https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=${layer}&STYLE=&TILEMATRIX=EPSG:900913:${tile.z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL=${tile.x}&TILEROW=${tile.y}`;
|
|
688
|
+
var parseMapilioCaptureTime = (value) => {
|
|
689
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
const parsed = Date.parse(String(value));
|
|
693
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
694
|
+
};
|
|
695
|
+
var parseMapilioResolution = (resolution) => {
|
|
696
|
+
if (typeof resolution !== "string") {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const match = resolution.match(/^(\d+)x(\d+)$/i);
|
|
700
|
+
if (!match) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
const width = Number(match[1]);
|
|
704
|
+
const height = Number(match[2]);
|
|
705
|
+
if (!width || !height) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const max = Math.max(width, height);
|
|
709
|
+
const min = Math.min(width, height);
|
|
710
|
+
return max % min === 0;
|
|
711
|
+
};
|
|
712
|
+
var normalizeMapilioPointFeature = (feature) => {
|
|
713
|
+
const lngLat = pointLngLat(feature);
|
|
714
|
+
if (!lngLat) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
const props = feature.properties ?? {};
|
|
718
|
+
const id = props.id;
|
|
719
|
+
if (id == null) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const heading = props.heading == null ? null : typeof props.heading === "number" ? props.heading : Number.parseFloat(String(props.heading));
|
|
723
|
+
return {
|
|
724
|
+
photoId: String(id),
|
|
725
|
+
sequenceId: props.sequence_uuid != null ? String(props.sequence_uuid) : null,
|
|
726
|
+
capturedAt: parseMapilioCaptureTime(props.capture_time),
|
|
727
|
+
isPano: parseMapilioResolution(props.resolution),
|
|
728
|
+
heading: heading != null && !Number.isNaN(heading) ? heading : null,
|
|
729
|
+
lngLat
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
var geometryKey = (geometry) => JSON.stringify(geometry.coordinates);
|
|
733
|
+
var normalizeMapilioRoadFeature = (feature) => {
|
|
734
|
+
const props = feature.properties ?? {};
|
|
735
|
+
const sequenceId = props.sequence_uuid;
|
|
736
|
+
if (sequenceId == null) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
const { geometry } = feature;
|
|
740
|
+
if (geometry.type !== "LineString" && geometry.type !== "MultiLineString") {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
sequenceId: String(sequenceId),
|
|
745
|
+
geometry,
|
|
746
|
+
capturedAt: parseMapilioCaptureTime(props.capture_time),
|
|
747
|
+
isPano: parseMapilioResolution(props.resolution)
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
var fetchMapilioTile = async (layer, tile, signal) => {
|
|
751
|
+
const cacheLayer = layer === POINTS_LAYER ? "points" : "roads";
|
|
752
|
+
const key = getTileCacheKey(`mapilio:${cacheLayer}`, tile);
|
|
753
|
+
return fetchTileCached(
|
|
754
|
+
key,
|
|
755
|
+
(innerSignal) => fetchMvt(wmtsUrl(layer, tile), tile, innerSignal),
|
|
756
|
+
signal
|
|
757
|
+
);
|
|
758
|
+
};
|
|
759
|
+
var fetchMapilioPointTiles = async (bbox, signal) => {
|
|
760
|
+
const tiles = tilesForBbox(bbox, TILE_ZOOM2, { skipNullIsland: true });
|
|
761
|
+
return collectSettledTiles(tiles.map((tile) => fetchMapilioTile(POINTS_LAYER, tile, signal)));
|
|
762
|
+
};
|
|
763
|
+
var fetchPhotos2 = async (bbox, _zoom, signal) => {
|
|
764
|
+
const tileLayers = await fetchMapilioPointTiles(bbox, signal);
|
|
765
|
+
const photos = [];
|
|
766
|
+
for (const layers of tileLayers) {
|
|
767
|
+
const pointFeatures = layers.map_points ?? [];
|
|
768
|
+
for (const feature of pointFeatures) {
|
|
769
|
+
const normalized = normalizeMapilioPointFeature(feature);
|
|
770
|
+
if (normalized) {
|
|
771
|
+
photos.push({ providerId: "mapilio", ...normalized });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return photos;
|
|
776
|
+
};
|
|
777
|
+
var fetchSequences = async (bbox, _zoom, signal) => {
|
|
778
|
+
const tiles = tilesForBbox(bbox, TILE_ZOOM2, { skipNullIsland: true });
|
|
779
|
+
const tileLayers = await collectSettledTiles(
|
|
780
|
+
tiles.map((tile) => fetchMapilioTile(ROADS_LAYER, tile, signal))
|
|
781
|
+
);
|
|
782
|
+
const sequences = [];
|
|
783
|
+
const seenGeometries = /* @__PURE__ */ new Map();
|
|
784
|
+
for (const layers of tileLayers) {
|
|
785
|
+
const roadFeatures = layers.map_roads_line ?? [];
|
|
786
|
+
for (const feature of roadFeatures) {
|
|
787
|
+
const normalized = normalizeMapilioRoadFeature(feature);
|
|
788
|
+
if (!normalized) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const key = geometryKey(normalized.geometry);
|
|
792
|
+
const seenForSequence = seenGeometries.get(normalized.sequenceId) ?? /* @__PURE__ */ new Set();
|
|
793
|
+
if (seenForSequence.has(key)) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
seenForSequence.add(key);
|
|
797
|
+
seenGeometries.set(normalized.sequenceId, seenForSequence);
|
|
798
|
+
sequences.push({ providerId: "mapilio", ...normalized });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return sequences;
|
|
802
|
+
};
|
|
803
|
+
var mapilioAdapter = {
|
|
804
|
+
id: "mapilio",
|
|
805
|
+
kind: "photo",
|
|
806
|
+
label: "Mapilio",
|
|
807
|
+
color: "#0D9488",
|
|
808
|
+
minZoom: 14,
|
|
809
|
+
defaultEnabled: false,
|
|
810
|
+
browserUnavailableReason: "Mapilio\u2019s tile server (geo.mapilio.com) does not allow browser requests from other sites (CORS). Coverage dots cannot load until Mapilio fixes this upstream.",
|
|
811
|
+
fetchPhotos: fetchPhotos2,
|
|
812
|
+
fetchSequences
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/providers/mapillaryShared.ts
|
|
816
|
+
var MAPILLARY_TILE_ZOOM = 14;
|
|
817
|
+
var mapillaryTileUrl = (path, tile) => {
|
|
818
|
+
const token = getStreetImageryConfig().mapillaryToken;
|
|
819
|
+
return `https://tiles.mapillary.com/maps/vtp/${path}/2/${tile.z}/${tile.x}/${tile.y}?access_token=${token}`;
|
|
820
|
+
};
|
|
821
|
+
var fetchMapillaryMvtTiles = async (cachePrefix, path, bbox, signal) => {
|
|
822
|
+
const tiles = tilesForBbox(bbox, MAPILLARY_TILE_ZOOM, { skipNullIsland: true });
|
|
823
|
+
return collectSettledTiles(
|
|
824
|
+
tiles.map((tile) => {
|
|
825
|
+
const key = getTileCacheKey(cachePrefix, tile);
|
|
826
|
+
return fetchTileCached(
|
|
827
|
+
key,
|
|
828
|
+
(innerSignal) => fetchMvt(mapillaryTileUrl(path, tile), tile, innerSignal),
|
|
829
|
+
signal
|
|
830
|
+
);
|
|
831
|
+
})
|
|
832
|
+
);
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// src/providers/adapters/mapillary.ts
|
|
836
|
+
var MVT_PATH = "mly1_public";
|
|
837
|
+
var normalizeMapillaryImageFeature = (feature) => {
|
|
838
|
+
const lngLat = pointLngLat(feature);
|
|
839
|
+
if (!lngLat) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const props = feature.properties ?? {};
|
|
843
|
+
const id = props.id;
|
|
844
|
+
if (id === void 0 || id === null) {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
photoId: String(id),
|
|
849
|
+
sequenceId: props.sequence_id != null ? String(props.sequence_id) : null,
|
|
850
|
+
capturedAt: typeof props.captured_at === "number" ? props.captured_at : null,
|
|
851
|
+
isPano: typeof props.is_pano === "boolean" ? props.is_pano : null,
|
|
852
|
+
heading: typeof props.compass_angle === "number" ? props.compass_angle : null,
|
|
853
|
+
lngLat
|
|
854
|
+
};
|
|
855
|
+
};
|
|
856
|
+
var normalizeMapillarySequenceFeature = (feature) => {
|
|
857
|
+
const props = feature.properties ?? {};
|
|
858
|
+
const id = props.id;
|
|
859
|
+
if (id == null) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
const { geometry } = feature;
|
|
863
|
+
if (geometry.type !== "LineString" && geometry.type !== "MultiLineString") {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
sequenceId: String(id),
|
|
868
|
+
geometry,
|
|
869
|
+
capturedAt: typeof props.captured_at === "number" ? props.captured_at : null,
|
|
870
|
+
isPano: typeof props.is_pano === "boolean" ? props.is_pano : null
|
|
871
|
+
};
|
|
872
|
+
};
|
|
873
|
+
var fetchMapillaryTiles = async (bbox, signal) => fetchMapillaryMvtTiles("mapillary", MVT_PATH, bbox, signal);
|
|
874
|
+
var fetchPhotos3 = async (bbox, _zoom, signal) => {
|
|
875
|
+
const tileLayers = await fetchMapillaryTiles(bbox, signal);
|
|
876
|
+
const photos = [];
|
|
877
|
+
for (const layers of tileLayers) {
|
|
878
|
+
const imageFeatures = layers.image ?? [];
|
|
879
|
+
for (const feature of imageFeatures) {
|
|
880
|
+
const normalized = normalizeMapillaryImageFeature(feature);
|
|
881
|
+
if (normalized) {
|
|
882
|
+
photos.push({ providerId: "mapillary", ...normalized });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return photos;
|
|
887
|
+
};
|
|
888
|
+
var fetchSequences2 = async (bbox, _zoom, signal) => {
|
|
889
|
+
const tileLayers = await fetchMapillaryTiles(bbox, signal);
|
|
890
|
+
const sequences = [];
|
|
891
|
+
for (const layers of tileLayers) {
|
|
892
|
+
const sequenceFeatures = layers.sequence ?? [];
|
|
893
|
+
for (const feature of sequenceFeatures) {
|
|
894
|
+
const normalized = normalizeMapillarySequenceFeature(feature);
|
|
895
|
+
if (normalized) {
|
|
896
|
+
sequences.push({ providerId: "mapillary", ...normalized });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return sequences;
|
|
901
|
+
};
|
|
902
|
+
var mapillaryAdapter = {
|
|
903
|
+
id: "mapillary",
|
|
904
|
+
kind: "photo",
|
|
905
|
+
label: "Mapillary",
|
|
906
|
+
color: "#05CB63",
|
|
907
|
+
minZoom: 12,
|
|
908
|
+
fetchPhotos: fetchPhotos3,
|
|
909
|
+
fetchSequences: fetchSequences2
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/providers/adapters/mapillary-map-features.ts
|
|
913
|
+
var MVT_PATH2 = "mly_map_feature_point";
|
|
914
|
+
var LAYER_NAME = "point";
|
|
915
|
+
var normalizeMapillaryMapFeature = (feature) => {
|
|
916
|
+
const lngLat = pointLngLat(feature);
|
|
917
|
+
if (!lngLat) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
const props = feature.properties ?? {};
|
|
921
|
+
const id = props.id;
|
|
922
|
+
if (id === void 0 || id === null) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
const value = props.value;
|
|
926
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
featureId: String(id),
|
|
931
|
+
value,
|
|
932
|
+
firstSeenAt: typeof props.first_seen_at === "number" ? props.first_seen_at : null,
|
|
933
|
+
lastSeenAt: typeof props.last_seen_at === "number" ? props.last_seen_at : null,
|
|
934
|
+
lngLat
|
|
935
|
+
};
|
|
936
|
+
};
|
|
937
|
+
var fetchMapFeatures = async (bbox, _zoom, signal) => {
|
|
938
|
+
const tileLayers = await fetchMapillaryMvtTiles("mapillary-map-features", MVT_PATH2, bbox, signal);
|
|
939
|
+
const features = [];
|
|
940
|
+
for (const layers of tileLayers) {
|
|
941
|
+
const pointFeatures = layers[LAYER_NAME] ?? [];
|
|
942
|
+
for (const feature of pointFeatures) {
|
|
943
|
+
const normalized = normalizeMapillaryMapFeature(feature);
|
|
944
|
+
if (normalized) {
|
|
945
|
+
features.push({ providerId: "mapillary-map-features", ...normalized });
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return features;
|
|
950
|
+
};
|
|
951
|
+
var mapillaryMapFeaturesAdapter = {
|
|
952
|
+
id: "mapillary-map-features",
|
|
953
|
+
kind: "mapFeature",
|
|
954
|
+
label: "Mapillary map features",
|
|
955
|
+
color: "#A855F7",
|
|
956
|
+
minZoom: 12,
|
|
957
|
+
fetchMapFeatures
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// src/providers/adapters/mapillary-signs.ts
|
|
961
|
+
var MVT_PATH3 = "mly_map_feature_traffic_sign";
|
|
962
|
+
var LAYER_NAME2 = "traffic_sign";
|
|
963
|
+
var normalizeMapillaryTrafficSignFeature = (feature) => {
|
|
964
|
+
const lngLat = pointLngLat(feature);
|
|
965
|
+
if (!lngLat) {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
const props = feature.properties ?? {};
|
|
969
|
+
const id = props.id;
|
|
970
|
+
if (id === void 0 || id === null) {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
const value = props.value;
|
|
974
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
return {
|
|
978
|
+
featureId: String(id),
|
|
979
|
+
value,
|
|
980
|
+
firstSeenAt: typeof props.first_seen_at === "number" ? props.first_seen_at : null,
|
|
981
|
+
lastSeenAt: typeof props.last_seen_at === "number" ? props.last_seen_at : null,
|
|
982
|
+
lngLat
|
|
983
|
+
};
|
|
984
|
+
};
|
|
985
|
+
var fetchMapFeatures2 = async (bbox, _zoom, signal) => {
|
|
986
|
+
const tileLayers = await fetchMapillaryMvtTiles("mapillary-signs", MVT_PATH3, bbox, signal);
|
|
987
|
+
const features = [];
|
|
988
|
+
for (const layers of tileLayers) {
|
|
989
|
+
const signFeatures = layers[LAYER_NAME2] ?? [];
|
|
990
|
+
for (const feature of signFeatures) {
|
|
991
|
+
const normalized = normalizeMapillaryTrafficSignFeature(feature);
|
|
992
|
+
if (normalized) {
|
|
993
|
+
features.push({ providerId: "mapillary-signs", ...normalized });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return features;
|
|
998
|
+
};
|
|
999
|
+
var mapillarySignsAdapter = {
|
|
1000
|
+
id: "mapillary-signs",
|
|
1001
|
+
kind: "mapFeature",
|
|
1002
|
+
label: "Mapillary signs",
|
|
1003
|
+
color: "#E11D48",
|
|
1004
|
+
minZoom: 12,
|
|
1005
|
+
fetchMapFeatures: fetchMapFeatures2
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
// src/providers/adapters/panoramax.ts
|
|
1009
|
+
var PHOTO_TILE_MAX_ZOOM = 15;
|
|
1010
|
+
var SEQUENCE_TILE_MIN_ZOOM = 10;
|
|
1011
|
+
var SEQUENCE_TILE_MAX_ZOOM = 15;
|
|
1012
|
+
var tileUrl = (tile) => {
|
|
1013
|
+
const base = getStreetImageryConfig().panoramaxApiBase;
|
|
1014
|
+
return `${base}/api/map/${tile.z}/${tile.x}/${tile.y}.mvt`;
|
|
1015
|
+
};
|
|
1016
|
+
var parsePanoramaxTimestamp = (value) => {
|
|
1017
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
const parsed = Date.parse(value);
|
|
1021
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1022
|
+
};
|
|
1023
|
+
var normalizePanoramaxPictureFeature = (feature) => {
|
|
1024
|
+
const lngLat = pointLngLat(feature);
|
|
1025
|
+
if (!lngLat) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
const props = feature.properties ?? {};
|
|
1029
|
+
const id = props.id;
|
|
1030
|
+
if (id == null) {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
const heading = props.heading == null ? null : typeof props.heading === "number" ? props.heading : Number.parseInt(String(props.heading), 10);
|
|
1034
|
+
return {
|
|
1035
|
+
photoId: String(id),
|
|
1036
|
+
sequenceId: props.first_sequence != null ? String(props.first_sequence) : null,
|
|
1037
|
+
capturedAt: parsePanoramaxTimestamp(props.ts),
|
|
1038
|
+
isPano: props.type === "equirectangular",
|
|
1039
|
+
heading: heading != null && !Number.isNaN(heading) ? heading : null,
|
|
1040
|
+
lngLat
|
|
1041
|
+
};
|
|
1042
|
+
};
|
|
1043
|
+
var normalizePanoramaxSequenceFeature = (feature) => {
|
|
1044
|
+
const props = feature.properties ?? {};
|
|
1045
|
+
const id = props.id;
|
|
1046
|
+
if (id == null) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
const { geometry } = feature;
|
|
1050
|
+
if (geometry.type !== "LineString" && geometry.type !== "MultiLineString") {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
sequenceId: String(id),
|
|
1055
|
+
geometry,
|
|
1056
|
+
capturedAt: parsePanoramaxTimestamp(props.ts ?? props.datetime ?? props.date),
|
|
1057
|
+
isPano: null
|
|
1058
|
+
};
|
|
1059
|
+
};
|
|
1060
|
+
var fetchPanoramaxTile = async (tile, signal) => {
|
|
1061
|
+
const key = getTileCacheKey("panoramax", tile);
|
|
1062
|
+
return fetchTileCached(key, (innerSignal) => fetchMvt(tileUrl(tile), tile, innerSignal), signal);
|
|
1063
|
+
};
|
|
1064
|
+
var fetchPanoramaxTiles = async (bbox, zoom, signal) => {
|
|
1065
|
+
const tileZoom = clampZoom(zoom, SEQUENCE_TILE_MIN_ZOOM, SEQUENCE_TILE_MAX_ZOOM);
|
|
1066
|
+
const tiles = tilesForBbox(bbox, tileZoom);
|
|
1067
|
+
return collectSettledTiles(tiles.map((tile) => fetchPanoramaxTile(tile, signal)));
|
|
1068
|
+
};
|
|
1069
|
+
var fetchPhotos4 = async (bbox, zoom, signal) => {
|
|
1070
|
+
const tileZoom = clampZoom(zoom, PHOTO_TILE_MAX_ZOOM, PHOTO_TILE_MAX_ZOOM);
|
|
1071
|
+
const tiles = tilesForBbox(bbox, tileZoom);
|
|
1072
|
+
const tileLayers = await collectSettledTiles(
|
|
1073
|
+
tiles.map((tile) => fetchPanoramaxTile(tile, signal))
|
|
1074
|
+
);
|
|
1075
|
+
const photos = [];
|
|
1076
|
+
for (const layers of tileLayers) {
|
|
1077
|
+
const pictureFeatures = layers.pictures ?? [];
|
|
1078
|
+
for (const feature of pictureFeatures) {
|
|
1079
|
+
const normalized = normalizePanoramaxPictureFeature(feature);
|
|
1080
|
+
if (normalized) {
|
|
1081
|
+
photos.push({ providerId: "panoramax", ...normalized });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return photos;
|
|
1086
|
+
};
|
|
1087
|
+
var fetchSequences3 = async (bbox, zoom, signal) => {
|
|
1088
|
+
const tileLayers = await fetchPanoramaxTiles(bbox, zoom, signal);
|
|
1089
|
+
const sequences = [];
|
|
1090
|
+
for (const layers of tileLayers) {
|
|
1091
|
+
const sequenceFeatures = layers.sequences ?? [];
|
|
1092
|
+
for (const feature of sequenceFeatures) {
|
|
1093
|
+
const normalized = normalizePanoramaxSequenceFeature(feature);
|
|
1094
|
+
if (normalized) {
|
|
1095
|
+
sequences.push({ providerId: "panoramax", ...normalized });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return sequences;
|
|
1100
|
+
};
|
|
1101
|
+
var panoramaxAdapter = {
|
|
1102
|
+
id: "panoramax",
|
|
1103
|
+
kind: "photo",
|
|
1104
|
+
label: "Panoramax",
|
|
1105
|
+
color: "#7C3AED",
|
|
1106
|
+
minZoom: 15,
|
|
1107
|
+
sequencesMinZoom: 10,
|
|
1108
|
+
fetchPhotos: fetchPhotos4,
|
|
1109
|
+
fetchSequences: fetchSequences3
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// src/providers/adapters/streetside.ts
|
|
1113
|
+
var BING_MAPS_KEY = "Auk3J0jR9g1_PVQgdmL95zCOKVOc8g-FGq5Zgb5ik7w1Ri5SRyWILV-kksgbw-Gh";
|
|
1114
|
+
var STREETSIDE_API = "https://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside";
|
|
1115
|
+
var TILE_ZOOM3 = 16;
|
|
1116
|
+
var MAX_RESULTS = 500;
|
|
1117
|
+
var DEFAULT_SUBDOMAINS = ["t0", "t1", "t2", "t3"];
|
|
1118
|
+
var parseStreetsideDate = (value) => {
|
|
1119
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
const parsed = Date.parse(value);
|
|
1123
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1124
|
+
};
|
|
1125
|
+
var normalizeStreetsideBubble = (bubble) => {
|
|
1126
|
+
const lng = +(bubble.lon ?? bubble.longitude ?? Number.NaN);
|
|
1127
|
+
const lat = +(bubble.lat ?? bubble.latitude ?? Number.NaN);
|
|
1128
|
+
if (Number.isNaN(lng) || Number.isNaN(lat)) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
const photoId = bubble.id ?? bubble.imageUrl;
|
|
1132
|
+
if (!photoId) {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
const headingRaw = bubble.he ?? bubble.heading;
|
|
1136
|
+
const heading = headingRaw == null ? null : +headingRaw;
|
|
1137
|
+
return {
|
|
1138
|
+
photoId,
|
|
1139
|
+
sequenceId: null,
|
|
1140
|
+
capturedAt: parseStreetsideDate(bubble.vintageEnd),
|
|
1141
|
+
isPano: true,
|
|
1142
|
+
heading: heading != null && !Number.isNaN(heading) ? heading : null,
|
|
1143
|
+
lngLat: [lng, lat],
|
|
1144
|
+
thumbUrl: bubble.imageUrl,
|
|
1145
|
+
streetside: bubble.imageUrl ? {
|
|
1146
|
+
urlTemplate: bubble.imageUrl,
|
|
1147
|
+
subdomains: bubble.imageUrlSubdomains && bubble.imageUrlSubdomains.length > 0 ? bubble.imageUrlSubdomains : DEFAULT_SUBDOMAINS
|
|
1148
|
+
} : void 0
|
|
1149
|
+
};
|
|
1150
|
+
};
|
|
1151
|
+
var fetchStreetsideTilePhotos = async (tile, signal) => {
|
|
1152
|
+
const [west, south, east, north] = tileBbox(tile);
|
|
1153
|
+
const mapArea = `${south},${west},${north},${east}`;
|
|
1154
|
+
const url = new URL(STREETSIDE_API);
|
|
1155
|
+
url.searchParams.set("mapArea", mapArea);
|
|
1156
|
+
url.searchParams.set("key", BING_MAPS_KEY);
|
|
1157
|
+
url.searchParams.set("count", String(MAX_RESULTS));
|
|
1158
|
+
url.searchParams.set("uriScheme", "https");
|
|
1159
|
+
const response = await fetch(url, { signal });
|
|
1160
|
+
if (!response.ok) {
|
|
1161
|
+
throw new Error(`Streetside fetch failed (${response.status})`);
|
|
1162
|
+
}
|
|
1163
|
+
const data = await response.json();
|
|
1164
|
+
const bubbles = data.resourceSets?.[0]?.resources ?? [];
|
|
1165
|
+
const photos = [];
|
|
1166
|
+
for (const bubble of bubbles) {
|
|
1167
|
+
const normalized = normalizeStreetsideBubble(bubble);
|
|
1168
|
+
if (normalized) {
|
|
1169
|
+
photos.push({ providerId: "streetside", ...normalized });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return photos;
|
|
1173
|
+
};
|
|
1174
|
+
var fetchStreetsideTile = async (tile, signal) => {
|
|
1175
|
+
const key = getTileCacheKey("streetside", tile);
|
|
1176
|
+
return fetchTileCached(key, (innerSignal) => fetchStreetsideTilePhotos(tile, innerSignal), signal);
|
|
1177
|
+
};
|
|
1178
|
+
var fetchPhotos5 = async (bbox, _zoom, signal) => {
|
|
1179
|
+
const tiles = tilesForBbox(bbox, TILE_ZOOM3, { skipNullIsland: true });
|
|
1180
|
+
const tileResults = await collectSettledTiles(
|
|
1181
|
+
tiles.map((tile) => fetchStreetsideTile(tile, signal))
|
|
1182
|
+
);
|
|
1183
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1184
|
+
for (const photos of tileResults) {
|
|
1185
|
+
for (const photo of photos) {
|
|
1186
|
+
byId.set(photo.photoId, photo);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return [...byId.values()];
|
|
1190
|
+
};
|
|
1191
|
+
var streetsideAdapter = {
|
|
1192
|
+
id: "streetside",
|
|
1193
|
+
kind: "photo",
|
|
1194
|
+
label: "Bing Streetside",
|
|
1195
|
+
color: "#0891B2",
|
|
1196
|
+
minZoom: 14,
|
|
1197
|
+
fetchPhotos: fetchPhotos5
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
// src/providers/adapters/streetview.ts
|
|
1201
|
+
var STREET_VIEW_METADATA_API = "https://maps.googleapis.com/maps/api/streetview/metadata";
|
|
1202
|
+
var getGoogleMapsApiKey = () => {
|
|
1203
|
+
const env = import.meta.env;
|
|
1204
|
+
const key = env?.VITE_GOOGLE_MAPS_API_KEY;
|
|
1205
|
+
return typeof key === "string" && key.length > 0 ? key : void 0;
|
|
1206
|
+
};
|
|
1207
|
+
var parseStreetViewDate = (value) => {
|
|
1208
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
const normalized = /^\d{4}-\d{2}$/.test(value) ? `${value}-01` : value;
|
|
1212
|
+
const parsed = Date.parse(normalized);
|
|
1213
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1214
|
+
};
|
|
1215
|
+
var normalizeStreetViewMetadata = (data, clickLng, clickLat) => {
|
|
1216
|
+
if (data.status !== "OK") {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const lng = data.location?.lng ?? clickLng;
|
|
1220
|
+
const lat = data.location?.lat ?? clickLat;
|
|
1221
|
+
const photoId = data.pano_id ?? `${lat},${lng}`;
|
|
1222
|
+
return {
|
|
1223
|
+
photoId,
|
|
1224
|
+
sequenceId: null,
|
|
1225
|
+
capturedAt: parseStreetViewDate(data.date),
|
|
1226
|
+
isPano: true,
|
|
1227
|
+
heading: null,
|
|
1228
|
+
lngLat: [lng, lat]
|
|
1229
|
+
};
|
|
1230
|
+
};
|
|
1231
|
+
var fetchStreetViewMetadata = async (lat, lng, signal) => {
|
|
1232
|
+
const apiKey = getGoogleMapsApiKey();
|
|
1233
|
+
if (!apiKey) {
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
const url = new URL(STREET_VIEW_METADATA_API);
|
|
1237
|
+
url.searchParams.set("location", `${lat},${lng}`);
|
|
1238
|
+
url.searchParams.set("key", apiKey);
|
|
1239
|
+
const response = await fetch(url, { signal });
|
|
1240
|
+
if (!response.ok) {
|
|
1241
|
+
throw new Error(`Google Street View metadata failed (${response.status})`);
|
|
1242
|
+
}
|
|
1243
|
+
const data = await response.json();
|
|
1244
|
+
if (data.status === "ZERO_RESULTS") {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
if (data.status !== "OK") {
|
|
1248
|
+
throw new Error(`Google Street View metadata: ${data.status}`);
|
|
1249
|
+
}
|
|
1250
|
+
const normalized = normalizeStreetViewMetadata(data, lng, lat);
|
|
1251
|
+
if (!normalized) {
|
|
1252
|
+
throw new Error("Google Street View metadata: unexpected empty OK response");
|
|
1253
|
+
}
|
|
1254
|
+
return { providerId: "streetview", ...normalized };
|
|
1255
|
+
};
|
|
1256
|
+
var streetViewAdapter = {
|
|
1257
|
+
id: "streetview",
|
|
1258
|
+
kind: "photo",
|
|
1259
|
+
label: "Google Street View",
|
|
1260
|
+
color: "#EA4335",
|
|
1261
|
+
minZoom: 0,
|
|
1262
|
+
defaultEnabled: false
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/providers/adapters/vegbilder.ts
|
|
1266
|
+
var TILE_ZOOM4 = 14;
|
|
1267
|
+
var OWS_BASE = "https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows";
|
|
1268
|
+
var CAPABILITIES_URL = `${OWS_BASE}?service=WFS&request=GetCapabilities&version=2.0.0`;
|
|
1269
|
+
var NORWAY_BBOX = [4, 57, 32, 72];
|
|
1270
|
+
var LAYER_NAME_REGEX = /vegbilder_1_0:Vegbilder(_360)?_(\d{4})/g;
|
|
1271
|
+
var parseVegbilderLayerNames = (capabilitiesXml) => {
|
|
1272
|
+
const layers = [];
|
|
1273
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1274
|
+
for (const match of capabilitiesXml.matchAll(LAYER_NAME_REGEX)) {
|
|
1275
|
+
const name = match[0];
|
|
1276
|
+
if (seen.has(name)) {
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
seen.add(name);
|
|
1280
|
+
layers.push({
|
|
1281
|
+
name,
|
|
1282
|
+
is360: match[1] === "_360",
|
|
1283
|
+
year: Number(match[2])
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
return layers;
|
|
1287
|
+
};
|
|
1288
|
+
var selectVegbilderLayers = (layers, minYear = 2020, maxLayers = 8) => layers.filter((layer) => layer.year >= minYear).sort((a, b) => b.year - a.year || Number(a.is360) - Number(b.is360)).slice(0, maxLayers).map((layer) => layer.name);
|
|
1289
|
+
var parseVegbilderTimestamp = (value) => {
|
|
1290
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
const parsed = Date.parse(String(value));
|
|
1294
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1295
|
+
};
|
|
1296
|
+
var buildVegbilderSequenceId = (props) => {
|
|
1297
|
+
const fylke = props.FYLKENUMMER;
|
|
1298
|
+
const category = props.VEGKATEGORI;
|
|
1299
|
+
const number = props.VEGNUMMER;
|
|
1300
|
+
if (typeof fylke !== "string" && typeof fylke !== "number" || typeof category !== "string" || typeof number !== "string" && typeof number !== "number") {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
return `${fylke}-${category}-${number}`;
|
|
1304
|
+
};
|
|
1305
|
+
var normalizeVegbilderFeature = (feature, layerYear) => {
|
|
1306
|
+
const lngLat = pointLngLat(feature);
|
|
1307
|
+
if (!lngLat) {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
const id = feature.id;
|
|
1311
|
+
if (id == null) {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
const props = feature.properties ?? {};
|
|
1315
|
+
const heading = props.RETNING == null ? null : typeof props.RETNING === "number" ? props.RETNING : Number.parseFloat(String(props.RETNING));
|
|
1316
|
+
const previewPath = props.URLPREVIEW;
|
|
1317
|
+
const thumbUrl = typeof previewPath === "string" && previewPath.length > 0 ? previewPath : void 0;
|
|
1318
|
+
const fullPath = props.URL;
|
|
1319
|
+
const fullUrl = typeof fullPath === "string" && fullPath.length > 0 ? fullPath : void 0;
|
|
1320
|
+
const yearFromTimestamp = (() => {
|
|
1321
|
+
const capturedAt = parseVegbilderTimestamp(props.TIDSPUNKT);
|
|
1322
|
+
return capturedAt != null ? new Date(capturedAt).getUTCFullYear() : void 0;
|
|
1323
|
+
})();
|
|
1324
|
+
return {
|
|
1325
|
+
photoId: String(id),
|
|
1326
|
+
sequenceId: buildVegbilderSequenceId(props),
|
|
1327
|
+
capturedAt: parseVegbilderTimestamp(props.TIDSPUNKT),
|
|
1328
|
+
isPano: props.BILDETYPE === "360",
|
|
1329
|
+
heading: heading != null && !Number.isNaN(heading) ? heading : null,
|
|
1330
|
+
lngLat,
|
|
1331
|
+
thumbUrl,
|
|
1332
|
+
fullUrl,
|
|
1333
|
+
viewerYear: layerYear ?? yearFromTimestamp
|
|
1334
|
+
};
|
|
1335
|
+
};
|
|
1336
|
+
var intersectsNorway = (bbox) => bboxIntersects(bbox, NORWAY_BBOX);
|
|
1337
|
+
var capabilitiesPromise = null;
|
|
1338
|
+
var loadCapabilities = (signal) => {
|
|
1339
|
+
if (!capabilitiesPromise) {
|
|
1340
|
+
capabilitiesPromise = (async () => {
|
|
1341
|
+
const response = await fetch(CAPABILITIES_URL, { signal });
|
|
1342
|
+
if (!response.ok) {
|
|
1343
|
+
throw new Error(`Vegbilder capabilities failed (${response.status})`);
|
|
1344
|
+
}
|
|
1345
|
+
return parseVegbilderLayerNames(await response.text());
|
|
1346
|
+
})().catch((error) => {
|
|
1347
|
+
capabilitiesPromise = null;
|
|
1348
|
+
throw error;
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
return capabilitiesPromise;
|
|
1352
|
+
};
|
|
1353
|
+
var wfsFeatureUrl = (layer, bbox) => {
|
|
1354
|
+
const [west, south, east, north] = bbox;
|
|
1355
|
+
const params = new URLSearchParams({
|
|
1356
|
+
service: "WFS",
|
|
1357
|
+
request: "GetFeature",
|
|
1358
|
+
version: "2.0.0",
|
|
1359
|
+
typenames: layer,
|
|
1360
|
+
bbox: `${south},${west},${north},${east}`,
|
|
1361
|
+
outputFormat: "json"
|
|
1362
|
+
});
|
|
1363
|
+
return `${OWS_BASE}?${params.toString()}`;
|
|
1364
|
+
};
|
|
1365
|
+
var fetchWfsLayer = async (layer, bbox, signal) => {
|
|
1366
|
+
const response = await fetch(wfsFeatureUrl(layer, bbox), { signal });
|
|
1367
|
+
if (!response.ok) {
|
|
1368
|
+
throw new Error(`Vegbilder WFS failed (${response.status}): ${layer}`);
|
|
1369
|
+
}
|
|
1370
|
+
const data = await response.json();
|
|
1371
|
+
return data.features ?? [];
|
|
1372
|
+
};
|
|
1373
|
+
var fetchVegbilderTile = async (tile, layers, signal) => {
|
|
1374
|
+
const key = getTileCacheKey("vegbilder", tile);
|
|
1375
|
+
return fetchTileCached(
|
|
1376
|
+
key,
|
|
1377
|
+
async (innerSignal) => {
|
|
1378
|
+
const bbox = tileBbox(tile);
|
|
1379
|
+
return Promise.all(
|
|
1380
|
+
layers.map(async (layer) => ({
|
|
1381
|
+
layer: layer.name,
|
|
1382
|
+
year: layer.year,
|
|
1383
|
+
features: await fetchWfsLayer(layer.name, bbox, innerSignal)
|
|
1384
|
+
}))
|
|
1385
|
+
);
|
|
1386
|
+
},
|
|
1387
|
+
signal
|
|
1388
|
+
);
|
|
1389
|
+
};
|
|
1390
|
+
var fetchPhotos6 = async (bbox, _zoom, signal) => {
|
|
1391
|
+
if (!intersectsNorway(bbox)) {
|
|
1392
|
+
return [];
|
|
1393
|
+
}
|
|
1394
|
+
const allLayers = await loadCapabilities(signal);
|
|
1395
|
+
const selectedLayerNames = selectVegbilderLayers(allLayers);
|
|
1396
|
+
const layers = allLayers.filter((layer) => selectedLayerNames.includes(layer.name));
|
|
1397
|
+
if (layers.length === 0) {
|
|
1398
|
+
return [];
|
|
1399
|
+
}
|
|
1400
|
+
const tiles = tilesForBbox(bbox, TILE_ZOOM4);
|
|
1401
|
+
const tileFeatures = await collectSettledTiles(
|
|
1402
|
+
tiles.map((tile) => fetchVegbilderTile(tile, layers, signal))
|
|
1403
|
+
);
|
|
1404
|
+
const photos = [];
|
|
1405
|
+
for (const tileLayers of tileFeatures) {
|
|
1406
|
+
for (const { features, year } of tileLayers) {
|
|
1407
|
+
for (const feature of features) {
|
|
1408
|
+
const normalized = normalizeVegbilderFeature(feature, year);
|
|
1409
|
+
if (normalized) {
|
|
1410
|
+
photos.push({ providerId: "vegbilder", ...normalized });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return photos;
|
|
1416
|
+
};
|
|
1417
|
+
var vegbilderAdapter = {
|
|
1418
|
+
id: "vegbilder",
|
|
1419
|
+
kind: "photo",
|
|
1420
|
+
label: "Vegbilder",
|
|
1421
|
+
color: "#EA580C",
|
|
1422
|
+
minZoom: 14,
|
|
1423
|
+
fetchPhotos: fetchPhotos6
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// src/providers/registry.ts
|
|
1427
|
+
var PROVIDER_HOMEPAGE_URLS = {
|
|
1428
|
+
mapillary: "https://www.mapillary.com",
|
|
1429
|
+
panoramax: "https://panoramax.xyz",
|
|
1430
|
+
kartaview: "https://kartaview.org",
|
|
1431
|
+
mapilio: "https://mapilio.com",
|
|
1432
|
+
streetside: "https://www.bing.com/maps",
|
|
1433
|
+
vegbilder: "https://vegbilder.atlas.vegvesen.no",
|
|
1434
|
+
streetview: "https://www.google.com/maps",
|
|
1435
|
+
lookaround: "https://www.apple.com/maps/",
|
|
1436
|
+
"mapillary-signs": "https://www.mapillary.com",
|
|
1437
|
+
"mapillary-map-features": "https://www.mapillary.com"
|
|
1438
|
+
};
|
|
1439
|
+
var PROVIDER_ADAPTERS = [
|
|
1440
|
+
mapillaryAdapter,
|
|
1441
|
+
panoramaxAdapter,
|
|
1442
|
+
kartaviewAdapter,
|
|
1443
|
+
mapilioAdapter,
|
|
1444
|
+
streetsideAdapter,
|
|
1445
|
+
vegbilderAdapter,
|
|
1446
|
+
streetViewAdapter,
|
|
1447
|
+
lookaroundAdapter,
|
|
1448
|
+
mapillarySignsAdapter,
|
|
1449
|
+
mapillaryMapFeaturesAdapter
|
|
1450
|
+
];
|
|
1451
|
+
var adapterById = Object.fromEntries(
|
|
1452
|
+
PROVIDER_ADAPTERS.map((adapter) => [adapter.id, adapter])
|
|
1453
|
+
);
|
|
1454
|
+
var PROVIDERS = PROVIDER_ADAPTERS.map((adapter) => ({
|
|
1455
|
+
id: adapter.id,
|
|
1456
|
+
kind: adapter.kind,
|
|
1457
|
+
label: adapter.label,
|
|
1458
|
+
color: adapter.color,
|
|
1459
|
+
minZoom: adapter.minZoom,
|
|
1460
|
+
sequencesMinZoom: adapter.sequencesMinZoom ?? adapter.minZoom,
|
|
1461
|
+
homepageUrl: PROVIDER_HOMEPAGE_URLS[adapter.id],
|
|
1462
|
+
browserUnavailableReason: adapter.browserUnavailableReason
|
|
1463
|
+
}));
|
|
1464
|
+
var DEFAULT_PROVIDER_IDS = PROVIDER_IDS.filter(
|
|
1465
|
+
(id) => adapterById[id].kind === "photo" && adapterById[id].defaultEnabled !== false
|
|
1466
|
+
);
|
|
1467
|
+
var isClickOnlyPhotoProvider = (providerId) => {
|
|
1468
|
+
const adapter = adapterById[providerId];
|
|
1469
|
+
return adapter.kind === "photo" && adapter.fetchPhotos == null;
|
|
1470
|
+
};
|
|
1471
|
+
var isBrowserAvailableProvider = (providerId) => adapterById[providerId].browserUnavailableReason == null;
|
|
1472
|
+
var providerById = Object.fromEntries(
|
|
1473
|
+
PROVIDERS.map((provider) => [provider.id, provider])
|
|
1474
|
+
);
|
|
1475
|
+
var photoLayerId = (providerId) => `photos-${providerId}`;
|
|
1476
|
+
var featureLayerId = (providerId) => `features-${providerId}`;
|
|
1477
|
+
var sequenceLayerId = (providerId) => `sequences-${providerId}`;
|
|
1478
|
+
var viewfieldLayerId = (providerId) => `viewfields-${providerId}`;
|
|
1479
|
+
var viewfieldLineLayerId = (providerId) => `viewfields-line-${providerId}`;
|
|
1480
|
+
var photoSourceId = (providerId) => `photos-source-${providerId}`;
|
|
1481
|
+
var featureSourceId = (providerId) => `features-source-${providerId}`;
|
|
1482
|
+
var sequenceSourceId = (providerId) => `sequences-source-${providerId}`;
|
|
1483
|
+
var viewfieldSourceId = (providerId) => `viewfields-source-${providerId}`;
|
|
1484
|
+
var isPhotoProviderId = (providerId) => adapterById[providerId].kind === "photo";
|
|
1485
|
+
var isMapFeatureProviderId = (providerId) => adapterById[providerId].kind === "mapFeature";
|
|
1486
|
+
|
|
1487
|
+
// src/viewer/clickRadius.ts
|
|
1488
|
+
var EARTH_RADIUS_METERS = 6371e3;
|
|
1489
|
+
var haversineDistanceMeters = (lng1, lat1, lng2, lat2) => {
|
|
1490
|
+
const toRadians = (degrees) => degrees * Math.PI / 180;
|
|
1491
|
+
const dLat = toRadians(lat2 - lat1);
|
|
1492
|
+
const dLng = toRadians(lng2 - lng1);
|
|
1493
|
+
const lat1Rad = toRadians(lat1);
|
|
1494
|
+
const lat2Rad = toRadians(lat2);
|
|
1495
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(dLng / 2) ** 2;
|
|
1496
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
1497
|
+
return EARTH_RADIUS_METERS * c;
|
|
1498
|
+
};
|
|
1499
|
+
var clickRadiusMeters = (zoom) => 50 * 2 ** (14 - zoom);
|
|
1500
|
+
|
|
1501
|
+
// src/viewer/externalLinks.ts
|
|
1502
|
+
var providerExternalLink = (photo) => {
|
|
1503
|
+
const [lng, lat] = photo.lngLat;
|
|
1504
|
+
switch (photo.providerId) {
|
|
1505
|
+
case "mapillary":
|
|
1506
|
+
return `https://www.mapillary.com/app/?pKey=${encodeURIComponent(photo.photoId)}&focus=photo`;
|
|
1507
|
+
case "panoramax":
|
|
1508
|
+
return `${getStreetImageryConfig().panoramaxApiBase}/#pic=${encodeURIComponent(photo.photoId)}&focus=pic`;
|
|
1509
|
+
case "kartaview":
|
|
1510
|
+
if (photo.sequenceId != null && photo.sequenceIndex != null) {
|
|
1511
|
+
return `https://kartaview.org/details/${encodeURIComponent(photo.sequenceId)}/${photo.sequenceIndex}`;
|
|
1512
|
+
}
|
|
1513
|
+
return `https://kartaview.org/photo/${encodeURIComponent(photo.photoId)}`;
|
|
1514
|
+
case "mapilio":
|
|
1515
|
+
return `https://mapilio.com/app?lat=${lat}&lng=${lng}&zoom=17&pId=${encodeURIComponent(photo.photoId)}`;
|
|
1516
|
+
case "streetside":
|
|
1517
|
+
return `https://www.bing.com/maps?cp=${lat}~${lng}&lvl=18&style=x`;
|
|
1518
|
+
case "streetview":
|
|
1519
|
+
return `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${lat},${lng}`;
|
|
1520
|
+
case "lookaround":
|
|
1521
|
+
return lookAroundDeepLink(lat, lng);
|
|
1522
|
+
case "vegbilder": {
|
|
1523
|
+
const year = photo.viewerYear ?? new Date(photo.capturedAt ?? Date.now()).getUTCFullYear();
|
|
1524
|
+
return `https://vegbilder.atlas.vegvesen.no/?year=${year}&lat=${lat}&lng=${lng}&view=image&imageId=${encodeURIComponent(photo.photoId)}`;
|
|
1525
|
+
}
|
|
1526
|
+
case "mapillary-map-features":
|
|
1527
|
+
case "mapillary-signs":
|
|
1528
|
+
default:
|
|
1529
|
+
return "#";
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
var mapFeatureExternalLink = (feature) => {
|
|
1533
|
+
const [lng, lat] = feature.lngLat;
|
|
1534
|
+
return `https://www.mapillary.com/app/?lat=${lat}&lng=${lng}&z=17&focus=map`;
|
|
1535
|
+
};
|
|
1536
|
+
var providerLocationLink = (providerId, lat, lng, zoom = 14) => {
|
|
1537
|
+
switch (providerId) {
|
|
1538
|
+
case "mapillary":
|
|
1539
|
+
case "mapillary-signs":
|
|
1540
|
+
case "mapillary-map-features":
|
|
1541
|
+
return `https://www.mapillary.com/app/?lat=${lat}&lng=${lng}&z=17&focus=map`;
|
|
1542
|
+
case "panoramax":
|
|
1543
|
+
return `${getStreetImageryConfig().panoramaxApiBase}/?focus=map&map=${zoom}/${lat}/${lng}`;
|
|
1544
|
+
case "kartaview":
|
|
1545
|
+
return `https://kartaview.org/map/@${lat},${lng},${zoom}z`;
|
|
1546
|
+
case "mapilio":
|
|
1547
|
+
return `https://mapilio.com/app?lat=${lat}&lng=${lng}&zoom=17`;
|
|
1548
|
+
case "streetside":
|
|
1549
|
+
return `https://www.bing.com/maps?cp=${lat}~${lng}&lvl=18&style=x`;
|
|
1550
|
+
case "streetview":
|
|
1551
|
+
return `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${lat},${lng}`;
|
|
1552
|
+
case "lookaround":
|
|
1553
|
+
return lookAroundDeepLink(lat, lng);
|
|
1554
|
+
case "vegbilder": {
|
|
1555
|
+
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
1556
|
+
return `https://vegbilder.atlas.vegvesen.no/?year=${year}&lat=${lat}&lng=${lng}`;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
// src/viewer/groupClickedPhotos.ts
|
|
1562
|
+
var sequenceGroupKey = (providerId, sequenceId) => `${providerId}:${sequenceId}`;
|
|
1563
|
+
var photoGroupSequenceId = (photo) => photo.sequenceId ?? `photo:${photo.photoId}`;
|
|
1564
|
+
var distanceToPhoto = (photo, lng, lat) => haversineDistanceMeters(lng, lat, photo.lngLat[0], photo.lngLat[1]);
|
|
1565
|
+
var findNearestPhoto = (photos, lng, lat) => {
|
|
1566
|
+
if (photos.length === 0) {
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
const first = photos[0];
|
|
1570
|
+
if (!first) {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
let nearest = first;
|
|
1574
|
+
let nearestDistance = distanceToPhoto(first, lng, lat);
|
|
1575
|
+
for (let index = 1; index < photos.length; index += 1) {
|
|
1576
|
+
const photo = photos[index];
|
|
1577
|
+
if (!photo) {
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
const distance = distanceToPhoto(photo, lng, lat);
|
|
1581
|
+
if (distance < nearestDistance) {
|
|
1582
|
+
nearest = photo;
|
|
1583
|
+
nearestDistance = distance;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return nearest;
|
|
1587
|
+
};
|
|
1588
|
+
var compareCapturedAt = (left, right) => {
|
|
1589
|
+
const leftTime = left.capturedAt ?? Number.POSITIVE_INFINITY;
|
|
1590
|
+
const rightTime = right.capturedAt ?? Number.POSITIVE_INFINITY;
|
|
1591
|
+
if (leftTime !== rightTime) {
|
|
1592
|
+
return leftTime - rightTime;
|
|
1593
|
+
}
|
|
1594
|
+
return left.photoId.localeCompare(right.photoId);
|
|
1595
|
+
};
|
|
1596
|
+
var dedupeNearbyPhotos = (photos, minDistanceMeters = 5) => {
|
|
1597
|
+
const sorted = [...photos].sort(compareCapturedAt);
|
|
1598
|
+
const kept = [];
|
|
1599
|
+
for (const photo of sorted) {
|
|
1600
|
+
const tooClose = kept.some(
|
|
1601
|
+
(existing) => haversineDistanceMeters(
|
|
1602
|
+
existing.lngLat[0],
|
|
1603
|
+
existing.lngLat[1],
|
|
1604
|
+
photo.lngLat[0],
|
|
1605
|
+
photo.lngLat[1]
|
|
1606
|
+
) < minDistanceMeters
|
|
1607
|
+
);
|
|
1608
|
+
if (!tooClose) {
|
|
1609
|
+
kept.push(photo);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return kept;
|
|
1613
|
+
};
|
|
1614
|
+
var groupClickedPhotos = (photos, lng, lat) => {
|
|
1615
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const photo of photos) {
|
|
1617
|
+
const sequenceId = photoGroupSequenceId(photo);
|
|
1618
|
+
const key = sequenceGroupKey(photo.providerId, sequenceId);
|
|
1619
|
+
const group = byGroup.get(key) ?? [];
|
|
1620
|
+
group.push(photo);
|
|
1621
|
+
byGroup.set(key, group);
|
|
1622
|
+
}
|
|
1623
|
+
const groups = [];
|
|
1624
|
+
for (const [groupKey, groupPhotos] of byGroup) {
|
|
1625
|
+
const deduped = dedupeNearbyPhotos(groupPhotos);
|
|
1626
|
+
const nearest = findNearestPhoto(deduped, lng, lat);
|
|
1627
|
+
if (!nearest) {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const sequenceId = photoGroupSequenceId(nearest);
|
|
1631
|
+
groups.push({
|
|
1632
|
+
providerId: nearest.providerId,
|
|
1633
|
+
sequenceId,
|
|
1634
|
+
groupKey,
|
|
1635
|
+
photos: deduped.sort(compareCapturedAt),
|
|
1636
|
+
distanceMeters: distanceToPhoto(nearest, lng, lat)
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
return groups.sort((left, right) => {
|
|
1640
|
+
if (left.distanceMeters !== right.distanceMeters) {
|
|
1641
|
+
return left.distanceMeters - right.distanceMeters;
|
|
1642
|
+
}
|
|
1643
|
+
return left.groupKey.localeCompare(right.groupKey);
|
|
1644
|
+
});
|
|
1645
|
+
};
|
|
1646
|
+
var findGroupBySelection = (groups, providerId, sequenceId) => groups.find((group) => group.providerId === providerId && group.sequenceId === sequenceId) ?? null;
|
|
1647
|
+
var findPhotoIndexInGroup = (group, photoId) => group.photos.findIndex((photo) => photo.photoId === photoId);
|
|
1648
|
+
var STREETSIDE_NEARBY_LIMIT = 15;
|
|
1649
|
+
var collectNearbyStreetsidePhotos = (groups, activePhoto, limit = STREETSIDE_NEARBY_LIMIT) => {
|
|
1650
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1651
|
+
const all = [];
|
|
1652
|
+
for (const group of groups) {
|
|
1653
|
+
if (group.providerId !== "streetside") {
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
for (const photo of group.photos) {
|
|
1657
|
+
if (seen.has(photo.photoId)) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
seen.add(photo.photoId);
|
|
1661
|
+
all.push(photo);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const [lng, lat] = activePhoto.lngLat;
|
|
1665
|
+
const byDistance = (left, right) => distanceToPhoto(left, lng, lat) - distanceToPhoto(right, lng, lat);
|
|
1666
|
+
const nearby = [...all].sort(byDistance).slice(0, limit);
|
|
1667
|
+
if (!nearby.some((photo) => photo.photoId === activePhoto.photoId)) {
|
|
1668
|
+
if (nearby.length >= limit) {
|
|
1669
|
+
nearby.pop();
|
|
1670
|
+
}
|
|
1671
|
+
nearby.push(activePhoto);
|
|
1672
|
+
nearby.sort(byDistance);
|
|
1673
|
+
}
|
|
1674
|
+
return nearby;
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// src/viewer/streetsidePreview.ts
|
|
1678
|
+
var buildStreetsidePreviewUrl = (imageUrlTemplate) => {
|
|
1679
|
+
if (!imageUrlTemplate || !imageUrlTemplate.includes("{")) {
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
return imageUrlTemplate.replace("{subdomain}", "t0").replace("{faceId}", "01").replace("{tileId}", "");
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
// src/viewer/photoThumbnails.ts
|
|
1686
|
+
var mapilioCdnUrl = (uploadedHash, filename, size) => `https://cdn.mapilio.com/im/${uploadedHash}/${filename}/${size}`;
|
|
1687
|
+
var fetchMapilioSequenceItem = async (sequenceId, photoId) => {
|
|
1688
|
+
const response = await fetch(
|
|
1689
|
+
`https://end.mapilio.com/api/sequence-detail?sequence_uuid=${encodeURIComponent(sequenceId)}`
|
|
1690
|
+
);
|
|
1691
|
+
if (!response.ok) {
|
|
1692
|
+
throw new Error(`Mapilio sequence detail failed (${response.status})`);
|
|
1693
|
+
}
|
|
1694
|
+
const data = await response.json();
|
|
1695
|
+
const items = Array.isArray(data.data) ? data.data : data.data?.data ?? [];
|
|
1696
|
+
const match = items.find((item) => String(item.id) === photoId);
|
|
1697
|
+
return match ?? null;
|
|
1698
|
+
};
|
|
1699
|
+
var fetchMapillaryThumbnail = async (photoId) => {
|
|
1700
|
+
const url = new URL(`https://graph.mapillary.com/${photoId}`);
|
|
1701
|
+
url.searchParams.set("fields", "thumb_1024_url");
|
|
1702
|
+
url.searchParams.set("access_token", getStreetImageryConfig().mapillaryToken);
|
|
1703
|
+
const response = await fetch(url);
|
|
1704
|
+
if (!response.ok) {
|
|
1705
|
+
throw new Error(`Mapillary thumbnail failed (${response.status})`);
|
|
1706
|
+
}
|
|
1707
|
+
const data = await response.json();
|
|
1708
|
+
return data.thumb_1024_url ?? null;
|
|
1709
|
+
};
|
|
1710
|
+
var fetchPanoramaxThumbnail = async (sequenceId, photoId) => {
|
|
1711
|
+
const base = getStreetImageryConfig().panoramaxApiBase;
|
|
1712
|
+
const response = await fetch(
|
|
1713
|
+
`${base}/api/collections/${encodeURIComponent(sequenceId)}/items/${encodeURIComponent(photoId)}`
|
|
1714
|
+
);
|
|
1715
|
+
if (!response.ok) {
|
|
1716
|
+
throw new Error(`Panoramax thumbnail failed (${response.status})`);
|
|
1717
|
+
}
|
|
1718
|
+
const data = await response.json();
|
|
1719
|
+
return data.assets?.sd?.href ?? data.assets?.hd?.href ?? null;
|
|
1720
|
+
};
|
|
1721
|
+
var fetchMapilioThumbnail = async (sequenceId, photoId) => {
|
|
1722
|
+
const match = await fetchMapilioSequenceItem(sequenceId, photoId);
|
|
1723
|
+
if (!match?.uploaded_hash || !match.filename) {
|
|
1724
|
+
return null;
|
|
1725
|
+
}
|
|
1726
|
+
return mapilioCdnUrl(match.uploaded_hash, match.filename, "1080");
|
|
1727
|
+
};
|
|
1728
|
+
var fetchMapilioFullUrl = async (sequenceId, photoId) => {
|
|
1729
|
+
const match = await fetchMapilioSequenceItem(sequenceId, photoId);
|
|
1730
|
+
if (!match?.uploaded_hash || !match.filename) {
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
return mapilioCdnUrl(match.uploaded_hash, match.filename, "2080");
|
|
1734
|
+
};
|
|
1735
|
+
var resolvePhotoFullUrl = async (photo) => {
|
|
1736
|
+
if (photo.providerId === "streetview" || photo.providerId === "lookaround") {
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
if (photo.fullUrl) {
|
|
1740
|
+
return photo.fullUrl;
|
|
1741
|
+
}
|
|
1742
|
+
switch (photo.providerId) {
|
|
1743
|
+
case "mapilio":
|
|
1744
|
+
if (!photo.sequenceId) {
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
return fetchMapilioFullUrl(photo.sequenceId, photo.photoId);
|
|
1748
|
+
case "kartaview":
|
|
1749
|
+
case "vegbilder":
|
|
1750
|
+
return photo.thumbUrl ?? null;
|
|
1751
|
+
case "mapillary": {
|
|
1752
|
+
throw new Error('Not implemented yet: "mapillary" case');
|
|
1753
|
+
}
|
|
1754
|
+
case "mapillary-map-features": {
|
|
1755
|
+
throw new Error('Not implemented yet: "mapillary-map-features" case');
|
|
1756
|
+
}
|
|
1757
|
+
case "mapillary-signs": {
|
|
1758
|
+
throw new Error('Not implemented yet: "mapillary-signs" case');
|
|
1759
|
+
}
|
|
1760
|
+
case "panoramax": {
|
|
1761
|
+
throw new Error('Not implemented yet: "panoramax" case');
|
|
1762
|
+
}
|
|
1763
|
+
case "streetside": {
|
|
1764
|
+
throw new Error('Not implemented yet: "streetside" case');
|
|
1765
|
+
}
|
|
1766
|
+
default:
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
var resolvePhotoPanoramaUrl = async (photo) => {
|
|
1771
|
+
const fullUrl = await resolvePhotoFullUrl(photo);
|
|
1772
|
+
if (fullUrl) {
|
|
1773
|
+
return fullUrl;
|
|
1774
|
+
}
|
|
1775
|
+
return photo.thumbUrl ?? null;
|
|
1776
|
+
};
|
|
1777
|
+
var resolvePhotoThumbnailUrl = async (photo) => {
|
|
1778
|
+
if (photo.providerId === "streetview" || photo.providerId === "lookaround") {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
if (photo.thumbUrl) {
|
|
1782
|
+
if (photo.providerId === "streetside") {
|
|
1783
|
+
return buildStreetsidePreviewUrl(photo.thumbUrl);
|
|
1784
|
+
}
|
|
1785
|
+
return photo.thumbUrl;
|
|
1786
|
+
}
|
|
1787
|
+
switch (photo.providerId) {
|
|
1788
|
+
case "mapillary":
|
|
1789
|
+
return fetchMapillaryThumbnail(photo.photoId);
|
|
1790
|
+
case "panoramax":
|
|
1791
|
+
if (!photo.sequenceId) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
return fetchPanoramaxThumbnail(photo.sequenceId, photo.photoId);
|
|
1795
|
+
case "mapilio":
|
|
1796
|
+
if (!photo.sequenceId) {
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
return fetchMapilioThumbnail(photo.sequenceId, photo.photoId);
|
|
1800
|
+
// These providers deliver thumbUrl with the photo data; without it there is no fallback fetch.
|
|
1801
|
+
case "kartaview":
|
|
1802
|
+
case "streetside":
|
|
1803
|
+
case "vegbilder":
|
|
1804
|
+
return null;
|
|
1805
|
+
case "mapillary-map-features":
|
|
1806
|
+
case "mapillary-signs":
|
|
1807
|
+
return null;
|
|
1808
|
+
default:
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
var photoFullUrlQueryKey = (photo) => ["photo-full-url", photo.providerId, photo.sequenceId, photo.photoId];
|
|
1813
|
+
var photoThumbnailQueryKey = (photo) => ["photo-thumbnail", photo.providerId, photo.sequenceId, photo.photoId];
|
|
1814
|
+
|
|
1815
|
+
export { DEFAULT_PROVIDER_IDS, FLAT_VIEWFIELD_FOV_DEG, PANO_VIEWFIELD_FOV_DEG, PROVIDERS, PROVIDER_ADAPTERS, PROVIDER_IDS, STREETSIDE_NEARBY_LIMIT, adapterById, bboxIntersects, buildMapFeatureLayerFilter, buildPhotoLayerFilter, buildStreetsidePreviewUrl, clampZoom, clearTileCache, clickRadiusMeters, collectNearbyStreetsidePhotos, collectSettledTiles, coneRadiusMeters, createStreetImageryConfig, dedupeNearbyPhotos, distanceToPhoto, emptyLineCollection, emptyPointCollection, emptyPolygonCollection, featureLayerId, featureSourceId, fetchMapilioFullUrl, fetchMapilioSequenceItem, fetchMapilioThumbnail, fetchMapillaryMvtTiles, fetchMapillaryThumbnail, fetchMvt, fetchPanoramaxThumbnail, fetchStreetViewMetadata, fetchTileCached, findGroupBySelection, findNearestPhoto, findPhotoIndexInGroup, flatViewfieldTriangle, getGoogleMapsApiKey, getStreetImageryConfig, getTileCacheKey, groupClickedPhotos, haversineDistanceMeters, isBrowserAvailableProvider, isClickOnlyPhotoProvider, isMapFeatureProviderId, isNullIslandTile, isPhotoProviderId, latToTileY, lonToTileX, lookAroundDeepLink, mapFeatureExternalLink, mapFeatureMatchesDateRange, mapFeaturesToFeatureCollection, mapillaryTileUrl, normalizeDateRange, parseIsoDateEndMs, parseIsoDateStartMs, photoFullUrlQueryKey, photoGroupSequenceId, photoLayerId, photoMatchesDateRange, photoMatchesFilters, photoMatchesPhotoTypes, photoSourceId, photoThumbnailQueryKey, photoTypesFilter, photosToFeatureCollection, photosToViewfieldsFeatureCollection, pointLngLat, providerById, providerExternalLink, providerLocationLink, resolvePhotoFullUrl, resolvePhotoPanoramaUrl, resolvePhotoThumbnailUrl, sequenceGroupKey, sequenceLayerId, sequenceSourceId, sequencesToFeatureCollection, setStreetImageryConfig, tileBbox, tilePixelSizeAtZoom, tileToLat, tileToLon, tilesForBbox, viewConeGeoJson, viewfieldLayerId, viewfieldLineLayerId, viewfieldSourceId };
|