3
\$\begingroup\$
  1. 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:

original part

And we also have the PNG, the miniature image:

png Original Head 1

Then in the SubAtlas' root directory, for example in cabeza, we have: cabezasegmentado01.nrrd which is a model with different Segments on it:

segmented nrrd

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.

  1. 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>
        );
    }
}
  1. 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:

Atlas Data

  1. 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:

CoverPage

/*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: raizCoverPageNrrdand 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);
        });
    }
  1. 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?

\$\endgroup\$
1
  • \$\begingroup\$ That's a lot - try to simplify your question, and you might get better responses. \$\endgroup\$
    – Johntron
    Nov 5, 2018 at 6:47

1 Answer 1

1
\$\begingroup\$

If you're asking if you should use a global variable or pass values as props down multiple components, I'd say neither.

The later, passing props to distant offspring, is a common problem in the React community. It leads to rigid apps that require many changes just to move one small thing around, and it makes it hard to reason about the app due to indirection. Note: I'm not saying you shouldn't use props, just don't pass the too far (one hop – two if they're in the same file – is my guideline).

Ultimately, you have some data, and the definition of this data can live anywhere – a module/file is a good place. Now, you don't want just any old thing to be able to mutate this object - you want to control access.

At some point this data will need to be initialized, you might need a way of triggering updates to this data, and you need changes in data to trigger behavior in your app.

For initialization, this could happen in the root of your app (if it's truly global and used by everything), or it could happen inside a component (e.g. componentDidMount()).

For triggering updates to this data, you just need your module to expose a method components can call when they need to trigger an update.

For triggering behavior when data changes, you can expose a hook – a function you can pass another function to, then just invoke the passed function when data changes. Components can pass one of their own methods for invocation when data changes.

While I don't recommend using this (see next paragraph), here's what that might look like:

class IndexAtlas {
  constructor() {
    this.subscribers = [];
    this.state = {};
  }
  
  update(args) {
    // fetch/initialize new data based on args (e.g. fire AJAX call)
    // then save new data to this.state
    // then notify all subscribers
    this.notify();
  }
  
  subscribe(notify) {
    this.subscribers.push(notify);
  }
  
  notify() {
    this.subscribers.forEach(notify => notify(this.state));
  }
}

export default new IndexAtlas(); // should only be one of these (aka Singleton)

The key thing to realize now, is that IndexAtlas can be imported by any Component, anywhere – even multiple Components. This is important, because often the things that know how to initialize and update it are not the same thing that uses IndexAtlas's data.

Now the (root component) could be used to initialize (or any other component):

import { update } from 'wherever/index-atlas.js';
import { Component } from 'react';

export default class App extends Component {
  componentDidMount() {
    update(/* whatever args, like horse species */);
  }
  
  render() {
    return (<OtherStuff />);
  }
}

Next, some button could be used to trigger updates:

import { update } from 'wherever/index-atlas.js';

export default class AtlasQueryButton extends Component {
  constructor() {
    super();
    this.state = {
      horseAttribute: someDefaultValue
    };
  }

  render() {
    return (
    <button 
      onClick = {() => update(this.state.horseAttribute)}
    />);
  }
}

And finally, something can render IndexAtlas's data:

import { subscribe } from 'wherever/index-atlas.js';

export default class HorseRenderer {
  constructor() {
    this.state = {
      traits: ''
    }
  }

  componentDidMount() {
    subscribe(this.horseUpdated);
  }
  
  horseUpdated(traits) {
    this.setState({ traits });
  }
  
  render() {
    if (!this.traits.length) {
      return 'Select a horse';
    }
    
    return (`Horse has these traits: ${this.state.traits}`);
  }
}

There are multiple ways of doing this in React – Context, Redux, Container components, and Hooks. If you're new to React, I would look at Container components, because it teaches you how to separate data from presentation, then use Hooks (warning: they're still a proposal at time of writing), because they have a readable syntax and support better composition.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.