import AbstractImageBuilder from '../AbstractImageBuilder'; import CanvasOffscreenBuffer from '../../../Common/Misc/CanvasOffscreenBuffer';
import '../../../React/CollapsibleControls/CollapsibleControlFactory/LookupTableManagerWidget'; import '../../../React/CollapsibleControls/CollapsibleControlFactory/ProbeControl'; import '../../../React/CollapsibleControls/CollapsibleControlFactory/QueryDataModelWidget';
const PROBE_LINE_READY_TOPIC = 'ProbeImageBuilder.chart.data.ready'; const PROBE_CHANGE_TOPIC = 'ProbeImageBuilder.probe.location.change'; const CROSSHAIR_VISIBILITY_CHANGE_TOPIC = 'ProbeImageBuilder.crosshair.visibility.change'; const RENDER_METHOD_CHANGE_TOPIC = 'ProbeImageBuilder.render.change'; const dataMapping = { XY: { idx: [0, 1, 2], hasChange: (probe, x, y, z) => probe[2] !== z, }, XZ: { idx: [0, 2, 1], hasChange: (probe, x, y, z) => probe[1] !== y, }, ZY: { idx: [2, 1, 0], hasChange: (probe, x, y, z) => probe[0] !== x, }, };
export default class BinaryDataProberImageBuilder extends AbstractImageBuilder { constructor(queryDataModel, lookupTableManager) { super({ queryDataModel, lookupTableManager, });
this.metadata = queryDataModel.originalData.DataProber; this.renderMethodMutable = true; this.renderMethod = 'XY'; this.triggerProbeLines = false; this.broadcastCrossHair = true; this.probeValue = 0; this.probeXYZ = [ Math.floor(this.metadata.dimensions[0] / 2), Math.floor(this.metadata.dimensions[1] / 2), Math.floor(this.metadata.dimensions[2] / 2), ]; this.fields = Object.keys(this.metadata.types); this.field = this.fields[0]; this.dataFields = null; this.pushMethod = 'pushToFrontAsBuffer';
this.lookupTableManager.updateActiveLookupTable(this.field); this.lookupTableManager.addFields( this.metadata.ranges, this.queryDataModel.originalData.LookupTables );
let maxSize = 0; for (let i = 0; i < 3; ++i) { const currentSize = this.metadata.dimensions[i]; maxSize = maxSize < currentSize ? currentSize : maxSize; } this.bgCanvas = new CanvasOffscreenBuffer(maxSize, maxSize); this.registerObjectToFree(this.bgCanvas);
this.fgCanvas = null;
this.registerSubscription( queryDataModel.onDataChange((data, envelope) => { this.dataFields = {}; Object.keys(data).forEach((field) => { this.dataFields[field] = new window[this.metadata.types[field]]( data[field].data ); }); this.render(); }) );
this.registerSubscription( this.lookupTableManager.onActiveLookupTableChange((data, envelope) => { if (this.field !== data) { this.field = data; this.render(); } }) );
this.registerSubscription( this.lookupTableManager.onChange((data, envelope) => { this.update(); }) );
const self = this; this.mouseListener = { click: (event, envelope) => { if (!event.activeArea) { return false; } const probe = [self.probeXYZ[0], self.probeXYZ[1], self.probeXYZ[2]]; const axisMap = dataMapping[self.renderMethod].idx; const dimensions = self.metadata.dimensions; const activeArea = event.activeArea;
let xRatio = (event.relative.x - activeArea[0]) / activeArea[2]; let yRatio = (event.relative.y - activeArea[1]) / activeArea[3];
if (event.modifier) { return false; }
xRatio = xRatio < 0 ? 0 : xRatio > 1 ? 1 : xRatio; yRatio = yRatio < 0 ? 0 : yRatio > 1 ? 1 : yRatio;
if (self.renderMethod === 'XZ') { yRatio = 1 - yRatio; }
const xPos = Math.floor(xRatio * dimensions[axisMap[0]]); const yPos = Math.floor(yRatio * dimensions[axisMap[1]]);
probe[axisMap[0]] = xPos; probe[axisMap[1]] = yPos;
self.setProbe(probe[0], probe[1], probe[2]);
return true; }, drag: (event, envelope) => { if (!event.activeArea) { return false; } const probe = [self.probeXYZ[0], self.probeXYZ[1], self.probeXYZ[2]]; const axisMap = dataMapping[self.renderMethod].idx; const dimensions = self.metadata.dimensions; const activeArea = event.activeArea;
let xRatio = (event.relative.x - activeArea[0]) / activeArea[2]; let yRatio = (event.relative.y - activeArea[1]) / activeArea[3];
if (event.modifier) { return false; }
xRatio = xRatio < 0 ? 0 : xRatio > 1 ? 1 : xRatio; yRatio = yRatio < 0 ? 0 : yRatio > 1 ? 1 : yRatio;
if (self.renderMethod === 'XZ') { yRatio = 1 - yRatio; }
const xPos = Math.floor(xRatio * dimensions[axisMap[0]]); const yPos = Math.floor(yRatio * dimensions[axisMap[1]]);
probe[axisMap[0]] = xPos; probe[axisMap[1]] = yPos;
self.setProbe(probe[0], probe[1], probe[2]);
return true; }, zoom: (event, envelope) => { const probe = [self.probeXYZ[0], self.probeXYZ[1], self.probeXYZ[2]]; const axisMap = dataMapping[self.renderMethod].idx; const idx = axisMap[2];
if (event.modifier) { return false; }
probe[idx] += event.deltaY < 0 ? -1 : 1;
if (probe[idx] < 0) { probe[idx] = 0; return true; }
if (probe[idx] >= self.metadata.dimensions[idx]) { probe[idx] = self.metadata.dimensions[idx] - 1; return true; }
self.setProbe(probe[0], probe[1], probe[2]);
return true; }, }; }
setPushMethodAsBuffer() { this.pushMethod = 'pushToFrontAsBuffer'; }
setPushMethodAsImage() { this.pushMethod = 'pushToFrontAsImage'; }
setProbeLineNotification(trigger) { this.triggerProbeLines = trigger; }
updateProbeValue() { const x = this.probeXYZ[0]; const y = this.probeXYZ[1]; const z = this.probeXYZ[2]; const xSize = this.metadata.dimensions[0]; const ySize = this.metadata.dimensions[1]; const array = this.dataFields[this.field];
if (array) { this.probeValue = array[x + (ySize - y - 1) * xSize + z * xSize * ySize]; } }
setProbe(i, j, k) { const fn = dataMapping[this.renderMethod].hasChange; const idx = dataMapping[this.renderMethod].idx; const previousValue = [].concat(this.probeXYZ); let x = i; let y = j; let z = k;
if (Array.isArray(i)) { z = i[2]; y = i[1]; x = i[0]; }
if (fn(this.probeXYZ, x, y, z)) { this.probeXYZ = [x, y, z]; this.render(); } else { this.probeXYZ = [x, y, z]; const dimensions = this.metadata.dimensions; const spacing = this.metadata.spacing;
this.updateProbeValue();
if (this.renderMethod === 'XZ') { this.pushToFront( dimensions[idx[0]], dimensions[idx[1]], spacing[idx[0]], spacing[idx[1]], this.probeXYZ[idx[0]], dimensions[idx[1]] - this.probeXYZ[idx[1]] - 1 ); } else { this.pushToFront( dimensions[idx[0]], dimensions[idx[1]], spacing[idx[0]], spacing[idx[1]], this.probeXYZ[idx[0]], this.probeXYZ[idx[1]] ); } }
if ( previousValue[0] === x && previousValue[1] === y && previousValue[2] === z ) { return; }
this.emit(PROBE_CHANGE_TOPIC, [x, y, z]); }
getProbe() { return this.probeXYZ; }
getFieldValueAtProbeLocation() { return this.probeValue; }
getProbeLine(axisIdx) { const probeData = { xRange: [0, 100], fields: [], }; const fields = this.fields; const px = this.probeXYZ[0]; const py = this.probeXYZ[1]; const pz = this.probeXYZ[2]; const xSize = this.metadata.dimensions[0]; const ySize = this.metadata.dimensions[1]; const zSize = this.metadata.dimensions[2]; const idxValues = [];
if (axisIdx === 0) { const offset = (ySize - py - 1) * xSize + pz * xSize * ySize; for (let x = 0; x < xSize; x++) { idxValues.push(offset + x); } } if (axisIdx === 1) { const offset = px + pz * xSize * ySize; for (let y = 0; y < ySize; y++) { idxValues.push(offset + (ySize - y - 1) * xSize); } idxValues.reverse(); } if (axisIdx === 2) { const offset = px + (ySize - py - 1) * xSize; const step = xSize * ySize; for (let z = 0; z < zSize; z++) { idxValues.push(offset + z * step); } }
const dataSize = idxValues.length; fields.forEach((name) => { const array = this.dataFields[name]; const data = []; const range = this.lookupTableManager .getLookupTable(name) .getScalarRange();
for (let i = 0; i < dataSize; i++) { data.push(array[idxValues[i]]); }
probeData.fields.push({ name, data, range, }); });
return probeData; }
render() { if (!this.dataFields) { return; }
this.updateProbeValue(); this[`render${this.renderMethod}`](); }
pushToFront(width, height, scaleX, scaleY, lineX, lineY) { this[this.pushMethod](width, height, scaleX, scaleY, lineX, lineY);
if (this.triggerProbeLines) { this.emit(PROBE_LINE_READY_TOPIC, { x: this.getProbeLine(0), y: this.getProbeLine(1), z: this.getProbeLine(2), }); } }
pushToFrontAsImage(width, height, scaleX, scaleY, lineX, lineY) { const destWidth = Math.floor(width * scaleX); const destHeight = Math.floor(height * scaleY); let ctx = null;
if (this.fgCanvas) { this.fgCanvas.size(destWidth, destHeight); } else { this.fgCanvas = new CanvasOffscreenBuffer(destWidth, destHeight); this.registerObjectToFree(this.fgCanvas); }
ctx = this.fgCanvas.get2DContext(); ctx.drawImage( this.bgCanvas.el, 0, 0, width, height, 0, 0, destWidth, destHeight );
ctx.beginPath(); ctx.moveTo(lineX * scaleX, 0); ctx.lineTo(lineX * scaleX, destHeight); ctx.moveTo(0, lineY * scaleY); ctx.lineTo(destWidth, lineY * scaleY); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke();
const readyImage = { url: this.fgCanvas.toDataURL(), type: this.renderMethod, builder: this, };
this.imageReady(readyImage); }
pushToFrontAsBuffer(width, height, scaleX, scaleY, lineX, lineY) { const destWidth = Math.floor(width * scaleX); const destHeight = Math.floor(height * scaleY);
const readyImage = { canvas: this.bgCanvas.el, imageData: this.bgCanvas.el .getContext('2d') .getImageData(0, 0, width, height), area: [0, 0, width, height], outputSize: [destWidth, destHeight], type: this.renderMethod, builder: this, };
if (this.broadcastCrossHair) { readyImage.crosshair = [lineX, lineY]; }
this.imageReady(readyImage); }
renderXY() { const ctx = this.bgCanvas.get2DContext(); const xyz = this.probeXYZ; const dimensions = this.metadata.dimensions; const xSize = dimensions[0]; const ySize = dimensions[1]; const spacing = this.metadata.spacing; const imageBuffer = ctx.createImageData(dimensions[0], dimensions[1]); const pixels = imageBuffer.data; const imageSize = dimensions[0] * dimensions[1]; const offset = imageSize * xyz[2]; const lut = this.lookupTableManager.getLookupTable(this.field); const array = this.dataFields[this.field];
let idx = 0; for (let y = 0; y < ySize; y++) { for (let x = 0; x < xSize; x++) { const color = lut.getColor(array[offset + x + xSize * (ySize - y - 1)]); pixels[idx * 4] = 255 * color[0]; pixels[idx * 4 + 1] = 255 * color[1]; pixels[idx * 4 + 2] = 255 * color[2]; pixels[idx * 4 + 3] = 255; idx += 1; } }
ctx.putImageData(imageBuffer, 0, 0); this.pushToFront( dimensions[0], dimensions[1], spacing[0], spacing[1], xyz[0], xyz[1] ); }
renderZY() { const ctx = this.bgCanvas.get2DContext(); const xyz = this.probeXYZ; const dimensions = this.metadata.dimensions; const offsetX = xyz[0]; const stepY = dimensions[0]; const stepZ = dimensions[0] * dimensions[1]; const ySize = dimensions[1]; const zSize = dimensions[2]; const spacing = this.metadata.spacing; const imageBuffer = ctx.createImageData(dimensions[2], dimensions[1]); const pixels = imageBuffer.data; const lut = this.lookupTableManager.getLookupTable(this.field); const array = this.dataFields[this.field];
let idx = 0; for (let y = 0; y < ySize; y++) { for (let z = 0; z < zSize; z++) { const color = lut.getColor( array[offsetX + stepY * (ySize - y - 1) + stepZ * z] ); pixels[idx * 4] = 255 * color[0]; pixels[idx * 4 + 1] = 255 * color[1]; pixels[idx * 4 + 2] = 255 * color[2]; pixels[idx * 4 + 3] = 255; idx += 1; } } ctx.putImageData(imageBuffer, 0, 0); this.pushToFront( dimensions[2], dimensions[1], spacing[2], spacing[1], xyz[2], xyz[1] ); }
renderXZ() { const ctx = this.bgCanvas.get2DContext(); const xyz = this.probeXYZ; const dimensions = this.metadata.dimensions; const xSize = dimensions[0]; const zSize = dimensions[2]; const zStep = xSize * dimensions[1]; const offset = xSize * (dimensions[1] - xyz[1] - 1); const spacing = this.metadata.spacing; const imageBuffer = ctx.createImageData(xSize, zSize); const pixels = imageBuffer.data; const lut = this.lookupTableManager.getLookupTable(this.field); const array = this.dataFields[this.field];
let idx = 0; for (let z = 0; z < zSize; z++) { for (let x = 0; x < xSize; x++) { const color = lut.getColor(array[offset + x + (zSize - z - 1) * zStep]); pixels[idx * 4] = 255 * color[0]; pixels[idx * 4 + 1] = 255 * color[1]; pixels[idx * 4 + 2] = 255 * color[2]; pixels[idx * 4 + 3] = 255; idx += 1; } }
ctx.putImageData(imageBuffer, 0, 0); this.pushToFront( dimensions[0], dimensions[2], spacing[0], spacing[2], xyz[0], zSize - xyz[2] - 1 ); }
isCrossHairEnabled() { return this.broadcastCrossHair; }
setCrossHairEnable(useCrossHair) { if (this.broadcastCrossHair !== useCrossHair) { this.broadcastCrossHair = useCrossHair; this.emit(CROSSHAIR_VISIBILITY_CHANGE_TOPIC, useCrossHair); this.setProbe(this.probeXYZ[0], this.probeXYZ[1], this.probeXYZ[2]); } }
setField(value) { this.field = value; }
getField() { return this.field; }
getFields() { return this.fields; }
setRenderMethod(renderMethod) { if (this.renderMethodMutable && this.renderMethod !== renderMethod) { this.renderMethod = renderMethod; this.render(); this.emit(RENDER_METHOD_CHANGE_TOPIC, renderMethod); } }
getRenderMethod() { return this.renderMethod; }
getRenderMethods() { return ['XY', 'ZY', 'XZ']; }
isRenderMethodMutable() { return this.renderMethodMutable; }
setRenderMethodImutable() { this.renderMethodMutable = false; }
setRenderMethodMutable() { this.renderMethodMutable = true; }
getListeners() { return this.mouseListener; }
onProbeLineReady(callback) { return this.on(PROBE_LINE_READY_TOPIC, callback); }
onProbeChange(callback) { return this.on(PROBE_CHANGE_TOPIC, callback); }
onRenderMethodChange(callback) { return this.on(RENDER_METHOD_CHANGE_TOPIC, callback); }
onCrosshairVisibilityChange(callback) { return this.on(CROSSHAIR_VISIBILITY_CHANGE_TOPIC, callback); }
destroy() { super.destroy();
this.off(); this.bgCanvas = null; this.fgCanvas = null; }
getControlWidgets() { const model = this; const { lookupTableManager, queryDataModel } = this.getControlModels(); return [ { name: 'LookupTableManagerWidget', lookupTableManager, }, { name: 'ProbeControl', model, }, { name: 'QueryDataModelWidget', queryDataModel, }, ]; } }
|