diff --git a/aec.go b/aec.go index fd9f3aa0..ba63c1c6 100644 --- a/aec.go +++ b/aec.go @@ -233,6 +233,10 @@ const ( AECgpsrangeshpbad AECgpsrangenoacc AECgpsrangelist + + // gps/scan + AECgpsscannodata + AECgpsscannoacc ) // HTTP error messages diff --git a/caches.go b/caches.go index 4b9816e5..ba4b0cd9 100644 --- a/caches.go +++ b/caches.go @@ -81,8 +81,8 @@ type PathStore struct { // Store is struct of sqlite3 item for cache with puid/T values. type Store[T any] struct { - Puid Puid_t `xorm:"pk"` - Prop T `xorm:"extends"` + Puid Puid_t `xorm:"pk" json:"puid" yaml:"puid" xml:"puid,attr"` + Prop T `xorm:"extends" json:"prop" yaml:"prop" xml:"prop"` } type ( @@ -206,7 +206,7 @@ func ExifStoreGet(session *Session, puid Puid_t) (ep ExifProp, ok bool) { ok = false return } - if !ep.IsZero() { + if ok = !ep.IsZero(); ok { ExifStoreSet(session, &ExifStore{ // update database Puid: puid, Prop: ep, @@ -218,7 +218,7 @@ func ExifStoreGet(session *Session, puid Puid_t) (ep ExifProp, ok bool) { // ExifStoreSet puts value to EXIF cache. func ExifStoreSet(session *Session, est *ExifStore) (err error) { // set to GPS cache - if est.Prop.Latitude != 0 { + if est.Prop.Latitude != 0 || est.Prop.Longitude != 0 { var gi GpsInfo gi.FromProp(&est.Prop) gpscache.Store(est.Puid, gi) @@ -254,7 +254,7 @@ func TagStoreGet(session *Session, puid Puid_t) (tp TagProp, ok bool) { ok = false return } - if !tp.IsZero() { + if ok = !tp.IsZero(); ok { TagStoreSet(session, &TagStore{ // update database Puid: puid, Prop: tp, diff --git a/frontend/devmode/cards.js b/frontend/devmode/cards.js index 4f804f9c..f15d55c1 100644 --- a/frontend/devmode/cards.js +++ b/frontend/devmode/cards.js @@ -51,6 +51,13 @@ const mm = { remove: 'remove', }; +// TypeEXIF checks that file extension belongs to images with EXIF tags. +const typeEXIF = { + ".tif": true, ".tiff": true, + ".jpg": true, ".jpe": true, ".jpeg": true, ".jfif": true, + ".png": true, ".webp": true, +}; + const gpxcolors = [ '#6495ED', // CornflowerBlue '#DA70D6', // Orchid @@ -603,7 +610,6 @@ const VueFileCard = { props: ["flist"], data() { return { - flisthub: makeeventhub(), expanded: true, sortorder: 1, sortmode: 'byalpha', @@ -621,6 +627,7 @@ const VueFileCard = { true // dir ], + flisthub: makeeventhub(), iid: makestrid(10) // instance ID }; }, @@ -766,7 +773,7 @@ const VueFileCard = { } }, methods: { - async fetchscan(flist) { + async fetchtmbscan(flist) { // not cached thumbnails const uncached = []; // get embedded thumbnails at first @@ -827,7 +834,6 @@ const VueFileCard = { throw new HttpError(response.status, response.data); } - const gpslist = []; uncached.splice(0); // refill uncached for (const tp of response.data.list) { if (tp.mime) { @@ -838,10 +844,6 @@ const VueFileCard = { } else { file.mtmb = tp.mime; // Vue.set } - // add gps-item - if (file.latitude && file.longitude && Number(file.mtmb) > 0) { - gpslist.push(file); - } break; } } @@ -849,8 +851,6 @@ const VueFileCard = { uncached.push(tp) } } - // update map card - self.$root.$refs.mcard.addmarkers(gpslist); // check cached state loop if (!uncached.length) { return; @@ -887,7 +887,7 @@ const VueFileCard = { onnewlist(newlist, oldlist) { (async () => { try { - await this.fetchscan(newlist); // fetch at backround + await this.fetchtmbscan(newlist); // fetch at backround } catch (e) { ajaxfail(e); } @@ -977,7 +977,7 @@ const VueFileCard = { (async () => { try { this.expanded = true; - await this.fetchscan(this.flist); // fetch at backround + await this.fetchtmbscan(this.flist); // fetch at backround } catch (e) { ajaxfail(e); } @@ -1203,6 +1203,7 @@ const VueTileCard = { tilemode: 'mode-246', tiles: [], sheet: [], + flisthub: makeeventhub(), iid: makestrid(10) // instance ID }; @@ -1304,7 +1305,7 @@ const VueTileCard = { } }, methods: { - async fetchscan() { + async fetchtilescan() { // not cached tiles const uncached = []; for (const tile of this.tiles) { @@ -1397,7 +1398,7 @@ const VueTileCard = { onwdhmult() { (async () => { try { - await this.fetchscan(); // fetch at backround + await this.fetchtilescan(); // fetch at backround } catch (e) { ajaxfail(e); } @@ -1410,7 +1411,7 @@ const VueTileCard = { this.flisthub.emit(null); (async () => { try { - await this.fetchscan(); // fetch at backround + await this.fetchtilescan(); // fetch at backround } catch (e) { ajaxfail(e); } @@ -1462,7 +1463,7 @@ const VueTileCard = { (async () => { try { this.expanded = true; - await this.fetchscan(); // fetch at backround + await this.fetchtilescan(); // fetch at backround } catch (e) { ajaxfail(e); } @@ -1507,6 +1508,7 @@ const VueMapCard = { props: ["flist"], data() { return { + expanded: true, isfullscreen: false, styleid: 'mapbox-hybrid', markermode: 'thumb', @@ -1525,12 +1527,14 @@ const VueMapCard = { tracks: null, gpslist: [], + flisthub: makeeventhub(), iid: makestrid(10) // instance ID }; }, watch: { flist: { handler(newlist, oldlist) { + this.flisthub.emit(null); this.onnewlist(newlist, oldlist); } }, @@ -1666,6 +1670,162 @@ const VueMapCard = { } }, methods: { + async fetchgpsscan(flist) { + const response = await fetchajaxauth("POST", "/api/gps/scan", { + aid: this.$root.aid, + list: flist.map(file => file.puid), + }); + traceajax(response); + if (!response.ok) { + throw new HttpError(response.status, response.data); + } + for (const gsi of response.data.list) { + for (const file of flist) { + if (gsi.puid === file.puid) { + file.latitude = gsi.prop.lat; + file.longitude = gsi.prop.lon; + file.altitude = gsi.prop.alt; + file.datetime = gsi.prop.time; + break; + } + } + } + }, + + async fetchtmbscan(flist) { + const mlist = []; + for (const file of flist) { + if (!file.mtmb && (file.latitude || file.longitude)) { + if (!this.hasmarker(file.puid)) { + mlist.push(file); + } + } + } + + // not cached thumbnails + const uncached = []; + // get embedded thumbnails at first + for (const file of mlist) { + if (!file.etmb && !file.mtmb) { + uncached.push({ puid: file.puid, tm: -1 }); + } + } + // then get rendered thumbnails + for (const file of mlist) { + if (!file.mtmb) { + uncached.push({ puid: file.puid, tm: 0 }); + } + } + if (!uncached.length) { + return; + } + + let stop = false; + const onnewlist = () => { + stop = true; + } + + const self = this; + const gen = (async function* () { + const response = await fetchjsonauth("POST", "/api/tile/scnstart", { + aid: self.$root.aid, + list: uncached, + }); + traceajax(response); + if (!response.ok) { + throw new HttpError(response.status, response.data); + } + + yield; + + let ul = uncached.length; + let uc = 0; + // cache folder thumnails + while (true) { + if (stop || !self.expanded) { + const response = await fetchjsonauth("POST", "/api/tile/scnbreak", { + aid: self.$root.aid, + list: uncached, + }); + traceajax(response); + if (!response.ok) { + throw new HttpError(response.status, response.data); + } + return; + } + const response = await fetchajaxauth("POST", "/api/tile/chk", { + aid: self.$root.aid, + list: uncached + }); + traceajax(response); + if (!response.ok) { + throw new HttpError(response.status, response.data); + } + + const gpslist = []; + uncached.splice(0); // refill uncached + for (const tp of response.data.list) { + if (tp.mime) { + for (const file of mlist) { + if (file.puid === tp.puid) { + if (tp.tm == -1) { + file.etmb = tp.mime; // Vue.set + } else { + file.mtmb = tp.mime; // Vue.set + } + gpslist.push(file); // add new marker + break; + } + } + } else { + uncached.push(tp) + } + } + this.addmarkers(gpslist); + // check cached state loop + if (!uncached.length) { + return; + } + // check for some files remains uncached + if (uncached.length >= ul) { + uc++; + if (uc > 9) { + // add remain markers without thumbnails as is + const gpslist = []; + for (const tp of uncached) { + for (const file of mlist) { + if (file.puid === tp.puid) { + gpslist.push(file); + break; + } + } + } + this.addmarkers(gpslist); + return; + } + } else { + ul = uncached.length; + uc = 0; + } + + yield; + } + })(); + + this.flisthub.on(null, onnewlist); + await (async () => { + while (true) { + const ret = await gen.next(); + if (ret.done) { + return; + } + // waits before new checkup iteration + await new Promise(resolve => setTimeout(resolve, 1500)); + } + })() + this.flisthub.off(null, onnewlist); + }, + // create new opened folder onnewlist(newlist, oldlist) { this.keepmap = this.$root.curpuid === PUID.map; @@ -1682,17 +1842,49 @@ const VueMapCard = { // no any gpx on map this.tracknum = 0; - // update map card with incoming files - const gpslist = []; + // prepare list of files to get coords + const gpsget = []; for (const file of newlist) { - if (file.latitude && file.longitude && Number(file.mtmb) > 0) { - gpslist.push(file); + if (file.type !== FT.file || file.latitude || file.longitude) { + continue; } - if (pathext(file.name) === ".gpx") { + const ext = pathext(file.name); + if (typeEXIF[ext]) { + gpsget.push(file); + } else if (ext === ".gpx") { this.addgpx(file); } } - this.addmarkers(gpslist); + + // update map card with incoming files + (async () => { + try { + if (gpsget.length) { + await this.fetchgpsscan(gpsget); // fetch at backround + } + const gpslist = []; + for (const file of newlist) { + if (file.mtmb && (file.latitude || file.longitude)) { + if (!this.hasmarker(file.puid)) { + gpslist.push(file); + } + } + } + this.addmarkers(gpslist); + await this.fetchtmbscan(newlist); + } catch (e) { + ajaxfail(e); + } + })(); + }, + + hasmarker(puid) { + for (const file of this.gpslist) { + if (file.puid === puid) { + return true; + } + } + return false; }, makemarkericon(file) { @@ -1701,6 +1893,8 @@ const VueMapCard = { let src = ""; if (Number(file.mtmb) > 0 && thumbmode) { src = ``; + } else if (Number(file.etmb) > 0 && thumbmode) { + src = ``; } else { for (fmt of iconmapping.iconfmt) { src += ``; @@ -1715,6 +1909,8 @@ const VueMapCard = { let src = ""; if (Number(file.mtmb) > 0 && thumbmode) { src = ``; + } else if (Number(file.etmb) > 0 && thumbmode) { + src = ``; } else { for (fmt of iconmapping.iconfmt) { src += ``; @@ -2061,8 +2257,23 @@ const VueMapCard = { } }, onexpand(e) { + (async () => { + try { + this.expanded = true; + await this.fetchtmbscan(this.flist); // fetch at backround + } catch (e) { + ajaxfail(e); + } + })(); }, oncollapse(e) { + (async () => { + try { + this.expanded = false; + } catch (e) { + ajaxfail(e); + } + })(); } }, created() { diff --git a/frontend/devmode/mainpage.js b/frontend/devmode/mainpage.js index 67f09733..9aa5f97c 100644 --- a/frontend/devmode/mainpage.js +++ b/frontend/devmode/mainpage.js @@ -999,7 +999,7 @@ const VueMainApp = { } } this.flist.push(response.data); - await this.$refs.fcard.fetchscan(); // fetch at backround + await this.$refs.fcard.fetchtmbscan(); // fetch at backround } catch (e) { ajaxfail(e); } diff --git a/gps.go b/gps.go index e1c43621..bf20d5b3 100644 --- a/gps.go +++ b/gps.go @@ -5,6 +5,8 @@ import ( "io/fs" "math" "net/http" + + "golang.org/x/sync/errgroup" ) // Haversine uses formula to calculate the great-circle distance between @@ -168,4 +170,89 @@ func gpsrangeAPI(w http.ResponseWriter, r *http.Request, auth *Profile) { WriteOK(w, r, &ret) } +// APIHANDLER +func gpsscanAPI(w http.ResponseWriter, r *http.Request) { + var err error + var arg struct { + XMLName xml.Name `json:"-" yaml:"-" xml:"arg"` + + AID ID_t `json:"aid" yaml:"aid" xml:"aid,attr"` + List []Puid_t `json:"list" yaml:"list" xml:"list>puid"` + } + var ret struct { + XMLName xml.Name `json:"-" yaml:"-" xml:"ret"` + + List []Store[GpsInfo] `json:"list" yaml:"list" xml:"list>tile"` + } + + // get arguments + if err = ParseBody(w, r, &arg); err != nil { + return + } + if len(arg.List) == 0 { + WriteError400(w, r, ErrNoData, AECgpsscannodata) + return + } + + var wg errgroup.Group + var session = xormStorage.NewSession() + defer func() { + go func() { + wg.Wait() + session.Close() + }() + }() + + var prf *Profile + if prf = prflist.ByID(arg.AID); prf == nil { + WriteError400(w, r, ErrNoAcc, AECgpsscannoacc) + return + } + var auth *Profile + if auth, err = GetAuth(w, r); err != nil { + return + } + + for _, puid := range arg.List { + var puid = puid // localize + if syspath, ok := PathStorePath(session, puid); ok { + if prf.PathAccess(syspath, auth == prf) { + if val, ok := gpscache.Load(puid); ok { + var gst = Store[GpsInfo]{ + Puid: puid, + Prop: val.(GpsInfo), + } + ret.List = append(ret.List, gst) + } else { + // check memory cache + if ok = exifcache.Has(puid); ok { + continue // there are tags without GPS + } + // try to extract from file + var ep ExifProp + if err := ep.Extract(syspath); err != nil { + continue + } + if ok = !ep.IsZero(); ok { + wg.Go(func() (err error) { + return ExifStoreSet(session, &ExifStore{ // update database + Puid: puid, + Prop: ep, + }) + }) + } + if ep.Latitude != 0 || ep.Longitude != 0 { + var gst Store[GpsInfo] + gst.Puid = puid + gst.Prop.FromProp(&ep) + ret.List = append(ret.List, gst) + } + } + } + } + } + + WriteOK(w, r, &ret) +} + // The End. diff --git a/routes.go b/routes.go index 1ca8b8ba..1134460b 100644 --- a/routes.go +++ b/routes.go @@ -536,6 +536,7 @@ func RegisterRoutes(gmux *Router) { api.Path("/edit/rename").HandlerFunc(AuthWrap(edtrenameAPI)) api.Path("/edit/delete").HandlerFunc(AuthWrap(edtdeleteAPI)) api.Path("/gps/range").HandlerFunc(AuthWrap(gpsrangeAPI)) + api.Path("/gps/scan").HandlerFunc(gpsscanAPI) } // The End.