feat: add gps data editor (close #418)

pull/461/head
Varun Patil 2023-03-08 10:22:36 -08:00
parent 608e8556d8
commit ffd105eac6
6 changed files with 230 additions and 6 deletions

View File

@ -133,6 +133,9 @@ class PageController extends Controller
$addImageDomain('https://*.tile.openstreetmap.org'); $addImageDomain('https://*.tile.openstreetmap.org');
$addImageDomain('https://*.a.ssl.fastly.net'); $addImageDomain('https://*.a.ssl.fastly.net');
// Allow Nominatim
$policy->addAllowedConnectDomain('nominatim.openstreetmap.org');
return $policy; return $policy;
} }

View File

@ -163,6 +163,8 @@ export default defineComponent({
subtitle: [], subtitle: [],
icon: LocationIcon, icon: LocationIcon,
href: this.mapFullUrl, href: this.mapFullUrl,
edit: () =>
globalThis.editMetadata([globalThis.currentViewerPhoto], [4]),
}); });
} }

View File

@ -7,10 +7,13 @@
<NcTextField <NcTextField
class="field" class="field"
:id="'exif-field-' + field.field" :id="'exif-field-' + field.field"
:value.sync="exif[field.field]"
:label-outside="true" :label-outside="true"
:value.sync="exif[field.field]"
:placeholder="placeholder(field)" :placeholder="placeholder(field)"
@input="dirty[field.field] = true" @input="dirty[field.field] = true"
trailing-button-icon="close"
:show-trailing-button="dirty[field.field]"
@trailing-button-click="reset(field)"
/> />
</div> </div>
</div> </div>
@ -119,6 +122,11 @@ export default defineComponent({
? t("memories", "Empty") ? t("memories", "Empty")
: t("memories", "Unchanged"); : t("memories", "Unchanged");
}, },
reset(field: any) {
this.exif[field.field] = "";
this.dirty[field.field] = false;
},
}, },
}); });
</script> </script>

View File

@ -0,0 +1,194 @@
<template>
<div class="outer">
<div class="lat-lon">
<span>{{ loc }}</span> {{ dirty ? "*" : "" }}
</div>
<NcTextField
:value.sync="searchBar"
:placeholder="t('memories', 'Search location / landmark')"
trailing-button-icon="arrowRight"
:show-trailing-button="searchBar.length > 0 && !loading"
@trailing-button-click="search"
@keypress.enter="search"
>
<Magnify :size="16" />
</NcTextField>
<NcLoadingIcon class="loading-spinner" v-if="loading" />
<ul v-if="options.length > 0">
<li
v-for="option in options"
:key="option.osm_id"
@click="select(option)"
>
{{ option.display_name }}
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IPhoto } from "../../types";
import axios from "@nextcloud/axios";
import { showError } from "@nextcloud/dialogs";
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
const NcListItem = () => import("@nextcloud/vue/dist/Components/NcListItem");
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
import Magnify from "vue-material-design-icons/Magnify.vue";
type NLocation = {
osm_id: number;
type: string;
icon: string;
display_name: string;
lat: string;
lon: string;
};
export default defineComponent({
components: {
NcTextField,
NcListItem,
NcLoadingIcon,
Magnify,
},
props: {
photos: {
type: Array<IPhoto>,
required: true,
},
},
data: () => ({
dirty: false,
lat: null as number | null,
lon: null as number | null,
searchBar: "",
loading: false,
options: [] as NLocation[],
}),
computed: {
loc() {
if (this.lat && this.lon) {
return `${this.lat.toFixed(6)}, ${this.lon.toFixed(6)}`;
}
return this.t("memories", "Unknown coordinates");
},
},
mounted() {
const photos = this.photos as IPhoto[];
let lat = 0,
lon = 0,
count = 0;
for (const photo of photos) {
const exif = photo.imageInfo?.exif;
if (!exif) {
continue;
}
if (exif.GPSLatitude && exif.GPSLongitude) {
lat += Number(exif.GPSLatitude);
lon += Number(exif.GPSLongitude);
count++;
}
}
if (count > 0) {
this.lat = lat / count;
this.lon = lon / count;
}
},
methods: {
search() {
if (this.loading || this.searchBar.length === 0) {
return;
}
this.loading = true;
const q = window.encodeURIComponent(this.searchBar);
axios
.get(
`https://nominatim.openstreetmap.org/search.php?q=${q}&format=jsonv2`
)
.then((response) => {
this.loading = false;
this.options = response.data.filter((x: NLocation) => {
return x.lat && x.lon && x.display_name;
});
})
.catch((error) => {
this.loading = false;
console.error(error);
showError(
this.t("memories", "Failed to search for location with Nominatim.")
);
});
},
select(option: NLocation) {
this.dirty = true;
this.lat = Number(option.lat);
this.lon = Number(option.lon);
this.options = [];
this.searchBar = "";
},
result() {
if (!this.dirty || !this.lat || !this.lon) {
return null;
}
return {
GPSLatitude: this.lat,
GPSLongitude: this.lon,
};
},
},
});
</script>
<style scoped lang="scss">
.outer {
margin-bottom: 10px;
.lat-lon {
padding: 4px;
> span {
user-select: all;
}
}
.loading-spinner {
margin: 10px;
}
ul {
margin: 10px 0;
max-height: 200px;
overflow-y: auto;
li {
font-size: 0.9em;
padding: 5px 10px;
margin: 2px 0;
cursor: pointer;
&:hover {
background-color: var(--color-background-hover);
}
}
}
}
</style>

View File

@ -37,6 +37,13 @@
</div> </div>
<EditExif ref="editExif" :photos="photos" /> <EditExif ref="editExif" :photos="photos" />
</div> </div>
<div v-if="sections.includes(4)">
<div class="title-text">
{{ t("memories", "Geolocation") }}
</div>
<EditLocation ref="editLocation" :photos="photos" />
</div>
</div> </div>
<div v-if="processing"> <div v-if="processing">
@ -56,9 +63,10 @@ const NcProgressBar = () =>
import("@nextcloud/vue/dist/Components/NcProgressBar"); import("@nextcloud/vue/dist/Components/NcProgressBar");
import Modal from "./Modal.vue"; import Modal from "./Modal.vue";
import EditExif from "./EditExif.vue";
import EditDate from "./EditDate.vue"; import EditDate from "./EditDate.vue";
import EditTags from "./EditTags.vue"; import EditTags from "./EditTags.vue";
import EditExif from "./EditExif.vue";
import EditLocation from "./EditLocation.vue";
import { showError } from "@nextcloud/dialogs"; import { showError } from "@nextcloud/dialogs";
import { emit } from "@nextcloud/event-bus"; import { emit } from "@nextcloud/event-bus";
@ -74,9 +82,10 @@ export default defineComponent({
NcProgressBar, NcProgressBar,
Modal, Modal,
EditExif,
EditDate, EditDate,
EditTags, EditTags,
EditExif,
EditLocation,
}, },
mixins: [UserConfig], mixins: [UserConfig],
@ -95,7 +104,7 @@ export default defineComponent({
this.$emit("refresh", val); this.$emit("refresh", val);
}, },
async open(photos: IPhoto[], sections: number[] = [1, 2, 3]) { async open(photos: IPhoto[], sections: number[] = [1, 2, 3, 4]) {
const state = (this.state = Math.random()); const state = (this.state = Math.random());
this.show = true; this.show = true;
this.processing = true; this.processing = true;
@ -164,7 +173,10 @@ export default defineComponent({
} }
// Get exif fields diff // Get exif fields diff
const exifResult = (<any>this.$refs.editExif)?.result?.() || {}; const exifResult = {
...((<any>this.$refs.editExif)?.result?.() || {}),
...((<any>this.$refs.editLocation)?.result?.() || {}),
};
const tagsResult = (<any>this.$refs.editTags)?.result?.() || null; const tagsResult = (<any>this.$refs.editTags)?.result?.() || null;
// Start processing // Start processing

View File

@ -6,7 +6,9 @@
:label="t('memories', 'Search')" :label="t('memories', 'Search')"
:placeholder="t('memories', 'Search')" :placeholder="t('memories', 'Search')"
@input="searchChanged" @input="searchChanged"
/> >
<Magnify :size="16" />
</NcTextField>
</div> </div>
<div class="photo" v-for="photo of detail" :key="photo.fileid"> <div class="photo" v-for="photo of detail" :key="photo.fileid">
@ -28,11 +30,14 @@ import NcTextField from "@nextcloud/vue/dist/Components/NcTextField";
import * as dav from "../../services/DavRequests"; import * as dav from "../../services/DavRequests";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import Magnify from "vue-material-design-icons/Magnify.vue";
export default defineComponent({ export default defineComponent({
name: "FaceList", name: "FaceList",
components: { components: {
Tag, Tag,
NcTextField, NcTextField,
Magnify,
}, },
data: () => ({ data: () => ({