fixes POI overlay
This commit is contained in:
@@ -52,7 +52,7 @@
|
|||||||
class:hidden={!showSwitcher}
|
class:hidden={!showSwitcher}
|
||||||
style="transform: translateX(calc(-100% + 29px))"
|
style="transform: translateX(calc(-100% + 29px))"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-x-12">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-semibold whitespace-nowrap text-base"
|
<span class="font-semibold whitespace-nowrap text-base"
|
||||||
>{$_("map-style")}</span
|
>{$_("map-style")}</span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1094,7 +1094,7 @@
|
|||||||
map!.getCanvas().style.cursor = "";
|
map!.getCanvas().style.cursor = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on("load", () => {
|
map.on("load", () => {
|
||||||
initMap(true);
|
initMap(true);
|
||||||
layerManager.init();
|
layerManager.init();
|
||||||
oninit?.(map!);
|
oninit?.(map!);
|
||||||
|
|||||||
@@ -257,6 +257,43 @@ export function createPopupFromTrail(trail: Trail) {
|
|||||||
return popup;
|
return popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createOverpassPopup(tags: Record<string, string>, coordinates: GeoJSON.Position) {
|
||||||
|
const name = tags.name ?? "?"
|
||||||
|
|
||||||
|
const popupContainer = document.createElement("div");
|
||||||
|
popupContainer.className = "p-4"
|
||||||
|
|
||||||
|
const popupHeading = document.createElement("h1");
|
||||||
|
popupHeading.classList = "font-medium text-lg"
|
||||||
|
popupHeading.textContent = name;
|
||||||
|
|
||||||
|
const coordinateSubtitle = document.createElement("p")
|
||||||
|
coordinateSubtitle.classList = "text-gray-500"
|
||||||
|
coordinateSubtitle.textContent = `${coordinates[0].toFixed(6)}, ${coordinates[1].toFixed(6)}`
|
||||||
|
|
||||||
|
popupContainer.appendChild(popupHeading)
|
||||||
|
popupContainer.appendChild(coordinateSubtitle)
|
||||||
|
|
||||||
|
const tagsGrid = document.createElement("div")
|
||||||
|
tagsGrid.classList = "grid grid-cols-2 gap-x-4 mt-4"
|
||||||
|
|
||||||
|
Object.entries(tags).forEach((([k, v]) => {
|
||||||
|
if (k == "name") return;
|
||||||
|
const kSpan = document.createElement("span")
|
||||||
|
kSpan.classList = "font-mono"
|
||||||
|
kSpan.textContent = k;
|
||||||
|
const vSpan = document.createElement("span")
|
||||||
|
vSpan.textContent = v;
|
||||||
|
|
||||||
|
tagsGrid.appendChild(kSpan);
|
||||||
|
tagsGrid.appendChild(vSpan);
|
||||||
|
}));
|
||||||
|
|
||||||
|
popupContainer.appendChild(tagsGrid)
|
||||||
|
|
||||||
|
return popupContainer;
|
||||||
|
}
|
||||||
|
|
||||||
export function calculatePixelPerMeter(map: M.Map, meters: number) {
|
export function calculatePixelPerMeter(map: M.Map, meters: number) {
|
||||||
const y = map.getCanvas().getBoundingClientRect().y;
|
const y = map.getCanvas().getBoundingClientRect().y;
|
||||||
const x = map.getCanvas().getBoundingClientRect().x;
|
const x = map.getCanvas().getBoundingClientRect().x;
|
||||||
|
|||||||
30
web/src/lib/vendor/maplibre-layer-manager/debug-layer.ts
vendored
Normal file
30
web/src/lib/vendor/maplibre-layer-manager/debug-layer.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Marker, StyleSpecification, MapMouseEvent } from "maplibre-gl";
|
||||||
|
import type { BaseLayer } from "./layers";
|
||||||
|
import * as M from "maplibre-gl";
|
||||||
|
|
||||||
|
export class DebugLayer implements BaseLayer {
|
||||||
|
spec: StyleSpecification = {
|
||||||
|
version: 8,
|
||||||
|
name: "debug",
|
||||||
|
sources: {
|
||||||
|
debug: {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'debug-layer',
|
||||||
|
type: 'line',
|
||||||
|
source: 'debug',
|
||||||
|
paint: {
|
||||||
|
'line-color': '#ff0000',
|
||||||
|
'line-width': 2,
|
||||||
|
'line-dasharray': [2, 2],
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
356
web/src/lib/vendor/maplibre-layer-manager/layers.ts
vendored
356
web/src/lib/vendor/maplibre-layer-manager/layers.ts
vendored
@@ -1,16 +1,18 @@
|
|||||||
import Bakery from "$lib/assets/svgs/pois/bakery.svg?raw"
|
import Bakery from "$lib/assets/svgs/pois/bakery.svg?raw"
|
||||||
import type { MapMouseEvent, Marker, StyleSpecification } from "maplibre-gl"
|
import type { FilterSpecification, MapMouseEvent, Marker, StyleSpecification } from "maplibre-gl"
|
||||||
|
|
||||||
export interface BaseLayer {
|
export interface BaseLayer {
|
||||||
markers?: Record<string, Marker>,
|
markers?: Record<string, Marker>,
|
||||||
spec: StyleSpecification,
|
spec: StyleSpecification,
|
||||||
listener?: {
|
filter?: FilterSpecification,
|
||||||
|
listeners?: Record<string, {
|
||||||
onMouseUp?: (e: MapMouseEvent) => void,
|
onMouseUp?: (e: MapMouseEvent) => void,
|
||||||
onMouseDown?: (e: MapMouseEvent) => void,
|
onMouseDown?: (e: MapMouseEvent) => void,
|
||||||
onEnter?: (e: MapMouseEvent) => void,
|
onEnter?: (e: MapMouseEvent) => void,
|
||||||
onLeave?: (e: MapMouseEvent) => void,
|
onLeave?: (e: MapMouseEvent) => void,
|
||||||
onMouseMove?: (e: MapMouseEvent) => void,
|
onMouseMove?: (e: MapMouseEvent) => void,
|
||||||
}
|
}>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const baseMapStyles: Record<string, string | StyleSpecification> = {
|
export const baseMapStyles: Record<string, string | StyleSpecification> = {
|
||||||
@@ -129,7 +131,7 @@ export const overlays: Record<string, StyleSpecification> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Skiing: {
|
skiing: {
|
||||||
name: "waymarkedTrailsWinter",
|
name: "waymarkedTrailsWinter",
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -152,38 +154,293 @@ export const overlays: Record<string, StyleSpecification> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type POI = { q: string, icon: { svg: any, bg: string } }
|
export type POI = {
|
||||||
export const pois: Record<string, POI> = {
|
tags:
|
||||||
"grocery-store": { q: "nwr[shop=supermarket];nwr[shop=convenience];", icon: { svg: "", bg: "red" } },
|
| Record<string, string | boolean | string[]>
|
||||||
"bakery": { q: "nwr[shop=bakery];", icon: { svg: Bakery, bg: "coral" } },
|
| Record<string, string | boolean | string[]>[], icon: { svg: any, bg: string }
|
||||||
"food-drinks": { q: "nwr[amenity=restaurant];nwr[amenity=fast_food];nwr[amenity=cafe];nwr[amenity=pub];nwr[amenity=bar];", icon: { svg: "", bg: "red" } },
|
|
||||||
"toilet": { q: "nwr[amenity=toilets];", icon: { svg: "", bg: "red" } },
|
|
||||||
"drinking-water": { q: "nwr[amenity=drinking_water];nwr[amenity=water_point];nwr[natural=spring][drinking_water=yes];", icon: { svg: "", bg: "red" } },
|
|
||||||
"shower": { q: "nwr[amenity=shower];", icon: { svg: "", bg: "red" } },
|
|
||||||
"shelter": { q: "nwr[amenity=shelter];", icon: { svg: "", bg: "red" } },
|
|
||||||
"barrier": { q: "nwr[barrier=true];", icon: { svg: "", bg: "red" } },
|
|
||||||
"attraction": { q: "nwr[tourism=attraction];", icon: { svg: "", bg: "red" } },
|
|
||||||
"viewpoint": { q: "nwr[tourism=viewpoint];", icon: { svg: "", bg: "red" } },
|
|
||||||
"hotel": { q: "nwr[tourism=hotel];nwr[tourism=hostel];nwr[tourism=guest_house];nwr[tourism=motel];", icon: { svg: "", bg: "red" } },
|
|
||||||
"camp-site": { q: "nwr[tourism=camp_site];", icon: { svg: "", bg: "red" } },
|
|
||||||
"hut": { q: "nwr[tourism=alpine_hut];nwr[tourism=wilderness_hut];", icon: { svg: "", bg: "red" } },
|
|
||||||
"peak": { q: "nwr[natural=peak];", icon: { svg: "", bg: "red" } },
|
|
||||||
"mountain-pass": { q: "nwr[mountain_pass=yes];", icon: { svg: "", bg: "red" } },
|
|
||||||
"climbing": { q: "nwr[sport=climbing];", icon: { svg: "", bg: "red" } },
|
|
||||||
"bicylce-parking": { q: "nwr[amenity=bicycle_parking];", icon: { svg: "", bg: "red" } },
|
|
||||||
"bicycle-rental": { q: "nwr[amenity=bicycle_rental];", icon: { svg: "", bg: "red" } },
|
|
||||||
"bicycle-shop": { q: "nwr[shop=bicycle];", icon: { svg: "", bg: "red" } },
|
|
||||||
"gas-station": { q: "nwr[amenity=fuel];", icon: { svg: "", bg: "red" } },
|
|
||||||
"parking": { q: "nwr[amenity=parking];", icon: { svg: "", bg: "red" } },
|
|
||||||
"car-repair": { q: "nwr[shop=car_repair];", icon: { svg: "", bg: "red" } },
|
|
||||||
"motorcycle-repair": { q: "nwr[shop=motorcycle_repair];", icon: { svg: "", bg: "red" } },
|
|
||||||
"railway-station": { q: "nwr[railway=station];", icon: { svg: "", bg: "red" } },
|
|
||||||
"subway": { q: "nwr[railway=subway_entrance];", icon: { svg: "", bg: "red" } },
|
|
||||||
"tram": { q: "nwr[railway=tram_stop];", icon: { svg: "", bg: "red" } },
|
|
||||||
"bus": { q: "nwr[public_transport=stop_position][bus=yes];nwr[public_transport=platform][bus=yes];", icon: { svg: "", bg: "red" } },
|
|
||||||
"ferry": { q: "nwr[amenity=ferry_terminal];", icon: { svg: "", bg: "red" } },
|
|
||||||
}
|
}
|
||||||
|
export const pois: Record<string, POI> = {
|
||||||
|
bakery: {
|
||||||
|
icon: {
|
||||||
|
svg: Bakery,
|
||||||
|
bg: 'Coral',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
shop: 'bakery',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'grocery-store': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Coral',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
shop: ['supermarket', 'convenience'],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
"food-drinks": {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Coral',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: ['restaurant', 'fast_food', 'cafe', 'pub', 'bar'],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
toilets: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DeepSkyBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'toilets',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
water: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DeepSkyBlue',
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
amenity: ['drinking_water', 'water_point'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
natural: 'spring',
|
||||||
|
drinking_water: 'yes',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
},
|
||||||
|
shower: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DeepSkyBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'shower',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
shelter: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#000000',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'shelter',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
'gas-station': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#000000',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'fuel',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
parking: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#000000',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'parking',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
garage: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#000000',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
shop: ['car_repair', 'motorcycle_repair'],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
barrier: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#000000',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
barrier: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attraction: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Green',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: 'attraction',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewpoint: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Green',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: 'viewpoint',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
hotel: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#e6c100',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: ['hotel', 'hostel', 'guest_house', 'motel'],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
campsite: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#e6c100',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: 'camp_site',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
hut: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: '#e6c100',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: ['alpine_hut', 'wilderness_hut'],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
picnic: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Green',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tourism: 'picnic_site',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
summit: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Green',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
natural: 'peak',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
pass: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Green',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
mountain_pass: 'yes',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
climbing: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'Green',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
sport: 'climbing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'bicycle-parking': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'HotPink',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'bicycle_parking',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
'bicycle-rental': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'HotPink',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'bicycle_rental',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'bicycle-shop': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'HotPink',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
shop: 'bicycle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'railway-station': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DarkBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
railway: 'station',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
'tram-stop': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DarkBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
railway: 'tram_stop',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
'subway-stop': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DarkBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
railway: 'subway_entrance',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
'bus-stop': {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DarkBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
public_transport: ['stop_position', 'platform'],
|
||||||
|
bus: 'yes',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
ferry: {
|
||||||
|
icon: {
|
||||||
|
svg: "",
|
||||||
|
bg: 'DarkBlue',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
amenity: 'ferry_terminal',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MapState = {
|
export type MapState = {
|
||||||
@@ -195,12 +452,10 @@ export type MapState = {
|
|||||||
export const defaultMapState: MapState = {
|
export const defaultMapState: MapState = {
|
||||||
base: "OpenFreeMap",
|
base: "OpenFreeMap",
|
||||||
overlays: {
|
overlays: {
|
||||||
waymarkedTrailsHiking: false,
|
hiking: false,
|
||||||
waymarkedTrailsCycling: false,
|
cycling: false,
|
||||||
waymarkedTrailsHorseRiding: false,
|
MTB: false,
|
||||||
waymarkedTrailsMTB: false,
|
skiing: false,
|
||||||
waymarkedTrailsSkating: false,
|
|
||||||
waymarkedTrailsWinter: false,
|
|
||||||
},
|
},
|
||||||
pois: {
|
pois: {
|
||||||
food: {
|
food: {
|
||||||
@@ -209,41 +464,40 @@ export const defaultMapState: MapState = {
|
|||||||
bakery: false,
|
bakery: false,
|
||||||
},
|
},
|
||||||
tourism: {
|
tourism: {
|
||||||
"camp-site": false,
|
campsite: false,
|
||||||
attraction: false,
|
attraction: false,
|
||||||
hotel: false,
|
hotel: false,
|
||||||
hut: false,
|
hut: false,
|
||||||
viewpoint: false,
|
viewpoint: false,
|
||||||
},
|
},
|
||||||
ammenity: {
|
ammenity: {
|
||||||
"drinking-water": false,
|
water: false,
|
||||||
barrier: false,
|
barrier: false,
|
||||||
shelter: false,
|
shelter: false,
|
||||||
shower: false,
|
shower: false,
|
||||||
toilet: false,
|
toilets: false,
|
||||||
},
|
},
|
||||||
hiking: {
|
hiking: {
|
||||||
"mountain-pass": false,
|
pass: false,
|
||||||
climbing: false,
|
climbing: false,
|
||||||
peak: false,
|
summit: false,
|
||||||
},
|
},
|
||||||
cycling: {
|
cycling: {
|
||||||
"bicycle-rental": false,
|
"bicycle-rental": false,
|
||||||
"bicycle-shop": false,
|
"bicycle-shop": false,
|
||||||
"bicylce-parking": false,
|
"bicycle-parking": false,
|
||||||
},
|
},
|
||||||
"car-motorcycle": {
|
"car-motorcycle": {
|
||||||
"car-repair": false,
|
"garage": false,
|
||||||
"gas-station": false,
|
"gas-station": false,
|
||||||
"motorcycle-repair": false,
|
|
||||||
parking: false,
|
parking: false,
|
||||||
},
|
},
|
||||||
"public-transport": {
|
"public-transport": {
|
||||||
"railway-station": false,
|
"railway-station": false,
|
||||||
bus: false,
|
"bus-stop": false,
|
||||||
ferry: false,
|
ferry: false,
|
||||||
subway: false,
|
"subway-stop": false,
|
||||||
tram: false,
|
"tram-stop": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as M from "maplibre-gl";
|
import * as M from "maplibre-gl";
|
||||||
import { baseMapStyles, defaultMapState, pois, type BaseLayer, type MapState } from "./layers";
|
import { DebugLayer } from "./debug-layer";
|
||||||
|
import { baseMapStyles, defaultMapState, type BaseLayer, type MapState } from "./layers";
|
||||||
import { OverlayLayer } from "./overlay-layer";
|
import { OverlayLayer } from "./overlay-layer";
|
||||||
import { OverpassLayer } from "./overpass-layer";
|
import { OverpassLayer } from "./overpass-layer";
|
||||||
|
|
||||||
@@ -8,7 +9,8 @@ import { OverpassLayer } from "./overpass-layer";
|
|||||||
export class LayerManager {
|
export class LayerManager {
|
||||||
private map: M.Map;
|
private map: M.Map;
|
||||||
state!: MapState;
|
state!: MapState;
|
||||||
layers: Record<string, BaseLayer> = {};
|
private layers: Record<string, BaseLayer> = {};
|
||||||
|
private addedListeners: Set<string> = new Set();
|
||||||
|
|
||||||
constructor(map: M.Map) {
|
constructor(map: M.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
@@ -30,9 +32,13 @@ export class LayerManager {
|
|||||||
try {
|
try {
|
||||||
this.update(this.state, true);
|
this.update(this.state, true);
|
||||||
|
|
||||||
const overpassLayer = new OverpassLayer()
|
const overpassLayer = new OverpassLayer(this.map)
|
||||||
this.addLayer("overpass", overpassLayer)
|
const debugLayer = new DebugLayer()
|
||||||
|
|
||||||
|
this.addLayer("overpass", overpassLayer)
|
||||||
|
this.addLayer("debug", debugLayer)
|
||||||
|
|
||||||
|
this.map.on('moveend', this.updateOverpassLayerAfterMapMoveBinded);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
// map is probably not initialized yet
|
// map is probably not initialized yet
|
||||||
@@ -56,25 +62,35 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const overpassLayer = this.layers.overpass;
|
this.updateOverpassLayer(newState);
|
||||||
if (overpassLayer) {
|
|
||||||
const castedOverpassLayer = overpassLayer as OverpassLayer
|
|
||||||
castedOverpassLayer.updateLayer(newState, this.map.getBounds()).then(() => {
|
|
||||||
this.loadIcons(castedOverpassLayer.pois);
|
|
||||||
(this.map.getSource('overpass') as M.GeoJSONSource).setData(castedOverpassLayer.data);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = newState
|
this.state = newState
|
||||||
localStorage.setItem("map-state", JSON.stringify(this.state));
|
localStorage.setItem("map-state", JSON.stringify(this.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateOverpassLayerAfterMapMoveBinded = this.updateOverpassLayerAfterMapMove.bind(this);
|
||||||
|
updateOverpassLayerAfterMapMove() {
|
||||||
|
this.updateOverpassLayer(this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateOverpassLayer(newState: MapState) {
|
||||||
|
const overpassLayer = this.layers.overpass;
|
||||||
|
if (overpassLayer) {
|
||||||
|
const castedOverpassLayer = overpassLayer as OverpassLayer;
|
||||||
|
overpassLayer.filter = await castedOverpassLayer.updateLayerIfNeeded(newState, this.map.getBounds());
|
||||||
|
(this.map.getSource('overpass') as M.GeoJSONSource).setData(castedOverpassLayer.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private updateBaseLayer(layer: string | M.StyleSpecification) {
|
private updateBaseLayer(layer: string | M.StyleSpecification) {
|
||||||
this.map.setStyle(layer);
|
this.map.setStyle(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private addLayer(id: string, layer: BaseLayer) {
|
private addLayer(id: string, layer: BaseLayer) {
|
||||||
|
if (this.layers[id] && this.map.getLayer(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const [id, s] of Object.entries(layer.spec.sources)) {
|
for (const [id, s] of Object.entries(layer.spec.sources)) {
|
||||||
if (!this.map.getSource(id)) {
|
if (!this.map.getSource(id)) {
|
||||||
this.map.addSource(id, s)
|
this.map.addSource(id, s)
|
||||||
@@ -87,6 +103,34 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (layer.listeners) {
|
||||||
|
for (const [id, listener] of Object.entries(layer.listeners)) {
|
||||||
|
if (listener.onEnter && !this.addedListeners.has("mouseenter-" + id)) {
|
||||||
|
this.addedListeners.add("mouseenter-" + id)
|
||||||
|
this.map.on('mouseenter', id, listener.onEnter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener.onLeave && !this.addedListeners.has("onleave-" + id)) {
|
||||||
|
this.addedListeners.add("mouseleave-" + id)
|
||||||
|
this.map.on('mouseleave', id, listener.onLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener.onMouseDown && !this.addedListeners.has("click-" + id)) {
|
||||||
|
this.addedListeners.add("click-" + id)
|
||||||
|
this.map.on('click', id, listener.onMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener.onMouseMove && !this.addedListeners.has("mousemove-" + id)) {
|
||||||
|
this.addedListeners.add("mousemove-" + id)
|
||||||
|
this.map.on('mousemove', id, listener.onMouseMove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.filter && !this.map.getFilter(id)) {
|
||||||
|
this.map.setFilter(id, layer.filter)
|
||||||
|
}
|
||||||
|
|
||||||
this.layers[id] = layer
|
this.layers[id] = layer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +150,29 @@ export class LayerManager {
|
|||||||
this.map.removeSource(id)
|
this.map.removeSource(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (layer.listeners) {
|
||||||
|
for (const [id, listener] of Object.entries(layer.listeners)) {
|
||||||
|
if (listener.onEnter) {
|
||||||
|
this.addedListeners.delete("mouseenter-" + id)
|
||||||
|
this.map.off('mouseenter', id, listener.onEnter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener.onLeave) {
|
||||||
|
this.addedListeners.delete("mouseleave-" + id)
|
||||||
|
this.map.off('mouseleave', id, listener.onLeave);
|
||||||
|
}
|
||||||
|
if (listener.onMouseDown) {
|
||||||
|
this.addedListeners.delete("click-" + id)
|
||||||
|
this.map.off('click', id, listener.onMouseDown);
|
||||||
|
}
|
||||||
|
if (listener.onMouseMove) {
|
||||||
|
this.addedListeners.delete("mousemove-" + id)
|
||||||
|
this.map.off('mousemove', id, listener.onMouseMove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delete this.layers[id]
|
delete this.layers[id]
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -115,30 +182,4 @@ export class LayerManager {
|
|||||||
this.addLayer(id, layer)
|
this.addLayer(id, layer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadIcons(activePois: string[]) {
|
|
||||||
activePois.forEach((poi) => {
|
|
||||||
if (!this.map.hasImage(`overpass-${poi}`)) {
|
|
||||||
let icon = new Image(100, 100);
|
|
||||||
icon.onload = () => {
|
|
||||||
if (!this.map.hasImage(`overpass-${poi}`)) {
|
|
||||||
this.map.addImage(`overpass-${poi}`, icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const svg = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
||||||
<circle cx="20" cy="20" r="20" fill="${pois[poi].icon.bg}" />
|
|
||||||
<g transform="translate(8 8) scale(0.05)">
|
|
||||||
${pois[poi].icon.svg}
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
`
|
|
||||||
|
|
||||||
icon.src =
|
|
||||||
'data:image/svg+xml,' +
|
|
||||||
encodeURIComponent(svg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { LngLatBounds, StyleSpecification } from "maplibre-gl";
|
import { createOverpassPopup } from "$lib/util/maplibre_util";
|
||||||
|
import * as M from "maplibre-gl";
|
||||||
|
import { type LngLatBounds, type MapMouseEvent, type StyleSpecification } from "maplibre-gl";
|
||||||
import { pois, type BaseLayer, type MapState } from "./layers";
|
import { pois, type BaseLayer, type MapState } from "./layers";
|
||||||
import type { OverpassResponse } from "./types";
|
import type { OverpassResponse } from "./types";
|
||||||
|
|
||||||
@@ -6,7 +8,13 @@ export class OverpassLayer implements BaseLayer {
|
|||||||
private overpassApiURL: string = "https://overpass.private.coffee/api/interpreter"
|
private overpassApiURL: string = "https://overpass.private.coffee/api/interpreter"
|
||||||
|
|
||||||
data: GeoJSON.FeatureCollection = ({ type: 'FeatureCollection', features: [] });
|
data: GeoJSON.FeatureCollection = ({ type: 'FeatureCollection', features: [] });
|
||||||
pois: string[] = [];
|
|
||||||
|
private minZoom = 12;
|
||||||
|
|
||||||
|
private cachedQueries: { x: string, y: string, query: string }[] = [];
|
||||||
|
private cachedData: { query: string, id: number, feature: GeoJSON.Feature }[] = []
|
||||||
|
private tileSize = 0.1;
|
||||||
|
|
||||||
|
|
||||||
spec: StyleSpecification = {
|
spec: StyleSpecification = {
|
||||||
version: 8,
|
version: 8,
|
||||||
@@ -32,61 +40,281 @@ export class OverpassLayer implements BaseLayer {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
listeners = {
|
||||||
|
"overpass": {
|
||||||
|
onMouseDown: (e: MapMouseEvent) => {
|
||||||
|
this.openPopup(e)
|
||||||
|
},
|
||||||
|
onEnter: (e: MapMouseEvent) => {
|
||||||
|
this.openPopup(e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateLayer(state: MapState, bounds: LngLatBounds) {
|
private popup: M.Popup;
|
||||||
let activePOIs: string[] = []
|
private map: M.Map;
|
||||||
for (const category of Object.keys(state.pois)) {
|
private currentPopupCoordinates: GeoJSON.Position | null = null
|
||||||
for (const [name, active] of Object.entries(state.pois[category])) {
|
|
||||||
if (active) {
|
constructor(map: M.Map) {
|
||||||
activePOIs.push(name)
|
this.map = map;
|
||||||
|
this.popup = new M.Popup()
|
||||||
|
.setMaxWidth("420px")
|
||||||
|
}
|
||||||
|
|
||||||
|
private openPopup(e: MapMouseEvent) {
|
||||||
|
const features = (e as any).features as GeoJSON.Feature[];
|
||||||
|
const point = features[0].geometry as GeoJSON.Point;
|
||||||
|
const tags = JSON.parse(features[0].properties?.tags);
|
||||||
|
const content = createOverpassPopup(tags, point.coordinates);
|
||||||
|
|
||||||
|
this.currentPopupCoordinates = point.coordinates;
|
||||||
|
this.popup
|
||||||
|
.setLngLat(point.coordinates as M.LngLatLike)
|
||||||
|
.setDOMContent(content)
|
||||||
|
.addTo(this.map);
|
||||||
|
|
||||||
|
this.map.on("mousemove", this.distanceNotifierBinded)
|
||||||
|
}
|
||||||
|
|
||||||
|
distanceNotifierBinded = this.distanceNotifier.bind(this);
|
||||||
|
private distanceNotifier(e: MapMouseEvent) {
|
||||||
|
if (this.currentPopupCoordinates === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.map.project(this.currentPopupCoordinates as M.LngLatLike).dist(this.map.project(e.lngLat)) > 60) {
|
||||||
|
this.popup.remove();
|
||||||
|
this.map.off("mousemove", this.distanceNotifier)
|
||||||
|
this.currentPopupCoordinates = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLayerIfNeeded(state: MapState, bounds: LngLatBounds) {
|
||||||
|
|
||||||
|
let activeQueries: string[] = this.getActiveQueries(state);
|
||||||
|
|
||||||
|
if (this.map.getZoom() >= this.minZoom) {
|
||||||
|
|
||||||
|
await this.fetchMissingTilesForBounds(bounds, activeQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const filter: M.FilterSpecification = ['in', 'query', ...this.getActiveQueries(state)];
|
||||||
|
|
||||||
|
this.map.setFilter('overpass', filter);
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchMissingTilesForBounds(bounds: M.LngLatBounds, activeQueries: string[]) {
|
||||||
|
const result: [string, LngLatBounds][] = [];
|
||||||
|
|
||||||
|
const south = bounds.getSouth();
|
||||||
|
const north = bounds.getNorth();
|
||||||
|
const west = bounds.getWest();
|
||||||
|
const east = bounds.getEast();
|
||||||
|
|
||||||
|
for (let lat = south; lat < north; lat += this.tileSize) {
|
||||||
|
for (let lng = west; lng < east; lng += this.tileSize) {
|
||||||
|
const x = Math.floor(lat / this.tileSize) * this.tileSize;
|
||||||
|
const y = Math.floor(lng / this.tileSize) * this.tileSize;
|
||||||
|
const cachedQueriesAtPosition = this.cachedQueries.filter(q => q.x == x.toFixed(4) && q.y == y.toFixed(4)) ?? [];
|
||||||
|
const missingQueries = activeQueries.filter(
|
||||||
|
(query) =>
|
||||||
|
!cachedQueriesAtPosition.some(
|
||||||
|
(querytile) =>
|
||||||
|
querytile.query === query
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (missingQueries.length > 0) {
|
||||||
|
const tileBounds = this.getBoundsForTile(x, y)
|
||||||
|
await this.fetchTile(x, y, missingQueries, tileBounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const q = this.getOverpassQuery(activePOIs, bounds)
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchTile(x: number, y: number, activeQueries: string[], bounds: LngLatBounds) {
|
||||||
|
const q = this.getOverpassQuery(activeQueries, bounds)
|
||||||
if (!q.length) {
|
if (!q.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const r = await fetch(`${this.overpassApiURL}?data=${q}`)
|
const r = await fetch(`${this.overpassApiURL}?data=${q}`)
|
||||||
const response: OverpassResponse = await r.json();
|
const response: OverpassResponse = await r.json();
|
||||||
|
|
||||||
this.updateData(response)
|
this.cacheData(x, y, response, activeQueries)
|
||||||
this.pois = activePOIs
|
this.loadIcons(activeQueries)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateData(data: OverpassResponse, ) {
|
private cacheData(x: number, y: number, data: OverpassResponse, activeQueries: string[]) {
|
||||||
let pois: GeoJSON.Feature[] = [];
|
|
||||||
|
|
||||||
if (data.elements === undefined) {
|
if (data.elements === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cachedQueries = this.cachedQueries.concat(activeQueries.map((query) => ({ x: x.toFixed(4), y: y.toFixed(4), query })));
|
||||||
|
|
||||||
for (let element of data.elements) {
|
for (let element of data.elements) {
|
||||||
console.log(element);
|
for (let query of activeQueries) {
|
||||||
|
if (this.belongsToQuery(element, query)) {
|
||||||
pois.push({
|
this.cachedData.push({
|
||||||
type: 'Feature',
|
query,
|
||||||
geometry: {
|
id: element.id,
|
||||||
type: 'Point',
|
feature: {
|
||||||
coordinates: element.center
|
type: 'Feature',
|
||||||
? [element.center.lon, element.center.lat]
|
geometry: {
|
||||||
: [element.lon, element.lat],
|
type: 'Point',
|
||||||
},
|
coordinates: element.center
|
||||||
properties: {
|
? [element.center.lon, element.center.lat]
|
||||||
id: element.id,
|
: [element.lon, element.lat],
|
||||||
lat: element.center ? element.center.lat : element.lat,
|
},
|
||||||
lon: element.center ? element.center.lon : element.lon,
|
properties: {
|
||||||
icon: `overpass-bakery`,
|
id: element.id,
|
||||||
tags: element.tags,
|
lat: element.center ? element.center.lat : element.lat,
|
||||||
type: element.type,
|
lon: element.center ? element.center.lon : element.lon,
|
||||||
},
|
query: query,
|
||||||
|
icon: `overpass-${query}`,
|
||||||
|
tags: element.tags,
|
||||||
|
type: element.type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.data.features = this.cachedData.map(d => d.feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getActiveQueries(state: MapState) {
|
||||||
|
const activeQueries: string[] = []
|
||||||
|
for (const category of Object.keys(state.pois)) {
|
||||||
|
for (const [name, active] of Object.entries(state.pois[category])) {
|
||||||
|
if (active) {
|
||||||
|
activeQueries.push(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
private belongsToQuery(element: OverpassResponse["elements"][number], query: string) {
|
||||||
|
if (Array.isArray(pois[query].tags)) {
|
||||||
|
return pois[query].tags.some((tags) => this.belongsToQueryItem(element, tags));
|
||||||
|
} else {
|
||||||
|
return this.belongsToQueryItem(element, pois[query].tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
|
||||||
|
return Object.entries(tags).every(([tag, value]) =>
|
||||||
|
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOverpassQuery(activeQueries: string[], bounds: LngLatBounds) {
|
||||||
|
return `[bbox:${bounds.getSouth()},${bounds.getWest()},${bounds.getNorth()},${bounds.getEast()}][out:json];(${this.getQueries(activeQueries)});out center;`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQueries(queries: string[]) {
|
||||||
|
return queries.map((query) => this.getQuery(query)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuery(query: string) {
|
||||||
|
if (Array.isArray(pois[query].tags)) {
|
||||||
|
return pois[query].tags.map((tags) => this.getQueryItem(tags)).join('');
|
||||||
|
} else {
|
||||||
|
return this.getQueryItem(pois[query].tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
||||||
|
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
|
||||||
|
if (arrayEntry !== undefined) {
|
||||||
|
return (arrayEntry[1] as string[])
|
||||||
|
.map(
|
||||||
|
(val) =>
|
||||||
|
`nwr${Object.entries(tags)
|
||||||
|
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
|
||||||
|
.join('')};`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
} else {
|
||||||
|
return `nwr${Object.entries(tags)
|
||||||
|
.map(([tag, value]) => `[${tag}=${value}]`)
|
||||||
|
.join('')};`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadIcons(activeQueries: string[]) {
|
||||||
|
activeQueries.forEach((q) => {
|
||||||
|
if (!this.map.hasImage(`overpass-${q}`)) {
|
||||||
|
let icon = new Image(100, 100);
|
||||||
|
icon.onload = () => {
|
||||||
|
if (!this.map.hasImage(`overpass-${q}`)) {
|
||||||
|
this.map.addImage(`overpass-${q}`, icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
|
<circle cx="20" cy="20" r="20" fill="${pois[q].icon.bg}" />
|
||||||
|
<g transform="translate(8 8) scale(0.05)">
|
||||||
|
${pois[q].icon.svg}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
icon.src =
|
||||||
|
'data:image/svg+xml,' +
|
||||||
|
encodeURIComponent(svg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBoundsForTile(lat: number, lng: number): LngLatBounds {
|
||||||
|
return new M.LngLatBounds(
|
||||||
|
[lng, lat],
|
||||||
|
[lng + this.tileSize, lat + this.tileSize]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showDebugTiles(tiles: [string, LngLatBounds][]) {
|
||||||
|
const features = tiles.map(([_, bounds]) => boundsToPolygonFeature(bounds));
|
||||||
|
|
||||||
|
const debugSource = this.map.getSource('debug') as M.GeoJSONSource;
|
||||||
|
|
||||||
|
if (debugSource) {
|
||||||
|
debugSource.setData({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.data.features = pois;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOverpassQuery(data: string[], bounds: LngLatBounds) {
|
}
|
||||||
const q = data.map(p => pois[p].q).join('')
|
|
||||||
return `[bbox:${bounds.getSouth()},${bounds.getWest()},${bounds.getNorth()},${bounds.getEast()}][out:json];(${q});out center;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
function boundsToPolygonFeature(bounds: M.LngLatBounds): GeoJSON.Feature<GeoJSON.Polygon> {
|
||||||
|
const [[west, south], [east, north]] = bounds.toArray();
|
||||||
|
const coordinates = [[
|
||||||
|
[west, south],
|
||||||
|
[east, south],
|
||||||
|
[east, north],
|
||||||
|
[west, north],
|
||||||
|
[west, south],
|
||||||
|
]];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: coordinates,
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,6 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(filter);
|
|
||||||
const trailsInBox = await trails_search_bounding_box(
|
const trailsInBox = await trails_search_bounding_box(
|
||||||
northEast,
|
northEast,
|
||||||
southWest,
|
southWest,
|
||||||
|
|||||||
Reference in New Issue
Block a user