mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 08:42:31 +00:00
overpass layers progress
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import { TramFront } from 'lucide-static';
|
import { TramFront, Utensils, ShoppingBasket, Droplet, ShowerHead, Fuel, CircleParking, Fence, FerrisWheel, Telescope, Bed, Mountain, Pickaxe, Store, TrainFront, Bus, Ship, Croissant } from 'lucide-static';
|
||||||
import { type AnySourceData, type Style } from 'mapbox-gl';
|
import { type AnySourceData, type Style } from 'mapbox-gl';
|
||||||
|
|
||||||
export const basemaps: { [key: string]: string | Style; } = {
|
export const basemaps: { [key: string]: string | Style; } = {
|
||||||
@@ -544,8 +544,38 @@ export const overlayTree: LayerTreeType = {
|
|||||||
// Hierachy containing all Overpass layers
|
// Hierachy containing all Overpass layers
|
||||||
export const overpassTree: LayerTreeType = {
|
export const overpassTree: LayerTreeType = {
|
||||||
points_of_interest: {
|
points_of_interest: {
|
||||||
transport: {
|
food: {
|
||||||
tram: true,
|
bakery: true,
|
||||||
|
"food-store": true,
|
||||||
|
"eat-and-drink": true,
|
||||||
|
},
|
||||||
|
amenities: {
|
||||||
|
toilets: true,
|
||||||
|
"water": true,
|
||||||
|
"water-spring": true,
|
||||||
|
shower: true,
|
||||||
|
"fuel-station": true,
|
||||||
|
parking: true,
|
||||||
|
barrier: true
|
||||||
|
},
|
||||||
|
tourism: {
|
||||||
|
attraction: true,
|
||||||
|
viewpoint: true,
|
||||||
|
sleep: true,
|
||||||
|
summit: true,
|
||||||
|
pass: true,
|
||||||
|
climbing: true,
|
||||||
|
},
|
||||||
|
bicycle: {
|
||||||
|
"bicycle-parking": true,
|
||||||
|
"bicycle-rental": true,
|
||||||
|
"bicycle-shop": true
|
||||||
|
},
|
||||||
|
"public-transport": {
|
||||||
|
"railway-station": true,
|
||||||
|
"tram-stop": true,
|
||||||
|
"bus-stop": true,
|
||||||
|
ferry: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -598,8 +628,38 @@ export const defaultOverlays = {
|
|||||||
// Default Overpass queries used (none)
|
// Default Overpass queries used (none)
|
||||||
export const defaultOverpassQueries: LayerTreeType = {
|
export const defaultOverpassQueries: LayerTreeType = {
|
||||||
points_of_interest: {
|
points_of_interest: {
|
||||||
transport: {
|
"food": {
|
||||||
tram: false,
|
bakery: false,
|
||||||
|
"food-store": false,
|
||||||
|
"eat-and-drink": false,
|
||||||
|
},
|
||||||
|
amenities: {
|
||||||
|
toilets: false,
|
||||||
|
"water": false,
|
||||||
|
"water-spring": false,
|
||||||
|
shower: false,
|
||||||
|
"fuel-station": false,
|
||||||
|
parking: false,
|
||||||
|
barrier: false
|
||||||
|
},
|
||||||
|
tourism: {
|
||||||
|
attraction: false,
|
||||||
|
viewpoint: false,
|
||||||
|
sleep: false,
|
||||||
|
summit: false,
|
||||||
|
pass: false,
|
||||||
|
climbing: false
|
||||||
|
},
|
||||||
|
bicycle: {
|
||||||
|
"bicycle-parking": false,
|
||||||
|
"bicycle-rental": false,
|
||||||
|
"bicycle-shop": false
|
||||||
|
},
|
||||||
|
"public-transport": {
|
||||||
|
"railway-station": false,
|
||||||
|
"tram-stop": false,
|
||||||
|
"bus-stop": false,
|
||||||
|
ferry: false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -702,8 +762,38 @@ export const defaultOverlayTree: LayerTreeType = {
|
|||||||
// Default Overpass queries shown in the layer menu
|
// Default Overpass queries shown in the layer menu
|
||||||
export const defaultOverpassTree: LayerTreeType = {
|
export const defaultOverpassTree: LayerTreeType = {
|
||||||
points_of_interest: {
|
points_of_interest: {
|
||||||
transport: {
|
"food": {
|
||||||
tram: true,
|
bakery: true,
|
||||||
|
"food-store": true,
|
||||||
|
"eat-and-drink": true,
|
||||||
|
},
|
||||||
|
amenities: {
|
||||||
|
toilets: true,
|
||||||
|
"water": true,
|
||||||
|
"water-spring": true,
|
||||||
|
shower: true,
|
||||||
|
"fuel-station": false,
|
||||||
|
parking: false,
|
||||||
|
barrier: false
|
||||||
|
},
|
||||||
|
tourism: {
|
||||||
|
attraction: true,
|
||||||
|
viewpoint: true,
|
||||||
|
sleep: true,
|
||||||
|
summit: true,
|
||||||
|
pass: true,
|
||||||
|
climbing: false
|
||||||
|
},
|
||||||
|
bicycle: {
|
||||||
|
"bicycle-parking": true,
|
||||||
|
"bicycle-rental": true,
|
||||||
|
"bicycle-shop": true
|
||||||
|
},
|
||||||
|
"public-transport": {
|
||||||
|
"railway-station": true,
|
||||||
|
"tram-stop": true,
|
||||||
|
"bus-stop": true,
|
||||||
|
ferry: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -718,18 +808,223 @@ export type CustomLayer = {
|
|||||||
value: string | {},
|
value: string | {},
|
||||||
};
|
};
|
||||||
|
|
||||||
type OverpassQuery = Record<string, string | undefined>;
|
type OverpassQueryData = {
|
||||||
|
icon: {
|
||||||
export const overpassQueries: Record<string, OverpassQuery> = {
|
svg: string,
|
||||||
tram: {
|
color: string,
|
||||||
railway: 'tram_stop',
|
|
||||||
},
|
},
|
||||||
|
tags: Record<string, string | boolean | string[]>,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const overpassIcons: Record<string, { svg: string, color: string }> = {
|
export const overpassQueryData: Record<string, OverpassQueryData> = {
|
||||||
tram: {
|
"bakery": {
|
||||||
svg: TramFront,
|
icon: {
|
||||||
color: '#000000',
|
svg: Croissant,
|
||||||
|
color: "Coral",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
shop: "bakery"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"food-store": {
|
||||||
|
icon: {
|
||||||
|
svg: ShoppingBasket,
|
||||||
|
color: "Coral",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
shop: ["supermarket", "convenience"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eat-and-drink": {
|
||||||
|
icon: {
|
||||||
|
svg: Utensils,
|
||||||
|
color: "Coral",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: ["restaurant", "fast_food", "cafe", "pub", "bar"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toilets": {
|
||||||
|
icon: {
|
||||||
|
svg: Droplet,
|
||||||
|
color: "DeepSkyBlue",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: "toilets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
water: {
|
||||||
|
icon: {
|
||||||
|
svg: Droplet,
|
||||||
|
color: "DeepSkyBlue",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: ["drinking_water", "water_point"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"water-spring": {
|
||||||
|
icon: {
|
||||||
|
svg: Droplet,
|
||||||
|
color: "DeepSkyBlue",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
natural: "spring",
|
||||||
|
drinking_water: "yes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shower: {
|
||||||
|
icon: {
|
||||||
|
svg: ShowerHead,
|
||||||
|
color: "DeepSkyBlue",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: "shower"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fuel-station": {
|
||||||
|
icon: {
|
||||||
|
svg: Fuel,
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: "fuel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parking: {
|
||||||
|
icon: {
|
||||||
|
svg: CircleParking,
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: "parking"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
barrier: {
|
||||||
|
icon: {
|
||||||
|
svg: Fence,
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
barrier: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attraction: {
|
||||||
|
icon: {
|
||||||
|
svg: FerrisWheel,
|
||||||
|
color: "Green",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: "attraction"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewpoint: {
|
||||||
|
icon: {
|
||||||
|
svg: Telescope,
|
||||||
|
color: "Green",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: "viewpoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sleep: {
|
||||||
|
icon: {
|
||||||
|
svg: Bed,
|
||||||
|
color: "Green",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: ["hotel", "hostel", "guest_house", "motel", "camp_site", "alpine_hut", "wilderness_hut"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
summit: {
|
||||||
|
icon: {
|
||||||
|
svg: Mountain,
|
||||||
|
color: "Green",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
natural: "peak"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pass: {
|
||||||
|
icon: {
|
||||||
|
svg: Mountain,
|
||||||
|
color: "Green",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
mountain_pass: "yes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
climbing: {
|
||||||
|
icon: {
|
||||||
|
svg: Pickaxe,
|
||||||
|
color: "Green",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
sport: "climbing"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bicycle-parking": {
|
||||||
|
icon: {
|
||||||
|
svg: CircleParking,
|
||||||
|
color: "HotPink",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: "bicycle_parking"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bicycle-rental": {
|
||||||
|
icon: {
|
||||||
|
svg: Store,
|
||||||
|
color: "HotPink",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: "bicycle_rental"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bicycle-shop": {
|
||||||
|
icon: {
|
||||||
|
svg: Store,
|
||||||
|
color: "HotPink",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
shop: "bicycle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"railway-station": {
|
||||||
|
icon: {
|
||||||
|
svg: TrainFront,
|
||||||
|
color: "DarkBlue",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
railway: "station"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tram-stop": {
|
||||||
|
icon: {
|
||||||
|
svg: TramFront,
|
||||||
|
color: 'DarkBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
railway: "tram_stop"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"bus-stop": {
|
||||||
|
icon: {
|
||||||
|
svg: Bus,
|
||||||
|
color: "DarkBlue",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
"public_transport": ["stop_position", "platform"],
|
||||||
|
bus: "yes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ferry: {
|
||||||
|
icon: {
|
||||||
|
svg: Ship,
|
||||||
|
color: "DarkBlue",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: "ferry_terminal"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -13,6 +13,7 @@
|
|||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { getLayers } from './utils';
|
import { getLayers } from './utils';
|
||||||
import { OverpassLayer } from './OverpassLayer';
|
import { OverpassLayer } from './OverpassLayer';
|
||||||
|
import OverpassPopup from './OverpassPopup.svelte';
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let overpassLayer: OverpassLayer;
|
let overpassLayer: OverpassLayer;
|
||||||
@@ -186,6 +187,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</CustomControl>
|
</CustomControl>
|
||||||
|
|
||||||
|
<OverpassPopup />
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
if (open && !cancelEvents && !container.contains(e.target)) {
|
if (open && !cancelEvents && !container.contains(e.target)) {
|
||||||
|
@@ -4,7 +4,7 @@ import mapboxgl from "mapbox-gl";
|
|||||||
import { get, writable } from "svelte/store";
|
import { get, writable } from "svelte/store";
|
||||||
import { liveQuery } from "dexie";
|
import { liveQuery } from "dexie";
|
||||||
import { db, settings } from "$lib/db";
|
import { db, settings } from "$lib/db";
|
||||||
import { overpassIcons, overpassQueries } from "$lib/assets/layers";
|
import { overpassQueryData } from "$lib/assets/layers";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentOverpassQueries
|
currentOverpassQueries
|
||||||
@@ -14,6 +14,14 @@ const mercator = new SphericalMercator({
|
|||||||
size: 256,
|
size: 256,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const overpassPopupPOI = writable<Record<string, any> | null>(null);
|
||||||
|
|
||||||
|
export const overpassPopup = new mapboxgl.Popup({
|
||||||
|
closeButton: false,
|
||||||
|
maxWidth: undefined,
|
||||||
|
offset: 15,
|
||||||
|
});
|
||||||
|
|
||||||
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
||||||
|
|
||||||
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
||||||
@@ -27,10 +35,13 @@ export class OverpassLayer {
|
|||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
|
|
||||||
currentQueries: Set<string> = new Set();
|
currentQueries: Set<string> = new Set();
|
||||||
|
nextQueries: Map<string, { x: number, y: number, queries: string[] }> = new Map();
|
||||||
|
|
||||||
unsubscribes: (() => void)[] = [];
|
unsubscribes: (() => void)[] = [];
|
||||||
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
||||||
updateBinded = this.update.bind(this);
|
updateBinded = this.update.bind(this);
|
||||||
|
onHoverBinded = this.onHover.bind(this);
|
||||||
|
maybeHidePopupBinded = this.maybeHidePopup.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map) {
|
constructor(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
@@ -45,8 +56,6 @@ export class OverpassLayer {
|
|||||||
this.queryIfNeededBinded();
|
this.queryIfNeededBinded();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.map.showTileBoundaries = true;
|
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +88,13 @@ export class OverpassLayer {
|
|||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'overpass',
|
source: 'overpass',
|
||||||
layout: {
|
layout: {
|
||||||
'icon-image': ['get', 'query'],
|
'icon-image': ['get', 'icon'],
|
||||||
'icon-size': 0.25,
|
'icon-size': 0.25,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
||||||
|
this.map.on('click', 'overpass', this.onHoverBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()]);
|
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()]);
|
||||||
@@ -105,6 +117,27 @@ export class OverpassLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onHover(e: any) {
|
||||||
|
overpassPopupPOI.set(e.features[0].properties);
|
||||||
|
overpassPopup.setLngLat(e.features[0].geometry.coordinates);
|
||||||
|
overpassPopup.addTo(this.map);
|
||||||
|
this.map.on('mousemove', this.maybeHidePopupBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeHidePopup(e: any) {
|
||||||
|
let poi = get(overpassPopupPOI);
|
||||||
|
if (poi && this.map.project([poi.lon, poi.lat]).dist(this.map.project(e.lngLat)) > 100) {
|
||||||
|
this.hideWaypointPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideWaypointPopup() {
|
||||||
|
overpassPopupPOI.set(null);
|
||||||
|
overpassPopup.remove();
|
||||||
|
|
||||||
|
this.map.off('mousemove', this.maybeHidePopupBinded);
|
||||||
|
}
|
||||||
|
|
||||||
query(bbox: [number, number, number, number]) {
|
query(bbox: [number, number, number, number]) {
|
||||||
let queries = getCurrentQueries();
|
let queries = getCurrentQueries();
|
||||||
if (queries.length === 0) {
|
if (queries.length === 0) {
|
||||||
@@ -130,11 +163,23 @@ export class OverpassLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queryTile(x: number, y: number, queries: string[]) {
|
queryTile(x: number, y: number, queries: string[]) {
|
||||||
|
if (this.currentQueries.size > 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.currentQueries.add(`${x},${y}`);
|
this.currentQueries.add(`${x},${y}`);
|
||||||
|
|
||||||
const bounds = mercator.bbox(x, y, this.queryZoom);
|
const bounds = mercator.bbox(x, y, this.queryZoom);
|
||||||
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
|
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
|
||||||
.then((response) => response.json())
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
try {
|
||||||
|
return response.json();
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
this.currentQueries.delete(`${x},${y}`);
|
||||||
|
return Promise.reject();
|
||||||
|
}, () => (this.currentQueries.delete(`${x},${y}`)))
|
||||||
.then((data) => this.storeOverpassData(x, y, queries, data));
|
.then((data) => this.storeOverpassData(x, y, queries, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +187,10 @@ export class OverpassLayer {
|
|||||||
let queryTiles = queries.map((query) => ({ x, y, query }));
|
let queryTiles = queries.map((query) => ({ x, y, query }));
|
||||||
let pois: { query: string, id: number, poi: GeoJSON.Feature }[] = [];
|
let pois: { query: string, id: number, poi: GeoJSON.Feature }[] = [];
|
||||||
|
|
||||||
|
if (data.elements === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let element of data.elements) {
|
for (let element of data.elements) {
|
||||||
for (let query of queries) {
|
for (let query of queries) {
|
||||||
if (belongsToQuery(element, query)) {
|
if (belongsToQuery(element, query)) {
|
||||||
@@ -152,13 +201,18 @@ export class OverpassLayer {
|
|||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [element.lon, element.lat],
|
coordinates: element.center ? [element.center.lon, element.center.lat] : [element.lon, element.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
id: element.id,
|
||||||
|
lat: element.center ? element.center.lat : element.lat,
|
||||||
|
lon: element.center ? element.center.lon : element.lon,
|
||||||
|
query: query,
|
||||||
|
icon: `overpass-${query}`,
|
||||||
|
tags: element.tags
|
||||||
},
|
},
|
||||||
properties: { query, tags: element.tags },
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,17 +228,21 @@ export class OverpassLayer {
|
|||||||
loadIcons() {
|
loadIcons() {
|
||||||
let currentQueries = getCurrentQueries();
|
let currentQueries = getCurrentQueries();
|
||||||
currentQueries.forEach((query) => {
|
currentQueries.forEach((query) => {
|
||||||
if (!this.map.hasImage(query)) {
|
if (!this.map.hasImage(`overpass-${query}`)) {
|
||||||
let icon = new Image(100, 100);
|
let icon = new Image(100, 100);
|
||||||
icon.onload = () => this.map.addImage(query, icon);
|
icon.onload = () => {
|
||||||
|
if (!this.map.hasImage(`overpass-${query}`)) {
|
||||||
|
this.map.addImage(`overpass-${query}`, icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
// Lucide icons are SVG files with a 24x24 viewBox
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
||||||
icon.src = 'data:image/svg+xml,' + encodeURIComponent(`
|
icon.src = 'data:image/svg+xml,' + encodeURIComponent(`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
<circle cx="20" cy="20" r="20" fill="${overpassIcons[query].color}" />
|
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
||||||
<g transform="translate(8 8)">
|
<g transform="translate(8 8)">
|
||||||
${overpassIcons[query].svg.replace('stroke="currentColor"', 'stroke="white"')}
|
${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
`);
|
`);
|
||||||
@@ -194,22 +252,29 @@ export class OverpassLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getQueryForBounds(bounds: [number, number, number, number], queries: string[]) {
|
function getQueryForBounds(bounds: [number, number, number, number], queries: string[]) {
|
||||||
return `[bbox:${bounds[1]},${bounds[0]},${bounds[3]},${bounds[2]}][out:json];(${getQueries(queries)});out;`;
|
return `[bbox:${bounds[1]},${bounds[0]},${bounds[3]},${bounds[2]}][out:json];(${getQueries(queries)});out center;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueries(queries: string[]) {
|
function getQueries(queries: string[]) {
|
||||||
return queries.map((query) => `node${getQuery(query)};`).join('');
|
return queries.map((query) => getQuery(query)).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuery(query: string) {
|
function getQuery(query: string) {
|
||||||
return Object.entries(overpassQueries[query])
|
let arrayEntry = Object.entries(overpassQueryData[query].tags).find(([_, value]) => Array.isArray(value));
|
||||||
.map(([tag, value]) => value ? `[${tag}=${value}]` : `[${tag}]`)
|
if (arrayEntry !== undefined) {
|
||||||
.join('');
|
return arrayEntry[1].map((val) => `nwr${Object.entries(overpassQueryData[query].tags)
|
||||||
|
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
|
||||||
|
.join('')};`).join('');
|
||||||
|
} else {
|
||||||
|
return `nwr${Object.entries(overpassQueryData[query].tags)
|
||||||
|
.map(([tag, value]) => `[${tag}=${value}]`)
|
||||||
|
.join('')};`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function belongsToQuery(element: any, query: string) {
|
function belongsToQuery(element: any, query: string) {
|
||||||
return Object.entries(overpassQueries[query])
|
return Object.entries(overpassQueryData[query].tags)
|
||||||
.every(([tag, value]) => value ? element.tags[tag] === value : element.tags[tag]);
|
.every(([tag, value]) => Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentQueries() {
|
function getCurrentQueries() {
|
||||||
|
@@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { overpassPopup, overpassPopupPOI } from './OverpassLayer';
|
||||||
|
import { PencilLine } from 'lucide-svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
let popupElement: HTMLDivElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
overpassPopup.setDOMContent(popupElement);
|
||||||
|
popupElement.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
let tags = {};
|
||||||
|
$: if ($overpassPopupPOI) {
|
||||||
|
tags = JSON.parse($overpassPopupPOI.tags);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={popupElement} class="hidden">
|
||||||
|
{#if $overpassPopupPOI}
|
||||||
|
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
|
||||||
|
<Card.Header class="p-0">
|
||||||
|
<Card.Title class="text-md">
|
||||||
|
<div class="flex flex-row gap-3">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{tags.name ?? ''}
|
||||||
|
<div class="text-muted-foreground text-sm font-normal">
|
||||||
|
{$overpassPopupPOI.lat.toFixed(6)}° {$overpassPopupPOI.lon.toFixed(6)}°
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
class="ml-auto p-1.5 h-8"
|
||||||
|
variant="outline"
|
||||||
|
href="https://www.openstreetmap.org/edit?editor=id&node={$overpassPopupPOI.id}"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<PencilLine size="16" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
{#if tags.image || tags['image:0']}
|
||||||
|
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
||||||
|
<img src={tags.image ?? tags['image:0']} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Card.Content
|
||||||
|
class="grid grid-cols-[auto_auto] gap-x-3 p-0 text-sm mt-1 whitespace-normal break-all"
|
||||||
|
>
|
||||||
|
{#each Object.entries(tags) as [key, value]}
|
||||||
|
{#if key !== 'name' && !key.includes('image')}
|
||||||
|
<span class="font-mono">{key}</span>
|
||||||
|
{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
|
||||||
|
<a href={value} target="_blank" class="text-blue-500 underline">{value}</a>
|
||||||
|
{:else if key === 'phone' || key === 'contact:phone'}
|
||||||
|
<a href={'tel:' + value} class="text-blue-500 underline">{value}</a>
|
||||||
|
{:else if key === 'email' || key === 'contact:email'}
|
||||||
|
<a href={'mailto:' + value} class="text-blue-500 underline">{value}</a>
|
||||||
|
{:else}
|
||||||
|
<span>{value}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@@ -285,7 +285,36 @@
|
|||||||
"waymarkedTrailsMTB": "MTB",
|
"waymarkedTrailsMTB": "MTB",
|
||||||
"waymarkedTrailsSkating": "Skating",
|
"waymarkedTrailsSkating": "Skating",
|
||||||
"waymarkedTrailsHorseRiding": "Horse Riding",
|
"waymarkedTrailsHorseRiding": "Horse Riding",
|
||||||
"waymarkedTrailsWinter": "Winter"
|
"waymarkedTrailsWinter": "Winter",
|
||||||
|
"points_of_interest": "Points of interest",
|
||||||
|
"food": "Food",
|
||||||
|
"bakery": "Bakery",
|
||||||
|
"food-store": "Food Store",
|
||||||
|
"eat-and-drink": "Eat and Drink",
|
||||||
|
"amenities": "Amenities",
|
||||||
|
"toilets": "Toilets",
|
||||||
|
"water": "Water",
|
||||||
|
"water-spring": "Water Spring",
|
||||||
|
"shower": "Shower",
|
||||||
|
"fuel-station": "Fuel Station",
|
||||||
|
"parking": "Parking",
|
||||||
|
"barrier": "Barrier",
|
||||||
|
"tourism": "Tourism",
|
||||||
|
"attraction": "Attraction",
|
||||||
|
"viewpoint": "Viewpoint",
|
||||||
|
"sleep": "Sleep",
|
||||||
|
"summit": "Summit",
|
||||||
|
"pass": "Pass",
|
||||||
|
"climbing": "Climbing",
|
||||||
|
"bicycle": "Bicycle",
|
||||||
|
"bicycle-parking": "Bicycle Parking",
|
||||||
|
"bicycle-rental": "Bicycle Rental",
|
||||||
|
"bicycle-shop": "Bicycle Shop",
|
||||||
|
"public-transport": "Public Transport",
|
||||||
|
"railway-station": "Railway Station",
|
||||||
|
"tram-stop": "Tram Stop",
|
||||||
|
"bus-stop": "Bus Stop",
|
||||||
|
"ferry": "Ferry"
|
||||||
},
|
},
|
||||||
"color": {
|
"color": {
|
||||||
"blue": "Blue",
|
"blue": "Blue",
|
||||||
|
Reference in New Issue
Block a user