server sided external api calls (#697)
* server sided external api calls * fix uploading gpx files * rename api/v1/nominatim with api/v1/geocoding to prepare photon support * remove NOMINATIM_MAX_RETRIES (side-kick from other PR) * re-add nominatim url fallback * docu, env var defaults and fallbacks, docker compose files * refactor * furhter refactorings and fixes * PUBLIC_VALHALLA_ENABLED in docker compose files added * fix env var names --------- Co-authored-by: Flomp <Flomp@users.noreply.github.com>
This commit is contained in:
@@ -64,9 +64,9 @@ services:
|
|||||||
UPLOAD_FOLDER: /app/uploads
|
UPLOAD_FOLDER: /app/uploads
|
||||||
UPLOAD_USER:
|
UPLOAD_USER:
|
||||||
UPLOAD_PASSWORD:
|
UPLOAD_PASSWORD:
|
||||||
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
|
OVERPASS_API_URL: https://overpass-api.de
|
||||||
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
|
VALHALLA_URL: https://valhalla1.openstreetmap.de
|
||||||
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
|
NOMINATIM_URL: https://nominatim.openstreetmap.org
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/uploads:/app/uploads
|
- ./data/uploads:/app/uploads
|
||||||
# - ./data/about.md:/app/build/client/md/about.md
|
# - ./data/about.md:/app/build/client/md/about.md
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ services:
|
|||||||
UPLOAD_FOLDER: /app/uploads
|
UPLOAD_FOLDER: /app/uploads
|
||||||
UPLOAD_USER:
|
UPLOAD_USER:
|
||||||
UPLOAD_PASSWORD:
|
UPLOAD_PASSWORD:
|
||||||
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
|
OVERPASS_API_URL: https://overpass-api.de
|
||||||
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
|
VALHALLA_URL: https://valhalla1.openstreetmap.de
|
||||||
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
|
NOMINATIM_URL: https://nominatim.openstreetmap.org
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/app/uploads
|
- uploads:/app/uploads
|
||||||
# - ./data/about.md:/app/build/client/md/about.md
|
# - ./data/about.md:/app/build/client/md/about.md
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ services:
|
|||||||
UPLOAD_FOLDER: /app/uploads
|
UPLOAD_FOLDER: /app/uploads
|
||||||
UPLOAD_USER:
|
UPLOAD_USER:
|
||||||
UPLOAD_PASSWORD:
|
UPLOAD_PASSWORD:
|
||||||
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
|
OVERPASS_API_URL: https://overpass-api.de
|
||||||
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
|
VALHALLA_URL: https://valhalla1.openstreetmap.de
|
||||||
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
|
NOMINATIM_URL: https://nominatim.openstreetmap.org
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/app/uploads
|
- uploads:/app/uploads
|
||||||
# - ./data/about.md:/app/build/client/md/about.md
|
# - ./data/about.md:/app/build/client/md/about.md
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export ORIGIN=http://localhost:5173
|
|||||||
export MEILI_URL=http://127.0.0.1:7700
|
export MEILI_URL=http://127.0.0.1:7700
|
||||||
export MEILI_MASTER_KEY=p2gYZAWODOrwTPr4AYoahCZ9CI8y9bUd0yQLGk-E3m8
|
export MEILI_MASTER_KEY=p2gYZAWODOrwTPr4AYoahCZ9CI8y9bUd0yQLGk-E3m8
|
||||||
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
|
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
|
||||||
export PUBLIC_VALHALLA_URL=https://valhalla1.openstreetmap.de
|
export VALHALLA_URL=https://valhalla1.openstreetmap.de
|
||||||
export POCKETBASE_ENCRYPTION_KEY=9ada3c93163812101e50e2bf49e880bc
|
export POCKETBASE_ENCRYPTION_KEY=9ada3c93163812101e50e2bf49e880bc
|
||||||
|
|
||||||
cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
|
cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
|
||||||
|
|||||||
@@ -42,10 +42,38 @@ Since we use an unmodified installation of meilisearch you can use all variables
|
|||||||
| BODY_SIZE_LIMIT | Maximum allowed upload size | Infinity |
|
| BODY_SIZE_LIMIT | Maximum allowed upload size | Infinity |
|
||||||
| PUBLIC_POCKETBASE_URL | IP or hostname (including the port) of your pocketbase instance | http://db:8090 |
|
| PUBLIC_POCKETBASE_URL | IP or hostname (including the port) of your pocketbase instance | http://db:8090 |
|
||||||
| PUBLIC_DISABLE_SIGNUP | Disables signup option for new users | false |
|
| PUBLIC_DISABLE_SIGNUP | Disables signup option for new users | false |
|
||||||
| PUBLIC_VALHALLA_URL | Public IP or hostname (including the port) of a valhalla instance | https://valhalla1.openstreetmap.de |
|
|
||||||
| PUBLIC_NOMINATIM_URL | Public IP or hostname (including the port) of a nominatim instance | https://nominatim.openstreetmap.org |
|
|
||||||
| PUBLIC_PRIVATE_INSTANCE | Setting this to true will block visitors from viewing content without an account | false |
|
| PUBLIC_PRIVATE_INSTANCE | Setting this to true will block visitors from viewing content without an account | false |
|
||||||
| UPLOAD_FOLDER | Folder from which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | /app/uploads |
|
| UPLOAD_FOLDER | Folder from which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | /app/uploads |
|
||||||
| UPLOAD_USER | Username for the account with which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | |
|
| UPLOAD_USER | Username for the account with which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | |
|
||||||
| UPLOAD_PASSWORD | Password for the account with which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | |
|
| UPLOAD_PASSWORD | Password for the account with which <span class="-tracking-[0.075em]">wanderer</span> auto-uploads trails | |
|
||||||
| PUBLIC_OVERPASS_API_URL | Overpass API URL used for map points of interest | https://overpass-api.de |
|
|
||||||
|
## Geocoding & Routing
|
||||||
|
|
||||||
|
These variables configure server-side requests to Valhalla, Nominatim and Overpass.
|
||||||
|
|
||||||
|
| Environment Variable | Description | Default |
|
||||||
|
| ------------------------ | --------------------------------------------------------------------------- | ----------------------------------- |
|
||||||
|
| VALHALLA_URL | Valhalla API URL used for auto-routing and elevation data | https://valhalla1.openstreetmap.de |
|
||||||
|
| NOMINATIM_URL | Nominatim API URL used for (reverse) geocoding | https://nominatim.openstreetmap.org |
|
||||||
|
| OVERPASS_API_URL | Overpass API URL used for map points of interest | https://overpass-api.de |
|
||||||
|
|
||||||
|
When `*_URL` is unset, the backend falls back to legacy `PUBLIC_*_URL`.
|
||||||
|
|
||||||
|
## Custom CA certificates
|
||||||
|
|
||||||
|
If your API endpoints use certificates signed by a private CA, add the CA bundle and set `NODE_EXTRA_CA_CERTS` for the `web` service.
|
||||||
|
|
||||||
|
| Environment Variable | Description | Default |
|
||||||
|
| -------------------- | ------------------------------------------------------------------------ | ------- |
|
||||||
|
| NODE_EXTRA_CA_CERTS | Path to an additional CA bundle used by Node.js TLS connections | |
|
||||||
|
|
||||||
|
Example (`docker-compose.yml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
environment:
|
||||||
|
NODE_EXTRA_CA_CERTS: /etc/ssl/private-ca/ca.pem
|
||||||
|
volumes:
|
||||||
|
- ./certs/ca.pem:/etc/ssl/private-ca/ca.pem:ro
|
||||||
|
```
|
||||||
|
|||||||
@@ -111,11 +111,14 @@ services:
|
|||||||
UPLOAD_FOLDER: /app/uploads
|
UPLOAD_FOLDER: /app/uploads
|
||||||
UPLOAD_USER:
|
UPLOAD_USER:
|
||||||
UPLOAD_PASSWORD:
|
UPLOAD_PASSWORD:
|
||||||
PUBLIC_OVERPASS_API_URL: https://overpass-api.de
|
OVERPASS_API_URL: https://overpass-api.de
|
||||||
PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
|
VALHALLA_URL: https://valhalla1.openstreetmap.de
|
||||||
PUBLIC_NOMINATIM_URL: https://nominatim.openstreetmap.org
|
NOMINATIM_URL: https://nominatim.openstreetmap.org
|
||||||
|
# Optional: trust private CAs for server-side API calls
|
||||||
|
# NODE_EXTRA_CA_CERTS: /etc/ssl/private-ca/ca.pem
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/uploads:/app/uploads
|
- ./data/uploads:/app/uploads
|
||||||
|
# - ./certs/ca.pem:/etc/ssl/private-ca/ca.pem:ro
|
||||||
# - ./data/about.md:/app/build/client/md/about.md
|
# - ./data/about.md:/app/build/client/md/about.md
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export ORIGIN=http://localhost:3000
|
|||||||
export MEILI_URL=http://127.0.0.1:7700
|
export MEILI_URL=http://127.0.0.1:7700
|
||||||
export MEILI_MASTER_KEY=YOU_SHOULD_DEFINITELY_CHANGE_ME
|
export MEILI_MASTER_KEY=YOU_SHOULD_DEFINITELY_CHANGE_ME
|
||||||
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
|
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
|
||||||
export PUBLIC_VALHALLA_URL=https://valhalla1.openstreetmap.de
|
export VALHALLA_URL=https://valhalla1.openstreetmap.de
|
||||||
export POCKETBASE_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE
|
export POCKETBASE_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE
|
||||||
|
|
||||||
# Optional configuration
|
# Optional configuration
|
||||||
@@ -76,6 +76,7 @@ export POCKETBASE_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE
|
|||||||
# export UPLOAD_FOLDER=/app/uploads
|
# export UPLOAD_FOLDER=/app/uploads
|
||||||
# export UPLOAD_USER=
|
# export UPLOAD_USER=
|
||||||
# export UPLOAD_PASSWORD=
|
# export UPLOAD_PASSWORD=
|
||||||
|
# export NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem
|
||||||
|
|
||||||
cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
|
cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
|
||||||
cd db && ./pocketbase serve &
|
cd db && ./pocketbase serve &
|
||||||
|
|||||||
18
web/src/lib/server/http.ts
Normal file
18
web/src/lib/server/http.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { json } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
function safeJson(text: string): any {
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return { message: text };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function proxyJsonResponse(response: Response) {
|
||||||
|
const text = await response.text();
|
||||||
|
const payload = text.length ? safeJson(text) : {};
|
||||||
|
if (!response.ok) {
|
||||||
|
return json(payload, { status: response.status });
|
||||||
|
}
|
||||||
|
return json(payload);
|
||||||
|
}
|
||||||
63
web/src/lib/server/nominatim.ts
Normal file
63
web/src/lib/server/nominatim.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { version } from "$app/environment";
|
||||||
|
import { resolveBaseUrl } from "$lib/server/url";
|
||||||
|
import type { RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
const NOMINATIM_RATE_LIMIT_MS = 1000;
|
||||||
|
const NOMINATIM_MAX_RETRIES = 2;
|
||||||
|
let lastNominatimCall = 0;
|
||||||
|
|
||||||
|
function getNominatimBaseUrl(): string {
|
||||||
|
return resolveBaseUrl("NOMINATIM_URL", "https://nominatim.openstreetmap.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
function needsRateLimiting(baseUrl: string): boolean {
|
||||||
|
return baseUrl.includes("nominatim.openstreetmap.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitTimer = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
async function nominatimRateLimiter(baseUrl: string) {
|
||||||
|
if (!needsRateLimiting(baseUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTimeMs = Date.now() - lastNominatimCall;
|
||||||
|
const waitTime = NOMINATIM_RATE_LIMIT_MS - elapsedTimeMs;
|
||||||
|
if (waitTime > 0) {
|
||||||
|
await waitTimer(waitTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastNominatimCall = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchNominatim(event: RequestEvent, path: string, params: URLSearchParams): Promise<Response> {
|
||||||
|
const baseUrl = getNominatimBaseUrl();
|
||||||
|
const base = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
||||||
|
const cleanPath = path.replace(/^\/+/, "");
|
||||||
|
const url = new URL(cleanPath, base);
|
||||||
|
const query = params.toString();
|
||||||
|
if (query.length) {
|
||||||
|
url.search = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
await nominatimRateLimiter(baseUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await event.fetch(url.toString(), {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"User-Agent": `wanderer/${version}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < NOMINATIM_MAX_RETRIES) {
|
||||||
|
attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`Nominatim fetch failed for ${url.toString()}`, { cause: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
web/src/lib/server/overpass.ts
Normal file
34
web/src/lib/server/overpass.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { resolveBaseUrl } from "$lib/server/url";
|
||||||
|
import type { RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
const OVERPASS_MAX_RETRIES = 2;
|
||||||
|
|
||||||
|
function getOverpassBaseUrl(): string {
|
||||||
|
return resolveBaseUrl("OVERPASS_API_URL", "https://overpass-api.de");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOverpass(event: RequestEvent, params: URLSearchParams): Promise<Response> {
|
||||||
|
const baseUrl = getOverpassBaseUrl();
|
||||||
|
const base = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
||||||
|
const url = new URL("api/interpreter", base);
|
||||||
|
const query = params.toString();
|
||||||
|
if (query.length) {
|
||||||
|
url.search = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await event.fetch(url.toString(), {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < OVERPASS_MAX_RETRIES) {
|
||||||
|
attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
web/src/lib/server/url.ts
Normal file
24
web/src/lib/server/url.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { env as privateEnv } from "$env/dynamic/private";
|
||||||
|
import { env as publicEnv } from "$env/dynamic/public";
|
||||||
|
|
||||||
|
export type ExternalServiceUrlKey = "VALHALLA_URL" | "NOMINATIM_URL" | "OVERPASS_API_URL";
|
||||||
|
|
||||||
|
export function normalizeBaseUrl(url: string): string {
|
||||||
|
const trimmedUrl = url.trim();
|
||||||
|
if (!trimmedUrl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (!/^https?:\/\//i.test(trimmedUrl)) {
|
||||||
|
return `https://${trimmedUrl}`;
|
||||||
|
}
|
||||||
|
return trimmedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBaseUrl(
|
||||||
|
key: ExternalServiceUrlKey,
|
||||||
|
fallback: string = "",
|
||||||
|
): string {
|
||||||
|
const publicKey = `PUBLIC_${key}` as `PUBLIC_${string}`;
|
||||||
|
const rawUrl = privateEnv[key] ?? publicEnv[publicKey] ?? fallback;
|
||||||
|
return normalizeBaseUrl(rawUrl);
|
||||||
|
}
|
||||||
5
web/src/lib/server/valhalla.ts
Normal file
5
web/src/lib/server/valhalla.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { resolveBaseUrl } from "$lib/server/url";
|
||||||
|
|
||||||
|
export function getValhallaBaseUrl(): string {
|
||||||
|
return resolveBaseUrl("VALHALLA_URL");
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { env } from "$env/dynamic/public";
|
|
||||||
import type { Actor } from "$lib/models/activitypub/actor";
|
import type { Actor } from "$lib/models/activitypub/actor";
|
||||||
import { defaultTrailSearchAttributes, type TrailSearchResult } from "$lib/models/trail";
|
import { defaultTrailSearchAttributes, type TrailSearchResult } from "$lib/models/trail";
|
||||||
import { APIError } from "$lib/util/api_util";
|
import { APIError } from "$lib/util/api_util";
|
||||||
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
|
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
|
||||||
import type { ListResult } from "pocketbase";
|
import type { ListResult } from "pocketbase";
|
||||||
import { version } from "$app/environment";
|
|
||||||
|
|
||||||
export type LocationSearchResult = {
|
export type LocationSearchResult = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -104,14 +102,17 @@ export async function searchTrails(q: string, options: SearchParams): Promise<Hi
|
|||||||
return response.hits
|
return response.hits
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchLocations(q: string, limit?: number): Promise<Hits<LocationSearchResult>> {
|
export async function searchLocations(q: string, limit?: number, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<Hits<LocationSearchResult>> {
|
||||||
const nominatimURL = env.PUBLIC_NOMINATIM_URL ?? "https://nominatim.openstreetmap.org"
|
if (!q.trim()) {
|
||||||
const r = await fetch(`${nominatimURL}/search?q=${q}&format=geojson&addressdetails=1${limit ? '&limit=' + limit : ''}`, {
|
return [];
|
||||||
method: "GET",
|
}
|
||||||
headers: new Headers({
|
|
||||||
"User-Agent": "wanderer/" + version
|
const params = new URLSearchParams({
|
||||||
})
|
q,
|
||||||
|
format: "geojson",
|
||||||
|
addressdetails: "1",
|
||||||
});
|
});
|
||||||
|
const r = await fetchGeocoding("search", params, f);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const response = await r.json();
|
const response = await r.json();
|
||||||
throw new APIError(r.status, response.message, response.detail)
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
@@ -127,14 +128,20 @@ export async function searchLocations(q: string, limit?: number): Promise<Hits<L
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchLocationReverse(lat: number, lon: number) {
|
async function fetchGeocoding(path: string, params: URLSearchParams, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<Response> {
|
||||||
const nominatimURL = env.PUBLIC_NOMINATIM_URL ?? "https://nominatim.openstreetmap.org"
|
const query = params.toString();
|
||||||
const r = await fetch(`${nominatimURL}/reverse?lat=${lat}&lon=${lon}&format=geojson&addressdetails=1`, {
|
const url = query.length ? `/api/v1/geocoding/${path}?${query}` : `/api/v1/geocoding/${path}`;
|
||||||
method: "GET",
|
return await f(url);
|
||||||
headers: new Headers({
|
}
|
||||||
"User-Agent": "wanderer/" + version
|
|
||||||
})
|
export async function searchLocationReverse(lat: number, lon: number, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
lat: String(lat),
|
||||||
|
lon: String(lon),
|
||||||
|
format: "geojson",
|
||||||
|
addressdetails: "1",
|
||||||
});
|
});
|
||||||
|
const r = await fetchGeocoding("reverse", params, f);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const response = await r.json();
|
const response = await r.json();
|
||||||
throw new APIError(r.status, response.message, response.detail)
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
@@ -170,14 +177,17 @@ function getLocationDescription(address: Address) {
|
|||||||
|
|
||||||
export async function searchMulti(options: MultiSearchParams): Promise<MultiSearchResult<any>[]> {
|
export async function searchMulti(options: MultiSearchParams): Promise<MultiSearchResult<any>[]> {
|
||||||
|
|
||||||
const locationQuery = options.queries.find(q => q.indexUid === "locations");
|
const locationQueryIndex = options.queries.findIndex(q => q.indexUid === "locations");
|
||||||
const locationQueryIndex = locationQuery ? options.queries.indexOf(locationQuery) : -1
|
const locationQuery = locationQueryIndex >= 0 ? options.queries[locationQueryIndex] : undefined;
|
||||||
if (locationQueryIndex >= 0) {
|
const queries = locationQueryIndex >= 0
|
||||||
options.queries.splice(locationQueryIndex, 1)
|
? options.queries.filter((_, index) => index !== locationQueryIndex)
|
||||||
}
|
: options.queries;
|
||||||
const r = await fetch("/api/v1/search/multi", {
|
const r = await fetch("/api/v1/search/multi", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(options),
|
body: JSON.stringify({
|
||||||
|
...options,
|
||||||
|
queries,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { OverpassResponse } from "./types";
|
|||||||
import { env } from '$env/dynamic/public'
|
import { env } from '$env/dynamic/public'
|
||||||
|
|
||||||
export class OverpassLayer implements BaseLayer {
|
export class OverpassLayer implements BaseLayer {
|
||||||
private overpassApiURL: string = (env.PUBLIC_OVERPASS_API_URL && env.PUBLIC_OVERPASS_API_URL.length > 0 ? env.PUBLIC_OVERPASS_API_URL : "https://overpass-api.de") + "/api/interpreter";
|
private overpassApiURL: string = "/api/v1/overpass/interpreter";
|
||||||
|
|
||||||
data: GeoJSON.FeatureCollection = ({ type: 'FeatureCollection', features: [] });
|
data: GeoJSON.FeatureCollection = ({ type: 'FeatureCollection', features: [] });
|
||||||
|
|
||||||
|
|||||||
36
web/src/routes/api/v1/geocoding/reverse/+server.ts
Normal file
36
web/src/routes/api/v1/geocoding/reverse/+server.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
import { proxyJsonResponse } from "$lib/server/http";
|
||||||
|
import { fetchNominatim } from "$lib/server/nominatim";
|
||||||
|
|
||||||
|
export async function GET(event: RequestEvent) {
|
||||||
|
const lat = event.url.searchParams.get("lat");
|
||||||
|
const lon = event.url.searchParams.get("lon");
|
||||||
|
if (!lat || !lon) {
|
||||||
|
return json({ message: "Missing query parameter: lat or lon" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(Number(lat)) || Number.isNaN(Number(lon))) {
|
||||||
|
return json({ message: "Invalid query parameter: lat or lon" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
format: "geojson",
|
||||||
|
addressdetails: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchNominatim(event, "/reverse", params);
|
||||||
|
return await proxyJsonResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
const err = error instanceof Error ? error : new Error(String(error));
|
||||||
|
const detail = {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
cause: err.cause instanceof Error ? err.cause.message : err.cause,
|
||||||
|
};
|
||||||
|
console.error("Nominatim reverse request failed", detail);
|
||||||
|
return json({ message: "Nominatim request failed", detail }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
38
web/src/routes/api/v1/geocoding/search/+server.ts
Normal file
38
web/src/routes/api/v1/geocoding/search/+server.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
import { proxyJsonResponse } from "$lib/server/http";
|
||||||
|
import { fetchNominatim } from "$lib/server/nominatim";
|
||||||
|
|
||||||
|
export async function GET(event: RequestEvent) {
|
||||||
|
const q = event.url.searchParams.get("q");
|
||||||
|
if (!q) {
|
||||||
|
return json({ message: "Missing query parameter: q" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = event.url.searchParams.get("limit");
|
||||||
|
if (limit !== null && Number.isNaN(Number(limit))) {
|
||||||
|
return json({ message: "Invalid query parameter: limit" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q,
|
||||||
|
format: "geojson",
|
||||||
|
addressdetails: "1",
|
||||||
|
});
|
||||||
|
if (limit) {
|
||||||
|
params.set("limit", limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchNominatim(event, "/search", params);
|
||||||
|
return await proxyJsonResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
const err = error instanceof Error ? error : new Error(String(error));
|
||||||
|
const detail = {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
cause: err.cause instanceof Error ? err.cause.message : err.cause,
|
||||||
|
};
|
||||||
|
console.error("Nominatim search request failed", detail);
|
||||||
|
return json({ message: "Nominatim request failed", detail }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
21
web/src/routes/api/v1/overpass/interpreter/+server.ts
Normal file
21
web/src/routes/api/v1/overpass/interpreter/+server.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
import { proxyJsonResponse } from "$lib/server/http";
|
||||||
|
import { fetchOverpass } from "$lib/server/overpass";
|
||||||
|
|
||||||
|
export async function GET(event: RequestEvent) {
|
||||||
|
const data = event.url.searchParams.get("data");
|
||||||
|
if (!data) {
|
||||||
|
return json({ message: "Missing query parameter: data" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchOverpass(event, params);
|
||||||
|
return await proxyJsonResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
return json({ message: "Overpass request failed" }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,8 +75,12 @@ export async function PUT(event: RequestEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (trail.lat && trail.lon) {
|
if (trail.lat && trail.lon) {
|
||||||
const location = await searchLocationReverse(trail.lat, trail.lon)
|
try {
|
||||||
|
const location = await searchLocationReverse(trail.lat, trail.lon, event.fetch)
|
||||||
trail.location ??= location;
|
trail.location ??= location;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn("Reverse geocoding failed during upload", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trail.public = event.locals.settings.privacy?.trails == "public"
|
trail.public = event.locals.settings.privacy?.trails == "public"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { env } from '$env/dynamic/public';
|
import { getValhallaBaseUrl } from '$lib/server/valhalla';
|
||||||
import { error, json, type NumericRange, type RequestEvent } from "@sveltejs/kit";
|
import { proxyJsonResponse } from '$lib/server/http';
|
||||||
|
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,18 +30,15 @@ import { error, json, type NumericRange, type RequestEvent } from "@sveltejs/kit
|
|||||||
* description: Internal Server Error
|
* description: Internal Server Error
|
||||||
*/
|
*/
|
||||||
export async function POST(event: RequestEvent) {
|
export async function POST(event: RequestEvent) {
|
||||||
|
const baseUrl = getValhallaBaseUrl();
|
||||||
const data = await event.request.json()
|
const data = await event.request.json()
|
||||||
if (!env.PUBLIC_VALHALLA_URL) {
|
if (!baseUrl) {
|
||||||
return error(400, "PUBLIC_VALHALLA_URL not set")
|
return json({ message: "VALHALLA_URL not set" }, { status: 400 })
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const r = await event.fetch(env.PUBLIC_VALHALLA_URL + '/height', { method: "POST", body: JSON.stringify(data) });
|
const response = await event.fetch(baseUrl + '/height', { method: "POST", body: JSON.stringify(data) });
|
||||||
const response = await r.json();
|
return await proxyJsonResponse(response);
|
||||||
if (!r.ok) {
|
|
||||||
throw error(r.status as NumericRange<400,500>, response);
|
|
||||||
}
|
|
||||||
return json(response);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw error(e.status || 500, e)
|
return json({ message: "Valhalla request failed" }, { status: 502 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { env } from '$env/dynamic/public';
|
import { getValhallaBaseUrl } from '$lib/server/valhalla';
|
||||||
import { error, json, type NumericRange, type RequestEvent } from "@sveltejs/kit";
|
import { proxyJsonResponse } from '$lib/server/http';
|
||||||
|
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
type RouteRequestBody = Record<string, unknown> & {
|
||||||
|
include_elevation_profile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
@@ -29,19 +33,19 @@ import { error, json, type NumericRange, type RequestEvent } from "@sveltejs/kit
|
|||||||
* description: Internal Server Error
|
* description: Internal Server Error
|
||||||
*/
|
*/
|
||||||
export async function POST(event: RequestEvent) {
|
export async function POST(event: RequestEvent) {
|
||||||
const data = await event.request.json()
|
const baseUrl = getValhallaBaseUrl();
|
||||||
if (!env.PUBLIC_VALHALLA_URL) {
|
const data: RouteRequestBody = await event.request.json();
|
||||||
return json({ message: "PUBLIC_VALHALLA_URL not set" }, { status: 400 })
|
if (!baseUrl) {
|
||||||
|
return json({ message: "VALHALLA_URL not set" }, { status: 400 })
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const r = await event.fetch(env.PUBLIC_VALHALLA_URL + '/route', { method: "POST", body: JSON.stringify(data) });
|
|
||||||
const response = await r.json();
|
|
||||||
if (!r.ok) {
|
|
||||||
return json({ message: response }, { status: r.status })
|
|
||||||
|
|
||||||
}
|
try {
|
||||||
return json(response);
|
const response = await event.fetch(baseUrl + '/route', {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
return await proxyJsonResponse(response);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return json({ message: e }, { status: 500 })
|
return json({ message: "Valhalla request failed" }, { status: 502 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { env } from "$env/dynamic/public";
|
|
||||||
import Button from "$lib/components/base/button.svelte";
|
import Button from "$lib/components/base/button.svelte";
|
||||||
import Datepicker from "$lib/components/base/datepicker.svelte";
|
import Datepicker from "$lib/components/base/datepicker.svelte";
|
||||||
import Select from "$lib/components/base/select.svelte";
|
import Select from "$lib/components/base/select.svelte";
|
||||||
@@ -124,6 +123,13 @@
|
|||||||
let gpxFile: File | Blob | null = null;
|
let gpxFile: File | Blob | null = null;
|
||||||
|
|
||||||
let drawingActive = $state(false);
|
let drawingActive = $state(false);
|
||||||
|
|
||||||
|
function routeCalculationErrorText(error: unknown) {
|
||||||
|
if (error instanceof Error && error.message) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return "Error calculating route";
|
||||||
|
}
|
||||||
let overwriteGPX = false;
|
let overwriteGPX = false;
|
||||||
let draggingMarker = false;
|
let draggingMarker = false;
|
||||||
|
|
||||||
@@ -660,7 +666,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
show_toast({
|
show_toast({
|
||||||
text: "Error calculating route",
|
text: routeCalculationErrorText(e),
|
||||||
icon: "close",
|
icon: "close",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
@@ -832,7 +838,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
show_toast({
|
show_toast({
|
||||||
text: "Error calculating route",
|
text: routeCalculationErrorText(e),
|
||||||
icon: "close",
|
icon: "close",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
@@ -882,7 +888,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
show_toast({
|
show_toast({
|
||||||
text: "Error calculating route",
|
text: routeCalculationErrorText(e),
|
||||||
icon: "close",
|
icon: "close",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
@@ -1219,7 +1225,6 @@
|
|||||||
? $_("upload-new-file")
|
? $_("upload-new-file")
|
||||||
: $_("upload-file")}</Button
|
: $_("upload-file")}</Button
|
||||||
>
|
>
|
||||||
{#if env.PUBLIC_VALHALLA_URL}
|
|
||||||
<div class="flex gap-4 items-center w-full">
|
<div class="flex gap-4 items-center w-full">
|
||||||
<hr class="basis-full border-input-border" />
|
<hr class="basis-full border-input-border" />
|
||||||
<span class="text-gray-500 uppercase">{$_("or")}</span>
|
<span class="text-gray-500 uppercase">{$_("or")}</span>
|
||||||
@@ -1244,7 +1249,6 @@
|
|||||||
? $_("stop-drawing")
|
? $_("stop-drawing")
|
||||||
: $_("draw-a-route")}</button
|
: $_("draw-a-route")}</button
|
||||||
>
|
>
|
||||||
{/if}
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
name="gpx"
|
name="gpx"
|
||||||
|
|||||||
Reference in New Issue
Block a user