- First I explain the data to give context:
We have the following folder structure:
atlas
caballo
cabeza
etiquetas
cabeza01-es-latin.txt
...
cabeza20-es-latin.txt
original
cabeza01.nrrd
cabeza01.png
...
cabeza20.nrrd
cabeza20.png
cabezasegmentado01.nrrd
...
cabezasemgentado20.nrrd
columna
costillas
original
pelviano
toracico
json
cabeza.json
columna.json
costillas.json
original.json
pelviano.json
toracico.json
Which means we have a horse's atlas, with different SubAtlas
(each anatomic family: head, backbone, ribs...). For example inside head (cabeza) we have:
etiquetas
: are txt files where the first line are the Segments
' names (each element into a SubAtlas
' Part
(eye, tongue, ear, apofisis...). In addition the second line are the colors of each segment. For example:
"" "Body of mandible" "Maxilla bone"
"0 0 0" "255 255 255" "0 50 215"
original: we have the original nrrd, which prints a black and white image. It is a Part
:
And we also have the PNG, the miniature image:
Then in the SubAtlas
' root directory, for example in cabeza
, we have: cabezasegmentado01.nrrd
which is a model with different Segments on it:
To communicate the directories structure with the code we have the json files. Each SubAtlas
has an array where each Parte
is an object as follows:
[
{
"original": "atlas/caballo/cabeza/original/cabeza01.nrrd",
"segmentado": "atlas/caballo/cabeza/cabezasegmentado01.nrrd",
"miniatura": "atlas/caballo/cabeza/original/cabeza01.png",
"etiquetas": "atlas/caballo/cabeza/etiquetas/cabeza01-es-latin1.txt",
"titulo": "Esqueleto de la cabeza: vista lateral derecha"
},
{
"original": "atlas/caballo/cabeza/original/cabeza02.nrrd",
"segmentado": "atlas/caballo/cabeza/cabezasegmentado02.nrrd",
"miniatura": "atlas/caballo/cabeza/original/cabeza02.png",
"etiquetas": "atlas/caballo/cabeza/etiquetas/cabeza02-es-latin1.txt",
"titulo": "Esqueleto de la cabeza: vista lateral derecha"
},
Where original is the main NRRD, segmentado
is the NRRD where the different segments inside the part are visible as plain gray colors, miniatura
is the PNG, etiquetas
, is the txt file, and titulo
is a string which describes the Part's title.
The current way to read the JSON files is to read the folder where they are, and then read each one of the different JSON files:
/*Esta clase lista todos loa directorios bajo la location pasada, y a su vez,
- carga cada una de esas carpetas como json, informando a la app de que ha acabado la cargaa
- Seria interesante quitar window.IndexAtlas como variable global*/
import jQuery from 'jquery-ajax'; import Atlas from "./Atlas";
export default class JSONHeader { constructor(location, callback) { loadAtlasStructure(location, callback);
function loadAtlasStructure(location, callback) { jQuery.ajax({ dataType: "json", url: location, async: true, success: function (files) { files.map((file, index, allFilesArray) => { jQuery.ajax({ dataType: "json", url: location + file, async: true, success: function (data) { console.log(data); if (!window.IndexAtlas) { window.IndexAtlas = new Atlas(); } window.IndexAtlas.addSubAtlas(data); console.log('JSONHeader::window.IndexAtlas', window.IndexAtlas); if (allSubAtlasHaveBeenLoaded(index, allFilesArray)) { callback(); } } }); }) } } ) } }
}
function allSubAtlasHaveBeenLoaded(index, allFilesArray) { return index >= allFilesArray.length - 1; }
Right now the atlas, IndexAtlas
, is stored as a global variable. The reason of it is because of we do not know how to return it to the App component (which is its parent, and the one which duty is to init the JSONHeader
request):
/*Esta clase se responsabiliza de mantener el estado de la carga del Atlas.
* Para ello tiene en su state, isAtlasloading
* Ademas inicia la carga asíncrona instanciando a JSONHeader.
* Observamos que para que su hijo, JSONHeader pueda cambiar el state isAtlasLoading, le pasamos
* finishCallback.
* Además tiene las rutas.*/
import React, {Component} from 'react';
import BrowserRouter from "react-router-dom/es/BrowserRouter";
import Switch from "react-router-dom/es/Switch";
import Route from "react-router-dom/es/Route";
import Redirect from "react-router-dom/es/Redirect";
import ScenePage from "../ScenePage/index";
import CoverPage from "../CoverPage/index";
import {INDEX, SCENE_PAGE} from "../../constantRoutes";
import JSONHeader from "../../newModel14Junio/JSONHeader";
export default class App extends Component {
constructor(props) {
super(props);
const finishCallback = () => {
this.setState({isAtlasLoading: false});
};
const header = new JSONHeader('/atlas/json/', finishCallback);
this.state = {
isAtlasLoading: true
};
}
render() {
if (this.state.isAtlasLoading) {
return (<div>Atlas loading</div>);
}
return (
<BrowserRouter>
<div>
<Switch>
<Route exact path={INDEX} component={() => <CoverPage IndexAtlas={window.IndexAtlas}/>}/>
<Route path={SCENE_PAGE} component={() => <ScenePage IndexAtlas={window.IndexAtlas}/>}/>
<Redirect from="*" to={INDEX}/>
</Switch>
</div>
</BrowserRouter>
);
}
}
- Here I comment how is the
IndexAtlas
made of, the model we have:
First Atlas:
Is an array which holds all the SubAtlas
(anatomic families). It also knows which is the current SubAtlas
chosen by putting in the URL ?subAtlas={number}
/*El Atlas guarda todas las familias anatomicas*/
import {getParameterByName} from "../constantRoutes";
import SubAtlas from "./SubAtlas";
export default class Atlas {
constructor() {
this.atlas = [];
}
addSubAtlas(subAtlas) {
this.atlas.push(new SubAtlas(subAtlas));
}
getSubAtlasActual(index = 0) {
return this.atlas[getParameterByName('subAtlas') || index];
}
}
It is made of SubAtlas
:
/*El SubAtlas guarda cada familia anatomica compuesta por Parte*/
import Parte from "./Parte";
import {getParameterByName} from "../constantRoutes";
export default class SubAtlas {
constructor(subAtlas) {
this.subAtlas = subAtlas.map((parte) => {
const newParte = new Parte(parte);
newParte.createSegmentos();
return newParte;
});
}
getParteActual(index = 0) {
return this.subAtlas[getParameterByName('parte') || index];
}
}
It is an array of Parte
. In addition knows what is the current part selected via query string: ?parte={number}
.
Each Parte
contains the URLs loaded via the JSON in its parte attribute:
/*Cada Parte encapsula al objeto con: original, segmentado (son NRRDs), miniatura, titulo,
* etiquetas*/
import Segmento from "./Segmento";
export default class Parte {
constructor(parte) {
this.parte = parte;
this.segmento = null;
}
createSegmentos() {
this.segmento = new Segmento(this.parte.etiquetas);
this.segmento.readTextFile(this.segmento.etiquetas);
this.segmento.createColors(this.segmento.text);
this.segmento.createNames(this.segmento.text);
this.segmento.createAllSegmentos();
}
getSegmentos() {
return this.segmento.segmentos;
}
}
And Parte
hosts a segmento
, which is an instance of Segmento
, which holds text files, colors, names.
/*Esta sin usar aún. Debería operar con las etiqquetas y extraer nombres y colores*/
export default class Segmento {
constructor(etiquetas) {
this.etiquetas = etiquetas;
this.text = '';
this.names = '';
this.colors = '';
}
readTextFile = (url) => {
const rawFile = new XMLHttpRequest();
rawFile.open("GET", url, false);
rawFile.overrideMimeType('text/xml; charset=iso-8859-1');
rawFile.onreadystatechange = () => {
if (rawFile.readyState === 4) {
this.text = rawFile.responseText;
}
};
rawFile.send();
};
splitLines(text) {
return text.split('\n');
}
splitWords(line) {
return line.split('" "');
}
createNames(text) {
const lines = this.splitLines(text);
this.names = this.splitWords(lines[0]);
}
createColors(text) {
const lines = this.splitLines(text);
this.colors = this.splitWords(lines[1]);
}
createAllSegmentos() {
this.segmentos = this.names.map((name, index) => {
return {
name: name,
color: this.colors[index]
}
});
}
}
And Segmento
is the way we have to read the text file, and mix in an object name and color.
To give a picture of it:
- I explain how data is used in code:
(1.1) App passes IndexAtlas
to CoverPage
which is used to show the SubAtlas
number 3, which has the horse's full NRRD:
/*Esta clase se responsabiliza de mantener el estado de la carga del Atlas.
* Para ello tiene en su state, isAtlasloading
* Ademas inicia la carga asíncrona instanciando a JSONHeader.
* Observamos que para que su hijo, JSONHeader pueda cambiar el state isAtlasLoading, le pasamos
* finishCallback.
* Además tiene las rutas.*/
import React, {Component} from 'react';
import BrowserRouter from "react-router-dom/es/BrowserRouter";
import Switch from "react-router-dom/es/Switch";
import Route from "react-router-dom/es/Route";
import Redirect from "react-router-dom/es/Redirect";
import ScenePage from "../ScenePage/index";
import CoverPage from "../CoverPage/index";
import {INDEX, SCENE_PAGE} from "../../constantRoutes";
import JSONHeader from "../../newModel14Junio/JSONHeader";
export default class App extends Component {
constructor(props) {
super(props);
const finishCallback = () => {
this.setState({isAtlasLoading: false});
};
const header = new JSONHeader('/atlas/json/', finishCallback);
this.state = {
isAtlasLoading: true
};
}
render() {
if (this.state.isAtlasLoading) {
return (<div>Atlas loading</div>);
}
return (
<BrowserRouter>
<div>
<Switch>
<Route exact path={INDEX} component={() => <CoverPage IndexAtlas={window.IndexAtlas}/>}/>
<Route path={SCENE_PAGE} component={() => <ScenePage IndexAtlas={window.IndexAtlas}/>}/>
<Redirect from="*" to={INDEX}/>
</Switch>
</div>
</BrowserRouter>
);
}
}
(1.2) NrrdCanvas
receives the filename in the prop: raizCoverPageNrrd
.
(1.3) NrrdCanvas
passes down both IndexAtlas
and raizCoverPageNrrd
to threeEntryPoint
.
/*Responsabilidad de relacionar la interfaz React con el pintar Three*/
import React, {Component} from 'react';
import threeEntryPoint from '../threejs/threeEntryPoint';
import spinner from './spinner.gif';
import './styles.css';
export default class NrrdCanvas extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
new threeEntryPoint(this.threeRootElement,
this.props.originalNrrd,
this.props.raizCoverPageNrrd,
this.props.distanceFromCameraToNrrd,
this.props.IndexAtlas);
}
render() {
return (
<div
className='nrrdCanvas'
style={
{
width: this.props.width,
height: this.props.height,
}}
ref={element => this.threeRootElement = element}
>
<img
className='spinner' //Required: we use it to removeSpinners() in SceneSubject
src={spinner}
/>
</div>
);
}
}
(1.4) threeEntryPoint
passed down both raizCoverPageNrrd
and IndexAtlas
:
/*Se responsabiliza de crear el canvas web. Además introduce al SceneManager en él.
* También vincula los eventos necesarios*/
import SceneManager from './SceneManager';
export default class threeEntryPoint {
constructor(container, originalNrrd, raizCoverPageNrrd, distanceFromCameraToNrrd, IndexAtlas) {
this.canvas = this.createCanvas(document, container);
this.sceneManager = new SceneManager(this.canvas, originalNrrd, raizCoverPageNrrd, distanceFromCameraToNrrd, IndexAtlas);
this.canvasHalfWidth;
this.canvasHalfHeight;
this.render = this.render.bind(this);
this.resizeCanvas = this.resizeCanvas.bind(this);
this.bindEventListeners();
this.render();
}
createCanvas(document, container) {
const canvas = document.createElement('canvas');
container.appendChild(canvas);
return canvas;
}
bindEventListeners() {
window.onresize = this.resizeCanvas;
this.resizeCanvas();
}
resizeCanvas() {
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
this.canvas.width = this.canvas.offsetWidth;
this.canvas.height = this.canvas.offsetHeight;
this.canvasHalfWidth = Math.round(this.canvas.offsetWidth / 2);
this.canvasHalfHeight = Math.round(this.canvas.offsetHeight / 2);
this.sceneManager.onWindowResize()
}
render(time) {
requestAnimationFrame(this.render);
this.sceneManager.update();
}
}
(1.5) Our SceneManager
knows that we are using him from the CoverPage
because of raizCoverPageNrrd
is defined, which means that creates two SceneSubjects
(one is the background and the other the colors). This is the function createSceneSubjects()
, where passed down both: raizCoverPageNrrd
and IndexAtlas
.
(1.6) In SceneSubject
we use the filename in raizCoverPageNrrd
to load the horse's nrrd:
import NRRDLoader from "../../loaders/NRRDLoader";
import MultiVolumesSlice from "./MultiVolumesSlice";
import {getParameterByName} from "../../constantRoutes";
export default class SceneSubject {
constructor(scene, originalNrrd, raizCoverPageNrrd, IndexAtlas) {
this.scene = scene;
this.loader = new NRRDLoader();
this.filename = '';
if (this.isThereANrrdFromCoverPage(raizCoverPageNrrd)) {
this.filename = raizCoverPageNrrd;
} else {
if (IndexAtlas.getSubAtlasActual()) {
// console.error(IndexAtlas.getSubAtlasActual());
// console.error(typeof IndexAtlas.getSubAtlasActual());
// console.error(IndexAtlas.getSubAtlasActual() instanceof SceneSubject);
this.filename = originalNrrd ? IndexAtlas.getSubAtlasActual().getParteActual().parte.original
: IndexAtlas.getSubAtlasActual().getParteActual().parte.segmentado;
}
}
console.log('SceneSubject::this.loader.load::this.filename', this.filename);
this.loader.load(this.filename, (volume) => {
//z plane
let sliceZ = volume.extractSlice('z', Math.floor(volume.RASDimensions[2] / 4));
this.putColorsInCoverPageNrrd(raizCoverPageNrrd, sliceZ);
if (this.isThereASpinner()) {
this.removeSpinners();
}
this.scene.add(sliceZ.mesh);
});
}
addVolumeSliceToMultiVolumeSlice(sliceZ) {
if (!this.isMultiVolumeSliceDefined()) {
this.createMultiVolumeSlice();
this.insertBackgroundInMultiVolumeSlice(sliceZ);
} else {
this.setColorMap(sliceZ);
this.insertColorInMultiVolumeSlice(sliceZ);
window.multiVolumeSlice.repaint(true, false, false);
}
}
insertBackgroundInMultiVolumeSlice(sliceZ) {
const insertInBackground = true;
window.multiVolumeSlice.addSlice(sliceZ, 1, insertInBackground);
}
insertColorInMultiVolumeSlice(sliceZ) {
const insertInBackground = false;
window.multiVolumeSlice.addSlice(sliceZ, 1, insertInBackground);
console.log('El color map del label es:', sliceZ.colorMap);
}
setColorMap(sliceZ) {
const arrayColors = ["rgba(0,0,0,255)", "rgba(243,8,20,255)",
"rgba(0,255,114,255)", "rgba(15,67,182,255)",
"rgba(255,236,0,255)", "rgba(255,0,219,255)"];
sliceZ.colorMap = arrayColors.map((color) => this.colorToHex(color));
sliceZ.volume.dataType = 'label';
console.warn('SceneSubject::sliceZ::sliceZ.volume.dataType', sliceZ.volume.dataType);
}
colorToHex(color, opacity) {
let match,
r,
g,
b,
a;
if (typeof color === 'string') {
const rgb = /^rgb *\( *(\d+) *, *(\d+) *, *(\d+) *\)$/;
const rgba = /^rgba *\( *(\d+) *, *(\d+) *, *(\d+) *, *(\d*\.?\d*) *\)$/;
match = color.match(rgb) || color.match(rgba);
if (match) {
r = Number(match[1]);
g = Number(match[2]);
b = Number(match[3]);
a = Number(opacity || match[4] || 255);
if (a <= 1) {
a = Math.round(a * 255);
}
return (r << 24) + (g << 16) + (b << 8) + a;
}
}
throw 'Application did not manage to parse a color from : ' + color;
}
putColorsInCoverPageNrrd(raizCoverPageNrrd, sliceZ) {
if (raizCoverPageNrrd) {
this.addVolumeSliceToMultiVolumeSlice(sliceZ);
}
}
isThereASpinner() {
return document.getElementsByClassName('spinner')[0];
}
isMultiVolumeSliceDefined() {
return window.multiVolumeSlice;
}
createMultiVolumeSlice() {
window.multiVolumeSlice = new MultiVolumesSlice();
}
update(time) {
}
removeSpinners() {
const spinners = document.getElementsByClassName('spinner');
spinners[0].remove();
}
isThereANrrdFromCoverPage(raizCoverPageNrrd) {
return raizCoverPageNrrd;
}
}
(2.1) If we come back to our App, we pass into the ScenePage
the IndexAtlas
.
(2.2) The ScenePage
passed down the IndexAtlas
to: Gallery
, SegmentsList
, TwoNrrdCanvas
:
/*Tiene la responsabilidad de mostrar la Galeria, la Lista de Segmentos, los Canvas y Titulos*/
import React, {Component} from 'react';
import SplitterLayout from 'react-splitter-layout';
import './styles.css';
import Gallery from "../Gallery/index";
import SegmentsList from "../SegmentsList/index";
import TwoNrrdCanvas from "../TwoNrrdCanvas/index";
import Header from "../Header/index";
class ScenePage extends Component {
constructor(props) {
super(props);
console.log('ScenePage::props', props);
}
render() {
if (!this.props.IndexAtlas) {
return (<div>Loading</div>);
}
return (
<div>
<Header/>
<SplitterLayout
vertical={false}
percentage={true}
secondaryInitialSize={75}
>
<SplitterLayout
vertical={true}
>
<Gallery IndexAtlas={this.props.IndexAtlas}/>
<SegmentsList IndexAtlas={this.props.IndexAtlas}/>
</SplitterLayout>
<TwoNrrdCanvas IndexAtlas={this.props.IndexAtlas}/>
</SplitterLayout>
</div>
);
}
}
export default ScenePage;
(2.2.1) In Gallery
we show each PNG which is into the SubAtlas
choosen by the user.
/*Responsabilidad de mostrar la galeria de miniaturas*/
import React, {Component} from 'react';
import './styles.css';
import {getParameterByName} from "../../constantRoutes";
// console.log('Gallery::window.IndexAtlas', window.IndexAtlas);
export default class Gallery extends Component {
constructor(props) {
super(props);
}
render() {
if (!this.props.IndexAtlas) {
return (<div>Loading</div>);
}
console.log(this.props.IndexAtlas.getSubAtlasActual().subAtlas);
return (
this.props.IndexAtlas.getSubAtlasActual().subAtlas.map((headPart, index) => {
return (
<a
href={`?parte=${index}&subAtlas=${getParameterByName('subAtlas')}`}
key={index}
>
<img
className='miniature'
src={headPart.parte.miniatura}
/>
</a>
);
}
)
)
;
};
};
(2.2.2) In the SegmentsList
all segments' names are stored in the current choosen Part
. It also displays all names in a list:
/*Tiene la responsabilidad de leer la lista de segmentos desde las etiquetas, el fichero txt
* Ademas muestra la lista de segmentos y colorea el ultimo sobre el cual se pulsa*/
import React, {Component} from 'react';
import {getParameterByName} from "../../constantRoutes";
import './styles.css';
import Parte from "../../newModel14Junio/Parte";
export default class SegmentsList extends Component {
constructor(props) {
super(props);
this.state = {words: [], activeWord: -1}
}
onClickFunction = (idx) => {
this.setState({activeWord: idx})
};
componentDidMount() {
this.setState(() => ({
words: this.props.IndexAtlas.getSubAtlasActual().getParteActual().getSegmentos()
.map((segmento) => segmento.name)
}));
}
render() {
return (
<div>
<ol>
{this.state.words.map((word, index) =>
<li
onClick={this.onClickFunction.bind(null, index)}
className={`segmentsList${this.state.activeWord === index ? ' selected' : ''}`}
key={index}
>
{word}
</li>
)}
</ol>
</div>
);
}
}
(2.2.3) In TwoNrrdCanvas
we use two canvas:
1) Draws the original nrrd, when originalNrrd
is true
2) Paints a segmented nrrd, when originalNrrd
is false.
/*Tiene la responsabilidad de mostrar dos canvas con sus nrrd en horizontal
* Ademas muestra los titulos asociados a cada uno de ellos debajo*/
import React, {Component} from 'react';
import SplitterLayout from 'react-splitter-layout';
import {getParameterByName} from "../../constantRoutes";
import NrrdCanvas from "../NrrdCanvas/index";
import './styles.css';
import Title from "../Title/index";
import Parte from "../../newModel14Junio/Parte";
class TwoNrrdCanvas extends Component {
constructor(props) {
super(props);
}
render() {
return (
<SplitterLayout
vertical={false}
percentage={true}
secondaryInitialSize={50}
>
<SplitterLayout
vertical={true}
percentage={true}
secondaryInitialSize={10}
>
<NrrdCanvas
width='400px'
height='400px'
originalNrrd={true}
IndexAtlas={this.props.IndexAtlas}
/>
<Title title={this.props.IndexAtlas.getSubAtlasActual() &&
this.props.IndexAtlas.getSubAtlasActual().getParteActual().parte.titulo}/>
</SplitterLayout>
<SplitterLayout
vertical={true}
percentage={true}
secondaryInitialSize={10}
>
<NrrdCanvas
width='400px'
height='400px'
originalNrrd={false}
IndexAtlas={this.props.IndexAtlas}
/>
<Title title='Titulo NRRD segmentado'/>
</SplitterLayout>
</SplitterLayout>
);
}
}
export default TwoNrrdCanvas;
(2.3) From TwoNrrdCanvas
we pass down to each NrrdCanvas
the IndexAtlas
(2.4) The NrrdCanvas
passes down to threeEntryPoint
the IndexAtlas
(2.5) Our threeEntryPoint
create a SceneManager
passing down the IndexAtlas
(2.6) The SceneManager
generates a SceneSubject
passing to it the IndexAtlas
(2.7) Finally the SceneSubject
loads the original or segmented nrrd into the canvas:
export default class SceneSubject {
constructor(scene, originalNrrd, raizCoverPageNrrd, IndexAtlas) {
this.scene = scene;
this.loader = new NRRDLoader();
this.filename = '';
if (this.isThereANrrdFromCoverPage(raizCoverPageNrrd)) {
this.filename = raizCoverPageNrrd;
} else {
if (IndexAtlas.getSubAtlasActual()) {
// console.error(IndexAtlas.getSubAtlasActual());
// console.error(typeof IndexAtlas.getSubAtlasActual());
// console.error(IndexAtlas.getSubAtlasActual() instanceof SceneSubject);
this.filename = originalNrrd ? IndexAtlas.getSubAtlasActual().getParteActual().parte.original
: IndexAtlas.getSubAtlasActual().getParteActual().parte.segmentado;
}
}
console.log('SceneSubject::this.loader.load::this.filename', this.filename);
this.loader.load(this.filename, (volume) => {
//z plane
let sliceZ = volume.extractSlice('z', Math.floor(volume.RASDimensions[2] / 4));
this.putColorsInCoverPageNrrd(raizCoverPageNrrd, sliceZ);
if (this.isThereASpinner()) {
this.removeSpinners();
}
this.scene.add(sliceZ.mesh);
});
}
- Question
Which approach is better? To use a global variable only in the Components
where we need it: Gallery
, SegmentsList
, SceneSubject
?
Or
To use a dependencies chain and convert the IndexAtlas
variable as a local one in JSONHeader
?