switches to nominatim
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
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, {
|
||||
type SearchItem,
|
||||
} from "$lib/components/base/search.svelte";
|
||||
@@ -7,69 +9,58 @@
|
||||
import Scene from "$lib/components/scene.svelte";
|
||||
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||
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 { country_codes } from "$lib/util/country_code_util";
|
||||
import { getIconForLocation } from "$lib/util/icon_util.js";
|
||||
import { Canvas } from "@threlte/core";
|
||||
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 searchDropdownItems: SearchItem[] = $state([]);
|
||||
|
||||
async function search(q: string) {
|
||||
const r = await fetch("/api/v1/search/multi", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
queries: [
|
||||
{
|
||||
indexUid: "trails",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "cities500",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
],
|
||||
}),
|
||||
const r = await searchMulti({
|
||||
queries: [
|
||||
{
|
||||
indexUid: "trails",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "locations",
|
||||
q: q,
|
||||
limit: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await r.json();
|
||||
|
||||
const trailItems = response.results[0].hits.map(
|
||||
(t: Record<string, any>) => ({
|
||||
text: t.name,
|
||||
description: `Trail | ${t.location}`,
|
||||
value: t.id,
|
||||
icon: "route",
|
||||
}),
|
||||
);
|
||||
const cityItems = response.results[1].hits.map(
|
||||
(c: Record<string, any>) => ({
|
||||
text: c.name,
|
||||
description: `City ${c.division ? `| ${c.division} ` : ""}| ${
|
||||
country_codes[
|
||||
c["country code"] as keyof typeof country_codes
|
||||
]
|
||||
}`,
|
||||
value: c,
|
||||
icon: "city",
|
||||
}),
|
||||
);
|
||||
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
||||
text: t.name,
|
||||
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
|
||||
value: t.id,
|
||||
icon: "route",
|
||||
}));
|
||||
const cityItems = r[1].hits.map((c: LocationSearchResult) => ({
|
||||
text: c.name,
|
||||
description: c.description,
|
||||
value: c,
|
||||
icon: getIconForLocation(c),
|
||||
}));
|
||||
|
||||
searchDropdownItems = [...trailItems, ...cityItems];
|
||||
}
|
||||
|
||||
function handleSearchClick(item: SearchItem) {
|
||||
if (item.icon == "city") {
|
||||
goto(`/map/?lat=${item.value._geo.lat}&lon=${item.value._geo.lng}`);
|
||||
}
|
||||
if (item.icon == "route") {
|
||||
goto(`/trail/view/${item.value}`);
|
||||
} else {
|
||||
goto(`/map/?lat=${item.value.lat}&lon=${item.value.lon}`);
|
||||
}
|
||||
}
|
||||
</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 { createForm } from "felte";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
searchTrails,
|
||||
type TrailSearchResult,
|
||||
} from "$lib/stores/search_store.js";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -150,28 +154,26 @@
|
||||
}
|
||||
|
||||
async function search(q: string) {
|
||||
const r = await fetch("/api/v1/search/trails", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
q,
|
||||
options: {
|
||||
filter: `author = ${$currentUser?.id} OR public = true`,
|
||||
sort: ["name:desc"],
|
||||
limit: 3,
|
||||
},
|
||||
}),
|
||||
});
|
||||
try {
|
||||
const r = await searchTrails(q, {
|
||||
filter: `author = ${$currentUser?.id} OR public = true`,
|
||||
sort: ["name:desc"],
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
const response = await r.json();
|
||||
|
||||
searchDropdownItems = response.hits
|
||||
.filter((h: List) => !$formData.trails?.includes(h.id!))
|
||||
.map((t: Record<string, any>) => ({
|
||||
text: t.name,
|
||||
description: `${t.location ?? "-"}`,
|
||||
value: t.id,
|
||||
icon: "route",
|
||||
}));
|
||||
searchDropdownItems = r
|
||||
.filter(
|
||||
(h: TrailSearchResult) => !$formData.trails?.includes(h.id),
|
||||
)
|
||||
.map((t) => ({
|
||||
text: t.name,
|
||||
description: `${t.location ?? "-"}`,
|
||||
value: t.id,
|
||||
icon: "route",
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSearchClick(item: SearchItem) {
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
TrailFilter,
|
||||
} from "$lib/models/trail";
|
||||
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 { country_codes } from "$lib/util/country_code_util";
|
||||
import { getIconForLocation } from "$lib/util/icon_util";
|
||||
import * as M from "maplibre-gl";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
@@ -44,52 +49,39 @@
|
||||
onMount(async () => {});
|
||||
|
||||
async function search(q: string) {
|
||||
const r = await fetch("/api/v1/search/multi", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
queries: [
|
||||
{
|
||||
indexUid: "trails",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "cities500",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
],
|
||||
}),
|
||||
const r = await searchMulti({
|
||||
queries: [
|
||||
{
|
||||
indexUid: "trails",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "locations",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await r.json();
|
||||
|
||||
const trailItems = response.results[0].hits.map(
|
||||
(t: Record<string, any>) => ({
|
||||
text: t.name,
|
||||
description: `Trail | ${t.location}`,
|
||||
value: t,
|
||||
icon: "route",
|
||||
}),
|
||||
);
|
||||
const cityItems = response.results[1].hits.map(
|
||||
(c: Record<string, any>) => ({
|
||||
text: c.name,
|
||||
description: `City ${c.division ? `| ${c.division} ` : ""}| ${
|
||||
country_codes[
|
||||
c["country code"] as keyof typeof country_codes
|
||||
]
|
||||
}`,
|
||||
value: c,
|
||||
icon: "city",
|
||||
}),
|
||||
);
|
||||
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
||||
text: t.name,
|
||||
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
|
||||
value: t,
|
||||
icon: "route",
|
||||
}));
|
||||
const cityItems = r[1].hits.map((c: LocationSearchResult) => ({
|
||||
text: c.name,
|
||||
description: c.description,
|
||||
value: c,
|
||||
icon: getIconForLocation(c),
|
||||
}));
|
||||
|
||||
searchDropdownItems = [...trailItems, ...cityItems];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,14 @@
|
||||
} from "$lib/components/base/select.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 { currentUser } from "$lib/stores/user_store";
|
||||
import { country_codes } from "$lib/util/country_code_util";
|
||||
import { getIconForLocation } from "$lib/util/icon_util";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
@@ -42,18 +47,13 @@
|
||||
});
|
||||
|
||||
async function searchCities(q: string) {
|
||||
const r = await fetch("/api/v1/search/cities500", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
||||
});
|
||||
const result = await r.json();
|
||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
||||
const r = await searchLocations(q, 5);
|
||||
|
||||
searchDropdownItems = r.map((h: LocationSearchResult) => ({
|
||||
text: h.name,
|
||||
description: `${h.division ? `${h.division} | ` : ""}${
|
||||
country_codes[h["country code"] as keyof typeof country_codes]
|
||||
}`,
|
||||
description: h.description,
|
||||
value: h,
|
||||
icon: "city",
|
||||
icon: getIconForLocation(h),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
<div class="mt-3">
|
||||
<Search
|
||||
items={searchDropdownItems}
|
||||
placeholder="{$_('search-cities')}..."
|
||||
placeholder="{$_('search-for-trails-places')}..."
|
||||
clearAfterSelect={false}
|
||||
bind:value={citySearchQuery}
|
||||
onupdate={searchCities}
|
||||
|
||||
@@ -72,12 +72,17 @@
|
||||
import { z } from "zod";
|
||||
import { page } from "$app/state";
|
||||
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 map: M.Map | undefined = $state();
|
||||
let mapTrail: Trail[] = $state([]);
|
||||
let lists = $state(data.lists)
|
||||
let lists = $state(data.lists);
|
||||
|
||||
let waypointModal: WaypointModal;
|
||||
let summitLogModal: SummitLogModal;
|
||||
@@ -315,22 +320,11 @@
|
||||
});
|
||||
return;
|
||||
}
|
||||
const r = await fetch("/api/v1/search/cities500", {
|
||||
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];
|
||||
const r = await searchLocationReverse($formData.lat!, $formData.lon!);
|
||||
|
||||
setFields("location", closestCity.name);
|
||||
if (r) {
|
||||
setFields("location", r);
|
||||
}
|
||||
}
|
||||
|
||||
function clearWaypoints() {
|
||||
@@ -514,11 +508,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
function stopDrawing() {
|
||||
async function stopDrawing() {
|
||||
drawingActive = false;
|
||||
for (const anchor of anchors) {
|
||||
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) {
|
||||
@@ -799,25 +810,19 @@
|
||||
|
||||
function handleSearchClick(item: SearchItem) {
|
||||
map?.flyTo({
|
||||
center: [item.value._geo.lng, item.value._geo.lat],
|
||||
center: [item.value.lon, item.value.lat],
|
||||
zoom: 13,
|
||||
animate: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function searchCities(q: string) {
|
||||
const r = await fetch("/api/v1/search/cities500", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
||||
});
|
||||
const result = await r.json();
|
||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
||||
const r = await searchLocations(q);
|
||||
searchDropdownItems = r.map((h) => ({
|
||||
text: h.name,
|
||||
description: `${h.division ? `${h.division} | ` : ""}${
|
||||
country_codes[h["country code"] as keyof typeof country_codes]
|
||||
}`,
|
||||
description: h.description,
|
||||
value: h,
|
||||
icon: "city",
|
||||
icon: getIconForLocation(h),
|
||||
}));
|
||||
}
|
||||
let gpxData = $derived($formData.expand?.gpx_data);
|
||||
@@ -845,7 +850,7 @@
|
||||
<Search
|
||||
onupdate={(q) => searchCities(q)}
|
||||
onclick={(item) => handleSearchClick(item)}
|
||||
placeholder="{$_('search-cities')}..."
|
||||
placeholder="{$_('search-places')}..."
|
||||
items={searchDropdownItems}
|
||||
></Search>
|
||||
<hr class="border-input-border" />
|
||||
|
||||
Reference in New Issue
Block a user