diff --git a/.gitignore b/.gitignore index afa45d9..2f4e346 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ testData/ .vscode node_modules/ yarn-error.log +coverage/ +.nyc_output/ diff --git a/.travis.yml b/.travis.yml index 4a4cf84..0ef341e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ services: - docker before_install: - - npm run clean-migrations && docker build -t posm-paths . + - npm run migrate:clean && docker build -t posm-paths . script: - - docker run posm-paths /bin/bash -c 'npm install --build-from-sources && SPATIALITE_LOCATION=/usr/lib/x86_64-linux-gnu ENVIRONMENT=test npm run test:migrate' \ No newline at end of file + - docker run posm-paths /bin/bash -c 'npm install --build-from-sources && SPATIALITE_LOCATION=/usr/lib/x86_64-linux-gnu ENVIRONMENT=test npm run test' diff --git a/adapters/sequence/README.md b/adapters/sequence/README.md deleted file mode 100644 index 63b5fc5..0000000 --- a/adapters/sequence/README.md +++ /dev/null @@ -1,3 +0,0 @@ -- make a new guy if the # is too large for a sequence -- if the distnace is too close, skip adding that image. -- or if when checking distance and time, one of them is greater than the max \ No newline at end of file diff --git a/adapters/sequence/build.js b/adapters/sequence/build.js deleted file mode 100644 index 958af13..0000000 --- a/adapters/sequence/build.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const meta = require('./meta'); -const split = require('./split'); - -Promise = require('bluebird'); - -module.exports = (images, params) => { - return Promise.map(images, async (image) => await meta(image)) - .then(async metas => await split(metas, params)) - .catch(e => { throw e; }) - -} \ No newline at end of file diff --git a/adapters/sequence/helpers.js b/adapters/sequence/helpers.js deleted file mode 100644 index d452c18..0000000 --- a/adapters/sequence/helpers.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -const dayjs = require('dayjs') -const uuidv4 = require('uuid/v4'); - -/** - * returns euclidean distance between two img locs. - * - * @param {object} metaLoc current img's loc - * @param {object} nextMeta next img's loc - * @return {number} euclidean distance between two points - */ -exports.calcDistance = (metaLoc, nextMetaLoc) => { - var R = 6371e3, - latRadians = [metaLoc.lat, nextMetaLoc.lat].map(lat => lat * (Math.PI / 180)), - latDiffs = (latRadians[0] - latRadians[1]) * (Math.PI / 180), - lonDiffs = (metaLoc.lon - nextMetaLoc.lon) * (Math.PI / 180), - - a = Math.pow(Math.sin(lonDiffs / 2), 2) + - (Math.cos(latRadians[0]) * Math.cos(latRadians[1]) * Math.pow(lonDiffs/2, 2)), - - c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return R * c; -} - -/** - * given two timestamp object, calculates the milisecond delta - * - * @param {string} metaDate current img's timestamp - * @param {string} nextMetaDate next img's timestamp - * @return {number} milisecond diff between two dates.ee - * - */ -exports.calcDelta = (metaDate, nextMetaDate) => nextMetaDate.diff(metaDate) / 1000; - -/** - * given a sequence map, adds a new sequence array and returns the map. - * - * @param {object} sequenceMap current sequence map - * @param {array} sequence sequence to be added to sequenceMap - * @return {object} updated sequenceMap - */ -exports.addSequence = (sequences, sequence) => { - sequences.push({ - sequenceId: uuidv4(), - sequence: sequence - }) - return sequences; -}; \ No newline at end of file diff --git a/adapters/sequence/index.js b/adapters/sequence/index.js index 490866c..819b1fb 100644 --- a/adapters/sequence/index.js +++ b/adapters/sequence/index.js @@ -1,52 +1,151 @@ 'use strict'; -const fs = require('fs-extra'); +Promise = require('bluebird') + +const readdir = require('fs-extra').readdir; const path = require('path'); -const buildSequences = require('./build'); +const { ExifTool } = require('exiftool-vendored'); +const exif = new ExifTool(); +const uuidv4 = require('uuid/v4'); +const dayjs = require('dayjs'); const flatten = require('../../helpers').flatten; -Promise = require('bluebird') +let instance; -/** - * - * @param {array} paths list of directory paths holding images to make sequences of - * @param {number} maxCutDist maximum distance allowed between two photos - * @param {number} minCutDist minimum distance allowed between two photos - * @param {number} cutTime maximum time between two images - * @param {number} cutSize maximum size of a sequence. - * @param {string} userId (optional) userId that when present attached to each sequence - * @return {array} array of sequence configuration objects - */ - -module.exports = (paths, minCutDist, maxCutDist, maxDelta, sequenceSize, userId) => { - return new Promise((resolve, reject) => { - Promise.map(paths, async (p) => { - const images = await fs.readdir(p); - return images.map(f => path.join(p, f)); - }) - .then(async (images) => { - try { - const params = { - maxDist: maxCutDist, - minDist: minCutDist, - maxDelta: maxDelta, - size: sequenceSize - }; - - let sequences = await buildSequences(flatten(images), params); - if (userId) { - sequences = sequences.map(sequence => { - sequence.userId = userId - return sequence - }); +class Sequence { + constructor () { + if (!Sequence.instance) { + Sequence.instance = this; + } + return Sequence.instance; + } + + addSequence (sequences, sequence) { + sequences.push({ sequenceId: uuidv4(), sequence: sequence }); + return sequences; + } + + build (source, type, minCutDist, maxCutDist, maxDelta, seqSize, userId) { + 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 => { + if (userId) { + sequences.map(sequence => { + sequence.userId = userId; + return sequence; + }) } + return sequences + }) + .catch((err) => { throw err; }) + }) + .catch((err) => { throw err; }) + } + + calcDelta(date, nextDate) { + return nextDate.diff(date) / 1000; + } - resolve(sequences); - } catch (e) { - reject(e); + // https://gist.github.com/rochacbruno/2883505 + 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 c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + cut(images, params) { + return Promise + .map(images, (image) => this.meta(image)) + .then(metas => this.split(metas, params)) + } + + fromSource(source, type) { + if (type === 'directory') { + return Promise.map(source, (dir) => { + return readdir(dir) + .then(files => files.map(f => path.join(dir, f))) + }) + } + } + + makeDate(tags) { + return dayjs(tags.GPSDateTime.toString()); + } + + makeLoc(tags) { + return { + lon: Number(tags.GPSLongitude), + lat: Number(tags.GPSLatitude) + } + } + + meta(image) { + return exif.read(image).then(tags => { + return { + image: image, + loc: this.makeLoc(tags), + timestamp: this.makeDate(tags), + id: uuidv4() } + }) + .catch(err => { throw err; }); + } + toRadian(coord) { + return coord * (Math.PI / 100) + } + + split(metas, params) { + const sortedMetas = metas.sort((a, b) => a.timestamp - b.timestamp); + const pelIndex = sortedMetas.length - 2; + 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; + + // ... 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 = []; + + } + + // add a uuid then add it to the sequence! + meta.id = uuidv4(); + currentSequence.push(meta); + + } }) - }) -} \ No newline at end of file + + if (currentSequence.length > 0) this.addSequence(sequences, currentSequence); + return sequences; + } +} + +instance = new Sequence(); +Object.freeze(instance); +module.exports = instance; \ No newline at end of file diff --git a/adapters/sequence/make.js b/adapters/sequence/make.js deleted file mode 100644 index 0215901..0000000 --- a/adapters/sequence/make.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const dayjs = require('dayjs') - -exports.date = (tags) => dayjs(tags.GPSDateTime.toString()); - -exports.loc = (tags) => { - return { - lat: Number(tags.GPSLatitude), - lon: Number(tags.GPSLongitude) - } -} \ No newline at end of file diff --git a/adapters/sequence/meta.js b/adapters/sequence/meta.js deleted file mode 100644 index 7260104..0000000 --- a/adapters/sequence/meta.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const { ExifTool } = require('exiftool-vendored'); -const exif = new ExifTool(); -const uuidv4 = require('uuid/v4'); -const makeLoc = require('./make').loc; -const makeDate = require('./make').date; - -Promise = require('bluebird'); - -/** - * Given an image path, replies object with relevant metadata - * - * @param {string} image path to image - * @return {object} object holding an image's geographic, time, and fs path metadata - */ -module.exports = (image) => { - return new Promise((resolve, reject) => - exif.read(image) - .then(tags => { - resolve({ - image: image, - loc: makeLoc(tags), - timestamp: makeDate(tags), - id: uuidv4() - }); - }) - .catch(e => reject(e)) - ) -} \ No newline at end of file diff --git a/adapters/sequence/sequence.js b/adapters/sequence/sequence.js deleted file mode 100644 index 78497b6..0000000 --- a/adapters/sequence/sequence.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -Promise = require('bluebird') - -const readdir = require('fs-extra').readdir; -const path = require('path'); -const ExifTool = require('exiftool-vendored'); -const exif = new ExifTool(); -const uuidv4 = require('uuid/v4'); -const dayjs = require('dayjs'); - -class Sequence { - constructor () {} - - build (source, type, minCutDist, maxCutDist, maxDelta, seqSize, userId) { - return fromSource(source, type).then(images => { - const params = { - maxDist: maxCutDist, - minDist: minCutDist, - maxDelta: maxDelta, - size: seqSize - } - return this.cut(flatten(images), params).then(sequences => { - return sequences.map(sequence => { - sequence.userId = userId; - return sequence; - }) - .catch((err) => { throw err; }); - }) - .catch((err) => { throw err; }) - }) - .catch((err) => { throw err; }) - } - - cut(images, params) { - return Promise - .map(images, (image) => meta(image)) - .then(metas => split(metas, params)) - } - - fromSource(source, type) { - if (type === 'directory') { - return Promise.map(source, (dir) => { - return readdir(dir) - .then(files => files.map(f => path.join(p, f))) - }) - } - } - - makeDate() { - return dayjs(tags.GPSDateTime.toString()); - } - - makeLoc(tags) { - return { - lon: Number(tags.GPSLongitude), - lat: Number(tags.GPSLatitude) - } - } - - meta(image) { - return exif.read(image).then(tags => { - return { - image: image, - loc: makeLoc(tags), - timestamp: makeDate(tags), - id: uuidv4() - } - }) - .catch(err => { throw err; }); - } - - split(metas, params) { - const sortedMetas = metas.sort((a, b) => a.timestamp - b.timestamp); - const pelIndex = sortedMetas.length - 2; - 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 = calcDistance(meta.loc, partnerMeta.loc), - tooClose = distance < minDist; - - // ... 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 = calcDelta(meta.timestamp, partnerMeta.timestamp), - needNewSequence = currentSequence.length === maxSize || distance > maxDist || delta > maxDelta; - - if (needNewSequence) { - addSequence(sequences, currentSequence); - currentSequence = []; - - } - - // add a uuid then add it to the sequence! - meta.id = uuidv4(); - currentSequence.push(meta); - - } - }) - - if (currentSequence.length > 0) addSequence(sequences, currentSequence); - return sequences; - } -} - -module.exports = Sequence; \ No newline at end of file diff --git a/adapters/sequence/split.js b/adapters/sequence/split.js deleted file mode 100644 index f253aed..0000000 --- a/adapters/sequence/split.js +++ /dev/null @@ -1,60 +0,0 @@ - -'use strict'; - -const calcDistance = require('./helpers').calcDistance; -const calcDelta = require('./helpers').calcDelta; -const addSequence = require('./helpers').addSequence -const uuidv4 = require('uuid/v4'); - - -Promise = require('bluebird'); - -/** - * provided list of image metadata objects and thresholds used to define sequences, - * build and reply an object of image sequences - * - * @param metas {list} list of image metadata objects - * @param params {object} map of user (or default) tresholds used to build sequences - * @return {object} map of generated image sequences - * - */ -module.exports = (metas, params) => { - const sortedMetas = metas.sort((a, b) => a.timestamp - b.timestamp), - pelIndex = sortedMetas.length - 2, - sequences = [], - maxDist = params.maxDist, - maxDelta = params.maxDelta, - maxSize = params.maxSize, - minDist = params.minDist; - - let currentSequence = []; - // for the 1st to 2nd to last meta... - sortedMetas.slice(0, pelIndex).forEach((meta, i) => { - const partnerMeta = sortedMetas[i + 1], - distance = calcDistance(meta.loc, partnerMeta.loc), - tooClose = distance < minDist; - - // ... 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 = calcDelta(meta.timestamp, partnerMeta.timestamp), - needNewSequence = currentSequence.length === maxSize || distance > maxDist || delta > maxDelta; - - if (needNewSequence) { - addSequence(sequences, currentSequence); - currentSequence = []; - - } - - // add a uuid then add it to the sequence! - meta.id = uuidv4(); - currentSequence.push(meta); - - } - }) - - if (currentSequence.length > 0) addSequence(sequences, currentSequence); - return sequences; -}; \ No newline at end of file diff --git a/db/index.js b/db/index.js index 68ae08c..40ad4bd 100644 --- a/db/index.js +++ b/db/index.js @@ -81,11 +81,11 @@ class Database { .then((result) => result) .catch((err) => { throw err; }) } - addSequence(userId, imageIds) { + addSequence(sequence) { const values = `( '${uuidv4()}', '${userId}', - json('${JSON.stringify(imageIds)}') + )`; const sql = `INSERT INTO Sequences VALUES ${values};`; return this diff --git a/handlers/sequence/post.js b/handlers/sequence/post.js index 2ab02a7..4dc9ffa 100644 --- a/handlers/sequence/post.js +++ b/handlers/sequence/post.js @@ -1,27 +1,21 @@ 'use strict'; const Boom = require('boom'); -const uuidv4 = require('uuid/v4'); -const buildSequences = require('../../adapters/sequence'); -const insertImagesSequences = require('./helpers').insertImagesSequence; +const Sequence = require('../../adapters/sequence'); + +Promise = require('bluebird'); module.exports = async (r, h) => { try { - const paths = r.payload, - userId = r.query.userId, - minCutDist = r.params.minDist || 0.5, - maxCutDist = r.params.maxDist || 300, - maxDelta = r.params.maxDelta || 120, - sequenceSize = r.params.size || 0, - sequences = await buildSequences(paths, minCutDist, maxCutDist, maxDelta, sequenceSize, userId); + const paths = r.payload; + const userId = r.query.userId; + 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, minCutDist, maxCutDist, maxDelta, sequenceSize, userId); - sequences.forEach(async (sequence) => { - try { - await insertImagesSequences(sequence) - } catch (e) { - throw e; - } - }) + await Promise.each(sequences, (sequence) => Database.addSequence(sequence).catch(err => { throw err; })) return h.response({ upload: 'successful' }).code(200); diff --git a/schema/sequences.js b/schema/sequences.js index 1d54024..73025a5 100644 --- a/schema/sequences.js +++ b/schema/sequences.js @@ -7,7 +7,8 @@ module.exports = Joi .array() .items( Joi.object().keys({ - sequenceId: Joi.string().regex(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i), + userId: Joi.string().guid({ versionO: [ 'uuidv4' ]}), + sequenceId: Joi.string().guid({ version: [ 'uuidv4' ] }), sequence: Joi.array().items(metadata) }) ); \ No newline at end of file diff --git a/test/sequence/adapter.js b/test/sequence/adapter.js index cb0cbd0..051773e 100644 --- a/test/sequence/adapter.js +++ b/test/sequence/adapter.js @@ -9,9 +9,7 @@ const Joi = require('joi'); const metadataSchema = require('../../schema').metadata; const sequencesSchema = require('../../schema').sequences; -/* seqeunce adapter components */ -const meta = require('../../adapters/sequence/meta'); -const sequenceAdapter = require('../../adapters/sequence'); +const Sequence = require('../../adapters/sequence'); const expect = chai.expect; Promise = require('bluebird'); @@ -20,7 +18,7 @@ describe('sequence', () => { it('meta reads then selializes an image\'s exif metadata', async () => { try { const image = './testData/exif-gps-samples/DSCN0010.JPG'; - const metadata = await meta(image) + const metadata = await Sequence.meta(image) const validation = Joi.validate(metadata, metadataSchema); expect(validation.value).to.be.eql(metadata); @@ -34,7 +32,7 @@ describe('sequence', () => { it ('given a path of images, generates a list of sequence objects', async () => { try { const paths = ['/testData/exif-gps-samples', '/testData/danbjoseph'].map(p => process.cwd() + p); - const sequences = await sequenceAdapter(paths); + const sequences = await Sequence.build(paths, 'directory'); const validation = Joi.validate(sequences, sequencesSchema); expect(validation.value).to.be.eql(sequences) diff --git a/test/sequence/handler.js b/test/sequence/handler.js index 6cb5a74..942fd4e 100644 --- a/test/sequence/handler.js +++ b/test/sequence/handler.js @@ -1,44 +1,44 @@ 'use strict'; -const Joi = require('joi'); -const chai = require('chai'); -const expect = chai.expect; +// const Joi = require('joi'); +// const chai = require('chai'); +// const expect = chai.expect; -const uuidv4 = require('uuid/v4'); +// const uuidv4 = require('uuid/v4'); -const server = require('../server'); -const mergeDefaults = require('../helpers').mergeDefaults; -const users = require('../../db/seeds/users'); -const danbjoseph = users[0]; -const routes = [ - // require('../../routes/sequence').get, - require('../../routes/sequence').post -]; +// const server = require('../server'); +// const mergeDefaults = require('../helpers').mergeDefaults; +// const users = require('../../db/seeds/users'); +// const danbjoseph = users[0]; +// const routes = [ +// // require('../../routes/sequence').get, +// require('../../routes/sequence').post +// ]; -const db = require('../../connection'); +// const db = require('../../connection'); -before(async () => await server.liftOff(routes)) -describe('post', () => { - it('replies 200 when sequence post is successful', async () => { - try { - const request = mergeDefaults({ - method: 'POST', - payload: ['/testData/danbjoseph'].map(p => process.cwd() + p), - url: `/sequence?userId=${danbjoseph.id}` - }), - r = await server.inject(request), - statusCode = r.statusCode; +// before(async () => await server.liftOff(routes)) +// describe('post', () => { +// it('replies 200 when sequence post is successful', async () => { +// try { +// const request = mergeDefaults({ +// method: 'POST', +// payload: ['/testData/danbjoseph'].map(p => process.cwd() + p), +// url: `/sequence?userId=${danbjoseph.id}` +// }), +// r = await server.inject(request), +// statusCode = r.statusCode; - expect(statusCode).to.be.eql(200); +// expect(statusCode).to.be.eql(200); - } catch (error) { - console.error(error); +// } catch (error) { +// console.error(error); - } - }) +// } +// }) // it('replies 400 when unsuccessful because user already in db', async () => { // }) -}) +// }) // describe('get', () => { // it ('replies 200 and username when given valid uuid', async () => { // }) diff --git a/test/sequence/index.js b/test/sequence/index.js index c089f4a..0935612 100644 --- a/test/sequence/index.js +++ b/test/sequence/index.js @@ -2,5 +2,5 @@ module.exports = { // handlerSpec: require('./handler'), - // adapterSpec: require('./adapter') + adapterSpec: require('./adapter') } \ No newline at end of file