From 9b8457beb3cb2f80d7f56555253943ce50a0922c Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Fri, 5 Feb 2021 16:06:58 +0100 Subject: [PATCH] added tests to plotly --- spec/test.ts | 3 + webpack/webpack.common.js | 10 -- worker/worker-plotly.js | 194 +++++++++++++++++++++++++++++++ worker/worker-plotly.spec.js | 36 ++++++ worker/worker.js | 216 ++++------------------------------- 5 files changed, 254 insertions(+), 205 deletions(-) create mode 100644 worker/worker-plotly.js create mode 100644 worker/worker-plotly.spec.js diff --git a/spec/test.ts b/spec/test.ts index 16b7c5500..478be25bb 100644 --- a/spec/test.ts +++ b/spec/test.ts @@ -21,4 +21,7 @@ getTestBed().initTestEnvironment( const testContext = require.context('../src', true, /\.spec\.ts$/) testContext.keys().map(testContext) +const workerCtx = require.context('../worker', true, /\.spec\.js$/) +workerCtx.keys().map(workerCtx) + require('../common/util.spec.js') diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index dee98b82a..1896b962e 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -9,16 +9,6 @@ module.exports = { loaders : ['ts-loader','angular2-template-loader?keepUrl=true'], exclude : /node_modules|[Ss]pec\.ts$/ }, - { - test : /third_party|.*?worker.*?\.js$/, - exclude: /[Ss]pec\.js$/, - use : { - loader : 'file-loader', - options: { - name : '[name].[ext]' - } - } - } ] }, plugins : [ diff --git a/worker/worker-plotly.js b/worker/worker-plotly.js new file mode 100644 index 000000000..3c7a6be2a --- /dev/null +++ b/worker/worker-plotly.js @@ -0,0 +1,194 @@ +(function(exports){ + /** + * units in mm --> convert to nm + */ + const plotyMultiple = 1e6 + const { vtkHeader } = globalThis.constants || {} + + const getPerpendicularPointsForLine = (A, B, scale) => { + + const lineWeight = 1e6 + const lineWidth = scale * lineWeight + const lineHeight = scale * lineWeight + + let u = A.map((item, index) => { + return item - B[index]; + }) + const uLength = Math.sqrt((u[0] * u[0]) + (u[1] * u[1]) + (u[2] * u[2])) + u = u.map((item, index) => { + return item/uLength + }) + + const n = [] + if(Math.abs(u[0]) <= Math.abs(u[1]) && Math.abs(u[0]) <= Math.abs(u[2])) { + n[0] = u[1] * u[1] + u[2] * u[2] + n[1] = -u[1] * u[0] + n[2] = -u[2] * u[0] + } + else if(Math.abs(u[1])<=Math.abs(u[0])&&Math.abs(u[1])<=Math.abs(u[2])) + { + n[0] = -u[0] * u[2] + n[1] = u[0] * u[0] + u[2] * u[2] + n[2] = -u[2] * u[1] + } + else if(Math.abs(u[2])<=Math.abs(u[0])&&Math.abs(u[2])<=Math.abs(u[1])) + { + n[0] = -u[0] * u[2] + n[1] = -u[1] * u[2] + n[2] = u[0] * u[0] + u[1] * u[1] + } + + const v = [ u[1] * n[2] - u[2] * n[1], u[2] * n[0] - u[0] * n[2], u[0] * n[1] - u[1] * n[0] ] + + const RMul = (k) => { + const res = [] + res[0] = v[0]*k[0] + n[0]*k[1] + u[0]*k[2] + res[1] = v[1]*k[0] + n[1]*k[1] + u[1]*k[2] + res[2] = v[2]*k[0] + n[2]*k[1] + u[2]*k[2] + return res + } + + const sumArrays = (a1, a2) => { + return a1.map((item, index) => { + return item + a2[index]; + }) + } + + const a = sumArrays(A, RMul([lineWidth,lineHeight,0])) + const b = sumArrays(A, RMul([-lineWidth,lineHeight,0])) + const c = sumArrays(A, RMul([lineWidth,-lineHeight,0])) + const d = sumArrays(A, RMul([-lineWidth,-lineHeight,0])) + + const e = sumArrays(B, RMul([lineWidth,lineHeight,0])) + const f = sumArrays(B, RMul([-lineWidth,lineHeight,0])) + const g = sumArrays(B, RMul([lineWidth,-lineHeight,0])) + const h = sumArrays(B, RMul([-lineWidth,-lineHeight,0])) + + return `${a.join(' ')}\n ${b.join(' ')}\n ${c.join(' ')}\n ${d.join(' ')}\n ${e.join(' ')}\n ${f.join(' ')}\n ${g.join(' ')}\n ${h.join(' ')}\n ` + } + + const getFragmentColorString = (colors) => { + + const hexToRgb = (hex) => { + const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)) + return `emitRGB(vec3(${r/255}, ${g/255}, ${b/255}))` + } + + const colorsUnique = colors.filter((cf, i) => colors[i-1] !== cf) + .map((color, j) => { + return `if (label > ${j - 0.01} && label < ${j + 0.01}) { ${hexToRgb(color)}; }` + }) + + const fragmentColorString = `${colorsUnique.join(' else ')} else {emitRGB(vec3(1.0, 0.1, 0.12));}` + return fragmentColorString + } + + const getLineDataVtkPolygonStringWithNumber = (neuronCoordinateLength) => { + let returnString = '' + for (let i = 0; i < neuronCoordinateLength; i++) { + const neuronNumber = 8*i + returnString += + `3 ${0 + neuronNumber} ${1 + neuronNumber} ${3 + neuronNumber}\n` + + `3 ${0 + neuronNumber} ${2 + neuronNumber} ${3 + neuronNumber}\n` + + `3 ${4 + neuronNumber} ${5 + neuronNumber} ${7 + neuronNumber}\n` + + `3 ${4 + neuronNumber} ${6 + neuronNumber} ${7 + neuronNumber}\n` + + `3 ${2 + neuronNumber} ${6 + neuronNumber} ${7 + neuronNumber}\n` + + `3 ${2 + neuronNumber} ${3 + neuronNumber} ${7 + neuronNumber}\n` + + `3 ${3 + neuronNumber} ${1 + neuronNumber} ${7 + neuronNumber}\n` + + `3 ${1 + neuronNumber} ${5 + neuronNumber} ${7 + neuronNumber}\n` + + `3 ${2 + neuronNumber} ${0 + neuronNumber} ${6 + neuronNumber}\n` + + `3 ${0 + neuronNumber} ${4 + neuronNumber} ${6 + neuronNumber}\n` + + `3 ${1 + neuronNumber} ${0 + neuronNumber} ${4 + neuronNumber}\n` + + `3 ${1 + neuronNumber} ${4 + neuronNumber} ${5 + neuronNumber}\n` + } + return returnString + } + + const getColorIds = (colors) => { + let returnString = '' + + let colorId = 0 + + for (let i=0; i < colors.length; i++){ + if (i > 0 && colors[i] !== colors[i-1]) { + colorId += 1 + } + for (let j=0; j < 8; j++){ + if (i === colors.length-1 && j === 7) { + returnString += colorId + } else { + returnString += colorId + '\n' + } + } + } + + return returnString + } + + const parseLineDataToVtk = (data, scale= 1, plotyMultiple) => { + const lineCoordinates = [] + const colors = [] + + for (let i = 1; i < data.x.length; i++) { + + if (data.x[i] !== null && data.x[i-1] !== null) { + lineCoordinates.push([[ + data.x[i-1] * plotyMultiple, + data.y[i-1] * plotyMultiple, + data.z[i-1] * plotyMultiple, + ], [ + data.x[i] * plotyMultiple, + data.y[i] * plotyMultiple, + data.z[i] * plotyMultiple, + ]]) + + colors.push(data.marker.color[i-1]) + } + } + + const coordinateLength = lineCoordinates.length + + const lineCoordinatesArrayToString = (() => { + let returnString = '' + lineCoordinates.forEach(lc => { + returnString += getPerpendicularPointsForLine(lc[0], lc[1], scale) + }) + return returnString + })() + + const customFragmentColor = getFragmentColorString(colors) + + const vtkString = `${vtkHeader}\n` + + `POINTS ${coordinateLength*8} float\n` + + lineCoordinatesArrayToString + + `POLYGONS ${coordinateLength*12} ${coordinateLength*48}\n` + + getLineDataVtkPolygonStringWithNumber(coordinateLength) + + `POINT_DATA ${coordinateLength*8}\n` + + 'SCALARS label unsigned_char 1\n' + + 'LOOKUP_TABLE none\n' + + getColorIds(colors) + + return {vtkString, customFragmentColor} + } + + exports.plotly = { + plotyMultiple, + convert: plotlyData => { + const { x, y, z } = plotlyData.traces[0] + const lm = [] + for (const idx in x) { + if (typeof x !== 'undefined' && x !== null) { + lm.push([x[idx]*plotyMultiple, y[idx]*plotyMultiple, z[idx]*plotyMultiple]) + } + } + const { vtkString, customFragmentColor } = parseLineDataToVtk(plotlyData.traces[0], 5e-3, plotyMultiple) + + return { + vtkString, + customFragmentColor, + } + } + } +})( + typeof exports === 'undefined' ? module.exports : exports +) diff --git a/worker/worker-plotly.spec.js b/worker/worker-plotly.spec.js new file mode 100644 index 000000000..c00f81922 --- /dev/null +++ b/worker/worker-plotly.spec.js @@ -0,0 +1,36 @@ +const vtkHeader = `# vtk DataFile Version 2.0 +Created by worker thread at https://github.com/HumanBrainProject/interactive-viewer +ASCII +DATASET POLYDATA` + +globalThis.constants = { + vtkHeader, +} + +const { plotly } = require('./worker-plotly') + +const input = { + traces: [{ + "type":"scatter3d", + "mode":"markers", + "name":"points", + "x":[-0.9557710289955139,null,-0.9557710289955139,-0.972806990146637,null], + "y":[4.165919780731201,null,4.165919780731201,4.160162925720215,null], + "z":[1.925400972366333,null,1.925400972366333,1.9260079860687256,null],"opacity":1, + "marker":{"size":1,"color":["#00dd00",null,"#ff0000","#ff0000",null], + "reversescale":false} + }] +} +const expectedOutput = { + customFragmentColor: "if (label > -0.01 && label < 0.01) { emitRGB(vec3(1, 0, 0)); } else {emitRGB(vec3(1.0, 0.1, 0.12));}", + vtkString: "# vtk DataFile Version 2.0\nCreated by worker thread at https://github.com/HumanBrainProject/interactive-viewer\nASCII\nDATASET POLYDATA\nPOINTS 8 float\n-954011.5298146864 4161239.595879443 1930395.281492923\n -957211.0971719137 4170707.908889603 1930395.281492923\n -954330.9608191141 4161131.6525727995 1920406.663239743\n -957530.5281763414 4170599.9655829594 1920406.663239743\n -971047.4909658094 4155482.7408684567 1931002.2951953155\n -974247.0583230368 4164951.0538786165 1931002.2951953155\n -971366.9219702372 4155374.797561813 1921013.6769421357\n -974566.4893274645 4164843.110571973 1921013.6769421357\n POLYGONS 12 48\n3 0 1 3\n3 0 2 3\n3 4 5 7\n3 4 6 7\n3 2 6 7\n3 2 3 7\n3 3 1 7\n3 1 5 7\n3 2 0 6\n3 0 4 6\n3 1 0 4\n3 1 4 5\nPOINT_DATA 8\nSCALARS label unsigned_char 1\nLOOKUP_TABLE none\n0\n0\n0\n0\n0\n0\n0\n0" +} + +describe('> worker-plotly.js', () => { + it('> expect input === output', () => { + + expect( + plotly.convert(input) + ).toEqual(expectedOutput) + }) +}) diff --git a/worker/worker.js b/worker/worker.js index 973d7831c..e60464fa3 100644 --- a/worker/worker.js +++ b/worker/worker.js @@ -1,4 +1,18 @@ -self.importScripts('./worker-second.js') +const vtkHeader = `# vtk DataFile Version 2.0 +Created by worker thread at https://github.com/HumanBrainProject/interactive-viewer +ASCII +DATASET POLYDATA` + +globalThis.constants = { + vtkHeader +} + +if (typeof self.importScripts === 'function') self.importScripts('./worker-second.js') + +/** + * TODO migrate processing functionalities to other scripts + * see worker-plotly.js + */ const validTypes = [ 'GET_LANDMARKS_VTK', @@ -22,11 +36,6 @@ const validOutType = [ 'UPDATE_PARCELLATION_REGIONS' ] -const vtkHeader = `# vtk DataFile Version 2.0 -Created by worker thread at https://github.com/HumanBrainProject/interactive-viewer -ASCII -DATASET POLYDATA` - const getVertexHeader = (numVertex) => `POINTS ${numVertex} float` const getPolyHeader = (numPoly) => `POLYGONS ${numPoly} ${4 * numPoly}` @@ -294,204 +303,21 @@ const processParcRegionAttr = (payload) => { }) } -const parseLineDataToVtk = (data, scale= 1, plotyMultiple) => { - const lineCoordinates = [] - const colors = [] - - for (let i = 1; i < data.x.length; i++) { - - if (data.x[i] !== null && data.x[i-1] !== null) { - lineCoordinates.push([[ - data.x[i-1] * plotyMultiple, - data.y[i-1] * plotyMultiple, - data.z[i-1] * plotyMultiple, - ], [ - data.x[i] * plotyMultiple, - data.y[i] * plotyMultiple, - data.z[i] * plotyMultiple, - ]]) - - colors.push(data.marker.color[i-1]) - } - } - - const coordinateLength = lineCoordinates.length - - const lineCoordinatesArrayToString = (() => { - let returnString = '' - lineCoordinates.forEach(lc => { - returnString += getPerpendicularPointsForLine(lc[0], lc[1], scale) - }) - return returnString - })() - - const customFragmentColor = getFragmentColorString(colors) - - const vtkString = `${vtkHeader}\n` + - `POINTS ${coordinateLength*8} float\n` + - lineCoordinatesArrayToString + - `POLYGONS ${coordinateLength*12} ${coordinateLength*48}\n` + - getLineDataVtkPolygonStringWithNumber(coordinateLength) + - `POINT_DATA ${coordinateLength*8}\n` + - 'SCALARS label unsigned_char 1\n' + - 'LOOKUP_TABLE none\n' + - getColorIds(colors) - - return {vtkString, customFragmentColor} -} - -const getFragmentColorString = (colors) => { - - const hexToRgb = (hex) => { - const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)) - return `emitRGB(vec3(${r/255}, ${g/255}, ${b/255}))` - } - - const colorsUnique = colors.filter((cf, i) => colors[i-1] !== cf) - .map((color, j) => { - return `if (label > ${j - 0.01} && label < ${j + 0.01}) { ${hexToRgb(color)}; }` - }) - - const fragmentColorString = `${colorsUnique.join(' else ')} else {emitRGB(vec3(1.0, 0.1, 0.12));}` - return fragmentColorString -} - -const getColorIds = (colors) => { - let returnString = '' - - let colorId = 0 - - for (let i=0; i < colors.length; i++){ - if (i > 0 && colors[i] !== colors[i-1]) { - colorId += 1 - } - for (let j=0; j < 8; j++){ - if (i === colors.length-1 && j === 7) { - returnString += colorId - } else { - returnString += colorId + '\n' - } - } - } - - return returnString -} - -const getLineDataVtkPolygonStringWithNumber = (neuronCoordinateLength) => { - let returnString = '' - for (let i = 0; i < neuronCoordinateLength; i++) { - const neuronNumber = 8*i - returnString += - `3 ${0 + neuronNumber} ${1 + neuronNumber} ${3 + neuronNumber}\n` + - `3 ${0 + neuronNumber} ${2 + neuronNumber} ${3 + neuronNumber}\n` + - `3 ${4 + neuronNumber} ${5 + neuronNumber} ${7 + neuronNumber}\n` + - `3 ${4 + neuronNumber} ${6 + neuronNumber} ${7 + neuronNumber}\n` + - `3 ${2 + neuronNumber} ${6 + neuronNumber} ${7 + neuronNumber}\n` + - `3 ${2 + neuronNumber} ${3 + neuronNumber} ${7 + neuronNumber}\n` + - `3 ${3 + neuronNumber} ${1 + neuronNumber} ${7 + neuronNumber}\n` + - `3 ${1 + neuronNumber} ${5 + neuronNumber} ${7 + neuronNumber}\n` + - `3 ${2 + neuronNumber} ${0 + neuronNumber} ${6 + neuronNumber}\n` + - `3 ${0 + neuronNumber} ${4 + neuronNumber} ${6 + neuronNumber}\n` + - `3 ${1 + neuronNumber} ${0 + neuronNumber} ${4 + neuronNumber}\n` + - `3 ${1 + neuronNumber} ${4 + neuronNumber} ${5 + neuronNumber}\n` - } - return returnString -} - -const getPerpendicularPointsForLine = (A, B, scale) => { - - const lineWeight = 1e6 - const lineWidth = scale * lineWeight - const lineHeight = scale * lineWeight - - let u = A.map((item, index) => { - return item - B[index]; - }) - const uLength = Math.sqrt((u[0] * u[0]) + (u[1] * u[1]) + (u[2] * u[2])) - u = u.map((item, index) => { - return item/uLength - }) - - const n = [] - if(Math.abs(u[0]) <= Math.abs(u[1]) && Math.abs(u[0]) <= Math.abs(u[2])) { - n[0] = u[1] * u[1] + u[2] * u[2] - n[1] = -u[1] * u[0] - n[2] = -u[2] * u[0] - } - else if(Math.abs(u[1])<=Math.abs(u[0])&&Math.abs(u[1])<=Math.abs(u[2])) - { - n[0] = -u[0] * u[2] - n[1] = u[0] * u[0] + u[2] * u[2] - n[2] = -u[2] * u[1] - } - else if(Math.abs(u[2])<=Math.abs(u[0])&&Math.abs(u[2])<=Math.abs(u[1])) - { - n[0] = -u[0] * u[2] - n[1] = -u[1] * u[2] - n[2] = u[0] * u[0] + u[1] * u[1] - } - - const v = [ u[1] * n[2] - u[2] * n[1], u[2] * n[0] - u[0] * n[2], u[0] * n[1] - u[1] * n[0] ] - - const RMul = (k) => { - const res = [] - res[0] = v[0]*k[0] + n[0]*k[1] + u[0]*k[2] - res[1] = v[1]*k[0] + n[1]*k[1] + u[1]*k[2] - res[2] = v[2]*k[0] + n[2]*k[1] + u[2]*k[2] - return res - } - - const sumArrays = (a1, a2) => { - return a1.map((item, index) => { - return item + a2[index]; - }) - } - - const a = sumArrays(A, RMul([lineWidth,lineHeight,0])) - const b = sumArrays(A, RMul([-lineWidth,lineHeight,0])) - const c = sumArrays(A, RMul([lineWidth,-lineHeight,0])) - const d = sumArrays(A, RMul([-lineWidth,-lineHeight,0])) - - const e = sumArrays(B, RMul([lineWidth,lineHeight,0])) - const f = sumArrays(B, RMul([-lineWidth,lineHeight,0])) - const g = sumArrays(B, RMul([lineWidth,-lineHeight,0])) - const h = sumArrays(B, RMul([-lineWidth,-lineHeight,0])) - - return `${a.join(' ')}\n ${b.join(' ')}\n ${c.join(' ')}\n ${d.join(' ')}\n ${e.join(' ')}\n ${f.join(' ')}\n ${g.join(' ')}\n ${h.join(' ')}\n ` -} - - let plotyVtkUrl onmessage = (message) => { - if (message.data.method === 'ping') { - return postMessage({ - id: message.data.id, - result: `pong ${self.meaningOfLife}` - }) - } - if (message.data.method && VALID_METHODS.indexOf(message.data.method) >= 0) { const { id } = message.data if (message.data.method === VALID_METHOD.PROCESS_PLOTLY) { - /** - * units in mm --> convert to nm - */ - const plotyMultiple=1e6 try { - const { data: plotlyData } = message.data.param - const { x, y, z } = plotlyData.traces[0] - const lm = [] - for (const idx in x) { - if (typeof x !== 'undefined' && x !== null) { - lm.push([x[idx]*plotyMultiple, y[idx]*plotyMultiple, z[idx]*plotyMultiple]) - } - } if (plotyVtkUrl) URL.revokeObjectURL(plotyVtkUrl) - const { vtkString, customFragmentColor} = parseLineDataToVtk(plotlyData.traces[0], 5e-3, plotyMultiple) - - plotyVtkUrl = URL.createObjectURL( + const { data: plotlyData } = message.data.param + const { + vtkString, + customFragmentColor + } = self.plotly.convert(plotlyData) + const plotyVtkUrl = URL.createObjectURL( new Blob([ encoder.encode(vtkString) ], { type: 'application/octet-stream' }) ) postMessage({ -- GitLab