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

View file

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

View file

@ -72,7 +72,7 @@
{{ photo.getCamera() }} {{ photo.getCamera() }}
<br/> <br/>
<v-icon size="14">location_on</v-icon> <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> @click.stop="openLocation(index)">{{ photo.getLocation() }}</span>
</div> </div>
</div> </div>

View file

@ -20,7 +20,7 @@
</td> </td>
<td @click="openPhoto(props.index)" class="p-pointer">{{ props.item.PhotoTitle }}</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>{{ 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>{{ props.item.CameraMake }} {{ props.item.CameraModel }}</td>
<td><v-btn icon small flat :ripple="false" <td><v-btn icon small flat :ripple="false"
class="p-photo-like" class="p-photo-like"
@ -49,7 +49,7 @@
{text: '', value: '', align: 'center', sortable: false, class: 'p-col-select'}, {text: '', value: '', align: 'center', sortable: false, class: 'p-col-select'},
{text: this.$gettext('Title'), value: 'PhotoTitle'}, {text: this.$gettext('Title'), value: 'PhotoTitle'},
{text: this.$gettext('Taken At'), value: 'TakenAt'}, {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('Camera'), value: 'CameraModel'},
{text: this.$gettext('Favorite'), value: 'PhotoFavorite', align: 'left'}, {text: this.$gettext('Favorite'), value: 'PhotoFavorite', align: 'left'},
], ],

View file

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

View file

@ -33,7 +33,7 @@ class Photo extends Abstract {
} }
getGoogleMapsLink() { 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) { getThumbnailUrl(type) {
@ -101,71 +101,17 @@ class Photo extends Abstract {
} }
hasLocation() { hasLocation() {
return this.PhotoLat !== 0 || this.PhotoLong !== 0; return this.PhotoLat !== 0 || this.PhotoLng !== 0;
} }
getLocation() { getLocation() {
const location = []; if (this.LocRegion) {
return this.LocRegion
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);
}
} else if (this.CountryName) { } else if (this.CountryName) {
location.push(this.CountryName); return this.CountryName;
} else {
location.push("Unknown");
} }
return location.join(", "); return "Unknown"
}
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(", ");
} }
getCamera() { getCamera() {

View file

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

View file

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

View file

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

View file

@ -55,7 +55,7 @@ describe("model/photo", () => {
}); });
it("should get photo maps link", () => { 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 photo = new Photo(values);
const result = photo.getGoogleMapsLink(); const result = photo.getGoogleMapsLink();
assert.equal(result, "https://www.google.com/maps/place/36.442881666666665,28.229493333333334"); 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", () => { 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 photo = new Photo(values);
const result = photo.hasLocation(); const result = photo.hasLocation();
assert.equal(result, true); assert.equal(result, true);
}); });
it("should test whether photo has location", () => { 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 photo = new Photo(values);
const result = photo.hasLocation(); const result = photo.hasLocation();
assert.equal(result, false); assert.equal(result, false);
}); });
it("should get location", () => { 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 photo = new Photo(values);
const result = photo.getLocation(); const result = photo.getLocation();
assert.equal(result, "Cape Point, Africa"); assert.equal(result, "Cape Point, South Africa");
}); });
it("should get location", () => { 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 photo = new Photo(values);
const result = photo.getLocation(); const result = photo.getLocation();
assert.equal(result, "Cape Town, State, Africa"); assert.equal(result, "Cape Point, State, South Africa");
}); });
it("should get location", () => { 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 photo = new Photo(values);
const result = photo.getLocation(); const result = photo.getLocation();
assert.equal(result, "Unknown"); assert.equal(result, "Unknown");
@ -162,27 +162,6 @@ describe("model/photo", () => {
assert.equal(result, "Africa"); 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", () => { it("should get camera", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", CameraModel: "EOSD10", CameraMake: "Canon"}; const values = {ID: 5, PhotoTitle: "Crazy Cat", CameraModel: "EOSD10", CameraMake: "Canon"};
const photo = new Photo(values); const photo = new Photo(values);

View file

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

View file

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

View file

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

View file

@ -1,71 +1,68 @@
package entity package entity
var locTypeLabels = map[string]string{ import (
"bay": "bay", "strings"
"art": "art exhibition", "time"
"fire station": "fire station",
"hairdresser": "hairdresser", olc "github.com/google/open-location-code/go"
"cape": "cape", "github.com/jinzhu/gorm"
"coastline": "coastline", "github.com/photoprism/photoprism/internal/maps"
"cliff": "cliff", "github.com/photoprism/photoprism/internal/util"
"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",
}
// Photo location // Photo location
type Location struct { type Location struct {
Model maps.Location
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
LocDescription string `gorm:"type:text;"` LocDescription string `gorm:"type:text;"`
LocNotes string `gorm:"type:text;"` LocNotes string `gorm:"type:text;"`
LocPhoto *Photo
LocPhotoID uint
LocFavorite bool 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 { 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) { func TestLocation_Label(t *testing.T) {
location := &Location{LocCategory: "restaurant", LocType: "bistro"} l := NewLocation(1,1)
result := location.Label() l.LocLabel = "restaurant"
result := l.Label()
assert.Equal(t, "restaurant", result) assert.Equal(t, "restaurant", result)
} }

View file

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

View file

@ -17,24 +17,24 @@ import (
type PhotoSearch struct { type PhotoSearch struct {
Query string `form:"q"` Query string `form:"q"`
Title string `form:"title"` Title string `form:"title"`
Description string `form:"description"` Description string `form:"description"`
Notes string `form:"notes"` Notes string `form:"notes"`
Artist string `form:"artist"` Artist string `form:"artist"`
Hash string `form:"hash"` Hash string `form:"hash"`
Duplicate bool `form:"duplicate"` Duplicate bool `form:"duplicate"`
Lat float64 `form:"lat"` Lat float64 `form:"lat"`
Long float64 `form:"long"` Lng float64 `form:"lng"`
Dist uint `form:"dist"` Dist uint `form:"dist"`
Fmin float64 `form:"fmin"` Fmin float64 `form:"fmin"`
Fmax float64 `form:"fmax"` Fmax float64 `form:"fmax"`
Chroma uint `form:"chroma"` Chroma uint `form:"chroma"`
Mono bool `form:"mono"` Mono bool `form:"mono"`
Portrait bool `form:"portrait"` Portrait bool `form:"portrait"`
Location bool `form:"location"` Location bool `form:"location"`
Album string `form:"album"` Album string `form:"album"`
Label string `form:"label"` Label string `form:"label"`
Country string `form:"country"` Country string `form:"country"`
Color string `form:"color"` Color string `form:"color"`
Camera int `form:"camera"` Camera int `form:"camera"`
Before time.Time `form:"before" time_format:"2006-01-02"` 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) assert.Equal(t, 33.45343166666667, form.Lat)
}) })
t.Run("valid query 2", func(t *testing.T) { 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() err := form.ParseQueryString()
@ -45,7 +45,7 @@ func TestParseQueryString(t *testing.T) {
assert.Equal(t, "test", form.Description) 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, time.Date(2018, 01, 15, 0, 0, 0, 0, time.UTC), form.After)
assert.Equal(t, false, form.Duplicate) 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) { t.Run("valid query with umlauts", func(t *testing.T) {
form := &PhotoSearch{Query: "description:\"tübingen\""} form := &PhotoSearch{Query: "description:\"tübingen\""}

View file

@ -1,7 +1,7 @@
// Code generated by go generate; DO NOT EDIT. // Code generated by go generate; DO NOT EDIT.
package maps package maps
var Countries = map[string]string{ var CountryNames = map[string]string{
"af": "Afghanistan", "af": "Afghanistan",
"ax": "Åland Islands", "ax": "Åland Islands",
"al": "Albania", "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. var packageTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
package maps package maps
var Countries = map[string]string{ var CountryNames = map[string]string{
{{- range .Countries }} {{- range .Countries }}
{{ printf "%q" .Code }}: {{ printf "%q" .Name }}, {{ printf "%q" .Code }}: {{ printf "%q" .Name }},
{{- end }} {{- end }}

View file

@ -8,21 +8,19 @@ import (
"github.com/photoprism/photoprism/internal/maps/osm" "github.com/photoprism/photoprism/internal/maps/osm"
) )
const SourceOSM = "osm"
// Photo location // Photo location
type Location struct { type Location struct {
ID string `gorm:"primary_key"` ID string `gorm:"primary_key"`
LocLat float64 LocLat float64
LocLng float64 LocLng float64
LocTitle string LocTitle string
LocCity string LocRegion string
LocSuburb string LocCity string
LocState string LocSuburb string
LocCountry string LocState string
LocRegion string LocCountryCode string
LocLabel string LocLabel string
LocSource string LocSource string
} }
type LocationSource interface { type LocationSource interface {
@ -32,13 +30,25 @@ type LocationSource interface {
City() string City() string
Suburb() string Suburb() string
State() string State() string
Country() string CountryCode() string
Label() string Label() string
Source() string Source() string
} }
func (l *Location) Query(lat, lng float64) error { func NewLocation (lat, lng float64) *Location {
o, err := osm.FindLocation(lat, lng) 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 { if err != nil {
return err return err
@ -49,22 +59,24 @@ func (l *Location) Query(lat, lng float64) error {
func (l *Location) Assign(s LocationSource) error { func (l *Location) Assign(s LocationSource) error {
l.LocSource = s.Source() 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() { if l.Unknown() {
l.LocLabel = "unknown" l.LocLabel = "unknown"
return errors.New("maps: unknown location") 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.LocTitle = s.Title()
l.LocCity = s.City() l.LocCity = s.City()
l.LocSuburb = s.Suburb() l.LocSuburb = s.Suburb()
l.LocState = s.State() l.LocState = s.State()
l.LocCountry = s.Country() l.LocCountryCode = s.CountryCode()
l.LocRegion = l.region()
l.LocLabel = s.Label() l.LocLabel = s.Label()
l.LocRegion = l.region()
return nil return nil
} }
@ -82,8 +94,9 @@ func (l *Location) region() string {
return "Unknown" return "Unknown"
} }
var countryName = Countries[l.LocCountry] var countryName = l.CountryName()
var loc []string var loc []string
shortCountry := len([]rune(countryName)) <= 20 shortCountry := len([]rune(countryName)) <= 20
shortCity := len([]rune(l.LocCity)) <= 20 shortCity := len([]rune(l.LocCity)) <= 20
@ -101,3 +114,47 @@ func (l *Location) region() string {
return strings.Join(loc[:], ", ") 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 lat := 52.5208
lng := 13.40953 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) 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) { t.Run("BerlinFernsehturm", func(t *testing.T) {
lat := 52.5208 lat := 52.5208
lng := 13.40953 lng := 13.40953
@ -223,7 +223,7 @@ func TestLocation_OpenStreetMap(t *testing.T) {
} }
assert.Equal(t, "4GWF24FX+F5H", l.ID) 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) assert.Equal(t, "Eastern Cape, South Africa", l.LocRegion)
}) })
} }

View file

@ -3,45 +3,161 @@ package osm
import "fmt" import "fmt"
var locationLabels = map[string]string{ var locationLabels = map[string]string{
"bay": "bay", "aeroway=*": "airport",
"art": "art exhibition", "natural=bay": "bay",
"fire station": "fire station", "natural=peninsula": "peninsula",
"hairdresser": "hairdresser", "natural=cape": "cape",
"cape": "cape", "natural=wood": "forest",
"coastline": "coastline", "natural=grassland": "grassland",
"cliff": "cliff", "*=beach": "beach",
"wetland": "wetland", "*=dune": "dune",
"nature reserve": "nature reserve", "*=water": "water",
"natural=beach": "beach", "*=wetland": "wetland",
"amenity=cafe": "cafe", "*=glacier": "glacier",
"amenity=internet_cafe": "cafe", "*=strait": "seashore",
"ice cream": "ice cream parlor", "*=coastline": "seashore",
"bistro": "restaurant", "*=reef": "reef",
"restaurant": "restaurant", "*=geyser": "geyser",
"ship": "ship", "natural=peak": "mountain",
"wholesale": "shop", "natural=hill": "hill",
"food": "shop", "natural=volcano": "volcano",
"supermarket": "supermarket", "natural=valley": "valley",
"florist": "florist", "natural=ridge": "mountain",
"pharmacy": "pharmacy", "natural=cliff": "cliff",
"seafood": "seafood", "natural=saddle": "mountain",
"clothes": "clothing store", "natural=isthmus": "seashore",
"residential": "residential area", "natural=sinkhole": "sinkhole",
"museum": "museum", "natural=*": "nature",
"castle": "castle", "place=sea": "ocean",
"aeroway=*": "airport", "*=ocean": "ocean",
"ferry terminal": "harbor", "*=gallery": "gallery",
"bridge": "bridge", "*=museum": "museum",
"university": "university", "*=alpine_hut": "alpine hut",
"mall": "mall", "*=aquarium": "aquarium",
"marina": "marina", "*=artwork": "exhibition",
"garden": "garden", "*=camp_pitch": "camping",
"pedestrian": "shopping area", "*=camp_site": "camping",
"bunker": "bunker", "*=caravan_site": "camping",
"viewpoint": "viewpoint", "*=hotel": "hotel",
"train station": "train station", "*=hostel": "hotel",
"farm": "farm", "*=motel": "hotel",
"highway=secondary": "highway", "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) { func (o Location) Label() (result string) {

View file

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

View file

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

View file

@ -21,22 +21,22 @@ type Exif struct {
Artist string Artist string
CameraMake string CameraMake string
CameraModel string CameraModel string
Description string Description string
LensMake string LensMake string
LensModel string LensModel string
Flash bool Flash bool
FocalLength int FocalLength int
Exposure string Exposure string
Aperture float64 Aperture float64
FNumber float64 FNumber float64
Iso int Iso int
Lat float64 Lat float64
Long float64 Lng float64
Altitude int Altitude int
Width int Width int
Height int Height int
Orientation int Orientation int
All map[string]string All map[string]string
} }
var im *exif.IfdMapping 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 ifd, err := index.RootIfd.ChildWithIfdPath(exif.IfdPathStandardGps); err == nil {
if gi, err := ifd.GpsInfo(); err == nil { if gi, err := ifd.GpsInfo(); err == nil {
m.exifData.Lat = gi.Latitude.Decimal() m.exifData.Lat = gi.Latitude.Decimal()
m.exifData.Long = gi.Longitude.Decimal() m.exifData.Lng = gi.Longitude.Decimal()
m.exifData.Altitude = gi.Altitude 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{ zones, err := tz.GetZone(tz.Point{
Lat: m.exifData.Lat, Lat: m.exifData.Lat,
Lon: m.exifData.Long, Lon: m.exifData.Lng,
}) })
if err != nil { 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, 10.0, info.FNumber)
assert.Equal(t, 200, info.Iso) assert.Equal(t, 200, info.Iso)
assert.Equal(t, -33.45347, info.Lat) 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, 190, info.Altitude)
assert.Equal(t, 1365, info.Width) assert.Equal(t, 1365, info.Width)
assert.Equal(t, 0, info.Height) 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, 4.971, info.Aperture)
assert.Equal(t, 1000, info.Iso) assert.Equal(t, 1000, info.Iso)
assert.Equal(t, 0.0, info.Lat) 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, 0, info.Altitude)
assert.Equal(t, 171, info.Width) assert.Equal(t, 171, info.Width)
assert.Equal(t, 0, info.Height) 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, 1.696, jpegInfo.Aperture)
assert.Equal(t, 20, jpegInfo.Iso) assert.Equal(t, 20, jpegInfo.Iso)
assert.Equal(t, 34.79745, jpegInfo.Lat) 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.Altitude)
assert.Equal(t, 0, jpegInfo.Width) assert.Equal(t, 0, jpegInfo.Width)
assert.Equal(t, 0, jpegInfo.Height) 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() { if photoQuery.Error != nil && m.HasTimeAndPlace() {
exifData, _ = m.Exif() 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 { } else {
photoQuery = i.db.Unscoped().First(&photo, "id = ?", file.PhotoID) 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 // Read UpdateExif data
if exifData, err := m.Exif(); err == nil { if exifData, err := m.Exif(); err == nil {
photo.PhotoLat = exifData.Lat photo.PhotoLat = exifData.Lat
photo.PhotoLong = exifData.Long photo.PhotoLng = exifData.Lng
photo.TakenAt = exifData.TakenAt photo.TakenAt = exifData.TakenAt
photo.TakenAtLocal = exifData.TakenAtLocal photo.TakenAtLocal = exifData.TakenAtLocal
photo.TimeZone = exifData.TimeZone photo.TimeZone = exifData.TimeZone
@ -128,7 +128,7 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult {
labels = append(labels, locLabels...) 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 != "" { 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")) photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(labels[0].Name), m.DateCreated().Format("2006"))
} else if !photo.TakenAtLocal.IsZero() { } else if !photo.TakenAtLocal.IsZero() {
@ -160,7 +160,7 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult {
if photoExists { if photoExists {
// Estimate location // Estimate location
if o.UpdateLocation && photo.LocationID == 0 { if o.UpdateLocation && photo.LocationID == "" {
i.estimateLocation(&photo) i.estimateLocation(&photo)
} }
@ -368,12 +368,18 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
var keywords []string var keywords []string
if location, err := mediaFile.Location(); err == nil { 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.Location = location
photo.LocationID = location.ID photo.LocationID = location.ID
photo.LocationEstimated = false 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 { if photo.Country.New {
event.Publish("count.countries", event.Data{ event.Publish("count.countries", event.Data{
@ -384,7 +390,7 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
countryName := photo.Country.CountryName countryName := photo.Country.CountryName
locLabel := location.Label() locLabel := location.Label()
keywords = append(keywords, util.Keywords(location.LocDisplayName)...) keywords = append(keywords, location.Keywords()...)
// Append label from OpenStreetMap // Append label from OpenStreetMap
if locLabel != "" { if locLabel != "" {
@ -393,33 +399,27 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
} }
if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false { 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) log.Infof("index: using label \"%s\" to create photo title", title)
if location.LocCity == "" || len(location.LocCity) > 16 || strings.Contains(title, location.LocCity) { 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")) photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), countryName, photo.TakenAt.Format("2006"))
} else { } else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.LocCity, photo.TakenAt.Format("2006")) photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.LocCity, photo.TakenAt.Format("2006"))
} }
} else if location.LocName != "" && location.LocCity != "" { } else if location.Title() != "" && location.City() != "" {
if len(location.LocName) > 45 { if len(location.Title()) > 45 {
photo.PhotoTitle = util.Title(location.LocName) photo.PhotoTitle = util.Title(location.Title())
} else if len(location.LocName) > 20 || len(location.LocCity) > 16 || strings.Contains(location.LocName, location.LocCity) { } else if len(location.Title()) > 20 || len(location.City()) > 16 || strings.Contains(location.Title(), location.City()) {
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(location.LocName), photo.TakenAt.Format("2006")) photo.PhotoTitle = fmt.Sprintf("%s / %s", location.Title(), photo.TakenAt.Format("2006"))
} else { } 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 != "" { } else if location.City() != "" && countryName != "" {
if len(location.LocCity) > 20 { if len(location.City()) > 20 {
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCity, photo.TakenAt.Format("2006")) photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCity, photo.TakenAt.Format("2006"))
} else { } else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, countryName, photo.TakenAt.Format("2006")) 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 == "" { 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 return false
} }
result := !exifData.TakenAt.IsZero() && exifData.Lat != 0 && exifData.Long != 0 result := !exifData.TakenAt.IsZero() && exifData.Lat != 0 && exifData.Lng != 0
return result 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") 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") return "UTC", errors.New("no latitude and longitude in image metadata")
} }
zones, err := tz.GetZone(tz.Point{ zones, err := tz.GetZone(tz.Point{
Lon: meta.Long, Lat: meta.Lat, Lon: meta.Lng, Lat: meta.Lat,
}) })
if err != nil { if err != nil {

View file

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

View file

@ -270,9 +270,9 @@ func TestSearch_Photos_Query(t *testing.T) {
t.Logf("results: %+v", photos) 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 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.Count = 3
f.Offset = 0 f.Offset = 0