Backend: Refactor location entity and indexer

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2019-12-20 20:23:16 +01:00
parent e55bc8330c
commit f3cf300590
36 changed files with 596 additions and 567 deletions

View file

@ -382,6 +382,7 @@ axolotl:
frog:
label: frog
threshold: 0.3
categories:
- animal
@ -594,21 +595,18 @@ black grouse:
categories:
- animal
- bird
- chicken
ptarmigan:
threshold: 0.3
categories:
- animal
- bird
- chicken
ruffed grouse:
threshold: 0.3
categories:
- animal
- bird
- chicken
prairie chicken:
threshold: 0.3
@ -1318,7 +1316,7 @@ hyena:
fox:
label: fox
threshold: 0.3
threshold: 0.5
categories:
- animal
@ -1414,7 +1412,7 @@ ant:
walking stick:
threshold: 0.3
priority: -1
priority: -2
cockroach:
threshold: 0.3
@ -1858,7 +1856,7 @@ airship:
ambulance:
label: van
categories:
- street
- road
- car
- vehicle
@ -2008,7 +2006,7 @@ cab:
threshold: 0.6
categories:
- car
- street
- road
cannon:
categories:
@ -2285,7 +2283,7 @@ freight car:
catgories:
- train
- vehicle
- street
- road
french horn:
see: instrument
@ -2363,6 +2361,11 @@ hatchet:
home theater:
label: screen
threshold: 0.3
television:
label: screen
threshold: 0.3
honeycomb:
priority: -1
@ -2481,12 +2484,12 @@ minibus:
categories:
- car
- van
- street
- road
minivan:
label: van
categories:
- street
- road
- car
- bus
@ -2538,7 +2541,7 @@ mountain tent:
moving van:
label: van
categories:
- street
- road
- car
- bus
@ -2583,13 +2586,16 @@ paddlewheel:
- sea
padlock:
label: tool
threshold: 0.5
priority: -1
paintbrush:
threshold: 0.3
categories:
- drawing
- painting
- art
- exhibition
palace:
label: historic architecture
@ -2613,7 +2619,7 @@ parachute:
car:
label: car
categories:
- street
- road
- vehicle
racer:
@ -2621,12 +2627,12 @@ racer:
threshold: 0.2
categories:
- car
- street
- road
passenger car:
see: car
categories:
- street
- road
- vehicle
beach wagon:
@ -2691,7 +2697,7 @@ polaroid camera:
police van:
label: van
categories:
- street
- road
- car
- bus
@ -2740,7 +2746,7 @@ recreational vehicle:
- bus
- van
- vehicle
- street
- road
reflex camera:
categories:
@ -2802,7 +2808,7 @@ school bus:
categories:
- bus
- van
- street
- road
schooner:
see: ship
@ -2892,7 +2898,7 @@ sports car:
categories:
- car
- vehicle
- street
- road
spotlight:
categories:
@ -2921,7 +2927,7 @@ streetcar:
categories:
- train
- vehicle
- street
- road
studio couch:
label: couch
@ -2998,7 +3004,7 @@ trolleybus:
label: bus
categories:
- van
- street
- road
trombone:
see: instrument
@ -3298,6 +3304,7 @@ eggnog:
- alcohol
alp:
label: alpine hut
categories:
- landscape
- mountains

View file

@ -53,8 +53,8 @@
:label="labels.country"
flat solo hide-details
color="secondary-dark"
item-value="LocCountryCode"
item-text="LocCountry"
item-value="code"
item-text="name"
v-model="filter.country"
:items="options.countries">
</v-select>
@ -106,8 +106,8 @@
data() {
const cameras = [{ID: 0, CameraModel: this.$gettext('All Cameras')}].concat(this.$config.getValue('cameras'));
const countries = [{
LocCountryCode: '',
LocCountry: this.$gettext('All Countries')
code: '',
name: this.$gettext('All Countries')
}].concat(this.$config.getValue('countries'));
return {

View file

@ -72,7 +72,7 @@
{{ photo.getCamera() }}
<br/>
<v-icon size="14">location_on</v-icon>
<span class="p-pointer" :title="photo.getFullLocation()"
<span class="p-pointer" :title="photo.LocationID"
@click.stop="openLocation(index)">{{ photo.getLocation() }}</span>
</div>
</div>

View file

@ -20,7 +20,7 @@
</td>
<td @click="openPhoto(props.index)" class="p-pointer">{{ props.item.PhotoTitle }}</td>
<td>{{ props.item.TakenAt | luxon:format('dd/MM/yyyy hh:mm:ss') }}</td>
<td @click="openLocation(props.index)" class="p-pointer">{{ props.item.LocCountry }}</td>
<td @click="openLocation(props.index)" class="p-pointer">{{ props.item.getLocation() }}</td>
<td>{{ props.item.CameraMake }} {{ props.item.CameraModel }}</td>
<td><v-btn icon small flat :ripple="false"
class="p-photo-like"
@ -49,7 +49,7 @@
{text: '', value: '', align: 'center', sortable: false, class: 'p-col-select'},
{text: this.$gettext('Title'), value: 'PhotoTitle'},
{text: this.$gettext('Taken At'), value: 'TakenAt'},
{text: this.$gettext('Country'), value: 'LocCountry'},
{text: this.$gettext('Location'), value: 'LocRegion'},
{text: this.$gettext('Camera'), value: 'CameraModel'},
{text: this.$gettext('Favorite'), value: 'PhotoFavorite', align: 'left'},
],

View file

@ -53,8 +53,8 @@
:label="labels.country"
flat solo hide-details
color="secondary-dark"
item-value="LocCountryCode"
item-text="LocCountry"
item-value="code"
item-text="name"
v-model="filter.country"
:items="options.countries">
</v-select>
@ -156,8 +156,8 @@
data() {
const cameras = [{ID: 0, CameraModel: this.$gettext('All Cameras')}].concat(this.$config.getValue('cameras'));
const countries = [{
LocCountryCode: '',
LocCountry: this.$gettext('All Countries')
code: '',
name: this.$gettext('All Countries')
}].concat(this.$config.getValue('countries'));
return {

View file

@ -33,7 +33,7 @@ class Photo extends Abstract {
}
getGoogleMapsLink() {
return "https://www.google.com/maps/place/" + this.PhotoLat + "," + this.PhotoLong;
return "https://www.google.com/maps/place/" + this.PhotoLat + "," + this.PhotoLng;
}
getThumbnailUrl(type) {
@ -101,71 +101,17 @@ class Photo extends Abstract {
}
hasLocation() {
return this.PhotoLat !== 0 || this.PhotoLong !== 0;
return this.PhotoLat !== 0 || this.PhotoLng !== 0;
}
getLocation() {
const location = [];
if (this.LocationID) {
if (this.LocName && !this.LocCity && !this.LocCounty) {
location.push(truncate(this.LocName, 20));
} else if (this.LocCity && this.LocCity.length < 20) {
location.push(this.LocCity);
} else if (this.LocCounty && this.LocCity.length < 20) {
location.push(this.LocCounty);
}
if (this.LocState && this.LocState !== this.LocCity) {
location.push(this.LocState);
}
if (this.LocCountry) {
location.push(this.LocCountry);
}
if (this.LocRegion) {
return this.LocRegion
} else if (this.CountryName) {
location.push(this.CountryName);
} else {
location.push("Unknown");
return this.CountryName;
}
return location.join(", ");
}
getFullLocation() {
const location = [];
if (this.LocationID) {
if (this.LocName) {
location.push(this.LocName);
}
if (this.LocCity) {
location.push(this.LocCity);
}
if (this.LocPostcode) {
location.push(this.LocPostcode);
}
if (this.LocCounty) {
location.push(this.LocCounty);
}
if (this.LocState) {
location.push(this.LocState);
}
if (this.LocCountry) {
location.push(this.LocCountry);
}
} else if (this.CountryName) {
location.push(this.CountryName);
} else {
location.push("Unknown");
}
return location.join(", ");
return "Unknown"
}
getCamera() {

View file

@ -115,14 +115,10 @@
openLocation(index) {
const photo = this.results[index];
if (photo.PhotoLat && photo.PhotoLong) {
this.$router.push({name: "places", query: {lat: String(photo.PhotoLat), long: String(photo.PhotoLong)}});
} else if (photo.LocName) {
this.$router.push({name: "places", query: {q: photo.LocName}});
if (photo.PhotoLat && photo.PhotoLng) {
this.$router.push({name: "places", query: {lat: String(photo.PhotoLat), lng: String(photo.PhotoLng)}});
} else if (photo.LocCity) {
this.$router.push({name: "places", query: {q: photo.LocCity}});
} else if (photo.LocCountry) {
this.$router.push({name: "places", query: {q: photo.LocCountry}});
} else {
this.$router.push({name: "places", query: {q: photo.CountryName}});
}

View file

@ -105,14 +105,10 @@
openLocation(index) {
const photo = this.results[index];
if (photo.PhotoLat && photo.PhotoLong) {
this.$router.push({name: "places", query: {lat: String(photo.PhotoLat), long: String(photo.PhotoLong)}});
} else if (photo.LocName) {
this.$router.push({name: "places", query: {q: photo.LocName}});
if (photo.PhotoLat && photo.PhotoLng) {
this.$router.push({name: "places", query: {lat: String(photo.PhotoLat), lng: String(photo.PhotoLng)}});
} else if (photo.LocCity) {
this.$router.push({name: "places", query: {q: photo.LocCity}});
} else if (photo.LocCountry) {
this.$router.push({name: "places", query: {q: photo.LocCountry}});
} else {
this.$router.push({name: "places", query: {q: photo.CountryName}});
}

View file

@ -51,7 +51,7 @@
loading: false,
zoom: zoom,
position: null,
center: L.latLng(parseFloat(pos.lat), parseFloat(pos.long)),
center: L.latLng(parseFloat(pos.lat), parseFloat(pos.lng)),
url: 'https://{s}.tile.osm.org/{z}/{x}/{y}.png',
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
options: {
@ -65,7 +65,7 @@
query: {
q: q,
lat: pos.lat,
long: pos.long,
lng: pos.lng,
dist: dist.toString(),
zoom: zoom.toString(),
},
@ -75,8 +75,8 @@
bounds: null,
minLat: null,
maxLat: null,
minLong: null,
maxLong: null,
minLng: null,
maxLng: null,
labels: {
search: this.$gettext("Search"),
},
@ -128,12 +128,12 @@
},
onCenter(pos) {
const changed = Math.abs(this.query.lat - pos.lat) > 0.001 ||
Math.abs(this.query.long - pos.lng) > 0.001;
Math.abs(this.query.lng - pos.lng) > 0.001;
if(!changed) return;
this.query.lat = pos.lat.toString();
this.query.long = pos.lng.toString();
this.query.lng = pos.lng.toString();
this.search();
},
@ -143,21 +143,21 @@
let result = {
lat: pos.lat.toString(),
long: pos.long.toString(),
lng: pos.lng.toString(),
};
const queryLat = query['lat'];
const queryLong = query['long'];
const queryLng = query['lng'];
let storedLat = window.localStorage.getItem("lat");
let storedLong = window.localStorage.getItem("long");
let storedLng = window.localStorage.getItem("lng");
if (queryLat && queryLong) {
if (queryLat && queryLng) {
result.lat = queryLat;
result.long = queryLong;
} else if (storedLat && storedLong) {
result.lng = queryLng;
} else if (storedLat && storedLng) {
result.lat = storedLat;
result.long = storedLong;
result.lng = storedLng;
}
return result;
@ -183,23 +183,23 @@
},
formChange() {
this.query.lat = "";
this.query.long = "";
this.query.lng = "";
this.search();
},
clearQuery() {
this.position = null;
this.query.q = "";
this.query.lat = "";
this.query.long = "";
this.query.lng = "";
this.search();
},
resetBoundingBox() {
this.minLat = null;
this.maxLat = null;
this.minLong = null;
this.maxLong = null;
this.minLng = null;
this.maxLng = null;
},
fitBoundingBox(lat, long) {
fitBoundingBox(lat, lng) {
if (this.maxLat === null || lat > this.maxLat) {
this.maxLat = lat;
}
@ -208,12 +208,12 @@
this.minLat = lat;
}
if (this.maxLong === null || long > this.maxLong) {
this.maxLong = long;
if (this.maxLng === null || lng > this.maxLng) {
this.maxLng = lng;
}
if (this.minLong === null || long < this.minLong) {
this.minLong = long;
if (this.minLng === null || lng < this.minLng) {
this.minLng = lng;
}
},
updateMap(results) {
@ -239,7 +239,7 @@
iconSize: [50, 50],
className: 'leaflet-marker-photo',
}),
location: L.latLng(result.PhotoLat, result.PhotoLong),
location: L.latLng(result.PhotoLat, result.PhotoLng),
});
}
@ -257,9 +257,9 @@
updateQuery() {
const query = Object(this.query);
if (this.query.lat && this.query.long) {
if (this.query.lat && this.query.lng) {
window.localStorage.setItem("lat", this.query.lat.toString());
window.localStorage.setItem("long", this.query.long.toString());
window.localStorage.setItem("lng", this.query.lng.toString());
} else {
this.position = null;
}

View file

@ -55,7 +55,7 @@ describe("model/photo", () => {
});
it("should get photo maps link", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 36.442881666666665, PhotoLong: 28.229493333333334};
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 36.442881666666665, PhotoLng: 28.229493333333334};
const photo = new Photo(values);
const result = photo.getGoogleMapsLink();
assert.equal(result, "https://www.google.com/maps/place/36.442881666666665,28.229493333333334");
@ -121,35 +121,35 @@ describe("model/photo", () => {
});
it("should test whether photo has location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 36.442881666666665, PhotoLong: 28.229493333333334};
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 36.442881666666665, PhotoLng: 28.229493333333334};
const photo = new Photo(values);
const result = photo.hasLocation();
assert.equal(result, true);
});
it("should test whether photo has location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 0, PhotoLong: 0};
const values = {ID: 5, PhotoTitle: "Crazy Cat", PhotoLat: 0, PhotoLng: 0};
const photo = new Photo(values);
const result = photo.hasLocation();
assert.equal(result, false);
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocName: "Cape Point", LocCountry: "Africa"};
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocRegion: "Cape Point, South Africa", LocCountry: "South Africa"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Cape Point, Africa");
assert.equal(result, "Cape Point, South Africa");
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocCountry: "Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocRegion: "Cape Point, State, South Africa", LocCountry: "South Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Cape Town, State, Africa");
assert.equal(result, "Cape Point, State, South Africa");
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocType: "viewpoint", LocName: "Cape Point", LocCountry: "Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocType: "viewpoint", LocTitle: "Cape Point", LocCountry: "Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Unknown");
@ -162,27 +162,6 @@ describe("model/photo", () => {
assert.equal(result, "Africa");
});
it("should get full location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 55, LocName: "Cape Point", LocCountry: "Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State", LocPostcode: 12345};
const photo = new Photo(values);
const result = photo.getFullLocation();
assert.equal(result, "Cape Point, Cape Town, 12345, County, State, Africa");
});
it("should get full location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CountryName: "Africa"};
const photo = new Photo(values);
const result = photo.getFullLocation();
assert.equal(result, "Africa");
});
it("should get full location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocCity: "Cape Town"};
const photo = new Photo(values);
const result = photo.getFullLocation();
assert.equal(result, "Unknown");
});
it("should get camera", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CameraModel: "EOSD10", CameraMake: "Canon"};
const photo = new Photo(values);

View file

@ -572,27 +572,22 @@ func (c *Config) ClientConfig() ClientConfig {
var cameras []*entity.Camera
var albums []*entity.Album
type country struct {
LocCountry string
LocCountryCode string
}
var position struct {
PhotoUUID string `json:"uuid"`
LocationID string `json:"olc"`
PhotoLat float64 `json:"lat"`
PhotoLong float64 `json:"long"`
PhotoLng float64 `json:"lng"`
TakenAt time.Time `json:"utc"`
TakenAtLocal time.Time `json:"time"`
}
db.Table("photos").
Select("photo_uuid, photo_lat, photo_long, taken_at, taken_at_local").
Where("deleted_at IS NULL AND photo_lat != 0 AND photo_long != 0").
Select("photo_uuid, location_id, photo_lat, photo_lng, taken_at, taken_at_local").
Where("deleted_at IS NULL AND photo_lat != 0 AND photo_lng != 0").
Order("taken_at DESC").
Limit(1).Offset(0).
Take(&position)
var countries []country
var count = struct {
Photos uint `json:"photos"`
Favorites uint `json:"favorites"`
@ -622,8 +617,15 @@ func (c *Config) ClientConfig() ClientConfig {
Select("COUNT(*) AS countries").
Take(&count)
db.Model(&entity.Location{}).
Select("DISTINCT loc_country_code, loc_country").
type country struct {
ID string `json:"code"`
CountryName string `json:"name"`
}
var countries []country
db.Model(&entity.Country{}).
Select("DISTINCT id, country_name").
Scan(&countries)
db.Where("deleted_at IS NULL").

View file

@ -23,7 +23,7 @@ type Album struct {
AlbumFavorite bool
AlbumPublic bool
AlbumLat float64
AlbumLong float64
AlbumLng float64
AlbumRadius float64
AlbumOrder string `gorm:"type:varchar(16);"`
AlbumTemplate string `gorm:"type:varchar(128);"`

View file

@ -18,7 +18,7 @@ type Event struct {
EventBegin time.Time `gorm:"type:datetime;"`
EventEnd time.Time `gorm:"type:datetime;"`
EventLat float64
EventLong float64
EventLng float64
EventDist float64
CreatedAt time.Time
UpdatedAt time.Time

View file

@ -1,71 +1,68 @@
package entity
var locTypeLabels = map[string]string{
"bay": "bay",
"art": "art exhibition",
"fire station": "fire station",
"hairdresser": "hairdresser",
"cape": "cape",
"coastline": "coastline",
"cliff": "cliff",
"wetland": "wetland",
"nature reserve": "nature reserve",
"beach": "beach",
"cafe": "cafe",
"internet cafe": "cafe",
"ice cream": "ice cream parlor",
"bistro": "restaurant",
"restaurant": "restaurant",
"ship": "ship",
"wholesale": "shop",
"food": "shop",
"supermarket": "supermarket",
"florist": "florist",
"pharmacy": "pharmacy",
"seafood": "seafood",
"clothes": "clothing store",
"residential": "residential area",
"museum": "museum",
"castle": "castle",
"terminal": "airport",
"ferry terminal": "harbor",
"bridge": "bridge",
"university": "university",
"mall": "mall",
"marina": "marina",
"garden": "garden",
"pedestrian": "shopping area",
"bunker": "bunker",
"viewpoint": "viewpoint",
"train station": "train station",
"farm": "farm",
}
import (
"strings"
"time"
olc "github.com/google/open-location-code/go"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/maps"
"github.com/photoprism/photoprism/internal/util"
)
// Photo location
type Location struct {
Model
LocDisplayName string
LocLat float64
LocLong float64
LocCategory string
LocType string
LocName string
LocHouseNr string
LocStreet string
LocSuburb string
LocCity string
LocPostcode string
LocCounty string
LocState string
LocCountry string
LocCountryCode string
maps.Location
LocDescription string `gorm:"type:text;"`
LocNotes string `gorm:"type:text;"`
LocPhoto *Photo
LocPhotoID uint
LocFavorite bool
CreatedAt time.Time
UpdatedAt time.Time
}
func NewLocation(lat, lng float64) *Location {
result := &Location{}
result.ID = olc.Encode(lat, lng, 11)
result.LocLat = lat
result.LocLng = lng
return result
}
func (m *Location) Label() string {
return locTypeLabels[m.LocType]
return m.LocLabel
}
func (m *Location) Find(db *gorm.DB) error {
if err := db.First(m, "id = ?", m.ID).Error; err == nil {
return err
}
if err := m.Query(); err != nil {
return err
}
if err := db.Create(m).Error; err != nil {
log.Errorf("location: %s", err)
return err
}
return nil
}
func (m *Location) Keywords() []string {
result := []string{
strings.ToLower(m.LocCity),
strings.ToLower(m.LocSuburb),
strings.ToLower(m.LocState),
strings.ToLower(m.CountryName()),
strings.ToLower(m.LocLabel),
}
result = append(result, util.Keywords(m.LocTitle)...)
result = append(result, util.Keywords(m.LocDescription)...)
result = append(result, util.Keywords(m.LocNotes)...)
return result
}

View file

@ -6,8 +6,9 @@ import (
)
func TestLocation_Label(t *testing.T) {
location := &Location{LocCategory: "restaurant", LocType: "bistro"}
result := location.Label()
l := NewLocation(1,1)
l.LocLabel = "restaurant"
result := l.Label()
assert.Equal(t, "restaurant", result)
}

View file

@ -25,7 +25,7 @@ type Photo struct {
PhotoNSFW bool `json:"PhotoNSFW"`
PhotoStory bool `json:"PhotoStory"`
PhotoLat float64 `gorm:"index;"`
PhotoLong float64 `gorm:"index;"`
PhotoLng float64 `gorm:"index;"`
PhotoAltitude int
PhotoFocalLength int
PhotoIso int
@ -40,7 +40,7 @@ type Photo struct {
CountryID string `gorm:"index;"`
CountryChanged bool
Location *Location
LocationID uint
LocationID string
LocationChanged bool
LocationEstimated bool
TakenAt time.Time `gorm:"type:datetime;index;"`

View file

@ -17,24 +17,24 @@ import (
type PhotoSearch struct {
Query string `form:"q"`
Title string `form:"title"`
Description string `form:"description"`
Notes string `form:"notes"`
Artist string `form:"artist"`
Hash string `form:"hash"`
Duplicate bool `form:"duplicate"`
Lat float64 `form:"lat"`
Long float64 `form:"long"`
Dist uint `form:"dist"`
Fmin float64 `form:"fmin"`
Fmax float64 `form:"fmax"`
Chroma uint `form:"chroma"`
Mono bool `form:"mono"`
Portrait bool `form:"portrait"`
Location bool `form:"location"`
Album string `form:"album"`
Label string `form:"label"`
Country string `form:"country"`
Title string `form:"title"`
Description string `form:"description"`
Notes string `form:"notes"`
Artist string `form:"artist"`
Hash string `form:"hash"`
Duplicate bool `form:"duplicate"`
Lat float64 `form:"lat"`
Lng float64 `form:"lng"`
Dist uint `form:"dist"`
Fmin float64 `form:"fmin"`
Fmax float64 `form:"fmax"`
Chroma uint `form:"chroma"`
Mono bool `form:"mono"`
Portrait bool `form:"portrait"`
Location bool `form:"location"`
Album string `form:"album"`
Label string `form:"label"`
Country string `form:"country"`
Color string `form:"color"`
Camera int `form:"camera"`
Before time.Time `form:"before" time_format:"2006-01-02"`

View file

@ -34,7 +34,7 @@ func TestParseQueryString(t *testing.T) {
assert.Equal(t, 33.45343166666667, form.Lat)
})
t.Run("valid query 2", func(t *testing.T) {
form := &PhotoSearch{Query: "chroma:600 description:\"test\" after:2018-01-15 duplicate:false favorites:true long:33.45343166666667"}
form := &PhotoSearch{Query: "chroma:600 description:\"test\" after:2018-01-15 duplicate:false favorites:true lng:33.45343166666667"}
err := form.ParseQueryString()
@ -45,7 +45,7 @@ func TestParseQueryString(t *testing.T) {
assert.Equal(t, "test", form.Description)
assert.Equal(t, time.Date(2018, 01, 15, 0, 0, 0, 0, time.UTC), form.After)
assert.Equal(t, false, form.Duplicate)
assert.Equal(t, 33.45343166666667, form.Long)
assert.Equal(t, 33.45343166666667, form.Lng)
})
t.Run("valid query with umlauts", func(t *testing.T) {
form := &PhotoSearch{Query: "description:\"tübingen\""}

View file

@ -1,7 +1,7 @@
// Code generated by go generate; DO NOT EDIT.
package maps
var Countries = map[string]string{
var CountryNames = map[string]string{
"af": "Afghanistan",
"ax": "Åland Islands",
"al": "Albania",

View file

@ -53,7 +53,7 @@ func main() {
var packageTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
package maps
var Countries = map[string]string{
var CountryNames = map[string]string{
{{- range .Countries }}
{{ printf "%q" .Code }}: {{ printf "%q" .Name }},
{{- end }}

View file

@ -8,21 +8,19 @@ import (
"github.com/photoprism/photoprism/internal/maps/osm"
)
const SourceOSM = "osm"
// Photo location
type Location struct {
ID string `gorm:"primary_key"`
LocLat float64
LocLng float64
LocTitle string
LocCity string
LocSuburb string
LocState string
LocCountry string
LocRegion string
LocLabel string
LocSource string
ID string `gorm:"primary_key"`
LocLat float64
LocLng float64
LocTitle string
LocRegion string
LocCity string
LocSuburb string
LocState string
LocCountryCode string
LocLabel string
LocSource string
}
type LocationSource interface {
@ -32,13 +30,25 @@ type LocationSource interface {
City() string
Suburb() string
State() string
Country() string
CountryCode() string
Label() string
Source() string
}
func (l *Location) Query(lat, lng float64) error {
o, err := osm.FindLocation(lat, lng)
func NewLocation (lat, lng float64) *Location {
id := olc.Encode(lat, lng, 11)
result := &Location{
ID: id,
LocLat: lat,
LocLng: lng,
}
return result
}
func (l *Location) Query() error {
o, err := osm.FindLocation(l.LocLat, l.LocLng)
if err != nil {
return err
@ -49,22 +59,24 @@ func (l *Location) Query(lat, lng float64) error {
func (l *Location) Assign(s LocationSource) error {
l.LocSource = s.Source()
l.LocLat = s.Latitude()
l.LocLng = s.Longitude()
if l.LocLat == 0 { l.LocLat = s.Latitude() }
if l.LocLng == 0 { l.LocLng = s.Longitude() }
if l.Unknown() {
l.LocLabel = "unknown"
return errors.New("maps: unknown location")
}
l.ID = olc.Encode(l.LocLat, l.LocLng, 11)
if l.ID == "" { l.ID = olc.Encode(l.LocLat, l.LocLng, 11) }
l.LocTitle = s.Title()
l.LocCity = s.City()
l.LocSuburb = s.Suburb()
l.LocState = s.State()
l.LocCountry = s.Country()
l.LocRegion = l.region()
l.LocCountryCode = s.CountryCode()
l.LocLabel = s.Label()
l.LocRegion = l.region()
return nil
}
@ -82,8 +94,9 @@ func (l *Location) region() string {
return "Unknown"
}
var countryName = Countries[l.LocCountry]
var countryName = l.CountryName()
var loc []string
shortCountry := len([]rune(countryName)) <= 20
shortCity := len([]rune(l.LocCity)) <= 20
@ -101,3 +114,47 @@ func (l *Location) region() string {
return strings.Join(loc[:], ", ")
}
func (l Location) Latitude() float64 {
return l.LocLat
}
func (l Location) Longitude() float64 {
return l.LocLng
}
func (l Location) Title() string {
return l.LocTitle
}
func (l Location) City() string {
return l.LocCity
}
func (l Location) Suburb() string {
return l.LocSuburb
}
func (l Location) State() string {
return l.LocState
}
func (l Location) Label() string {
return l.LocLabel
}
func (l Location) Source() string {
return l.LocSource
}
func (l Location) Region() string {
return l.LocRegion
}
func (l Location) CountryCode() string {
return l.LocCountryCode
}
func (l Location) CountryName() string {
return CountryNames[l.LocCountryCode]
}

View file

@ -12,9 +12,9 @@ func TestLocation_Query(t *testing.T) {
lat := 52.5208
lng := 13.40953
var l Location
l := NewLocation(lat, lng)
if err := l.Query(lat, lng); err != nil {
if err := l.Query(); err != nil {
t.Fatal(err)
}
@ -23,7 +23,7 @@ func TestLocation_Query(t *testing.T) {
})
}
func TestLocation_OpenStreetMap(t *testing.T) {
func TestLocation_Assign(t *testing.T) {
t.Run("BerlinFernsehturm", func(t *testing.T) {
lat := 52.5208
lng := 13.40953
@ -223,7 +223,7 @@ func TestLocation_OpenStreetMap(t *testing.T) {
}
assert.Equal(t, "4GWF24FX+F5H", l.ID)
assert.Equal(t, "Route R411", l.LocTitle)
assert.Equal(t, "R411", l.LocTitle)
assert.Equal(t, "Eastern Cape, South Africa", l.LocRegion)
})
}

View file

@ -3,45 +3,161 @@ package osm
import "fmt"
var locationLabels = map[string]string{
"bay": "bay",
"art": "art exhibition",
"fire station": "fire station",
"hairdresser": "hairdresser",
"cape": "cape",
"coastline": "coastline",
"cliff": "cliff",
"wetland": "wetland",
"nature reserve": "nature reserve",
"natural=beach": "beach",
"amenity=cafe": "cafe",
"amenity=internet_cafe": "cafe",
"ice cream": "ice cream parlor",
"bistro": "restaurant",
"restaurant": "restaurant",
"ship": "ship",
"wholesale": "shop",
"food": "shop",
"supermarket": "supermarket",
"florist": "florist",
"pharmacy": "pharmacy",
"seafood": "seafood",
"clothes": "clothing store",
"residential": "residential area",
"museum": "museum",
"castle": "castle",
"aeroway=*": "airport",
"ferry terminal": "harbor",
"bridge": "bridge",
"university": "university",
"mall": "mall",
"marina": "marina",
"garden": "garden",
"pedestrian": "shopping area",
"bunker": "bunker",
"viewpoint": "viewpoint",
"train station": "train station",
"farm": "farm",
"highway=secondary": "highway",
"aeroway=*": "airport",
"natural=bay": "bay",
"natural=peninsula": "peninsula",
"natural=cape": "cape",
"natural=wood": "forest",
"natural=grassland": "grassland",
"*=beach": "beach",
"*=dune": "dune",
"*=water": "water",
"*=wetland": "wetland",
"*=glacier": "glacier",
"*=strait": "seashore",
"*=coastline": "seashore",
"*=reef": "reef",
"*=geyser": "geyser",
"natural=peak": "mountain",
"natural=hill": "hill",
"natural=volcano": "volcano",
"natural=valley": "valley",
"natural=ridge": "mountain",
"natural=cliff": "cliff",
"natural=saddle": "mountain",
"natural=isthmus": "seashore",
"natural=sinkhole": "sinkhole",
"natural=*": "nature",
"place=sea": "ocean",
"*=ocean": "ocean",
"*=gallery": "gallery",
"*=museum": "museum",
"*=alpine_hut": "alpine hut",
"*=aquarium": "aquarium",
"*=artwork": "exhibition",
"*=camp_pitch": "camping",
"*=camp_site": "camping",
"*=caravan_site": "camping",
"*=hotel": "hotel",
"*=hostel": "hotel",
"*=motel": "hotel",
"tourism=information": "visitor center",
"*=picnic_site": "hiking",
"*=theme_park": "theme park",
"*=viewpoint": "viewpoint",
"*=wilderness_hut": "hiking",
"*=zoo": "zoo",
"shop=*": "shop",
"shop=butcher": "butcher",
"shop=department_store": "department store",
"*=supermarket": "supermarket",
"*=mall": "mall",
"*=boutique": "boutique",
"*=fashion": "boutique",
"*=fashion_accessories": "boutique",
"*=clothes": "boutique",
"*=fabric": "boutique",
"shop=leather": "boutique",
"shop=baby_goods": "boutique",
"shop=bag": "boutique",
"shop=books": "bookstore",
"shop=cannabis": "headshop",
"*=fire_station": "fire station",
"amenity=bar": "bar",
"amenity=biergarten": "biergarten",
"amenity=cafe": "cafe",
"amenity=internet_cafe": "cafe",
"amenity=ice_cream": "ice cream parlor",
"amenity=bistro": "restaurant",
"amenity=restaurant": "restaurant",
"amenity=fast_food": "restaurant",
"amenity=food_court": "restaurant",
"amenity=pub": "pub",
"amenity=college": "university",
"amenity=university": "university",
"amenity=kindergarten": "kindergarten",
"amenity=language_school": "school",
"amenity=driving_school": "school",
"amenity=music_school": "school",
"amenity=school": "school",
"amenity=car_rental": "car",
"amenity=ferry_terminal": "harbor",
"amenity=parking": "parking",
"amenity=parking_entrance": "parking",
"amenity=parking_space": "parking",
"*=bank": "bank",
"*=clinic": "hospital",
"*=hospital": "hospital",
"*=pharmacy": "pharmacy",
"*=arts_centre": "exhibition",
"*=casino": "casino",
"*=cinema": "cinema",
"*=gambling": "casino",
"*=planetarium": "planetarium",
"*=nightclub": "nightclub",
"*=theatre": "theatre",
"*=embassy": "embassy",
"*=grave_yard": "cemetery",
"*=cemetery": "cemetery",
"*=marketplace": "marketplace",
"*=monastery": "monastery",
"*=police": "police",
"*=prison": "prison",
"*=public_bath": "swimming",
"*=shelter": "shelter",
"*=aircraft": "aircraft",
"*=castle": "castle",
"*=castle_wall": "castle",
"*=church": "church",
"*=farm": "farm",
"*=memorial": "memorial",
"*=ship": "ship",
"*=tank": "tank",
"*=tower": "tower",
"*=wreck": "ship",
"*=houseboat": "ship",
"*=office": "office",
"*=warehouse": "warehouse",
"*=cathedral": "cathedral",
"*=chapel": "chapel",
"*=mosque": "mosque",
"*=shrine": "shrine",
"*=synagogue": "synagogue",
"*=temple": "temple",
"*=train_station": "train station",
"*=cowshed": "farm",
"*=greenhouse": "greenhouse",
"*=stable": "farm",
"*=farm_auxiliary": "farm",
"*=barn": "farm",
"*=sty": "farm",
"*=stadium": "stadium",
"*=hangar": "hangar",
"*=parking": "parking",
"*=water_tower": "tower",
"*=transformer_tower": "tower",
"*=bunker": "bunker",
"*=bridge": "bridge",
"*=garden": "botanical garden",
"*=adult_gaming_centre": "casino",
"*=amusement_arcade": "casino",
"*=beach_resort": "beach",
"*=dog_park": "dog park",
"*=escape_game": "escape game",
"*=firepit": "camping",
"*=golf_course": "golf",
"*=miniature_golf": "golf",
"*=hackerspace": "hackerspace",
"*=marina": "marina",
"*=nature_reserve": "nature reserve",
"*=park": "park",
"*=picnic_table": "outdoor",
"*=pitch": "sports",
"*=sports_centre": "sports",
"*=swimming_area": "swimming",
"*=swimming_pool": "swimming",
"*=water_park": "water park",
"*=wildlife_hide": "wildlife",
}
func (o Location) Label() (result string) {

View file

@ -92,7 +92,7 @@ func (o Location) Suburb() (result string) {
return strings.TrimSpace(result)
}
func (o Location) Country() (result string) {
func (o Location) CountryCode() (result string) {
result = o.Address.CountryCode
return strings.ToLower(strings.TrimSpace(result))

View file

@ -8,7 +8,7 @@ import (
var labelTitles = map[string]string{
"airport": "Airport",
"highway": "Route %name%",
"visitor center": "Visitor Center",
}
func (o Location) Title() (result string) {

View file

@ -21,22 +21,22 @@ type Exif struct {
Artist string
CameraMake string
CameraModel string
Description string
LensMake string
LensModel string
Flash bool
FocalLength int
Exposure string
Aperture float64
FNumber float64
Iso int
Lat float64
Long float64
Altitude int
Width int
Height int
Orientation int
All map[string]string
Description string
LensMake string
LensModel string
Flash bool
FocalLength int
Exposure string
Aperture float64
FNumber float64
Iso int
Lat float64
Lng float64
Altitude int
Width int
Height int
Orientation int
All map[string]string
}
var im *exif.IfdMapping
@ -227,15 +227,15 @@ func (m *MediaFile) Exif() (result *Exif, err error) {
if ifd, err := index.RootIfd.ChildWithIfdPath(exif.IfdPathStandardGps); err == nil {
if gi, err := ifd.GpsInfo(); err == nil {
m.exifData.Lat = gi.Latitude.Decimal()
m.exifData.Long = gi.Longitude.Decimal()
m.exifData.Lng = gi.Longitude.Decimal()
m.exifData.Altitude = gi.Altitude
}
}
if m.exifData.Lat != 0 && m.exifData.Long != 0 {
if m.exifData.Lat != 0 && m.exifData.Lng != 0 {
zones, err := tz.GetZone(tz.Point{
Lat: m.exifData.Lat,
Lon: m.exifData.Long,
Lon: m.exifData.Lng,
})
if err != nil {

View file

@ -38,7 +38,7 @@ func TestMediaFile_Exif_JPEG(t *testing.T) {
assert.Equal(t, 10.0, info.FNumber)
assert.Equal(t, 200, info.Iso)
assert.Equal(t, -33.45347, info.Lat)
assert.Equal(t, 25.764645, info.Long)
assert.Equal(t, 25.764645, info.Lng)
assert.Equal(t, 190, info.Altitude)
assert.Equal(t, 1365, info.Width)
assert.Equal(t, 0, info.Height)
@ -112,7 +112,7 @@ func TestMediaFile_Exif_DNG(t *testing.T) {
assert.Equal(t, 4.971, info.Aperture)
assert.Equal(t, 1000, info.Iso)
assert.Equal(t, 0.0, info.Lat)
assert.Equal(t, 0.0, info.Long)
assert.Equal(t, 0.0, info.Lng)
assert.Equal(t, 0, info.Altitude)
assert.Equal(t, 171, info.Width)
assert.Equal(t, 0, info.Height)
@ -164,7 +164,7 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
assert.Equal(t, 1.696, jpegInfo.Aperture)
assert.Equal(t, 20, jpegInfo.Iso)
assert.Equal(t, 34.79745, jpegInfo.Lat)
assert.Equal(t, 134.76463333333334, jpegInfo.Long)
assert.Equal(t, 134.76463333333334, jpegInfo.Lng)
assert.Equal(t, 0, jpegInfo.Altitude)
assert.Equal(t, 0, jpegInfo.Width)
assert.Equal(t, 0, jpegInfo.Height)

View file

@ -56,7 +56,7 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult {
if photoQuery.Error != nil && m.HasTimeAndPlace() {
exifData, _ = m.Exif()
photoQuery = i.db.Unscoped().First(&photo, "photo_lat = ? AND photo_long = ? AND taken_at = ?", exifData.Lat, exifData.Long, exifData.TakenAt)
photoQuery = i.db.Unscoped().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ?", exifData.Lat, exifData.Lng, exifData.TakenAt)
}
} else {
photoQuery = i.db.Unscoped().First(&photo, "id = ?", file.PhotoID)
@ -97,7 +97,7 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult {
// Read UpdateExif data
if exifData, err := m.Exif(); err == nil {
photo.PhotoLat = exifData.Lat
photo.PhotoLong = exifData.Long
photo.PhotoLng = exifData.Lng
photo.TakenAt = exifData.TakenAt
photo.TakenAtLocal = exifData.TakenAtLocal
photo.TimeZone = exifData.TimeZone
@ -128,7 +128,7 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult {
labels = append(labels, locLabels...)
}
if photo.PhotoTitle == "" || (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false && photo.LocationID == 0 {
if photo.PhotoTitle == "" || (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false && photo.LocationID == "" {
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(labels[0].Name), m.DateCreated().Format("2006"))
} else if !photo.TakenAtLocal.IsZero() {
@ -160,7 +160,7 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult {
if photoExists {
// Estimate location
if o.UpdateLocation && photo.LocationID == 0 {
if o.UpdateLocation && photo.LocationID == "" {
i.estimateLocation(&photo)
}
@ -368,12 +368,18 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
var keywords []string
if location, err := mediaFile.Location(); err == nil {
i.db.FirstOrCreate(location, "id = ?", location.ID)
err := location.Find(i.db)
if err != nil {
log.Error(err)
return keywords, labels
}
photo.Location = location
photo.LocationID = location.ID
photo.LocationEstimated = false
photo.Country = entity.NewCountry(location.LocCountryCode, location.LocCountry).FirstOrCreate(i.db)
photo.Country = entity.NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(i.db)
if photo.Country.New {
event.Publish("count.countries", event.Data{
@ -384,7 +390,7 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
countryName := photo.Country.CountryName
locLabel := location.Label()
keywords = append(keywords, util.Keywords(location.LocDisplayName)...)
keywords = append(keywords, location.Keywords()...)
// Append label from OpenStreetMap
if locLabel != "" {
@ -393,33 +399,27 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
}
if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false {
if title := labels.Title(location.LocName); title != "" { // TODO: User defined title format
if title := labels.Title(location.Title()); title != "" { // TODO: User defined title format
log.Infof("index: using label \"%s\" to create photo title", title)
if location.LocCity == "" || len(location.LocCity) > 16 || strings.Contains(title, location.LocCity) {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), countryName, photo.TakenAt.Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.LocCity, photo.TakenAt.Format("2006"))
}
} else if location.LocName != "" && location.LocCity != "" {
if len(location.LocName) > 45 {
photo.PhotoTitle = util.Title(location.LocName)
} else if len(location.LocName) > 20 || len(location.LocCity) > 16 || strings.Contains(location.LocName, location.LocCity) {
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(location.LocName), photo.TakenAt.Format("2006"))
} else if location.Title() != "" && location.City() != "" {
if len(location.Title()) > 45 {
photo.PhotoTitle = util.Title(location.Title())
} else if len(location.Title()) > 20 || len(location.City()) > 16 || strings.Contains(location.Title(), location.City()) {
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.Title(), photo.TakenAt.Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(location.LocName), location.LocCity, photo.TakenAt.Format("2006"))
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.Title(), location.LocCity, photo.TakenAt.Format("2006"))
}
} else if location.LocCity != "" && countryName != "" {
if len(location.LocCity) > 20 {
} else if location.City() != "" && countryName != "" {
if len(location.City()) > 20 {
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCity, photo.TakenAt.Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, countryName, photo.TakenAt.Format("2006"))
}
} else if location.LocCounty != "" && countryName != "" {
if len(location.LocCounty) > 20 {
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCounty, photo.TakenAt.Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCounty, countryName, photo.TakenAt.Format("2006"))
}
}
if photo.PhotoTitle == "" {

View file

@ -0,0 +1,26 @@
package photoprism
import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/pkg/errors"
)
func (m *MediaFile) Location() (*entity.Location, error) {
if m.location != nil {
return m.location, nil
}
data, err := m.Exif()
if err != nil {
return nil, err
}
if data.Lat == 0 && data.Lng == 0 {
return nil, errors.New("mediafile: no latitude and longitude in image metadata")
}
m.location = entity.NewLocation(data.Lat, data.Lng)
return m.location, nil
}

View file

@ -0,0 +1,88 @@
package photoprism
import (
"testing"
"github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert"
)
func TestMediaFile_Location(t *testing.T) {
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
if err != nil {
t.Fatal(err)
}
location, err := mediaFile.Location()
if err != nil {
t.Fatal(err)
}
if err = location.Find(conf.Db()); err != nil {
t.Fatal(err)
}
assert.Equal(t, "Himeji", location.City())
assert.Equal(t, "Kinki Region", location.State())
assert.Equal(t, "Japan", location.CountryName())
assert.Equal(t, "", location.Label())
assert.Equal(t, 34.79745, location.Latitude())
assert.Equal(t, "8Q6PQQW7+XVJ", location.ID)
location2, err := mediaFile.Location()
if err != nil {
t.Fatal(err)
}
if err = location.Find(conf.Db()); err != nil {
t.Fatal(err)
}
assert.Equal(t, "Himeji", location2.LocCity)
assert.Equal(t, "Kinki Region", location2.LocState)
})
t.Run("/cat_brown.jpg", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg")
if err != nil {
t.Fatal(err)
}
location, err := mediaFile.Location()
if err != nil {
t.Fatal(err)
}
if err = location.Find(conf.Db()); err != nil {
t.Fatal(err)
}
assert.Equal(t, "Tübingen", location.City())
assert.Equal(t, "de", location.CountryCode())
assert.Equal(t, "Germany", location.CountryName())
assert.Equal(t, 48.53870833333333, location.Latitude())
assert.Equal(t, "8FWFG2Q6+FVM", location.ID)
})
t.Run("/dog_orange.jpg", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/dog_orange.jpg")
if err != nil {
t.Fatal(err)
}
if _, err := mediaFile.Location(); err == nil {
t.Fatal("mediaFile.Location() should return error")
} else {
assert.Equal(t, "mediafile: no latitude and longitude in image metadata", err.Error())
}
})
}

View file

@ -85,7 +85,7 @@ func (m *MediaFile) HasTimeAndPlace() bool {
return false
}
result := !exifData.TakenAt.IsZero() && exifData.Lat != 0 && exifData.Long != 0
result := !exifData.TakenAt.IsZero() && exifData.Lat != 0 && exifData.Lng != 0
return result
}

View file

@ -1,127 +0,0 @@
package photoprism
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/util"
"github.com/pkg/errors"
)
type openstreetmapAddress struct {
HouseNumber string `json:"house_number"`
Road string `json:"road"`
Suburb string `json:"suburb"`
Town string `json:"town"`
City string `json:"city"`
Postcode string `json:"postcode"`
County string `json:"county"`
State string `json:"state"`
Country string `json:"country"`
CountryCode string `json:"country_code"`
}
type openstreetmapLocation struct {
PlaceID uint `json:"place_id"`
Lat string `json:"lat"`
Lon string `json:"lon"`
Name string `json:"name"`
Category string `json:"category"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Address *openstreetmapAddress `json:"address"`
}
// Location See https://wiki.openstreetmap.org/wiki/Nominatim#Reverse_Geocoding
func (m *MediaFile) Location() (*entity.Location, error) {
if m.location != nil {
return m.location, nil
}
location := &entity.Location{}
openstreetmapLocation := &openstreetmapLocation{
Address: &openstreetmapAddress{},
}
if exifData, err := m.Exif(); err == nil {
if exifData.Lat == 0 && exifData.Long == 0 {
return nil, errors.New("no latitude and longitude in image metadata")
}
url := fmt.Sprintf(
"https://nominatim.openstreetmap.org/reverse?lat=%f&lon=%f&format=jsonv2&accept-language=en&zoom=18",
exifData.Lat,
exifData.Long)
if res, err := http.Get(url); err == nil {
err = json.NewDecoder(res.Body).Decode(openstreetmapLocation)
if err != nil {
return nil, err
}
} else {
return nil, err
}
} else {
return nil, err
}
if id := openstreetmapLocation.PlaceID; id > 0 {
location.ID = id
} else {
return nil, errors.New("query returned no result")
}
if openstreetmapLocation.Address.City != "" {
location.LocCity = openstreetmapLocation.Address.City
} else {
location.LocCity = openstreetmapLocation.Address.Town
}
if lat, err := strconv.ParseFloat(openstreetmapLocation.Lat, 64); err == nil {
location.LocLat = lat
}
if lon, err := strconv.ParseFloat(openstreetmapLocation.Lon, 64); err == nil {
location.LocLong = lon
}
if len(openstreetmapLocation.Name) > 1 {
s := openstreetmapLocation.Name
s = strings.Replace(s, " - ", " / ", -1)
s = strings.Replace(s, ", ", " / ", -1)
location.LocName = util.Title(strings.TrimSpace(strings.Replace(s, "_", " ", -1)))
}
if len(openstreetmapLocation.Address.County) > 1 {
s := openstreetmapLocation.Address.County
s = strings.Replace(s, " - ", " / ", -1)
s = strings.Replace(s, ", ", " / ", -1)
location.LocCounty = util.Title(strings.TrimSpace(strings.Replace(s, "_", " ", -1)))
}
location.LocHouseNr = strings.TrimSpace(openstreetmapLocation.Address.HouseNumber)
location.LocStreet = strings.TrimSpace(openstreetmapLocation.Address.Road)
location.LocSuburb = strings.TrimSpace(openstreetmapLocation.Address.Suburb)
location.LocPostcode = strings.TrimSpace(openstreetmapLocation.Address.Postcode)
location.LocState = strings.TrimSpace(openstreetmapLocation.Address.State)
location.LocCountry = strings.TrimSpace(openstreetmapLocation.Address.Country)
location.LocCountryCode = strings.TrimSpace(openstreetmapLocation.Address.CountryCode)
location.LocDisplayName = strings.TrimSpace(openstreetmapLocation.DisplayName)
locationCategory := strings.TrimSpace(strings.Replace(openstreetmapLocation.Category, "_", " ", -1))
location.LocCategory = locationCategory
locationType := strings.TrimSpace(strings.Replace(openstreetmapLocation.Type, "_", " ", -1))
location.LocType = locationType
m.location = location
return m.location, nil
}

View file

@ -1,53 +0,0 @@
package photoprism
import (
"testing"
"github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert"
)
func TestMediaFile_Location(t *testing.T) {
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
assert.Nil(t, err)
location, err := mediaFile.Location()
assert.Nil(t, err)
assert.Equal(t, "Himeji", location.LocCity)
assert.Equal(t, "Kinki Region", location.LocState)
assert.Equal(t, "Japan", location.LocCountry)
assert.Equal(t, "highway", location.LocCategory)
assert.Equal(t, 34.7974872, location.LocLat)
location2, err := mediaFile.Location()
assert.Nil(t, err)
assert.Equal(t, "Himeji", location2.LocCity)
assert.Equal(t, "Kinki Region", location2.LocState)
})
t.Run("/cat_brown.jpg", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg")
assert.Nil(t, err)
location, err := mediaFile.Location()
assert.Nil(t, err)
assert.Equal(t, "Geißwiesenstraße", location.LocStreet)
assert.Equal(t, "14", location.LocHouseNr)
assert.Equal(t, "72070", location.LocPostcode)
assert.Equal(t, "Tübingen", location.LocCity)
assert.Equal(t, "Landkreis Tübingen", location.LocCounty)
assert.Equal(t, "Germany", location.LocCountry)
assert.Equal(t, "building", location.LocCategory)
assert.Equal(t, 48.53870475, location.LocLat)
})
t.Run("/dog_orange.jpg", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/dog_orange.jpg")
assert.Nil(t, err)
location, err := mediaFile.Location()
assert.Nil(t, location)
assert.Equal(t, "no latitude and longitude in image metadata", err.Error())
})
}

View file

@ -14,12 +14,12 @@ func (m *MediaFile) TimeZone() (string, error) {
return "UTC", errors.New("no image metadata")
}
if meta.Lat == 0 && meta.Long == 0 {
if meta.Lat == 0 && meta.Lng == 0 {
return "UTC", errors.New("no latitude and longitude in image metadata")
}
zones, err := tz.GetZone(tz.Point{
Lon: meta.Long, Lat: meta.Lat,
Lon: meta.Lng, Lat: meta.Lat,
})
if err != nil {

View file

@ -35,7 +35,7 @@ type PhotoResult struct {
PhotoSensitive bool
PhotoStory bool
PhotoLat float64
PhotoLong float64
PhotoLng float64
PhotoAltitude int
PhotoFocalLength int
PhotoIso int
@ -57,17 +57,15 @@ type PhotoResult struct {
CountryName string
// Location
LocationID uint
LocDisplayName string
LocName string
LocationID string
LocTitle string
LocCity string
LocPostcode string
LocCounty string
LocSuburb string
LocState string
LocCountry string
LocCountryCode string
LocCategory string
LocType string
LocRegion string
LocLabel string
LocSource string
LocationChanged bool
LocationEstimated bool
@ -123,8 +121,8 @@ func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
cameras.camera_make, cameras.camera_model,
lenses.lens_make, lenses.lens_model,
countries.country_name,
locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_county,
locations.loc_state, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type`).
locations.loc_title, locations.loc_city, locations.loc_suburb, locations.loc_state, locations.loc_country_code,
locations.loc_region, locations.loc_label, locations.loc_source`).
Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN cameras ON cameras.id = photos.camera_id").
Joins("JOIN lenses ON lenses.id = photos.lens_id").
@ -278,10 +276,10 @@ func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
q = q.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
}
if f.Long > 0 {
longMin := f.Long - SearchRadius*float64(f.Dist)
longMax := f.Long + SearchRadius*float64(f.Dist)
q = q.Where("photos.photo_long BETWEEN ? AND ?", longMin, longMax)
if f.Lng > 0 {
lngMin := f.Lng - SearchRadius*float64(f.Dist)
lngMax := f.Lng + SearchRadius*float64(f.Dist)
q = q.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
}
if !f.Before.IsZero() {

View file

@ -270,9 +270,9 @@ func TestSearch_Photos_Query(t *testing.T) {
t.Logf("results: %+v", photos)
})
t.Run("form.Lat and form.Long and Order:imported", func(t *testing.T) {
t.Run("form.Lat and form.Lng and Order:imported", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Lat:33.45343166666667 Long:25.764711666666667 Dist:2000 Order:imported"
f.Query = "Lat:33.45343166666667 Lng:25.764711666666667 Dist:2000 Order:imported"
f.Count = 3
f.Offset = 0