Places: Improve parsing and querying of GPS boundaries #1187 #3657

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-09-20 03:18:30 +02:00
parent b65362b35a
commit 85506f9373
9 changed files with 79 additions and 53 deletions

View file

@ -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;
}
}

View file

@ -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)"`

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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,

View file

@ -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"

View file

@ -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
}

View file

@ -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) {