diff --git a/db/migrations/1744651602_add_polyline.go b/db/migrations/1744651602_add_polyline.go new file mode 100644 index 00000000..69947a62 --- /dev/null +++ b/db/migrations/1744651602_add_polyline.go @@ -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 + }) +} diff --git a/db/util/meilisearch.go b/db/util/meilisearch.go index 658840dc..e129a933 100644 --- a/db/util/meilisearch.go +++ b/db/util/meilisearch.go @@ -1,15 +1,19 @@ package util import ( + "bytes" "errors" "fmt" + "io" "log" "github.com/meilisearch/meilisearch-go" "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") thumbnail := "" if len(photos) > 0 { @@ -27,6 +31,11 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares tags[i] = v.GetString("name") } + polyline, err := getPolyline(app, r) + if err != nil { + return nil + } + document := map[string]interface{}{ "id": r.Id, "author": r.GetString("author"), @@ -48,6 +57,7 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares "thumbnail": thumbnail, "gpx": r.GetString("gpx"), "tags": tags, + "polyline": polyline, "_geo": map[string]float64{ "lat": r.GetFloat("lat"), "lng": r.GetFloat("lon"), @@ -61,6 +71,44 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares 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{} { document := map[string]interface{}{ "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) } - 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 { 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) } - documents := documentFromTrailRecord(r, author, false) + documents := documentFromTrailRecord(app, r, author, false) if _, err := client.Index("trails").UpdateDocuments(documents); err != nil { return err diff --git a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte index 763a6407..977a852e 100644 --- a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte +++ b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte @@ -12,6 +12,7 @@ createPopupFromTrail, FontawesomeMarker, } from "$lib/util/maplibre_util"; + import { polylineToGeoJSON } from "$lib/util/polyline_util"; import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control"; import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control"; import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule"; @@ -182,8 +183,11 @@ } const r: GeoJSON[] = []; + console.time("start decode") 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); } else if (t.lat !== null && t.lon !== null) { r.push({ @@ -197,6 +201,8 @@ } as GeoJSON); } }); + console.timeEnd("start decode") + return r; } diff --git a/web/src/lib/models/trail.ts b/web/src/lib/models/trail.ts index 1aaa6ac8..458eda09 100644 --- a/web/src/lib/models/trail.ts +++ b/web/src/lib/models/trail.ts @@ -27,6 +27,7 @@ class Trail { tags: string[]; waypoints: string[]; summit_logs: string[]; + polyline?: string; expand?: { tags?: Tag[] category?: Category; @@ -165,6 +166,7 @@ interface TrailSearchResult { created: number; public: boolean; thumbnail: string; + polyline?: string; shares?: string[]; tags?: 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 }; diff --git a/web/src/lib/stores/search_store.ts b/web/src/lib/stores/search_store.ts index fc237019..307e8302 100644 --- a/web/src/lib/stores/search_store.ts +++ b/web/src/lib/stores/search_store.ts @@ -1,4 +1,5 @@ import { env } from "$env/dynamic/public"; +import { defaultTrailSearchAttributes } from "$lib/models/trail"; import { APIError } from "$lib/util/api_util"; import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch"; @@ -97,6 +98,7 @@ export async function searchTrails(q: string, options: SearchParams): Promise 1 ? trails.concat(resultTrails) : resultTrails @@ -444,7 +446,7 @@ export async function fetchGPX(trail: { gpx?: string } & Record, f: return gpxData } -async function searchResultToTrailList(hits: Hits, loadGPX: boolean = false): Promise { +async function searchResultToTrailList(hits: Hits): Promise { const trails: Trail[] = [] for (const h of hits) { const t: Trail & RecordModel = { @@ -472,6 +474,7 @@ async function searchResultToTrailList(hits: Hits, loadGPX: b lon: h._geo.lng, location: h.location, gpx: h.gpx, + polyline: h.polyline, thumbnail: 0, expand: { author: { @@ -489,10 +492,6 @@ async function searchResultToTrailList(hits: Hits, loadGPX: b } } - if (loadGPX) { - const gpxData: string = await fetchGPX(t); - t.expand!.gpx_data = gpxData; - } trails.push(t) } diff --git a/web/src/lib/util/polyline_util.ts b/web/src/lib/util/polyline_util.ts index c16b34f4..28ec3f6d 100644 --- a/web/src/lib/util/polyline_util.ts +++ b/web/src/lib/util/polyline_util.ts @@ -1,3 +1,7 @@ +import type { GeoJSON } from "geojson"; +import { bbox } from "./geojson_util"; + + function py2_round(value: number) { 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; +}; + +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 }; \ No newline at end of file diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 53bfa5cc..9720954a 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -8,6 +8,7 @@ import CategoryCard from "$lib/components/category_card.svelte"; import Scene from "$lib/components/scene.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 { searchMulti, @@ -30,6 +31,7 @@ queries: [ { indexUid: "trails", + attributesToRetrieve: defaultTrailSearchAttributes, q: q, limit: 3, }, diff --git a/web/src/routes/map/+page.svelte b/web/src/routes/map/+page.svelte index 28bb4eeb..b8b176a9 100644 --- a/web/src/routes/map/+page.svelte +++ b/web/src/routes/map/+page.svelte @@ -11,10 +11,11 @@ import TrailCard from "$lib/components/trail/trail_card.svelte"; import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte"; import type { Settings } from "$lib/models/settings"; - import type { - Trail, - TrailBoundingBox, - TrailFilter, + import { + defaultTrailSearchAttributes, + type Trail, + type TrailBoundingBox, + type TrailFilter, } from "$lib/models/trail"; import { categories } from "$lib/stores/category_store"; import { @@ -67,6 +68,7 @@ queries: [ { indexUid: "trails", + attributesToRetrieve: defaultTrailSearchAttributes, q: q, limit: 3, },