switches to nominatim
This commit is contained in:
@@ -7,7 +7,7 @@ x-common-env: &cenv
|
|||||||
services:
|
services:
|
||||||
search:
|
search:
|
||||||
container_name: wanderer-search
|
container_name: wanderer-search
|
||||||
image: flomp/wanderer-search
|
image: getmeili/meilisearch:v1.11.3
|
||||||
environment:
|
environment:
|
||||||
<<: *cenv
|
<<: *cenv
|
||||||
MEILI_NO_ANALYTICS: true
|
MEILI_NO_ANALYTICS: true
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
FROM getmeili/meilisearch:v1.11.3
|
FROM getmeili/meilisearch:v1.11.3
|
||||||
|
|
||||||
COPY ./migrations/migration.dump /meili_data/dumps/migration.dump
|
|
||||||
COPY ./entrypoint.sh /entrypoint.sh
|
COPY ./entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
if [ -z "$(ls -A /meili_data/data.ms/indexes)" ]; then
|
|
||||||
meilisearch --import-dump /meili_data/dumps/migration.dump
|
|
||||||
else
|
|
||||||
meilisearch
|
meilisearch
|
||||||
fi
|
|
||||||
|
|||||||
Binary file not shown.
@@ -8,7 +8,7 @@
|
|||||||
label?: string;
|
label?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onchange?: (value: SelectItem[]) => void
|
onchange?: (value: SelectItem[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
label = "",
|
label = "",
|
||||||
name = "",
|
name = "",
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
onchange
|
onchange,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let showDropdown = $state(false);
|
let showDropdown = $state(false);
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
class="min-w-44 w-full flex flex-wrap items-center gap-2 border border-input-border bg-input-background min-h-[50px] p-3 rounded-md transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0"
|
class="relative min-w-44 w-full flex flex-wrap items-center gap-2 border border-input-border bg-input-background min-h-[50px] p-3 rounded-md transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 cursor-pointer pr-6"
|
||||||
onclick={() => (showDropdown = !showDropdown)}
|
onclick={() => (showDropdown = !showDropdown)}
|
||||||
>
|
>
|
||||||
{#if value.length === 0}
|
{#if value.length === 0}
|
||||||
@@ -71,13 +71,17 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<i
|
||||||
|
class="fa fa-caret-down absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 transition-transform"
|
||||||
|
class:rotate-180={showDropdown}
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dropdown menu -->
|
<!-- Dropdown menu -->
|
||||||
{#if showDropdown}
|
{#if showDropdown}
|
||||||
<div
|
<div
|
||||||
bind:this={dropdownRef}
|
bind:this={dropdownRef}
|
||||||
class="absolute z-10 mt-1 w-full bg-menu-background border border-input-border rounded-md max-h-40 overflow-y-auto"
|
class="absolute z-10 mt-1 w-full bg-menu-background border border-input-border rounded-md max-h-40 overflow-y-auto shadow-lg"
|
||||||
>
|
>
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot making the component unusable -->
|
|
||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
export type SearchItem = {
|
export type SearchItem = {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -38,14 +37,16 @@
|
|||||||
clearAfterSelect = true,
|
clearAfterSelect = true,
|
||||||
prepend,
|
prepend,
|
||||||
onupdate,
|
onupdate,
|
||||||
onclick
|
onclick,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let lastSearch: string = "";
|
let lastSearch: string = "";
|
||||||
let searching: boolean = $state(false);
|
let searching: boolean = $state(false);
|
||||||
let typingTimer!: any;
|
let typingTimer!: any;
|
||||||
|
|
||||||
let dropDownOpen = $derived(value.length > 0 && items.length > 0 && searching);
|
let dropDownOpen = $derived(
|
||||||
|
value.length > 0 && items.length > 0 && searching,
|
||||||
|
);
|
||||||
|
|
||||||
function onSearchType() {
|
function onSearchType() {
|
||||||
clearTimeout(typingTimer);
|
clearTimeout(typingTimer);
|
||||||
@@ -131,8 +132,8 @@
|
|||||||
onmousedown={(e) => handleItemClick(e, item)}
|
onmousedown={(e) => handleItemClick(e, item)}
|
||||||
onkeydown={(e) => handleItemClick(e, item)}
|
onkeydown={(e) => handleItemClick(e, item)}
|
||||||
>
|
>
|
||||||
{#if prepend}{@render prepend({ item, })}{:else}
|
{#if prepend}{@render prepend({ item })}{:else}
|
||||||
<i class="fa fa-{item.icon} mr-6"></i>
|
<i class="fa fa-{item.icon} basis-8 shrink-0"></i>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
import Slider from "../base/slider.svelte";
|
import Slider from "../base/slider.svelte";
|
||||||
import UserSearch from "../user_search.svelte";
|
import UserSearch from "../user_search.svelte";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
|
import { searchLocations } from "$lib/stores/search_store";
|
||||||
|
import { getIconForLocation } from "$lib/util/icon_util";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
@@ -114,19 +116,13 @@
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const r = await fetch("/api/v1/search/cities500", {
|
const r = await searchLocations(q, 5);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
|
||||||
});
|
|
||||||
const result = await r.json();
|
|
||||||
|
|
||||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
searchDropdownItems = r.map((h) => ({
|
||||||
text: h.name,
|
text: h.name,
|
||||||
description: `${h.division ? `${h.division} | ` : ""}${
|
description: h.description,
|
||||||
country_codes[h["country code"] as keyof typeof country_codes]
|
|
||||||
}`,
|
|
||||||
value: h,
|
value: h,
|
||||||
icon: "city",
|
icon: getIconForLocation(h),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +217,7 @@
|
|||||||
<Search
|
<Search
|
||||||
items={searchDropdownItems}
|
items={searchDropdownItems}
|
||||||
label={$_("near")}
|
label={$_("near")}
|
||||||
placeholder="{$_('search-cities')}..."
|
placeholder="{$_('search-places')}..."
|
||||||
clearAfterSelect={false}
|
clearAfterSelect={false}
|
||||||
bind:value={citySearchQuery}
|
bind:value={citySearchQuery}
|
||||||
onupdate={(q) => searchCities(q)}
|
onupdate={(q) => searchCities(q)}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"edit-entry": "Eintrag bearbeiten",
|
"edit-entry": "Eintrag bearbeiten",
|
||||||
"edit-list": "Liste bearbeiten",
|
"edit-list": "Liste bearbeiten",
|
||||||
"edit-route": "",
|
"edit-route": "Route bearbeiten",
|
||||||
"edit-waypoint": "Wegpunkt bearbeiten",
|
"edit-waypoint": "Wegpunkt bearbeiten",
|
||||||
"edited": "bearbeitet",
|
"edited": "bearbeitet",
|
||||||
"elevation-gain": "Höhenunterschied (aufw.)",
|
"elevation-gain": "Höhenunterschied (aufw.)",
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
"login-details": "Login Details",
|
"login-details": "Login Details",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"longitude": "Längengrad",
|
"longitude": "Längengrad",
|
||||||
"loop": "",
|
"loop": "Rundweg",
|
||||||
"make-one": "Neues erstellen!",
|
"make-one": "Neues erstellen!",
|
||||||
"make-thumbnail": "Thumbnail festlegen",
|
"make-thumbnail": "Thumbnail festlegen",
|
||||||
"map": "Karte",
|
"map": "Karte",
|
||||||
@@ -223,13 +223,14 @@
|
|||||||
"removed-trail-from": "Route entfernt aus",
|
"removed-trail-from": "Route entfernt aus",
|
||||||
"required": "Pflichtfeld",
|
"required": "Pflichtfeld",
|
||||||
"reset-password": "Passwort zurücksetzen",
|
"reset-password": "Passwort zurücksetzen",
|
||||||
"route-point": "",
|
"route-point": "Routenpunkt",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"save-list": "Liste speichern",
|
"save-list": "Liste speichern",
|
||||||
"save-trail": "Route speichern",
|
"save-trail": "Route speichern",
|
||||||
"save-your-trail-first": "Route zuerst speichern",
|
"save-your-trail-first": "Route zuerst speichern",
|
||||||
"search-cities": "Städte suchen",
|
"search-cities": "Städte suchen",
|
||||||
"search-for-trails-places": "Suche nach Routen, Orten",
|
"search-for-trails-places": "Suche nach Routen, Orten",
|
||||||
|
"search-places": "Orte suchen",
|
||||||
"search-trails": "Route suchen",
|
"search-trails": "Route suchen",
|
||||||
"select-list": "Liste auswählen",
|
"select-list": "Liste auswählen",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
@@ -262,7 +263,7 @@
|
|||||||
"speed": "Geschwindigkeit",
|
"speed": "Geschwindigkeit",
|
||||||
"statistics": "Statistiken",
|
"statistics": "Statistiken",
|
||||||
"stop-drawing": "Zeichnen beenden",
|
"stop-drawing": "Zeichnen beenden",
|
||||||
"stop-editing": "",
|
"stop-editing": "Bearbeiten beenden",
|
||||||
"summit-book": "Gipfelbuch",
|
"summit-book": "Gipfelbuch",
|
||||||
"table": "Tabelle",
|
"table": "Tabelle",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
@@ -274,7 +275,7 @@
|
|||||||
"units": "Einheiten",
|
"units": "Einheiten",
|
||||||
"upload-file": "Datei hochladen",
|
"upload-file": "Datei hochladen",
|
||||||
"upload-gpx": "GPX hochladen",
|
"upload-gpx": "GPX hochladen",
|
||||||
"upload-new-file": "",
|
"upload-new-file": "Neue Datei hochladen",
|
||||||
"uploaded": "hochgeladen",
|
"uploaded": "hochgeladen",
|
||||||
"username": "Nutzername",
|
"username": "Nutzername",
|
||||||
"view": "Ansehen",
|
"view": "Ansehen",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Save your trail first",
|
"save-your-trail-first": "Save your trail first",
|
||||||
"search-cities": "Search cities",
|
"search-cities": "Search cities",
|
||||||
"search-for-trails-places": "Search for trails, places",
|
"search-for-trails-places": "Search for trails, places",
|
||||||
|
"search-places": "Search places",
|
||||||
"search-trails": "Search trails",
|
"search-trails": "Search trails",
|
||||||
"select-list": "Select List",
|
"select-list": "Select List",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Guarda tu ruta primero",
|
"save-your-trail-first": "Guarda tu ruta primero",
|
||||||
"search-cities": "Buscar ciudades",
|
"search-cities": "Buscar ciudades",
|
||||||
"search-for-trails-places": "Busca rutas, lugares",
|
"search-for-trails-places": "Busca rutas, lugares",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "Buscar ruta",
|
"search-trails": "Buscar ruta",
|
||||||
"select-list": "Seleccionar Lista",
|
"select-list": "Seleccionar Lista",
|
||||||
"settings": "Configuración",
|
"settings": "Configuración",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Enregistrez d'abord votre itinéraire",
|
"save-your-trail-first": "Enregistrez d'abord votre itinéraire",
|
||||||
"search-cities": "Recherche une ville",
|
"search-cities": "Recherche une ville",
|
||||||
"search-for-trails-places": "Chercher un itinéraire ou un lieu",
|
"search-for-trails-places": "Chercher un itinéraire ou un lieu",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "Chercher un itinéraire",
|
"search-trails": "Chercher un itinéraire",
|
||||||
"select-list": "Liste de choix",
|
"select-list": "Liste de choix",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Először mentsd el a nyomvonaladat",
|
"save-your-trail-first": "Először mentsd el a nyomvonaladat",
|
||||||
"search-cities": "Városok keresése",
|
"search-cities": "Városok keresése",
|
||||||
"search-for-trails-places": "Nyomvonalak, helyek keresése",
|
"search-for-trails-places": "Nyomvonalak, helyek keresése",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "Nyomvonalak keresése",
|
"search-trails": "Nyomvonalak keresése",
|
||||||
"select-list": "Lista kiválasztása",
|
"select-list": "Lista kiválasztása",
|
||||||
"settings": "Beállítások",
|
"settings": "Beállítások",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Salva prima il tuo percorso",
|
"save-your-trail-first": "Salva prima il tuo percorso",
|
||||||
"search-cities": "Cerca città",
|
"search-cities": "Cerca città",
|
||||||
"search-for-trails-places": "Cerca percorsi, luoghi",
|
"search-for-trails-places": "Cerca percorsi, luoghi",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "Cerca percorsi",
|
"search-trails": "Cerca percorsi",
|
||||||
"select-list": "Seleziona lista",
|
"select-list": "Seleziona lista",
|
||||||
"settings": "Impostazioni",
|
"settings": "Impostazioni",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Bewaar eerst je wandelroute",
|
"save-your-trail-first": "Bewaar eerst je wandelroute",
|
||||||
"search-cities": "Zoeken naar steden",
|
"search-cities": "Zoeken naar steden",
|
||||||
"search-for-trails-places": "Zoeken naar locaties en wandelroutes",
|
"search-for-trails-places": "Zoeken naar locaties en wandelroutes",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "Zoeken naar wandelroutes",
|
"search-trails": "Zoeken naar wandelroutes",
|
||||||
"select-list": "Kies een lijst",
|
"select-list": "Kies een lijst",
|
||||||
"settings": "Instellingen",
|
"settings": "Instellingen",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Najpierw zapisz swój szlak",
|
"save-your-trail-first": "Najpierw zapisz swój szlak",
|
||||||
"search-cities": "Szukaj miasta",
|
"search-cities": "Szukaj miasta",
|
||||||
"search-for-trails-places": "Szukaj szlaków lub miejsc",
|
"search-for-trails-places": "Szukaj szlaków lub miejsc",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "Szukaj szlaków",
|
"search-trails": "Szukaj szlaków",
|
||||||
"select-list": "Wybierz Listę",
|
"select-list": "Wybierz Listę",
|
||||||
"settings": "Ustawienia",
|
"settings": "Ustawienia",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "Salve sua trilha primeiro",
|
"save-your-trail-first": "Salve sua trilha primeiro",
|
||||||
"search-cities": "Procurar cidades",
|
"search-cities": "Procurar cidades",
|
||||||
"search-for-trails-places": "Procurar trilhos, locais",
|
"search-for-trails-places": "Procurar trilhos, locais",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "Procurar trilhos",
|
"search-trails": "Procurar trilhos",
|
||||||
"select-list": "Selecionar lista",
|
"select-list": "Selecionar lista",
|
||||||
"settings": "Definições",
|
"settings": "Definições",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"save-your-trail-first": "先保存你的路线",
|
"save-your-trail-first": "先保存你的路线",
|
||||||
"search-cities": "搜索城市",
|
"search-cities": "搜索城市",
|
||||||
"search-for-trails-places": "搜索路线、地点",
|
"search-for-trails-places": "搜索路线、地点",
|
||||||
|
"search-places": "",
|
||||||
"search-trails": "搜索路线",
|
"search-trails": "搜索路线",
|
||||||
"select-list": "选择列表",
|
"select-list": "选择列表",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
|
|||||||
183
web/src/lib/stores/search_store.ts
Normal file
183
web/src/lib/stores/search_store.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import { APIError } from "$lib/util/api_util";
|
||||||
|
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
|
||||||
|
|
||||||
|
export type LocationSearchResult = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
category: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TrailSearchResult = {
|
||||||
|
id: string;
|
||||||
|
_geo: {
|
||||||
|
lat: number,
|
||||||
|
lon: number
|
||||||
|
}
|
||||||
|
auhtor: string;
|
||||||
|
category: string;
|
||||||
|
completed: boolean;
|
||||||
|
created: number;
|
||||||
|
date: number;
|
||||||
|
description: string;
|
||||||
|
difficulty: "easy" | "moderate" | "difficult"
|
||||||
|
distance: number;
|
||||||
|
duration: number
|
||||||
|
elevation_gain: number;
|
||||||
|
elevation_loss: number
|
||||||
|
location: string;
|
||||||
|
name: string;
|
||||||
|
public: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NominatimResponse = {
|
||||||
|
type: string
|
||||||
|
licence: string
|
||||||
|
features: Feature[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feature = {
|
||||||
|
type: string
|
||||||
|
properties: Properties
|
||||||
|
bbox: number[]
|
||||||
|
geometry: Geometry
|
||||||
|
}
|
||||||
|
|
||||||
|
type Address = {
|
||||||
|
amenity: string
|
||||||
|
road: string
|
||||||
|
neighbourhood: string
|
||||||
|
suburb: string
|
||||||
|
city_district: string
|
||||||
|
city: string
|
||||||
|
state: string
|
||||||
|
"ISO3166-2-lvl4": string
|
||||||
|
postcode: string
|
||||||
|
country: string
|
||||||
|
country_code: string
|
||||||
|
}
|
||||||
|
type Properties = {
|
||||||
|
place_id: number
|
||||||
|
osm_type: string
|
||||||
|
osm_id: number
|
||||||
|
place_rank: number
|
||||||
|
category: string
|
||||||
|
type: string
|
||||||
|
importance: number
|
||||||
|
addresstype: string
|
||||||
|
name: string
|
||||||
|
display_name: string
|
||||||
|
address: Address
|
||||||
|
}
|
||||||
|
|
||||||
|
type Geometry = {
|
||||||
|
type: string
|
||||||
|
coordinates: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function searchTrails(q: string, options: SearchParams): Promise<Hits<TrailSearchResult>> {
|
||||||
|
const r = await fetch("/api/v1/search/trails", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
q,
|
||||||
|
options
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: SearchResponse<TrailSearchResult> = await r.json();
|
||||||
|
|
||||||
|
return response.hits
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchLocations(q: string, limit?: number): Promise<Hits<LocationSearchResult>> {
|
||||||
|
const nominatimURL = env.PUBLIC_NOMINATIM_URL ?? "https://nominatim.openstreetmap.org"
|
||||||
|
const r = await fetch(`${nominatimURL}/search?q=${q}&format=geojson&addressdetails=1${limit ? '&limit=' + limit : ''}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
const response: NominatimResponse = await r.json();
|
||||||
|
return response.features.map(f => ({
|
||||||
|
category: f.properties.category,
|
||||||
|
type: f.properties.type == "administrative" ? f.properties.addresstype : f.properties.type,
|
||||||
|
description: getLocationDescription(f.properties.address),
|
||||||
|
name: f.properties.name.length ? f.properties.name : f.properties.display_name,
|
||||||
|
lat: f.geometry.coordinates[1],
|
||||||
|
lon: f.geometry.coordinates[0],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchLocationReverse(lat: number, lon: number) {
|
||||||
|
const nominatimURL = env.PUBLIC_NOMINATIM_URL ?? "https://nominatim.openstreetmap.org"
|
||||||
|
const r = await fetch(`${nominatimURL}/reverse?lat=${lat}&lon=${lon}&format=geojson&addressdetails=1`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
const response: NominatimResponse = await r.json();
|
||||||
|
|
||||||
|
if (response.features?.at(0)?.properties.address) {
|
||||||
|
return getLocationDescription(response.features[0].properties.address)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocationDescription(address: Address) {
|
||||||
|
let description = ""
|
||||||
|
|
||||||
|
if (address.country) {
|
||||||
|
description += address.country;
|
||||||
|
}
|
||||||
|
if (address.state) {
|
||||||
|
description = `${address.state}, ` + description
|
||||||
|
}
|
||||||
|
if (address.city) {
|
||||||
|
description = `${address.city}, ` + description
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchMulti(options: MultiSearchParams): Promise<MultiSearchResult<any>[]> {
|
||||||
|
|
||||||
|
const locationQuery = options.queries.find(q => q.indexUid === "locations");
|
||||||
|
const locationQueryIndex = locationQuery ? options.queries.indexOf(locationQuery) : -1
|
||||||
|
if (locationQueryIndex >= 0) {
|
||||||
|
options.queries.splice(locationQueryIndex, 1)
|
||||||
|
}
|
||||||
|
const r = await fetch("/api/v1/search/multi", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(options),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: MultiSearchResponse<any> = await r.json();
|
||||||
|
|
||||||
|
|
||||||
|
if (locationQuery && locationQuery.q !== undefined && locationQuery.q !== null) {
|
||||||
|
const locationsResults = await searchLocations(locationQuery.q, locationQuery.limit)
|
||||||
|
response.results.splice(locationQueryIndex,
|
||||||
|
0,
|
||||||
|
{ hits: locationsResults, indexUid: "locations", query: locationQuery.q, processingTimeMs: 0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return response.results
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { LocationSearchResult } from "$lib/stores/search_store";
|
||||||
|
|
||||||
export const icons = ["0",
|
export const icons = ["0",
|
||||||
"1",
|
"1",
|
||||||
"2",
|
"2",
|
||||||
@@ -1388,4 +1390,153 @@ export const icons = ["0",
|
|||||||
"yen-sign",
|
"yen-sign",
|
||||||
"yin-yang",
|
"yin-yang",
|
||||||
"z",
|
"z",
|
||||||
]
|
] as const
|
||||||
|
|
||||||
|
export function getIconForLocation(l: LocationSearchResult): typeof icons[number] {
|
||||||
|
if (l.category === "aerialway") {
|
||||||
|
return "cable-car"
|
||||||
|
} else if (l.category === "amenity") {
|
||||||
|
switch (l.type) {
|
||||||
|
// Sustenance
|
||||||
|
case "bar":
|
||||||
|
return "martini-glass"
|
||||||
|
case "pub":
|
||||||
|
case "biergarten":
|
||||||
|
return "beer-mug-empty"
|
||||||
|
case "cafe":
|
||||||
|
return "mug-saucer"
|
||||||
|
case "ice_cream":
|
||||||
|
return "ice-cream"
|
||||||
|
case "fast_food":
|
||||||
|
return "burger"
|
||||||
|
case "restaurant":
|
||||||
|
case "food_court":
|
||||||
|
return "utensils"
|
||||||
|
|
||||||
|
// Transportation
|
||||||
|
case "ferry_terminal":
|
||||||
|
return "ferry"
|
||||||
|
case "bus_station":
|
||||||
|
return "bus"
|
||||||
|
case "parking":
|
||||||
|
return "square-parking"
|
||||||
|
// Others
|
||||||
|
case "place_of_worship":
|
||||||
|
return "place-of-worship"
|
||||||
|
}
|
||||||
|
} else if (l.category === "building") {
|
||||||
|
switch (l.type) {
|
||||||
|
// Accommodation
|
||||||
|
case "hotel":
|
||||||
|
return "hotel"
|
||||||
|
|
||||||
|
// Religious
|
||||||
|
case "cathedral":
|
||||||
|
case "church":
|
||||||
|
case "chapel":
|
||||||
|
return "church"
|
||||||
|
case "mosque":
|
||||||
|
return "mosque"
|
||||||
|
case "synagogue":
|
||||||
|
return "synagogue"
|
||||||
|
case "temple":
|
||||||
|
return "gopuram"
|
||||||
|
|
||||||
|
// Civic
|
||||||
|
case "train_station":
|
||||||
|
return "train"
|
||||||
|
}
|
||||||
|
} else if (l.category === "natural") {
|
||||||
|
switch (l.type) {
|
||||||
|
case "peak":
|
||||||
|
return "mountain"
|
||||||
|
case "wood":
|
||||||
|
return "tree"
|
||||||
|
case "beach":
|
||||||
|
return "umbrella-beach"
|
||||||
|
case "water":
|
||||||
|
return "water"
|
||||||
|
}
|
||||||
|
} else if (l.category === "leisure") {
|
||||||
|
switch (l.type) {
|
||||||
|
case "garden":
|
||||||
|
return "leaf"
|
||||||
|
case "park":
|
||||||
|
return "tree"
|
||||||
|
case "swimming_area":
|
||||||
|
case "swimming_pool":
|
||||||
|
case "water_park ":
|
||||||
|
return "person-swimming"
|
||||||
|
}
|
||||||
|
} else if (l.category === "aeroway") {
|
||||||
|
return "plane-departure"
|
||||||
|
} else if (l.category === "railway") {
|
||||||
|
switch (l.type) {
|
||||||
|
case "station":
|
||||||
|
case "halt":
|
||||||
|
return "train"
|
||||||
|
case "subway_entrance":
|
||||||
|
return "train-subway"
|
||||||
|
case "tram_stop":
|
||||||
|
return "train-tram"
|
||||||
|
}
|
||||||
|
} else if (l.category === "tourism") {
|
||||||
|
switch (l.type) {
|
||||||
|
case "hotel":
|
||||||
|
return "hotel"
|
||||||
|
case "alpine_hut":
|
||||||
|
case "apartment":
|
||||||
|
case "chalet":
|
||||||
|
return "house-chimney"
|
||||||
|
case "guest_house":
|
||||||
|
case "motel":
|
||||||
|
case "hostel":
|
||||||
|
return "bed"
|
||||||
|
case "caravan_site":
|
||||||
|
return "caravan"
|
||||||
|
case "camp_site":
|
||||||
|
case "camp_pitch":
|
||||||
|
return "campground"
|
||||||
|
case "aqaurium":
|
||||||
|
return "fish-fins"
|
||||||
|
case "artwork":
|
||||||
|
return "palette"
|
||||||
|
case "viewpoint":
|
||||||
|
case "attraction":
|
||||||
|
return "eye"
|
||||||
|
case "zoo":
|
||||||
|
return "hippo"
|
||||||
|
}
|
||||||
|
} else if (l.category === "tourism") {
|
||||||
|
switch (l.type) {
|
||||||
|
case "national_park":
|
||||||
|
return "tree"
|
||||||
|
}
|
||||||
|
} else if (l.category === "place") {
|
||||||
|
switch (l.type) {
|
||||||
|
case "city":
|
||||||
|
return "city"
|
||||||
|
case "town":
|
||||||
|
case "village":
|
||||||
|
case "hamlet":
|
||||||
|
return "building"
|
||||||
|
}
|
||||||
|
} else if (l.category === "boundary") {
|
||||||
|
switch (l.type) {
|
||||||
|
case "city":
|
||||||
|
return "city"
|
||||||
|
case "town":
|
||||||
|
case "village":
|
||||||
|
case "hamlet":
|
||||||
|
return "building"
|
||||||
|
}
|
||||||
|
} else if(l.category === "historic") {
|
||||||
|
return "eye"
|
||||||
|
}
|
||||||
|
switch (l.category) {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return "location-dot"
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
|
||||||
|
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
|
||||||
import Search, {
|
import Search, {
|
||||||
type SearchItem,
|
type SearchItem,
|
||||||
} from "$lib/components/base/search.svelte";
|
} from "$lib/components/base/search.svelte";
|
||||||
@@ -7,22 +9,23 @@
|
|||||||
import Scene from "$lib/components/scene.svelte";
|
import Scene from "$lib/components/scene.svelte";
|
||||||
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
|
import {
|
||||||
|
searchMulti,
|
||||||
|
type LocationSearchResult,
|
||||||
|
type TrailSearchResult,
|
||||||
|
} from "$lib/stores/search_store.js";
|
||||||
|
import { theme } from "$lib/stores/theme_store";
|
||||||
import { currentUser } from "$lib/stores/user_store";
|
import { currentUser } from "$lib/stores/user_store";
|
||||||
import { country_codes } from "$lib/util/country_code_util";
|
import { getIconForLocation } from "$lib/util/icon_util.js";
|
||||||
import { Canvas } from "@threlte/core";
|
import { Canvas } from "@threlte/core";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
|
|
||||||
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
|
|
||||||
import { theme } from "$lib/stores/theme_store";
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let searchDropdownItems: SearchItem[] = $state([]);
|
let searchDropdownItems: SearchItem[] = $state([]);
|
||||||
|
|
||||||
async function search(q: string) {
|
async function search(q: string) {
|
||||||
const r = await fetch("/api/v1/search/multi", {
|
const r = await searchMulti({
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
indexUid: "trails",
|
indexUid: "trails",
|
||||||
@@ -30,46 +33,34 @@
|
|||||||
limit: 3,
|
limit: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
indexUid: "cities500",
|
indexUid: "locations",
|
||||||
q: q,
|
q: q,
|
||||||
limit: 3,
|
limit: 5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await r.json();
|
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
||||||
|
|
||||||
const trailItems = response.results[0].hits.map(
|
|
||||||
(t: Record<string, any>) => ({
|
|
||||||
text: t.name,
|
text: t.name,
|
||||||
description: `Trail | ${t.location}`,
|
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
|
||||||
value: t.id,
|
value: t.id,
|
||||||
icon: "route",
|
icon: "route",
|
||||||
}),
|
}));
|
||||||
);
|
const cityItems = r[1].hits.map((c: LocationSearchResult) => ({
|
||||||
const cityItems = response.results[1].hits.map(
|
|
||||||
(c: Record<string, any>) => ({
|
|
||||||
text: c.name,
|
text: c.name,
|
||||||
description: `City ${c.division ? `| ${c.division} ` : ""}| ${
|
description: c.description,
|
||||||
country_codes[
|
|
||||||
c["country code"] as keyof typeof country_codes
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
value: c,
|
value: c,
|
||||||
icon: "city",
|
icon: getIconForLocation(c),
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
searchDropdownItems = [...trailItems, ...cityItems];
|
searchDropdownItems = [...trailItems, ...cityItems];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchClick(item: SearchItem) {
|
function handleSearchClick(item: SearchItem) {
|
||||||
if (item.icon == "city") {
|
|
||||||
goto(`/map/?lat=${item.value._geo.lat}&lon=${item.value._geo.lng}`);
|
|
||||||
}
|
|
||||||
if (item.icon == "route") {
|
if (item.icon == "route") {
|
||||||
goto(`/trail/view/${item.value}`);
|
goto(`/trail/view/${item.value}`);
|
||||||
|
} else {
|
||||||
|
goto(`/map/?lat=${item.value.lat}&lon=${item.value.lon}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
17
web/src/routes/api/v1/search/nominatim/+server.ts
Normal file
17
web/src/routes/api/v1/search/nominatim/+server.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export async function POST(event: RequestEvent) {
|
||||||
|
const data = await event.request.json()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await event.fetch(`${env.NOMINATIM_URL}/search?q=${data.q}&format=geocodejson&limit=${data.limit}`)
|
||||||
|
return json(r);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(e);
|
||||||
|
|
||||||
|
throw error(e.httpStatus, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,10 @@
|
|||||||
import { validator } from "@felte/validator-zod";
|
import { validator } from "@felte/validator-zod";
|
||||||
import { createForm } from "felte";
|
import { createForm } from "felte";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
searchTrails,
|
||||||
|
type TrailSearchResult,
|
||||||
|
} from "$lib/stores/search_store.js";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -150,28 +154,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function search(q: string) {
|
async function search(q: string) {
|
||||||
const r = await fetch("/api/v1/search/trails", {
|
try {
|
||||||
method: "POST",
|
const r = await searchTrails(q, {
|
||||||
body: JSON.stringify({
|
|
||||||
q,
|
|
||||||
options: {
|
|
||||||
filter: `author = ${$currentUser?.id} OR public = true`,
|
filter: `author = ${$currentUser?.id} OR public = true`,
|
||||||
sort: ["name:desc"],
|
sort: ["name:desc"],
|
||||||
limit: 3,
|
limit: 3,
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await r.json();
|
searchDropdownItems = r
|
||||||
|
.filter(
|
||||||
searchDropdownItems = response.hits
|
(h: TrailSearchResult) => !$formData.trails?.includes(h.id),
|
||||||
.filter((h: List) => !$formData.trails?.includes(h.id!))
|
)
|
||||||
.map((t: Record<string, any>) => ({
|
.map((t) => ({
|
||||||
text: t.name,
|
text: t.name,
|
||||||
description: `${t.location ?? "-"}`,
|
description: `${t.location ?? "-"}`,
|
||||||
value: t.id,
|
value: t.id,
|
||||||
icon: "route",
|
icon: "route",
|
||||||
}));
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSearchClick(item: SearchItem) {
|
async function handleSearchClick(item: SearchItem) {
|
||||||
|
|||||||
@@ -17,8 +17,13 @@
|
|||||||
TrailFilter,
|
TrailFilter,
|
||||||
} from "$lib/models/trail";
|
} from "$lib/models/trail";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
|
import {
|
||||||
|
searchMulti,
|
||||||
|
type LocationSearchResult,
|
||||||
|
type TrailSearchResult,
|
||||||
|
} from "$lib/stores/search_store";
|
||||||
import { trails_search_bounding_box } from "$lib/stores/trail_store";
|
import { trails_search_bounding_box } from "$lib/stores/trail_store";
|
||||||
import { country_codes } from "$lib/util/country_code_util";
|
import { getIconForLocation } from "$lib/util/icon_util";
|
||||||
import * as M from "maplibre-gl";
|
import * as M from "maplibre-gl";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
@@ -44,9 +49,7 @@
|
|||||||
onMount(async () => {});
|
onMount(async () => {});
|
||||||
|
|
||||||
async function search(q: string) {
|
async function search(q: string) {
|
||||||
const r = await fetch("/api/v1/search/multi", {
|
const r = await searchMulti({
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
indexUid: "trails",
|
indexUid: "trails",
|
||||||
@@ -54,42 +57,31 @@
|
|||||||
limit: 3,
|
limit: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
indexUid: "cities500",
|
indexUid: "locations",
|
||||||
q: q,
|
q: q,
|
||||||
limit: 3,
|
limit: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await r.json();
|
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
||||||
|
|
||||||
const trailItems = response.results[0].hits.map(
|
|
||||||
(t: Record<string, any>) => ({
|
|
||||||
text: t.name,
|
text: t.name,
|
||||||
description: `Trail | ${t.location}`,
|
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
|
||||||
value: t,
|
value: t,
|
||||||
icon: "route",
|
icon: "route",
|
||||||
}),
|
}));
|
||||||
);
|
const cityItems = r[1].hits.map((c: LocationSearchResult) => ({
|
||||||
const cityItems = response.results[1].hits.map(
|
|
||||||
(c: Record<string, any>) => ({
|
|
||||||
text: c.name,
|
text: c.name,
|
||||||
description: `City ${c.division ? `| ${c.division} ` : ""}| ${
|
description: c.description,
|
||||||
country_codes[
|
|
||||||
c["country code"] as keyof typeof country_codes
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
value: c,
|
value: c,
|
||||||
icon: "city",
|
icon: getIconForLocation(c),
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
searchDropdownItems = [...trailItems, ...cityItems];
|
searchDropdownItems = [...trailItems, ...cityItems];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchClick(item: SearchItem) {
|
function handleSearchClick(item: SearchItem) {
|
||||||
map?.setCenter([item.value._geo.lng, item.value._geo.lat]);
|
map?.setCenter([item.value.lon, item.value.lat]);
|
||||||
map?.setZoom(14);
|
map?.setZoom(14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,14 @@
|
|||||||
} from "$lib/components/base/select.svelte";
|
} from "$lib/components/base/select.svelte";
|
||||||
|
|
||||||
import TextField from "$lib/components/base/text_field.svelte";
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
|
import {
|
||||||
|
searchLocations,
|
||||||
|
type LocationSearchResult,
|
||||||
|
} from "$lib/stores/search_store";
|
||||||
import { settings_update } from "$lib/stores/settings_store";
|
import { settings_update } from "$lib/stores/settings_store";
|
||||||
import { currentUser } from "$lib/stores/user_store";
|
import { currentUser } from "$lib/stores/user_store";
|
||||||
import { country_codes } from "$lib/util/country_code_util";
|
import { country_codes } from "$lib/util/country_code_util";
|
||||||
|
import { getIconForLocation } from "$lib/util/icon_util";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
@@ -42,18 +47,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function searchCities(q: string) {
|
async function searchCities(q: string) {
|
||||||
const r = await fetch("/api/v1/search/cities500", {
|
const r = await searchLocations(q, 5);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
searchDropdownItems = r.map((h: LocationSearchResult) => ({
|
||||||
});
|
|
||||||
const result = await r.json();
|
|
||||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
|
||||||
text: h.name,
|
text: h.name,
|
||||||
description: `${h.division ? `${h.division} | ` : ""}${
|
description: h.description,
|
||||||
country_codes[h["country code"] as keyof typeof country_codes]
|
|
||||||
}`,
|
|
||||||
value: h,
|
value: h,
|
||||||
icon: "city",
|
icon: getIconForLocation(h),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<Search
|
<Search
|
||||||
items={searchDropdownItems}
|
items={searchDropdownItems}
|
||||||
placeholder="{$_('search-cities')}..."
|
placeholder="{$_('search-for-trails-places')}..."
|
||||||
clearAfterSelect={false}
|
clearAfterSelect={false}
|
||||||
bind:value={citySearchQuery}
|
bind:value={citySearchQuery}
|
||||||
onupdate={searchCities}
|
onupdate={searchCities}
|
||||||
|
|||||||
@@ -72,12 +72,17 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||||
|
import {
|
||||||
|
searchLocationReverse,
|
||||||
|
searchLocations,
|
||||||
|
} from "$lib/stores/search_store.js";
|
||||||
|
import { getIconForLocation } from "$lib/util/icon_util.js";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let map: M.Map | undefined = $state();
|
let map: M.Map | undefined = $state();
|
||||||
let mapTrail: Trail[] = $state([]);
|
let mapTrail: Trail[] = $state([]);
|
||||||
let lists = $state(data.lists)
|
let lists = $state(data.lists);
|
||||||
|
|
||||||
let waypointModal: WaypointModal;
|
let waypointModal: WaypointModal;
|
||||||
let summitLogModal: SummitLogModal;
|
let summitLogModal: SummitLogModal;
|
||||||
@@ -315,22 +320,11 @@
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const r = await fetch("/api/v1/search/cities500", {
|
const r = await searchLocationReverse($formData.lat!, $formData.lon!);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
q: "",
|
|
||||||
options: {
|
|
||||||
filter: [
|
|
||||||
`_geoRadius(${$formData.lat}, ${$formData.lon}, 10000)`,
|
|
||||||
],
|
|
||||||
sort: [`_geoPoint(${$formData.lat}, ${$formData.lon}):asc`],
|
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const closestCity = (await r.json()).hits[0];
|
|
||||||
|
|
||||||
setFields("location", closestCity.name);
|
if (r) {
|
||||||
|
setFields("location", r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearWaypoints() {
|
function clearWaypoints() {
|
||||||
@@ -514,11 +508,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopDrawing() {
|
async function stopDrawing() {
|
||||||
drawingActive = false;
|
drawingActive = false;
|
||||||
for (const anchor of anchors) {
|
for (const anchor of anchors) {
|
||||||
anchor.marker?.remove();
|
anchor.marker?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)) {
|
||||||
|
$formData.lat = route.trk
|
||||||
|
?.at(0)
|
||||||
|
?.trkseg?.at(0)
|
||||||
|
?.trkpt?.at(0)?.$.lat;
|
||||||
|
$formData.lon = route.trk
|
||||||
|
?.at(0)
|
||||||
|
?.trkseg?.at(0)
|
||||||
|
?.trkpt?.at(0)?.$.lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await searchLocationReverse($formData.lat!, $formData.lon!);
|
||||||
|
|
||||||
|
if (r) {
|
||||||
|
setFields("location", r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMapClick(e: M.MapMouseEvent) {
|
async function handleMapClick(e: M.MapMouseEvent) {
|
||||||
@@ -799,25 +810,19 @@
|
|||||||
|
|
||||||
function handleSearchClick(item: SearchItem) {
|
function handleSearchClick(item: SearchItem) {
|
||||||
map?.flyTo({
|
map?.flyTo({
|
||||||
center: [item.value._geo.lng, item.value._geo.lat],
|
center: [item.value.lon, item.value.lat],
|
||||||
zoom: 13,
|
zoom: 13,
|
||||||
animate: false,
|
animate: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchCities(q: string) {
|
async function searchCities(q: string) {
|
||||||
const r = await fetch("/api/v1/search/cities500", {
|
const r = await searchLocations(q);
|
||||||
method: "POST",
|
searchDropdownItems = r.map((h) => ({
|
||||||
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
|
||||||
});
|
|
||||||
const result = await r.json();
|
|
||||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
|
||||||
text: h.name,
|
text: h.name,
|
||||||
description: `${h.division ? `${h.division} | ` : ""}${
|
description: h.description,
|
||||||
country_codes[h["country code"] as keyof typeof country_codes]
|
|
||||||
}`,
|
|
||||||
value: h,
|
value: h,
|
||||||
icon: "city",
|
icon: getIconForLocation(h),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
let gpxData = $derived($formData.expand?.gpx_data);
|
let gpxData = $derived($formData.expand?.gpx_data);
|
||||||
@@ -845,7 +850,7 @@
|
|||||||
<Search
|
<Search
|
||||||
onupdate={(q) => searchCities(q)}
|
onupdate={(q) => searchCities(q)}
|
||||||
onclick={(item) => handleSearchClick(item)}
|
onclick={(item) => handleSearchClick(item)}
|
||||||
placeholder="{$_('search-cities')}..."
|
placeholder="{$_('search-places')}..."
|
||||||
items={searchDropdownItems}
|
items={searchDropdownItems}
|
||||||
></Search>
|
></Search>
|
||||||
<hr class="border-input-border" />
|
<hr class="border-input-border" />
|
||||||
|
|||||||
Reference in New Issue
Block a user