diff --git a/adapters/sequence/index.js b/adapters/sequence/index.js index 819b1fb..13469ab 100644 --- a/adapters/sequence/index.js +++ b/adapters/sequence/index.js @@ -20,20 +20,14 @@ class Sequence { return Sequence.instance; } - addSequence (sequences, sequence) { - sequences.push({ sequenceId: uuidv4(), sequence: sequence }); + addSequence (sequences, images) { + sequences.push({ sequenceId: uuidv4(), images: images }); return sequences; } - build (source, type, minCutDist, maxCutDist, maxDelta, seqSize, userId) { + build (source, type, userId, params) { return this.fromSource(source, type).then(images => { - const params = { - maxDist: maxCutDist, - minDist: minCutDist, - maxDelta: maxDelta, - size: seqSize - } - return this.cut(flatten(images), params).then(sequences => { + return this.cut(flatten(images), params || {}).then(sequences => { if (userId) { sequences.map(sequence => { sequence.userId = userId; @@ -48,17 +42,22 @@ class Sequence { } calcDelta(date, nextDate) { - return nextDate.diff(date) / 1000; + return nextDate.diff(date) / 1000; // second difference } - // https://gist.github.com/rochacbruno/2883505 + // https://www.movable-type.co.uk/scripts/latlong.html + // response is in meters calcDistance(loc, nextLoc) { - const R = 6371e3; - const diffLat = this.toRadian(nextLoc.lat) - this.toRadian(loc.lat); - const diffLon = this.toRadian(nextLoc.lon) - this.toRadian(loc.lon); - const a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2) + Math.cos(this.toRadian(loc.lat)) * - Math.cos(this.toRadian(nextLoc.lat)) * Math.sin(diffLon / 2) * Math.sin(diffLon / 2); - + const R = 6371e3; // meters + const radLat1 = this.toRadian(loc.lat); + const radLat2 = this.toRadian(nextLoc.lat) + const radDiffLat = this.toRadian(nextLoc.lat - loc.lat); + const radDiffLon = this.toRadian(nextLoc.lon - loc.lon); + + const a = Math.sin(radDiffLat/2) * Math.sin(radDiffLat/2) + + Math.cos(radLat1) * Math.cos(radLat2) * + Math.sin(radDiffLon/2) * Math.sin(radDiffLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } @@ -79,7 +78,8 @@ class Sequence { } makeDate(tags) { - return dayjs(tags.GPSDateTime.toString()); + if (tags.GPSDateTime === undefined) console.log(tags); + return dayjs(tags.GPSDateTime.toString()) } makeLoc(tags) { @@ -91,57 +91,65 @@ class Sequence { meta(image) { return exif.read(image).then(tags => { - return { - image: image, - loc: this.makeLoc(tags), - timestamp: this.makeDate(tags), - id: uuidv4() + const spatial = !tags.hasOwnProperty('GPSLongitude') && !tags.hasOwnProperty('GPSLatitude'); + if (spatial) { + return { + image: image, + loc: this.makeLoc(tags), + timestamp: this.makeDate(tags), + id: uuidv4() + } + } else { + return {} } }) .catch(err => { throw err; }); } toRadian(coord) { - return coord * (Math.PI / 100) + return coord * (Math.PI / 180) } split(metas, params) { - const sortedMetas = metas.sort((a, b) => a.timestamp - b.timestamp); - const pelIndex = sortedMetas.length - 2; + metas = metas.sort((a, b) => a.timestamp - b.timestamp); const sequences = []; - const maxDist = params.maxDist; - const maxDelta = params.maxDelta; - const maxSize = params.maxSize; - const minDist = params.minDist; - let currentSequence = []; - - sortedMetas.slice(0 , pelIndex).forEach((meta, i) => { - const partnerMeta = sortedMetas[i + 1], - distance = this.calcDistance(meta.loc, partnerMeta.loc), - tooClose = distance < minDist; + const minDist = params.minDist || 0.5; // meters, half a meter + const maxDist = params.maxDist || 300; + const maxDelta = params.maxDelta || 120; // seconds, 2 minutes a part + const maxSize = params.maxSize || 100; + let currentImages = []; + + for (let i = 0, metasLength = metas.length; i < metasLength; i++) { + const meta = metas[i] + meta.id = uuidv4(); - // ... if image is not too close to its partner, add it to a sequence. - if (!tooClose) { - // ... if the current sequence length matches the maximum size, - // or images are too far apart (in space or time), - // add the current sequence to the sequence map, then make a new sequence. - const delta = this.calcDelta(meta.timestamp, partnerMeta.timestamp), - needNewSequence = currentSequence.length === maxSize || distance > maxDist || delta > maxDelta; - - if (needNewSequence) { - this.addSequence(sequences, currentSequence); - currentSequence = []; - - } + if (i === metasLength - 1) { + currentImages.push(meta) + } else { + + const partnerMeta = metas[i + 1]; + const distance = this.calcDistance(meta.loc, partnerMeta.loc); + const tooClose = distance < minDist; - // add a uuid then add it to the sequence! - meta.id = uuidv4(); - currentSequence.push(meta); - - } - }) + // ... if image is not too close to its partner, add it to a sequence. + if (!tooClose) { + // ... if the current sequence length matches the maximum size, + // or images are too far apart (in space or time), + // add the current sequence to the sequence map, then make a new sequence. + const delta = this.calcDelta(meta.timestamp, partnerMeta.timestamp); + const needNewSequence = currentImages.length === maxSize || distance > maxDist || delta > maxDelta; + if (needNewSequence) { + this.addSequence(sequences, currentImages); + currentImages = []; + + } + // add a uuid then add it to the sequence! + currentImages.push(meta); + } + } + } - if (currentSequence.length > 0) this.addSequence(sequences, currentSequence); + if (currentImages.length > 0) this.addSequence(sequences, currentImages); return sequences; } } diff --git a/db/index.js b/db/index.js index 3fcfdf7..35c3364 100644 --- a/db/index.js +++ b/db/index.js @@ -86,7 +86,7 @@ class Database { addSequence(sequence) { const userId = sequence.userId; const sequenceId = sequence.sequenceId; - const images = sequence.sequence; + const images = sequence.images; return this .addImages(userId, sequenceId, images) .then((res) => res) diff --git a/handlers/sequence/post.js b/handlers/sequence/post.js index dbd0424..6bd4f30 100644 --- a/handlers/sequence/post.js +++ b/handlers/sequence/post.js @@ -12,11 +12,13 @@ module.exports = async (r, h) => { const paths = r.payload; const userId = r.query.userId; const type = r.query.type; - const minCutDist = r.params.minDist || 0.5; - const maxCutDist = r.params.maxDist || 300; - const maxDelta = r.params.maxDelta || 120; - const sequenceSize = r.params.size || 0; - const sequences = await Sequence.build(paths, type, minCutDist, maxCutDist, maxDelta, sequenceSize, userId); + const params = {}; + Object.keys(r.params).forEach(k => { + if (['maxDist', 'maxDelta', 'maxSize', 'minDist'].indexOf(k) > -1) { + params[k] = r.params[k]; + } + }) + const sequences = await Sequence.build(paths, type, userId, params); Database.connect(databaseLocation); await Promise.each(sequences, (sequence) => Database.addSequence(sequence).catch(err => { throw err; })) Database.close(); diff --git a/schema/sequences.js b/schema/sequences.js index 73025a5..7c3bba7 100644 --- a/schema/sequences.js +++ b/schema/sequences.js @@ -9,6 +9,6 @@ module.exports = Joi Joi.object().keys({ userId: Joi.string().guid({ versionO: [ 'uuidv4' ]}), sequenceId: Joi.string().guid({ version: [ 'uuidv4' ] }), - sequence: Joi.array().items(metadata) + images: Joi.array().items(metadata) }) ); \ No newline at end of file diff --git a/test/sequence/adapter.js b/test/sequence/adapter.js index 051773e..b45599c 100644 --- a/test/sequence/adapter.js +++ b/test/sequence/adapter.js @@ -1,6 +1,7 @@ 'use strict' const fs = require('fs-extra'); +const uuidv4 = require('uuid/v4'); const path = require('path'); const chai = require('chai'); const Joi = require('joi'); @@ -15,6 +16,14 @@ const expect = chai.expect; Promise = require('bluebird'); describe('sequence', () => { + describe('#calcDistance', () => { + it('calculates Haverstein distance between two points', () => { + const point1 = { lat: 40, lon: -122 }; + const point2 = { lat: 40.2, lon: -122.009 }; + const dist = Sequence.calcDistance(point1, point2); + expect(dist).to.not.be.null + }) + }) it('meta reads then selializes an image\'s exif metadata', async () => { try { const image = './testData/exif-gps-samples/DSCN0010.JPG'; @@ -29,19 +38,44 @@ describe('sequence', () => { } }) - it ('given a path of images, generates a list of sequence objects', async () => { + it ('given a path of images sufficiently close together in space in time, returns sequence of same length', async () => { try { - const paths = ['/testData/exif-gps-samples', '/testData/danbjoseph'].map(p => process.cwd() + p); + const paths = [ '/testData/danbjoseph2' ].map(p => process.cwd() + p); const sequences = await Sequence.build(paths, 'directory'); const validation = Joi.validate(sequences, sequencesSchema); expect(validation.value).to.be.eql(sequences) expect(validation.error).to.be.null; - return; + + expect(sequences[0].images.length).to.eql(8); + } catch (e) { console.error(e); } }) - .timeout(10000000) + // it('given path with images less than max size, makes one big sequence', async () => { + // try { + // const paths = ['/testData/100MSDCF'].map(p => process.cwd() + p); + // const sequences = await Sequence.build(paths, 'directory', uuidv4(), { maxSize: 10000 }); + // expect(sequences.length).to.eql(1); + // } catch (e) { + // console.log(e); + // } + // }) + // .timeout(10000000) + // it('given multiple image paths, makes multiple sequenecs', async () => { + // try { + // const paths = ['/testData/100MSDCF', '/testData/103MSDCF'].map(p => process.cwd() + p); + // const sequences = await Sequence.build(paths, 'directory', uuidv4(), { maxSize: 1000 }); + // const validation = Joi.validate(sequences, sequencesSchema); + + // expect(validation.value).to.be.eql(sequences) + // expect(validation.error).to.be.null; + + // } catch (e) { + // console.log(e); + // } + // }) + // .timeout(10000000) }) \ No newline at end of file