Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
d5a1526291
commit
4d1003846c
11 changed files with 138 additions and 56 deletions
|
@ -46,8 +46,8 @@ type SearchPhotos struct {
|
|||
Near string `form:"near" example:"near:pqbcf5j446s0futy" notes:"Finds nearby pictures (UID)"`
|
||||
S2 string `form:"s2" example:"s2:4799e370ca54c8b9" notes:"S2 Position (Cell ID)"`
|
||||
Olc string `form:"olc" example:"olc:8FWCHX7W+" notes:"OLC Position (Open Location Code)"`
|
||||
Lat float32 `form:"lat" example:"lat:41.894043" notes:"GPS Position (Latitude)"`
|
||||
Lng float32 `form:"lng" example:"lng:-87.62448" notes:"GPS Position (Longitude)"`
|
||||
Lat float64 `form:"lat" example:"lat:41.894043" notes:"GPS Position (Latitude)"`
|
||||
Lng float64 `form:"lng" example:"lng:-87.62448" notes:"GPS Position (Longitude)"`
|
||||
Dist uint `form:"dist" example:"dist:50" notes:"Distance to Position (km)"`
|
||||
Latlng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"`
|
||||
Fmin float32 `form:"fmin" notes:"F-number (min)"`
|
||||
|
|
|
@ -44,8 +44,8 @@ type SearchPhotosGeo struct {
|
|||
Near string `form:"near" example:"near:pqbcf5j446s0futy" notes:"Finds nearby pictures (UID)"`
|
||||
S2 string `form:"s2" example:"s2:4799e370ca54c8b9" notes:"S2 Position (Cell ID)"`
|
||||
Olc string `form:"olc" example:"olc:8FWCHX7W+" notes:"OLC Position (Open Location Code)"`
|
||||
Lat float32 `form:"lat" example:"lat:41.894043" notes:"GPS Position (Latitude)"`
|
||||
Lng float32 `form:"lng" example:"lng:-87.62448" notes:"GPS Position (Longitude)"`
|
||||
Lat float64 `form:"lat" example:"lat:41.894043" notes:"GPS Position (Latitude)"`
|
||||
Lng float64 `form:"lng" example:"lng:-87.62448" notes:"GPS Position (Longitude)"`
|
||||
Dist uint `form:"dist" example:"dist:50" notes:"Distance to Position (km)"`
|
||||
Latlng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"`
|
||||
Person string `form:"person"` // Alias for Subject
|
||||
|
|
|
@ -108,7 +108,7 @@ func TestSearchPhotosGeo(t *testing.T) {
|
|||
assert.Equal(t, "fooBar baz", form.Query)
|
||||
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
|
||||
assert.Equal(t, uint(0x61a8), form.Dist)
|
||||
assert.Equal(t, float32(33.45343), form.Lat)
|
||||
assert.Equal(t, 33.45343166666667, form.Lat)
|
||||
})
|
||||
t.Run("valid query path empty folder not empty", func(t *testing.T) {
|
||||
form := &SearchPhotosGeo{Query: "q:\"fooBar baz\" before:2019-01-15 dist:25000 lat:33.45343166666667 folder:test"}
|
||||
|
@ -126,7 +126,7 @@ func TestSearchPhotosGeo(t *testing.T) {
|
|||
assert.Equal(t, "", form.Folder)
|
||||
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
|
||||
assert.Equal(t, uint(0x61a8), form.Dist)
|
||||
assert.Equal(t, float32(33.45343), form.Lat)
|
||||
assert.Equal(t, 33.45343166666667, form.Lat)
|
||||
})
|
||||
t.Run("valid query with filter", func(t *testing.T) {
|
||||
form := &SearchPhotosGeo{Query: "keywords:cat title:\"fooBar baz\"", Filter: "keywords:dog"}
|
||||
|
|
|
@ -122,7 +122,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
|
||||
assert.Equal(t, "false", form.Favorite)
|
||||
assert.Equal(t, uint(0x61a8), form.Dist)
|
||||
assert.Equal(t, float32(33.45343), form.Lat)
|
||||
assert.Equal(t, 33.45343166666667, form.Lat)
|
||||
})
|
||||
t.Run("valid query 2", func(t *testing.T) {
|
||||
form := &SearchPhotos{Query: "chroma:200 title:\"te:st\" after:2018-01-15 favorite:true lng:33.45343166666667"}
|
||||
|
@ -138,7 +138,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
assert.Equal(t, int16(200), form.Chroma)
|
||||
assert.Equal(t, "te:st", form.Title)
|
||||
assert.Equal(t, time.Date(2018, 01, 15, 0, 0, 0, 0, time.UTC), form.After)
|
||||
assert.Equal(t, float32(33.45343), form.Lng)
|
||||
assert.Equal(t, 33.45343166666667, form.Lng)
|
||||
})
|
||||
t.Run("valid query with filter", func(t *testing.T) {
|
||||
form := &SearchPhotos{Query: "label:cat title:\"fooBar baz\"", Filter: "label:dog"}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/geo"
|
||||
"github.com/photoprism/photoprism/pkg/pluscode"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
|
@ -70,18 +71,13 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
|
|||
|
||||
// Set the S2 Cell ID to search for.
|
||||
f.S2 = photo.CellID
|
||||
|
||||
// Set the search distance if unspecified.
|
||||
if f.Dist <= 0 {
|
||||
f.Dist = 2
|
||||
}
|
||||
}
|
||||
|
||||
// Set default search distance.
|
||||
if f.Dist <= 0 {
|
||||
f.Dist = 50
|
||||
} else if f.Dist > 5000 {
|
||||
f.Dist = 5000
|
||||
f.Dist = geo.DefaultDist
|
||||
} else if f.Dist > geo.DistLimit {
|
||||
f.Dist = geo.DistLimit
|
||||
}
|
||||
|
||||
// Specify table names and joins.
|
||||
|
@ -118,9 +114,11 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
|
|||
s = s.Where("files.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 1 AND pa.album_uid = ?)", a.AlbumUID)
|
||||
}
|
||||
|
||||
// Limit search distance.
|
||||
if f.Dist <= 0 || f.Dist > 50 {
|
||||
f.Dist = 50
|
||||
// Enforce search distance range (km).
|
||||
if f.Dist <= 0 {
|
||||
f.Dist = geo.DefaultDist
|
||||
} else if f.Dist > geo.ScopeDistLimit {
|
||||
f.Dist = geo.ScopeDistLimit
|
||||
}
|
||||
} else {
|
||||
f.Scope = ""
|
||||
|
@ -668,24 +666,19 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
|
|||
}
|
||||
|
||||
// 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)
|
||||
if latN, lngE, latS, lngW, boundsErr := clean.GPSBounds(f.Latlng); boundsErr == nil {
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latS, latN)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngW, lngE)
|
||||
}
|
||||
|
||||
// Filter by approx distance to coordinates.
|
||||
if f.Lat != 0 && f.Lat >= -90 && f.Lat <= 90 {
|
||||
// Latitude (from +90 to -90 degrees).
|
||||
latNorth := f.Lat + Radius*float32(f.Dist)
|
||||
latSouth := f.Lat - Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latSouth, latNorth)
|
||||
// Filter by GPS Latitude (from +90 to -90 degrees).
|
||||
if latN, latS, latErr := clean.GPSLatRange(f.Lat, f.Dist); latErr == nil {
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latS, latN)
|
||||
}
|
||||
|
||||
if f.Lng != 0 && f.Lng >= -180 && f.Lng <= 180 {
|
||||
// Longitude (from -180 to +180 degrees).
|
||||
lngWest := f.Lng - Radius*float32(f.Dist)
|
||||
lngEast := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngWest, lngEast)
|
||||
// Filter by GPS Longitude (from -180 to +180 degrees).
|
||||
if lngE, lngW, lngErr := clean.GPSLngRange(f.Lng, f.Dist); lngErr == nil {
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngW, lngE)
|
||||
}
|
||||
|
||||
// Find photos taken before date.
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/geo"
|
||||
"github.com/photoprism/photoprism/pkg/pluscode"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
|
@ -54,15 +55,15 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
|
|||
|
||||
// Set the search distance if unspecified.
|
||||
if f.Dist <= 0 {
|
||||
f.Dist = 2
|
||||
f.Dist = geo.DefaultDist
|
||||
}
|
||||
}
|
||||
|
||||
// Set default search distance.
|
||||
if f.Dist <= 0 {
|
||||
f.Dist = 50
|
||||
} else if f.Dist > 5000 {
|
||||
f.Dist = 5000
|
||||
f.Dist = geo.DefaultDist
|
||||
} else if f.Dist > geo.DistLimit {
|
||||
f.Dist = geo.DistLimit
|
||||
}
|
||||
|
||||
// Specify table names and joins.
|
||||
|
@ -99,9 +100,11 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
|
|||
s = s.Where("files.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 1 AND pa.album_uid = ?)", a.AlbumUID)
|
||||
}
|
||||
|
||||
// Limit search distance.
|
||||
if f.Dist <= 0 || f.Dist > 50 {
|
||||
f.Dist = 50
|
||||
// Enforce search distance range (km).
|
||||
if f.Dist <= 0 {
|
||||
f.Dist = geo.DefaultDist
|
||||
} else if f.Dist > geo.ScopeDistLimit {
|
||||
f.Dist = geo.ScopeDistLimit
|
||||
}
|
||||
} else {
|
||||
f.Scope = ""
|
||||
|
@ -533,24 +536,19 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
|
|||
}
|
||||
|
||||
// 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)
|
||||
if latN, lngE, latS, lngW, boundsErr := clean.GPSBounds(f.Latlng); boundsErr == nil {
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latS, latN)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngW, lngE)
|
||||
}
|
||||
|
||||
// Filter by approx distance to coordinates.
|
||||
if f.Lat != 0 && f.Lat >= -90 && f.Lat <= 90 {
|
||||
// Latitude (from +90 to -90 degrees).
|
||||
latNorth := f.Lat + Radius*float32(f.Dist)
|
||||
latSouth := f.Lat - Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latSouth, latNorth)
|
||||
// Filter by GPS Latitude (from +90 to -90 degrees).
|
||||
if latN, latS, latErr := clean.GPSLatRange(f.Lat, f.Dist); latErr == nil {
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latS, latN)
|
||||
}
|
||||
|
||||
if f.Lng != 0 && f.Lng >= -180 && f.Lng <= 180 {
|
||||
// Longitude (from -180 to +180 degrees).
|
||||
lngWest := f.Lng - Radius*float32(f.Dist)
|
||||
lngEast := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngWest, lngEast)
|
||||
// Filter by GPS Longitude (from -180 to +180 degrees).
|
||||
if lngE, lngW, lngErr := clean.GPSLngRange(f.Lng, f.Dist); lngErr == nil {
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngW, lngE)
|
||||
}
|
||||
|
||||
// Find photos taken before date.
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/geo"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
|
@ -79,3 +80,47 @@ func GPSBounds(bounds string) (latN, lngE, latS, lngW float32, err error) {
|
|||
// Return rounded coordinates.
|
||||
return gpsCeil(latNorth), gpsCeil(lngEast), gpsFloor(latSouth), gpsFloor(lngWest), nil
|
||||
}
|
||||
|
||||
// GPSLatRange returns a range based on the specified latitude and distance in km, or an error otherwise.
|
||||
func GPSLatRange(lat float64, km uint) (latN, latS float32, err error) {
|
||||
// Latitude (from +90 to -90 degrees).
|
||||
if lat == 0 || lat < -90 || lat > 90 {
|
||||
return 0, 0, fmt.Errorf("invalid latitude")
|
||||
}
|
||||
|
||||
// Approximate range.
|
||||
latN = gpsCeil(lat + geo.KmToDeg(km))
|
||||
latS = gpsFloor(lat - geo.KmToDeg(km))
|
||||
|
||||
if latN > 90 {
|
||||
latN = 90
|
||||
}
|
||||
|
||||
if latS < -90 {
|
||||
latS = -90
|
||||
}
|
||||
|
||||
return latN, latS, nil
|
||||
}
|
||||
|
||||
// GPSLngRange returns a range based on the specified longitude and distance in km, or an error otherwise.
|
||||
func GPSLngRange(lng float64, km uint) (lngE, lngW float32, err error) {
|
||||
// Longitude (from -180 to +180 degrees).
|
||||
if lng == 0 || lng < -180 || lng > 180 {
|
||||
return 0, 0, fmt.Errorf("invalid longitude")
|
||||
}
|
||||
|
||||
// Approximate range.
|
||||
lngE = gpsCeil(lng + geo.KmToDeg(km))
|
||||
lngW = gpsFloor(lng - geo.KmToDeg(km))
|
||||
|
||||
if lngE > 180 {
|
||||
lngE = 180
|
||||
}
|
||||
|
||||
if lngW < -180 {
|
||||
lngW = -180
|
||||
}
|
||||
|
||||
return lngE, lngW, nil
|
||||
}
|
||||
|
|
|
@ -64,3 +64,33 @@ func TestGPSBounds(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGPSLatRange(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
latNorth, latSouth, err := GPSLatRange(41.87760543823242, 2)
|
||||
assert.Equal(t, float32(41.8958), latNorth)
|
||||
assert.Equal(t, float32(41.8594), latSouth)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
latNorth, latSouth, err := GPSLatRange(0, 2)
|
||||
assert.Equal(t, float32(0), latNorth)
|
||||
assert.Equal(t, float32(0), latSouth)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGPSLngRange(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
lngEast, lngWest, err := GPSLngRange(-87.62521362304688, 2)
|
||||
assert.Equal(t, float32(-87.6434), lngWest)
|
||||
assert.Equal(t, float32(-87.607), lngEast)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
lngEast, lngWest, err := GPSLngRange(0, 2)
|
||||
assert.Equal(t, float32(0), lngEast)
|
||||
assert.Equal(t, float32(0), lngWest)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,17 @@ import (
|
|||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
DistLimit uint = 5000
|
||||
ScopeDistLimit uint = 50
|
||||
DefaultDist uint = 2
|
||||
)
|
||||
|
||||
// KmToDeg returns the approximate distance in decimal degrees.
|
||||
func KmToDeg(km uint) float64 {
|
||||
return 0.009009009 * float64(km)
|
||||
}
|
||||
|
||||
// DegToRad converts a value from degrees to radians.
|
||||
func DegToRad(d float64) float64 {
|
||||
return d * math.Pi / 180
|
||||
|
|
|
@ -6,7 +6,13 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDist(t *testing.T) {
|
||||
func TestKmToDeg(t *testing.T) {
|
||||
t.Run("10km", func(t *testing.T) {
|
||||
assert.Equal(t, 0.09009009, KmToDeg(10))
|
||||
})
|
||||
}
|
||||
|
||||
func TestKm(t *testing.T) {
|
||||
t.Run("BerlinShanghai", func(t *testing.T) {
|
||||
berlin := Position{Name: "Berlin", Lat: 52.5243700, Lng: 13.4105300}
|
||||
shanghai := Position{Name: "Shanghai", Lat: 31.2222200, Lng: 121.4580600}
|
||||
|
|
|
@ -106,7 +106,6 @@ func TestLevel(t *testing.T) {
|
|||
t.Run("8000", func(t *testing.T) {
|
||||
assert.Equal(t, 0, Level(8000))
|
||||
})
|
||||
|
||||
t.Run("150", func(t *testing.T) {
|
||||
assert.Equal(t, 6, Level(150))
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue