diff --git a/.eslintrc.js b/.eslintrc.js index ae235cb..387f733 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { "env": { "browser": true, - "es2017": true, + "es2018": true, "node": true, "mocha": true }, diff --git a/app/app.js b/app/app.js index 4088f49..84ab8e3 100644 --- a/app/app.js +++ b/app/app.js @@ -97,6 +97,21 @@ const start = async function () { }); + // CORS + app.use(function (req, res, next) { + // Website you wish to allow to connect + res.setHeader('Access-Control-Allow-Origin', '*'); // 'http://localhost:8888'); + // Request methods you wish to allow + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); + // Request headers you wish to allow + res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + // Set to true if you need the website to include cookies in the requests sent + // to the API (e.g. in case you use sessions) + res.setHeader('Access-Control-Allow-Credentials', true); + // Pass to next layer of middleware + next(); + }); + // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); if (process.env.NODE_ENV !== 'production') { @@ -106,7 +121,6 @@ const start = async function () { app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(express.static(path.join(__dirname, 'public'))); - /* setup DB */ // var checkAnyoneUser = function () { diff --git a/app/controller/api/api.spec.js b/app/controller/api/api.spec.js index 426bef6..826b163 100644 --- a/app/controller/api/api.spec.js +++ b/app/controller/api/api.spec.js @@ -1,606 +1,602 @@ -const assert = require('assert') -const chai = require('chai') +/* eslint-disable max-lines */ +// disable eslint rule no-unused-expressions to work with chai +/* eslint-disable no-unused-expressions */ +const assert = require('assert'); +const chai = require('chai'); const bodyParser = require('body-parser'); -const chaiHttp = require('chai-http') -const expect = chai.expect -const sinon = require('sinon') -const fs = require('fs') +const chaiHttp = require('chai-http'); +const {expect} = chai; +const sinon = require('sinon'); +const fs = require('fs'); -const express = require('express') -const app = express() +const express = require('express'); +const app = express(); -function buildFileID({source, slice}) { - return `${source}&slice=${slice}`; -} -app.use(bodyParser.urlencoded({ extended: true })) +app.use(bodyParser.urlencoded({ extended: true })); /** * simulate authenication status * testing authentication is not the aim of this test suite * these tests only check if api works as intended */ -let authenticatedUser = null +let authenticatedUser = null; const USER_ANONYMOUSE = { - username: 'anyone' -} + username: 'anyone' +}; const USER_BOB = { - username: 'bob' -} + username: 'bob' +}; const USER_ALICE = { - username: 'alice' -} -const USER_CINDY = { - username: 'cindy' -} + username: 'alice' +}; +// const USER_CINDY = { +// username: 'cindy' +// }; app.use((req, res, next) => { - if (authenticatedUser) { - req.user = authenticatedUser - } - next() -}) -app.use(require('./index')) + if (authenticatedUser) { + req.user = authenticatedUser; + } + next(); +}); +app.use(require('./index')); -chai.use(chaiHttp) +chai.use(chaiHttp); describe('Mocha works', () => { - it('mocha works in api.spec.js', () => { - assert.strictEqual(1, 1) - }) -}) + it('mocha works in api.spec.js', () => { + assert.strictEqual(1, 1); + }); +}); describe('sinon works', () => { - it('fake called works', () => { - const fake = sinon.fake() - expect(fake.called).to.be.false - fake() - expect(fake.called).to.be.true - }) - - it('fake calls with arguements works', () => { - const fake = sinon.fake() - const arg = { - hello: 'world' - } - fake(arg) - assert(fake.calledWith({ - hello: 'world' - })) - }) - - it('stub can dynamically change return val', () => { - const stub = sinon.stub() - let flag = true - stub.callsFake(() => flag) - expect(stub()).to.equal(true) - - flag = false - expect(stub()).to.equal(false) - }) -}) + it('fake called works', () => { + const fake = sinon.fake(); + expect(fake.called).to.be.false; + fake(); + expect(fake.called).to.be.true; + }); + + it('fake calls with arguements works', () => { + const fake = sinon.fake(); + const arg = { + hello: 'world' + }; + fake(arg); + assert(fake.calledWith({ + hello: 'world' + })); + }); + + it('stub can dynamically change return val', () => { + const stub = sinon.stub(); + let flag = true; + stub.callsFake(() => flag); + expect(stub()).to.equal(true); + + flag = false; + expect(stub()).to.equal(false); + }); +}); describe('controller/api/index.js', () => { - const annotationInDb = { - Regions: [{annotation: {uid: 'abc'}}, {annotation: {uid: 'cde'}}] - } - let _server, - queryPublicProject = false, - publicProject = { - owner: USER_BOB.username, - collaborators: { - list: [ USER_ALICE, USER_BOB, USER_ANONYMOUSE ] - } - }, - privateProject = { - owner: USER_ALICE.username, - collaborators: { - list: [ USER_ALICE, USER_BOB ] - } - }, - port = 10002, - url = `http://127.0.0.1:${port}`, - - returnFoundAnnotation = true, - updateAnnotations = sinon.stub().resolves(), - insertAnnotations = sinon.stub().resolves(), - findAnnotations = sinon.stub().callsFake(() => Promise.resolve( returnFoundAnnotation ? annotationInDb : {Regions: []} )), - queryProject = sinon.stub().callsFake(() => Promise.resolve(queryPublicProject ? publicProject : privateProject)) - - before(() => { - app.db = { - updateAnnotations, - insertAnnotations, - findAnnotations, - queryProject - } - _server = app.listen(port, () => console.log(`mocha test listening at ${port}`)) - }) - - beforeEach(() => { - updateAnnotations.resetHistory() - insertAnnotations.resetHistory() - findAnnotations.resetHistory() - queryProject.resetHistory() - }) - - after(() => { - _server.close() - }) - - describe('saveFromGUI', () => { - - describe('/GET', () => { - - describe('public project', () => { - before(() => { - queryPublicProject = true - }) - - it('if user not part of project, should return 200', (done) => { - authenticatedUser = null - chai.request(url) - .get('/') - .query({ - project: 'testProject' - }) - .end((err, res) => { - expect(!!err).to.be.false - expect(res).to.have.status(200) - done() - }) - }) - - it('if user is part of project, should return status 200', (done) => { - authenticatedUser = USER_BOB - chai.request(url) - .get('/') - .query({ - project: 'testProject' - }) - .end((err, res) => { - expect(!!err).to.be.false - expect(res).to.have.status(200) - done() - }) - }) + const annotationInDb = [{annotation: {uid: 'abc'}}, {annotation: {uid: 'cde'}}]; + let _server, + queryPublicProject = false; + const returnFoundAnnotation = true; + const findAnnotations = sinon.stub().callsFake(() => Promise.resolve(returnFoundAnnotation ? annotationInDb : [])), + insertAnnotations = sinon.stub().resolves(), + port = 10002, + privateProject = { + owner: USER_ALICE.username, + collaborators: { + list: [USER_ALICE, USER_BOB] + } + }, + publicProject = { + owner: USER_BOB.username, + collaborators: { + list: [USER_ALICE, USER_BOB, USER_ANONYMOUSE] + } + }, + queryProject = sinon.stub().callsFake(() => Promise.resolve(queryPublicProject ? publicProject : privateProject)), + updateAnnotations = sinon.stub().resolves(), + url = `http://127.0.0.1:${port}`; + + before(() => { + app.db = { + updateAnnotations, + insertAnnotations, + findAnnotations, + queryProject + }; + _server = app.listen(port, () => console.log(`mocha test listening at ${port}`)); + }); + + beforeEach(() => { + updateAnnotations.resetHistory(); + insertAnnotations.resetHistory(); + findAnnotations.resetHistory(); + queryProject.resetHistory(); + }); + + after(() => { + _server.close(); + }); + + describe('saveFromGUI', () => { + + describe('/GET', () => { + + describe('public project', () => { + before(() => { + queryPublicProject = true; + }); + + it('if user not part of project, should return 200', (done) => { + authenticatedUser = null; + chai.request(url) + .get('/') + .query({ + project: 'testProject' }) - - describe('private project', () => { - - before(() => { - queryPublicProject = false - }) - - it('if user not part of project, should return 403', (done) => { - authenticatedUser = null - chai.request(url) - .get('/') - .query({ - project: 'testProject' - }) - .end((err, res) => { - expect(!!err).to.be.false - expect(res).to.have.status(403) - done() - }) - }) - - it('if user is part of project, should return status 200', (done) => { - authenticatedUser = USER_BOB - chai.request(url) - .get('/') - .query({ - project: 'testProject' - }) - .end((err, res) => { - expect(!!err).to.be.false - expect(res).to.have.status(200) - done() - }) - }) + .end((err, res) => { + expect(Boolean(err)).to.be.false; + expect(res).to.have.status(200); + done(); + }); + }); + + it('if user is part of project, should return status 200', (done) => { + authenticatedUser = USER_BOB; + chai.request(url) + .get('/') + .query({ + project: 'testProject' }) + .end((err, res) => { + expect(Boolean(err)).to.be.false; + expect(res).to.have.status(200); + done(); + }); + }); + }); - describe('fetching annotation from different project querys project as expected', () => { - const getTest = project => chai.request(url) - .get('/') - .query( - project ? ({ project }) : ({}) - ) - .then((res) => { + describe('private project', () => { - /** + before(() => { + queryPublicProject = false; + }); + + it('if user not part of project, should return 403', (done) => { + authenticatedUser = null; + chai.request(url) + .get('/') + .query({ + project: 'testProject' + }) + .end((err, res) => { + expect(Boolean(err)).to.be.false; + expect(res).to.have.status(403); + done(); + }); + }); + + it('if user is part of project, should return status 200', (done) => { + authenticatedUser = USER_BOB; + chai.request(url) + .get('/') + .query({ + project: 'testProject' + }) + .end((err, res) => { + expect(Boolean(err)).to.be.false; + expect(res).to.have.status(200); + done(); + }); + }); + }); + + describe('fetching annotation from different project querys project as expected', () => { + const getTest = (project) => chai.request(url) + .get('/') + .query( + project ? ({ project }) : ({}) + ) + .then(() => { + + /** * queryProject will only be called if user is querying annotation from a specific project * then, their authorisation will be assessed */ - if (project) { - assert(queryProject.called) + if (project) { + assert(queryProject.called); - /** + /** * if project not provided, as if project is an empty string */ - assert(queryProject.calledWith({ shortname: project })) - } else { - /** + assert(queryProject.calledWith({ shortname: project })); + } else { + + /** * if user is querying public (default) work space, query project would not be called */ - assert(queryProject.notCalled) - } - }) - - for (const p of ['', 'test1', 'test2', null]){ - it(`fetching project: "${p}"`, done => { - - getTest(p) - .then(() => done()) - .catch(done) - }) - } - }) - }) - - describe('/POST', () => { - describe('findAnnotation', () => { - it('should be called with correct arg', done => { - - const project = 'projectIsAlreadyTestedAbove' - const getTest = ({ source, slice, project }) => chai.request(url) - .get('/') - .query({ - source, - slice, - project - }) - .then(res => { - findAnnotations.callArg - assert(findAnnotations.calledWith({ - fileID: `${source}&slice=${slice}`, - project - })) - }) - - Promise.all([ - getTest({ source: 'test1', slice: '123', project }), - getTest({ source: 'test2', slice: '1234', project }), - getTest({ source: '', slice: '', project }), - chai.request(url) - .get('/') - .query({ - project - }) - .then((res) => { - assert(findAnnotations.calledWith({ - fileID: `undefined&slice=undefined`, - project - })) - }) - ]).then(() => done()) - .catch(done) - }) - }) - - describe('varying users', () => { - const sendItem = { - action: 'save', - source: '/path/to/json.json', - slice: 24, - Hash: 'testworld', - annotation: '{"Regions": [], "RegionsToRemove": []}', - project: 'alreadyTestedPreviously' - } - const { - action, - source, - slice, - ...rest - } = sendItem - let res - - beforeEach(async () => { - res = await chai.request(url) - .post('/') - .set('content-type', 'application/x-www-form-urlencoded') - .send(sendItem) - }) - - it('ok', () => { - assert(true) - }) - - describe('unauthenticated user', () => { - before(() => { - authenticatedUser = null - }) - - it('should return 403', () => { - const { status } = res - expect(status).to.equal(403) - }) - }) - - describe('authenicated user, with correct permission', () => { - before(() => { - authenticatedUser = USER_BOB - }) - - it('should return 200', () => { - const { status } = res - expect(status).to.equal(200) - }) - - it('updateAnnotations should be called', () => { - assert(updateAnnotations.called) - }) - - it('insertAnnotations should be called', () => { - assert(insertAnnotations.called) - }) - - // it('updateAnnotation called with correct arg', () => { - // assert(updateAnnotations.calledWith({ - // fileID: '/path/to/json.json&slice=24', - // user: USER_BOB.username, - // ...rest - // })) - // }) - }) - }) - }) - }) + assert(queryProject.notCalled); + } + }); - describe('saveFromAPI', () => { + for (const p of ['', 'test1', 'test2', null]) { + it(`fetching project: "${p}"`, (done) => { - let FILENAME1 = `FILENAME1.json` - let FILENAME2 = `FILENAME2.json` - const correctJson = [ + getTest(p) + .then(() => done()) + .catch(done); + }); + } + }); + }); + + describe('/POST', () => { + describe('findAnnotation', () => { + it('should be called with correct arg', (done) => { + + const projectName = 'projectIsAlreadyTestedAbove'; + const getTest = ({ source, slice, project }) => chai.request(url) + .get('/') + .query({ + source, + slice, + project + }) + .then(() => { + findAnnotations.callArg; + assert(findAnnotations.calledWith({ + fileID: `${source}&slice=${slice}`, + project + })); + }); + + Promise.all([ + getTest({ source: 'test1', slice: '123', project: projectName }), + getTest({ source: 'test2', slice: '1234', project: projectName }), + getTest({ source: '', slice: '', project: projectName }), + chai.request(url) + .get('/') + .query({ + project: projectName + }) + .then(() => { + assert(findAnnotations.calledWith({ + fileID: `undefined&slice=undefined`, + project: projectName + })); + }) + ]).then(() => done()) + .catch(done); + }); + }); + + describe('varying users', () => { + const sendItem = { + action: 'save', + source: '/path/to/json.json', + slice: 24, + Hash: 'testworld', + annotation: '{"Regions": [], "RegionsToRemove": []}', + project: 'alreadyTestedPreviously' + }; + let res; + + beforeEach(async () => { + res = await chai.request(url) + .post('/') + .set('content-type', 'application/x-www-form-urlencoded') + .send(sendItem); + }); + + it('ok', () => { + assert(true); + }); + + describe('unauthenticated user', () => { + before(() => { + authenticatedUser = null; + }); + + it('should return 403', () => { + const { status } = res; + expect(status).to.equal(403); + }); + }); + + describe('authenicated user, with correct permission', () => { + before(() => { + authenticatedUser = USER_BOB; + }); + + it('should return 200', () => { + const { status } = res; + expect(status).to.equal(200); + }); + + it('updateAnnotations should be called', () => { + assert(updateAnnotations.called); + }); + + it('insertAnnotations should be called', () => { + assert(insertAnnotations.called); + }); + + // it('updateAnnotation called with correct arg', () => { + // assert(updateAnnotations.calledWith({ + // fileID: '/path/to/json.json&slice=24', + // user: USER_BOB.username, + // ...rest + // })) + // }) + }); + }); + }); + }); + + describe('saveFromAPI', () => { + + const FILENAME1 = `FILENAME1.json`; + const FILENAME2 = `FILENAME2.json`; + const correctJson = [ + { + "annotation": { + "path": [ + "Path", { - "annotation": { - "path": [ - "Path", - { - "applyMatrix": true, - "segments": [ - [345, 157], - [386, 159], - [385, 199] - ], - "closed": true, - "fillColor": [0.1, 0.7, 0.6, 0.5], - "strokeColor": [0, 0, 0], - "strokeScaling": false - } - ], - "name": "Contour 1" - } - }, + "applyMatrix": true, + "segments": [ + [345, 157], + [386, 159], + [385, 199] + ], + "closed": true, + "fillColor": [0.1, 0.7, 0.6, 0.5], + "strokeColor": [0, 0, 0], + "strokeScaling": false + } + ], + "name": "Contour 1" + } + }, + { + "annotation": { + "path": [ + "Path", { - "annotation": { - "path": [ - "Path", - { - "applyMatrix": true, - "segments": [ - [475, 227], - [502, 155], - [544, 221] - ], - "closed": true, - "fillColor": [0.0, 0.0, 0.6, 0.5], - "strokeColor": [0, 0, 0], - "strokeScaling": false - } - ], - "name": "Contour 2" - } + "applyMatrix": true, + "segments": [ + [475, 227], + [502, 155], + [544, 221] + ], + "closed": true, + "fillColor": [0.0, 0.0, 0.6, 0.5], + "strokeColor": [0, 0, 0], + "strokeScaling": false } - ] - - const incorrectJSON = { - hello: "world" + ], + "name": "Contour 2" } + } + ]; - const getQueryParam = ({ action = 'append' } = {}) => ({ - source: '/path/to/json.json', - slice: 24, - Hash: 'hello world', - action, - project: 'alreadyTestedPreviously' - }) + const incorrectJSON = { + hello: "world" + }; - let readFileStub + const getQueryParam = ({ action = 'append' } = {}) => ({ + source: '/path/to/json.json', + slice: 24, + Hash: 'hello world', + action, + project: 'alreadyTestedPreviously' + }); - before(() => { - readFileStub = sinon.stub(fs, 'readFileSync') - readFileStub.withArgs(FILENAME1).returns(Buffer.from(JSON.stringify(correctJson))) - readFileStub.withArgs(FILENAME2).returns(Buffer.from(JSON.stringify(incorrectJSON))) - }) - - after(() => { - readFileStub.restore() - }) - - const getTest = (queryParam, { illFormedJson = false } = {}) => chai.request(url) - .post('/upload') - .attach( - 'data', - illFormedJson - ? fs.readFileSync(FILENAME2) - : fs.readFileSync(FILENAME1), - illFormedJson - ? FILENAME2 - : FILENAME1 - ) - .query(queryParam) - - - describe('/POST', () => { - - describe('authenciation status behave expectedly', () => { - describe('unauthenicated user', () => { - describe('action=save', () => { - - let res - before(async () => { - authenticatedUser = null - res = await getTest({ ...getQueryParam({ action: 'save' }) }) - }) - - it('returns 401', () => { - assert(res.status === 401) - }) - - it('status text is as expected', () => { - expect(res.body).to.deep.equal({ - msg: 'Invalid user' - }) - }) - - it('findAnnotation is NOT called', () => { - assert(findAnnotations.notCalled) - }) - - it('updateAnnotations NOT called', () => { - assert(updateAnnotations.notCalled) - }) - - it('insertAnnotations NOT called', () => { - assert(insertAnnotations.notCalled) - }) }) - describe('action=append', () => { - - let res - before(async () => { - authenticatedUser = null - res = await getTest({ ...getQueryParam({ action: 'append' }) }) - }) - - it('returns 401', () => { - assert(res.status === 401) - }) - - it('status text is as expected', () => { - expect(res.body).to.deep.equal({ - msg: 'Invalid user' - }) - }) - - it('findAnnotation is NOT called', () => { - assert(findAnnotations.notCalled) - }) - - it('updateAnnotation NOT called', () => { - assert(updateAnnotations.notCalled) - }) - - it('insertAnnotation NOT called', () => { - assert(insertAnnotations.notCalled) - }) }) - }) - - // describe('authenicated user', () => { - - // describe('action=save', () => { - // let res, param - // before(async () => { - // authenticatedUser = USER_BOB - - // param = getQueryParam({ action: 'save' }) - - // const { source: paramSource, slice, action, project, ...rest } = param - // res = await getTest(param) - // }) - - // it('returns 200', () => { - // assert(res.status === 200) - // }) - - // it('return body text is as expected', () => { - // expect(res.body).to.deep.equal({ - // msg: 'Annotation successfully saved' - // }) - // }) - - // /** - // * action=save does not call findAnnotation - // */ - // it('findAnnotation not called', () => { - // assert(findAnnotations.notCalled) - // }) - - // it('updateAnnotation is called', () => { - // assert(updateAnnotation.called) - // }) - - // it('updateAnnotation called with correct param', () => { - - // const { source, slice, ...rest } = param - - // console.log('--------------------') - // console.log(updateAnnotation.firstCall.args) - // assert(updateAnnotation.calledWith({ - // fileID: buildFileID({ source, slice }), - // user: USER_BOB.username, - // project, - // annotation: JSON.stringify({ - // Regions: correctJson.map(v => v.annotation) - // }), - // ...rest, - // })) - // }) - // }) - - // describe('action=append', () => { - // let res2 - - // before(async () => { - // authenticatedUser = USER_BOB - // res2 = await getTest({ ...getQueryParam({ action: 'append' }) }) - // }) - - // it('returns 200', () => { - // assert(res2.status === 200) - // }) - - // it('body text as expected', () => { - // expect(res2.body).to.deep.equal({ - // msg: 'Annotation successfully saved' - // }) - // }) - - // it('findAnnotation is called', () => { - // assert(findAnnotations.called) - // }) - - // it('findAnnotation called with correct param', () => { - // assert(findAnnotations.calledWith({ - // fileID: buildFileID({ source, slice}), - // user: USER_BOB.username, - // project - // })) - // }) - - // it('update annotation called', () => { - // assert(updateAnnotation.called) - // }) - - // it('updatedannotation called with correct param', () => { - - // assert(updateAnnotation.calledWith({ - // fileID: buildFileID({ source, slice}), - // user: USER_BOB.username, - // project, - // annotation: JSON.stringify({ - // Regions: correctJson.map(v => v.annotation) - // }), - // ...rest, - - // })) - // }) - // }) - // }) - }) - }) - }) -}) + let readFileStub; + + before(() => { + readFileStub = sinon.stub(fs, 'readFileSync'); + readFileStub.withArgs(FILENAME1).returns(Buffer.from(JSON.stringify(correctJson))); + readFileStub.withArgs(FILENAME2).returns(Buffer.from(JSON.stringify(incorrectJSON))); + }); + + after(() => { + readFileStub.restore(); + }); + + const getTest = (queryParam, { illFormedJson = false } = {}) => chai.request(url) + .post('/upload') + .attach( + 'data', + illFormedJson + // eslint-disable-next-line no-sync + ? fs.readFileSync(FILENAME2) + // eslint-disable-next-line no-sync + : fs.readFileSync(FILENAME1), + illFormedJson + ? FILENAME2 + : FILENAME1 + ) + .query(queryParam); + + + describe('/POST', () => { + + describe('authenciation status behave expectedly', () => { + describe('unauthenicated user', () => { + describe('action=save', () => { + + let res; + before(async () => { + authenticatedUser = null; + res = await getTest({ ...getQueryParam({ action: 'save' }) }); + }); + + it('returns 401', () => { + assert(res.status === 401); + }); + + it('status text is as expected', () => { + expect(res.body).to.deep.equal({ + msg: 'Invalid user' + }); + }); + + it('findAnnotation is NOT called', () => { + assert(findAnnotations.notCalled); + }); + + it('updateAnnotations NOT called', () => { + assert(updateAnnotations.notCalled); + }); + + it('insertAnnotations NOT called', () => { + assert(insertAnnotations.notCalled); + }); + }); + describe('action=append', () => { + + let res; + before(async () => { + authenticatedUser = null; + res = await getTest({ ...getQueryParam({ action: 'append' }) }); + }); + + it('returns 401', () => { + assert(res.status === 401); + }); + + it('status text is as expected', () => { + expect(res.body).to.deep.equal({ + msg: 'Invalid user' + }); + }); + + it('findAnnotation is NOT called', () => { + assert(findAnnotations.notCalled); + }); + + it('updateAnnotation NOT called', () => { + assert(updateAnnotations.notCalled); + }); + + it('insertAnnotation NOT called', () => { + assert(insertAnnotations.notCalled); + }); + }); + }); + + // describe('authenicated user', () => { + + // describe('action=save', () => { + // let res, param + // before(async () => { + // authenticatedUser = USER_BOB + + // param = getQueryParam({ action: 'save' }) + + // const { source: paramSource, slice, action, project, ...rest } = param + // res = await getTest(param) + // }) + + // it('returns 200', () => { + // assert(res.status === 200) + // }) + + // it('return body text is as expected', () => { + // expect(res.body).to.deep.equal({ + // msg: 'Annotation successfully saved' + // }) + // }) + + // /** + // * action=save does not call findAnnotation + // */ + // it('findAnnotation not called', () => { + // assert(findAnnotations.notCalled) + // }) + + // it('updateAnnotation is called', () => { + // assert(updateAnnotation.called) + // }) + + // it('updateAnnotation called with correct param', () => { + + // const { source, slice, ...rest } = param + + // console.log('--------------------') + // console.log(updateAnnotation.firstCall.args) + // assert(updateAnnotation.calledWith({ + // fileID: buildFileID({ source, slice }), + // user: USER_BOB.username, + // project, + // annotation: JSON.stringify({ + // Regions: correctJson.map(v => v.annotation) + // }), + // ...rest, + // })) + // }) + // }) + + // describe('action=append', () => { + // let res2 + + // before(async () => { + // authenticatedUser = USER_BOB + // res2 = await getTest({ ...getQueryParam({ action: 'append' }) }) + // }) + + // it('returns 200', () => { + // assert(res2.status === 200) + // }) + + // it('body text as expected', () => { + // expect(res2.body).to.deep.equal({ + // msg: 'Annotation successfully saved' + // }) + // }) + + // it('findAnnotation is called', () => { + // assert(findAnnotations.called) + // }) + + // it('findAnnotation called with correct param', () => { + // assert(findAnnotations.calledWith({ + // fileID: buildFileID({ source, slice}), + // user: USER_BOB.username, + // project + // })) + // }) + + // it('update annotation called', () => { + // assert(updateAnnotation.called) + // }) + + // it('updatedannotation called with correct param', () => { + + // assert(updateAnnotation.calledWith({ + // fileID: buildFileID({ source, slice}), + // user: USER_BOB.username, + // project, + // annotation: JSON.stringify({ + // Regions: correctJson.map(v => v.annotation) + // }), + // ...rest, + + // })) + // }) + // }) + // }) + }); + }); + }); +}); diff --git a/app/controller/api/index.js b/app/controller/api/index.js index 8c420ee..5258335 100644 --- a/app/controller/api/index.js +++ b/app/controller/api/index.js @@ -1,195 +1,179 @@ const express = require('express'); -const router = express.Router(); +const router = new express.Router(); const multer = require('multer'); const fs = require('fs'); -const TMP_DIR = process.env.TMP_DIR +const {TMP_DIR} = process.env; const storage = TMP_DIR - ? multer.diskStorage({ - destination: TMP_DIR - }) - : multer.memoryStorage() + ? multer.diskStorage({ + destination: TMP_DIR + }) + : multer.memoryStorage(); + +const buildFileID = function ({ source, slice }) { + return `${source}&slice=${slice}`; +}; // API routes +// eslint-disable-next-line max-statements router.get('/', async function (req, res) { - console.warn("call to GET api"); - console.warn(req.query); + console.warn("call to GET api"); + console.warn(req.query); - // current user - const username = (req.user && req.user.username) || 'anyone'; - console.log(`current user: ${username}`); + // current user + const username = (req.user && req.user.username) || 'anyone'; + console.log(`current user: ${username}`); - // project name - const project = (req.query.project) || ''; - console.log(`current project: ${project}`); + // project name + const project = (req.query.project) || ''; + console.log(`current project: ${project}`); - if(project) { - // project owner and project users - const result = await req.app.db.queryProject({shortname: project}); - const owner = result + if(project) { + // project owner and project users + const result = await req.app.db.queryProject({shortname: project}); + const owner = result && result.owner; - const users = result + const users = result && result.collaborators && result.collaborators.list - && result.collaborators.list.map((u)=>u.username) || []; - console.log(`project owner: ${owner}, users: ${users}`); + && result.collaborators.list.map((u) => u.username) || []; + console.log(`project owner: ${owner}, users: ${users}`); - // check if user is among the allowed project's users - userIndex = [...users, owner].indexOf(username); - if(userIndex<0) { - res.status(403).send(`User ${username} not part of project ${project}`); + // check if user is among the allowed project's users + const userIndex = [...users, owner].indexOf(username); + if(userIndex<0) { + res.status(403).send(`User ${username} not part of project ${project}`); - return; - } + return; } + } - // include backups - const backup = (typeof req.query.backup === "undefined")?false:true; - - const query = { - fileID: buildFileID(req.query), - // user: { $in: [...users, username] }, - project: project, - }; - console.log("api get query", query); - - let annotations; - try { - annotations = await req.app.db.findAnnotations(query, backup); - } catch(err) { - throw new Error(err); - } + // include backups + const backup = typeof req.query.backup !== "undefined"; - res.status(200).send(annotations); -}); + const query = { + fileID: buildFileID(req.query), + // user: { $in: [...users, username] }, + project: project + }; + console.log("api get query", query); -function buildFileID({source, slice}) { - return `${source}&slice=${slice}`; -} + const annotations = await req.app.db.findAnnotations(query, backup); + res.status(200).send(annotations); +}); + +// eslint-disable-next-line max-statements const updateAnnotation = async (req, { - fileID, - user, - project, - Hash, - annotationString + fileID, + user, + project, + Hash, + annotationString }) => { - // get new annotations and ID - const annotation = JSON.parse(annotationString); - const {Regions: newAnnotations, RegionsToRemove: uidsToRemove} = annotation; - - // get previous annotations - let prevAnnotations; - try { - prevAnnotations = await req.app.db.findAnnotations({fileID, user, project}); - } catch(err) { - throw new Error(err); + // get new annotations and ID + const annotation = JSON.parse(annotationString); + const {Regions: newAnnotations, RegionsToRemove: uidsToRemove} = annotation; + + // get previous annotations + const prevAnnotations = await req.app.db.findAnnotations({fileID, user, project}); + + // update previous annotations with new annotations, + // overwrite what already exists, remove those marked for removal + for(const prevAnnot of prevAnnotations) { + const {uid} = prevAnnot.annotation; + let foundInPrevious = false; + for(const newAnnot of newAnnotations) { + if(newAnnot.uid === uid) { + foundInPrevious = true; + break; + } } - // update previous annotations with new annotations, - // overwrite what already exists, remove those marked for removal - for(const prevAnnot of prevAnnotations.Regions) { - const {uid} = prevAnnot.annotation; - let foundInPrevious = false; - for(const newAnnot of newAnnotations) { - if(newAnnot.uid === uid) { - foundInPrevious = true; + let markedForRemoval = false; + if(uidsToRemove) { + for(const uidToRemove of uidsToRemove) { + if(uid === uidToRemove) { + markedForRemoval = true; break; } } - - let markedForRemoval = false; - if(uidsToRemove) { - for(const uidToRemove of uidsToRemove) { - if(uid === uidToRemove) { - markedForRemoval = true; - break; - } - } - } - - if(!foundInPrevious && !markedForRemoval) { - newAnnotations.push(prevAnnot.annotation); - } - } - annotation.Regions = newAnnotations; - - // mark previous version as backup - try { - await req.app.db.updateAnnotations( - Object.assign( - {}, - { fileID, /*user,*/ project }, // update annotations authored by anyone - { backup: { $exists: false } } - ), - { $set: { backup: true } }, - { multi: true } - ); - } catch (err) { - throw new Error(err); } - // add new version - const arrayTobeSaved = annotation.Regions.map((region) => ({ - fileID, - user, // "user" property in annotation corresponds to "username" everywhere else - project, - Hash, - annotation: region - })); - try { - await req.app.db.insertAnnotations(arrayTobeSaved); - } catch(err) { - throw new Error(err); + if(!foundInPrevious && !markedForRemoval) { + newAnnotations.push(prevAnnot.annotation); } -} + } + annotation.Regions = newAnnotations; + + // mark previous version as backup + const query = Object.assign( + {}, + { fileID, /*user,*/ project }, // update annotations authored by anyone + { backup: { $exists: false } } + ); + const update = { $set: { backup: true } }; + const options = { multi: true }; + + // add new version + const arrayTobeSaved = annotation.Regions.map((region) => ({ + fileID, + user, // "user" property in annotation corresponds to "username" everywhere else + project, + Hash, + annotation: region + })); + await req.app.db.updateAnnotations({ query, update, options }); + await req.app.db.insertAnnotations(arrayTobeSaved); +}; +// eslint-disable-next-line max-statements const saveFromGUI = async function (req, res) { - const { Hash, annotation } = req.body; - - // current user - const username = (req.user && req.user.username) || 'anyone'; - console.log(`current user: ${username}`); + const { Hash, annotation } = req.body; - // project name - const project = (req.body.project) || ''; - console.log(`current project: ${project}`); + // current user + const username = (req.user && req.user.username) || 'anyone'; + console.log(`current user: ${username}`); - // project owner and project users - if(project) { - const result = await req.app.db.queryProject({shortname: project}); - const owner = result + // project name + const project = (req.body.project) || ''; + console.log(`current project: ${project}`); + + // project owner and project users + if(project) { + const result = await req.app.db.queryProject({shortname: project}); + const owner = result && result.owner; - const users = result + const users = result && result.collaborators && result.collaborators.list && result.collaborators.list.map((u) => u.username); - console.log(`project owner: ${owner}, users: ${users}`); + console.log(`project owner: ${owner}, users: ${users}`); - // check if user is among the allowed project's users - userIndex = [...users, owner].indexOf(username); - if(userIndex<0) { - res.status(403).send(`User ${username} not part of project ${project}`); + // check if user is among the allowed project's users + const userIndex = [...users, owner].indexOf(username); + if(userIndex<0) { + res.status(403).send(`User ${username} not part of project ${project}`); - return; - } + return; } + } - const fileID = buildFileID(req.body); - updateAnnotation(req, { - fileID, - user: username, - project, - Hash, - annotationString: annotation - }) + const fileID = buildFileID(req.body); + updateAnnotation(req, { + fileID, + user: username, + project, + Hash, + annotationString: annotation + }) .then(() => { - res.status(200).send({success: true}) + res.status(200).send({success: true}); }) .catch((e) => { - res.status(500).send({err:JSON.stringify(e)}) + res.status(500).send({err:e.message}); }); }; @@ -199,101 +183,102 @@ const saveFromGUI = async function (req, res) { * @returns {boolean} True if the object is valid */ const jsonIsValid = function (obj) { - if(typeof obj === 'undefined') { - return false; - } else if(obj.constructor !== Array) { - return false; - } else if(!obj.every(item => item.annotation && item.annotation.path)) { - return false; - } - - return true; + if(typeof obj === 'undefined') { + return false; + } else if(obj.constructor !== Array) { + return false; + } else if(!obj.every((item) => item.annotation && item.annotation.path)) { + return false; + } + + return true; }; /** * Loads a json file containing an annotation object. + * Not used anymore * @param {string} annotationPath Path to json file containing an annotation * @returns {object} A valid annotation object or nothing. */ -const loadAnnotationFile = function (annotationPath) { - const json = JSON.parse(fs.readFileSync(annotationPath).toString()); - if(jsonIsValid(json) === true) { - return json; - } -}; - +// const loadAnnotationFile = function (annotationPath) { +// const json = JSON.parse(fs.readFileSync(annotationPath).toString()); +// if(jsonIsValid(json) === true) { +// return json; +// } +// }; + +// eslint-disable-next-line max-statements const saveFromAPI = async function (req, res) { - const fileID = buildFileID(req.query); - const username = req.user && req.user.username; - const { project, Hash } = req.query; - const rawString = TMP_DIR - ? fs.readFileSync(req.files[0].path).toString() - : req.files[0].buffer.toString(); - const json = JSON.parse(rawString); - if (typeof project === 'undefined') { - res.status(401).json({msg: "Invalid project"}); - } else if(!jsonIsValid(json)) { - res.status(401).json({msg: "Invalid annotation file"}); - } else { - const { action } = req.query; - const annotations = action === 'append' - ? await req.app.db.findAnnotations({ fileID, user: username, project }) - : { Regions: [] }; - - /** + const fileID = buildFileID(req.query); + const username = req.user && req.user.username; + const { project, Hash } = req.query; + const rawString = TMP_DIR + ? await fs.promises.readFile(req.files[0].path).toString() + : req.files[0].buffer.toString(); + const json = JSON.parse(rawString); + if (typeof project === 'undefined') { + res.status(401).json({msg: "Invalid project"}); + } else if(!jsonIsValid(json)) { + res.status(401).json({msg: "Invalid annotation file"}); + } else { + const { action } = req.query; + const annotations = action === 'append' + ? await req.app.db.findAnnotations({ fileID, user: username, project }) + : []; + + /** * use object destruction to avoid mutation of annotations object */ - const { Regions, ...rest } = annotations - updateAnnotation(req, { - fileID, - user: username, - project, - Hash, - annotationString: JSON.stringify({ - ...rest, - Regions: Regions.concat(json.map(v => v.annotation)) - }) - }) - .then(() => res.status(200).send({msg: "Annotation successfully saved"})) - .catch((e) => res.status(500).send({err:JSON.stringify(e)})); - } + const { Regions, ...rest } = annotations; + updateAnnotation(req, { + fileID, + user: username, + project, + Hash, + annotationString: JSON.stringify({ + ...rest, + Regions: Regions.concat(json.map((v) => v.annotation)) + }) + }) + .then(() => res.status(200).send({msg: "Annotation successfully saved"})) + .catch((e) => res.status(500).send({ err: e.message})); + } }; router.post('/', function (req, res) { - console.warn("call to POST from GUI"); + console.warn("call to POST from GUI"); - if(req.body.action === 'save') { - saveFromGUI(req, res); - } else { - res.status(500).send({err:'actions other than save are no longer supported.'}); - } + if(req.body.action === 'save') { + saveFromGUI(req, res); + } else { + res.status(500).send({err:'actions other than save are no longer supported.'}); + } }); const filterAuthorizedUserOnly = (req, res, next) => { - const username = req.user && req.user.username; - if (typeof username === 'undefined') { - return res.status(401).send({msg:'Invalid user'}); - } else { - return next() - } -} + const username = req.user && req.user.username; + if (typeof username === 'undefined') { + return res.status(401).send({msg:'Invalid user'}); + } -router.post('/upload', filterAuthorizedUserOnly, multer({ storage }).array('data'), function (req, res) { - console.warn("call to POST from API"); + return next(); - const { action } = req.query - switch(action) { - case 'save': - case 'append': - saveFromAPI(req, res) - break; - default: - return res.status(500).send({err: `actions other than save and append are no longer supported`}) - } +}; + +router.post('/upload', filterAuthorizedUserOnly, multer({ storage }).array('data'), function (req, res) { + console.warn("call to POST from API"); + + const { action } = req.query; + switch(action) { + case 'save': + case 'append': + saveFromAPI(req, res); + break; + default: + return res.status(500).send({err: `actions other than save and append are no longer supported`}); + } }); -router.use('', (req, res) => { - return res.redirect('/') -}) +router.use('', (req, res) => res.redirect('/')); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/public/js/.eslintrc.json b/app/public/js/.eslintrc.json new file mode 100644 index 0000000..3c4df18 --- /dev/null +++ b/app/public/js/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "node": false + }, + "parserOptions": { + "sourceType": "script" + } +} diff --git a/app/public/js/microdraw-ws.js b/app/public/js/microdraw-ws.js index e94f978..6d64a8c 100644 --- a/app/public/js/microdraw-ws.js +++ b/app/public/js/microdraw-ws.js @@ -10,42 +10,41 @@ if(Microdraw.secure) { } else { wshostname = "ws://" + Microdraw.wshostname; } -var ws = new window.WebSocket(wshostname); +const ws = new window.WebSocket(wshostname); + +const randomUuid = Math.floor(Math.random() * 65535); + +const _decodeRandomUuidToNickname = (n) => { + if (typeof n !== 'number') { + throw new Error('argument to _decodeRandomUuidToNickname must be a number.'); + } + if (Number.isNaN(n)) { + throw new Error('argument to _decodeRandomUuidToNickname cannot be NaN'); + } + const { HippyHippo } = window.HippyHippo; + + return HippyHippo.getNickName(n); +}; /** * @param {object} data Data received * @returns {void} */ const receiveChatMessage = (data) => { - const {dom} = Microdraw; + const { dom } = Microdraw; let theUsername; if (data.username !== "Anonymous") { theUsername = data.username; + } else if (typeof data.randomUuid === 'number') { + theUsername = _decodeRandomUuidToNickname(data.randomUuid); } else { - if (typeof data.randomUuid === 'number') { - theUsername = _decodeRandomUuidToNickname(data.randomUuid); - } else { - theUsername = data.uid; - } + theUsername = data.uid; } const msg = "" + theUsername + ": " + data.msg + "
"; dom.querySelector("#logChat .text").innerHTML += msg; dom.querySelector("#logChat .text").scrollTop = dom.querySelector("#logChat .text").scrollHeight; }; -const randomUuid = Math.floor(Math.random() * 65535); - -const _decodeRandomUuidToNickname = n => { - if (typeof n !== 'number') { - throw new Error('argument to _decodeRandomUuidToNickname must be a number.'); - } - if (Number.isNaN(n)) { - throw new Error('argument to _decodeRandomUuidToNickname cannot be NaN'); - } - const { HippyHippo } = window.HippyHippo - return HippyHippo.getNickName(n) -}; - const _getUserName = () => { let username = document.querySelector("#MyLogin a").innerText; @@ -71,7 +70,7 @@ const _makeMessageObject = () => { const _displayOwnMessage = (msg) => { const {dom} = Microdraw; - const _username = _getUserName() + const _username = _getUserName(); const username = _username === 'Anonymous' ? _decodeRandomUuidToNickname(randomUuid) : _username; diff --git a/app/public/js/microdraw.js b/app/public/js/microdraw.js index bb6197c..3314202 100644 --- a/app/public/js/microdraw.js +++ b/app/public/js/microdraw.js @@ -5,206 +5,205 @@ /*eslint no-alert: "off"*/ /* global paper */ /*global OpenSeadragon*/ -/*global Ontology*/ /*global MUI*/ +/* exported Microdraw */ const Microdraw = (function () { const me = { - debug: 1, - hostname: "http://localhost:3000", - wshostname: "localhost:8080", - secure: true, - - ImageInfo: {}, // regions for each sections, can be accessed by the section name. (e.g. me.ImageInfo[me.imageOrder[viewer.current_page()]]) - // regions contain a paper.js path, a unique ID and a name - imageOrder: [], // names of sections ordered by their openseadragon page numbers - currentImage: null, // name of the current image - prevImage: null, // name of the last image - currentLabelIndex: 0, // current label to use - region: null, // currently selected region (one element of Regions[]) - copyRegion: null, // clone of the currently selected region for copy/paste - handle: null, // currently selected control point or handle (if any) - selectedTool: null, // currently selected tool - viewer: null, // open seadragon viewer - isAnimating: false, // flag indicating whether there an animation going on (zoom, pan) - navEnabled: true, // flag indicating whether the navigator is enabled (if it's not, the annotation tools are) - magicV: 1000, // resolution of the annotation canvas - is changed automatically to reflect the size of the tileSource - params: null, // URL parameters - source: null, // data source - section: null, // section index in a multi-section dataset - UndoStack: [], - RedoStack: [], - mouseUndo: null, // tentative undo information. - shortCuts: [], // List of shortcuts - newRegionFlag: null, // true when a region is being drawn - drawingPolygonFlag: false, // true when drawing a polygon - annotationLoadingFlag: null, // true when an annotation is being loaded - config: {}, // App configuration object - isMac: navigator.platform.match(/Mac/i), - isIOS: navigator.platform.match(/(iPhone|iPod|iPad)/i), - tolerance: 4, // tolerance for hit test. This value is divided by paper.view.zoom to make it work across resolutions - counter: 1, - tap: false, - currentColorRegion: null, - tools : {}, - - /* + debug: 1, + hostname: window.location.origin, + wshostname: `${window.location.hostname}:8080`, + secure: true, + + ImageInfo: {}, // regions for each sections, can be accessed by the section name. (e.g. me.ImageInfo[me.imageOrder[viewer.current_page()]]) + // regions contain a paper.js path, a unique ID and a name + imageOrder: [], // names of sections ordered by their openseadragon page numbers + currentImage: null, // name of the current image + prevImage: null, // name of the last image + currentLabelIndex: 0, // current label to use + region: null, // currently selected region (one element of Regions[]) + copyRegion: null, // clone of the currently selected region for copy/paste + handle: null, // currently selected control point or handle (if any) + selectedTool: null, // currently selected tool + viewer: null, // open seadragon viewer + isAnimating: false, // flag indicating whether there an animation going on (zoom, pan) + navEnabled: true, // flag indicating whether the navigator is enabled (if it's not, the annotation tools are) + magicV: 1000, // resolution of the annotation canvas - is changed automatically to reflect the size of the tileSource + params: null, // URL parameters + source: null, // data source + section: null, // section index in a multi-section dataset + UndoStack: [], + RedoStack: [], + mouseUndo: null, // tentative undo information. + shortCuts: [], // List of shortcuts + newRegionFlag: null, // true when a region is being drawn + drawingPolygonFlag: false, // true when drawing a polygon + annotationLoadingFlag: null, // true when an annotation is being loaded + config: {}, // App configuration object + isMac: navigator.platform.match(/Mac/i), + isIOS: navigator.platform.match(/(iPhone|iPod|iPad)/i), + tolerance: 4, // tolerance for hit test. This value is divided by paper.view.zoom to make it work across resolutions + counter: 1, + tap: false, + currentColorRegion: null, + tools : {}, + + /* Region handling functions */ - /** + /** * @param {string} msg Message to print to console. * @param {int} level Minimum debug level to print. * @returns {void} */ - debugPrint: function (msg, level) { - if (me.debug >= level) { - console.log(msg); - } - }, + debugPrint: function (msg, level) { + if (me.debug >= level) { + console.log(msg); + } + }, - /** + /** * @returns {string} Get a unique random alphanumeric identifier for the region */ - regionUID: function () { - if( me.debug>1 ) { console.log("> regionUID"); } + regionUID: function () { + if( me.debug>1 ) { console.log("> regionUID"); } - return Math.random().toString(16).slice(2); + return Math.random().toString(16) + .slice(2); // me.counter = me.ImageInfo[me.currentImage].Regions.reduce( // (a, b) => Math.max(a, Math.floor(b.uid) + 1), me.counter // ); // return me.counter; - }, + }, - /** + /** * @param {string} str String to hash * @returns {string} A hash */ - hash: function (str) { - const result = str.split("").reduce(function(a, b) { - a = ((a<<5)-a) + b.charCodeAt(0); + hash: function (str) { + const result = str.split("").reduce(function(a, b) { + a = ((a<<5)-a) + b.charCodeAt(0); - return a&a; - }, 0); + return a&a; + }, 0); - return result; - }, + return result; + }, - sectionValueForHashing: function (section) { - const value = { - Regions: [], - RegionsToRemove: [] - }; - - for(const reg of section.Regions) { - value.Regions.push({ - path: JSON.parse(reg.path.exportJSON()), - name: reg.name, - uid: reg.uid - }); - } - - for( const uid of section.RegionsToRemove ) { - value.RegionsToRemove.push(uid); - } + sectionValueForHashing: function (section) { + const value = { + Regions: [], + RegionsToRemove: [] + }; - return value; - }, + for(const reg of section.Regions) { + value.Regions.push({ + path: JSON.parse(reg.path.exportJSON()), + name: reg.name, + uid: reg.uid + }); + } - sectionHash: function (section) { - const value = me.sectionValueForHashing(section); - const hash = me.hash(JSON.stringify(value)).toString(16); - - return hash; - }, + for( const uid of section.RegionsToRemove ) { + value.RegionsToRemove.push(uid); + } - /** + return value; + }, + + sectionHash: function (section) { + const value = me.sectionValueForHashing(section); + const hash = me.hash(JSON.stringify(value)).toString(16); + + return hash; + }, + + /** * @desc Produces a color based on a region name. * @param {string} name Name of the region. * @returns {number} color Default color of the region based on its name. */ - regionHashColor: function (name) { - const color = {}; - let h = me.hash(name); + regionHashColor: function (name) { + const color = {}; + let h = me.hash(name); - // add some randomness - h = Math.sin(h += 1)*10000; - h = 0xffffff*(h - Math.floor(h)); + // add some randomness + h = Math.sin(h += 1)*10000; + h = 0xffffff*(h - Math.floor(h)); - color.red = h & 0xff; - color.green = (h & 0xff00)>>8; - color.blue = (h & 0xff0000)>>16; + color.red = h & 0xff; + color.green = (h & 0xff00)>>8; + color.blue = (h & 0xff0000)>>16; - return color; - }, + return color; + }, - /** + /** * @desc Gets the color for a region based on its name * @param {string} name Name of the region. * @returns {number} color Color of the region based on its name. */ - regionColor: function (name) { - const color = {}; - for(const {name: rname, value, color: rcolor, url} of me.ontology.labels) { - if(name === rname) { - color.red = rcolor[0]; - color.green = rcolor[1]; - color.blue = rcolor[2]; + regionColor: function (name) { + const color = {}; + for(const {name: rname, color: rcolor} of me.ontology.labels) { + if(name === rname) { + [color.red, color.green, color.blue] = rcolor; - return color; - } + return color; } + } - // name not found: assign one based on the name - return me.regionHashColor(name); - }, + // name not found: assign one based on the name + return me.regionHashColor(name); + }, - updateLabelDisplay: function () { - const {color} = me.ontology.labels[me.currentLabelIndex]; - me.dom.querySelector("#color").style["background-color"] = `rgb(${color[0]}, ${color[1]}, ${color[2]})`; - }, + updateLabelDisplay: function () { + const {color} = me.ontology.labels[me.currentLabelIndex]; + me.dom.querySelector("#color").style["background-color"] = `rgb(${color[0]}, ${color[1]}, ${color[2]})`; + }, - /** + /** * @param {number} uid Unique ID of a region. * @returns {object} The region corresponding to the given ID */ - findRegionByUID: function (uid) { - me.debugPrint("> findRegionByUID", 1); + findRegionByUID: function (uid) { + me.debugPrint("> findRegionByUID", 1); - me.debugPrint( "look for uid: " + uid, 2); - me.debugPrint( "region array lenght: " + me.ImageInfo[me.currentImage].Regions.length, 2 ); - // if( me.debug > 2 ) console.log( me.ImageInfo ); + me.debugPrint( "look for uid: " + uid, 2); + me.debugPrint( "region array lenght: " + me.ImageInfo[me.currentImage].Regions.length, 2 ); + // if( me.debug > 2 ) console.log( me.ImageInfo ); - for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { - // if( parseInt(me.ImageInfo[me.currentImage].Regions[i].uid, 10) === parseInt(uid, 10) ) { - if( me.ImageInfo[me.currentImage].Regions[i].uid === uid ) { - me.debugPrint( "region " + me.ImageInfo[me.currentImage].Regions[i].uid + ": ", 2); - me.debugPrint( me.ImageInfo[me.currentImage].Regions[i], 2 ); + for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { + // if( parseInt(me.ImageInfo[me.currentImage].Regions[i].uid, 10) === parseInt(uid, 10) ) { + if( me.ImageInfo[me.currentImage].Regions[i].uid === uid ) { + me.debugPrint( "region " + me.ImageInfo[me.currentImage].Regions[i].uid + ": ", 2); + me.debugPrint( me.ImageInfo[me.currentImage].Regions[i], 2 ); - return me.ImageInfo[me.currentImage].Regions[i]; - } + return me.ImageInfo[me.currentImage].Regions[i]; } - console.log(`WARNING: Region with unique ID ${uid} not found`); - }, + } + console.log(`WARNING: Region with unique ID ${uid} not found`); + }, - /** + /** * @param {string} name Name of the region. * @param {string} uid Unique ID of the region. * @returns {string} str The color of the region. */ - regionTag: function (name, uid) { - if( me.debug>1 ) console.log("> regionTag"); - - let str; - const color = me.regionColor(name); - if( uid ) { - const reg = me.findRegionByUID(uid); - let mult = 1.0; - if( reg ) { - mult = 255; - color = reg.path.fillColor; - } - str = ` + regionTag: function (name, uid) { + if( me.debug>1 ) { console.log("> regionTag"); } + + let str; + let color = me.regionColor(name); + if( uid ) { + const reg = me.findRegionByUID(uid); + let mult = 1.0; + if( reg ) { + mult = 255; + color = reg.path.fillColor; + } + str = `
${name}
`; - } else { - str = ` + } else { + str = `
${name}
`; - } + } - return str; - }, + return str; + }, - /** + /** * Create a new path from a Paper json object * @param {object} json Paper json object * @returns {object} Paper path (which is automatically added to the active project) */ - _pathFromJSON: (json) => { - let path; - switch(json[0]) { - case 'Path': { - path = new paper.Path(); - path.importJSON(json); - break; - } - case 'CompoundPath': { - path = new paper.CompoundPath(); - path.importJSON(json); - break; - } - } + _pathFromJSON: (json) => { + let path; + switch(json[0]) { + case 'Path': { + path = new paper.Path(); + path.importJSON(json); + break; + } + case 'CompoundPath': { + path = new paper.CompoundPath(); + path.importJSON(json); + break; + } + } - return path; - }, + return path; + }, - _selectRegionInList: function (reg) { - // Select region name in list - [].forEach.call(me.dom.querySelectorAll("#regionList > .region-tag"), function(r) { - r.classList.add("deselected"); - r.classList.remove("selected"); - }); + _selectRegionInList: function (reg) { + // Select region name in list + [].forEach.call(me.dom.querySelectorAll("#regionList > .region-tag"), function(r) { + r.classList.add("deselected"); + r.classList.remove("selected"); + }); - if (reg) { - // Need to use unicode character for ID since CSS3 doesn't support ID selectors that start with a digit: - // If reg.uid starts with a number, the first number has to be converted to unicode - // for example, 10a to #\\31 0a - // var tag = me.dom.querySelector("#regionList > .region-tag#\\3" + (reg.uid.toString().length > 1 ? reg.uid.toString()[0] + ' ' + reg.uid.toString().slice(1) : reg.uid.toString()) ); - let uid = reg.uid.toString(); - if(isNaN(parseInt(uid[0])) === false) { - uid = "\\3" + (uid.length > 1 ? uid[0] + ' ' + uid.slice(1) : uid) - } + if (reg) { + // Need to use unicode character for ID since CSS3 doesn't support ID selectors that start with a digit: + // If reg.uid starts with a number, the first number has to be converted to unicode + // for example, 10a to #\\31 0a + // var tag = me.dom.querySelector("#regionList > .region-tag#\\3" + (reg.uid.toString().length > 1 ? reg.uid.toString()[0] + ' ' + reg.uid.toString().slice(1) : reg.uid.toString()) ); + let uid = reg.uid.toString(); + if(isNaN(parseInt(uid[0], 10)) === false) { + uid = "\\3" + (uid.length > 1 ? uid[0] + ' ' + uid.slice(1) : uid); + } - const tag = me.dom.querySelector(`#regionList > .region-tag#${uid}`); - if(tag) { - tag.classList.remove("deselected"); - tag.classList.add("selected"); - } + const tag = me.dom.querySelector(`#regionList > .region-tag#${uid}`); + if(tag) { + tag.classList.remove("deselected"); + tag.classList.add("selected"); } - }, + } + }, - /** + /** * @desc Make the region selected * @param {object} reg The region to select, or null to deselect allr egions * @returns {void} */ - selectRegion: function (reg) { - if( me.debug>1 ) { console.log("> selectRegion"); } + selectRegion: function (reg) { + if( me.debug>1 ) { console.log("> selectRegion"); } - // Select path - for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { - if( me.ImageInfo[me.currentImage].Regions[i] === reg ) { - reg.path.selected = true; - reg.path.fullySelected = true; - me.region = reg; - } else { - me.ImageInfo[me.currentImage].Regions[i].path.selected = false; - me.ImageInfo[me.currentImage].Regions[i].path.fullySelected = false; - } + // Select path + for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { + if( me.ImageInfo[me.currentImage].Regions[i] === reg ) { + reg.path.selected = true; + reg.path.fullySelected = true; + me.region = reg; + } else { + me.ImageInfo[me.currentImage].Regions[i].path.selected = false; + me.ImageInfo[me.currentImage].Regions[i].path.fullySelected = false; } - paper.view.draw(); + } + paper.view.draw(); - me._selectRegionInList(reg); + me._selectRegionInList(reg); - if(me.debug>1) { console.log("< selectRegion"); } - }, + if(me.debug>1) { console.log("< selectRegion"); } + }, - /** + /** * @param {object} reg The entry in the region's array. * @param {string} name Name of the region. * @returns {void} */ - changeRegionName: function (reg, name) { - if( me.debug>1 ) { console.log("> changeRegionName"); } + changeRegionName: function (reg, name) { + if( me.debug>1 ) { console.log("> changeRegionName"); } - const color = me.regionColor(name); + const color = me.regionColor(name); - // Update path - reg.name = name; - reg.path.fillColor = 'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',0.5)'; - paper.view.draw(); + // Update path + reg.name = name; + reg.path.fillColor = 'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',0.5)'; + paper.view.draw(); - // Update region tag - // me.dom.querySelector(".region-tag#" + reg.uid + ">.region-name").textContent = name; - // me.dom.querySelector(".region-tag#" + reg.uid + ">.region-color").style['background-color'] = 'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',0.67)'; - }, + // Update region tag + // me.dom.querySelector(".region-tag#" + reg.uid + ">.region-name").textContent = name; + // me.dom.querySelector(".region-tag#" + reg.uid + ">.region-color").style['background-color'] = 'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',0.67)'; + }, - /** + /** * @desc Toggle the visibility of a region * @param {object} reg The region whose visibility is to toggle * @returns {void} */ - toggleRegion: function (reg) { - if( me.region !== null ) { - if( me.debug>1 ) { console.log("> toggle region"); } + toggleRegion: function (reg) { + if( me.region !== null ) { + if( me.debug>1 ) { console.log("> toggle region"); } - if( reg.path.fillColor !== null ) { - reg.path.storeColor = reg.path.fillColor; - reg.path.fillColor = null; + if( reg.path.fillColor !== null ) { + reg.path.storeColor = reg.path.fillColor; + reg.path.fillColor = null; - reg.path.strokeWidth = 0; - reg.path.fullySelected = false; - reg.storeName = reg.name; - me.dom.querySelector('#eye_' + reg.uid).setAttribute('src', '/img/eyeClosed.svg'); - } else { - reg.path.fillColor = reg.path.storeColor; - reg.path.strokeWidth = 1; - reg.name = reg.storeName; - me.dom.querySelector('#eye_' + reg.uid).setAttribute('src', '/img/eyeOpened.svg'); - } - paper.view.draw(); - me.dom.querySelector(".region-tag#" + reg.uid + ">.region-name").textContent = reg.name; + reg.path.strokeWidth = 0; + reg.path.fullySelected = false; + reg.storeName = reg.name; + me.dom.querySelector('#eye_' + reg.uid).setAttribute('src', '/img/eyeClosed.svg'); + } else { + reg.path.fillColor = reg.path.storeColor; + reg.path.strokeWidth = 1; + reg.name = reg.storeName; + me.dom.querySelector('#eye_' + reg.uid).setAttribute('src', '/img/eyeOpened.svg'); } - }, + paper.view.draw(); + me.dom.querySelector(".region-tag#" + reg.uid + ">.region-name").textContent = reg.name; + } + }, - /** + /** * @desc Add leading zeros * @param {number} number A number * @param {length} length The desired length for the resulting string * @returns {string} number string padded with zeroes */ - pad: function (number, length) { - let str = String(number); - while( str.length < length ) { str = '0' + str; } + pad: function (number, length) { + let str = String(number); + while( str.length < length ) { str = '0' + str; } - return str; - }, + return str; + }, - /** + /** * @desc Get current alpha & color values for colorPicker display * @param {object} reg The selected region. * @returns {void} */ - annotationStyle: function (reg) { - if( me.debug>1 ) { console.log(reg.path.fillColor); } + annotationStyle: function (reg) { + if( me.debug>1 ) { console.log(reg.path.fillColor); } - if( me.region !== null ) { - if( me.debug>1 ) { console.log("> changing annotation style"); } + if( me.region !== null ) { + if( me.debug>1 ) { console.log("> changing annotation style"); } - me.currentColorRegion = reg; - let {alpha} = reg.path.fillColor.alpha; - me.dom.querySelector('#alphaSlider').value = alpha*100; - me.dom.querySelector('#alphaFill').value = parseInt(alpha*100, 10); + me.currentColorRegion = reg; + let {alpha} = reg.path.fillColor.alpha; + me.dom.querySelector('#alphaSlider').value = alpha*100; + me.dom.querySelector('#alphaFill').value = parseInt(alpha*100, 10); - const hexColor = '#' + const hexColor = '#' + me.pad(( parseInt(reg.path.fillColor.red * 255, 10) ).toString(16), 2) + me.pad(( parseInt(reg.path.fillColor.green * 255, 10) ).toString(16), 2) + me.pad(( parseInt(reg.path.fillColor.blue * 255, 10) ).toString(16), 2); - if( me.debug>1 ) { console.log(hexColor); } + if( me.debug>1 ) { console.log(hexColor); } - me.dom.querySelector('#fillColorPicker').value = hexColor; + me.dom.querySelector('#fillColorPicker').value = hexColor; - if( me.dom.querySelector('#colorSelector').style.display === 'none' ) { + if( me.dom.querySelector('#colorSelector').style.display === 'none' ) { - /** @todo On show, populate alpha */ - reg = me.currentColorRegion; - alpha = reg.path.fillColor.alpha; - me.dom.querySelector('#alphaSlider').value = alpha * 100; - me.dom.querySelector('#alphaFill').value = alpha * 100; + /** @todo On show, populate alpha */ + reg = me.currentColorRegion; + ({alpha} = reg.path.fillColor); + me.dom.querySelector('#alphaSlider').value = alpha * 100; + me.dom.querySelector('#alphaFill').value = alpha * 100; - me.dom.querySelector('#colorSelector').style.display = 'block'; - } else { - me.dom.querySelector('#colorSelector').style.display = 'none'; - } + me.dom.querySelector('#colorSelector').style.display = 'block'; + } else { + me.dom.querySelector('#colorSelector').style.display = 'none'; } - }, + } + }, - /** + /** * Create a new region, adding it to the ImageInfo structure and the current paper project * @param {object} arg An object containing the name, uid and path of the region * @param {number} imageNumber The number of the image section where the region will be created * @returns {object} Reference to the new region that was added */ - newRegion: function (arg, imageNumber) { - if( me.debug>1 ) { console.log("> newRegion"); } - const reg = {}; + newRegion: function (arg, imageNumber) { + if( me.debug>1 ) { console.log("> newRegion"); } + const reg = {}; - if(arg.uid) { - reg.uid = arg.uid; - } else { - reg.uid = me.regionUID(); - } + if(arg.uid) { + reg.uid = arg.uid; + } else { + reg.uid = me.regionUID(); + } - if( arg.name ) { - reg.name = arg.name; - } else { - reg.name = me.ontology.labels[me.currentLabelIndex].name; - } + if( arg.name ) { + reg.name = arg.name; + } else { + reg.name = me.ontology.labels[me.currentLabelIndex].name; + } - const color = me.regionColor(reg.name); + const color = me.regionColor(reg.name); - if( arg.path ) { - reg.path = arg.path; - reg.path.strokeWidth = arg.path.strokeWidth ? arg.path.strokeWidth : me.config.defaultStrokeWidth; - reg.path.strokeColor = arg.path.strokeColor ? arg.path.strokeColor : me.config.defaultStrokeColor; - reg.path.strokeScaling = false; - reg.path.fillColor = arg.path.fillColor ? arg.path.fillColor :'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',' + me.config.defaultFillAlpha + ')'; - reg.path.selected = false; - } + if( arg.path ) { + reg.path = arg.path; + reg.path.strokeWidth = arg.path.strokeWidth ? arg.path.strokeWidth : me.config.defaultStrokeWidth; + reg.path.strokeColor = arg.path.strokeColor ? arg.path.strokeColor : me.config.defaultStrokeColor; + reg.path.strokeScaling = false; + reg.path.fillColor = arg.path.fillColor ? arg.path.fillColor :'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',' + me.config.defaultFillAlpha + ')'; + reg.path.selected = false; + } - if( typeof imageNumber === "undefined" ) { - imageNumber = me.currentImage; - } - // if( imageNumber === me.currentImage ) { - // // append region tag to regionList - // const regionTag = me.regionTag(reg.name, reg.uid); - // var el = me.dom.querySelector(regionTag); - // me.dom.querySelector("#regionList").appendChild(el); + if( typeof imageNumber === "undefined" ) { + imageNumber = me.currentImage; + } + // if( imageNumber === me.currentImage ) { + // // append region tag to regionList + // const regionTag = me.regionTag(reg.name, reg.uid); + // var el = me.dom.querySelector(regionTag); + // me.dom.querySelector("#regionList").appendChild(el); - // // handle single click on computers - // el.click(me.singlePressOnRegion); + // // handle single click on computers + // el.click(me.singlePressOnRegion); - // // handle double click on computers - // el.dblclick(me.doublePressOnRegion); + // // handle double click on computers + // el.dblclick(me.doublePressOnRegion); - // // handle single and double tap on touch devices - // /** - // * @todo it seems that a click event is also fired on touch devices, making this one redundant - // */ + // // handle single and double tap on touch devices + // /** + // * @todo it seems that a click event is also fired on touch devices, making this one redundant + // */ - // el.on("touchstart", me.handleRegionTap); - // } + // el.on("touchstart", me.handleRegionTap); + // } - // push the new region to the Regions array - me.ImageInfo[imageNumber].Regions.push(reg); + // push the new region to the Regions array + me.ImageInfo[imageNumber].Regions.push(reg); - // Select region name in list - // me.selectRegion(reg); + // Select region name in list + // me.selectRegion(reg); - return reg; - }, + return reg; + }, - /** + /** * Remove region from current image. The image not directly removed, but marked for removal, * so that it can be removed from the database. * @param {object} reg The region to be removed * @param {number} imageNumber The number of the image where the region will be removed * @returns {void} */ - removeRegion: function (reg, imageNumber) { - if( me.debug>1 ) { console.log("> removeRegion"); } + removeRegion: function (reg, imageNumber) { + if( me.debug>1 ) { console.log("> removeRegion"); } - if( typeof imageNumber === "undefined" ) { - imageNumber = me.currentImage; - } + if( typeof imageNumber === "undefined" ) { + imageNumber = me.currentImage; + } - // mark for removal - me.ImageInfo[imageNumber].RegionsToRemove.push(reg.uid); - // remove from Regions array - me.ImageInfo[imageNumber].Regions.splice(me.ImageInfo[imageNumber].Regions.indexOf(reg), 1); - // remove from paths - reg.path.remove(); + // mark for removal + me.ImageInfo[imageNumber].RegionsToRemove.push(reg.uid); + // remove from Regions array + me.ImageInfo[imageNumber].Regions.splice(me.ImageInfo[imageNumber].Regions.indexOf(reg), 1); + // remove from paths + reg.path.remove(); - }, + }, - /** + /** * @desc Find region by its name * @param {string} name Name of the region from the ontology list * @returns {object} The region */ - findRegionByName: function (name) { - if( me.debug>1 ) { console.log("> findRegionByName"); } + findRegionByName: function (name) { + if( me.debug>1 ) { console.log("> findRegionByName"); } - for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { - if( me.ImageInfo[me.currentImage].Regions[i].name === name ) { - return me.ImageInfo[me.currentImage].Regions[i]; - } + for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { + if( me.ImageInfo[me.currentImage].Regions[i].name === name ) { + return me.ImageInfo[me.currentImage].Regions[i]; } - console.log("WARNING: Region with name " + name + " not found"); + } + console.log("WARNING: Region with name " + name + " not found"); - return null; - }, + return null; + }, - /** + /** * @param {array} o Array with ontology terms * @returns {void} */ - appendRegionTagsFromOntology: function (o) { - if( me.debug>1 ) { console.log("> appendRegionTagsFromOntology"); } + appendRegionTagsFromOntology: function (o) { + if( me.debug>1 ) { console.log("> appendRegionTagsFromOntology"); } - for( let i = 0; i < o.length; i += 1 ) { - if( o[i].parts ) { - const el = document.createElement("div"); - el.textContent = o[i].name; - me.dom.querySelector("#regionPicker").appendChild(el); - me.appendRegionTagsFromOntology(o[i].parts); - } else { - const tag = me.regionTag(o[i].name); - const el = me.dom.querySelector(tag); - el.classList.add("ontology"); - me.dom.querySelector("#regionPicker").appendChild(el); + for( let i = 0; i < o.length; i += 1 ) { + if( o[i].parts ) { + const el = document.createElement("div"); + el.textContent = o[i].name; + me.dom.querySelector("#regionPicker").appendChild(el); + me.appendRegionTagsFromOntology(o[i].parts); + } else { + const tag = me.regionTag(o[i].name); + const el = me.dom.querySelector(tag); + el.classList.add("ontology"); + me.dom.querySelector("#regionPicker").appendChild(el); - // handle single click on computers - el.click(me.singlePressOnRegion); + // handle single click on computers + el.click(me.singlePressOnRegion); - // handle double click on computers - el.dblclick(me.doublePressOnRegion); + // handle double click on computers + el.dblclick(me.doublePressOnRegion); - el.on("touchstart", me.handleRegionTap); - } + el.on("touchstart", me.handleRegionTap); } - }, + } + }, - /** + /** * @desc Interaction: mouse and tap: If on a computer, it will send click event; if on tablet, it will send touch event. * @param {object} event Event * @returns {void} */ - clickHandler: function (event) { - if( me.debug>1 ) { console.log("> clickHandler"); } - event.stopHandlers = !me.navEnabled; - }, + clickHandler: function (event) { + if( me.debug>1 ) { console.log("> clickHandler"); } + event.stopHandlers = !me.navEnabled; + }, - /** + /** * @param {object} event Event * @returns {void} */ - pressHandler: function (event) { - if( me.debug>1 ) { console.log("> pressHandler"); } + pressHandler: function (event) { + if( me.debug>1 ) { console.log("> pressHandler"); } - if( !me.navEnabled ) { - event.stopHandlers = true; - me.mouseDown(event.originalEvent.layerX, event.originalEvent.layerY); - } - }, + if( !me.navEnabled ) { + event.stopHandlers = true; + me.mouseDown(event.originalEvent.layerX, event.originalEvent.layerY); + } + }, - /** + /** * @param {object} event Event * @returns {void} */ - releaseHandler: function (event) { - if( me.debug>1 ) { console.log("> releaseHandler"); } + releaseHandler: function (event) { + if( me.debug>1 ) { console.log("> releaseHandler"); } - if( !me.navEnabled ) { - event.stopHandlers = true; - me.mouseUp(); - } - }, + if( !me.navEnabled ) { + event.stopHandlers = true; + me.mouseUp(); + } + }, - /** + /** * @param {object} event Event * @returns {void} */ - dragHandler: function (event) { - if( me.debug > 1 ) { console.log("> dragHandler"); } + dragHandler: function (event) { + if( me.debug > 1 ) { console.log("> dragHandler"); } - if( !me.navEnabled ) { - event.stopHandlers = true; - me.mouseDrag(event.originalEvent.layerX, event.originalEvent.layerY, event.delta.x, event.delta.y); - } - }, + if( !me.navEnabled ) { + event.stopHandlers = true; + me.mouseDrag(event.originalEvent.layerX, event.originalEvent.layerY, event.delta.x, event.delta.y); + } + }, - /** + /** * @param {object} event Event * @returns {void} */ - dragEndHandler: function (event) { - if( me.debug>1 ) { console.log("> dragEndHandler"); } + dragEndHandler: function (event) { + if( me.debug>1 ) { console.log("> dragEndHandler"); } - if( !me.navEnabled ) { - event.stopHandlers = true; - me.mouseUp(); - } - }, + if( !me.navEnabled ) { + event.stopHandlers = true; + me.mouseUp(); + } + }, - /** + /** * @param {object} ev Scroll event * @returns {void} */ - scrollHandler: function (ev) { - if( me.debug>1 ) { console.log("> scrollHandler") } + scrollHandler: function (ev) { + if( me.debug>1 ) { console.log("> scrollHandler"); } - if( me.tools[me.selectedTool] + if( me.tools[me.selectedTool] && me.tools[me.selectedTool].scrollHandler ) { - me.tools[me.selectedTool].scrollHandler(ev); - } - paper.view.draw(); - }, + me.tools[me.selectedTool].scrollHandler(ev); + } + paper.view.draw(); + }, - /** + /** * @param {number} x X-coordinate for mouse down * @param {number} y Y-coordinate for mouse down * @returns {void} */ - mouseDown: function (x, y) { - me.debugPrint("> mouseDown", 1); + mouseDown: function (x, y) { + me.debugPrint("> mouseDown", 1); - me.mouseUndo = me.getUndo(); - const point = paper.view.viewToProject(new paper.Point(x, y)); + me.mouseUndo = me.getUndo(); + const point = paper.view.viewToProject(new paper.Point(x, y)); - me.handle = null; + me.handle = null; - if( me.tools[me.selectedTool] + if( me.tools[me.selectedTool] && me.tools[me.selectedTool].mouseDown ) { - me.tools[me.selectedTool].mouseDown(point); - } - paper.view.draw(); - }, + me.tools[me.selectedTool].mouseDown(point); + } + paper.view.draw(); + }, - /** + /** * @param {number} x X-coordinate where drag event started * @param {number} y Y-coordinate where drag event started * @param {number} dx Size of the drag step in the X axis * @param {number} dy Size of the drag step in the Y axis * @returns {void} */ - mouseDrag: function (x, y, dx, dy) { - if( me.debug>2 ) console.log("> mouseDrag"); - - // transform screen coordinate into world coordinate - const point = paper.view.viewToProject(new paper.Point(x, y)); - - // transform screen delta into world delta - const orig = paper.view.viewToProject(new paper.Point(0, 0)); - const dpoint = paper.view.viewToProject(new paper.Point(dx, dy)); - dpoint.x -= orig.x; - dpoint.y -= orig.y; - if( me.handle ) { - me.handle.x += point.x-me.handle.point.x; - me.handle.y += point.y-me.handle.point.y; - me.handle.point = point; - me.commitMouseUndo(); - } else if (me.tools[me.selectedTool] && me.tools[me.selectedTool].mouseDrag) { - me.tools[me.selectedTool].mouseDrag(point, dpoint); - } - paper.view.draw(); - }, + mouseDrag: function (x, y, dx, dy) { + if( me.debug>2 ) { console.log("> mouseDrag"); } + + // transform screen coordinate into world coordinate + const point = paper.view.viewToProject(new paper.Point(x, y)); + + // transform screen delta into world delta + const orig = paper.view.viewToProject(new paper.Point(0, 0)); + const dpoint = paper.view.viewToProject(new paper.Point(dx, dy)); + dpoint.x -= orig.x; + dpoint.y -= orig.y; + if( me.handle ) { + me.handle.x += point.x-me.handle.point.x; + me.handle.y += point.y-me.handle.point.y; + me.handle.point = point; + me.commitMouseUndo(); + } else if (me.tools[me.selectedTool] && me.tools[me.selectedTool].mouseDrag) { + me.tools[me.selectedTool].mouseDrag(point, dpoint); + } + paper.view.draw(); + }, - /** + /** * @returns {void} */ - mouseUp: function () { - if( me.debug>1 ) { console.log("> mouseUp"); } - if(me.tools[me.selectedTool] && me.tools[me.selectedTool].mouseUp) { - me.tools[me.selectedTool].mouseUp(); - } - }, + mouseUp: function () { + if( me.debug>1 ) { console.log("> mouseUp"); } + if(me.tools[me.selectedTool] && me.tools[me.selectedTool].mouseUp) { + me.tools[me.selectedTool].mouseUp(); + } + }, - /** + /** * @desc Simplify the region path * @returns {void} */ - simplify: function () { - if( me.region !== null ) { - if( me.debug>1 ) { console.log("> simplifying region path"); } + simplify: function () { + if( me.region !== null ) { + if( me.debug>1 ) { console.log("> simplifying region path"); } - const origSegments = me.region.path.segments.length; - me.region.path.simplify(); - const finalSegments = me.region.path.segments.length; - console.log( parseInt(finalSegments/origSegments*100, 10) + "% segments conserved" ); - paper.view.draw(); - } - }, + const origSegments = me.region.path.segments.length; + me.region.path.simplify(); + const finalSegments = me.region.path.segments.length; + console.log( parseInt(finalSegments/origSegments*100, 10) + "% segments conserved" ); + paper.view.draw(); + } + }, - /** + /** * @desc Set picked color & alpha * @returns {void} */ - setRegionColor: function () { - const reg = me.currentColorRegion; - const hexColor = me.dom.querySelector('#fillColorPicker').value; - const red = parseInt( hexColor.substring(1, 3), 16 ); - const green = parseInt( hexColor.substring(3, 5), 16 ); - const blue = parseInt( hexColor.substring(5, 7), 16 ); + setRegionColor: function () { + const reg = me.currentColorRegion; + const hexColor = me.dom.querySelector('#fillColorPicker').value; + const red = parseInt( hexColor.substring(1, 3), 16 ); + const green = parseInt( hexColor.substring(3, 5), 16 ); + const blue = parseInt( hexColor.substring(5, 7), 16 ); - reg.path.fillColor.red = red / 255; - reg.path.fillColor.green = green / 255; - reg.path.fillColor.blue = blue / 255; - reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; + reg.path.fillColor.red = red / 255; + reg.path.fillColor.green = green / 255; + reg.path.fillColor.blue = blue / 255; + reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; - // update region tag - me.dom - .querySelector(".region-tag#" + reg.uid + ">.region-color") - .style['background-color'] ='rgba(' + red + ',' + green + ',' + blue + ',0.67)' + // update region tag + me.dom + .querySelector(".region-tag#" + reg.uid + ">.region-color") + .style['background-color'] ='rgba(' + red + ',' + green + ',' + blue + ',0.67)'; - // update stroke color - const {selectedIndex} = me.dom.querySelector('#selectStrokeColor'); - reg.path.strokeColor = ["black", "white", "red", "green", "blue", "yellow"][selectedIndex]; + // update stroke color + const {selectedIndex} = me.dom.querySelector('#selectStrokeColor'); + reg.path.strokeColor = ["black", "white", "red", "green", "blue", "yellow"][selectedIndex]; - me.dom.querySelector('#colorSelector').style.display = 'none'; - }, + me.dom.querySelector('#colorSelector').style.display = 'none'; + }, - /** + /** * @desc Update all values on the fly * @param {number} value The value assigned to the color picker * @returns {void} */ - onFillColorPicker: function (value) { - me.dom.querySelector('#fillColorPicker').value = value; - const reg = me.currentColorRegion; - const hexColor = me.dom.querySelector('#fillColorPicker').value; - const red = parseInt( hexColor.substring(1, 3), 16 ); - const green = parseInt( hexColor.substring(3, 5), 16); - const blue = parseInt( hexColor.substring(5, 7), 16); - reg.path.fillColor.red = red / 255; - reg.path.fillColor.green = green / 255; - reg.path.fillColor.blue = blue / 255; - reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; - paper.view.draw(); - }, - - /** + onFillColorPicker: function (value) { + me.dom.querySelector('#fillColorPicker').value = value; + const reg = me.currentColorRegion; + const hexColor = me.dom.querySelector('#fillColorPicker').value; + const red = parseInt( hexColor.substring(1, 3), 16 ); + const green = parseInt( hexColor.substring(3, 5), 16); + const blue = parseInt( hexColor.substring(5, 7), 16); + reg.path.fillColor.red = red / 255; + reg.path.fillColor.green = green / 255; + reg.path.fillColor.blue = blue / 255; + reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; + paper.view.draw(); + }, + + /** * @returns {void} */ - onSelectStrokeColor: function () { - const reg = me.currentColorRegion; - const {selectedIndex} = me.dom.querySelector('#selectStrokeColor'); - reg.path.strokeColor = ["black", "white", "red", "green", "blue", "yellow"][selectedIndex]; - paper.view.draw(); - }, + onSelectStrokeColor: function () { + const reg = me.currentColorRegion; + const {selectedIndex} = me.dom.querySelector('#selectStrokeColor'); + reg.path.strokeColor = ["black", "white", "red", "green", "blue", "yellow"][selectedIndex]; + paper.view.draw(); + }, - /** + /** * @param {number} value The value assigned to alpha slider * @returns {void} */ - onAlphaSlider: function (value) { - me.dom.querySelector('#alphaFill').value = value; - const reg = me.currentColorRegion; - reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; - paper.view.draw(); - }, + onAlphaSlider: function (value) { + me.dom.querySelector('#alphaFill').value = value; + const reg = me.currentColorRegion; + reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; + paper.view.draw(); + }, - /** + /** * @param {number} value The value assigned to alpha input field * @returns {void} */ - onAlphaInput: function (value) { - me.dom.querySelector('#alphaSlider').value = value; - const reg = me.currentColorRegion; - reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; - paper.view.draw(); - }, + onAlphaInput: function (value) { + me.dom.querySelector('#alphaSlider').value = value; + const reg = me.currentColorRegion; + reg.path.fillColor.alpha = me.dom.querySelector('#alphaSlider').value / 100; + paper.view.draw(); + }, - /** + /** * @returns {void} */ - onStrokeWidthDec: function () { - const reg = me.currentColorRegion; - reg.path.strokeWidth = Math.max(me.region.path.strokeWidth - 1, 1); - paper.view.draw(); - }, + onStrokeWidthDec: function () { + const reg = me.currentColorRegion; + reg.path.strokeWidth = Math.max(me.region.path.strokeWidth - 1, 1); + paper.view.draw(); + }, - /** + /** * @returns {void} */ - onStrokeWidthInc: function () { - const reg = me.currentColorRegion; - reg.path.strokeWidth = Math.min(me.region.path.strokeWidth + 1, 10); - paper.view.draw(); - }, + onStrokeWidthInc: function () { + const reg = me.currentColorRegion; + reg.path.strokeWidth = Math.min(me.region.path.strokeWidth + 1, 10); + paper.view.draw(); + }, - /*** UNDO ***/ + /*** UNDO ***/ - /** + /** * @desc Command to actually perform an undo. * @returns {void} */ - cmdUndo: function () { - if( me.UndoStack.length > 0 ) { - const redoInfo = me.getUndo(); - const undoInfo = me.UndoStack.pop(); - me.applyUndo(undoInfo); - me.RedoStack.push(redoInfo); - paper.view.draw(); - } - }, + cmdUndo: function () { + if( me.UndoStack.length > 0 ) { + const redoInfo = me.getUndo(); + const undoInfo = me.UndoStack.pop(); + me.applyUndo(undoInfo); + me.RedoStack.push(redoInfo); + paper.view.draw(); + } + }, - /** + /** * @desc Command to actually perform a redo. * @returns {void} */ - cmdRedo: function () { - if( me.RedoStack.length > 0 ) { - const undoInfo = me.getUndo(); - const redoInfo = me.RedoStack.pop(); - me.applyUndo(redoInfo); - me.UndoStack.push(undoInfo); - paper.view.draw(); - } - }, + cmdRedo: function () { + if( me.RedoStack.length > 0 ) { + const undoInfo = me.getUndo(); + const redoInfo = me.RedoStack.pop(); + me.applyUndo(redoInfo); + me.UndoStack.push(undoInfo); + paper.view.draw(); + } + }, - /** + /** * @desc Return a complete copy of the current state as an undo object. * @returns {Object} The undo object */ - getUndo: function () { - const undo = { - imageNumber: me.currentImage, - regions: [], - drawingPolygonFlag: me.drawingPolygonFlag + getUndo: function () { + const undo = { + imageNumber: me.currentImage, + regions: [], + drawingPolygonFlag: me.drawingPolygonFlag + }; + const info = me.ImageInfo[me.currentImage].Regions; + + for( let i = 0; i < info.length; i += 1 ) { + const el = { + json: JSON.parse(info[i].path.exportJSON()), + name: info[i].name, + uid: info[i].uid, + selected: info[i].path.selected, + fullySelected: info[i].path.fullySelected }; - const info = me.ImageInfo[me.currentImage].Regions; - - for( let i = 0; i < info.length; i += 1 ) { - const el = { - json: JSON.parse(info[i].path.exportJSON()), - name: info[i].name, - uid: info[i].uid, - selected: info[i].path.selected, - fullySelected: info[i].path.fullySelected - }; - undo.regions.push(el); - } + undo.regions.push(el); + } - return undo; - }, + return undo; + }, - /** + /** * @desc Save an undo object. This has the side-effect of initializing the redo stack. * @param {object} undoInfo The undo info object * @returns {void} */ - saveUndo: function (undoInfo) { - me.UndoStack.push(undoInfo); - me.RedoStack = []; - }, + saveUndo: function (undoInfo) { + me.UndoStack.push(undoInfo); + me.RedoStack = []; + }, - /** + /** * @param {number} imageNumber The image number * @returns {void} */ - setImage: function (imageNumber) { - if( me.debug>1 ) { console.log("> setImage"); } - const index = me.imageOrder.indexOf(imageNumber); + setImage: function (imageNumber) { + if( me.debug>1 ) { console.log("> setImage"); } + const index = me.imageOrder.indexOf(imageNumber); - // update image slider - me.updateSliderValue(index); + // update image slider + me.updateSliderValue(index); - //update url - me.updateURL(index); + //update url + me.updateURL(index); - me.loadImage(me.imageOrder[index]); - }, + me.loadImage(me.imageOrder[index]); + }, - /** + /** * @desc Restore the current state from an undo object. * @param {object} undo The undo object to apply * @returns {void} */ - applyUndo: function (undo) { - // go to the image involved - if( undo.imageNumber !== me.currentImage ) { - me.setImage(undo.imageNumber); - } + applyUndo: function (undo) { + // go to the image involved + if( undo.imageNumber !== me.currentImage ) { + me.setImage(undo.imageNumber); + } - // remove its current contents - const {Regions: regions} = me.ImageInfo[undo.imageNumber]; - while( regions.length > 0 ) { - me.removeRegion(regions[0], undo.imageNumber); - } + // remove its current contents + const {Regions: regions} = me.ImageInfo[undo.imageNumber]; + while( regions.length > 0 ) { + me.removeRegion(regions[0], undo.imageNumber); + } - // add contents from the undo object - me.region = null; - for( let i = 0; i < undo.regions.length; i += 1 ) { - const el = undo.regions[i]; - const path = me._pathFromJSON(el.json); - // add to the correct project activeLayer, which may not be the current one - paper.project.activeLayer.addChild(path); - - const reg = me.newRegion({ - name: el.name, - uid: el.uid, - path: path - }, undo.imageNumber); - - // here order matters: if fully selected is set after selected, partially selected paths will be incorrect - reg.path.fullySelected = el.fullySelected; - reg.path.selected = el.selected; - if( el.selected ) { - if( me.region === null ) { - me.region = reg; - } else { - console.log("WARNING: This should not happen. Are two regions selected?"); - } + // add contents from the undo object + me.region = null; + for( let i = 0; i < undo.regions.length; i += 1 ) { + const el = undo.regions[i]; + const path = me._pathFromJSON(el.json); + // add to the correct project activeLayer, which may not be the current one + paper.project.activeLayer.addChild(path); + + const reg = me.newRegion({ + name: el.name, + uid: el.uid, + path: path + }, undo.imageNumber); + + // here order matters: if fully selected is set after selected, partially selected paths will be incorrect + reg.path.fullySelected = el.fullySelected; + reg.path.selected = el.selected; + if( el.selected ) { + if( me.region === null ) { + me.region = reg; + } else { + console.log("WARNING: This should not happen. Are two regions selected?"); } } + } - if(undo.callback && typeof undo.callback === 'function') { - undo.callback(); - } + if(undo.callback && typeof undo.callback === 'function') { + undo.callback(); + } - /** + /** * @todo This line produces an error when the undo object is undefined. However, the code seems to work fine without this line. Check what the line was supposed to do */ - // me.drawingPolygonFlag = me.undo.drawingPolygonFlag; - }, + // me.drawingPolygonFlag = me.undo.drawingPolygonFlag; + }, - /** + /** * @desc If we have actually made a change with a mouse operation, commit the undo information. * @returns {void} */ - commitMouseUndo: function () { - if( me.mouseUndo !== null ) { - me.saveUndo(me.mouseUndo); - me.mouseUndo = null; - } - }, + commitMouseUndo: function () { + if( me.mouseUndo !== null ) { + me.saveUndo(me.mouseUndo); + me.mouseUndo = null; + } + }, - /** + /** * @param {string} prevTool Name of the previously selected tool * @returns {void} */ - backToPreviousTool: function (prevTool) { - setTimeout(function() { - me.selectedTool = prevTool; - // me.selectTool(); - }, 500); - }, + backToPreviousTool: function (prevTool) { + setTimeout(function() { + me.selectedTool = prevTool; + // me.selectTool(); + }, 500); + }, - /** + /** * @returns {void} */ - backToSelect: function () { - setTimeout(function() { - me.selectedTool = "select"; - // me.selectTool(); - }, 500); - }, + backToSelect: function () { + setTimeout(function() { + me.selectedTool = "select"; + // me.selectTool(); + }, 500); + }, - /** + /** * @desc This function deletes the currently selected object. * @returns {void} */ - cmdDeleteSelected: function () { - const undoInfo = me.getUndo(); - for( const region of me.ImageInfo[me.currentImage].Regions ) { - if( region.path.selected ) { - me.removeRegion(region); - me.saveUndo(undoInfo); - paper.view.draw(); - break; - } + cmdDeleteSelected: function () { + const undoInfo = me.getUndo(); + for( const region of me.ImageInfo[me.currentImage].Regions ) { + if( region.path.selected ) { + me.removeRegion(region); + me.saveUndo(undoInfo); + paper.view.draw(); + break; } - }, + } + }, - /** + /** * @returns {void} */ - cmdPaste: function () { - if( me.copyRegion !== null ) { - const undoInfo = me.getUndo(); - me.saveUndo(undoInfo); - if(me.debug) { console.log( "paste " + me.copyRegion.name ); } - if( me.findRegionByName(me.copyRegion.name) ) { - // me.copyRegion.name += " Copy"; - } + cmdPaste: function () { + if( me.copyRegion !== null ) { + const undoInfo = me.getUndo(); + me.saveUndo(undoInfo); + if(me.debug) { console.log( "paste " + me.copyRegion.name ); } + if( me.findRegionByName(me.copyRegion.name) ) { + // me.copyRegion.name += " Copy"; + } - const reg = JSON.parse(JSON.stringify(me.copyRegion)); - reg.path = me._pathFromJSON(JSON.parse(me.copyRegion.path)); - reg.path.fullySelected = true; + const reg = JSON.parse(JSON.stringify(me.copyRegion)); + reg.path = me._pathFromJSON(JSON.parse(me.copyRegion.path)); + reg.path.fullySelected = true; - const color = me.regionColor(reg.name); - reg.path.fillColor = 'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',0.5)'; + const color = me.regionColor(reg.name); + reg.path.fillColor = 'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',0.5)'; - me.newRegion({ - name: me.copyRegion.name, - uid: me.regionUID(), - path: reg.path - }); - } - paper.view.draw(); - }, + me.newRegion({ + name: me.copyRegion.name, + uid: me.regionUID(), + path: reg.path + }); + } + paper.view.draw(); + }, - /** + /** * @returns {void} */ - cmdCopy: function () { - if(me.debug>1) { console.log( "< copy " + me.copyRegion.name ); } - if( me.region !== null ) { - const json = me.region.path.exportJSON(); - me.copyRegion = JSON.parse(JSON.stringify(me.region)); - me.copyRegion.path = json; - } - }, + cmdCopy: function () { + if(me.debug>1) { console.log( "< copy " + me.copyRegion.name ); } + if( me.region !== null ) { + const json = me.region.path.exportJSON(); + me.copyRegion = JSON.parse(JSON.stringify(me.region)); + me.copyRegion.path = json; + } + }, - // /** - // * @returns {void} - // */ - // selectTool: function () { - // if( me.debug>1 ) { console.log("> selectTool"); } + // /** + // * @returns {void} + // */ + // selectTool: function () { + // if( me.debug>1 ) { console.log("> selectTool"); } - // me.dom.querySelector("img.button1").classList.remove("selected"); - // me.dom.querySelector("img.button1#" + me.selectedTool).classList.add("selected"); - // }, + // me.dom.querySelector("img.button1").classList.remove("selected"); + // me.dom.querySelector("img.button1#" + me.selectedTool).classList.add("selected"); + // }, - clickTool: function (tool) { - const prevTool = me.selectedTool; + clickTool: function (tool) { + const prevTool = me.selectedTool; - if( me.tools[prevTool] && me.tools[prevTool].onDeselect ) { - me.tools[prevTool].onDeselect(); - } + if( me.tools[prevTool] && me.tools[prevTool].onDeselect ) { + me.tools[prevTool].onDeselect(); + } - me.selectedTool = tool; - // me.selectTool(); + me.selectedTool = tool; + // me.selectTool(); - if( me.tools[me.selectedTool] && me.tools[me.selectedTool].click ) { - me.tools[me.selectedTool].click(prevTool); - } - }, + if( me.tools[me.selectedTool] && me.tools[me.selectedTool].click ) { + me.tools[me.selectedTool].click(prevTool); + } + }, - /** + /** * @returns {void} */ - toolSelection: function () { - if( me.debug>1 ) { console.log("> toolSelection"); } - const tool = this.id; - me.clickTool(tool); - }, + toolSelection: function () { + if( me.debug>1 ) { console.log("> toolSelection"); } + const tool = this.id; + me.clickTool(tool); + }, - /* + /* Annotation storage */ - /** + /** * @desc Load SVG overlay from microdrawDB * @returns {Promise} A promise to return an array of paths of the current section. * @default returns an empty array. Can/should be overwritten in save.js. Users can use their own save.js for different backend. */ - microdrawDBLoad: function () { - return new Promise(function(resolve) { - if( me.debug>1 ) { console.log("> default microdrawDBLoad promise, returning an empty array. Overwrite Microdraw.microdrawDBLoad() to load annotations."); } - resolve([]); - }); - }, + microdrawDBLoad: function () { + return new Promise(function(resolve) { + if( me.debug>1 ) { console.log("> default microdrawDBLoad promise, returning an empty array. Overwrite Microdraw.microdrawDBLoad() to load annotations."); } + resolve([]); + }); + }, - /** + /** * @returns {void} */ - save: function () { - if( me.debug ) { console.log("> save"); } - - const obj = {}; - obj.Regions = []; - for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { - const el = {}; - el.path = me.ImageInfo[me.currentImage].Regions[i].path.exportJSON(); - el.name = me.ImageInfo[me.currentImage].Regions[i].name; - el.uid = me.ImageInfo[me.currentImage].Regions[i].uid; - obj.Regions.push(el); - } - localStorage.Microdraw = JSON.stringify(obj); + save: function () { + if( me.debug ) { console.log("> save"); } + + const obj = {}; + obj.Regions = []; + for( let i = 0; i < me.ImageInfo[me.currentImage].Regions.length; i += 1 ) { + const el = {}; + el.path = me.ImageInfo[me.currentImage].Regions[i].path.exportJSON(); + el.name = me.ImageInfo[me.currentImage].Regions[i].name; + el.uid = me.ImageInfo[me.currentImage].Regions[i].uid; + obj.Regions.push(el); + } + localStorage.Microdraw = JSON.stringify(obj); - if( me.debug>1 ) { - console.log("+ saved regions:", me.ImageInfo[me.currentImage].Regions.length); - } - }, + if( me.debug>1 ) { + console.log("+ saved regions:", me.ImageInfo[me.currentImage].Regions.length); + } + }, - /** + /** * @returns {void} */ - load: function () { - if( me.debug>1 ) { console.log("> load"); } - - if( localStorage.Microdraw ) { - console.log("Loading data from localStorage"); - const obj = JSON.parse(localStorage.Microdraw); - for( let i = 0; i < obj.Regions.length; i += 1 ) { - me.newRegion({ - name: obj.Regions[i].name, - uid: obj.Regions[i].uid, - path: me._pathFromJSON(obj.Regions[i].path) - }); - } - paper.view.draw(); + load: function () { + if( me.debug>1 ) { console.log("> load"); } + + if( localStorage.Microdraw ) { + console.log("Loading data from localStorage"); + const obj = JSON.parse(localStorage.Microdraw); + for( let i = 0; i < obj.Regions.length; i += 1 ) { + me.newRegion({ + name: obj.Regions[i].name, + uid: obj.Regions[i].uid, + path: me._pathFromJSON(obj.Regions[i].path) + }); } - }, + paper.view.draw(); + } + }, - /***5 + /***5 Initialisation */ - /** + /** * @param {number} imageNumber The image number * @returns {void} */ - loadImage: function (imageNumber) { - if( me.debug>1 ) { console.log("> loadImage(" + imageNumber + ")"); } + loadImage: function (imageNumber) { + if( me.debug>1 ) { console.log("> loadImage(" + imageNumber + ")"); } - // when load a new image, deselect any currently selecting regions - // n.b. this needs to be called before me.currentImage is set - me.selectRegion(null); + // when load a new image, deselect any currently selecting regions + // n.b. this needs to be called before me.currentImage is set + me.selectRegion(null); - // save previous image for some (later) cleanup - me.prevImage = me.currentImage; + // save previous image for some (later) cleanup + me.prevImage = me.currentImage; - // set current image to new image - me.currentImage = imageNumber; + // set current image to new image + me.currentImage = imageNumber; - // display slice number - me.dom.querySelector("#slice-number").innerHTML = `Slice ${imageNumber}`; + // display slice number + me.dom.querySelector("#slice-number").innerHTML = `Slice ${imageNumber}`; - me.viewer.open(me.ImageInfo[me.currentImage].source); - }, + me.viewer.open(me.ImageInfo[me.currentImage].source); + }, - /** + /** * @returns {void} */ - loadNextImage: function () { - if( me.debug>1 ) { console.log("> loadNextImage"); } - const index = me.imageOrder.indexOf(me.currentImage); - const nextIndex = (index + 1) % me.imageOrder.length; + loadNextImage: function () { + if( me.debug>1 ) { console.log("> loadNextImage"); } + const index = me.imageOrder.indexOf(me.currentImage); + const nextIndex = (index + 1) % me.imageOrder.length; - // update image slider - me.updateSliderValue(nextIndex); + // update image slider + me.updateSliderValue(nextIndex); - // update URL - me.updateURL(nextIndex); + // update URL + me.updateURL(nextIndex); - me.loadImage(me.imageOrder[nextIndex]); - }, + me.loadImage(me.imageOrder[nextIndex]); + }, - /** + /** * @returns {void} */ - loadPreviousImage: function () { - if(me.debug>1) { console.log("> loadPrevImage"); } - const index = me.imageOrder.indexOf(me.currentImage); - const previousIndex = ((index - 1 >= 0)? index - 1 : me.imageOrder.length - 1 ); + loadPreviousImage: function () { + if(me.debug>1) { console.log("> loadPrevImage"); } + const index = me.imageOrder.indexOf(me.currentImage); + const previousIndex = ((index - 1 >= 0)? index - 1 : me.imageOrder.length - 1 ); - // update image slider - me.updateSliderValue(previousIndex); + // update image slider + me.updateSliderValue(previousIndex); - // update URL - me.updateURL(previousIndex); + // update URL + me.updateURL(previousIndex); - me.loadImage(me.imageOrder[previousIndex]); - }, + me.loadImage(me.imageOrder[previousIndex]); + }, - /** + /** * @returns {void} */ - resizeAnnotationOverlay: function () { - if( me.debug>1 ) { console.log("> resizeAnnotationOverlay"); } - - const width = me.dom.querySelector("#paperjs-container").offsetWidth; - const height = me.dom.querySelector("#paperjs-container").offsetHeight; - me.dom.querySelector("canvas.overlay").offsetWidth = width; - me.dom.querySelector("canvas.overlay").offsetHeight = height; - paper.view.viewSize = [ - width, - height - ]; - me.transform(); - }, - - _convertDBAnnotationsToRegions: (data) => { - const regions = []; - let path; - for( let i = 0; i < data.length; i += 1 ) { - const json = data[i].annotation.path; - const [type] = json; - const reg = { - name: data[i].annotation.name, - uid: data[i].annotation.uid - }; - switch(type) { - case 'Path': { - path = me._pathFromJSON(json); - path.remove(); - break; - } - case 'CompoundPath': { - path = new paper.CompoundPath(); - path.importJSON(json); - path.remove(); - break; - } - default: - // catch future path types - path = me._pathFromJSON(json); - path.remove(); - } - reg.path = path; - regions.push(reg); + resizeAnnotationOverlay: function () { + if( me.debug>1 ) { console.log("> resizeAnnotationOverlay"); } + + const width = me.dom.querySelector("#paperjs-container").offsetWidth; + const height = me.dom.querySelector("#paperjs-container").offsetHeight; + me.dom.querySelector("canvas.overlay").offsetWidth = width; + me.dom.querySelector("canvas.overlay").offsetHeight = height; + paper.view.viewSize = [ + width, + height + ]; + me.transform(); + }, + + _convertDBAnnotationsToRegions: (data) => { + const regions = []; + let path; + for( let i = 0; i < data.length; i += 1 ) { + const json = data[i].annotation.path; + const [type] = json; + const reg = { + name: data[i].annotation.name, + uid: data[i].annotation.uid + }; + switch(type) { + case 'Path': { + path = me._pathFromJSON(json); + path.remove(); + break; } - return regions; - }, - - _addRegionsToCurrentImage: function(regions) { - for(const region of regions) { - // regions are added to the ImageInfo[currentImage]; - me.newRegion(region); + case 'CompoundPath': { + path = new paper.CompoundPath(); + path.importJSON(json); + path.remove(); + break; } - - // if image has no hash, save one - me.ImageInfo[me.currentImage].Hash = regions.Hash ? regions.Hash : me.sectionHash(me.ImageInfo[me.currentImage]); - }, - - _drawRegionsInPaper: function(regions) { - for(const region of regions) { - paper.project.activeLayer.addChild(region.path); + default: + // catch future path types + path = me._pathFromJSON(json); + path.remove(); } - me._resizePaperViewToMatchContainer(); - me.transform(); - paper.view.draw(); - }, - - _resizePaperViewToMatchContainer: () => { - const width = me.dom.querySelector("#paperjs-container").offsetWidth; - const height = me.dom.querySelector("#paperjs-container").offsetHeight; - paper.view.viewSize = [ - width, - height - ]; - paper.settings.handleSize = 10; - // me.updateRegionList(); - paper.view.draw(); - }, + reg.path = path; + regions.push(reg); + } - _createCanvasAndAddToPaper: () => { - const canvas = document.createElement("canvas"); - canvas.classList.add("overlay"); - canvas.id = me.currentImage; - me.dom.querySelector("#paperjs-container").appendChild(canvas); + return regions; + }, - // create project - paper.setup(canvas); - }, + _addRegionsToCurrentImage: function(regions) { + for(const region of regions) { + // regions are added to the ImageInfo[currentImage]; + me.newRegion(region); + } - /** + // if image has no hash, save one + me.ImageInfo[me.currentImage].Hash = regions.Hash ? regions.Hash : me.sectionHash(me.ImageInfo[me.currentImage]); + }, + + _drawRegionsInPaper: function(regions) { + for(const region of regions) { + paper.project.activeLayer.addChild(region.path); + } + me._resizePaperViewToMatchContainer(); + me.transform(); + paper.view.draw(); + }, + + _resizePaperViewToMatchContainer: () => { + const width = me.dom.querySelector("#paperjs-container").offsetWidth; + const height = me.dom.querySelector("#paperjs-container").offsetHeight; + paper.view.viewSize = [ + width, + height + ]; + paper.settings.handleSize = 10; + // me.updateRegionList(); + paper.view.draw(); + }, + + _createCanvasAndAddToPaper: () => { + const canvas = document.createElement("canvas"); + canvas.classList.add("overlay"); + canvas.id = me.currentImage; + me.dom.querySelector("#paperjs-container").appendChild(canvas); + + // create project + paper.setup(canvas); + }, + + /** * @returns {void} */ - initAnnotationOverlay: async () => { - if( me.debug>1 ) { console.log("> initAnnotationOverlay"); } + initAnnotationOverlay: async () => { + if( me.debug>1 ) { console.log("> initAnnotationOverlay"); } - // do not start loading a new annotation if a previous one is still being loaded - if(me.annotationLoadingFlag === true) { - return; - } + // do not start loading a new annotation if a previous one is still being loaded + if(me.annotationLoadingFlag === true) { + return; + } - // change current section index (for loading and saving) - me.section = me.currentImage; - me.fileID = `${me.source}`; + // change current section index (for loading and saving) + me.section = me.currentImage; + me.fileID = `${me.source}`; - paper.project.activeLayer.removeChildren(); + paper.project.activeLayer.removeChildren(); - let regions; - if(me.ImageInfo[me.currentImage].Regions.length > 0) { - ({Regions: regions} = me.ImageInfo[me.currentImage]); - } else { - // first time this section is accessed: create its canvas, project, - // and load its regions from the database - // me._createCanvasAndAddToPaper(); - - // load regions from database - if( me.config.useDatabase ) { - let data; - try { - data = await me.microdrawDBLoad(); - } catch(err) { - throw new Error(err); - } - regions = me._convertDBAnnotationsToRegions(data); - me._addRegionsToCurrentImage(regions); - } + let regions; + if(me.ImageInfo[me.currentImage].Regions.length > 0) { + ({Regions: regions} = me.ImageInfo[me.currentImage]); + } else { + // first time this section is accessed: create its canvas, project, + // and load its regions from the database + // me._createCanvasAndAddToPaper(); + + // load regions from database + // eslint-disable-next-line no-lonely-if + if( me.config.useDatabase ) { + const data = await me.microdrawDBLoad(); + regions = me._convertDBAnnotationsToRegions(data); + me._addRegionsToCurrentImage(regions); } + } - me._drawRegionsInPaper(regions); - }, + me._drawRegionsInPaper(regions); + }, - /** + /** * @returns {void} */ - transform: function () { - const z = me.viewer.viewport.viewportToImageZoom(me.viewer.viewport.getZoom(true)); - const sw = me.viewer.source.width; - const bounds = me.viewer.viewport.getBounds(true); - const [x, y, w, h] = [ - me.magicV * bounds.x, - me.magicV * bounds.y, - me.magicV * bounds.width, - me.magicV * bounds.height - ]; - paper.view.setCenter(x + (w/2), y + (h/2)); - paper.view.zoom = (sw * z) / me.magicV; - }, - - /** + transform: function () { + const z = me.viewer.viewport.viewportToImageZoom(me.viewer.viewport.getZoom(true)); + const sw = me.viewer.source.width; + const bounds = me.viewer.viewport.getBounds(true); + const [x, y, w, h] = [ + me.magicV * bounds.x, + me.magicV * bounds.y, + me.magicV * bounds.width, + me.magicV * bounds.height + ]; + paper.view.setCenter(x + (w/2), y + (h/2)); + paper.view.zoom = (sw * z) / me.magicV; + }, + + /** * @returns {Object} Returns an object containing URL parametres */ - deparam: function () { - if( me.debug>1 ) { console.log("> deparam"); } - - /** @todo Use URLSearchParams instead */ - const search = location.search.substring(1); - const result = search? - JSON.parse('{"' + search.replace(/[&]/g, '","').replace(/[=]/g, '":"') + '"}', - function(key, value) { return key === "" ? value : decodeURIComponent(value); }) : - {}; - if( me.debug>1 ) { - console.log("url parametres:", result); - } + deparam: function () { + if( me.debug>1 ) { console.log("> deparam"); } + + /** @todo Use URLSearchParams instead */ + const search = location.search.substring(1); + const result = search? + JSON.parse('{"' + search.replace(/[&]/g, '","').replace(/[=]/g, '":"') + '"}', + function(key, value) { return key === "" ? value : decodeURIComponent(value); }) : + {}; + if( me.debug>1 ) { + console.log("url parametres:", result); + } - return result; - }, + return result; + }, - /** + /** * @returns {void} Returns a promise that is fulfilled when the user is loged in */ - loginChanged: function () { - if( me.debug ) { console.log("> loginChanged"); } + loginChanged: function () { + if( me.debug ) { console.log("> loginChanged"); } - // updateUser(); + // updateUser(); - /** @todo Maybe log to db?? */ + /** @todo Maybe log to db?? */ - // remove all annotations and paper projects from old user - paper.project.activeLayer.removeChildren(); + // remove all annotations and paper projects from old user + paper.project.activeLayer.removeChildren(); - // load new users data - me.viewer.open(me.ImageInfo[me.currentImage].source); - }, + // load new users data + me.viewer.open(me.ImageInfo[me.currentImage].source); + }, - /** + /** * @returns {void} */ - initShortCutHandler: function () { - window.addEventListener("keydown", e => { - if (e.isComposing || e.keyCode === 229) { - return; - } - const key = []; - if( e.ctrlKey ) { key.push("^"); } - if( e.altKey ) { key.push("alt"); } - if( e.shiftKey ) { key.push("shift"); } - if( e.metaKey ) { key.push("cmd"); } - key.push(String.fromCharCode(e.keyCode)); - const code = key.join(" "); - if( me.shortCuts[code] ) { - const shortcut = me.shortCuts[code]; - shortcut(); - e.stopPropagation(); - } - }); - }, - - /** + initShortCutHandler: function () { + window.addEventListener("keydown", (e) => { + if (e.isComposing || e.keyCode === 229) { + return; + } + const key = []; + if( e.ctrlKey ) { key.push("^"); } + if( e.altKey ) { key.push("alt"); } + if( e.shiftKey ) { key.push("shift"); } + if( e.metaKey ) { key.push("cmd"); } + key.push(String.fromCharCode(e.keyCode)); + const code = key.join(" "); + if( me.shortCuts[code] ) { + const shortcut = me.shortCuts[code]; + shortcut(); + e.stopPropagation(); + } + }); + }, + + /** * @param {string} theKey Key used for the shortcut * @param {function} callback Function called for the specific key shortcut * @returns {void} */ - shortCutHandler: function (theKey, callback) { - let key = me.isMac?theKey.mac:theKey.pc; - const arr = key.split(" "); - for( let i = 0; i < arr.length; i += 1 ) { - if( arr[i].charAt(0) === "#" ) { - arr[i] = String.fromCharCode(parseInt(arr[i].substring(1), 10)); - } else - if( arr[i].length === 1 ) { - arr[i] = arr[i].toUpperCase(); - } + shortCutHandler: function (theKey, callback) { + let key = me.isMac?theKey.mac:theKey.pc; + const arr = key.split(" "); + for( let i = 0; i < arr.length; i += 1 ) { + if( arr[i].charAt(0) === "#" ) { + arr[i] = String.fromCharCode(parseInt(arr[i].substring(1), 10)); + } else + if( arr[i].length === 1 ) { + arr[i] = arr[i].toUpperCase(); } - key = arr.join(" "); - me.shortCuts[key] = callback; - }, + } + key = arr.join(" "); + me.shortCuts[key] = callback; + }, - /** + /** * @desc Initialises a slider to change between sections * @param {number} minVal Minimum value * @param {number} maxVal Maximum value @@ -1398,607 +1394,620 @@ const Microdraw = (function () { * @param {number} defaultValue Value at which the slider is initialised * @returns {void} */ - initSlider: function (minVal, maxVal, step, defaultValue) { - if( me.debug>1 ) { console.log("> initSlider promise"); } - const slider = me.dom.querySelector("#slice"); - if( slider ) { // only if slider could be found - slider.dataset.min = minVal; - slider.dataset.max = maxVal - 1; - slider.dataset.step = step; - slider.dataset.val = defaultValue; - - me.updateSliderDisplay(); - - // slider.on("change", function() { - // me.sliderOnChange(this.value); - // }); - - // Input event can only be used when not using database, otherwise the annotations will be loaded several times - /** @todo Fix the issue with the annotations for real */ - - // if (me.config.useDatabase === false) { - // slider.on("input", function () { - // me.sliderOnChange(this.value); - // }); - // } - } - }, + initSlider: function (minVal, maxVal, step, defaultValue) { + if( me.debug>1 ) { console.log("> initSlider promise"); } + const slider = me.dom.querySelector("#slice"); + if( slider ) { // only if slider could be found + slider.dataset.min = minVal; + slider.dataset.max = maxVal - 1; + slider.dataset.step = step; + slider.dataset.val = defaultValue; - /** + me.updateSliderDisplay(); + + // slider.on("change", function() { + // me.sliderOnChange(this.value); + // }); + + // Input event can only be used when not using database, otherwise the annotations will be loaded several times + /** @todo Fix the issue with the annotations for real */ + + // if (me.config.useDatabase === false) { + // slider.on("input", function () { + // me.sliderOnChange(this.value); + // }); + // } + } + }, + + /** * @desc Called when the slider value is changed to load a new section * @param {number} newImageNumber Index of the image selected using the slider * @returns {void} */ - sliderOnChange: function (newImageNumber) { - if( me.debug>1 ) { console.log("> sliderOnChange promise"); } - const imageNumber = me.imageOrder[newImageNumber]; - me.loadImage(imageNumber); - me.updateURL(imageNumber); - }, + sliderOnChange: function (newImageNumber) { + if( me.debug>1 ) { console.log("> sliderOnChange promise"); } + const imageNumber = me.imageOrder[newImageNumber]; + me.loadImage(imageNumber); + me.updateURL(imageNumber); + }, - /** + /** * @desc Used to update the slider value if the section was changed by another control * @param {number} newIndex section number to which the slider will be set * @returns {void} */ - updateSliderValue: function (newIndex) { - if( me.debug>1 ) { console.log("> updateSliderValue promise"); } - const slider = me.dom.querySelector("#slice"); - if( slider ) { // only if slider could be found - slider.dataset.val = newIndex; - me.updateSliderDisplay(); - } - }, + updateSliderValue: function (newIndex) { + if( me.debug>1 ) { console.log("> updateSliderValue promise"); } + const slider = me.dom.querySelector("#slice"); + if( slider ) { // only if slider could be found + slider.dataset.val = newIndex; + me.updateSliderDisplay(); + } + }, - updateSliderDisplay: () => { - let {val, max} = me.dom.querySelector("#slice").dataset; - const thumb = me.dom.querySelector("#slice .mui-thumb"); - val = Number(val); - max = Number(max); - thumb.style.left = (val*100/max) + "%"; - }, + updateSliderDisplay: () => { + let {val, max} = me.dom.querySelector("#slice").dataset; + const thumb = me.dom.querySelector("#slice .mui-thumb"); + val = Number(val); + max = Number(max); + thumb.style.left = (val*100/max) + "%"; + }, - /** + /** * Searches for the given section-number. * If the number could be found its index will be returned. Otherwise -1 * @param {String} numberStr Section number * @returns {void} */ - findSectionNumber: function (numberStr) { - const number = parseInt(numberStr, 10); // number = NaN if cast to int failed! - if( !isNaN(number) ) { - for( let i = 0; i < me.imageOrder.length; i += 1 ) { - const sectionNumber = parseInt(me.imageOrder[i], 10); - // Compare the int values because the string values might be different (e.g. "0001" != "1") - if( number === sectionNumber ) { - return i; - } + findSectionNumber: function (numberStr) { + const number = parseInt(numberStr, 10); // number = NaN if cast to int failed! + if( !isNaN(number) ) { + for( let i = 0; i < me.imageOrder.length; i += 1 ) { + const sectionNumber = parseInt(me.imageOrder[i], 10); + // Compare the int values because the string values might be different (e.g. "0001" != "1") + if( number === sectionNumber ) { + return i; } } + } - return -1; - }, + return -1; + }, - /** + /** * @param {object} event Event produced by the enter key * @returns {void} */ - sectionNameOnEnter: function (event) { - if( me.debug>1 ) { console.log("> sectionNameOnEnter promise"); } - if( event.keyCode === 13 ) { // enter key - const sectionNumber = this.value; - const index = me.findSectionNumber(sectionNumber); - if( index > -1 ) { // if section number exists - me.updateSliderValue(index); - me.loadImage(me.imageOrder[index]); - me.updateURL(index); - } + sectionNameOnEnter: function (event) { + if( me.debug>1 ) { console.log("> sectionNameOnEnter promise"); } + if( event.keyCode === 13 ) { // enter key + const sectionNumber = this.value; + const index = me.findSectionNumber(sectionNumber); + if( index > -1 ) { // if section number exists + me.updateSliderValue(index); + me.loadImage(me.imageOrder[index]); + me.updateURL(index); } - event.stopPropagation(); // prevent the default action (scroll / move caret) - }, + } + event.stopPropagation(); // prevent the default action (scroll / move caret) + }, - /** + /** * @desc Used to update the URL with the slice value if the section was changed by another control * @param {number} newIndex section number to which the URL will be set * @returns {void} */ - updateURL : function (newIndex) { - if( me.debug>1 ) { console.log('> updateURL'); } - const urlParams = new URLSearchParams(window.location.search); - urlParams.set('slice', newIndex); - const newURL = [ - window.location.protocol, - '//', - window.location.host, - window.location.pathname, - '?', - urlParams.toString() - ].join(''); - const stateObj = { - oldURL: newURL - }; - history.pushState(stateObj, "", newURL); - }, - - /** + updateURL : function (newIndex) { + if( me.debug>1 ) { console.log('> updateURL'); } + const urlParams = new URLSearchParams(window.location.search); + urlParams.set('slice', newIndex); + const newURL = [ + window.location.protocol, + '//', + window.location.host, + window.location.pathname, + '?', + urlParams.toString() + ].join(''); + const stateObj = { + oldURL: newURL + }; + history.pushState(stateObj, "", newURL); + }, + + /** * @desc Used to update the URL with the slice value if none is given by user * @param {number} newIndex section number to which the URL will be set * @returns {void} */ - addSliceToURL : function (newIndex) { - if( me.debug>1 ) { console.log('> addSliceToURL'); } - const urlParams = new URLSearchParams(window.location.search); - urlParams.set('slice', newIndex); - const newURL = [ - window.location.protocol, - '//', - window.location.host, - window.location.pathname, - '?', - urlParams.toString() - ].join(''); - const stateObj = { - oldURL: newURL - }; - history.pushState(stateObj, "", newURL); - }, - - /** + addSliceToURL : function (newIndex) { + if( me.debug>1 ) { console.log('> addSliceToURL'); } + const urlParams = new URLSearchParams(window.location.search); + urlParams.set('slice', newIndex); + const newURL = [ + window.location.protocol, + '//', + window.location.host, + window.location.pathname, + '?', + urlParams.toString() + ].join(''); + const stateObj = { + oldURL: newURL + }; + history.pushState(stateObj, "", newURL); + }, + + /** * @desc Load source json (from server) * @returns {promise} returns a promise, resolving as a microdraw compatible object */ - loadSourceJson : function () { - if( me.debug ) { console.log('> loadSourceJson'); } - - return new Promise((resolve, reject) => { - const directFetch = new Promise((rs, rj) => { - // decide between json (local) and jsonp (cross-origin) - let ext = me.params.source.split("."); - ext = ext[ext.length - 1]; - if( ext === "jsonp" ) { - if( me.debug ) { console.log("Reading cross-origin jsonp file"); } - $.ajax({ - type: 'GET', - url: me.params.source + "?callback=?", - jsonpCallback: 'f', - dataType: 'jsonp', - contentType: "application/json", - success: function(obj) { - rs(obj); - }, - error: function(err) { - rj(err); - } - }); - } else - if( ext === "json" ) { - if( me.debug ) { console.log("Reading local json file"); } - $.ajax({ - type: 'GET', - url: me.params.source, - dataType: "json", - contentType: "application/json", - success: function(obj) { - rs(obj); - }, - error: function(err) { - rj(err); - } - }); - } else { - fetch(me.params.source) - .then((data) => data.json()) - .then((json) => { - rs(json); - }) - .catch((e) => rj(e)); - } - }); + loadSourceJson : function () { + if( me.debug ) { console.log('> loadSourceJson'); } + + return new Promise((resolve, reject) => { + const directFetch = new Promise((rs, rj) => { + // decide between json (local) and jsonp (cross-origin) + let ext = me.params.source.split("."); + ext = ext[ext.length - 1]; + if( ext === "jsonp" ) { + if( me.debug ) { console.log("Reading cross-origin jsonp file"); } + $.ajax({ + type: 'GET', + url: me.params.source + "?callback=?", + jsonpCallback: 'f', + dataType: 'jsonp', + contentType: "application/json", + success: function(obj) { + rs(obj); + }, + error: function(err) { + rj(err); + } + }); + } else + if( ext === "json" ) { + if( me.debug ) { console.log("Reading local json file"); } + $.ajax({ + type: 'GET', + url: me.params.source, + dataType: "json", + contentType: "application/json", + success: function(obj) { + rs(obj); + }, + error: function(err) { + rj(err); + } + }); + } else { + fetch(me.params.source) + .then((data) => data.json()) + .then((json) => { + rs(json); + }) + .catch((e) => rj(e)); + } + }); - directFetch - .then( function (json) { - resolve(json); - }) - .catch( (err) => { - console.warn('> loadSourceJson : direct fetching of source failed ... ', err, 'attempting to fetch via microdraw server'); - - fetch('/getJson?source='+me.params.source) - .then((data) => data.json()) - .then((json) => { - resolve(json); - }) - .catch( (err2) => { - reject(err2); - }); - }); + directFetch + .then( function (json) { + resolve(json); + }) + .catch( (err) => { + console.warn('> loadSourceJson : direct fetching of source failed ... ', err, 'attempting to fetch via microdraw server'); + + fetch('/getJson?source='+me.params.source) + .then((data) => data.json()) + .then((json) => { + resolve(json); + }) + .catch( (err2) => { + reject(err2); + }); }); - }, + }); + }, - /** + /** * @desc Load general microdraw configuration * @returns {Promise} returns a promise that resolves when the configuration is loaded */ - loadConfiguration: async () => { - await Promise.all([ - me.loadScript("/lib/jquery-1.11.0.min.js"), - me.loadScript("/lib/paper-full-0.12.11.min.js"), - me.loadScript("/lib/openseadragon/openseadragon.js"), - me.loadScript("https://unpkg.com/hippy-hippo@0.0.1/dist/hippy-hippo-umd.js"), - me.loadScript("/js/microdraw-ws.js") - ]); - - await me.loadScript("/lib/openseadragon-viewerinputhook.min.js"); - - await Promise.all([ - me.loadScript("/lib/OpenSeadragonScalebar/openseadragon-scalebar.js"), - // me.loadScript("/lib/openseadragon-screenshot/openseadragonScreenshot.min.js"), - me.loadScript("https://cdn.jsdelivr.net/gh/r03ert0/Openseadragon-screenshot@v0.0.1/openseadragonScreenshot.js"), - me.loadScript("/lib/FileSaver.js/FileSaver.min.js"), - me.loadScript("/js/neurolex-ontology.js"), - me.loadScript("https://cdn.jsdelivr.net/gh/r03ert0/muijs@v0.1.2/mui.js"), - me.loadScript("https://unpkg.com/codeflask/build/codeflask.min.js"), - me.loadScript("https://cdn.jsdelivr.net/gh/r03ert0/consolita.js@0.2.1/consolita.js"), - - me.loadScript('/js/tools/fullscreen.js'), - me.loadScript('/js/tools/home.js'), - me.loadScript('/js/tools/navigate.js'), - me.loadScript('/js/tools/zoomIn.js'), - me.loadScript('/js/tools/zoomOut.js'), - me.loadScript('/js/tools/previous.js'), - me.loadScript('/js/tools/next.js'), - me.loadScript('/js/tools/closeMenu.js'), - me.loadScript('/js/tools/openMenu.js') - ]); - - $.extend(me.tools, ToolFullscreen); - $.extend(me.tools, ToolHome); - $.extend(me.tools, ToolNavigate); - $.extend(me.tools, ToolZoomIn); - $.extend(me.tools, ToolZoomOut); - $.extend(me.tools, ToolPrevious); - $.extend(me.tools, ToolNext); - $.extend(me.tools, ToolCloseMenu); - $.extend(me.tools, ToolOpenMenu); - - // load configuration file, then load the tools accordingly - const r = await fetch("/js/configuration.json") - const data = await r.json(); - me.config = data; - - // tools loaded dynamically, based on user configuration, server configuration etc. - data.presets.default.map( async (item) => { - // load script + extend me.tools - await me.loadScript(item.scriptPath); - - // there maybe multiple exported variables - item.exportedVar.forEach( (variable) => { - /** @todo use ES 6 for proper module import. eval should be avoided when possible */ - eval(`$.extend(me.tools,${variable})`); - }); + loadConfiguration: async () => { + await Promise.all([ + me.loadScript("/lib/jquery-1.11.0.min.js"), + me.loadScript("/lib/paper-full-0.12.11.min.js"), + me.loadScript("/lib/openseadragon/openseadragon.js"), + me.loadScript("https://unpkg.com/hippy-hippo@0.0.1/dist/hippy-hippo-umd.js"), + me.loadScript("/js/microdraw-ws.js") + ]); + + await me.loadScript("/lib/openseadragon-viewerinputhook.min.js"); + + await Promise.all([ + me.loadScript("/lib/OpenSeadragonScalebar/openseadragon-scalebar.js"), + // me.loadScript("/lib/openseadragon-screenshot/openseadragonScreenshot.min.js"), + me.loadScript("https://cdn.jsdelivr.net/gh/r03ert0/Openseadragon-screenshot@v0.0.1/openseadragonScreenshot.js"), + me.loadScript("/lib/FileSaver.js/FileSaver.min.js"), + me.loadScript("/js/neurolex-ontology.js"), + me.loadScript("https://cdn.jsdelivr.net/gh/r03ert0/muijs@v0.1.2/mui.js"), + me.loadScript("https://unpkg.com/codeflask/build/codeflask.min.js"), + me.loadScript("https://cdn.jsdelivr.net/gh/r03ert0/consolita.js@0.2.1/consolita.js"), + /* global Consolita */ + + me.loadScript('/js/tools/fullscreen.js'), + me.loadScript('/js/tools/home.js'), + me.loadScript('/js/tools/navigate.js'), + me.loadScript('/js/tools/zoomIn.js'), + me.loadScript('/js/tools/zoomOut.js'), + me.loadScript('/js/tools/previous.js'), + me.loadScript('/js/tools/next.js'), + me.loadScript('/js/tools/closeMenu.js'), + me.loadScript('/js/tools/openMenu.js') + ]); + + /* global ToolFullscreen */ + /* global ToolHome */ + /* global ToolNavigate */ + /* global ToolZoomIn */ + /* global ToolZoomOut */ + /* global ToolPrevious */ + /* global ToolNext */ + /* global ToolCloseMenu */ + /* global ToolOpenMenu */ + + $.extend(me.tools, ToolFullscreen); + $.extend(me.tools, ToolHome); + $.extend(me.tools, ToolNavigate); + $.extend(me.tools, ToolZoomIn); + $.extend(me.tools, ToolZoomOut); + $.extend(me.tools, ToolPrevious); + $.extend(me.tools, ToolNext); + $.extend(me.tools, ToolCloseMenu); + $.extend(me.tools, ToolOpenMenu); + + // load configuration file, then load the tools accordingly + const r = await fetch("/js/configuration.json"); + const data = await r.json(); + me.config = data; + + // tools loaded dynamically, based on user configuration, server configuration etc. + data.presets.default.map( async (item) => { + // load script + extend me.tools + await me.loadScript(item.scriptPath); + + // there maybe multiple exported variables + item.exportedVar.forEach( (variable) => { + + /** @todo use ES 6 for proper module import. eval should be avoided when possible */ + $.extend(me.tools, this[variable]); + // eval(`$.extend(me.tools,${variable})`); }); - }, + }); + }, - /** + /** * @desc Loads script from path if test is not fulfilled * @param {string} path Path to script, either a local path or a url * @param {function} testScriptPresent Function to test if the script is already present. If undefined, the script will be loaded. * @returns {promise} A promise fulfilled when the script is loaded */ - loadScript: function (path, testScriptPresent) { - return new Promise(function (resolve, reject) { - if(testScriptPresent && testScriptPresent()) { - console.log("[loadScript] Script", path, "already present, not loading it again"); - resolve(); - } - const s = document.createElement("script"); - s.src = path; - s.onload=function () { - console.log("Loaded", path); - resolve(); - }; - s.onerror=function() { - console.log("Error", path); - reject(new Error("something bad happened")); - }; - document.body.appendChild(s); - }); - }, + loadScript: function (path, testScriptPresent) { + return new Promise(function (resolve, reject) { + if(testScriptPresent && testScriptPresent()) { + console.log("[loadScript] Script", path, "already present, not loading it again"); + resolve(); + } + const s = document.createElement("script"); + s.src = path; + s.onload=function () { + console.log("Loaded", path); + resolve(); + }; + s.onerror=function() { + console.log("Error", path); + reject(new Error("something bad happened")); + }; + document.body.appendChild(s); + }); + }, - /** + /** * @desc Changes the way in which the toolbar is displayed * @param {string} display Position where the toolbar is displayed * @returns {void} */ - changeToolbarDisplay: function (display) { - switch(display) { - case "minimize": - me.dom.querySelector("#tools-maximized").style.display = "none"; - me.dom.querySelector("#tools-minimized").style.display = "block"; - break; - case "maximize": - me.dom.querySelector("#tools-maximized").style.display = "block"; - me.dom.querySelector("#tools-minimized").style.display = "none"; - break; - case "left": - me.dom.querySelector("body").setAttribute("data-toolbarDisplay", "left"); - break; - case "right": - me.dom.querySelector("body").setAttribute("data-toolbarDisplay", "right"); - break; - } - }, + changeToolbarDisplay: function (display) { + switch(display) { + case "minimize": + me.dom.querySelector("#tools-maximized").style.display = "none"; + me.dom.querySelector("#tools-minimized").style.display = "block"; + break; + case "maximize": + me.dom.querySelector("#tools-maximized").style.display = "block"; + me.dom.querySelector("#tools-minimized").style.display = "none"; + break; + case "left": + me.dom.querySelector("body").setAttribute("data-toolbarDisplay", "left"); + break; + case "right": + me.dom.querySelector("body").setAttribute("data-toolbarDisplay", "right"); + break; + } + }, - /** + /** * @param {string} mode One from Chat or Script * @returns {void} */ - toggleTextInput: function (mode) { - switch(mode) { - case "Chat": - me.dom.querySelector("#textInputBlock").style.display = "block"; - me.dom.getElementById("logScript").classList.add("hidden"); - me.dom.getElementById("logChat").classList.remove("hidden"); - me.dom.querySelector("#logChat #msg").focus(); - break; - case "Script": - me.dom.querySelector("#textInputBlock").style.display = "block"; - me.dom.getElementById("logScript").classList.remove("hidden"); - me.dom.getElementById("logChat").classList.add("hidden"); - me.dom.querySelector("#logScript textarea").focus(); - break; - default: - me.dom.querySelector("#textInputBlock").style.display = "none"; - } - }, + toggleTextInput: function (mode) { + switch(mode) { + case "Chat": + me.dom.querySelector("#textInputBlock").style.display = "block"; + me.dom.getElementById("logScript").classList.add("hidden"); + me.dom.getElementById("logChat").classList.remove("hidden"); + me.dom.querySelector("#logChat #msg").focus(); + break; + case "Script": + me.dom.querySelector("#textInputBlock").style.display = "block"; + me.dom.getElementById("logScript").classList.remove("hidden"); + me.dom.getElementById("logChat").classList.add("hidden"); + me.dom.querySelector("#logScript textarea").focus(); + break; + default: + me.dom.querySelector("#textInputBlock").style.display = "none"; + } + }, - /** + /** * @returns {void} */ - initMicrodraw: async () => { - if( me.debug>1 ) { console.log("> initMicrodraw promise"); } - - // Enable click on toolbar buttons - Array.prototype.forEach.call(me.dom.querySelectorAll('#buttonsBlock div.mui.push'), (el) => { - el.addEventListener('click', me.toolSelection); - }); - MUI.toggle(me.dom.querySelector("#fullscreen"), () => { me.clickTool("fullscreen"); }); - MUI.push(me.dom.querySelector("#sliderBlock #previous"), () => { me.clickTool("previous"); }); - MUI.push(me.dom.querySelector("#sliderBlock #next"), () => { me.clickTool("next"); }); - MUI.slider(me.dom.querySelector("#sliderBlock #slice"), (x) => { - const newImageNumber = Math.round((me.imageOrder.length-1)*x/100); - me.sliderOnChange(newImageNumber); + initMicrodraw: async () => { + if( me.debug>1 ) { console.log("> initMicrodraw promise"); } + + // Enable click on toolbar buttons + Array.prototype.forEach.call(me.dom.querySelectorAll('#buttonsBlock div.mui.push'), (el) => { + el.addEventListener('click', me.toolSelection); + }); + MUI.toggle(me.dom.querySelector("#fullscreen"), () => { me.clickTool("fullscreen"); }); + MUI.push(me.dom.querySelector("#sliderBlock #previous"), () => { me.clickTool("previous"); }); + MUI.push(me.dom.querySelector("#sliderBlock #next"), () => { me.clickTool("next"); }); + MUI.slider(me.dom.querySelector("#sliderBlock #slice"), (x) => { + const newImageNumber = Math.round((me.imageOrder.length-1)*x/100); + me.sliderOnChange(newImageNumber); + }); + MUI.chose(me.dom.querySelector("#clickTool.mui-chose"), (title) => { + const el = me.dom.querySelector(`[title="${title}"]`); + const tool = el.id; + me.clickTool(tool); + }); + + // set annotation loading flag to false + me.annotationLoadingFlag = false; + + // Initialize the control key handler and set shortcuts + me.initShortCutHandler(); + me.shortCutHandler({pc:'^ z', mac:'cmd z'}, me.cmdUndo); + me.shortCutHandler({pc:'shift ^ z', mac:'shift cmd z'}, me.cmdRedo); + if( me.config.drawingEnabled ) { + me.shortCutHandler({pc:'^ x', mac:'cmd x'}, function () { + console.log("cut!"); }); - MUI.chose(me.dom.querySelector("#clickTool.mui-chose"), (title) => { - const el = me.dom.querySelector(`[title="${title}"]`); - const tool = el.id; - me.clickTool(tool); + me.shortCutHandler({pc:'^ v', mac:'cmd v'}, me.cmdPaste); + me.shortCutHandler({pc:'^ a', mac:'cmd a'}, function () { + console.log("select all!"); }); + me.shortCutHandler({pc:'^ c', mac:'cmd c'}, me.cmdCopy); + me.shortCutHandler({pc:'#46', mac:'#8'}, me.cmdDeleteSelected); // delete key + } + me.shortCutHandler({pc:'#37', mac:'#37'}, me.loadPreviousImage); // left-arrow key + me.shortCutHandler({pc:'#39', mac:'#39'}, me.loadNextImage); // right-arrow key - // set annotation loading flag to false - me.annotationLoadingFlag = false; + // Configure currently selected tool + me.selectedTool = "navigate"; - // Initialize the control key handler and set shortcuts - me.initShortCutHandler(); - me.shortCutHandler({pc:'^ z', mac:'cmd z'}, me.cmdUndo); - me.shortCutHandler({pc:'shift ^ z', mac:'shift cmd z'}, me.cmdRedo); - if( me.config.drawingEnabled ) { - me.shortCutHandler({pc:'^ x', mac:'cmd x'}, function () { - console.log("cut!"); - }); - me.shortCutHandler({pc:'^ v', mac:'cmd v'}, me.cmdPaste); - me.shortCutHandler({pc:'^ a', mac:'cmd a'}, function () { - console.log("select all!"); - }); - me.shortCutHandler({pc:'^ c', mac:'cmd c'}, me.cmdCopy); - me.shortCutHandler({pc:'#46', mac:'#8'}, me.cmdDeleteSelected); // delete key - } - me.shortCutHandler({pc:'#37', mac:'#37'}, me.loadPreviousImage); // left-arrow key - me.shortCutHandler({pc:'#39', mac:'#39'}, me.loadNextImage); // right-arrow key + document.body.dataset.toolbardisplay = "left"; + me.dom.querySelector("#tools-minimized").style.display = "none"; + me.dom.querySelector("#tools-minimized").addEventListener("click", () => { me.changeToolbarDisplay("maximize"); }); + MUI.push(me.dom.querySelector(".push#display-minimize"), () => { me.changeToolbarDisplay("minimize"); }); + MUI.push(me.dom.querySelector(".push#display-left"), () => { me.changeToolbarDisplay("left"); }); + MUI.push(me.dom.querySelector(".push#display-right"), () => { me.changeToolbarDisplay("right"); }); - // Configure currently selected tool - me.selectedTool = "navigate"; + MUI.chose3state(me.dom.querySelector("#text.mui-chose"), me.toggleTextInput); - document.body.dataset.toolbardisplay = "left"; - me.dom.querySelector("#tools-minimized").style.display = "none"; - me.dom.querySelector("#tools-minimized").addEventListener("click", () => { me.changeToolbarDisplay("maximize"); }); - MUI.push(me.dom.querySelector(".push#display-minimize"), () => { me.changeToolbarDisplay("minimize"); }); - MUI.push(me.dom.querySelector(".push#display-left"), () => { me.changeToolbarDisplay("left"); }); - MUI.push(me.dom.querySelector(".push#display-right"), () => { me.changeToolbarDisplay("right"); }); - - MUI.chose3state(me.dom.querySelector("#text.mui-chose"), me.toggleTextInput); + Consolita.init(me.dom.querySelector("#logScript"), me.dom); - Consolita.init(me.dom.querySelector("#logScript"), me.dom); - - $(window).resize(function() { - me.resizeAnnotationOverlay(); - }); + $(window).resize(function() { + me.resizeAnnotationOverlay(); + }); - // Load regions label set - const res = await fetch("/js/10regions.json"); - const labels = await res.json(); - me.ontology = labels; - me.updateLabelDisplay(); - }, + // Load regions label set + const res = await fetch("/js/10regions.json"); + const labels = await res.json(); + me.ontology = labels; + me.updateLabelDisplay(); + }, - /** + /** * @param {Object} obj DZI json configuration object * @returns {void} */ - initOpenSeadragon: function (obj) { - if( me.debug>1 ) { console.log("json file:", obj); } + initOpenSeadragon: function (obj) { + if( me.debug>1 ) { console.log("json file:", obj); } - // for loading the bigbrain - if( obj.tileCodeY ) { - obj.tileSources = obj.tileCodeY; - } + // for loading the bigbrain + if( obj.tileCodeY ) { + obj.tileSources = obj.tileCodeY; + } - // set up the ImageInfo array and me.imageOrder array - for( let i = 0; i < obj.tileSources.length; i += 1 ) { - // name is either the index of the tileSource or a named specified in the json file - const name = ((obj.names && obj.names[i]) ? String(obj.names[i]) : String(i)); - me.imageOrder.push(name); - me.ImageInfo[name] = { - source: obj.tileSources[i], - Regions: [], - RegionsToRemove: [] - }; - // if getTileUrl is specified, we might need to eval it to get the function - if( obj.tileSources[i].getTileUrl && typeof obj.tileSources[i].getTileUrl === 'string' ) { - eval(`me.ImageInfo[name].source.getTileUrl = ${obj.tileSources[i].getTileUrl}`) - } - } + // set up the ImageInfo array and me.imageOrder array + for( let i = 0; i < obj.tileSources.length; i += 1 ) { + // name is either the index of the tileSource or a named specified in the json file + const name = ((obj.names && obj.names[i]) ? String(obj.names[i]) : String(i)); + me.imageOrder.push(name); + me.ImageInfo[name] = { + source: obj.tileSources[i], + Regions: [], + RegionsToRemove: [] + }; + // if getTileUrl is specified, we might need to eval it to get the function + // if( obj.tileSources[i].getTileUrl && typeof obj.tileSources[i].getTileUrl === 'string' ) { + // eval(`me.ImageInfo[name].source.getTileUrl = ${obj.tileSources[i].getTileUrl}`); + // } + } - // set default values for new regions (general configuration) - if (typeof me.config.defaultStrokeColor === "undefined") { - me.config.defaultStrokeColor = 'black'; - } - if (typeof me.config.defaultStrokeWidth === "undefined") { - me.config.defaultStrokeWidth = 1; - } - if (typeof me.config.defaultFillAlpha === "undefined") { - me.config.defaultFillAlpha = 0.5; - } - // set default values for new regions (per-brain configuration) - if (obj.configuration) { - if (typeof obj.configuration.defaultStrokeColor !== "undefined") { - me.config.defaultStrokeColor = obj.configuration.defaultStrokeColor; - } - if (typeof obj.configuration.defaultStrokeWidth !== "undefined") { - me.config.defaultStrokeWidth = obj.configuration.defaultStrokeWidth; - } - if (typeof obj.configuration.defaultFillAlpha !== "undefined") { - me.config.defaultFillAlpha = obj.configuration.defaultFillAlpha; - } + // set default values for new regions (general configuration) + if (typeof me.config.defaultStrokeColor === "undefined") { + me.config.defaultStrokeColor = 'black'; + } + if (typeof me.config.defaultStrokeWidth === "undefined") { + me.config.defaultStrokeWidth = 1; + } + if (typeof me.config.defaultFillAlpha === "undefined") { + me.config.defaultFillAlpha = 0.5; + } + // set default values for new regions (per-brain configuration) + if (obj.configuration) { + if (typeof obj.configuration.defaultStrokeColor !== "undefined") { + me.config.defaultStrokeColor = obj.configuration.defaultStrokeColor; } - - // init slider that can be used to change between slides - - if(me.params.slice === "undefined" || typeof me.params.slice === "undefined") { // this is correct: the string "undefined", or the type - me.initSlider(0, obj.tileSources.length, 1, Math.round(obj.tileSources.length / 2)); - const newIndex = Math.floor(obj.tileSources.length / 2) - me.currentImage = me.imageOrder[newIndex]; - me.addSliceToURL(newIndex); - } else { - me.initSlider(0, obj.tileSources.length, 1, me.params.slice); - me.currentImage = me.imageOrder[[parseInt(me.params.slice, 10)]]; + if (typeof obj.configuration.defaultStrokeWidth !== "undefined") { + me.config.defaultStrokeWidth = obj.configuration.defaultStrokeWidth; } - - // display slice number - me.dom.querySelector("#slice-number").innerHTML = `Slice ${me.currentImage}`; - - me.params.tileSources = obj.tileSources; - if (typeof obj.fileID !== 'undefined') { - me.fileID = obj.fileID; - } else { - me.fileID = me.source + '_' + me.section; + if (typeof obj.configuration.defaultFillAlpha !== "undefined") { + me.config.defaultFillAlpha = obj.configuration.defaultFillAlpha; } - me.viewer = new OpenSeadragon({ - // id: "openseadragon1", - element: me.dom.querySelector("#openseadragon1"), - prefixUrl: "/lib/openseadragon/images/", - tileSources: [], - showReferenceStrip: false, - referenceStripSizeRatio: 0.2, - showNavigator: true, - sequenceMode: false, - // navigatorId: "myNavigator", - navigatorPosition: "BOTTOM_RIGHT", - homeButton:"homee", - maxZoomPixelRatio:10, - preserveViewport: true - }); - - // open the currentImage - me.viewer.open(me.ImageInfo[me.currentImage].source); - - // add the scalebar - me.viewer.scalebar({ - type: OpenSeadragon.ScalebarType.MICROSCOPE, - minWidth:'150px', - pixelsPerMeter:obj.pixelsPerMeter, - color:'black', - fontColor:'black', - backgroundColor:"rgba(255, 255, 255, 0.5)", - barThickness:4, - location: OpenSeadragon.ScalebarLocation.TOP_RIGHT, - xOffset:5, - yOffset:5 - }); - - /* fixes https://github.com/r03ert0/microdraw/issues/142 */ - me.viewer.scalebarInstance.divElt.style.pointerEvents = `none`; + } - // add screenshot - me.viewer.screenshot({ - showOptions: false, // Default is false - // keyboardShortcut: 'p', // Default is null - // showScreenshotControl: true // Default is true - }); + // init slider that can be used to change between slides - // initialise paperjs - me._createCanvasAndAddToPaper(); + if(me.params.slice === "undefined" || typeof me.params.slice === "undefined") { // this is correct: the string "undefined", or the type + me.initSlider(0, obj.tileSources.length, 1, Math.round(obj.tileSources.length / 2)); + const newIndex = Math.floor(obj.tileSources.length / 2); + me.currentImage = me.imageOrder[newIndex]; + me.addSliceToURL(newIndex); + } else { + me.initSlider(0, obj.tileSources.length, 1, me.params.slice); + me.currentImage = me.imageOrder[[parseInt(me.params.slice, 10)]]; + } - // add handlers: update section name, animation, page change, mouse actions - me.viewer.addHandler('open', function () { - me.initAnnotationOverlay(); - }); - me.viewer.addHandler('animation', me.transform); - me.viewer.addHandler("animation-start", function () { - me.isAnimating = true; - }); - me.viewer.addHandler("animation-finish", function () { - me.isAnimating = false; - }); - me.viewer.addHandler("page", function (data) { - console.log("page", data.page, me.params.tileSources[data.page]); - }); - me.viewer.addViewerInputHook({hooks: [ - {tracker: 'viewer', handler: 'clickHandler', hookHandler: me.clickHandler}, - {tracker: 'viewer', handler: 'pressHandler', hookHandler: me.pressHandler}, - {tracker: 'viewer', handler: 'releaseHandler', hookHandler: me.releaseHandler}, - {tracker: 'viewer', handler: 'dragHandler', hookHandler: me.dragHandler}, - // {tracker: 'viewer', handler: 'dragEndHandler', hookHandler: me.dragEndHandler}, - {tracker: 'viewer', handler: 'scrollHandler', hookHandler: me.scrollHandler} - ]}); - - if( me.debug>1 ) { console.log("< initOpenSeadragon resolve: success"); } - }, + // display slice number + me.dom.querySelector("#slice-number").innerHTML = `Slice ${me.currentImage}`; - /** + me.params.tileSources = obj.tileSources; + if (typeof obj.fileID !== 'undefined') { + me.fileID = obj.fileID; + } else { + me.fileID = me.source + '_' + me.section; + } + me.viewer = new OpenSeadragon({ + // id: "openseadragon1", + element: me.dom.querySelector("#openseadragon1"), + prefixUrl: "/lib/openseadragon/images/", + tileSources: [], + showReferenceStrip: false, + referenceStripSizeRatio: 0.2, + showNavigator: true, + sequenceMode: false, + // navigatorId: "myNavigator", + navigatorPosition: "BOTTOM_RIGHT", + homeButton:"homee", + maxZoomPixelRatio:10, + preserveViewport: true + }); + + // open the currentImage + me.viewer.open(me.ImageInfo[me.currentImage].source); + + // add the scalebar + me.viewer.scalebar({ + type: OpenSeadragon.ScalebarType.MICROSCOPE, + minWidth:'150px', + pixelsPerMeter:obj.pixelsPerMeter, + color:'black', + fontColor:'black', + backgroundColor:"rgba(255, 255, 255, 0.5)", + barThickness:4, + location: OpenSeadragon.ScalebarLocation.TOP_RIGHT, + xOffset:5, + yOffset:5 + }); + + /* fixes https://github.com/r03ert0/microdraw/issues/142 */ + me.viewer.scalebarInstance.divElt.style.pointerEvents = `none`; + + // add screenshot + me.viewer.screenshot({ + showOptions: false // Default is false + // keyboardShortcut: 'p', // Default is null + // showScreenshotControl: true // Default is true + }); + + // initialise paperjs + me._createCanvasAndAddToPaper(); + + // add handlers: update section name, animation, page change, mouse actions + me.viewer.addHandler('open', function () { + me.initAnnotationOverlay(); + }); + me.viewer.addHandler('animation', me.transform); + me.viewer.addHandler("animation-start", function () { + me.isAnimating = true; + }); + me.viewer.addHandler("animation-finish", function () { + me.isAnimating = false; + }); + me.viewer.addHandler("page", function (data) { + console.log("page", data.page, me.params.tileSources[data.page]); + }); + me.viewer.addViewerInputHook({hooks: [ + {tracker: 'viewer', handler: 'clickHandler', hookHandler: me.clickHandler}, + {tracker: 'viewer', handler: 'pressHandler', hookHandler: me.pressHandler}, + {tracker: 'viewer', handler: 'releaseHandler', hookHandler: me.releaseHandler}, + {tracker: 'viewer', handler: 'dragHandler', hookHandler: me.dragHandler}, + // {tracker: 'viewer', handler: 'dragEndHandler', hookHandler: me.dragEndHandler}, + {tracker: 'viewer', handler: 'scrollHandler', hookHandler: me.scrollHandler} + ]}); + + if( me.debug>1 ) { console.log("< initOpenSeadragon resolve: success"); } + }, + + /** * @return {void} */ - toggleMenu: function () { - if( me.dom.querySelector('#menuBar').style.display === 'none' ) { - me.dom.querySelector('#menuBar').style.display = 'block'; - me.dom.querySelector('#menuButton').style.display = 'none'; - } else { - me.dom.querySelector('#menuBar').style.display = 'none'; - me.dom.querySelector('#menuButton').style.display = 'block'; - } - }, + toggleMenu: function () { + if( me.dom.querySelector('#menuBar').style.display === 'none' ) { + me.dom.querySelector('#menuBar').style.display = 'block'; + me.dom.querySelector('#menuButton').style.display = 'none'; + } else { + me.dom.querySelector('#menuBar').style.display = 'none'; + me.dom.querySelector('#menuButton').style.display = 'block'; + } + }, - init: async function (dom) { - me.dom = dom; + init: async function (dom) { + me.dom = dom; - await me.loadConfiguration(); + await me.loadConfiguration(); - me.params = me.deparam(); + me.params = me.deparam(); - if( me.config.useDatabase ) { - me.section = me.currentImage; - me.source = me.params.source; - if(typeof me.params.project !== 'undefined') { - me.project = me.params.project; - } + if( me.config.useDatabase ) { + me.section = me.currentImage; + me.source = me.params.source; + if(typeof me.params.project !== 'undefined') { + me.project = me.params.project; } + } - me.initMicrodraw(); + me.initMicrodraw(); - const json = await me.loadSourceJson(); - me.initOpenSeadragon(json); - } + const json = await me.loadSourceJson(); + me.initOpenSeadragon(json); + } }; - + return me; }()); diff --git a/app/public/js/neurolex-ontology.js b/app/public/js/neurolex-ontology.js index 3070be9..1a335cc 100755 --- a/app/public/js/neurolex-ontology.js +++ b/app/public/js/neurolex-ontology.js @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars */ +/* exported Ontology */ const Ontology = [ { name:"Telencephalon", diff --git a/app/public/js/tools/save.js b/app/public/js/tools/save.js index 8827f36..6be0d89 100644 --- a/app/public/js/tools/save.js +++ b/app/public/js/tools/save.js @@ -40,13 +40,14 @@ const _dialog = async ({el, message, doFadeOut=true, delay=2000, background="#33 fadeOut(el); } resolve(); - }, delay); + }, delay); }); }; -var ToolSave = { save: (function() { +window.ToolSave = { save: (function() { - const _processOneSection = function (sl) { + // eslint-disable-next-line max-statements + const _processOneSection = async function (sl) { if ((Microdraw.config.multiImageSave === false) && (sl !== Microdraw.currentImage)) { return; @@ -67,33 +68,27 @@ var ToolSave = { save: (function() { value.Hash = h; - const pr = new Promise(async (resolve, reject) => { - let res, req; - try { - req = await fetch('/api', { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - action: 'save', - source: Microdraw.source, - slice: sl, - project: Microdraw.project, - Hash: h, - annotation: JSON.stringify(value) - }) - }); - res = await req.json(); + const res = await fetch('/api', { + method: "POST", + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + action: 'save', + source: Microdraw.source, + slice: sl, + project: Microdraw.project, + Hash: h, + annotation: JSON.stringify(value) + }) + }); - // update hash - section.Hash = h; + if (!res.ok) { + throw await res.json(); + } - resolve(sl); - } catch(err) { - reject(err); - } - }); + // update hash + section.Hash = h; - return pr; + return sl; }; const _savingFeedback = async function (savedSections) { @@ -108,8 +103,8 @@ var ToolSave = { save: (function() { await _dialog({el, message, doFadeOut: false}); } }; - - const _successFeedback = function (savedSections) { + + const _successFeedback = function () { const el = Microdraw.dom.querySelector('#saveDialog'); _dialog({el, message: "Successfully saved", delay: 1000, background: "#2a3"}); }; diff --git a/package-lock.json b/package-lock.json index 6213dfc..8031534 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "monk": "^7.1.1", "multer": "^1.4.1", "mustache-express": "latest", - "neuroweblab": "github:neuroanatomy/neuroweblab#43eab1a99c9fffe4ff258e31733c600fe447959f", + "neuroweblab": "github:neuroanatomy/neuroweblab", "passport": "^0.4.0", "passport-github": "latest", "passport-local": "^1.0.0", @@ -3399,9 +3399,9 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.4.2.tgz", - "integrity": "sha512-mZUXF6nUzRWk5J3h41MsPv13ukWlH4jOMSk6astVeoZ1EbdTJyF5I3wxKkvqBAOoVtzLgyEYUvDjrGdcPlKjAw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.1.tgz", + "integrity": "sha512-0GAJKc1LBXzlWPhtj9uGawIlYSkTXkgpW9wZ97b4ySEuKbE5j9a0OdLGM31AWMhRS2ut49Z0kufSYsamGEIb8Q==", "peer": true, "dependencies": { "@types/whatwg-url": "^8.2.1", @@ -3600,8 +3600,7 @@ }, "node_modules/neuroweblab": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/neuroanatomy/neuroweblab.git#43eab1a99c9fffe4ff258e31733c600fe447959f", - "integrity": "sha512-VwXSn9yU7jOKKna2A8hIuBGYKXyixwLquwDfT+P2kJMW8UQJhfGjDNsPusl0xMU6JWT7YCLIbAF/W/RqJNN0eA==", + "resolved": "git+ssh://git@github.com/neuroanatomy/neuroweblab.git#38bc9cad27d890c75d0ff32317bb6a3e0f570fb1", "license": "ISC", "dependencies": { "bcrypt": "^5.0.1", @@ -7767,9 +7766,9 @@ } }, "mongodb-connection-string-url": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.4.2.tgz", - "integrity": "sha512-mZUXF6nUzRWk5J3h41MsPv13ukWlH4jOMSk6astVeoZ1EbdTJyF5I3wxKkvqBAOoVtzLgyEYUvDjrGdcPlKjAw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.1.tgz", + "integrity": "sha512-0GAJKc1LBXzlWPhtj9uGawIlYSkTXkgpW9wZ97b4ySEuKbE5j9a0OdLGM31AWMhRS2ut49Z0kufSYsamGEIb8Q==", "peer": true, "requires": { "@types/whatwg-url": "^8.2.1", @@ -7920,9 +7919,8 @@ "version": "0.6.2" }, "neuroweblab": { - "version": "git+ssh://git@github.com/neuroanatomy/neuroweblab.git#43eab1a99c9fffe4ff258e31733c600fe447959f", - "integrity": "sha512-VwXSn9yU7jOKKna2A8hIuBGYKXyixwLquwDfT+P2kJMW8UQJhfGjDNsPusl0xMU6JWT7YCLIbAF/W/RqJNN0eA==", - "from": "neuroweblab@github:neuroanatomy/neuroweblab#43eab1a99c9fffe4ff258e31733c600fe447959f", + "version": "git+ssh://git@github.com/neuroanatomy/neuroweblab.git#38bc9cad27d890c75d0ff32317bb6a3e0f570fb1", + "from": "neuroweblab@neuroanatomy/neuroweblab", "requires": { "bcrypt": "^5.0.1", "connect-mongo": "^4.4.1", diff --git a/package.json b/package.json index 2c81ef7..8d47542 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "monk": "^7.1.1", "multer": "^1.4.1", "mustache-express": "latest", - "neuroweblab": "github:neuroanatomy/neuroweblab#43eab1a99c9fffe4ff258e31733c600fe447959f", + "neuroweblab": "github:neuroanatomy/neuroweblab", "passport": "^0.4.0", "passport-github": "latest", "passport-local": "^1.0.0", diff --git a/test/e2e/microdraw.addRegion.spec.js b/test/e2e/microdraw.addRegion.spec.js index 7037d77..31fe6d9 100644 --- a/test/e2e/microdraw.addRegion.spec.js +++ b/test/e2e/microdraw.addRegion.spec.js @@ -37,6 +37,7 @@ describe('Editing tools: Add regions', () => { assert(diff<1000, `${diff} pixels were different`); }).timeout(0); + // eslint-disable-next-line max-statements it('draws a square', async () => { // select the polygon tool await shadowclick(UI.DRAWPOLYGON); @@ -47,6 +48,7 @@ describe('Editing tools: Add regions', () => { await page.mouse.click(400, 500); await page.mouse.click(400, 400); + await U.waitUntilHTMLRendered(page); const filename = "addRegion.02.cat-square-A.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -61,6 +63,7 @@ describe('Editing tools: Add regions', () => { await page.mouse.click(450, 550); await page.mouse.click(450, 450); + await U.waitUntilHTMLRendered(page); const filename = "addRegion.03.cat-square-B.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -77,6 +80,7 @@ describe('Editing tools: Add regions', () => { // click on square B (square A is already selected) await page.mouse.click(540, 540); + await U.waitUntilHTMLRendered(page); const filename = "addRegion.04.cat-union.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); diff --git a/test/e2e/microdraw.multiple.spec.js b/test/e2e/microdraw.multiple.spec.js index 2129373..6013846 100644 --- a/test/e2e/microdraw.multiple.spec.js +++ b/test/e2e/microdraw.multiple.spec.js @@ -54,6 +54,7 @@ describe('Editing tools: draw polygons and curves', () => { await page2.mouse.click(400, 200); await page2.mouse.click(400, 100); await shadowclick(UI.SAVE, page2); + await U.waitUntilHTMLRendered(page2); const filename = "multiple.04.page2-square.png"; await page2.screenshot({path: U.newPath + filename}); @@ -61,6 +62,7 @@ describe('Editing tools: draw polygons and curves', () => { assert(diff { await shadowclick(UI.DRAWPOLYGON, page1); await page1.mouse.click(300, 100); @@ -68,6 +70,7 @@ describe('Editing tools: draw polygons and curves', () => { await page1.mouse.click(350, 200); await page1.mouse.click(300, 100); await shadowclick(UI.SAVE, page1); + await U.waitUntilHTMLRendered(page1); const filename = "multiple.03.page1-triangle.png"; await page1.screenshot({path: U.newPath + filename}); @@ -96,6 +99,7 @@ describe('Editing tools: draw polygons and curves', () => { await shadowclick(UI.DELETE, page1); await shadowclick(UI.SAVE, page1); + await U.waitUntilHTMLRendered(page1); const filename = "multiple.06.page1-cleanup.png"; await page1.screenshot({path: U.newPath + filename}); diff --git a/test/e2e/microdraw.order.spec.js b/test/e2e/microdraw.order.spec.js index 0306d9c..bbbf772 100644 --- a/test/e2e/microdraw.order.spec.js +++ b/test/e2e/microdraw.order.spec.js @@ -53,12 +53,14 @@ describe('Editing tools: order', () => { await shadowclick(UI.SELECT); await page.mouse.click(500, 100); + await U.waitUntilHTMLRendered(page); const filename = "order.02.triangles.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); assert(diff<1000, `${diff} pixels were different`); }).timeout(0); + // eslint-disable-next-line max-statements it('invert the order by sending front', async () => { for(let i=2; i>=0; i--) { /* eslint-disable no-await-in-loop */ @@ -71,12 +73,14 @@ describe('Editing tools: order', () => { await shadowclick(UI.SELECT); await page.mouse.click(500, 100); + await U.waitUntilHTMLRendered(page); const filename = "order.03.invert.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); assert(diff { for (let i = 2; i >= 0; i--) { /* eslint-disable no-await-in-loop */ @@ -89,6 +93,7 @@ describe('Editing tools: order', () => { await shadowclick(UI.SELECT); await page.mouse.click(500, 100); + await U.waitUntilHTMLRendered(page); const filename = "order.04.invert-again.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); diff --git a/test/e2e/microdraw.splitRegion.spec.js b/test/e2e/microdraw.splitRegion.spec.js index 2b96be8..bd5e593 100644 --- a/test/e2e/microdraw.splitRegion.spec.js +++ b/test/e2e/microdraw.splitRegion.spec.js @@ -35,6 +35,7 @@ describe('Editing tools: split regions', () => { assert(diff { await shadowclick(UI.DRAWPOLYGON); await page.mouse.click(400, 400); @@ -43,6 +44,7 @@ describe('Editing tools: split regions', () => { await page.mouse.click(400, 500); await page.mouse.click(400, 400); + await U.waitUntilHTMLRendered(page); const filename = "split.02.cat-square-E.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -56,6 +58,7 @@ describe('Editing tools: split regions', () => { await page.mouse.click(450, 550); await page.mouse.click(450, 450); + await U.waitUntilHTMLRendered(page); const filename = "split.03.cat-square-F.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -68,6 +71,7 @@ describe('Editing tools: split regions', () => { // click on square E (square F is already selected) await page.mouse.click(405, 405); + await U.waitUntilHTMLRendered(page); const filename = "split.04.cat-split.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); diff --git a/test/e2e/microdraw.subtractRegion.spec.js b/test/e2e/microdraw.subtractRegion.spec.js index 471667d..381ec13 100644 --- a/test/e2e/microdraw.subtractRegion.spec.js +++ b/test/e2e/microdraw.subtractRegion.spec.js @@ -37,6 +37,7 @@ describe('Editing tools: subtract regions', () => { assert(diff<1000, `${diff} pixels were different`); }).timeout(0); + // eslint-disable-next-line max-statements it('draws a square', async () => { // select the polygon tool await shadowclick(UI.DRAWPOLYGON); @@ -47,6 +48,7 @@ describe('Editing tools: subtract regions', () => { await page.mouse.click(400, 500); await page.mouse.click(400, 400); + await U.waitUntilHTMLRendered(page); const filename = "subtractRegion.02.cat-square-C.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -61,6 +63,7 @@ describe('Editing tools: subtract regions', () => { await page.mouse.click(450, 550); await page.mouse.click(450, 450); + await U.waitUntilHTMLRendered(page); const filename = "subtractRegion.03.cat-square-D.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -73,6 +76,7 @@ describe('Editing tools: subtract regions', () => { // click on square C (square D is already selected) await page.mouse.click(405, 405); + await U.waitUntilHTMLRendered(page); const filename = "subtractRegion.04.cat-subtraction.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); diff --git a/test/e2e/microdraw.toBezier-toPolygon.spec.js b/test/e2e/microdraw.toBezier-toPolygon.spec.js index 48a7fc8..7ffb6b7 100644 --- a/test/e2e/microdraw.toBezier-toPolygon.spec.js +++ b/test/e2e/microdraw.toBezier-toPolygon.spec.js @@ -50,6 +50,7 @@ describe('Editing tools: convert polygons to bézier and vice-versa', () => { await page.mouse.click(x, y); } + await U.waitUntilHTMLRendered(page); const filename = "toBezierPolygon.02.cat-star.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -59,6 +60,7 @@ describe('Editing tools: convert polygons to bézier and vice-versa', () => { it('converts the star polygon to a bézier curve', async () => { await shadowclick(UI.TOBEZIER); + await U.waitUntilHTMLRendered(page); const filename = "toBezierPolygon.03.cat-star-toBezier.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -73,6 +75,7 @@ describe('Editing tools: convert polygons to bézier and vice-versa', () => { await shadowclick(UI.TOPOLYGON); + await U.waitUntilHTMLRendered(page); const filename = "toBezierPolygon.04.cat-star-toPolygon.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); diff --git a/test/e2e/microdraw.translate-rotate-flip.spec.js b/test/e2e/microdraw.translate-rotate-flip.spec.js index a3fb369..498e1c2 100644 --- a/test/e2e/microdraw.translate-rotate-flip.spec.js +++ b/test/e2e/microdraw.translate-rotate-flip.spec.js @@ -37,6 +37,7 @@ describe('Editing tools: Translate, rotate, flip', () => { assert(diff<1000, `${diff} pixels were different`); }).timeout(0); + // eslint-disable-next-line max-statements it('draws a square', async () => { // select the polygon tool await shadowclick(UI.DRAWPOLYGON); @@ -47,12 +48,14 @@ describe('Editing tools: Translate, rotate, flip', () => { await page.mouse.click(400, 500); await page.mouse.click(400, 400); + await U.waitUntilHTMLRendered(page); const filename = "transform.02.cat-square.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); assert(diff { await shadowclick(UI.SELECT); await page.mouse.click(405, 405); @@ -62,6 +65,7 @@ describe('Editing tools: Translate, rotate, flip', () => { await page.mouse.move(255, 255, {steps: 10}); await page.mouse.up(); + await U.waitUntilHTMLRendered(page); const filename = "transform.03.cat-translate.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -80,6 +84,7 @@ describe('Editing tools: Translate, rotate, flip', () => { await page.mouse.move(450, 300, {steps: 10}); await page.mouse.up(); + await U.waitUntilHTMLRendered(page); const filename = "transform.04.cat-rotate.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -89,6 +94,7 @@ describe('Editing tools: Translate, rotate, flip', () => { it('flip', async () => { await shadowclick(UI.FLIPREGION); + await U.waitUntilHTMLRendered(page); const filename = "transform.05.cat-flip.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); diff --git a/test/e2e/microdraw.view.spec.js b/test/e2e/microdraw.view.spec.js index dc5c1fb..9d2ccf3 100644 --- a/test/e2e/microdraw.view.spec.js +++ b/test/e2e/microdraw.view.spec.js @@ -49,31 +49,35 @@ describe('View pages and data', () => { it('can go to the next page', async () => { await shadowclick(UI.NEXT); + await page.waitForFunction('Microdraw.isAnimating === false'); + await U.waitUntilHTMLRendered(page); const filename = "view.03.cat-next.png"; await page.screenshot({path: U.newPath + filename}); - await page.waitForFunction('Microdraw.isAnimating === false'); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); assert(diff { await shadowclick(UI.PREVIOUS); + await page.waitForFunction('Microdraw.isAnimating === false'); + await U.waitUntilHTMLRendered(page); const filename = "view.04.cat-prev.png"; await page.screenshot({path: U.newPath + filename}); - await page.waitForFunction('Microdraw.isAnimating === false'); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); assert(diff { await shadowclick(UI.ZOOMIN); + await page.waitForFunction('Microdraw.isAnimating === false'); + await U.waitUntilHTMLRendered(page); const filename = "view.05.cat-zoom-in.png"; await page.screenshot({path: U.newPath + filename}); - await page.waitForFunction('Microdraw.isAnimating === false'); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); assert(diff { await shadowclick(UI.NAVIGATE); @@ -82,6 +86,7 @@ describe('View pages and data', () => { await page.mouse.move(U.width*2/3, U.height/2, {steps:50}); await page.mouse.up(); await page.waitForFunction('Microdraw.isAnimating === false'); + await U.waitUntilHTMLRendered(page); const filename = "view.06.cat-zoom-in-translate.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename); @@ -90,6 +95,7 @@ describe('View pages and data', () => { it('can zoom out', async () => { await shadowclick(UI.ZOOMOUT); + await U.waitUntilHTMLRendered(page); const filename = "view.07.cat-zoom-out.png"; await page.screenshot({path: U.newPath + filename}); const diff = await U.compareImages(U.newPath + filename, U.refPath + filename);