From 85506f9373649a6e00bca4079a5cbe9a1ff42801 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Wed, 20 Sep 2023 03:18:30 +0200 Subject: [PATCH] Places: Improve parsing and querying of GPS boundaries #1187 #3657 Signed-off-by: Michael Mayer --- frontend/src/page/places.vue | 21 +++++++++++--------- internal/form/search_photos.go | 4 ++-- internal/form/search_photos_geo.go | 4 ++-- internal/search/photos.go | 16 ++++++++------- internal/search/photos_geo.go | 16 ++++++++------- internal/search/photos_geo_test.go | 8 ++++---- internal/search/photos_test.go | 8 ++++---- pkg/clean/gps.go | 31 ++++++++++++++++++++++++------ pkg/clean/gps_test.go | 24 +++++++++++------------ 9 files changed, 79 insertions(+), 53 deletions(-) diff --git a/frontend/src/page/places.vue b/frontend/src/page/places.vue index fcf887713..12638553d 100644 --- a/frontend/src/page/places.vue +++ b/frontend/src/page/places.vue @@ -134,13 +134,16 @@ export default { } const settings = this.$config.settings(); + const features = settings.features; - if (settings && settings.features.private) { - filter.public = "true"; - } + if (settings) { + if (features.private) { + filter.public = "true"; + } - if (settings && settings.features.review && (!this.staticFilter || !("quality" in this.staticFilter))) { - filter.quality = "3"; + if (features.review && (!this.staticFilter || !("quality" in this.staticFilter))) { + filter.quality = "3"; + } } switch (style) { @@ -351,16 +354,16 @@ export default { let latNorth, lngEast, latSouth, lngWest; for (const feature of clusterFeatures) { const [lng, lat] = feature.geometry.coordinates; - if (latNorth === undefined || lat < latNorth) { + if (latNorth === undefined || lat > latNorth) { latNorth = lat; } - if (lngEast === undefined || lng < lngEast) { + if (lngEast === undefined || lng > lngEast) { lngEast = lng; } - if (latSouth === undefined || lat > latSouth) { + if (latSouth === undefined || lat < latSouth) { latSouth = lat; } - if (lngWest === undefined || lng > lngWest) { + if (lngWest === undefined || lng < lngWest) { lngWest = lng; } } diff --git a/internal/form/search_photos.go b/internal/form/search_photos.go index 9b0f6f3b9..a8b9dedb7 100644 --- a/internal/form/search_photos.go +++ b/internal/form/search_photos.go @@ -46,9 +46,9 @@ type SearchPhotos struct { Lat float32 `form:"lat" notes:"GPS Position (Latitude)"` Lng float32 `form:"lng" notes:"GPS Position (Longitude)"` Dist uint `form:"dist" example:"dist:5" notes:"Distance to GPS Position (km)"` - LatLng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"` + Latlng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"` S2 string `form:"s2" notes:"S2 Position (Cell ID)"` - OLC string `form:"olc" notes:"Open Location Code (OLC)"` + Olc string `form:"olc" notes:"Open Location Code (OLC)"` Fmin float32 `form:"fmin" notes:"F-number (min)"` Fmax float32 `form:"fmax" notes:"F-number (max)"` Chroma int16 `form:"chroma" example:"chroma:70" notes:"Chroma (0-100)"` diff --git a/internal/form/search_photos_geo.go b/internal/form/search_photos_geo.go index 3c6a77d04..40f202119 100644 --- a/internal/form/search_photos_geo.go +++ b/internal/form/search_photos_geo.go @@ -45,9 +45,9 @@ type SearchPhotosGeo struct { Lat float32 `form:"lat" notes:"GPS Position (Latitude)"` Lng float32 `form:"lng" notes:"GPS Position (Longitude)"` Dist uint `form:"dist" example:"dist:5" notes:"Distance to GPS Position (km)"` - LatLng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"` + Latlng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"` S2 string `form:"s2" notes:"S2 Position (Cell ID)"` - OLC string `form:"olc" notes:"Open Location Code (OLC)"` + Olc string `form:"olc" notes:"Open Location Code (OLC)"` Person string `form:"person"` // Alias for Subject Subjects string `form:"subjects"` // Text People string `form:"people"` // Alias for Subjects diff --git a/internal/search/photos.go b/internal/search/photos.go index 9a212c14a..69f2a9218 100644 --- a/internal/search/photos.go +++ b/internal/search/photos.go @@ -628,19 +628,21 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) s = s.Where("photos.photo_f_number <= ?", f.Fmax) } - // Filter by location. + // Filter by location code. if f.S2 != "" { // S2 Cell ID. s2Min, s2Max := s2.PrefixedRange(f.S2, S2Levels) s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max) - } else if f.OLC != "" { + } else if f.Olc != "" { // Open Location Code (OLC). - s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.OLC), S2Levels) + s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.Olc), S2Levels) s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max) - } else if latNorth, lngEast, latSouth, lngWest, parseErr := clean.GPSBounds(f.LatLng); parseErr == nil { - // GPS Bounds (Lat N, Lng E, Lat S, Lng W). - s = s.Where("photos.photo_lat BETWEEN ? AND ?", latNorth, latSouth) - s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngEast, lngWest) + } + + // Filter by GPS Bounds (Lat N, Lng E, Lat S, Lng W). + if latNorth, lngEast, latSouth, lngWest, parseErr := clean.GPSBounds(f.Latlng); parseErr == nil { + s = s.Where("photos.photo_lat BETWEEN ? AND ?", latSouth, latNorth) + s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngWest, lngEast) } // Filter by approx distance to coordinates. diff --git a/internal/search/photos_geo.go b/internal/search/photos_geo.go index 8b6506c46..a0c6e70e1 100644 --- a/internal/search/photos_geo.go +++ b/internal/search/photos_geo.go @@ -509,19 +509,21 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma) } - // Filter by location. + // Filter by location code. if f.S2 != "" { // S2 Cell ID. s2Min, s2Max := s2.PrefixedRange(f.S2, S2Levels) s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max) - } else if f.OLC != "" { + } else if f.Olc != "" { // Open Location Code (OLC). - s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.OLC), S2Levels) + s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.Olc), S2Levels) s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max) - } else if latNorth, lngEast, latSouth, lngWest, parseErr := clean.GPSBounds(f.LatLng); parseErr == nil { - // GPS Bounds (Lat N, Lng E, Lat S, Lng W). - s = s.Where("photos.photo_lat BETWEEN ? AND ?", latNorth, latSouth) - s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngEast, lngWest) + } + + // Filter by GPS Bounds (Lat N, Lng E, Lat S, Lng W). + if latNorth, lngEast, latSouth, lngWest, parseErr := clean.GPSBounds(f.Latlng); parseErr == nil { + s = s.Where("photos.photo_lat BETWEEN ? AND ?", latSouth, latNorth) + s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngWest, lngEast) } // Filter by approx distance to coordinates. diff --git a/internal/search/photos_geo_test.go b/internal/search/photos_geo_test.go index 30b441e83..3e82c4968 100644 --- a/internal/search/photos_geo_test.go +++ b/internal/search/photos_geo_test.go @@ -130,7 +130,7 @@ func TestGeo(t *testing.T) { Lat: 1.234, Lng: 4.321, S2: "", - OLC: "", + Olc: "", Dist: 0, Quality: 0, Review: true, @@ -158,7 +158,7 @@ func TestGeo(t *testing.T) { Lat: 0, Lng: 0, S2: "", - OLC: "", + Olc: "", Dist: 0, Quality: 3, Review: false, @@ -181,7 +181,7 @@ func TestGeo(t *testing.T) { Lat: 0, Lng: 0, S2: "85", - OLC: "", + Olc: "", Dist: 0, Quality: 0, Review: false, @@ -204,7 +204,7 @@ func TestGeo(t *testing.T) { Lat: 0, Lng: 0, S2: "", - OLC: "9", + Olc: "9", Dist: 0, Quality: 0, Review: false, diff --git a/internal/search/photos_test.go b/internal/search/photos_test.go index 39294f108..e5ace29cb 100644 --- a/internal/search/photos_test.go +++ b/internal/search/photos_test.go @@ -667,9 +667,9 @@ func TestPhotos(t *testing.T) { assert.LessOrEqual(t, 2, len(photos)) }) - t.Run("LatLng:33.453431,-180.0,49.519234,180.0", func(t *testing.T) { + t.Run("latlng:33.453431,-180.0,49.519234,180.0", func(t *testing.T) { var f form.SearchPhotos - f.Query = "LatLng:33.453431,-180.0,49.519234,180.0" + f.Query = "latlng:33.453431,-180.0,49.519234,180.0" f.Count = 10 f.Offset = 0 f.Order = "imported" @@ -688,9 +688,9 @@ func TestPhotos(t *testing.T) { assert.LessOrEqual(t, 2, len(photos)) }) - t.Run("LatLng:0.00,-30.123.0,49.519234,9.1001234", func(t *testing.T) { + t.Run("latlng:0.00,-30.123.0,49.519234,9.1001234", func(t *testing.T) { var f form.SearchPhotos - f.Query = "LatLng:0.00,-30.123.0,49.519234,9.1001234" + f.Query = "latlng:0.00,-30.123.0,49.519234,9.1001234" f.Count = 10 f.Offset = 0 f.Order = "imported" diff --git a/pkg/clean/gps.go b/pkg/clean/gps.go index 2bc8299d8..0941ea4cd 100644 --- a/pkg/clean/gps.go +++ b/pkg/clean/gps.go @@ -2,17 +2,33 @@ package clean import ( "fmt" + "math" "strings" "github.com/photoprism/photoprism/pkg/txt" ) +// gpsCeil converts a GPS coordinate to a rounded float32 for use in queries. +func gpsCeil(f float64) float32 { + return float32((math.Ceil(f*10000) / 10000) + 0.0001) +} + +// gpsFloor converts a GPS coordinate to a rounded float32 for use in queries. +func gpsFloor(f float64) float32 { + return float32((math.Floor(f*10000) / 10000) - 0.0001) +} + // GPSBounds parses the GPS bounds (Lat N, Lng E, Lat S, Lng W) and returns the coordinates if any. -func GPSBounds(bounds string) (latNorth, lngEast, latSouth, lngWest float32, err error) { +func GPSBounds(bounds string) (latN, lngE, latS, lngW float32, err error) { + // Bounds string not long enough? if len(bounds) < 7 { return 0, 0, 0, 0, fmt.Errorf("no coordinates found") } + // Trim whitespace and invalid characters. + bounds = strings.Trim(bounds, " |\\<>\n\r\t\"'#$%!^*()[]{}") + + // Split string into values. values := strings.SplitN(bounds, ",", 5) found := len(values) @@ -22,7 +38,7 @@ func GPSBounds(bounds string) (latNorth, lngEast, latSouth, lngWest float32, err } // Parse floating point coordinates. - latNorth, lngEast, latSouth, lngWest = txt.Float32(values[0]), txt.Float32(values[1]), txt.Float32(values[2]), txt.Float32(values[3]) + latNorth, lngEast, latSouth, lngWest := txt.Float(values[0]), txt.Float(values[1]), txt.Float(values[2]), txt.Float(values[3]) // Latitudes (from +90 to -90 degrees). if latNorth > 90 { @@ -37,11 +53,12 @@ func GPSBounds(bounds string) (latNorth, lngEast, latSouth, lngWest float32, err latSouth = -90 } - if latNorth > latSouth { + // latSouth must be smaller. + if latSouth > latNorth { latNorth, latSouth = latSouth, latNorth } - // Longitudes (from -180 to 180 degrees). + // Longitudes (from -180 to +180 degrees). if lngEast > 180 { lngEast = 180 } else if lngEast < -180 { @@ -54,9 +71,11 @@ func GPSBounds(bounds string) (latNorth, lngEast, latSouth, lngWest float32, err lngWest = -180 } - if lngEast > lngWest { + // lngWest must be smaller. + if lngWest > lngEast { lngEast, lngWest = lngWest, lngEast } - return latNorth, lngEast, latSouth, lngWest, nil + // Return rounded coordinates. + return gpsCeil(latNorth), gpsCeil(lngEast), gpsFloor(latSouth), gpsFloor(lngWest), nil } diff --git a/pkg/clean/gps_test.go b/pkg/clean/gps_test.go index 320545ef5..c271b3b0f 100644 --- a/pkg/clean/gps_test.go +++ b/pkg/clean/gps_test.go @@ -9,26 +9,26 @@ import ( func TestGPSBounds(t *testing.T) { t.Run("Valid", func(t *testing.T) { latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.87760543823242,-87.62521362304688,41.89404296875,-87.6215591430664") - assert.Equal(t, float32(41.87760543823242), latNorth) - assert.Equal(t, float32(-87.62521362304688), lngEast) - assert.Equal(t, float32(41.89404296875), latSouth) - assert.Equal(t, float32(-87.6215591430664), lngWest) + assert.Equal(t, float32(41.8942), latNorth) + assert.Equal(t, float32(41.8775), latSouth) + assert.Equal(t, float32(-87.6254), lngWest) + assert.Equal(t, float32(-87.6214), lngEast) assert.NoError(t, err) }) t.Run("FlippedLat", func(t *testing.T) { latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.89404296875,-87.62521362304688,41.87760543823242,-87.6215591430664") - assert.Equal(t, float32(41.87760543823242), latNorth) - assert.Equal(t, float32(-87.62521362304688), lngEast) - assert.Equal(t, float32(41.89404296875), latSouth) - assert.Equal(t, float32(-87.6215591430664), lngWest) + assert.Equal(t, float32(41.8942), latNorth) + assert.Equal(t, float32(41.8775), latSouth) + assert.Equal(t, float32(-87.6254), lngWest) + assert.Equal(t, float32(-87.6214), lngEast) assert.NoError(t, err) }) t.Run("FlippedLng", func(t *testing.T) { latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.87760543823242,-87.6215591430664,41.89404296875,-87.62521362304688") - assert.Equal(t, float32(41.87760543823242), latNorth) - assert.Equal(t, float32(-87.62521362304688), lngEast) - assert.Equal(t, float32(41.89404296875), latSouth) - assert.Equal(t, float32(-87.6215591430664), lngWest) + assert.Equal(t, float32(41.8942), latNorth) + assert.Equal(t, float32(41.8775), latSouth) + assert.Equal(t, float32(-87.6254), lngWest) + assert.Equal(t, float32(-87.6214), lngEast) assert.NoError(t, err) }) t.Run("Empty", func(t *testing.T) {