major performance improvements map
This commit is contained in:
30
db/migrations/1744651602_add_polyline.go
Normal file
30
db/migrations/1744651602_add_polyline.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/meilisearch/meilisearch-go"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
client := meilisearch.New(os.Getenv("MEILI_URL"), meilisearch.WithAPIKey(os.Getenv("MEILI_MASTER_KEY")))
|
||||||
|
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
searchableAttributes := []string{
|
||||||
|
"author_name",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"location",
|
||||||
|
"tags",
|
||||||
|
}
|
||||||
|
client.Index("trails").UpdateSearchableAttributes(&searchableAttributes)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, func(app core.App) error {
|
||||||
|
// add down queries...
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/meilisearch/meilisearch-go"
|
"github.com/meilisearch/meilisearch-go"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/twpayne/go-gpx"
|
||||||
|
"github.com/twpayne/go-polyline"
|
||||||
)
|
)
|
||||||
|
|
||||||
func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares bool) map[string]interface{} {
|
func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record, includeShares bool) map[string]interface{} {
|
||||||
photos := r.GetStringSlice("photos")
|
photos := r.GetStringSlice("photos")
|
||||||
thumbnail := ""
|
thumbnail := ""
|
||||||
if len(photos) > 0 {
|
if len(photos) > 0 {
|
||||||
@@ -27,6 +31,11 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
|
|||||||
tags[i] = v.GetString("name")
|
tags[i] = v.GetString("name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
polyline, err := getPolyline(app, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
document := map[string]interface{}{
|
document := map[string]interface{}{
|
||||||
"id": r.Id,
|
"id": r.Id,
|
||||||
"author": r.GetString("author"),
|
"author": r.GetString("author"),
|
||||||
@@ -48,6 +57,7 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
|
|||||||
"thumbnail": thumbnail,
|
"thumbnail": thumbnail,
|
||||||
"gpx": r.GetString("gpx"),
|
"gpx": r.GetString("gpx"),
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
|
"polyline": polyline,
|
||||||
"_geo": map[string]float64{
|
"_geo": map[string]float64{
|
||||||
"lat": r.GetFloat("lat"),
|
"lat": r.GetFloat("lat"),
|
||||||
"lng": r.GetFloat("lon"),
|
"lng": r.GetFloat("lon"),
|
||||||
@@ -61,6 +71,44 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
|
|||||||
return document
|
return document
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPolyline(app core.App, r *core.Record) (string, error) {
|
||||||
|
gpxPath := r.GetString("gpx")
|
||||||
|
if len(gpxPath) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
avatarKey := r.BaseFilesPath() + "/" + gpxPath
|
||||||
|
fsys, err := app.NewFilesystem()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer fsys.Close()
|
||||||
|
|
||||||
|
gpxFile, err := fsys.GetFile(avatarKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer gpxFile.Close()
|
||||||
|
|
||||||
|
content := new(bytes.Buffer)
|
||||||
|
_, err = io.Copy(content, gpxFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
gpxData, err := gpx.Read(content)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
coordinates := make([][]float64, 4)
|
||||||
|
for _, trk := range gpxData.Trk {
|
||||||
|
for _, seg := range trk.TrkSeg {
|
||||||
|
for _, pt := range seg.TrkPt {
|
||||||
|
coordinates = append(coordinates, []float64{pt.Lat, pt.Lon})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(polyline.EncodeCoords(coordinates)), nil
|
||||||
|
}
|
||||||
|
|
||||||
func documentFromListRecord(r *core.Record, includeShares bool) map[string]interface{} {
|
func documentFromListRecord(r *core.Record, includeShares bool) map[string]interface{} {
|
||||||
document := map[string]interface{}{
|
document := map[string]interface{}{
|
||||||
"id": r.Id,
|
"id": r.Id,
|
||||||
@@ -85,7 +133,7 @@ func IndexTrail(app core.App, r *core.Record, author *core.Record, client meilis
|
|||||||
return fmt.Errorf("failed to expand: %v", errs)
|
return fmt.Errorf("failed to expand: %v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
documents := []map[string]interface{}{documentFromTrailRecord(r, author, true)}
|
documents := []map[string]interface{}{documentFromTrailRecord(app, r, author, true)}
|
||||||
|
|
||||||
if _, err := client.Index("trails").AddDocuments(documents); err != nil {
|
if _, err := client.Index("trails").AddDocuments(documents); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -100,7 +148,7 @@ func UpdateTrail(app core.App, r *core.Record, author *core.Record, client meili
|
|||||||
return fmt.Errorf("failed to expand: %v", errs)
|
return fmt.Errorf("failed to expand: %v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
documents := documentFromTrailRecord(r, author, false)
|
documents := documentFromTrailRecord(app, r, author, false)
|
||||||
|
|
||||||
if _, err := client.Index("trails").UpdateDocuments(documents); err != nil {
|
if _, err := client.Index("trails").UpdateDocuments(documents); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
createPopupFromTrail,
|
createPopupFromTrail,
|
||||||
FontawesomeMarker,
|
FontawesomeMarker,
|
||||||
} from "$lib/util/maplibre_util";
|
} from "$lib/util/maplibre_util";
|
||||||
|
import { polylineToGeoJSON } from "$lib/util/polyline_util";
|
||||||
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
||||||
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
||||||
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
||||||
@@ -182,8 +183,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const r: GeoJSON[] = [];
|
const r: GeoJSON[] = [];
|
||||||
|
console.time("start decode")
|
||||||
trails.forEach((t) => {
|
trails.forEach((t) => {
|
||||||
if (t.expand?.gpx_data) {
|
if (t.polyline) {
|
||||||
|
r.push(polylineToGeoJSON(t.polyline, 5));
|
||||||
|
} else if (t.expand?.gpx_data) {
|
||||||
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
|
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
|
||||||
} else if (t.lat !== null && t.lon !== null) {
|
} else if (t.lat !== null && t.lon !== null) {
|
||||||
r.push({
|
r.push({
|
||||||
@@ -197,6 +201,8 @@
|
|||||||
} as GeoJSON);
|
} as GeoJSON);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.timeEnd("start decode")
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class Trail {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
waypoints: string[];
|
waypoints: string[];
|
||||||
summit_logs: string[];
|
summit_logs: string[];
|
||||||
|
polyline?: string;
|
||||||
expand?: {
|
expand?: {
|
||||||
tags?: Tag[]
|
tags?: Tag[]
|
||||||
category?: Category;
|
category?: Category;
|
||||||
@@ -165,6 +166,7 @@ interface TrailSearchResult {
|
|||||||
created: number;
|
created: number;
|
||||||
public: boolean;
|
public: boolean;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
|
polyline?: string;
|
||||||
shares?: string[];
|
shares?: string[];
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
gpx: string;
|
gpx: string;
|
||||||
@@ -174,6 +176,29 @@ interface TrailSearchResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultTrailSearchAttributes = [
|
||||||
|
"id",
|
||||||
|
"author",
|
||||||
|
"author_name",
|
||||||
|
"author_avatar",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"location",
|
||||||
|
"distance",
|
||||||
|
"elevation_gain",
|
||||||
|
"elevation_loss",
|
||||||
|
"duration",
|
||||||
|
"difficulty",
|
||||||
|
"category",
|
||||||
|
"completed",
|
||||||
|
"date",
|
||||||
|
"created",
|
||||||
|
"public",
|
||||||
|
"thumbnail",
|
||||||
|
"gpx",
|
||||||
|
"tags",
|
||||||
|
"_geo",]
|
||||||
|
|
||||||
|
|
||||||
export { Trail };
|
export { Trail };
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { env } from "$env/dynamic/public";
|
import { env } from "$env/dynamic/public";
|
||||||
|
import { defaultTrailSearchAttributes } 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";
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ export async function searchTrails(q: string, options: SearchParams): Promise<Hi
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
q,
|
q,
|
||||||
|
attributesToRetrieve: defaultTrailSearchAttributes,
|
||||||
options
|
options
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -161,7 +163,7 @@ function getLocationDescription(address: Address) {
|
|||||||
description = `${address.city}, ` + description
|
description = `${address.city}, ` + description
|
||||||
} else if (address.town) {
|
} else if (address.town) {
|
||||||
description = `${address.town}, ` + description
|
description = `${address.town}, ` + description
|
||||||
}else if (address.hamlet) {
|
} else if (address.hamlet) {
|
||||||
description = `${address.hamlet}, ` + description
|
description = `${address.hamlet}, ` + description
|
||||||
} else if (address.village) {
|
} else if (address.village) {
|
||||||
description = `${address.village}, ` + description
|
description = `${address.village}, ` + description
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SummitLog } from "$lib/models/summit_log";
|
import type { SummitLog } from "$lib/models/summit_log";
|
||||||
import { Trail, type TrailFilter, type TrailFilterValues, type TrailSearchResult } from "$lib/models/trail";
|
import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailFilterValues, type TrailSearchResult } from "$lib/models/trail";
|
||||||
import type { Waypoint } from "$lib/models/waypoint";
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
import { deepEqual } from "$lib/util/deep_util";
|
import { deepEqual } from "$lib/util/deep_util";
|
||||||
@@ -65,6 +65,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
|
|||||||
q: filter.q,
|
q: filter.q,
|
||||||
options: {
|
options: {
|
||||||
filter: filterText,
|
filter: filterText,
|
||||||
|
attributesToRetrieve: defaultTrailSearchAttributes,
|
||||||
sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`],
|
sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`],
|
||||||
hitsPerPage: 12,
|
hitsPerPage: 12,
|
||||||
page: page
|
page: page
|
||||||
@@ -89,7 +90,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trails_search_bounding_box(northEast: M.LngLat, southWest: M.LngLat, filter?: TrailFilter, page: number = 1, loadGPX: boolean = true) {
|
export async function trails_search_bounding_box(northEast: M.LngLat, southWest: M.LngLat, filter?: TrailFilter, page: number = 1, includePolyline: boolean = true) {
|
||||||
|
|
||||||
let filterText: string = "";
|
let filterText: string = "";
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
|
|||||||
`_geoBoundingBox([${northEast.lat}, ${northEast.lng}], [${southWest.lat}, ${southWest.lng}])`,
|
`_geoBoundingBox([${northEast.lat}, ${northEast.lng}], [${southWest.lat}, ${southWest.lng}])`,
|
||||||
filterText
|
filterText
|
||||||
],
|
],
|
||||||
|
attributesToRetrieve: [...defaultTrailSearchAttributes, ...(includePolyline ? ["polyline"] : [])],
|
||||||
hitsPerPage: 500,
|
hitsPerPage: 500,
|
||||||
page: page
|
page: page
|
||||||
}
|
}
|
||||||
@@ -118,7 +120,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
|
|||||||
return { trails: [], ...result }
|
return { trails: [], ...result }
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultTrails: Trail[] = await searchResultToTrailList(result.hits, loadGPX)
|
const resultTrails: Trail[] = await searchResultToTrailList(result.hits)
|
||||||
|
|
||||||
trails = page > 1 ? trails.concat(resultTrails) : resultTrails
|
trails = page > 1 ? trails.concat(resultTrails) : resultTrails
|
||||||
|
|
||||||
@@ -444,7 +446,7 @@ export async function fetchGPX(trail: { gpx?: string } & Record<string, any>, f:
|
|||||||
return gpxData
|
return gpxData
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchResultToTrailList(hits: Hits<TrailSearchResult>, loadGPX: boolean = false): Promise<Trail[]> {
|
async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<Trail[]> {
|
||||||
const trails: Trail[] = []
|
const trails: Trail[] = []
|
||||||
for (const h of hits) {
|
for (const h of hits) {
|
||||||
const t: Trail & RecordModel = {
|
const t: Trail & RecordModel = {
|
||||||
@@ -472,6 +474,7 @@ async function searchResultToTrailList(hits: Hits<TrailSearchResult>, loadGPX: b
|
|||||||
lon: h._geo.lng,
|
lon: h._geo.lng,
|
||||||
location: h.location,
|
location: h.location,
|
||||||
gpx: h.gpx,
|
gpx: h.gpx,
|
||||||
|
polyline: h.polyline,
|
||||||
thumbnail: 0,
|
thumbnail: 0,
|
||||||
expand: {
|
expand: {
|
||||||
author: {
|
author: {
|
||||||
@@ -489,10 +492,6 @@ async function searchResultToTrailList(hits: Hits<TrailSearchResult>, loadGPX: b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadGPX) {
|
|
||||||
const gpxData: string = await fetchGPX(t);
|
|
||||||
t.expand!.gpx_data = gpxData;
|
|
||||||
}
|
|
||||||
|
|
||||||
trails.push(t)
|
trails.push(t)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import type { GeoJSON } from "geojson";
|
||||||
|
import { bbox } from "./geojson_util";
|
||||||
|
|
||||||
|
|
||||||
function py2_round(value: number) {
|
function py2_round(value: number) {
|
||||||
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
|
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
|
||||||
}
|
}
|
||||||
@@ -77,4 +81,34 @@ export function encodePolyline(coordinates: number[][], precision: number = 6) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
function flipped(coords: number[][]) {
|
||||||
|
var flipped = [];
|
||||||
|
for (var i = 0; i < coords.length; i++) {
|
||||||
|
var coord = coords[i].slice();
|
||||||
|
flipped.push([coord[1], coord[0]]);
|
||||||
|
}
|
||||||
|
return flipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function polylineToGeoJSON(str: string, precision: number = 6) {
|
||||||
|
var coords = decodePolyline(str, precision);
|
||||||
|
const geojson = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
properties: {},
|
||||||
|
type: "Feature",
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: flipped(coords),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
} as GeoJSON;
|
||||||
|
geojson.bbox = bbox(geojson)
|
||||||
|
|
||||||
|
return geojson
|
||||||
};
|
};
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import CategoryCard from "$lib/components/category_card.svelte";
|
import CategoryCard from "$lib/components/category_card.svelte";
|
||||||
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 { defaultTrailSearchAttributes } from "$lib/models/trail.js";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import {
|
import {
|
||||||
searchMulti,
|
searchMulti,
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
indexUid: "trails",
|
indexUid: "trails",
|
||||||
|
attributesToRetrieve: defaultTrailSearchAttributes,
|
||||||
q: q,
|
q: q,
|
||||||
limit: 3,
|
limit: 3,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||||
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
||||||
import type { Settings } from "$lib/models/settings";
|
import type { Settings } from "$lib/models/settings";
|
||||||
import type {
|
import {
|
||||||
Trail,
|
defaultTrailSearchAttributes,
|
||||||
TrailBoundingBox,
|
type Trail,
|
||||||
TrailFilter,
|
type TrailBoundingBox,
|
||||||
|
type TrailFilter,
|
||||||
} from "$lib/models/trail";
|
} from "$lib/models/trail";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import {
|
import {
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
indexUid: "trails",
|
indexUid: "trails",
|
||||||
|
attributesToRetrieve: defaultTrailSearchAttributes,
|
||||||
q: q,
|
q: q,
|
||||||
limit: 3,
|
limit: 3,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user