This commit is contained in:
vcoppe committed 2026-10-08 18:34:19 +02:00
1 parent 606a0e974a
commit 67a4c090bd
107 files changed
+3474 -3948

No files matched your search

@@ -1,5 +1,4 @@
import { get, type Readable } from 'svelte/store';
import type { GPXFile } from 'gpx';
import maplibregl, {
type GeoJSONSource,
type FilterSpecification,
@@ -13,7 +12,6 @@ import { getElevation, loadSVGIcon } from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { fileStateCollection } from '$lib/logic/file-state';
import { engine, type FileState, type Selection } from '$lib/engine';
import { isCovered, hasSelectionWithin, type FileTreeNode } from '$lib/selection-helpers';
import { isSegmentHidden, isWaypointHidden } from '$lib/file-visibility';
@@ -615,7 +613,7 @@ export class GPXLayer {
getElevation([
{
lat: e.lngLat.lat,
lon: e.lngLat.lng,
lng: e.lngLat.lng,
},
]).then(async (ele) => {
if (this.draggedWaypointIndex === null) {
@@ -1,12 +1,11 @@
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { hoveredPoint } from '$lib/logic/statistics';
import type { GeoJSONSource } from 'maplibre-gl';
import { get } from 'svelte/store';
import { map } from '$lib/components/map/map';
import { allHidden } from '$lib/all-hidden';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import { loadSVGIcon } from '$lib/utils';
import { slicedStatistics } from '$lib/logic/selection-statistics';
import { hoveredPoint, slicedStatistics } from '$lib/logic/selection-statistics';
import { engine } from '$lib/engine';
const startMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
@@ -93,7 +92,7 @@ export class StartEndMarkers {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [hovered.lon, hovered.lat],
coordinates: [hovered.lng, hovered.lat],
},
properties: {
icon: 'hover-marker',
@@ -33,7 +33,7 @@
const desc = Object.entries(tags)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
const [ele] = await getElevation([{ lat: poi.item.lat, lon: poi.item.lng }]);
const [ele] = await getElevation([{ lat: poi.item.lat, lng: poi.item.lng }]);
await engine.newWaypoint({
lng: poi.item.lng,
lat: poi.item.lat,
@@ -1,39 +1,38 @@
import { SphericalMercator } from '@mapbox/sphericalmercator';
import { getLayers } from './utils';
import { get, writable } from 'svelte/store';
import { liveQuery } from 'dexie';
import { overpassQueryData } from '$lib/assets/layers';
import { MapPopup } from '$lib/components/map/map-popup';
import { settings } from '$lib/logic/settings';
import { db } from '$lib/db';
import type { GeoJSONSource } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
import { loadSVGIcon } from '$lib/utils';
import { cachedFetch } from '$lib/cached-fetch';
const { currentOverpassQueries } = settings;
/** The responses of Overpass are kept in the browser for a week. */
const CACHE = { name: 'overpass', maxAge: 7 * 24 * 3600 * 1000 };
const mercator = new SphericalMercator({
size: 256,
});
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
data.set({ type: 'FeatureCollection', features: pois.map((poi) => poi.poi) });
});
export class OverpassLayer {
overpassUrl = 'https://overpass.gpx.studio/api/interpreter';
minZoom = 12;
queryZoom = 12;
expirationTime = 7 * 24 * 3600 * 1000;
map: maplibregl.Map;
layerEventManager: MapLayerEventManager;
popup: MapPopup;
data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
// What was queried this session: the queries that were made for each tile. The responses are
// kept longer, see `cachedFetch`.
queriedTiles = new Map<string, Set<string>>();
currentQueries: Set<string> = new Set();
nextQueries: Map<string, { x: number; y: number; queries: string[] }> = new Map();
unsubscribes: (() => void)[] = [];
queryIfNeededBinded = this.queryIfNeeded.bind(this);
@@ -54,7 +53,7 @@ export class OverpassLayer {
add() {
this.map.on('moveend', this.queryIfNeededBinded);
this.map.on('style.load', this.updateBinded);
this.unsubscribes.push(data.subscribe(this.updateBinded));
this.unsubscribes.push(this.data.subscribe(this.updateBinded));
this.unsubscribes.push(
currentOverpassQueries.subscribe(() => {
this.updateBinded();
@@ -77,7 +76,7 @@ export class OverpassLayer {
update() {
this.loadIcons();
const fullData = get(data);
const fullData = get(this.data);
const queries = getCurrentQueries();
const d: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
@@ -158,7 +157,6 @@ export class OverpassLayer {
}
let tileLimits = mercator.xyz(bbox, this.queryZoom);
let time = Date.now();
for (let x = tileLimits.minX; x <= tileLimits.maxX; x++) {
for (let y = tileLimits.minY; y <= tileLimits.maxY; y++) {
@@ -166,66 +164,35 @@ export class OverpassLayer {
continue;
}
db.overpasstiles
.where('[x+y]')
.equals([x, y])
.toArray()
.then((querytiles) => {
let missingQueries = queries.filter(
(query) =>
!querytiles.some(
(querytile) =>
querytile.query === query &&
time - querytile.time < this.expirationTime
)
);
if (missingQueries.length > 0) {
this.queryTile(x, y, missingQueries);
}
});
const queried = this.queriedTiles.get(`${x},${y}`);
const missingQueries = queries.filter((query) => !queried?.has(query));
if (missingQueries.length > 0) {
this.queryTile(x, y, missingQueries);
}
}
}
}
queryTile(x: number, y: number, queries: string[]) {
if (this.currentQueries.size > 5) {
return;
}
this.currentQueries.add(`${x},${y}`);
const bounds = mercator.bbox(x, y, this.queryZoom);
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
.then(
(response) => {
if (response.ok) {
return response.json();
}
this.currentQueries.delete(`${x},${y}`);
return Promise.reject();
},
() => this.currentQueries.delete(`${x},${y}`)
)
cachedFetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`, CACHE)
.then((response) => (response.ok ? response.json() : Promise.reject()))
.then((data) => this.storeOverpassData(x, y, queries, data))
.catch(() => this.currentQueries.delete(`${x},${y}`));
}
storeOverpassData(x: number, y: number, queries: string[], data: any) {
let time = Date.now();
let queryTiles = queries.map((query) => ({ x, y, query, time }));
let pois: { query: string; id: number; poi: GeoJSON.Feature }[] = [];
if (data.elements === undefined) {
return;
}
for (let element of data.elements) {
for (let query of queries) {
if (belongsToQuery(element, query)) {
pois.push({
query,
id: element.id,
poi: {
this.data.update((updatedData) => {
for (let element of data.elements) {
for (let query of queries) {
if (belongsToQuery(element, query)) {
updatedData.features.push({
type: 'Feature',
geometry: {
type: 'Point',
@@ -242,17 +209,17 @@ export class OverpassLayer {
tags: element.tags,
type: element.type,
},
},
});
});
}
}
}
}
db.transaction('rw', db.overpasstiles, db.overpassdata, async () => {
await db.overpasstiles.bulkPut(queryTiles);
await db.overpassdata.bulkPut(pois);
return updatedData;
});
const queried = this.queriedTiles.get(`${x},${y}`) ?? new Set<string>();
queries.forEach((query) => queried.add(query));
this.queriedTiles.set(`${x},${y}`, queried);
this.currentQueries.delete(`${x},${y}`);
}
@@ -1,4 +1,5 @@
import { fileStateCollection } from '$lib/logic/file-state';
import { engine } from '$lib/engine';
import { get } from 'svelte/store';
import maplibregl from 'maplibre-gl';
type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
@@ -246,17 +247,23 @@ export class MapLayerEventManager {
layerIds: string[],
bounds: maplibregl.LngLatBounds
): string[] {
const files = get(engine.files);
let result = layerIds.filter((layerId) => {
if (!this._map.getLayer(layerId)) return false;
const fileId = layerId.replace('-waypoints', '');
if (fileId === layerId) {
return fileStateCollection.getStatistics(fileId)?.intersectsBBox(bounds) ?? true;
} else {
return (
fileStateCollection.getStatistics(fileId)?.intersectsWaypointBBox(bounds) ??
true
);
// the tracks of a file are all within its bounds
const file = files.get(fileId);
const box = file && get(file).statistics.bounds;
return box
? bounds.getWest() <= box.east &&
bounds.getEast() >= box.west &&
bounds.getSouth() <= box.north &&
bounds.getNorth() >= box.south
: true;
}
// the waypoints have no bounds of their own
return true;
});
return result;
}