// history_begins_here
/**
    Original script from 
      http://math.hws.edu/eck/cs424/notes2013/threejs/pyramidIFS.html
    downloaded on 2017-06-08.
    It was based on revision 59 of the javascript library three.min.js.

    Gradual changes of the original html/javascript:

    From 2017-06-08 to 2017-06-22:
    -- link to recent version of three.js (revision 85, downloaded from threejs.org on 2017-06-01).
       Note that this version does not accept anymore:
       -- THREE.Face4( 3, 2, 1, 0) 
       -- pyramidGeom.computeCentroids() --> "A THREE.Face3 will be created instead."
       -- THREE.MeshFaceMaterial --> "Use an Array instead."
    -- rewritten function createWorld()
    -- added global var data with data of faces and lines to be plotted
    -- rewritten function doKey(evt)
       -- new key bindings
       -- camera is moved and rotated along its own axes
    -- added <div>'s in html text for about camera position etc.
    -- set 3d window to full browser window width and height
       with the example of webgl_buffergeometry_constructed_from_geometry.html from threejs.org/examples/
       -- added meta viewport and style within <head>
       -- changed <body> contents
       -- added function onWindowResize()
       -- few changes in init()
    -- added animations to show (time) steps, individual faces and camera orbit
    -- added window.onload = function() { init() };
       (this way seems more modern than <body onload="init()">)
    -- added an AxisHelper in createWorld()
    -- added stereo effect, by help of
         https://threejs.org/examples/#webgl_effects_stereo
    -- added function printCameraPosition() with translation of camera.getWorldDirection() into (azimuth,elevation)
    -- failed to print the elevation of the up direction vector of the camera in function printCameraPosition()
    -- added function handleDeviceOrientation()
       (measured device orientation is only printed on screen; preparation towards use of VR-set)
    -- added toggle on/off of full-screen mode
    
    On 2017-07-19/21/22:
    -- added data.texts, data.textcolorscolumnlength, data.textcolors, data.textproperties
             global var lineglyphs and lineglyphshapes
             functions createLineGlyphs(), initLineGlyphs, maximum(), minimum(), multiply(), lineglyphFromText()

    On 2017-07-24:
    -- added buttons for toggle on/off stepping, orbit, full screen and toggle on stereo view

    On 2017-07-26:
    -- continued with lineglyphs
    -- almost completed glyph list (missing are: @#$^&| and diacritics)
    -- added extra source paths for three.js and StereoEffect.js: in same directory as this html

    On 2017-07-28:
    -- continued with lineglyphFromText(), added commands ^C, ^U , ^D, ^S, ^O and ^A.
    -- added the glyph ^ and most diacritics

    On 2017-08-03:
    -- if no faces are given in data.faces, the boundingbox is defined from data.lines
    -- corrected showNextStep()

    On 2017-08-06/07:
    -- in lineglyphFromText(): automatic positioning of diacritics which are stacked
    -- added the use of data.shorttitle --> <title>
                        data.longtitle --> <div name=longtitle>
    -- in lineglyphFromText(): variable baselineskip due to diacritics and super/subscripts
    -- added function ifdefined(A,B) because the OR operator is not properly working for e.g. "var x = p["key"] || 1" when x should either 0 or 1, but here x will always be 1 !
    -- slant is now a property to be set in data.textproperties.
    -- added shapes for: @#$&|, dotless i and j, arrows <- and ->, circumference
    -- changed the shape of lower-case l

    On 2017-11-15:
    -- added the use of data.cameratour = [xp,yp,zp,xu,yu,zu,xf,yf,zf; ...] (position, up direction, focus)
    -- added a "spiky sine" function for showNextOrbit()

    On 2017-11-16:
    -- the threejs sources may now be found in three different locations

    On 2018-02-23:
    -- More space before and after dot, comma and other interpunction 
       (imposed xmax=3) for the lineglyphshapes.a typeface.
    -- Split of the code in geo_template.html into geo_subroutines.js and geo_example.html.

    On 2018-08-11:
    -- corrected in function showNextTour()

    On 2018-09-27:
    -- added camera station: data.camerastation = [xp,yp,zp,xu,yu,zu,xf,yf,zf; ...] (position, up direction, focus)

    On 2018-09-29:
    -- added orbit modes and orbit speeds
    -- use of iorbit for leading index at orbiting (instead of ocam.az etc.)
    -- added data.cameraorbitmodes.spiral.deltaazimuth = 10

    On 2018-10-01:
    -- added orbit modes "cardinal" and "across"
    -- tour and station buttons depend on existing of data

    On 2018-10-02:
    -- removed ocam and introduced envelope instead
    
    On 2018-10-03:
    -- recursive version of function fillindefaultfields()

    On 2018-10-11:
    -- added button for stepping speed and doToggleSteppingSpeed()
    -- corrected doToggleOrbitMode() and doToggleOrbitSpeed()
    -- isteppingspeed defines animation speed for showNextFace and showNextStep
    -- iorbitspeed defines animation speed for showNextOrbit and showNextTour
    -- introduced key strokes for changing speed levels and orbit mode
    -- introduced var shorttextKeyUse and changed in html:
         <div id="keyuse">
       into:
         <div id="innerinfo">
         <span id="keyuse"></span>
       an extra <div> tag (called innerinfo) without closing </div> seems to be necessary to prevent
       strange make-up (cannot understand why).

    On 2020-08-27:
    -- implemented a new version of three.js: r120 released on 2020-08-26
       -- this new version works right-a-way :)
       -- only these complains and suggestions presented in the web console:
          -- THREE.BufferGeometry: .addAttribute() has been renamed to .setAttribute().
          -- THREE.AxisHelper has been renamed to THREE.AxesHelper.
          -- Use of the orientation sensor is deprecated.
          -- THREE.Object3D: .getWorldRotation() has been removed. Use THREE.Object3D.getWorldQuaternion( target ) instead.
    -- added loading script OrbitControls.js and adapted code with the example from:
          Three.js Rendering on Demand
          https://threejsfundamentals.org/threejs/lessons/threejs-rendering-on-demand.html
          -- js code example: https://threejsfundamentals.org/threejs/threejs-render-on-demand.html
       -- the adaptations work right-a-way :)
       
    On 2020-08-28:
    -- studying OrbitControls.js:
       -- default behaviour:
          -- Orbit - left mouse / touch: one-finger move
          -- Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
          -- Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
       -- the ctrl/meta/shift keys are used in combination with mouse
       -- the four arrow keys do nothing more than panning:
            this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
          and strokes on these keys cause a call to internal function pan(...,...),
          but not to internal functions 
          -- dollyIn(...), dollyOut(...) (i.e. zooming),
          -- rotateLeft(...), rotateUp(..) (i.e. orbitting).
    -- remapped keys in doKey():
       -- k: forward, comma: backward, m: turn left, dot: turn right
       -- no use of arrow keys
       ==> the two times .addEventListener("keydown",...) within OrbitControls.js
           and document.addEventListener("keydown", doKey, false) below causes
           bad interference: the arrow keys react only after a stroke on another key
           for which there is an reaction in doKey(). But even this does not always 
           cause the arrow keys to react. 
       ==> this problem is solved by:
           -- issuing controls.enableKeys = false
           -- double functionality in doKey(): 
              k/arrow up: forward, comma/arrow down: backward, 
              m/arrow left: turn left, dot/arrow right: turn right
    -- introduced <div id="buttonrow"> (generated by init()) 
       and removed old code on buttons    
    -- introduced automatic creation of <div id="info"> and its
       children
    -- only if <div id="info"> is already present in the html body,
       this block is nicely put centered at the top of the page and 
       _onto_ the 3d picture; otherwise, it is put at the left corner
       and the 3d picture is put below the info element.
    -- printCameraPosition() is now only called within render()
    -- rewriting doKey() and introducing new functions goForward() etc.
       
    On 2020-08-29:
    -- rewritten doKey() and introducing new functions goForward() etc.
    -- cleaning code: 
       -- removed old versions of doKey()
       -- renaming functions doToggleXXX into toggleXXX etc.
       -- old code on pyramid dating back from the example script in
          http://math.hws.edu/eck/cs424/notes2013/threejs/pyramidIFS.html
    -- added functions printMessage() and window.onerror = function ...
       in geo_example.html for better error notification, and
       put all the functional scripts below the <body> element.
    -- issuing         
          printMessage("info","message","we got here");
          throw new Error("we got here");
       will print even more informative error message with line and 
       column numbers!
    -- adapted showNextFace() so that data.facenames may be an empty array
       (although data.faces not)
    -- updated threejs code:
       -- THREE.BufferGeometry: .addAttribute() renamed to .setAttribute().
       -- THREE.AxisHelper renamed to THREE.AxesHelper.
       -- removed THREE.Object3D.getWorldRotation() without replacement.
    -- started with experimental createColourbars()

    On 2020-08-30:
    -- geo_example.html renamed as hgeo_example.html
    -- this file geo_subroutines.js renamed as hgeo_subroutines.js

    On 2020-08-31:
    -- added forgotten case for key stroke "b" in doKey()
    
    On 2020-09-03:
    -- corrected THREE.PerspectiveCamera(45, canvas2.width/canvas2.height, 0.1, 2000)
       (instead of 100 for far fustrum plane)
    -- trying by adding camera.updateProjectionMatrix() in function render()
       --> no improvement, even it slows down --> not implemented afterall
    -- added in functions showNextOrbit(), showNextStation() and showNextTour():
         // this sets the correct up direction of the camera (the reason is not understood):
         controls.target = fcam;
         controls.update();
       after existing:
         camera.position.set(pcam.x,pcam.y,pcam.z);	
         camera.lookAt(fcam);
         camera.up.set(ucam.x,ucam.y,ucam.z);

    On 2020-09-04:
    -- written function translateRoteteCamera(), thanks to which the use of .translateX(),
       .rotateX() is better understood.
    -- written function putCamera() to avoid repetition.
    -- rewritten originalPosition().
    -- removed old moveCamera() and then renamed translateRoteteCamera() as moveCamera().
    -- introduced goLeftward() and goRightward().
    -- rewritten doKey() so that it uses only data from variable buttonrows[].
    -- introduced style column in buttonrows[]
    
    On 2020-09-05:
    -- experimenting with experimental functions createSurfacesForCB() and createColourbars()
    -- introduced global vars renderer2, scene2, camera2 and canvas1relwidth
    -- introduced a <div> elemennt to contain one or two canvasses 
    -- rewritten onWindowResize()
    
    On 2020-09-08/09:
    -- experimenting with euler angles, up directions and axis-angle representations 
       in printCameraPosition()

    On 2020-09-11/12:
    -- overhauling related to multiple canvasses, and by renaming variables:
       -- new global variable wgeo[iCanvas].
       -- global variables renderer, camera etc. are arrays with one element per canvas.
       -- global variable data may be given as a record (one canvas is drawn), or
          an array of records (multiple canvasses are drawn).
    -- written updateEnvelope() and camera positioning due to the envelope
       camera[icanvas].fov and camera[icanvas].aspect in createGeometries().
    -- introduced nStep[icanvas] and adapted showNextStep()
    -- introduced spaceSteps etc. (among others in moveCamera() and createGeometries())
    -- introduced button.value[id] and "<value>" for button texts.
       
    On 2020-09-18:
    -- still trying to get camera tilt (around direction) in printCameraPosition()
       without success
    -- added goUpward() and goDownward()
    -- reduced button sizes
    -- introduced "halfxheightabovefirstbaseline" for textproperty "anchorvertical"
    -- elaborated <value> on buttons for time stepping, orbitting, touring,
       faces, full screen and stereo.
    -- improved button effects, among others by distinguishing showNextFace() from
       showNextFaceAnimated(), and showNextStep() from showNextStepAnimated()
    -- renamed variable istep and nstep as iTimestep and nTimestep[icanvas]
    -- written function stopOrbitTour() for an eventlistener, but it does not stop 
       orbitting (and touring) when the touch pad is used
    -- written progressionBar() and used in showNextTimestepAnimated(), 
       showNextFaceAnimated(), showNextTour().
    -- improved toggleTimestepping() and toggleFaces() because timestepping and
       showing faces are mutually exclusive.
    
    On 2020-09-19:
    -- because timestepping and animating individual faces are mutually exclusive,
       improved: toggleTimestepping(), showNextTimestep(), showFirstTimestep(), 
       toggleFaces(), showNextFace(), showAllFaces()
    -- introduced wgeo[icanvas].colorsfrom, so that the scene on the second 
       canvas can use the colors from the first one.
    -- introduced transparency for faces (boolean transparentFaces, function 
       toggleTransparency(), etc.)
    -- checked color indices -1 (invisible) and -2 (white/mark) in createSurfaces()
       and hgeo_to_html.m: these values are allowed in data[icanvas].facecolors !
    -- corrected createLineSegments() for color index value -2 (white/mark) in 
       data[icanvas].linecolors
       
    On 2020-09-25:
    -- still trying to get camera tilt (around direction) in printCameraPosition()
       without success
    -- written calcAzElR() and cleaned printCameraPosition()
    -- added the times symbol in variable lineglyphshapes (to be invoked by ^Catimes^E )
    -- renamed data.camerastation as data.camerastations
    -- introduced data.camerastationnames and improved showNextStation()
    -- writing calcExtraStations()

    On 2020-09-26:
    -- renamed global variable istation as iStation
    -- introduced global variables nStation[icanvas] and nExtraStation[icanvas]
    -- written calcExtraStations(), recalcExtraStations() and showExtraStation()
    -- renamed doKey() as handleKeyStroke(), 
       introduced handleButtonPress() and global variable lastAction
    -- adapted onWindowResize() and render(), introduced render_iCanvas0()

    On 2020-10-08:
    -- succeeded calculation of camera roll (or tilt or swing) in printCameraPosition()
    -- abolished extra stations with imethod == 0
    -- introduced global variable page{} to prevent too often recalculations 
       of extra stations in calcExtraStations()

    On 2020-10-09:
    -- streamlining functions render(), calcExtraStations() etc.:
       -- written detectWindowChange() and renderAll()
       -- adapted calcExtraStations() and onWindowResize()
       -- deleted recalcExtraStations()
       -- recalculation of extra camera stations is now done only when necessary
       -- rendering must be done at:
          -- toggleButtonRow() because of _possible_ page aspect change
          -- onWindowResize() because of page aspect change
          -- after change of camera position or orientation
       -- render(icanvas) is called by:
          moveCamera()
          originalPosition()
          showNextFaceAnimated()
          showNextOrbit()
          showNextTour()
          showNextStation()
          toggleStereo()
          showExtraStation() (--> render(icanvas) is always called, 
                                  sometimes via showNextStation() or after putCameraAtStation())
          renderAll() (via showExtraStation())
          render_iCanvas0()
       -- renderAll(true) is called by:
          showNextTimestepAnimated()
          toggleTransparency()
          init()
       -- renderAll(false) is called by:
          toggleButtonRow()
          onWindowResize()
    -- added timings of page load, data read and init() function
    -- reduced number of console.log() called in subroutines called by createGeometries()
    -- added "vertical-align: top" in
       <span style="display: inline-block; width: 49%; text-align: right; vertical-align: top;">
       and converted into <span class="leftside"> and <span class="rightside">, respectively
    
    On 2020-10-10:
    -- introduced station and interactive clipping planes:
       -- data.camerastations has now a 10-th element per station
       -- introduced global variables clipStation and clipInteract
       -- written toggleClipStation(), toggleClipInteract(), convertClippingPlanes(),
          applyClippingPlanes(), increaseAz() etc.
       -- adapted toggleButtonRow(), createGeometries(), showNextStation(), showNextOrbit()

    On 2020-10-12:
    -- added "tags" for history
    
    On 2020-10-14:
    -- commented out window.addEventListener("deviceorientation", handleDeviceOrientation, true)
       (otherwise, firefox on smartphone complained)

    On 2020-10-16:
    -- updated to do list
       
    On 2020-10-23:
    -- introduced <span class=contrast> for transparent (black) background behind texts in html

    On 2020-12-09:
    -- trying to pick points by the mouse pointer
       https://threejs.org/docs/#api/en/core/Raycaster
       https://threejs.org/docs/#api/en/objects/Points
       https://threejs.org/docs/#api/en/materials/PointsMaterial
       https://threejs.org/examples/?q=interac#webgl_interactive_lines
       https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_lines.html
       https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_buffergeometry.html
       https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_raycasting_points.html
       https://stackoverflow.com/questions/55278137/how-to-select-and-highlight-multiple-objects-with-rectangular-selection-ribbon
    -- succeeded with THREE.Raycaster and THREE.Points
       -- introduced: global variables raycaster[icanvas], mouse[icanvas], iPick and pickModes[]
       -- extended wgeo[icanvas] with fields "facepoints", "linepoints", "crosshair"
       -- written functions togglePickMode(), createPoints(), pickPoint(), createCrosshair()
       -- adapted function render(icanvas)
    -- thicker linewidth (unknown units) does not seem to have effect, neither in Mozilla nor in Chrome:
       var linesegmentsMat = new THREE.LineBasicMaterial( { linewidth: 5, vertexColors: THREE.VertexColors } );
       --> https://stackoverflow.com/questions/11638883/thickness-of-lines-using-three-linebasicmaterial
           --> webgl problem ? not longer supported by webgl ?
           --> use https://github.com/spite/THREE.MeshLine ?
               --> very nice
    -- thicker pointwidth (unknown units) is visible in Chrome, but not in Mozilla
         pointMat = new THREE.PointsMaterial( { size: 0.1 } );
       --> solution with different sizes:
           https://threejs.org/examples/?q=points#webgl_interactive_points
    -- nothing visible in mozilla at some of the threejs.org examples (but no problems in chrome):
         "Failed to create WebGL context: WebGL creation failed:
          * tryANGLE
          * Exhausted GL driver options."
       --> set true webgl.force-enabled in about:config --> no effect
       --> only a problem on old Windows laptop?

    On 2020-12-10:
    -- threejs.org example https://threejs.org/examples/?q=points#webgl_interactive_points
       is vissible in mozilla on modern linux laptop.
    -- provisonal function onClick() putting the crosshair at each click.
    -- crosshair disappears too easily (at zooming e.g.).
       --> reading https://threejs.org/docs/index.html#manual/en/introduction/How-to-update-things

    On 2020-12-11:
    -- solved problems with easily disapearing crosshair. by experimenting in
       function createCrosshair() with:
         wgeo[icanvas].crosshair.geometry.attributes.position.needsUpdate = true; // (a)
         wgeo[icanvas].crosshair.geometry.verticesNeedUpdate = true; // (b)
         wgeo[icanvas].crosshair.geometry.normalsNeedUpdate = true;  // (b)
         wgeo[icanvas].crosshair.geometry.computeBoundingBox(); // (c)
         wgeo[icanvas].crosshair.geometry.computeBoundingSphere(); // (c)
       solution b is chosen (which seems to work as well as solution c).
       see explanations in function createCrosshair().
    -- applied above solution b also in functions createLineSegments() and
       createLineGlyphs().
    -- picking points is not very precise now:
       -- changing raycaster[icanvas].params.Line.threshold from 0.1 to 0.001 
          does not have significant effect.
       -- solved problem by selection on intersects[k].distanceToRay (instead
          of using intersects[0], which has been sorted by intersects[k].distance).
    -- added visiblity of the crosshair in function createCrosshair().
       Unfortunately, this reintroduced the easy dissapearance of the crosshair!
       Solved by applying the above solution c instead of b.
       The same solution c is implemented also in createLineSegments() and
       createLineGlyphs().
    -- rewritten function onClick().
    -- pickPoint() only works for external points, not internal points!

    On 2021-12-11:
    -- corrected a minor type error at printing in pickPoint()
    -- added writing to clipboard in pickPoint()
    -- introduced variable shadowClipboard and function copyToClipboard()
    -- adapted printCameraPosition() for writing camera position to the
       clipboard 
    
  To do:
  -- pickPoint() only works for external points, not internal points!
     why? how to fix?
  -- three.js mentions that .dynamic is deprecated
     --> update: wgeo[icanvas].XXX.geometry.dynamic = true;
  -- introduce balls:
     data[0].balls = [x y z]
            .ballradii = [r r r] --> timestepping?
            .ballcolors = [ic ic ic]
     -- form as:
        -- see https://threejs.org/examples/?q=points#webgl_interactive_points
        -- var geometry = new THREE.SphereGeometry( 5, 32, 32 ); // or THREE.SphereBufferGeometry( 5, 32, 32 );
           var sphere = new THREE.Mesh( geometry, material );
        -- var geometry = new THREE.BoxGeometry( 1, 1, 1 ); // or THREE.BoxBufferGeometry( 1, 1, 1 );
           var cube = new THREE.Mesh( geometry, material );
     -- call them balls, bubbles, spheres, ...?
  -- why not data[icanvas].textcolours = [ic ic ic; ...] instead of [r g b] in .textprop ?
  -- print camera position etc.
     -- view direction relative to north
         data[0].north = ... degrees from positive y axis
  -- default (extra) camera station on second (?) canvas is not always nicely 
     vertically centered on canvas
    -- background color: white?
  -- user input background and mark colour ?
     -- attention to text colours in the info <div> element
  -- controls.target = fcam
     and fcam = pcam + lambda * dcam --> better value for lambda depending on model size ?
  -- still jumps or bad rotation point for touchpad movement
  -- simplify code according to the example 
     https://threejsfundamentals.org/threejs/threejs-render-on-demand.html
  -- is it possible to circumvent all modules by putting all the code together
     in one file?
     -- http://rollupjs.org/guide/en/
     -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
     -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
     -- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
     -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
     -- https://v8.dev/features/modules#mjs
  -- second canvas always in front view?
  -- faster reading by var data = JSON.parse(text);
  -- replace variable names:
     -- in createLineSegments() and createLineGlyphs() by new ones
        according to those in createSurfaces()
     -- rename iorbit as iOrbit etc.
       
  -- explain circumflex code
  -- in lineglyphFromText(): what to do if both super and subscripts appear together?
  -- try https://threejs.org/examples/#webgl_effects_anaglyph
         https://threejs.org/docs/#api/cameras/StereoCamera
  -- use THREE.BufferGeometry instead of THREE.Geometry for the faces
     -- see https://threejs.org/docs/#api/en/core/BufferGeometry
            https://github.com/mrdoob/three.js/blob/master/examples/webgl_buffergeometry_indexed.html
     -- see https://threejsfundamentals.org/threejs/lessons/threejs-custom-buffergeometry.html
     -- the later website shows how THREE.BufferGeometry is organised:
        much more data has to be supplied: each face has got its own vertices,
        and each vertex has its own normal and colour. so, much repetition.
     -- convert from THREE.Geometry:
        var bufferGeometry = new THREE.BufferGeometry().fromGeometry( geometry );
     -- attractive is resetting colours by:
        geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
     -- problematic is changing transparency in BufferGeometry
  -- stop touring/orbitting when camera is moved by keys/mouse (see eventlistener for 
     stopOrbitTour() --> not working)
  -- instead of timestep, other names: variant, version
     instead of timestepping, other names: alternate, vary
**/
// history_ends_here

// global variables:

var dataReadTiming; // timing (in ms) of reading global variable data

var iCanvas0 = 0;  // index of the default canvas (the first canvas, always existing).
var nCanvas = 1;   // number of canvasses (just one at default).

var renderer = []; // three.js webgl renderer (per canvas).
var effect;        // effect to the renderer[iCanvas0] (in our case: stereo view, if desired).
var scene = [];    // 3d scene that will be rendered (per canvas).
var camera = [];   // camera that takes the picture of the scene (per canvas).
var controls = []; // camera controls (per canvas, we use here OrbitControls.js).
var mouse = [];     // mouse coordinates (per canvas), needed for the raycaster.
var raycaster = []; // raycaster (per canvas), for picking points by the mouse.

var wgeo = [];     // some data on the webgl model (per canvas).
// wgeo[icanvas] = {
//   colorsfrom : ic, // color list are taken from canvas ic
//   faces : new THREE.Mesh( faceGeometry, faceMaterial ),
//           // i.e. three.js record that contains the mesh obtained from data[icanvas].faces
//   ifaces : [iface, iface, ...], // where each row corresponds to wgeo[icanvas].faces.geometry.faces 
//                                 // and iface to the face index from data[icanvas].faces
//   lines : new THREE.LineSegments( linesegmentsGeo, linesegmentsMat ),
//           // i.e. three.js record that contains the line segments from data[icanvas].lines
//   texts : new THREE.LineSegments( lineglyphsGeo, lineglyphsMat ),
//           // i.e. three.js record that contains the line segments obtained from data[icanvas].texts
//   crosshair : new THREE.LineSegments( linesegmentsGeo, linesegmentsMat ),
//           // i.e. three.js record that contains the line segments for the crosshair
//   facepoints : new THREE.Points( pointGeometry, pointMat )
//           // i.e. three.js record that contains the points copied from data[icanvas].facepoints
//   linepoints : new THREE.Points( pointGeometry, pointMat )
//           // i.e. three.js record that contains the points copied from data[icanvas].linepoints
// };

var nspecialcolor; // total number of special colors defined in wgeo[icanvas].faces by createSurfaces().

var canvasrelwidth = [1]; // relative width per canvas.
var canvasrelheight = [1]; // relative width per canvas.

var page = {
  changed : false, // values of variable page are set by detectWindowChange()
  vinfo : 0,   // vertical size in pixels of "info" <div> element
  vwindow : 0, // vertical size in pixels of window/page
  hwindow : 0  // horizontal size in pixels of window/page
};

var zcaminit = 30; // default z position of camera.

var envelope = [];  // centre and size of the bounding envelope (per canvas).
// envelope[icanvas] = { 
//   box : new THREE.Box3(),            // bounding box
//   sphere : new THREE.Sphere(),       // bounding sphere (calculated from the bounding box)
//   centre : new THREE.Vector3(0,0,0), // focus for the camera orbit
//   radius : 3,                        // radius for the camera orbit
//   default : "posz",                  // default camera position (needed on icanvas != iCanvas0)
//   cam : {                            // camera positions and orientations
//     home : { pcam : ..., ucam : ..., fcam : ... } // "home" position and orientation
//     posz : i                         // index in data[icanvas].camerastation for "posz" position
//   }
// };

var lastAction = ""; // function name of the last/actual action (instigated by handleKeyStroke()
                     // and handleButtonPress()

var iFace = 0; // actual shown face (in the scene of canvas iCanvas0).
var nFace = 1; // total number of faces (in the scene of canvas iCanvas0).

var transparentFaces = true; // actual setting whether the faces are transparent (or opaque).

var clipStation = { // clipping planes defined by actual camera station.
  showing : false,
  planes : []
};
var clipInteract = { // clipping plane set by interaction.
  showing : false,
  az : 0,
  el : 0,
  x : 0,
  y : 0,
  z : 0
};

var iSpaceStep = 0; // actual index in spaceSteps.
var spaceSteps = [0.1, 0.5, 5]; // series of space step sizes.

var iTimestep = 0; // actual shown timestep (on canvas iCanvas0).
var nTimestep = [1]; // total number of timesteps (per canvas).
var showingTimesteps = false;
var wShowingTimesteps; // variable for window.clearInterval()

var timesteppingSpeed = 500; // basic speed level in ms.
var timesteppingSpeeds = [1, 0.5, 0.25, 3, 2]; // at least one element ! eventual time interval is (timesteppingSpeed * timesteppingSpeeds[iTimesteppingSpeed]) milliseconds
var iTimesteppingSpeed = 0;

var showingFaces = false;
var wShowingFaces; // variable for window.clearInterval()

var showingOrbitTour = 0; // 0 = off, 1 = orbit, 2 = tour
var wShowingOrbitTour; // variable for window.clearInterval()

var orbitModes = ["spiral","cardinal","across"];
var orbitSpeed = 500; // basic speed level in ms.
var orbitSpeeds = [1, 0.5, 0.25, 3, 2]; // at least one element ! eventual time interval is (orbitSpeed * orbitSpeeds[iorbitspeed]) milliseconds

var iorbitmode = 0;
var iorbitspeed = 0;
var iorbit = -1;   // in showNextOrbit() we start with iorbit = -1, so that first orbit step has iorbit=0
var norbit = 0;

var itour = -1;    // in showNextTour() we start with itour = -1, so that first tour step has itour=0

var iStation = -1; // in showNextStation() we start with iStation = -1, so that first station has iStation=0
var nStation = [0]; // number of camera stations given by the user in data[icanvas].camerastations
var nExtraStation = [0]; // number of extra camera stations automatically included into
                         // data[icanvas].camerastations by calcExtraStations()

var shadowClipboard = ""; // provisional string which can be copied to the system clipboard                         
                         
var showingFullScreen = false;
var showingStereo = false;

var iPick = 0;  // index to pickModes
var pickModes = ["off","pick face points","pick line points"];

var icount = 0;  // counter for averaging at handleDeviceOrientation
var ncount = 50; // number of counts after which to capture/show/use device orientation

// definition of the button rows, indicated by:
// -- column 0: function name (used as identifier too)
// -- column 1: array of event.key and/or event.code strings
//              (prefixes "Key" and "Digit" do not need to be included; do not write capitals)
//              (the first one will be used to print onto the button)
// -- column 2: text string before button
// -- column 3: text string on button
// -- column 4: default availability (value must be the same for the same column 0 value)
// -- column 5: html/css style, see e.g. https://www.w3schools.com/colors/colors_picker.asp
var buttonrowbutton = ["toggleButtonRow", ["b"], "", "Other buttons.", true, "background-color: rgb(220, 204, 255);"]; 
var buttonrows = [
   [ 
     ["goForward",        ["k","arrowup"], "Move around also by mouse, touch pad, or keyboard.", "Go forward.", true, ""],   
     ["goBackward",       [",","comma","arrowdown"], "", "Go backward.", true, ""],
     ["turnLeft",         ["m","arrowleft"], "", "Turn left.", true, ""],
     ["turnRight",        [".","period","arrowright"], "", "Turn right.", true, ""],
     ["turnUp",           ["s"], "", "Turn up.", true, ""],
     ["turnDown",         ["x"], "", "Turn down.", true, ""],
     ["turnLeftsideDown", ["z"], "", "Turn rightside up.", true, ""],
     ["turnLeftsideUp",   ["c"], "", "Turn leftside up.", true, ""],
     ["goLeftward",       ["[","bracketleft"], "", "Go leftward.", true, ""],   
     ["goRightward",      ["]","bracketright"], "", "Go rightward.", true, ""],
     ["goUpward",         ["=","equal"], "", "Go upward.", true, ""],   
     ["goDownward",       ["'","quote"], "", "Go downward.", true, ""],
     ["toggleSpaceStep",  ["j"], "", "Step size (actual <value> m).", true, ""],
     ["toggleTransparency", ["v"], "If transparency is on, only the front side of a face is visible.", "Transparency <value>.", true, ""],
     ["togglePickMode",   ["2"], "", "Pick mode (actually <value>).", true, ""],
     ["copyToClipboard",  ["a"], "", "Copy camera position.", true, ""],
     buttonrowbutton
   ],
   [
     ["toggleTimestepping",      ["t"], "", "Timestepping <value>.", false, ""],
     ["toggleTimesteppingSpeed", ["6"], "", "Timestepping speed.", false, ""],
     ["toggleOrbit",         ["o"], "", "Orbit <value>.", true, ""],
     ["toggleOrbitMode",     ["9"], "", "Orbit mode.", true, ""],
     ["toggleOrbitSpeed",    ["0"], "", "Orbit speed.", true, ""],
     ["toggleTour",          ["p"], "", "Tour <value>.", false, ""],
     ["toggleStation",       ["enter"], "", "Next station.", false, ""],
     buttonrowbutton
   ],
   [
     ["showNextTimestep",    [" ","space"], "", "Next timestep.", false, ""],
     ["showFirstTimestep",   ["y"], "", "First timestep.", false, ""],
     ["toggleFaces",         ["f"], "", "Individual faces <value>.", true, ""],
     ["showNextFace",        ["g"], "", "Next face.", true, ""],
     ["showAllFaces",        ["u"], "", "All faces.", true, ""],
     ["originalPosition",    ["home"], "", "Original position.", true, ""],
     buttonrowbutton
   ],
   [
     ["toggleClipStation",   ["-","minus"], "", "Station clip <value>.", false, ""],
     ["toggleClipInteract",  [], "", "Interactive clip <value>.", true, ""],
     ["decreaseAz",          [], "", "azimuth &lt; (actual <value> deg).", false, ""],
     ["increaseAz",          [], "", "azimuth &gt; (actual <value> deg).", false, ""],
     ["decreaseEl",          [], "", "elevation &lt; (actual <value> deg).", false, ""],
     ["increaseEl",          [], "", "elevation &gt; (actual <value> deg).", false, ""],
     ["decreaseX",           [], "", "x position &lt; (actual <value> m).", false, ""],
     ["increaseX",           [], "", "x position &gt; (actual <value> m).", false, ""],
     ["decreaseY",           [], "", "y position &lt; (actual <value> m).", false, ""],
     ["increaseY",           [], "", "y position &gt; (actual <value> m).", false, ""],
     ["decreaseZ",           [], "", "z position &lt; (actual <value> m).", false, ""],
     ["increaseZ",           [], "", "z position &gt; (actual <value> m).", false, ""],
     ["toggleSpaceStep",     ["j"], "", "Step size (actual <value> m).", true, ""],
     buttonrowbutton
   ],
   [
     ["toggleLongTexts",  ["i"], "Sorry that the selected keys for actions have some logic positions only on a qwerty keyboard probably.", "Long texts.", true, ""],
     ["toggleFullScreen", ["1"], "", "Full screen <value>.", true, ""],
     ["toggleStereo",     ["8"], "", "Stereo view <value>.", true, ""],
     buttonrowbutton
   ]
];

var nbuttonrow = buttonrows.length; // number of button rows
var ibuttonrow = nbuttonrow; // actual button row
var showingLongTexts = false;

var button = {}; // a record for the buttons
defaultsButton();

/*  Function to fill in defaults in record button{}.
 */
function defaultsButton() {
  button.avail = {}; // whether the button is available and can be shown
  button.value = {}; // string which is filled in at <value> on the button text
  
  var ir, ib, id;
  
  for (ir = 0; ir < nbuttonrow; ir = ir + 1) {
    for (ib = 0; ib < buttonrows[ir].length; ib = ib + 1) {
      id = buttonrows[ir][ib][0];
      button.avail[id] = buttonrows[ir][ib][4];
      button.value[id] = "";
    };
  }; 
}

/*  Show the buttons on screen depening on ibuttonrow and showingLongTexts.
 */
function toggleButtonRow() {
  ibuttonrow += 1;
  if (ibuttonrow >= nbuttonrow) {
    ibuttonrow = 0;
  };
  
  // set the values to be printed on the buttons:
  button.value.toggleSpaceStep = (spaceSteps[iSpaceStep]).toFixed(3);
  button.value.toggleTransparency = ["off","on"][0+(! transparentFaces)];
  button.value.togglePickMode = pickModes[iPick];
  button.value.toggleClipStation = ["off","on"][0+(! clipStation.showing)];
  button.value.toggleClipInteract = ["off","on"][0+(! clipInteract.showing)];
  button.value.toggleTimestepping = ["off","on"][0+(! showingTimesteps)];
  button.value.toggleOrbit = ["off","on"][0+(! (showingOrbitTour==1))];
  button.value.toggleTour = ["off","on"][0+(! (showingOrbitTour==2))];
  button.value.toggleFaces = ["off","on"][0+(! showingFaces)];
  button.value.toggleFullScreen = ["off","on"][0+(! showingFullScreen)];
  button.value.toggleStereo = ["off","on"][0+(! showingStereo)];

  button.value.increaseAz = (clipInteract.az).toFixed(0);
  button.value.decreaseAz = (clipInteract.az).toFixed(0);
  button.value.increaseEl = (clipInteract.el).toFixed(0);
  button.value.decreaseEl = (clipInteract.el).toFixed(0);
  button.value.increaseX = (clipInteract.x).toFixed(3);
  button.value.decreaseX = (clipInteract.x).toFixed(3);
  button.value.increaseY = (clipInteract.y).toFixed(3);
  button.value.decreaseY = (clipInteract.y).toFixed(3);
  button.value.increaseZ = (clipInteract.z).toFixed(3);
  button.value.decreaseZ = (clipInteract.z).toFixed(3);
  
  var html = "";
  var ib, id, key, style, buttontext, value;
  
  for (ib = 0; ib < buttonrows[ibuttonrow].length; ib = ib + 1) {
    id = buttonrows[ibuttonrow][ib][0];
    if (button.avail[id] == true) {
      style = buttonrows[ibuttonrow][ib][5];
      if (style != "") {
        style = " style=\"" + style + "\"";
      };
      //console.log(style);
      buttontext = buttonrows[ibuttonrow][ib][3];
      value = button.value[id];
      buttontext = buttontext.replace("<value>",value);
      if (showingLongTexts) {
        if (buttonrows[ibuttonrow][ib][1].length > 0) {
          key = buttonrows[ibuttonrow][ib][1][0]; // use first element in array
          if (key == " ") {
            key = "<b>space bar</b>";
          } else if (key == ",") {
            key = "<b>comma</b> key";
          } else if (key == ".") {
            key = "<b>dot</b> key";
          } else {
            key = "<b>" + key + "</b> key";
          };
          key = " (" + key + ")";
        } else {
          key = "";
        };
        html = html 
               + buttonrows[ibuttonrow][ib][2] 
               + " <button onclick=\"handleButtonPress(\'" + id + "\')\" class=\"button\"" + style + ">"
               + buttontext 
               + key 
               + "</button> ";
      } else {
        html = html 
               + "<button onclick=\"handleButtonPress(\'" + id + "\')\" class=\"button\"" + style + ">"
               + buttontext 
               + "</button> ";
      };
    };
  };
  
  document.getElementById("buttonrow").innerHTML = html;
  
  console.log("(re)drawn button row for ibuttonrow=" + String(ibuttonrow));
  
  // function toggleButtonRow() is also called before createGeometries:
  if (envelope.length > 0) {
    renderAll(false);
  };
}

/*  Show short or long texts between and on the buttons.
 */
function toggleLongTexts() {
  if (showingLongTexts) {
    showingLongTexts = false;
  } else {
    showingLongTexts = true;
  };
  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
}

/**
 *  Write text to the system clipboard.
 * 
 * See 
 * -- https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
 * -- https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard
 * 
 * Note that navigator.clipboard.writeText() is officially a promise:
 * 
 *   navigator.clipboard.writeText("text").then(
 *     function() {
 *       // clipboard successfully written
 *     }, 
 *     function() {
 *       // clipboard write failed
 *     }
 *   );
 *
 * But, issuing navigator.clipboard.writeText("text") simply works too.
 * 
 * See https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
 * for the deprecated alternative by
 *   document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
 *
 */
function writeToClipboard(text) {
  navigator.clipboard.writeText(text);
}

/**
 *  Copy text of shadowClipboard to the system clipboard.
 *
 */
function copyToClipboard() {
  writeToClipboard(shadowClipboard);
}

/* In the geo_example.html file are defined:

  var data = { 
    .... 
  };
  
   *  Run the program only if the html page is totally read by the browser:
   * 
  window.onload = function() { 
    init();
  };
  
*/

/*  Add all geometries to the scene. The function is called by the init() method.
 *  The geometries have been defined by the user, in global variable data[icanvas]. 
 * 
 */
function createGeometries(icanvas) {
  console.log("create geometries for canvas " + String(icanvas));
  
  // ensure the existance of certain fields in data[icanvas]:
  //
  //   (field "colors" will be checked in a special way below!)
  //
  var fields = ["camerastations", [],
                "camerastationnames", [],
                "cameratour", [],
                "facepoints", [],
                "linepoints", [],
                "faces", [],
                "lines", [],
                "texts", [],
                "facecolorscolumnlength", 1,
                "linecolorscolumnlength", 1,
                "textcolorscolumnlength", 1,
                "facecolors", [],
                "linecolors", [],
                "facecolors", [],
                "textcolors", [],
                "textproperties", [],
                "stepnames", [],
                "cameraorbitmodes", {},
                "shorttitle", "", 
                "longtitle", ""];
  var ifield;
  for (ifield = 0; ifield < fields.length; ifield += 2) {
    if (typeof data[icanvas][ fields[ifield] ] === "undefined") {
      data[icanvas][ fields[ifield] ] = fields[ifield+1];
    };
  };
  
  // create a record for some data on the webgl model:
  wgeo[icanvas] = {};
  
  // check the integrity of data
  //
  var errors = "";
  if ( (data[icanvas].camerastations.length % 10) != 0 ) {
    errors = errors + " data[" + String(icanvas) + "].camerastations does not contain 10-tuples;\n";
  } else {
    if ( (data[icanvas].camerastationnames.length > 0) && 
         (data[icanvas].camerastationnames.length != (data[icanvas].camerastations.length/10)) ) {
      errors = errors + " data[" + String(icanvas) + "].camerastationnames does not contain a number of elements which corresponds to the size of data[" + String(icanvas) + "].camerastations;\n";
    };
  };
  if ( (data[icanvas].cameratour.length % 9) != 0 ) {
    errors = errors + " data[" + String(icanvas) + "].cameratour does not contain 9-tuples;\n";
  };
  if (typeof data[icanvas].colors === "undefined") {
    if (icanvas == iCanvas0) {
      errors = errors + " data[" + String(icanvas) + "].colors does not exist;\n";
    } else {
      // if no colors were listed for a canvas other than iCanvas0, 
      // we take the color list from canvas iCanvas0:
      wgeo[icanvas].colorsfrom = iCanvas0;
    };
  } else {
    wgeo[icanvas].colorsfrom = icanvas;
    if ( (data[icanvas].colors.length % 3) != 0 ) {
      errors = errors + " data[" + String(icanvas) + "].colors does not contain triples;\n";
    };
  };
  if ( (data[icanvas].facepoints.length % 3) != 0 ) {
    errors = errors + " data[" + String(icanvas) + "].facepoints does not contain triples;\n";
  };
  if ( (data[icanvas].linepoints.length % 3) != 0 ) {
    errors = errors + " data[" + String(icanvas) + "].linepoints does not contain triples;\n";
  };
  var nfaces = data[icanvas].faces.length / 4; 
  if ( (data[icanvas].faces.length % 4) != 0 ) {
    errors = errors + " data[" + String(icanvas) + "].faces does not contain quadruples;\n";
  };
  var nlines = data[icanvas].lines.length / 2; 
  if ( (data[icanvas].lines.length % 2) != 0 ) {
    errors = errors + " data[" + String(icanvas) + "].lines does not contain pairs of items;\n";
  };
  var ntexts = data[icanvas].texts.length / 4; 
  if ( (data[icanvas].texts.length % 4) != 0 ) {
    errors = errors + " data[" + String(icanvas) + "].texts does not contain quadruples;\n";
  };
  var nsteps_faces = data[icanvas].facecolorscolumnlength; 
  var nsteps_lines = data[icanvas].linecolorscolumnlength; 
  var nsteps_texts = data[icanvas].textcolorscolumnlength; 
  if ( data[icanvas].facecolors.length != (nfaces * nsteps_faces) ) {
    errors = errors + " data[" + String(icanvas) + "].facecolors does not contain " + String(nfaces) + " * " + String(nsteps_faces) + " items;\n";
  };
  if ( data[icanvas].linecolors.length != (nlines * nsteps_lines) ) {
    errors = errors + " data[" + String(icanvas) + "].linecolors does not contain " + String(nlines) + " * " + String(nsteps_lines) + " items;\n";
  };
  if ( data[icanvas].textcolors.length != (ntexts * nsteps_texts) ) {
    errors = errors + " data[" + String(icanvas) + "].textcolors does not contain " + String(ntexts) + " * " + String(nsteps_texts) + " items;\n";
  };
  var nsteps = data[icanvas].stepnames.length; 
  if ( ! ( (nsteps_faces == 1) || (nsteps_faces == nsteps) ) ) {
    errors = errors + " data[" + String(icanvas) + "].facecolorscolumnlength should equal to one or data[icanvas].stepnames.length;\n";
  };
  if ( ! ( (nsteps_lines == 1) || (nsteps_lines == nsteps) ) ) {
    errors = errors + " data[" + String(icanvas) + "].linecolorscolumnlength should equal to one or data[icanvas].stepnames.length;\n";
  };
  if ( ! ( (nsteps_texts == 1) || (nsteps_texts == nsteps) ) ) {
    errors = errors + " data[" + String(icanvas) + "].textcolorscolumnlength should equal to one or data[icanvas].stepnames.length;\n";
  };

  // total number of timesteps per canvas:
  // (which equals to data[icanvas].stepnames.length, in fact):
  nTimestep[icanvas] = nsteps; 
  
  if (icanvas == iCanvas0) {
    // index of first timestep:
    iTimestep = 0;
  } else {
    // nsteps of each canvas not being the default (first)
    // is equal to 1 or to nTimestep[iCanvas0]:
    if ( (nsteps > 1) && (nsteps != nTimestep[iCanvas0]) ) {
      errors = errors + " the number of timesteps in data[" + String(icanvas) + "] should equal to one or to the number of timesteps in data[" + String(iCanvas0) + "];\n";
    };
  };

  if (errors.length != 0) {
    errors = "Error(s): " + errors;
    printMessage("info","message",errors);
    throw new Error(errors);
    return;
  };
  
  // create a record for information on the bounding envelope,
  // to be updated by createSurfaces(), createLineSegments()
  // and createLineGlyphs():
  envelope[icanvas] = { 
    box : new THREE.Box3(),      // bounding box
    sphere : new THREE.Sphere()  // bounding sphere (calculated from the bounding box)
  };
 
  // draw the faces obtained from 
  //    data[icanvas].faces, 
  //    data[icanvas].facecolors, and 
  //    data[icanvas].facepoints:
  //
  createSurfaces(icanvas);

  // draw the line segments obtained from 
  //    data[icanvas].lines, 
  //    data[icanvas].linecolors, and
  //    data[icanvas].linepoints:
  //
  createLineSegments(icanvas,1);
  
  // draw the texts/glyphs as line segments obtained from 
  //     data[icanvas].texts, 
  //     data[icanvas].textcolors, 
  //     data[icanvas].textproperties:
  //
  if (icanvas == 0) {
    initLineGlyphs();
  };
  createLineGlyphs(icanvas,1);
  
  // create points for picking, taken from:
  //    data[icanvas].facepoints:
  //    data[icanvas].linepoints:
  //
  if (icanvas == iCanvas0) {
    createPoints(icanvas);
  };
  
  // draw crosshair to indicate picked point:
  //
  if (icanvas == iCanvas0) {
    createCrosshair(icanvas,new THREE.Vector3(1,1,1),false,1);
  }; 

  // number of camera stations given by the user:
  //
  nStation[icanvas] = data[icanvas].camerastations.length / 10;
  
  // number of extra camera stations provided by calcExtraStations():
  nExtraStation[icanvas] = 0;
  
  // fill in empty strings in data[icanvas].camerastationnames 
  // if the latter is empty:
  //
  var i;
  if (data[icanvas].camerastationnames.length == 0) {
    for (i = 0; i < nStation[icanvas]; i++) {
      data[icanvas].camerastationnames[i] = "";
    };
  };

  // convert clippling plane definitions in data[icanvas].camerastations
  // from an array of normal and point x,y,z components
  // into an array of THREE.plane() objects:
  //
  for (i = 0; i < data[icanvas].camerastations.length; i = i + 10) {
    data[icanvas].camerastations[i+9] = convertClippingPlanes( data[icanvas].camerastations[i+9] );
  };

  // find camera positions based on the information
  // on the bounding box and bounding sphere in
  // envelope[icanvas]:
  //
  envelope[icanvas].cam = {};
  
  // the "home" position is directed towards the
  // origin of the model:
  envelope[icanvas].cam.home = {
    pcam : new THREE.Vector3(0,0,1),
    ucam : new THREE.Vector3(0,1,0),
    fcam : new THREE.Vector3(0,0,0),
  };

  if (envelope[icanvas].sphere.radius <= 0) {
    // there is probably no sensible geometry:
    envelope[icanvas].centre = new THREE.Vector3(0,0,0);
    envelope[icanvas].radius = 1.5;
    
    calcExtraStations(icanvas);
    
  } else {
    envelope[icanvas].centre = envelope[icanvas].sphere.center;
    envelope[icanvas].radius = 1.5 * envelope[icanvas].sphere.radius;
    
    calcExtraStations(icanvas);

    // home position on the positive z axis:
    envelope[icanvas].cam.home.pcam.z =
      data[icanvas].camerastations[ envelope[icanvas].cam[envelope[icanvas].default] + 2 ];
  };
  
  // put the camera at the default position, especially
  // needed if the camera will not be moved at all:
  //
  putCameraAtStation(icanvas,envelope[icanvas].default);
  
  // a light shining from far above:
  //
  var light;  
  light = new THREE.DirectionalLight();
  light.position.set(0,0,200);
  scene[icanvas].add( light );

  // do the following only for the default (first) canvas:
  //
  if (icanvas == iCanvas0) {

    // set some space step sizes:
    // FIXME:
    var prec = 0.1;
    var s0 = prec * Math.round( envelope[icanvas].radius / (prec * 10) ); 
    spaceSteps.push(s0);
    
    // show the x,y,z axes:
    //
    var axisHelper = new THREE.AxesHelper( 3 );
    scene[icanvas].add( axisHelper );

    // fill in default values in data[icanvas].cameraorbitmodes if missing:
    //
    var cameraorbitmodes = { "spiral": {"deltaazimuth": 10},
                             "swirl": {"n": 50, /* number of steps within one turn */
                                       "d": 1 /* number of turns */},
                             "cardinal" : [ // pairs of azimuth and elevation angles in degrees
                                           0,   0, 45,   0, 90,   0, 135,   0, 180,   0, 225,   0, 270,   0, 315,   0, 
                                           0,  45, 45,  45, 90,  45, 135,  45, 180,  45, 225,  45, 270,  45, 315,  45,
                                           0,   0, 45,   0, 90,   0, 135,   0, 180,   0, 225,   0, 270,   0, 315,   0, 
                                           0, -45, 45, -45, 90, -45, 135, -45, 180, -45, 225, -45, 270, -45, 315, -45],
                             "across" : [ /* tuples of azimuth angle in degrees, elevation angle 
                                             in degrees and radius in relative length units.
                                             avoid radius == 0, because this is focus */
                                          0, 0, 1,   0, 0, 0.5,   0, 0, 0.1,  180, 0, 0.1,  180, 0, 0.5,   180, 0, 1,
                                          90, 0, 1,   90, 0, 0.5,   90, 0, 0.1,  270, 0, 0.1,  270, 0, 0.5,   270, 0, 1]
                         };
    data[icanvas].cameraorbitmodes = fillindefaultfields(data[icanvas].cameraorbitmodes,
                                                         "data[icanvas].cameraorbitmodes",
                                                         cameraorbitmodes);
  
    // change the title if data[icanvas].shorttitle is given:
    if (data[icanvas].shorttitle.length > 0) {
      document.title = document.title + ": " + data[icanvas].shorttitle;
    };

    // show the long title if data[icanvas].longtitle is given:
    if (data[icanvas].longtitle.length > 0) {
      document.getElementById("longtitle").innerHTML = "<span class=\"contrast\">" 
                                                       + data[icanvas].longtitle
                                                       + "</span>";
    };

    // put the timestepping buttons in place:
    //
    if (nsteps > 1) {
      button.avail.toggleTimestepping = true;
      button.avail.toggleTimesteppingSpeed = true;
      button.avail.showNextTimestep = true;
      button.avail.showFirstTimestep = true;
    };
  
    // put the tour button in place:
    //
    if (data[icanvas].cameratour.length > 0) {
      button.avail.toggleTour = true;
    };

    // if a camera tour is given, it will 
    // constitute one of the orbit modes:
    //
    if (data[icanvas].cameratour.length > 9) {
      orbitModes.push("tour");
    };
  
    // put the station button in place
    //
    if (data[icanvas].camerastations.length > 0) {
      button.avail.toggleStation = true;
    };    
    
    // if more than one camera stations are given, 
    // they will constitute one of the orbit modes:
    //
    if (data[icanvas].camerastations.length > 10) {
      orbitModes.push("station");
    };

  };

}

/**  
 *   Detect change of window size and page aspect, and store
 *   the results in global variable page.
 * 
 */
function detectWindowChange() {
  // the actual (total) height of the canvasses:
  var vwindow = window.innerHeight;

  // the actual (total) width of the canvasses:
  var hwindow = window.innerWidth;

  // get the actual space which the info <div> with
  // buttons etc. takes on screen:
  var info = document.getElementById("info");
  var infobox = info.getBoundingClientRect();
  // infobox = e.g. { x: 0, y: 0, width: 1673, height: 106, 
  //                  top: 0, right: 1673, bottom: 106, left: 0 }
  var vinfo = infobox.height;
  
  if ( (page.vinfo != vinfo) || (page.vwindow != vwindow) || (page.hwindow != hwindow) ) {
    console.log("detected change of window size or page aspect");
    if (page.vinfo != vinfo) {
      console.log("   vinfo = " + String(page.vinfo) + " --> " + String(vinfo));
    };
    if (page.vwindow != vwindow) {
      console.log("   vwindow = " + String(page.vwindow) + " --> " + String(vwindow));
    };
    if (page.hwindow != hwindow) {
      console.log("   hwindow = " + String(page.hwindow) + " --> " + String(hwindow));
    };
    page.changed = true;
    page.vinfo = vinfo;
    page.vwindow = vwindow;
    page.hwindow = hwindow;
  } else {
    console.log("no change of window size or page aspect");
  };
}

/**  
 *   Create extra camera stations (top, front, left etc. views),
 *   based on the information on the model envelope. The function is
 *   called when the window size or page aspect has been changed.
 * 
 *   At least one (default) position must be created!
 * 
 */
function calcExtraStations(icanvas) {
  console.log("(re)calculation of extra stations for canvas " + String(icanvas));
  
  // information on window size and page aspect is stored
  // in global variable page:
  //
  // -- the vertical space of the info <div> element:
  //      we assume that 
  //      -- only this size of the info <div> element
  //         is relevant,
  //      -- it is over the upper part of the (two) canvasses
  //         (the first canvas is at the left side of the other):
  var vinfo = page.vinfo;
  //
  // -- the actual height of the canvasses:
  var vwindow = page.vwindow;

  // camera frustum vertical field of view, 
  // from bottom to top of view, in degrees:
  var fov = camera[icanvas].fov; 
  // if we multiply with a factor < 1, the model
  // appears within the vertical vof:
  var tanv = 0.9 * Math.tan( (fov/2) * Math.PI / 180 );
  // the horizontal vof depends on the 
  // camera frustum aspect ratio, usually 
  // the canvas width / canvas height:
  var tanh = camera[icanvas].aspect * tanv;
    
  // vertical fraction of the canvas taken by the info
  // <div> element, with some maximum value < 0.50:
  var finfo = Math.min(vinfo/vwindow, 0.40);

  // centre of the model will be the focus:
  var centre = envelope[icanvas].sphere.center;
  
  // bounding box and its size:
  var box = envelope[icanvas].box;
  var size = new THREE.Vector3();
  box.getSize(size);
  
  // -- the upper half of the canvas is taken by the info
  //    <div> element and some "upper" part of the model
  //    along the dimension seen as vertical on screen
  // -- the lower half of the canvas is taken by the 
  //    remainder part of the model along the dimension
  //    seen as vertical on screen
  //
  // if we project the canvas onto the plane which bounds
  // the model in the viewing direction, the vertical size
  // of this projection is:
  //
  //   let
  //    si = vertically projected size of the info <div> element
  //    sm = size of the model bounds seen as vertical
  //   by definition:
  //    finfo = si / (si + sm)
  //   thus
  //    si = finfo * sm / (1 - finfo)
  //   and therefore the projected total vertical canvas size is:
  //    si + sm = finfo * sm / (1 - finfo) + sm
  //            = sm * (1 + (finfo / (1 - finfo)))
  //            = sm / (1 - finfo)
  //
  var sizev = { x : size.x / (1 - finfo),
                y : size.y / (1 - finfo),
                z : size.z / (1 - finfo)
  };
  
  /*
  console.log("  vinfo = " + String(vinfo));
  console.log("  vwindow = " + String(vwindow));
  console.log("  finfo = " + String(finfo));
  console.log("  size:  x=" + size.x.toFixed(3) + " y=" + size.x.toFixed(3) + " z=" + size.x.toFixed(3));
  console.log("  sizev: x=" + sizev.x.toFixed(3) + " y=" + sizev.x.toFixed(3) + " z=" + sizev.x.toFixed(3));
  */
  // instruction list for extra camera stations:
  //
  //                    0   1        2      3      4      5     6
  //  extrastations = [ id, imethod, idimh, idimv, idima, dira, name, ... ] 
  //
  // the ordre of extra stations in this list will be maintained
  // for the extra stations to be appended in data[icanvas].camerastations !
  //
  extrastations = [
  /*
    // the space taken by the info <div> element is not taken into account:
      // far away from the centre with a positive z coordinate:
     "posz",  0, 0, 1, 2, +1, "top view (automatically included)",
      // far away from the centre with a negative y coordinate:
     "negy",  0, 0, 2, 1, -1, "front view (automatically included)",
      // far away from the centre with a negative x coordinate:
     "negx",  0, 1, 2, 0, -1, "left view (automatically included)",
      // far away from the centre with a positive y coordinate:
     "posy",  0, 0, 2, 1, +1, "back view (automatically included)",
      // far away from the centre with a positive x coordinate:
     "posx",  0, 1, 2, 0, +1, "right view (automatically included)",
      // far away from the centre with a negative z coordinate:
     "negz",  0, 0, 1, 2, -1, "bottom view (automatically included)",
  */
    // the space taken by the info <div> element __is__ taken into account:
      // far away from the centre with a positive z coordinate:
     "posz2", 1, 0, 1, 2, +1, "top view (automatically included)",
      // far away from the centre with a negative y coordinate:
     "negy2", 1, 0, 2, 1, -1, "front view (automatically included)",
      // far away from the centre with a negative x coordinate:
     "negx2", 1, 1, 2, 0, -1, "left view (automatically included)",
      // far away from the centre with a positive y coordinate:
     "posy2", 1, 0, 2, 1, +1, "back view (automatically included)",
      // far away from the centre with a positive x coordinate:
     "posx2", 1, 1, 2, 0, +1, "right view (automatically included)",
      // far away from the centre with a negative z coordinate:
     "negz2", 1, 0, 1, 2, -1, "bottom view (automatically included)"
  ];
  
  // at least one (default) camera station must be created,
  // with an id mentioned in the above list:
  envelope[icanvas].default = "posz2";
  
  // calculate the extra camera stations:
  //
  var i, ie, id, imethod, idimh, idimv, idima, dira, name, dimh, dimv
  var pcam, ucam, fcam;
  var a, b, ah;
  var dimname = [ "x", "y", "z" ];
  var unitvector = [ [1,0,0], [0,1,0], [0,0,1] ];
  
  var iextrastation = -1;
  
  for (ie = 0; ie < extrastations.length; ie += 7) {
    iextrastation += 1;
    
    id      = extrastations[ie+0];
    imethod = extrastations[ie+1];
    idimh   = extrastations[ie+2];
    idimv   = extrastations[ie+3]; 
    idima   = extrastations[ie+4];
    dira    = extrastations[ie+5];
    name    = extrastations[ie+6];
    
    dimh = dimname[idimh];
    dimv = dimname[idimv];
    dima = dimname[idima];

    // camera focus is at the model centre:
    fcam = [centre.x, centre.y, centre.z];
    
    // camera up direction along the dimension which is
    // seen as vertical on the canvas:
    ucam = unitvector[idimv];
    
    // to start with, we take the model centre as camera position
    pcam = [centre.x, centre.y, centre.z];
    
    if (imethod == 0) {
      // the space taken by the info <div> element is not taken into account:
      
      // secondly, we put the camera at the plane which bounds the 
      // model and on which the canvas is projected:
      
      pcam[idima] = pcam[idima] + dira * (size[dima]/2);

      // thirdly, we add the distance based on the field-of-view angle:
      
      pcam[idima] = pcam[idima] + dira * Math.max( size[dimv]/(2*tanv), size[dimh]/(2*tanh) );
      
    } else if (imethod == 1) {
      // the space taken by the info <div> element is taken into account:
      
      // the distance between model boundary and the camera
      // position is:
      a = sizev[dimv]/(2*tanv);
    
      // the camera position and focus must be translated
      // up along the dimension seen as vertical on screen:
      b = (a*tanv) - (size[dimv]/2);
    
      //console.log("  id = " + id + ": a = " + String(a) + ", b = " + String(b));

      // unless the model size along the dimension seen as
      // horizontal on the screen results in a larger
      // distance:
      ah = size[dimh]/(2*tanh);
      if (ah > a) {
        a = ah;
        
        // recalculate the vertical translation:
        //     v2 is the vertical size of the screen 
        //     projected onto the model boundary:
        //   v2 = a * 2 * tanv;
        //     v2info is the vertical size of the 
        //     info <div> element projected onto the 
        //     model boundary:
        //   v2info = finfo * v2;
        //     v2rem is the remaining vertical size:
        //   v2rem = v2 - vinfo;
        //     the vertical translation:
        //   b = (v2/2) - (v2rem/2) 
        //     = v2info/2
        //     = finfo * ( a * 2 * tanv );
        // for simplicity, we do not check whether the
        // model fits within the canvas after this translation.
        //
        b = finfo * ( a * 2 * tanv );

        //console.log("  id = " + id + ": a = " + String(a) + ", b = " + String(b));
      };

      // secondly, we put the camera at the plane which bounds the 
      // model and on which the canvas is projected:
      
      pcam[idima] = pcam[idima] + dira * (size[dima]/2);

      // thirdly, we add the distance based on the field-of-view angle:
      
      pcam[idima] = pcam[idima] + dira * a;
      
      // finally, we apply the vertical translations:
      
      pcam[idimv] = pcam[idimv] + b;
      
      fcam[idimv] = fcam[idimv] + b;
      
    };
    
    // write the results into data[icanvas].camerastations:
    //
    i = (nStation[icanvas] + iextrastation) * 10;

    data[icanvas].camerastations[i]   = pcam[0];
    data[icanvas].camerastations[i+1] = pcam[1];
    data[icanvas].camerastations[i+2] = pcam[2];
    data[icanvas].camerastations[i+3] = ucam[0];
    data[icanvas].camerastations[i+4] = ucam[1];
    data[icanvas].camerastations[i+5] = ucam[2];
    data[icanvas].camerastations[i+6] = fcam[0];
    data[icanvas].camerastations[i+7] = fcam[1];
    data[icanvas].camerastations[i+8] = fcam[2];
    data[icanvas].camerastations[i+9] = []; // no clipping planes

    // for direct reference to the index in data[icanvas].camerastations:
    //
    envelope[icanvas].cam[id] = i;
    
    // write into data[icanvas].camerastationnames:
    //
    i = nStation[icanvas] + iextrastation;

    data[icanvas].camerastationnames[i] = name;
  };
  
  nExtraStation[icanvas] = iextrastation + 1;
    
  /* old code:
    
    // far away from the centre with a positive z coordinate:
    envelope[icanvas].cam.posz = {
      pcam : new THREE.Vector3( centre.x, 
                                centre.y, 
                                centre.z + (size.z/2) + Math.max( size.y/(2*tanv), size.x/(2*tanh) ) ),
      ucam : new THREE.Vector3(0,1,0),
      name : "top view (automatically included)"
    };
    
    // far away from the centre with a negative y coordinate:
    envelope[icanvas].cam.negy = {
      pcam : new THREE.Vector3( centre.x, 
                                centre.y - (size.y/2) - Math.max( size.z/(2*tanv), size.x/(2*tanh) ),
                                centre.z ),
      ucam : new THREE.Vector3(0,0,1),
      name : "front view (automatically included)"
    };

    // far away from the centre with a negative x coordinate:
    envelope[icanvas].cam.negx = {
      pcam : new THREE.Vector3( centre.x - (size.x/2) - Math.max( size.z/(2*tanv), size.y/(2*tanh) ), 
                                centre.y,
                                centre.z ),
      ucam : new THREE.Vector3(0,0,1),
      name : "left view (automatically included)"
    };
    
    // far away from the centre with a positive y coordinate:
    envelope[icanvas].cam.posy = {
      pcam : new THREE.Vector3( centre.x, 
                                centre.y + (size.y/2) + Math.max( size.z/(2*tanv), size.x/(2*tanh) ),
                                centre.z ),
      ucam : new THREE.Vector3(0,0,1),
      name : "back view (automatically included)"
    };

    // far away from the centre with a positive x coordinate:
    envelope[icanvas].cam.posx = {
      pcam : new THREE.Vector3( centre.x + (size.x/2) + Math.max( size.z/(2*tanv), size.y/(2*tanh) ), 
                                centre.y,
                                centre.z ),
      ucam : new THREE.Vector3(0,0,1),
      name : "right view (automatically included)"
    };

    // far away from the centre with a negative z coordinate:
    envelope[icanvas].cam.negz = {
      pcam : new THREE.Vector3( centre.x, 
                                centre.y, 
                                centre.z - (size.z/2) - Math.max( size.y/(2*tanv), size.x/(2*tanh) ) ),
      ucam : new THREE.Vector3(0,1,0),
      name : "bottom view (automatically included)"
    };

  */
}

/**  
 *   Put the camera on the recalculated camera station, if applicable.
 * 
 */
function showExtraStation(icanvas) {
  var rendered = false;
  
  if (icanvas == iCanvas0) {
    
    // FIXME: implement for lastAction=="showNextOrbit" when an extra station is shown too
    
    if (lastAction == "showNextStation") {
      if (iStation > (nStation[icanvas] - 1)) {
        iStation -= 1; // trick to show the same station again (but with new camera position):
        showNextStation(); // rendering is included
        rendered = true;
      };
    };
    
  } else {
    putCameraAtStation(icanvas,envelope[icanvas].default);
  };

  if (! rendered) {
    render(icanvas);
  };
}

/**  
 *   Fill in missing record-fields in object o, by
 *   defaults given in (template) object d.
 *   Recursive algorithm.
 *   Output: the completed object o.
 */
function fillindefaultfields(o,o_varname,d) {
  var f1;
  if ( (typeof o === "undefined") && (typeof d !== "undefined") ) {
    // thus, there is something (a record, an array, a number or 
    // a string) in d, which is totally missing in o
    o = d;
    console.log("filling in default for " + o_varname);
  }
  else {
    // thus, there may be some of the record-fields missing
    if (typeof d === "object") {
      // thus d may be a record or an array, but not a number or string
      if (! Array.isArray(d) ) {
        // thus d is a record
        for (f1 in d) {
          o[f1] = fillindefaultfields(o[f1],o_varname + "." + f1,d[f1]);
        };
      };
    };
  };
  return o;
}

/*  Update envelope[icanvas].box and envelope[icanvas].sphere 
 *  with new bounding box (if n > 0),
 *  after printing the the input bounding box and bounding sphere.
 *
 */
function updateEnvelope(icanvas,n,prefix,boundingBox,boundingSphere) {
  // first print the input information:
  console.log(prefix + ".boundingBox on canvas " + String(icanvas) + ":"
              + " x: " + boundingBox.min.x.toFixed(4) + ".." 
                       + boundingBox.max.x.toFixed(4)
              + " y: " + boundingBox.min.y.toFixed(4) + ".." 
                       + boundingBox.max.y.toFixed(4)
              + " z: " + boundingBox.min.z.toFixed(4) + ".." 
                       + boundingBox.max.z.toFixed(4) );

  console.log(prefix + ".boundingSphere on canvas " + String(icanvas) + ":"
              + " x: " + boundingSphere.center.x.toFixed(4)
              + " y: " + boundingSphere.center.y.toFixed(4)
              + " z: " + boundingSphere.center.z.toFixed(4)
              + " radius: " + boundingSphere.radius.toFixed(4) );
  
  if (n > 0) {
    // second, update the envelope[icanvas].box 
    // with the given bounding box:
    envelope[icanvas].box.union(boundingBox);
  
    // recalculate the centre of the bounding box:
    boundingBox.getCenter(envelope[icanvas].centre);
  
    // recalculate the bounding sphere from the 
    // extreme points of the bounding box:
    var points = [boundingBox.min, 
                  boundingBox.max];
    envelope[icanvas].sphere.setFromPoints(points,envelope[icanvas].centre);
  
    console.log("updated envelope[" + String(icanvas) + "].box" 
                + " x: " + envelope[icanvas].box.min.x.toFixed(4) + ".." 
                         + envelope[icanvas].box.max.x.toFixed(4) 
                + " y: " + envelope[icanvas].box.min.y.toFixed(4) + ".." 
                         + envelope[icanvas].box.max.y.toFixed(4)
                + " z: " + envelope[icanvas].box.min.z.toFixed(4) + ".." 
                         + envelope[icanvas].box.max.z.toFixed(4) );

    console.log("updated envelope[" + String(icanvas) + "].sphere" 
                + " x: " + envelope[icanvas].sphere.center.x.toFixed(4) 
                + " y: " + envelope[icanvas].sphere.center.y.toFixed(4)
                + " z: " + envelope[icanvas].sphere.center.z.toFixed(4)
                + " radius: " + envelope[icanvas].sphere.radius.toFixed(4) );
  };
}

/*  This function is called within createWorld().
 *
 */
function createSurfaces(icanvas) {
  var i;
  
  var faceGeometry = new THREE.Geometry();

  // put the x,y,z coordinates of each point in faceGeometry.vertices:
  // (note that data[icanvas].facepoints is an 1-dimensional array of reals, and 
  // each consecutive three reals represents one point.)
  //
  for (i = 0; i < data[icanvas].facepoints.length; i = i + 3) {
    faceGeometry.vertices.push( new THREE.Vector3( data[icanvas].facepoints[i],
                                                   data[icanvas].facepoints[i+1],
                                                   data[icanvas].facepoints[i+2] ) );
  }; 

  // put the indices to the points of one face in faceGeometry.faces:
  // (note that data[icanvas].faces is an 1-dimensional array of integers, and 
  // each consecutive four integers refers to the three or four points 
  // of a triangle or quadrangle.)
  //
  var j = -1; // face index in faceGeometry.faces
  var iface = -1; // face index from data[icanvas].faces
  var ifaces = []; // each element corresponds to the same row in faceGeometry.faces, 
                   // and consists of iface, i.e. the face index from data[icanvas].faces

  for (i = 0; i < data[icanvas].faces.length; i = i + 4 ) {
    iface++;

    if ( data[icanvas].faces[i+3] == -1 ) {
      // the face is a triangle
      j++;
      /*
      console.log("at data[" + String(icanvas) + "].faces[" + String(i) + "]: face " + String(iface) 
                  + " is a triangle, and is represented at faceGeometry.faces[" + String(j) + "]");
      */
      faceGeometry.faces.push( new THREE.Face3( data[icanvas].faces[i], 
                                                data[icanvas].faces[i+1], 
                                                data[icanvas].faces[i+2] ) );
      ifaces.push( iface );
 
    } else {
     // the face is a quadrangle
      j++; j++;
      /*
      console.log("at data[" + String(icanvas) + "].faces[" + String(i) + "]: face " + String(iface) 
                  + " is a quadrangle, and is represented at faceGeometry.faces[" + String(j-1) 
                  + "] and .faces[" + String(j) + "]");
      */
      faceGeometry.faces.push( new THREE.Face3( data[icanvas].faces[i],   
                                                data[icanvas].faces[i+1], 
                                                data[icanvas].faces[i+2] ) );
      faceGeometry.faces.push( new THREE.Face3( data[icanvas].faces[i+2], 
                                                data[icanvas].faces[i+3], 
                                                data[icanvas].faces[i]   ) );
      ifaces.push( iface );
      ifaces.push( iface );
    };
  };

  var nface = iface + 1;
  console.log("total number of faces in data[" + String(icanvas) + "].faces: " + String(nface));
  
  if (icanvas == iCanvas0) {
    nFace = nface;
  };

  faceGeometry.computeFaceNormals();
  faceGeometry.computeVertexNormals();

  faceGeometry.computeBoundingBox();
  faceGeometry.computeBoundingSphere();

  updateEnvelope(icanvas,nface,"faceGeometry",
                 faceGeometry.boundingBox,faceGeometry.boundingSphere);

  // colors are defined in data[icanvascolor].colors,
  // with icanvascolor indicated by wgeo[icanvas].colorsfrom:
  var icanvascolor = wgeo[icanvas].colorsfrom;
  
  var ncolor = data[icanvascolor].colors.length/3;
  
  // define each material (color, opacity, side visibility) from data[icanvascolor].colors 
  // (note that data[icanvascolor].colors is an 1-dimensional array of reals with values
  // from 1 to 0, and each consecutive three reals refers to red, green and blue.) 
  //
  // but first, we define two special colors to be used in showNextFace():
  //

  // very white, not so transparent and double-sided:
  faceMaterial_mark = new THREE.MeshBasicMaterial( 
      { 
        color: new THREE.Color( 1,1,1 ), 
        transparent : true,
        opacity : 0.75, 
        side: THREE.DoubleSide
      }
  ); 

  // invisible color, almost transparent and double-sided !!
  faceMaterial_invis = new THREE.MeshBasicMaterial( 
      { 
        color: new THREE.Color( 0,0,0 ),
        transparent : true,
        opacity : 0.0, 
        side: THREE.DoubleSide,
        visible: false
      } 
  ); 

  nspecialcolor = 2;

  // the two special colors and the given colors in data[icanvascolor].facecolors
  // form a complete set for a defined combination of transparency and side visibility.
  // this set has to be repeated in the list faceMaterial, because we want different
  // transparencies and side visibilities:
  //
  var faceMaterial = [];
  
  // for now, just two combinations:
  var opacities    = [0.5,             1.0,];
  var visibilities = [THREE.FrontSide, THREE.DoubleSide];
  
  var icombi;
  
  for (icombi = 0; icombi < opacities.length; icombi += 1) {
    
    // note that in data[icanvascolor].facecolors an invisible material is 
    // referred to with "-1", so this material should be found at 
    //    faceMaterial[ -1 + nspecialcolor + ncoloroffset ].
    // with
    //    ncoloroffset = icombi * (ncolor + nspecialcolor),
    // or
    //    ncoloroffset = (0 + ! transparentFaces) * (ncolor + nspecialcolor),
  
    faceMaterial.push( faceMaterial_mark );
    faceMaterial.push( faceMaterial_invis );
    
    for (i = 0; i < data[icanvascolor].colors.length; i = i + 3) {
      faceMaterial.push( 
        new THREE.MeshBasicMaterial( 
          {
            color: new THREE.Color( data[icanvascolor].colors[i],
                                    data[icanvascolor].colors[i+1],
                                    data[icanvascolor].colors[i+2] ), 
            transparent : true, 
            opacity : opacities[icombi], 
            side: visibilities[icombi] // side is THREE.FrontSide or THREE.DoubleSide
          }
        ) 
      );
    };
  };

  var nstepface = data[icanvas].facecolorscolumnlength; // total number of timesteps.

  // define a color for each face in faceGeometry.faces. To start with, we
  // take the index given in the first column of data[icanvas].facecolors. 
  // (note that data[icanvas].facecolors is an 1-dimensional array of integers, and 
  // each consecutive nstep integers refers to the nstep consecutive colors
  // which a face will have at each of the nstep timesteps.) 
  //
  for (i = 0; i < faceGeometry.faces.length; i++) {
    faceGeometry.faces[i].materialIndex = data[icanvas].facecolors[ (ifaces[i] * nstepface) ] 
                                          + nspecialcolor;
    /*
    console.log("faceGeometry.faces[" + String(i) + "].materialIndex = " 
                + String(faceGeometry.faces[i].materialIndex - nspecialcolor) + " + " 
                + String(nspecialcolor) + " for iface = " + String(ifaces[i]));
    */
  };

  // store some data on the three.js model:
  wgeo[icanvas].iface = ifaces; // in this global (!) variable, each element 
                                // corresponds to the same row in faceGeometry.faces, 
                                // and consists of iface == face index from data[icanvas].faces
  
  // combine the geometry and materials in a mesh called wgeo[icanvas].faces, 
  // and add it to the scene:
  //
  wgeo[icanvas].faces = new THREE.Mesh( faceGeometry, faceMaterial );
  scene[icanvas].add( wgeo[icanvas].faces );
  
}

/*  Create the actual visible line segments. The function is called in createWorld() for
 *  the first time with firstcall==1. The function is called subsequently in showNextTimestepAnimated()
 *  with firstcall==0.
 */
function createLineSegments(icanvas,firstcall) {

  // examples at https://github.com/mrdoob/three.js/blob/master/src/helpers/AxisHelper.js
  //             https://github.com/mrdoob/three.js/blob/master/examples/webgl_buffergeometry_lines_indexed.html

  // put the x,y,z coordinates and r,g,b colors of each pair of 
  // points which form a line segment in linesegmentPoints and
  // linesegmentColors, respectively.
  // (note that data[icanvas].lines is an 1-dimensional array of integers, and 
  // each consecutive two integers represents one line segment.)
  //
  var linesegmentPoints = [];
  var linesegmentColors = [];

  var icanvascolor = wgeo[icanvas].colorsfrom; // colors are defined in data[icanvascolor].colors
  
  var nstepline = data[icanvas].linecolorscolumnlength; // total number of timesteps.

  var i;
  var c; 
  var ic;  // index in data[icanvas].colors     for r-value of the color of a line segment
  var ip0; // index in data[icanvas].linepoints for x-value of the begin point of a line segment
  var ip1; // index in data[icanvas].linepoints for x-value of the end point of a line segment
  var iLine = -1; // line index from data[icanvas].lines 

  for (i = 0; i < data[icanvas].lines.length; i = i + 2) {
    iLine++;
    c  = data[icanvas].linecolors[ iTimestep + (iLine * nstepline) ];

    if (c != -1) {
      // so the line is visible:

      ip0 = 3 * data[icanvas].lines[i];
      ip1 = 3 * data[icanvas].lines[i+1];

      linesegmentPoints.push( data[icanvas].linepoints[ip0], 
                              data[icanvas].linepoints[ip0+1], 
                              data[icanvas].linepoints[ip0+2],
                              data[icanvas].linepoints[ip1], 
                              data[icanvas].linepoints[ip1+1], 
                              data[icanvas].linepoints[ip1+2] );
      if (c > -1) {
        // so the line has a color defined in data[icanvascolor].colors:
        ic = 3 * c;

        linesegmentColors.push( data[icanvascolor].colors[ic], 
                                data[icanvascolor].colors[ic+1], 
                                data[icanvascolor].colors[ic+2], 
                                data[icanvascolor].colors[ic], 
                                data[icanvascolor].colors[ic+1], 
                                data[icanvascolor].colors[ic+2] );
      } else {
        // so c == -2 and thus the line has the special color white:
        
        linesegmentColors.push( 1, 
                                1, 
                                1, 
                                1, 
                                1, 
                                1 );
      };
    };
    //if (firstcall == 1) {
    //  console.log("at data[icanvas].lines[" + String(i) + "]: line " + String(iLine) + " with color " + String(c));
    //};
  }; 

  if (firstcall == 1) {
    var nline = iLine + 1;
    console.log("total number of lines in data[" + String(icanvas) + "].lines: " + String(nline));

    // I can get the definition of line segments only well with THREE.BufferGeometry():
    //
    var linesegmentsGeo = new THREE.BufferGeometry();
    linesegmentsGeo.setAttribute( 'position', new THREE.Float32BufferAttribute( linesegmentPoints, 3 ) );
    linesegmentsGeo.setAttribute( 'color', new THREE.Float32BufferAttribute( linesegmentColors, 3 ) );

    // calculate the bounding sizes:
    //
    linesegmentsGeo.computeFaceNormals();
    linesegmentsGeo.computeVertexNormals();

    linesegmentsGeo.computeBoundingBox();
    linesegmentsGeo.computeBoundingSphere();

    updateEnvelope(icanvas,nline,"linesegmentsGeo",
                   linesegmentsGeo.boundingBox,linesegmentsGeo.boundingSphere);
    
    var linesegmentsMat = new THREE.LineBasicMaterial( { vertexColors: THREE.VertexColors } );

    // combine the geometry and materials of the line segments, and add it to the scene:
    //
    wgeo[icanvas].lines = new THREE.LineSegments( linesegmentsGeo, linesegmentsMat );
    wgeo[icanvas].lines.geometry.dynamic = true;
    scene[icanvas].add( wgeo[icanvas].lines );
  } else {
    // only update points and colors:
    //
    wgeo[icanvas].lines.geometry.setAttribute( 
      'position', new THREE.Float32BufferAttribute( linesegmentPoints, 3 ) 
    );
    wgeo[icanvas].lines.geometry.setAttribute( 
      'color', new THREE.Float32BufferAttribute( linesegmentColors, 3 ) 
    );
    //wgeo[icanvas].lines.geometry.verticesNeedUpdate = true;
    //wgeo[icanvas].lines.geometry.normalsNeedUpdate = true;
    wgeo[icanvas].lines.geometry.colorsNeedUpdate = true;
    wgeo[icanvas].lines.geometry.computeBoundingBox();
    wgeo[icanvas].lines.geometry.computeBoundingSphere();
  };

}

/*  Create points for picking them.
 *  -- points taken from data[icanvas].facepoints;
 *  -- points taken from data[icanvas].linepoints.
 */
function createPoints(icanvas) {
  var pointGeometry;
  var pointMat;
  
  var pointgroups = ["facepoints","linepoints"];
  var pointgroup;
  var ig;
  
  for (ig = 0; ig < pointgroups.length; ig++) {
    // the field name in wgeo[icanvas] and data[icanvas] are the same:
    pointgroup = pointgroups[ig];

    pointGeometry = new THREE.BufferGeometry();
    pointGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( data[icanvas][pointgroup], 3 ) );

    // clearly visible small white rectangles with size 0.1 in Chrome:
    //pointMat = new THREE.PointsMaterial( { size: 0 } );
    
    // very small white rectangles with size 0 in Chrome:
    pointMat = new THREE.PointsMaterial( { size: 0 } );

    wgeo[icanvas][pointgroup] = new THREE.Points( pointGeometry, pointMat );
    scene[icanvas].add( wgeo[icanvas][pointgroup] );
  };

}

/*  Create or update crosshair.
 */
function createCrosshair(icanvas,p,v,firstcall) {
  // p is THREE.Vector3() for centre point of the crosshair.
  // v is visibility (true or false)
  
  // distance from centre point:
  var r = 1;
  
  // projected distance along axes for oblique lines:
  var d = Math.sqrt(r*r/2);
    
  var linesegmentPoints = [];
  var linesegmentColors = [];

  if (v == true) {
    // thus, the crosshair is visible:
    
    // two points per line segment:
    linesegmentPoints = [
      // along the axes:
      p.x-r, p.y,   p.z,    p.x+r, p.y,   p.z,
      p.x,   p.y-r, p.z,    p.x,   p.y+r, p.z,
      p.x,   p.y,   p.z-r,  p.x,   p.y,   p.z+r,
      // oblique:
      p.x-d, p.y-d, p.z-d,  p.x+d, p.y+d, p.z+d,
      p.x-d, p.y+d, p.z-d,  p.x+d, p.y-d, p.z+d,
      p.x-d, p.y-d, p.z+d,  p.x+d, p.y+d, p.z-d,
      p.x-d, p.y+d, p.z+d,  p.x+d, p.y-d, p.z-d
    ];
  
    // two colours per line segment:
    linesegmentColors = [
      // along the axes: red, green, blue
      1, 0, 0,  1, 0, 0,
      0, 1, 0,  0, 1, 0,
      0, 0, 0,  0, 0, 1,
      // oblique: white
      1, 1, 1,  1, 1, 1,
      1, 1, 1,  1, 1, 1,
      1, 1, 1,  1, 1, 1,
      1, 1, 1,  1, 1, 1
    ];
  };
  
  if (firstcall == 1) {
    // create new points and colors:
    //
    var linesegmentsGeo = new THREE.BufferGeometry();
    linesegmentsGeo.setAttribute( 'position', new THREE.Float32BufferAttribute( linesegmentPoints, 3 ) );
    linesegmentsGeo.setAttribute( 'color', new THREE.Float32BufferAttribute( linesegmentColors, 3 ) );

    // without the following two commands, the lines disappear too easily from sight at zooming etc.:
    linesegmentsGeo.computeFaceNormals();
    linesegmentsGeo.computeVertexNormals();

    var linesegmentsMat = new THREE.LineBasicMaterial( { linewidth: 5, vertexColors: THREE.VertexColors } );

    wgeo[icanvas].crosshair = new THREE.LineSegments( linesegmentsGeo, linesegmentsMat );
    wgeo[icanvas].crosshair.geometry.dynamic = true;
    scene[icanvas].add( wgeo[icanvas].crosshair );
  } else {
    // only update points and colors:
    //
    wgeo[icanvas].crosshair.geometry.setAttribute( 
      'position', new THREE.Float32BufferAttribute( linesegmentPoints, 3 ) 
    );
    wgeo[icanvas].crosshair.geometry.setAttribute( 
      'color', new THREE.Float32BufferAttribute( linesegmentColors, 3 ) 
    );
    // in the manual https://threejs.org/docs/#manual/en/introduction/How-to-update-things
    // this is suggested (a):
    //   wgeo[icanvas].crosshair.geometry.attributes.position.needsUpdate = true;
    // but the following two commands, found in the three.js code, seem to work 
    // equally well, and are similar to the .colorsNeedUpdate command which I already
    // used (b):
    //wgeo[icanvas].crosshair.geometry.verticesNeedUpdate = true;
    //wgeo[icanvas].crosshair.geometry.normalsNeedUpdate = true;
    wgeo[icanvas].crosshair.geometry.colorsNeedUpdate = true;
    // in the manual https://threejs.org/docs/#manual/en/introduction/How-to-update-things
    // these two commands are suggested (c):
    wgeo[icanvas].crosshair.geometry.computeBoundingBox();
    wgeo[icanvas].crosshair.geometry.computeBoundingSphere();
    // but it seems that either 
    //   .verticesNeedUpdate and .normalsNeedUpdate commands, or 
    //   .computeBoundingBox and .computeBoundingSphere commands
    // have the same effect, i.e. that they prevent that lines 
    // disappear too easily from sight at zooming etc.
    // _but_ after introduction of the visibility of the crosshair
    // it seems that only the second group of commands prevents
    // easy disappearances. Therefore, solution c is preferred 
    // in the end.
  };
  
}

/*  Create the actual visible texts. The function is called in createWorld() for
 *  the first time with firstcall==1. The function is called subsequently in showNextTimestepAnimated()
 *  with firstcall==0.
 */
function createLineGlyphs(icanvas,firstcall) {
  var i;

  // first, normalise the vectors v and w in data[icanvas].textproperties:
  //
  if (firstcall == 1) {
    var v, w;
    var lv, lw;

    for (i = 0; i < data[icanvas].textproperties.length; i += 1) {
      v = data[icanvas].textproperties[i].v;
      w = data[icanvas].textproperties[i].w;

      lv = ( v[0]*v[0] + v[1]*v[1] + v[2]*v[2] )**(0.5);
      lw = ( w[0]*w[0] + w[1]*w[1] + w[2]*w[2] )**(0.5);

      data[icanvas].textproperties[i].v = [ (v[0]/lv), (v[1]/lv), (v[2]/lv) ]; 
      data[icanvas].textproperties[i].w = [ (w[0]/lw), (w[1]/lw), (w[2]/lw) ]; 
    };
  };

  // put the x,y,z coordinates and r,g,b colors of each pair of 
  // points which form a line segment in linesegmentPoints and
  // linesegmentColors, respectively.
  // (note that data[icanvas].lines is an 1-dimensional array of integers, and 
  // each consecutive two integers represents one line segment.)
  //
  var lineglyphPoints = [];
  var lineglyphColors = [];

  var nstepline = data[icanvas].textcolorscolumnlength; // total number of timesteps.

  var j;
  var c; // index to object in data[icanvas].textproperties
  var cc;
  var lines; // object with line information 
  var iText = -1; // text index from data[icanvas].texts 

  for (i = 0; i < data[icanvas].texts.length; i = i + 4) {
    iText++;
    c  = data[icanvas].textcolors[ iTimestep + (iText * nstepline) ];

    if (firstcall == 1) {
      console.log("at data[" + String(icanvas) + "].texts[" + String(i) + "]: text " 
                  + String(iText) + " with color " + String(c) + " and contents \"" 
                  + data[icanvas].texts[i+3] + "\".");
    };

    if (c > -1) {
      // so the text is visible:
      lines = lineglyphFromText( "text " + String(iText),
                                 data[icanvas].texts[i],
                                 data[icanvas].texts[i+1],
                                 data[icanvas].texts[i+2],
                                 data[icanvas].texts[i+3],
                                 data[icanvas].textproperties[c] );
      
      cc = data[icanvas].textproperties[c].color;

      for (j = 0; j < lines.x0.length; j = j + 1) {
        lineglyphPoints.push( lines.x0[j], lines.y0[j], lines.z0[j],
                              lines.x1[j], lines.y1[j], lines.z1[j] );
        lineglyphColors.push( cc[0], cc[1], cc[2], 
                              cc[0], cc[1], cc[2] );
      };
    };
  }; 

  if (firstcall == 1) {
    var nline = lineglyphPoints.length/2;
    console.log("total number of glyph line segments due to data[" + String(icanvas) + "].texts: " + String(nline));
    
    // I can get the definition of line segments only well with THREE.BufferGeometry():
    //
    var lineglyphsGeo = new THREE.BufferGeometry();
    lineglyphsGeo.setAttribute( 'position', new THREE.Float32BufferAttribute( lineglyphPoints, 3 ) );
    lineglyphsGeo.setAttribute( 'color', new THREE.Float32BufferAttribute( lineglyphColors, 3 ) );

    // calculate the bounding sizes:
    //
    lineglyphsGeo.computeFaceNormals();
    lineglyphsGeo.computeVertexNormals();

    lineglyphsGeo.computeBoundingBox();
    lineglyphsGeo.computeBoundingSphere();
    
    updateEnvelope(icanvas,nline,"lineglyphsGeo",
                   lineglyphsGeo.boundingBox,lineglyphsGeo.boundingSphere);
    
    var lineglyphsMat = new THREE.LineBasicMaterial( { vertexColors: THREE.VertexColors } );

    // combine the geometry and materials of the line segments, and add it to the scene:
    //
    wgeo[icanvas].texts = new THREE.LineSegments( lineglyphsGeo, lineglyphsMat );
    wgeo[icanvas].texts.geometry.dynamic = true;
    scene[icanvas].add( wgeo[icanvas].texts );
  } else {
    // only update points and colors:
    //
    wgeo[icanvas].texts.geometry.setAttribute( 
      'position', new THREE.Float32BufferAttribute( lineglyphPoints, 3 ) 
    );
    wgeo[icanvas].texts.geometry.setAttribute( 
      'color', new THREE.Float32BufferAttribute( lineglyphColors, 3 ) 
    );
    //wgeo[icanvas].texts.geometry.verticesNeedUpdate = true;
    //wgeo[icanvas].texts.geometry.normalsNeedUpdate = true;
    wgeo[icanvas].texts.geometry.colorsNeedUpdate = true;
    wgeo[icanvas].texts.geometry.computeBoundingBox();
    wgeo[icanvas].texts.geometry.computeBoundingSphere();
    
  };

}

/*  Global variable for the data by which the shapes of lineglyphs are described. The data in the
 *  variable is completed by function initLineGlyphs().
 *
 *  In typography, a glyph is "the specific shape, design, or representation of a character"
 *  (https://en.wikipedia.org/wiki/Glyph, 2017-07-26).
 *
 *  All the values (of x, y, dxjoin, xmax etc.) __within the same font_ are in an arbitrary unit.
 *  They will be automatically normalised with the height of the lower-case letter x 
 *  by funtion initLineGlyph().
 *
 *  The variable is organised like this: 
 *     lineglyphshapes.<font>.<charactername>.x
 *     lineglyphshapes.<font>.<charactername>.y
 *     lineglyphshapes.<font>.<charactername>.xmax       (if necessary)
 *     lineglyphshapes.<font>.<charactername>.ymincen    (if necessary, minimum of y at centre)
 *     lineglyphshapes.<font>.<charactername>.ymaxcen    (if necessary, maximum of y at centre)
 *     lineglyphshapes.<font>.<charactername>.diacritic  (if necessary, 0=no or neither, 1=below baseline, 1=above baseline)
 *     lineglyphshapes.<font>.dxjoin                     (interletter space)
 *     lineglyphshapes.<font>.dyjoin                     (interline space, without the descender depth)
 *     lineglyphshapes.<font>.dylinedown                 (descender depth; always >= 0)
 *     lineglyphshapes.<font>.dybelowdia                 (vertical space between two diacritics when stacked below each other; always >= 0)
 *     lineglyphshapes.<font>.dyabovedia                 (vertical space between two diacritics when stacked above each other; always >= 0)
 *
 *  There must be a font called "a". It is the default.
 *
 *  The .x and .y fields contain arrays of coordinates. An emtpy element in the array indicates the
 *  start of a new stroke (the pencil is lifted from the paper).
 *
 *  The x,y coordinates (0,0) always constitute the same position of a character shape: 
 *  the left side on the baseline.
 *
 *  The value of lineglyphshapes.<font>.<charactername>.xmax must be given if it not equal to the
 *  maximal value of the array lineglyphshapes.<font>.<charactername>.x. 
 *
 *  The fields lineglyphshapes.<font>.<charactername>.ymincen, lineglyphshapes.<font>.<charactername>.ymaxcen
 *  and lineglyphshapes.<font>.<charactername>.diacritic must be given, if they can not be computed by the
 *  simple evaluation of the values in the .x and .y fields. (if necessary, 0=no or neither, 1=below baseline, 1=above baseline)
 *
 *  Each character name has the prefix "a" (see the prefix as the undefined article). But diacritics
 *  have the prefix "A" (derived from the command ^A). There are three characters whose shapes 
 *  must be defined: lower-case x, upper-case X and "aunknown".
 *
 *  There are some optional fields, which we present here with their default values:
 *     lineglyphshapes.<font>.scale = [1, 0.7]; // array of scales for super/subscript, with a length greater than zero; second and following elements refer to first and following super/subscript levels
 *     lineglyphshapes.<font>.shiftup = 0.7; // shift upwards for superscript, relative to xheight
 *     lineglyphshapes.<font>.shiftdown = -0.7; // shift for subscript, relative to xheight; note the negative value
 *     lineglyphshapes.<font>.slant = 0.2; // slant proportion: after slanting, a vertical line through x,y (0,0) and (0,xheight) wil go through (0,0) and (0+slant*xheight,xheight) 
 *
 */
var lineglyphshapes = {
  a : {
    dxjoin : 1,
    dyjoin : 1,
    dylinedown : 2,
    dybelowdia : 1,
    dyabovedia : 1,
    aunknown : {
            x: [0, 6,  6,  0, 0, , 3, 3, , 3, 3, 5, 5, 4, 2, 1], 
            y: [0, 0, 10, 10, 0, , 1, 2, , 3, 5, 7, 8, 9, 9, 8],
          },
   'ahere<-' : {
            x: [1, 0, 3, , 0, 3, 6], 
            y: [3, 0, 1, , 0, 3, 3],
          },
   'ahere->' : {
            x: [5, 6, 3, , 0, 3, 6], 
            y: [3, 0, 1, , 3, 3, 0],
          },
   'a<-' : {
            x: [2, 0, 2, , 0, 5], 
            y: [6, 3, 0, , 3, 3],
          },
   'a->' : {
            x: [3, 5, 3, , 0, 5], 
            y: [6, 3, 0, , 3, 3],
          },
   'atimes' :  { x: [1, 3, , 1, 3],
                 y: [2, 4, , 4, 2],
               },
   'acircum' : {
            x: [ 0,  4, 8, 8,  4,  0, -4, -4,  0], 
            y: [-2, -2, 2, 6, 10, 10,  6,  2, -2],
            xmax: 4,
          },
    a1 :  { x: [0, 2, 2],
            y: [7, 8, 0],
          },
    a2 :  { x: [0, 1, 3, 4, 0, 4],
            y: [7, 8, 8, 7, 0, 0],
          },
    a3 :  { x: [0, 1, 3, 4, 4, 3, 4, 4, 3, 1, 0], 
            y: [7, 8, 8, 7, 6, 5, 4, 1, 0, 0, 1],
          },
    a4 :  { x: [2, 0, 4, , 2, 2],
            y: [8, 4, 4, , 5, 0],
          },
    a5 :  { x: [0, 1, 3, 4, 4, 3, 0, 0, 4],
            y: [1, 0, 0, 1, 5, 6, 6, 8, 8],
          },
    a6 :  { x: [2, 0, 0, 1, 3, 4, 4, 3, 0],
            y: [8, 4, 1, 0, 0, 1, 3, 4, 4],
          },
    a7 :  { x: [0, 4, 0, , 1, 3],
            y: [8, 8, 0, , 4, 4],
          },
    a8 :  { x: [2, 0, 0, 1, 3, 4, 4, 3, 1, 0, 0, 1, 3, 4, 4, 2],
            y: [4, 3, 1, 0, 0, 1, 3, 4, 4, 5, 7, 8, 8, 7, 5, 4],
          },
    a9 :  { x: [4, 1, 0, 0, 1, 3, 4, 4, 2],
            y: [4, 4, 5, 7, 8, 8, 7, 4, 0],
          },
    a0 :  { x: [0, 1, 3, 4, 4, 3, 1, 0, 0, , 1, 3],
            y: [1, 0, 0, 1, 7, 8, 8, 7, 1, , 0, 8],
          },
   'a@' : { x: [ 3,  1, 0, 0, 1, 4, 5, 5, 2, 1, 1, 2, 5], 
            y: [-2, -2, 0, 6, 7, 7, 6, 0, 0, 1, 3, 4, 4],
          },
   'a#' : { x: [1, 1, , 3, 3, , 0, 4, , 0, 4],
            y: [0, 6, , 0, 6, , 2, 2, , 4, 4],
          },
   'a$' : { x: [0, 1, 4, 5, 5, 0, 0, 1, 4, 5, ,  2,  2, ,  3,  3],
            y: [1, 0, 0, 1, 3, 5, 7, 8, 8, 7, , -2, 10, , -2, 10], 
          },
   'a%' : { x: [0,  6, , 0, 2,  2,  0, 0, , 4, 6, 6, 4, 4],
            y: [0, 10, , 8, 8, 10, 10, 8, , 0, 0, 2, 2, 0],
          },
   'a&' : { x: [4,  3,  1, 0, 0, 1, 0, 0, 1, 4, 5, , 1, 6, , 5, 5, 6],
            y: [9, 10, 10, 9, 7, 6, 5, 1, 0, 0, 1, , 6, 6, , 7, 1, 0],
          },
   'a*' : { x: [0, 4, , 0, 4, , 0, 4, , 2, 2],
            y: [0, 6, , 6, 0, , 3, 3, , 0, 6],
          },
   'a~' : { x: [0, 1, 3, 4],
            y: [2, 4, 2, 4],
          },
   'a+' : { x: [0, 4, , 2, 2],
            y: [3, 3, , 0, 6],
          },
   'a-' : { x: [0, 4],
            y: [3, 3],
          },
   'a=' : { x: [0, 4, , 0, 4],
            y: [2, 2, , 4, 4],
          },
   'a(' : { x: [ 2,  1, 0, 0,  1,  2],
            y: [-1, -1, 1, 8, 10, 10],
          },
   'a)' : { x: [ 0,  1, 2, 2,  1,  0],
            y: [-1, -1, 1, 8, 10, 10],
          },
   'a{' : { x: [ 3,  2, 1, 1,   0, 1, 1,  2,  3],
            y: [-1, -1, 1, 4, 4.5, 5, 8, 10, 10],
          },
   'a}' : { x: [ 0,  1, 2, 2,   3, 2, 2,  1,  0],
            y: [-1, -1, 1, 4, 4.5, 5, 8, 10, 10],
          },
   'a[' : { x: [ 2,  0,  0,  2],
            y: [-1, -1, 10, 10],
          },
   'a]' : { x: [ 0,  2,  2,  0],
            y: [-1, -1, 10, 10],
          },
   'a|' : { //x: [ 0,  0],
            //y: [-2, 10],
            x: [ 0.5,  0.5],
            y: [-2,   10],
            xmax: 2,
          },
   'a<' : { x: [4, 0, 4],
            y: [6, 3, 0],
          },
   'a>' : { x: [0, 4, 0],
            y: [6, 3, 0],
          },
   'a/' : { x: [ 0,  4],
            y: [-1, 10],
          },
   'a\\': { x: [ 4,  0],
            y: [-1, 10],
          },
   'a!' : { x: [ 0, 2,  4,  0, , 2, 2],
            y: [10, 2, 10, 10, , 1, 0],
          },
   'a?' : { x: [0,  1,  4, 5, 5, 2, 2, , 2, 2], 
            y: [9, 10, 10, 9, 7, 4, 2, , 1, 0],
          },
   'a.' : { //x: [0,  0],
            //y: [2,  0],
            x: [1,  1],
            y: [2,  0],
            xmax: 3,
          },
   'a:' : { //x: [0, 0, , 0, 0],
            //y: [2, 1, , 5, 4],
            x: [1, 1, , 1, 1],
            y: [2, 1, , 5, 4],
            xmax: 3,
          },
   'a,' : { //x: [0, 0, -1],
            //y: [1, 0, -1],
            //xmax: 0,
            x: [1, 1,  0],
            y: [1, 0, -1],
            xmax: 3,
          },
   'a;' : { //x: [0, 0, -1, , 0, 0],
            //y: [1, 0, -1, , 5, 4],
            //xmax: 0,
            x: [1, 1,  0, , 1, 1],
            y: [1, 0, -1, , 5, 4],
            xmax: 3,
          },
   'a\'': { //x: [0,  0, -1],
            //y: [10, 9,  8],
            //xmax: 0,
            x: [1,  1,  0],
            y: [10, 9,  8],
            xmax: 3,
          },
   'a`' : { //x: [0,  0, 1],
            //y: [10, 9, 8],
            //xmax: 0,
            x: [1,  1, 0],
            y: [10, 9, 8],
            xmax: 3,
          },
   'a"' : { //x: [0,  0, ,  1, 1],
            //y: [10, 8, , 10, 8],
            x: [1,  1, ,  2, 2],
            y: [10, 8, , 10, 8],
            xmax: 3,
          },
   'a ' : { x: [],
            y: [],
            xmax: 4, // xmax must be imposed for a space
          },
   'a_' : { x: [0, 4],
            y: [0, 0],
          },
   'a^' : { x: [0, 1.5, 3],
            y: [7, 9.0, 7],
          },
    aA :  { x: [0,  3, 6, , 2, 4],
            y: [0, 10, 0, , 6, 6],
          },
    aa :  { x: [0, 1, 3, 4, 4, 1, 0, 0, 1, 4], 
            y: [5, 6, 6, 5, 0, 0, 1, 3, 4, 4],
          },
    aB :  { x: [ 0, 0, 4, 5, 5, 4, 0, , 3, 4, 4,  3, 0],
            y: [10, 0, 0, 1, 5, 6, 6, , 6, 7, 9, 10, 10],
          },
    ab :  { x: [ 0, 0, 3, 4, 4, 3, 0],
            y: [10, 0, 0, 1, 5, 6, 6],
            ymaxcen: 6,
          },
    aC :  { x: [5, 4, 1, 0, 0,  1,  4, 5, ,  5, 5],
            y: [1, 0, 0, 1, 9, 10, 10, 9, , 10, 8],
          },
    ac :  { x: [4, 3, 1, 0, 0, 1, 3, 4],
            y: [1, 0, 0, 1, 5, 6, 6, 5],
          },
    aD :  { x: [0, 4, 5, 5,  3,  0, 0],
            y: [0, 0, 1, 8, 10, 10, 0],
          },
    ad :  { x: [ 4, 4, 1, 0, 0, 1, 4],
            y: [10, 0, 0, 1, 5, 6, 6],
            ymaxcen: 6,
          },
    aE :  { x: [5, 0,  0,  5, , 0, 5],
            y: [0, 0, 10, 10, , 6, 6],
          },
    ae :  { x: [4, 3, 1, 0, 0, 1, 3, 4, 4, 0],
            y: [1, 0, 0, 1, 5, 6, 6, 5, 4, 4],
          },
    aF :  { x: [0,  0,  5, , 0, 5],
            y: [0, 10, 10, , 6, 6],
          },
    af :  { x: [1, 1,  2, , 0, 2],
            y: [0, 9, 10, , 6, 6],
          },
    aG :  { x: [3, 5, 5, 4, 1, 0, 0,  1,  4, 5],
            y: [5, 5, 1, 0, 0, 1, 9, 10, 10, 9],
          },
    ag :  { x: [ 0,  1,  3,  4, 4, 1, 0, 0, 1, 4],
            y: [-1, -2, -2, -1, 6, 6, 5, 1, 0, 0],
          },
    aH :  { x: [0,  0, , 5,  5, , 0, 5],
            y: [0, 10, , 0, 10, , 6, 6],
          },
    ah :  { x: [0,  0, , 0, 3, 4, 4],
            y: [0, 10, , 6, 6, 5, 0],
            ymaxcen: 6,
          },
    aI :  { x: [0, 4, ,  0,  4, , 2,  2],
            y: [0, 0, , 10, 10, , 0, 10],
          },
    ai :  { x: [0, 1, 1, ,   1,   1],
            y: [6, 6, 0, , 7.5, 8.5],
          },
    adotlessi :  { x: [0, 1, 1],
                   y: [6, 6, 0],
          },
    aJ :  { x: [0, 0, 1, 4, 5, 5,   3],
            y: [3, 1, 0, 0, 1, 10, 10],
          },
    aj :  { x: [0, 1,  1,  0, ,   1,   1],
            y: [6, 6, -1, -2, , 7.5, 8.5],
          },
    adotlessj :  { x: [0, 1,  1,  0],
                   y: [6, 6, -1, -2],
          },
    aK :  { x: [0,  0, ,  5, 0, 5], 
            y: [0, 10, , 10, 6, 0],
          },
    ak :  { x: [0,  0, ,  4, 0, 4], 
            y: [0, 10, ,  6, 4, 0],
            ymaxcen: 6,
          },
    aL :  { x: [ 0, 0, 5, 5],
            y: [10, 0, 0, 2],
          },
    al :  { x: [ 0, 0, 1],
            y: [10, 1, 0],
          },
    aM :  { x: [0,  0, 3,  6, 6],
            y: [0, 10, 6, 10, 0],
          },
    am :  { x: [0, 0, 2, 3, , 3, 3, 5, 6, 6],
            y: [0, 6, 6, 5, , 0, 6, 6, 5, 0],
          },
    aN :  { x: [0,  0, 5,  5],
            y: [0, 10, 0, 10],
          },
    an :  { x: [0, 0, 3, 4, 4],
            y: [0, 6, 6, 5, 0],
          },
    aO :  { x: [0, 1, 4, 5, 5,  4,  1, 0, 0],
            y: [1, 0, 0, 1, 9, 10, 10, 9, 1],
          },
    ao :  { x: [0, 1, 3, 4, 4, 3, 1, 0, 0],
            y: [1, 0, 0, 1, 5, 6, 6, 5, 1],
          },
    aP :  { x: [0,  0,  4, 5, 5, 4, 0],
            y: [0, 10, 10, 9, 7, 6, 6],
          },
    ap :  { x: [ 0, 0, 3, 4, 4, 3, 0],
            y: [-2, 6, 6, 5, 1, 0, 0],
            ymincen: 0,
          },
    aQ :  { x: [0, 1, 4, 5, 5,  4,  1, 0, 0, , 4, 6],
            y: [1, 0, 0, 1, 9, 10, 10, 9, 1, , 2, 0],
          },
    aq :  { x: [ 4, 4, 1, 0, 0, 1, 4],
            y: [-2, 6, 6, 5, 1, 0, 0],
            ymincen: 0,
          },
    aR :  { x: [0,  0,  4, 5, 5, 4, 0, , 2, 5],
            y: [0, 10, 10, 9, 7, 6, 6, , 6, 0],
          },
    ar :  { x: [0, 0, 3, 4],
            y: [0, 6, 6, 5],
          },
    aS :  { x: [0, 1, 4, 5, 5, 0, 0,  1,  4, 5],
            y: [1, 0, 0, 1, 3, 7, 9, 10, 10, 9],
          },
    as :  { x: [0, 1, 3, 4, 4, 0, 0, 1, 3, 4],
            y: [1, 0, 0, 1, 2, 4, 5, 6, 6, 5],
          },
    aT :  { x: [0,  0,  6, 6, , 3,  3],
            y: [8, 10, 10, 8, , 0, 10],
          },
    at :  { x: [1, 1, 2, , 0, 2],
            y: [7, 1, 0, , 6, 6],
          },
    aU :  { x: [ 0, 0, 1, 4, 5,  5],
            y: [10, 1, 0, 0, 1, 10],
          },
    au :  { x: [0, 0, 1, 4, 4],
            y: [6, 1, 0, 0, 6],
          },
    aV :  { x: [ 0, 3,  6],
            y: [10, 0, 10],
          },
    av :  { x: [0, 2, 4],
            y: [6, 0, 6],
          },
    aW :  { x: [ 0, 2, 3, 4,  6],
            y: [10, 0, 5, 0, 10],
          },
    aw :  { x: [0, 2, 4, , 2, 4, 6],
            y: [6, 0, 6, , 6, 0, 6],
          },
    aX :  { x: [0,  5, ,  0, 5],
            y: [0, 10, , 10, 0],
          },
    ax :  { x: [0, 4, , 0, 4],
            y: [0, 6, , 6, 0],
          },
    aY :  { x: [ 0, 3,  6, , 3, 3],
            y: [10, 6, 10, , 6, 0],
          },
    ay :  { x: [0, 0, 1, 4, , 4,  4,  3],
            y: [6, 1, 0, 0, , 6, -1, -2],
            ymincen: 0,
          },
    aZ :  { x: [ 0,  6, 0, 6],
            y: [10, 10, 0, 0],
          },
    az :  { x: [0, 4, 0, 4],
            y: [6, 6, 0, 0],
          },
   'Aa' : { x: [0, 3],
            y: [7, 9],
          }, // diacritic "acute"
   'Ag' : { x: [0, 3],
            y: [9, 7],
          }, // diacritic "grave"
   'Ae' : { x: [0, 1.5, 3],
            y: [7, 9.0, 7],
          }, // diacritic "circumflex"
   'AU' : { x: [0.0, 0.0, , 1.5, 1.5],
            y: [7.5, 8.5, , 7.5, 8.5],
          }, // diacritic "diaeresis/umlaut"
   'At' : { x: [0, 1, 2, 3],
            y: [8, 7, 9, 8],
          }, // diacritic "tilde"
   'Al' : { x: [0, 3],
            y: [8, 8],
          }, // diacritic "macron"
   'Ap' : { x: [ 0,  0],
            y: [-2, -1],
          }, // diacritic "dot above"
   'Au' : { x: [0, 1, 2, 3],
            y: [9, 8, 8, 9],
          }, // diacritic "breve"
   'Av' : { x: [0, 1.5, 3],
            y: [9, 7.0, 9],
          }, // diacritic "caron"
   'AH' : { x: [0, 1.5, , 1.5, 3],
            y: [7, 9.0, , 7.0, 9],
          }, // diacritic "double acute"
   'Ac' : { x: [0,  0, -1],
            y: [0, -1, -2],
            xmax: 0,
          }, // diacritic "cedilla"
   'Ad' : { x: [ 0,  0],
            y: [-1, -2],
          }, // diacritic "dot below" 
   'Ab' : { x: [ 0,  3],
            y: [-1, -1],
          }, // diacritic "line below"
   'Ar' : { x: [0.0, 1.0, 1.0, 0.0, 0.0],
            y: [7.5, 7.5, 8.5, 8.5, 7.5],
          }, // diacritic "ring above"
   'Ax' : { x: [0, 1],
            y: [1, 9],
            diacritic: 0,
          }, // diacritic "stroke" which in fact must have many forms
   'Ak' : { x: [0,  0,  1],
            y: [0, -1, -2],
            xmax: 0,
          }, // diacritic "ogonek"
  },
};

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max
//function maxOfArray(numArray) {
//  return Math.max.apply(null, numArray);
//}
//
// adapted from an example with function GetMaxDISCRETE(A) in https://stackoverflow.com/questions/1669190/find-the-min-max-element-of-an-array-in-javascript
/*  
 *  Find the maximum value of the numbers in array A.
 *
 */
function maximum(A) {
  var m = A[0];
  var l = A.length;
  var i;
  var v;
  for (i = 1; i < l; i++) {
    v = A[i];
    if (typeof v === "number") {
      if ( m < v ) {
        m = v;
      };
    };
  };
  return m;
}

/*  
 *  Find the minimum value of the numbers in array A.
 *
 */
function minimum(A) {
  var m = A[0];
  var l = A.length;
  var i;
  var v;
  for (i = 1; i < l; i++) {
    v = A[i];
    if (typeof v === "number") {
      if ( m > v ) {
        m = v;
      };
    };
  };
  return m;
}

/*  
 *  Multiply the numbers in array A with scalar b.
 *
 */
function multiply(A,b) {
  var m = A;
  var l = m.length;
  var i;
  var v;
  for (i = 0; i < l; i++) {
    v = m[i];
    if (typeof v === "number") {
      m[i] = v * b;
    };
  };
  return m;
}

/*  
 *  Return the value of A, if A is defined; or return the value of B.
 *
 */
function ifdefined(A,B) {
  if (typeof A !== "undefined") {
    return A;
  }
  else {
    return B;
  };
};


/*  
 *  Complete and finish the data in lineglyphshapes and data[icanvas].textproperties. The function must be called 
 *  before the first call to function createLineGlyphs().
 *
 */
function initLineGlyphs() {

  // normalise the glyph data in lineglyphshapes
  //

  var f; // font name
  var c; // char name
  var u; // x-height

  for (f in lineglyphshapes) {
    // the height of the lower-case letter x is taken as unit length by which all shapes are normalised:
    u = maximum(lineglyphshapes[f]["ax"]["y"]);

    for (c in lineglyphshapes[f]) {
      if ((c.substr(0,1) == "a") || (c.substr(0,1) == "A")) {
        // normalise the shapes by x/u and y/u, and determine max(x):
        lineglyphshapes[f][c]["x"] = multiply(lineglyphshapes[f][c]["x"],(1/u));
        lineglyphshapes[f][c]["y"] = multiply(lineglyphshapes[f][c]["y"],(1/u));

        if (typeof lineglyphshapes[f][c]["xmax"] === "undefined") {
          lineglyphshapes[f][c]["xmax"] = maximum(lineglyphshapes[f][c]["x"]);
        } else {
          // e.g. for space field xmax must be given, if there are no lines:
          lineglyphshapes[f][c]["xmax"] = lineglyphshapes[f][c]["xmax"] / u;
        };

        if (typeof lineglyphshapes[f][c]["ymaxcen"] === "undefined") {
          lineglyphshapes[f][c]["ymaxcen"] = maximum(lineglyphshapes[f][c]["y"]);
        } else {
          // e.g. for letters like h, d and b, the field ymaxcen should be given:
          lineglyphshapes[f][c]["ymaxcen"] = lineglyphshapes[f][c]["ymaxcen"] / u;
        };

        if (typeof lineglyphshapes[f][c]["ymincen"] === "undefined") {
          lineglyphshapes[f][c]["ymincen"] = minimum(lineglyphshapes[f][c]["y"]);
        } else {
          // e.g. for letters like p and q, the field ymincen should be given:
          lineglyphshapes[f][c]["ymincen"] = lineglyphshapes[f][c]["ymincen"] / u;
        };
      };
      if (c.substr(0,1) == "A") {
        // determine the the diacritic type, if it has not been given:
        if (typeof lineglyphshapes[f][c]["diacritic"] === "undefined") {
          if ((lineglyphshapes[f][c]["ymincen"] < 0) || (lineglyphshapes[f][c]["ymaxcen"] < 0)) {
            // diacritic is below the baseline
            lineglyphshapes[f][c]["diacritic"] = 1;
          };
          if ((lineglyphshapes[f][c]["ymincen"] > 0) || (lineglyphshapes[f][c]["ymaxcen"] > 0)) {
            // diacritic is above the baseline
            lineglyphshapes[f][c]["diacritic"] = 2;
          };
        };
      };
      if (c.substr(0,1) == "a") {
        // for normal characters, the diacritic type equals always to zero:
        lineglyphshapes[f][c]["diacritic"] = 0;
      };
    };

    // the (normalised) height of the upper-case letter X is line distance:
    lineglyphshapes[f]["ymax"] = maximum(lineglyphshapes[f]["aX"]["y"]);

    // the down height of letters below the baseline is always given, and must be normalised to:
    lineglyphshapes[f]["dylinedown"] = lineglyphshapes[f]["dylinedown"] / u;

    // normalise these too:
    lineglyphshapes[f]["dxjoin"] = lineglyphshapes[f]["dxjoin"] / u;
    lineglyphshapes[f]["dyjoin"] = lineglyphshapes[f]["dyjoin"] / u;
    lineglyphshapes[f]["dybelowdia"] = lineglyphshapes[f]["dybelowdia"] / u;
    lineglyphshapes[f]["dyabovedia"] = lineglyphshapes[f]["dyabovedia"] / u;
  }; 

}

/*  
 *  Function to generate coordinates of line segments, which constitute characters of the input text.
 *
 *  It is called like this:
 *  lines = lineglyphFromText(reference,data[icanvas].texts[i],data[icanvas].texts[i+1],data[icanvas].texts[i+2],data[icanvas].texts[i+3],data[icanvas].textproperties[c]);
 *
 *  with the meaning:
 *  lines = lineglyphFromText(<string: text for reference in console.log>,
 *                            <real: x coordinate of anchor>,
 *                            <real: y coordinate of anchor>,
 *                            <real: z coordinate of anchor>,
 *                            <string: input text>,
 *                            <object from data[icanvas].textproperties>);
 *
 *  The last input argument is an element from the array which is data[icanvas].textproperties. It is organised as following:
 *  {
 *    // the following fields must be given:
 *      v: [1, 0, 0], // 3-d direction of the baseline of the text; by function createLineGlyphs() it is ensured that the vector is a unit vector with a length equal to one absolute length unit
 *      w: [0, 1, 0], // 3-d direction of the vertical of the text; by function createLineGlyphs() it is ensured that the vector is a unit vector with a length equal to one absolute length unit
 *      xheight: 1, // height of lower-case letter x in absolute length units
 *      color: [1, 1, 1], // r,g,b channels (value from 0 to 1); they are not used in lineglyphFromText() but in createLineGlyphs()
 *    // the following fields are optional, and the default values are presented:
 *      font: "a", // the font name; font "a" is the default 
 *      xwidthoverxheight: 1, // ratio of width to height of lower-case letter x
 *      includedylinedown: 1, // whether (1) or not (0) lineglyphshapes[font]["dylinedown"] is added to the line skip
 *      lineskipconstant: 0, // whether (1) or not (0) the line skip is kept constant, of it depends on the possible extra vertical space due to diacritics, superscipts and subscripts
 *      slant: 0.2, // slant proportion: after slanting, a vertical line through x,y (0,0) and (0,xheight) wil go through (0,0) and (0+slant*xheight,xheight) 
 *      anchorvertical: "top", // position of the text block anchor along its vertical; possible values are: top, halfxheightabovefirstbaseline, firstbaseline, centre, lastbaseline, bottom
 *      anchorhorizontal: "left", // position of the text block anchor along its horizontal (i.e. baselines); possible values are: left, centre, right
 *      alignment: "centre", // horizontal alignment of lines within the text block; possible values are: left, centre, right
 *  }
 *
 *  The output variable lines is an object, consisting of 6 arrays. See the code below.
 *
 */
function lineglyphFromText(reference,xa,ya,za,t,p) {

  // output variables (x,y,z coordinates of the beginning and end of line segments which form the characters):
  var x0 = [];
  var y0 = [];
  var z0 = [];
  var x1 = [];
  var y1 = [];
  var z1 = [];

  // these variables are always given in p (i.e. data[icanvas].textproperties[i]):
  //
  var v = p["v"]; // 3-d direction of the baseline of the text; must be a unit vector with a length equal to one absolute length unit
  var w = p["w"]; // 3-d direction of the vertical of the text; must be a unit vector with a length equal to one absolute length unit
  var xheight = p["xheight"]; // height of lower-case letter x in absolute length units

  // these variables are optional in p (i.e. data[icanvas].textproperties[i]):
  //
  var font = ifdefined( p["font"], "a" ); // the font name; font "a" is the default 
  var wh = ifdefined( p["xwidthoverxheight"], 1 ); // ratio of width to height of lower-case letter x
  var includedylinedown = ifdefined( p["includedylinedown"], 1 ); // whether (1) or not (0) lineglyphshapes[font]["dylinedown"] is added to the line skip
  var lineskipconstant = ifdefined( p["lineskipconstant"], 0 ); // whether (1) or not (0) the line skip is kept constant, of it depends on the possible extra vertical space due to diacritics, superscipts and subscripts
  var slant = ifdefined( p["slant"], 0.2 ); // slant proportion: after slanting, a vertical line through x,y (0,0) and (0,xheight) wil go through (0,0) and (0+slant*xheight,xheight) 
  var anchorver = ifdefined( p["anchorvertical"], "firstbaseline" ); // position of the text block anchor along its vertical; possible values are: top, halfxheightabovefirstbaseline, firstbaseline, centre, lastbaseline, bottom 
  var anchorhor = ifdefined( p["anchorhorizontal"], "left" ); // position of the text block anchor along its horizontal (i.e. baselines); possible values are: left, centre, right
  var alignment = ifdefined( p["alignment"], "left" ); // horizontal alignment of lines within the text block; possible values are: left, centre, right

  // these variables are optional in lineglyphshapes[font]:
  //
  var ascale = ifdefined( lineglyphshapes[font]["scale"], [1, 0.7] ); // array of scales for super/subscript, with a length greater than zero; second and following elements refer to first and following super/subscript levels
  var shiftup = ifdefined( lineglyphshapes[font]["shiftup"], 0.7 ); // shift upwards for superscript, relative to xheight
  var shiftdown = ifdefined( lineglyphshapes[font]["shiftdown"], -0.7 ); // shift for subscript, relative to xheight; note the negative value

  var iscale = 0;
  var scale = ascale[iscale]; // the actual scale

  var slantx = 0; // the actual slant (in x direction) 

  // these values depend on the font:
  //
  var dxjoin = scale * lineglyphshapes[font]["dxjoin"] * wh * xheight;
  var dyjoin = scale * lineglyphshapes[font]["dyjoin"] * xheight;
  var dylineup = scale * lineglyphshapes[font]["ymax"] * xheight;
  var dylinedown = scale * includedylinedown * lineglyphshapes[font]["dylinedown"] * xheight;

  var shape;
  var dx, dy;

  var char = {}; // an object with information of a character in the text line, with the fields: font, charname, shift, scale, slant and advance
  var ic = -1; // index in chars
  var chars = []; // array of char objects
  var textline = { // an object with information on a text line, with the fields: dxline, dylineup, dylinedown, xstart, ystart and chars
                   dxline : 0,
                   dylineup : dylineup,
                   dylinedown : dylinedown,
                   xstart : 0,
                   ystart : 0,
                   chars : chars,
  };
  var textlines = [textline]; // array of textline objects
  var il = 0; // index in textlines 

  // first iteration: filling the textlines object:
  //
  var it = 0; // index in string t
  var c, c2;  // the character which is read from t
  var charname = ''; // the character name used in lineglyphshapes[font][charname]
  var iscommand = false; // will be true if a valid command ^X has been found
  var ischarname = false; // will be true if the character read in t is between ^C and first appearing ^E
  var acommand = []; // array (or stack) of issued ^U/^D/^S commands
  var adyshift = [0]; // array (or stack) of vertical shift displacements in absolute length units, due to issued ^U/^D commands
  var command;
  var dyshift = adyshift[0]; // the actual vertical shift displacement in absolute length units
  var aover = []; // array (or stack) of character indices in textlines[il]["chars"] for characters to overwrite
  var isnext1 = false; // will be true if ^O or ^A was just issued
  var isnext2 = false; // wlll be true if the character after ^O or ^A was issued 
  var isdiacritic = false; // will be true if ^A was just issued; it relates to prefix in charname
  var diacritic = 0; // the type of diacritic: 0 (default), 1 (below), or 2 (above)
  var ymincen, ymaxcen; // the minimum/maximum of y values of the shape of the main character
  var ymincend, ymaxcend; // the minimum/maximum of y values of the shape of the diacritic
  var dybelowdia, dyabovedia; // the vertical space between main character and diacritic
  var shiftx = 0; // corrective shift along x-direction for a overlaying character
  var shifty = 0; // corrective shift along y-direction for a overlaying character

  //console.log(reference + " scale =" + String(scale) + ", includedylinedown=" + String(includedylinedown) + " and dylinedown=" + String(dylinedown) + ".");

  while (it < t.length) {
    c = t.substr(it,1);
    iscommand = false;

    if ((it < (t.length - 1)) && (c == "^")) {
      // found the sequence "^X" to indicate a command
      c2 = t.substr(it+1,1); 
      if (c2 == "C") {
        // start a character name
        iscommand = true;
        ischarname = true;

        //console.log(reference + " got command ^C at it=" + String(it) + ".");
      }
      else {
        if (c2 == "N") {
          // start a new line
          iscommand = true;

          // first, substract the last interletter space at the "old" line:
          textlines[il]["dxline"] -= dxjoin;

          // second, substract the actual dylineup to ystart at the "old" line, if it is the second or subsequent line:
          if (il > 0) {
            textlines[il]["ystart"] -= textlines[il]["dylineup"];
          };

          // second, append a new textline:
          //         while calculating xstart asif alignment == "left" and anchorhor == "left", and ystart asif anchorver == "firstbaseline"
          //
          textlines.push( { dxline : 0,
                            dylineup : dylineup,
                            dylinedown : dylinedown,
                            xstart : 0,
                            ystart : ( textlines[il]["ystart"] - textlines[il]["dylinedown"] - dyjoin), // note that the actual dylineup of the new line will be substracted when the line is totally read
                            chars : [],
                          } );
          il += 1;
          ic = -1; 

          //console.log(reference + " got command ^N at it=" + String(it) + ".");
        } 
        else {
          if (c2 == "U") {
            // begin of superscript field
            iscommand = true;

            dyshift = dyshift + scale*shiftup*xheight;

            acommand.push( "U" );
            adyshift.push( dyshift );

            iscale += 1;
            if (iscale > (ascale.length - 1)) {
              iscale = ascale.length - 1;
            };
            scale = ascale[iscale]; 

            //console.log(reference + " got command ^U at it=" + String(it) + ".");
          } 
          else {
            if (c2 == "D") {
              // begin of subscript field
              iscommand = true;

              dyshift = dyshift + scale*shiftdown*xheight;
              acommand.push( "D" );
              adyshift.push( dyshift );

              iscale += 1;
              if (iscale > (ascale.length - 1)) {
                iscale = ascale.length - 1;
              };
              scale = ascale[iscale]; 

              //console.log(reference + " got command ^D at it=" + String(it) + ".");
            } 
            else {
              if (c2 == "S") {
                // begin of slant field
                iscommand = true;

                acommand.push( "S" );
                slantx = slant;

                //console.log(reference + " got command ^S at it=" + String(it) + ".");
              } 
              else {
                if (c2 == "E") {
                  // end of a field
                  iscommand = true;
                  
                  if (ischarname) {
                    // thus, the end of a ^C field
                    ischarname = false;

                    // a tricky trick: charname must now be printed, therefore:
                    iscommand = false; // to circumvent the condition "if (iscommand)" below
                    it += 1; // so that c2 must not be read again
                  }
                  else {
                    // thus, the end of a ^U/^D/^S field
                    command = acommand.pop();

                    if ((command == "U") || (command == "D")) {
                      adyshift.pop();
                      dyshift = adyshift[adyshift.length - 1];
                      iscale -= 1;
                      if (iscale < 0) {
                        iscale = 0;
                      };
                      scale = ascale[iscale];
                    };

                    if (command == "S") {
                      slantx = 0;
                    };
                  };

                  //console.log(reference + " got command ^E at it=" + String(it) + ".");
                } 
                else {
                  if (c2 == "O") {
                    // the next character must be overwriten over the subsequent character
                    iscommand = true;

                    isnext1 = true;
                    isnext2 = false;

                    //console.log(reference + " got command ^O at it=" + String(it) + ".");
                  }
                  else {
                    if (c2 == "A") {
                      // the next character is a diacritic and must be overwriten over the subsequent character
                      iscommand = true;

                      isdiacritic = true;
                      isnext1 = true;
                      isnext2 = false;

                      //console.log(reference + " got command ^A at it=" + String(it) + ".");
                    } 
                    else {
                      console.log(reference + " found unknown command \"^" + c2 + "\" in text \"" + t + ".");
                    }; // if (c2 == "A")
                  }; // if (c2 == "O")
                }; // if (c2 == "E")
              }; // if (c2 == "S")
            }; // if (c2 == "D")
          }; // if (c2 == "U")
        }; // if (c2 == "N")
      }; // if (c2 == "C")
    };

    if (iscommand) {
      // thus, c2 was interpreted and must not be read again, therefore:
      it += 1;
      // and note that just before the end of the while-loop, variable it is advanced again!
    }
    else {
      // thus, c is not (part of) a command

      if (ischarname) {
        // thus, c is between ^C and ^E

        charname = charname + c;
      }
      else {
        // thus, c is not between ^C and ^E

        if (charname.length == 0) {
          if (isdiacritic) {
            charname = "A" + c;
            isdiacritic = false;
          }
          else {
            charname = "a" + c;
          };
        }
        else {
          c = charname; // a trick for the console.log below
        };
        shape = lineglyphshapes[font][charname];
        if (typeof shape === "undefined") {
          console.log(reference + ": no glyph exists for character \"" + c + "\" in text \"" + t + "\".");
          charname = "aunknown";
          shape = lineglyphshapes[font][charname];
        };

        // dx is the width of the character plus interletter space, in absolute length units:
        dx = scale * lineglyphshapes[font][charname]["xmax"] * wh * xheight + dxjoin;

        // char.font = font; // the font name is added for possible future use: if the shape of a character is not defined in one font, the program may find an other shape in an other font
        // char.charname = charname;
        // char.shift = [0, dyshift]; // displacement from actual character position (at left side on baseline), in x and y directions, in absolute length units
        // char.scale = scale;
        // char.slant = [slantx, 0]; 
        // char.advance = [dx, 0]; // displacement to next character position (at left side on baseline), in x and y directions, in absolute length units

        textlines[il]["chars"].push( { font : font, 
                                       charname : charname, 
                                       shift : [0, dyshift],
                                       scale : scale, 
                                       slant : [slantx, 0],
                                       advance : [dx,0]} );
        ic += 1; 
        textlines[il]["dxline"] += dx;

        // if constant line skipping is not desired, recalculate textlines[il]["dylineup"], textlines[il]["ystart"] and textlines[il]["dylinedown"]:
        //
        if (lineskipconstant == 0) {
          dy = dyshift + scale * lineglyphshapes[font]["ymax"] * xheight; // note that the actual dyshift and scale may change
          if ((dy > 0) && (textlines[il]["dylineup"] < dy)) {
            textlines[il]["dylineup"] = dy;
          };
          dy = dyshift - scale * includedylinedown * lineglyphshapes[font]["dylinedown"] * xheight; // note that the actual dyshift and scale may change
          if ((dy < 0) && (-1 * textlines[il]["dylinedown"] > dy)) {
            textlines[il]["dylinedown"] = -1 * dy; // note that the value of textlines[il]["dylinedown"] is always >= 0 
          };
        };

        //console.log(reference + " storing character \"" + charname + " with scale =" + String(scale) + " and dyshift=" + String(dyshift) + ".");
        //console.log(reference + " (1) storing character \"" + charname + "\" in textlines[" + String(il) + "][\"chars\"][" + String(ic) + "].");
        //console.log(reference + " (2) storing character \"" + textlines[il]["chars"][ic]["charname"] + "\" in textlines[" + String(il) + "][\"chars\"][" + String(ic) + "].");

        // handle the second or main character after ^O or ^A, i.e. "z" in "^Oxz" or "^Ox^Oyz" etc.:
        if (isnext2) {
          //console.log(reference + " found main character \"" + charname + "\" with own width " + (dx-dxjoin).toFixed(4) + "; with " + String(aover.length) + " overlaying characters.");

          //   note (1) that only diacritics are stacked, whereas ^O overlaying characters are just printed through the main character
          //
          //   note (2) that we start from the main character via the ..., third, second towards the first overlaying diacritic,
          //   so that the first overlaying diacritic will be closest to the main character 
          //
          // ymincen and ymaxcen are accumulative, starting from the main chacacter:
          //
          ymincen = dyshift + scale * lineglyphshapes[font][charname]["ymincen"] * xheight;
          ymaxcen = dyshift + scale * lineglyphshapes[font][charname]["ymaxcen"] * xheight;
          dyabovedia = scale * lineglyphshapes[font]["dyabovedia"] * xheight;
          dybelowdia = scale * lineglyphshapes[font]["dybelowdia"] * xheight;

          for (var i = (aover.length - 1); i > -1; i -= 1) {
            char = textlines[il]["chars"][aover[i]];

            // set the variable advance[0] to zero for the overlaying characters,
            // shift the overlaying characters to the centre of the main character,
            // and correct dxline:
            //
            textlines[il]["dxline"] -= char["advance"][0];
            shiftx = (dx - dxjoin)/2 - (char["advance"][0] - dxjoin)/2;
            //console.log(reference + " overlaying character " + String(i) + " is \"" + char.charname + "\" and has own width " + (char["advance"][0]-dxjoin).toFixed(4) + "; therefor shift=" + shiftx.toFixed(4) + ".");
            textlines[il]["chars"][aover[i]]["advance"][0] = 0;
            // why does this-> not work?  textlines[il]["chars"][aover[i]]["shift"][0] += (dx - dxjoin)/2 - (char["advance"][0] - dxjoin)/2;
            textlines[il]["chars"][aover[i]]["shift"][0] += shiftx;

            // if the overlaying character is a diacritic, shift it above or below:
            //
            shifty = textlines[il]["chars"][aover[i]]["shift"][1];
            ymincend = scale * lineglyphshapes[char.font][char.charname]["ymincen"] * xheight; // FIXME or not a problem: we assume that main and overlaying characters have the same scale and xheight 
            ymaxcend = scale * lineglyphshapes[char.font][char.charname]["ymaxcen"] * xheight;
            diacritic = lineglyphshapes[char.font][char.charname]["diacritic"];

            if (diacritic == 2) {
              if (ymincend < ymaxcen) {
                shifty = ymaxcen - ymincend + dyabovedia;
              };
              ymaxcen = ymaxcend + shifty;
            } 
            else {
              if (diacritic == 1) {
                if (ymaxcend > ymincen) {
                  shifty = ymincen - ymaxcend - dybelowdia;
                };
                ymincen = ymincend + shifty;
              } 
              else {
                // thus, diacritic == 0
                shifty = 0;
                ymaxcen = maximum([ymaxcen, ymaxcend]);
                ymincen = minimum([ymincen, ymincend]);
              };
            };
            textlines[il]["chars"][aover[i]]["shift"][1] = shifty;

            // if constant line skipping is not desired, recalculate textlines[il]["dylineup"], textlines[il]["ystart"] and textlines[il]["dylinedown"]:
            //
            if (lineskipconstant == 0) {
              if ((ymaxcen > 0) && (textlines[il]["dylineup"] < ymaxcen)) {
                textlines[il]["dylineup"] = ymaxcen;
              };
              if ((ymincen < 0) && (-1 * textlines[il]["dylinedown"] > ymincen)) {
                textlines[il]["dylinedown"] = -1 * ymincen; // note that the value of textlines[il]["dylinedown"] is always >= 0 
              };
            };

          };
          isnext2 = false;
          aover = [];
        };

        // handle the first character after ^O or ^A, i.e. the overlaying character:
        if (isnext1) {
          // store the index of the overlaying character in textlines[il]["chars"]:
          //console.log(reference + " found overlaying character \"" + charname + "\".");
          aover.push( ic );
          isnext1 = false;
          isnext2 = true;
        };

        charname = "";

      }; // if (ischarname)
    }; // if (iscommand)

    // finally, advance variable it 
    it += 1;
  };
  
  // substract the last interletter space at the last text line:
  if (ic > -1) {
    textlines[il]["dxline"] -= dxjoin;
  };

  // substract the actual dylineup to ystart at the last text line, if there are two or more lines:
  if (il > 0) {
    textlines[il]["ystart"] -= textlines[il]["dylineup"];
  };

  // second iteration: calculation of block width and block heigth, 
  //                   and recalculate (xstart,ystart) of each line depending on the anchor position and alignment:
  //
  var dxblock = 0;
  var dyblock = 0;
  for (il = 0; il < textlines.length; il += 1) {
    dx = textlines[il]["dxline"];
    if ( dxblock < dx ) {
      dxblock = dx;
    };
    dyblock = dyblock + textlines[il]["dylineup"] + textlines[il]["dylinedown"];
    //console.log(reference + " line " + String(il) + ": " + textlines[il]["dylineup"].toFixed(4) + " plus " + textlines[il]["dylinedown"].toFixed(4) + " ==> " + dyblock.toFixed(4));
  };
  if (textlines.length > 0) {
    dyblock += (textlines.length - 1) * dyjoin;
  };
  //console.log(reference + " dyblock= " + dyblock.toFixed(4));

  var dylineupfirst = 0;
  var dylineupdownlast = 0;
  if (textlines.length > 0) {
    dylineupfirst  = textlines[0]["dylineup"]; // at first line
    dylinedownlast = textlines[textlines.length-1]["dylinedown"]; // at last line
  }
  
  var dyv = 0; // for default: anchorver == "firstbaseline"
  if (anchorver == "halfxheightabovefirstbaseline") {
    dyv = - 0.5 * xheight;
  }
  if (anchorver == "top") {
    dyv = -1 * dylineupfirst;
  }
  if ((anchorver == "centre") || (anchorver == "center")) {
    dyv = -1 * dylineupfirst + dyblock / 2;
  }
  if (anchorver == "lastbaseline") {
    dyv = dyblock - dylineupfirst - dylinedownlast;
  }
  if (anchorver == "bottom") {
    dyv = dyblock - dylineupfirst;
  }
  
  var dxh = 0; // for default: anchorhor == "left"
  if ((anchorhor == "centre") || (anchorhor == "center")) {
    dxh = dxblock / 2;
  }
  if (anchorhor == "right") {
    dxh = dxblock;
  }

  var ma = 0;  // for default: alignment == "left"
  var dxa = 0; // for default: alignment == "left"
  if ((alignment == "centre") || (alignment == "center")) {
    ma = -0.5;
    dxa = dxblock / 2;
  }
  if (alignment == "right") {
    ma = -1;
    dxa = dxblock;
  }
  
  for (il = 0; il < textlines.length; il += 1) {
    textlines[il]["xstart"] = textlines[il]["xstart"] - dxh + ma * textlines[il]["dxline"] + dxa;
    textlines[il]["ystart"] = textlines[il]["ystart"] + dyv;
  };
  
  // third iteration: typesetting and gathering all the lines from which the characters will be drawn:
  //
  var xc, yc; // character position at left side on baseline, in 2d, in absolute length units
  var x, y; // coordinates in 2d, in absolute length units
  var iline;
  for (il = 0; il < textlines.length; il += 1) {
    xc = textlines[il]["xstart"];
    yc = textlines[il]["ystart"];

    //console.log(reference + " line " + String(il) + " xstart=" + xc.toFixed(4) + " ystart=" + yc.toFixed(4));

    for (ic = 0; ic < textlines[il]["chars"].length; ic += 1) {
      char = textlines[il]["chars"][ic];
      shape = lineglyphshapes[char.font][char.charname];

      //console.log(reference + " line " + String(il) + " font \"" + char.font + "\" char " + String(ic) + " \"" + char.charname + "\" xc=" + xc.toFixed(4) + " yc=" + yc.toFixed(4));

      iline = 0;

      for (var i = 0; i < (shape.x.length - 1); i += 1) {
        if ((typeof shape.x[i] !== "undefined") && (typeof shape.x[i+1] !== "undefined")) {
          iline += 1;

          // in 2d:             ( xc + char.shift[0] + char.scale * (shape.x[i] + char.slant[0] * shape.y[i]) * wh * xheight )
          // in 3d: xa + v[0] * ( xc + char.shift[0] + char.scale * (shape.x[i] + char.slant[0] * shape.y[i]) * wh * xheight ) + w[0] * ( yc + char.shift[1] + char.scale * (shape.y[i] + char.slant[1] * shape.x[i]) * xheight )

          x = ( xc + char.shift[0] + char.scale * (shape.x[i] + char.slant[0] * shape.y[i]) * wh * xheight );
          y = ( yc + char.shift[1] + char.scale * (shape.y[i] + char.slant[1] * shape.x[i]) * xheight );

          //console.log(reference + " line " + String(il) + " linesegment " + String(iline) + " x0=" + shape.x[i].toFixed(4) + " y0=" + shape.y[i].toFixed(4) + " x=" + x.toFixed(4) + " y=" + y.toFixed(4) + " scale=" + char.scale + " dxshift=" + char.shift[0] + " dyshift=" + char.shift[1]);
          //console.log(reference + " line " + String(il) + " linesegment " + String(iline) + " x1=" + shape.x[i+1].toFixed(4) + " y1=" + shape.y[i+1].toFixed(4) );

          x0.push( (xa + v[0] * x + w[0] * y) );
          y0.push( (ya + v[1] * x + w[1] * y) );
          z0.push( (za + v[2] * x + w[2] * y) );
      
          x = ( xc + char.shift[0] + char.scale * (shape.x[i+1] + char.slant[0] * shape.y[i+1]) * wh * xheight );
          y = ( yc + char.shift[1] + char.scale * (shape.y[i+1] + char.slant[1] * shape.x[i+1]) * xheight );

          x1.push( (xa + v[0] * x + w[0] * y) );
          y1.push( (ya + v[1] * x + w[1] * y) );
          z1.push( (za + v[2] * x + w[2] * y) );
        };
      };

      xc += char.advance[0];
      yc += char.advance[1];
    };
  };

  return { x0: x0,
           y0: y0,
           z0: z0,
           x1: x1,
           y1: y1,
           z1: z1,
          };
}

/**
 *  Render (redraw) the scene in canvas icanvas. It must also be called at each 
 *  change of camera position and orientation.
 *
 */
function render(icanvas) {
  if ( (icanvas == iCanvas0) && showingStereo) {
    effect.render(scene[icanvas], camera[icanvas]);
  } else {
    renderer[icanvas].render(scene[icanvas], camera[icanvas]);
  };

  if (icanvas != iCanvas0) {
    // this message for iCanvas0 would cause too many writings on the console:
    console.log("rendered canvas " + String(icanvas));
  };
  
  printCameraPosition();
}

/**
 *  Pick a point by the mouse, depending on variable iPick.
 *
 */
function pickPoint(icanvas) {
  if (iPick > 0) {
    console.log("called pickPoint():");
      
    console.log("relative mouse coordinates on canvas " + String(icanvas) +
                ": x=" + mouse[icanvas].x.toFixed(5) + ", y=" + mouse[icanvas].y.toFixed(5));

    // update the picking ray with the camera and mouse position
    raycaster[icanvas].setFromCamera( mouse[icanvas], camera[icanvas] );
      
    // the field name in wgeo[icanvas] and data[icanvas] are the same:
    var pointgroup = ["oops","facepoints", "linepoints"][iPick];

    // calculate the intersections between the picking ray and the objects:
    var intersects = raycaster[icanvas].intersectObject( wgeo[icanvas][pointgroup] );

    // the first element is the closest intersection:
    if (Array.isArray(intersects)) {
      if (intersects.length > 0) {
        var k, ip, ie;
        var p;
        var d;

        // intersects[i] has fields:
        // -- .point: the point of intersection. It is not
        //    the point of an object which we are looking for.
        // -- .distance: the distance between the origin of the 
        //    ray and the intersection.
        //    Array intersects[] is sorted by this value!
        // -- .distanceToRay: not mentioned (2020-12-11) in 
        //    https://threejs.org/docs/#api/en/core/Raycaster
        //    This quantity seems more useful here than
        //    .distance !
        // -- .index: not mentioned (2020-12-11) in 
        //    https://threejs.org/docs/#api/en/core/Raycaster
        //    This item is the index in the list of given points.

        // index of the picked point in the list of points,
        // given by the first element in intersects[]:
        ip = intersects[0].index;
        
        // find the point closest to the intersective ray, by 
        // looking for the .distanceToRay value of the first 
        // 5 items in intersects[]:
        //
        // distance of the closest intersection:
        d = Infinity;
        for (k = 0; k < Math.min(5,intersects.length); k++) {
          console.log("intersection point " + String(k) + ": ",intersects[k]);
          if (typeof intersects[k].distanceToRay !== "undefined") {
            if (intersects[k].distanceToRay < d) {
              d = intersects[k].distanceToRay;
              ip = intersects[k].index;
            };
          };
        };

        // now, ip should refer to the picked point, closest 
        // to the intersective ray.

        // index of the first coordinate of the picked point 
        // in the list of coordinate value:
        ie = 3 * ip;
        // coordinates of the picked point:
        p = new THREE.Vector3(data[icanvas][pointgroup][ie],
                              data[icanvas][pointgroup][ie+1],
                              data[icanvas][pointgroup][ie+2]);
        
        // put the crosshair at the picked point:
        createCrosshair(icanvas,p,true,0);
        
        // write the index on screen:
        document.getElementById("face").innerHTML = 
            "<span class=\"contrast\">Picked point with <b>index " + String(ip) + " from " + pointgroup 
          + "</b>, with coordinates x=" + p.x.toFixed(4) + " y=" + p.y.toFixed(4) + " z=" + p.z.toFixed(4) 
          + ". (Note the zero-based index!)</span>";

        // write the index on the system clipboard in octave-compatible code:
        writeToClipboard(String(ip) + ", " 
          + p.x.toFixed(4) + ", " + p.y.toFixed(4) + ", " + p.z.toFixed(4) + "; "
          + "% [ index_to_" + pointgroup + ", x, y, z;]\n" );
          
      } else {
        console.log("no points picked.");
      };
    } else {
      console.log("no points picked.");
    };
  };
}

/**
 *  A function to toggle to the next pick mode.
 *
 */
function togglePickMode() {
  iPick += 1;
  if (iPick >= pickModes.length) {
    iPick = 0;
  };
  console.log("new pick mode: iPick=" + String(iPick));
  
  if (iPick == 0) {
    // switch off the visibility of the crosshair:
    createCrosshair(iCanvas0,new THREE.Vector3(1,1,1),false,0);
  };    

  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
}

/**
 *  An event listener for rendering the default canvas.
 *
 */
function render_iCanvas0() {
  render(iCanvas0);
}

/**
 *  An event listener for the keydown event. It is installed by the init() function.
 *
 */
function handleKeyStroke_old(evt) {

  var k = evt.keyCode;
  console.log("key stroke: keyCode=" + String(k) + " key=\"" + evt.key + "\"" + " code=\"" + evt.code + "\"");

  var f; // holds function to be called
  var action = true;
  switch (k) {
    case  38: f = goForward; break;        // up arrow (forward)
    case  75: f = goForward; break;        // letter k (forward)
    case  40: f = goBackward; break;       // down arrow (backward)
    case 188: f = goBackward; break;       // comma      (backward)
    case  37: f = turnLeft; break;         // left arrow (turn left)
    case  77: f = turnLeft; break;         // letter m   (turn left)
    case  39: f = turnRight; break;        // right arrow (turn right)
    case 190: f = turnRight; break;        // period/dot  (turn right)

    case 219: f = goLeftward; break;       // symbol [ (pan left)
    case 221: f = goRightward; break;      // symbol ] (pan right)
    
    case  83: f = turnUp; break;           // letter s (turn up)
    case  88: f = turnDown; break;         // letter x (turn down)
    case  90: f = turnLeftsideDown; break; // letter z (turn leftdown)
    case  67: f = turnLeftsideUp; break;   // letter c (turn leftup)

    case  36: f = originalPosition; break; // home

    case  32: f = showNextTimestep; break;     // space bar
    case  89: f = showFirstTimestep; break;    // letter y
    case  84: f = toggleTimestepping; break;   // letter t
    case  54: f = toggleTimesteppingSpeed; break; // digit 6

    case  71: f = showNextFace; break;     // letter g
    case  85: f = showAllFaces; break;     // letter u
    case  70: f = toggleFaces; break;      // letter f

    case  79: f = toggleOrbit; break;      // letter o
    case  57: f = toggleOrbitMode; break;  // digit 9
    case  48: f = toggleOrbitSpeed; break; // digit 0

    case  80: f = toggleTour; break;       // letter p

    case  13: f = toggleStation; break;    // return

    case  73: f = toggleLongTexts; break;  // letter i
    case  56: f = toggleStereo; break;     // digit 8
    case  49: f = toggleFullScreen; break; // digit 1

    case  66: f = toggleButtonRow; break;  // letter b

    case  51: f = translatex; break;  // digit 3
    case  52: f = translatey; break;  // digit 4
    case  53: f = translatez; break;  // digit 5

    case  87: f = rotatex; break;  // letter w
    case  69: f = rotatey; break;  // letter e
    case  82: f = rotatez; break;  // letter r

    default: action = false;
  }

  if (action) {
    f();
    evt.preventDefault();
  }
}

/**
 *  An event listener for the keydown event. It is installed by the init() function.
 * 
 *  To see quickly the keycodes: http://keycode.info/ .
 *  Note:
 *  -- https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
 *       ==> deprecated KeyboardEvent.keyCode
 *  -- https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
 *       ==> KeyboardEvent.code returns a string, e.g. "KeyQ" for the Q key on a QWERTY
 *           layout keyboard, but returns the _same_ string on any other keyboard layout!
 *  -- https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
 *       ==> KeyboardEvent.key returns meaning of key, depending on keyboard layout.
 *     https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event
 *  -- OrbitControls.js still uses keyCode, however. 
 *  -- it would be handy to use only the fysical key codes (returned by KeyboardEvent.code),
 *     (not their meaning by a string returned by KeyboardEvent.key), and 
 *     translate the fysical key code to their meaning only for printing onto the 
 *     web page buttons. Possible solutions: 
 *       https://github.com/kabirbaidhya/keycode-js
 *       https://github.com/nbeach/keycoder
 *     But it seems to cumbersome! ==> therefore, we use only KeyboardEvent.code in the
 *     function below !
 *  -- not that all the (names of) functions are listed in global variable window:
 *       var fn = window["functionName"];
 *       if (typeof fn === 'function') {
 *         fn();
 *       }
 *     https://stackoverflow.com/questions/359788/how-to-execute-a-javascript-function-when-i-have-its-name-as-a-string
 *     https://stackoverflow.com/questions/912596/how-to-turn-a-string-into-a-javascript-function-call
 *     https://www.sitepoint.com/call-javascript-function-string-without-using-eval
 */
function handleKeyStroke(evt) {

  var code = evt.code;
  var CODE = code.toUpperCase();

  var fun; // holds function to be called
  var action = false;
  var ir, ib, ik, c, C;
  
  searchloop: {
    for (ir = 0; ir < nbuttonrow; ir = ir + 1) {
      for (ib = 0; ib < buttonrows[ir].length; ib = ib + 1) {
        fun  = buttonrows[ir][ib][0];
        keys = buttonrows[ir][ib][1];
        for (ik = 0; ik < keys.length; ik = ik + 1) {
          c = keys[ik];
          if (c == code) {
            action = true;
            break searchloop;
          };
          C = c.toUpperCase();
          if (C == CODE) {
            action = true;
            break searchloop;
          };
          if ( ("KEY" + C) == CODE) {
            action = true;
            break searchloop;
          };
          if ( ("DIGIT" + C) == CODE) {
            //console.log("found");
            action = true;
            break searchloop;
          };
        };
      };
    };
  };
  
  if (action) {
    console.log("key stroke: keyCode=" + String(evt.keyCode) + " key=\"" + evt.key + "\"" + " code=\"" + code + "\" ===> calling " + fun + "()");
    
    var f = window[fun];
    if (typeof f === 'function') {
      lastAction = fun;
      f();
    } else {
      console.warn("no function with name \"" + fun + "\" is found!");
    };
    
    // prevent that the browser will interpret the key stroke:
    evt.preventDefault();
    
  } else {
    console.log("key stroke: keyCode=" + String(evt.keyCode) + " key=\"" + evt.key + "\"" + " code=\"" + code + "\"");
  };
}

/**
 *  An function called by the html button when pressed.
 * 
 */
function handleButtonPress(fun) {
  var f = window[fun];
  if (typeof f === 'function') {
    lastAction = fun;
    f();
  } else {
    console.warn("no function with name \"" + fun + "\" is found!");
  };
}

/**
 *  Move the camera forward, backward etc. by increments
 */
function goForward() {
  moveCamera([0,0,-1],[0,0,0]);
}
function goBackward() {
  moveCamera([0,0,+1],[0,0,0]);
}
function turnLeft() {
  moveCamera([0,0,0],[0,+1,0]);
}
function turnRight() {
  moveCamera([0,0,0],[0,-1,0]);
}
function turnUp() {
  moveCamera([0,0,0],[+1,0,0]);
}
function turnDown() {
  moveCamera([0,0,0],[-1,0,0]);
}
function turnLeftsideDown() {
  moveCamera([0,0,0],[0,0,-1]);
}
function turnLeftsideUp() {
  moveCamera([0,0,0],[0,0,+1]);
}

function goLeftward() {
  moveCamera([-1,0,0],[0,0,0]);
}
function goRightward() {
  moveCamera([+1,0,0],[0,0,0]);
}

function goUpward() {
  moveCamera([0,+1,0],[0,0,0]);
}
function goDownward() {
  moveCamera([0,-1,0],[0,0,0]);
}

/**
 *  Experimental functions to see what camera.translateX(d), 
 *  camera.rotateX(a) etc. do.
 */
function translatex() {
  moveCamera([+1,0,0],[0,0,0]);
}
function translatey() {
  moveCamera([0,+1,0],[0,0,0]);
}
function translatez() {
  moveCamera([0,0,+1],[0,0,0]);
}
function rotatex() {
  moveCamera([0,0,0],[+1,0,0]);
}
function rotatey() {
  moveCamera([0,0,0],[0,+1,0]);
}
function rotatez() {
  moveCamera([0,0,0],[0,0,+1]);
}

/**
 *  Move the camera by incremental units for position and rotation.
 * 
 *  In total, the two input arguments
 *       p = [ipx,ipy,ipz] 
 *       r = [irx,iry,irz]
 *  contain six values, of which only _one_ is non-zero, namely
 *  either +1 or -1.
 * 
 *  If ipx == +1, camera goes to right, with unchanged view direction.
 *  If ipy == +1, camera goes up, with unchanged view direction.
 *  If ipz == +1, camera goes backward, with unchanged view direction.
 * 
 *  If irx == +1, camera turns up.
 *  If iry == +1, camera turns left.
 *  If irz == +1, camera turns leftside up.
 * 
 *  ==> Thus, the (local) axes of the camera are: 
 *      positive x axis points to right,
 *      positive y axis points up,
 *      positive z axis points backwards.
 *  ==> Strangely, irz should cause a turn rightside up according to the right-hand rule !?
 */
function moveCamera(p,r) {
  // incremental units of camera position or rotation 
  // (values -1 or 0 or +1):
  var mp = new THREE.Vector3(p[0],p[1],p[2]);
  var mr = new THREE.Vector3(r[0],r[1],r[2]); 

  // translate the incremental units into real units 
  // (meters and radians, respectively):  
  mp.multiplyScalar(spaceSteps[iSpaceStep]);
  mr.multiplyScalar(5 * Math.PI / 180);
  
  // for OrbitControls we need to track the focus of the camera:
  var changedfocus = true;
  var pcam1 = new THREE.Vector3(0,0,0); // position of camera
  var dcam1 = new THREE.Vector3(0,0,0); // view direction of camera
  camera[iCanvas0].getWorldPosition(pcam1);
  camera[iCanvas0].getWorldDirection(dcam1);
  dcam1.normalize;

  if (mp.x != 0) {
    // positive value: camera goes to right, with unchanged view direction
    camera[iCanvas0].translateX(mp.x);
  };
  if (mp.y != 0) {
    // positive value: camera goes up, with unchanged view direction
    camera[iCanvas0].translateY(mp.y);
  };
  if (mp.z != 0) {
    // positive value: camera goes backward, with unchanged view direction
    camera[iCanvas0].translateZ(mp.z);
    
    // alternative solution: moving along the view direction of the camera:
    /*
    var pcam2 = new THREE.Vector3(pcam1.x,pcam1.y,pcam1.z);
    dcam1.multiplyScalar(mp.z);
    pcam2.add(dcam1);
    camera[iCanvas0].position.set(pcam2.x,pcam2.y,pcam2.z);
    */
    
    changedfocus = false;
  };
  if (mr.x != 0) {
    // positive value: camera turns up
    camera[iCanvas0].rotateX(mr.x);
  };
  if (mr.y != 0) {
    // positive value: camera turns left
    camera[iCanvas0].rotateY(mr.y);
  };
  if (mr.z != 0) {
    // positive value: camera turns leftside up
    camera[iCanvas0].rotateZ(mr.z);
  };
  
  if (changedfocus) {
    var pcam2 = new THREE.Vector3(0,0,0); // new position of camera
    var dcam2 = new THREE.Vector3(0,0,0); // new view direction of camera
    camera[iCanvas0].getWorldPosition(pcam2);
    camera[iCanvas0].getWorldDirection(dcam2);
    dcam2.normalize;

    // translation vector of camera:
    var tcam = new THREE.Vector3((pcam2.x - pcam1.x),(pcam2.y - pcam1.y),(pcam2.z - pcam1.z)); 
    if ( (tcam.x != 0) || (tcam.y != 0) || (tcam.z != 0) ) {
      console.log("camera has been translated by (" + tcam.x.toFixed(4) + "," + tcam.y.toFixed(4) + "," + tcam.z.toFixed(4) + ")");
    };

    // direction difference vector of camera:
    var rcam = new THREE.Vector3((dcam2.x - dcam1.x),(dcam2.y - dcam1.y),(dcam2.z - dcam1.z)); 
    if ( (rcam.x != 0) || (rcam.y != 0) || (rcam.z != 0) ) {
      console.log("camera has been turned");
    };
    
    // new focus of camera:
    var fcam = new THREE.Vector3(0,0,0);
    fcam.add(pcam2);
    dcam2.multiplyScalar(spaceSteps[iSpaceStep]); // FIXME should depend on model size
    fcam.add(dcam2);

    // inform OrbitControls about the new focus:
    controls[iCanvas0].target = fcam;

    // note that 
    //    controls[iCanvas0].update();
    // must not be issued here, otherwise the default up direction 
    // of the camera would be effectuated, and therefore a possible 
    // turn leftside up would be undone.
    
    // we should change camera.up() too FIXME
  };
  
  document.getElementById("move").innerHTML = "";
  
  render(iCanvas0);
}

/**
 *  Put the camera at point p with up direction u and
 *  looking at focus point f. 
 * 
 *  All three vectors are in world coordinates.
 * 
 *  Up direction u does not need to be a unit vector.
 * 
 *  Note that if the up direction is in the view 
 *  direction (i.e. f - p), the up direction of the
 *  camera is not changed and remains at the previously
 *  set up direction.
 * 
 *  There are two possible calls:
 *     putCamera(px,py,pz,ux,uy,uz,fx,fy,fz) with 6 numbers
 *     putCamera(p,u,f) with 3 THREE.Vector3's
 * 
 *  Learned from:
 *     https://stackoverflow.com/questions/27553512/how-to-get-the-camera-local-axis-in-three-js
 *     https://cs.wellesley.edu/~cs307/readings/06-camera.html
 *  Other suggestions:
 *     https://stackoverflow.com/questions/14023764/how-to-get-orientation-of-camera-in-three-js?rq=1
 *     https://stackoverflow.com/questions/15696963/three-js-set-and-read-camera-look-vector/15697227#15697227
 *     https://stackoverflow.com/questions/61592242/three-js-how-to-change-camera-up-vector?rq=1
 */
function putCamera(px,py,pz,ux,uy,uz,fx,fy,fz) {
  var pcam, ucam, fcam;
  if ( px.isVector3 ) {
    pcam = px;
    ucam = py;
    fcam = pz;
  } else {
    pcam = new THREE.Vector3(px,py,pz);
    ucam = new THREE.Vector3(ux,uy,uz);
    fcam = new THREE.Vector3(fx,fy,fz);
  };

  // issuing 
  //    camera.position = pcam;
  // does not work!
  camera[iCanvas0].position.set(pcam.x,pcam.y,pcam.z); 
  camera[iCanvas0].lookAt(fcam);
  camera[iCanvas0].up.set(ucam.x,ucam.y,ucam.z);
 
  // this sets the correct up direction of the camera (the reason is not understood):
  controls[iCanvas0].target = fcam;
  controls[iCanvas0].update();
}

/**
 *  Put the camera at a station indicated by global variable envelope.
 * 
 *  This function is only used for (extra) stations and the home station.
 *  And cannot be used for user-defined stations, because their id's are not 
 *  defined by envelope[icanvas].cam[id].
 * 
 */
function putCameraAtStation(icanvas,id) {
  var fcam = new THREE.Vector3();
  
  if (id == "home") {
    fcam.set(envelope[icanvas].cam[id].fcam.x,
             envelope[icanvas].cam[id].fcam.y,
             envelope[icanvas].cam[id].fcam.z);
    
    // issuing 
    //    camera.position = envelope[icanvas].cam[id].pcam;
    // does not work!
    camera[icanvas].position.set(envelope[icanvas].cam[id].pcam.x,
                                 envelope[icanvas].cam[id].pcam.y,
                                 envelope[icanvas].cam[id].pcam.z); 
    camera[icanvas].lookAt(fcam);
    camera[icanvas].up.set(envelope[icanvas].cam[id].ucam.x,
                           envelope[icanvas].cam[id].ucam.y,
                           envelope[icanvas].cam[id].ucam.z);

  } else {  
  
    var i = envelope[icanvas].cam[id];

    fcam.set(data[icanvas].camerastations[i+6],
             data[icanvas].camerastations[i+7],
             data[icanvas].camerastations[i+8]);

    camera[icanvas].position.set(data[icanvas].camerastations[i],
                                 data[icanvas].camerastations[i+1],
                                 data[icanvas].camerastations[i+2]); 
    camera[icanvas].lookAt(fcam);
    camera[icanvas].up.set(data[icanvas].camerastations[i+3],
                           data[icanvas].camerastations[i+4],
                           data[icanvas].camerastations[i+5]);
  };
 
  if (icanvas == iCanvas0) {
    // this sets the correct up direction of the camera (the reason is not understood):
    controls[icanvas].target = fcam;
    controls[icanvas].update();
  };
}

/**
 *  Move the camera to the "original" position.
 */
function originalPosition() {
  /* working but old code:
  var pcam = new THREE.Vector3(0,0,0);   // position of camera
  var rcam = new THREE.Vector3(0,0,0);   // rotation of camera 
  var dcam = new THREE.Vector3(0,0,0);   // view direction vector of camera
  
  pcam.set(0,0,zcaminit);
  rcam.set(0,0,0);
  camera[iCanvas0].position.set(pcam.x,pcam.y,pcam.z);	
  camera[iCanvas0].rotation.set(rcam.x,rcam.y,rcam.z);	
  */
  
  /*
  putCamera(0,0,zcaminit,
            0,1,0,
            0,0,0);
  */
  putCamera(envelope[iCanvas0].cam.home.pcam,
            envelope[iCanvas0].cam.home.ucam,
            envelope[iCanvas0].cam.home.fcam);
  
  document.getElementById("move").innerHTML = "";

  render(iCanvas0);
}

/**
 *  Toggle the space step size.
 *
 */
function toggleSpaceStep() {
  iSpaceStep += 1;
  if (iSpaceStep > (spaceSteps.length - 1)) {
    iSpaceStep = 0;
  };

  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
  
  console.log("new space step: spaceSteps[" + String(iSpaceStep) + "] = " 
              + String(spaceSteps[iSpaceStep]));
}

/**
 *  Go to first timestep. This function is called by handleKeyStroke() or a html button.
 *
 */
function showFirstTimestep() {
  if (showingTimesteps) {
    // animation is on and should be put off now
    window.clearInterval(wShowingTimesteps);
    showingTimesteps = false;

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
    
  };

  // showing individual faces and timestepping are mutually exclusive:
  document.getElementById("face").innerHTML = "";
  if (showingFaces) {
    // animation is on and should be put off now
    window.clearInterval(wShowingFaces);
    showingFaces = false;
        
    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };
  
  iTimestep = -1; // trick to show the colors of iTimestep=0
  showNextTimestepAnimated();
}

/**
 *  Go to next timestep. This function is called by handleKeyStroke() or a html button.
 *
 */
function showNextTimestep() {
  if (showingTimesteps) {
    // animation is on and should be put off now
    window.clearInterval(wShowingTimesteps);
    showingTimesteps = false;

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };
  
  // showing individual faces and timestepping are mutually exclusive:
  document.getElementById("face").innerHTML = "";
  if (showingFaces) {
    // animation is on and should be put off now
    window.clearInterval(wShowingFaces);
    showingFaces = false;
        
    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };

  showNextTimestepAnimated();
}

/**
 *  Replace one character at an index in a string.
 * 
 *  Note that b[2] = "x" does not work in Javascript.
 *
 */
function replaceAt(string, index, replace) {
  return string.substring(0, index) + replace + string.substring(index + 1);
}

/**
 *  Draw a simple progression bar by ascii characters,
 *  e.g.  "===|=======".
 *
 */
function progressionBar(istep,nstep) {
  //         012345678901234567890123456789
  var bar = "==============================";
  var nbar = bar.length;
  if (nstep < nbar) {
    nbar = nstep;
    bar = bar.slice(0,nstep);
  }
  // both ibar and istep go from 0 (inclusive) to nstep (exclusive)
  var ibar = Math.floor((istep/nstep) * nbar);
  bar = replaceAt(bar,ibar,"|");
  return bar;
}

/**
 *  Go one timestep ahead. In fact, changing iTimestep results in changing the color of each face of the mesh.
 *  The function is called by window.setInterval() and showNextTimestep().
 *
 */
function showNextTimestepAnimated() {
  iTimestep += 1;
  if (iTimestep > (nTimestep[iCanvas0] - 1)) {
    iTimestep = 0;
  }

  var icanvas;
  for (icanvas = 0; icanvas < nCanvas; icanvas++) {
    
    var nstepface = data[icanvas].facecolorscolumnlength; // total number of timesteps.
    
    if (nstepface == nTimestep[iCanvas0]) {
      // thus, the faces on canvas icanvas have the same number of timesteps
      // of the faces on the default canvas;
      // so, synchronise the coloring of the faces on canvas icanvas with
      // those on the default canvas iCanvas0:
      
      var icanvascolor = wgeo[icanvas].colorsfrom; // colors are defined in data[icanvascolor].colors
      var ncolor = data[icanvascolor].colors.length / 3;
      var ncoloroffset = (0 + ! transparentFaces) * (ncolor + nspecialcolor);
      
      var i;
      var ifaces = wgeo[icanvas].iface;
      for (i = 0; i < wgeo[icanvas].faces.geometry.faces.length; i++) {
        wgeo[icanvas].faces.geometry.faces[i].materialIndex = 
          data[icanvas].facecolors[ (ifaces[i] * nstepface + iTimestep) ] 
          + nspecialcolor + ncoloroffset;
      };

      console.log("face materials in the scene of canvas " + String(icanvas) 
                  + " updated for iTimestep=" + String(iTimestep));
      wgeo[icanvas].faces.material.needsUpdate = true;
      wgeo[icanvas].faces.geometry.elementsNeedUpdate = true;
    };

    if (data[icanvas].linecolorscolumnlength == nTimestep[iCanvas0]) {
      createLineSegments(icanvas,0);
    };

    if (data[icanvas].textcolorscolumnlength == nTimestep[iCanvas0]) {
      createLineGlyphs(icanvas,0);
    };

  };
 
  document.getElementById("timestep").innerHTML = 
    "<span class=\"leftside\"><span class=\"contrast\">"
    + "<b>" + progressionBar(iTimestep,nTimestep[iCanvas0]) + "</b>  Timestep&nbsp;</span></span>"
    + "<span class=\"rightside\"><span class=\"contrast\">" 
    + String(iTimestep+1) + "/" + String(nTimestep[iCanvas0]) 
    + " \"" + data[iCanvas0].stepnames[iTimestep] + "\".</span></span>";
    
  // render the scenes of all canvasses:
  renderAll(true);
}

/**
 *  Toggle face transparency.
 *
 */
function toggleTransparency() {
  transparentFaces = ! transparentFaces;
  
  var icanvas;
  for (icanvas = 0; icanvas < nCanvas; icanvas++) {
    
    var icanvascolor = wgeo[icanvas].colorsfrom; // colors are defined in data[icanvascolor].colors
    var ncolor = data[icanvascolor].colors.length / 3;
    var ncoloroffset = (0 + ! transparentFaces) * (ncolor + nspecialcolor);
    
    var istepface = iTimestep;
    var nstepface = data[icanvas].facecolorscolumnlength; // total number of timesteps.
    if (nstepface != nTimestep[iCanvas0]) {
      // thus, the faces on canvas icanvas do not have the same number of timesteps
      // of the faces on the default canvas;
      // therefore we can not synchronise the coloring of the faces on canvas icanvas with
      // those on the default canvas iCanvas0:
      istepface = 0;
    };
      
    var i;
    var ifaces = wgeo[icanvas].iface;
    for (i = 0; i < wgeo[icanvas].faces.geometry.faces.length; i++) {
      wgeo[icanvas].faces.geometry.faces[i].materialIndex = 
        data[icanvas].facecolors[ (ifaces[i] * nstepface + istepface) ] 
        + nspecialcolor + ncoloroffset;
    };

    console.log("face transparency in the scene of canvas " + String(icanvas) 
                  + " changed into " + String(transparentFaces));
    wgeo[icanvas].faces.material.needsUpdate = true;
    wgeo[icanvas].faces.geometry.elementsNeedUpdate = true;
  };

  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
  
  // render the scenes of all canvasses:
  renderAll(true);
}

/**
 *  Convert clippling plane definitions 
 *  from an array of normal and point x,y,z components:
 *    np = [ nx0, ny0, nz0, x0, y0, z0,
 *           nx1, ny1, nz1, x1, y1, z1, 
 *           ...
 *         ]
 *         taken from data[icanvas].camerastations[istation+9];
 *  into an array of THREE.plane() objects.
 *
 */
function convertClippingPlanes(np) {
  var i, l;
  var normal = new THREE.Vector3();
  var point = new THREE.Vector3();
  var plane = new THREE.Plane();

  var planes = [];

  for (i = 0; i < np.length; i = i + 6) {
    normal.set( np[i+0], np[i+1], np[i+2] );
    point.set(  np[i+3], np[i+4], np[i+5] );

    l = normal.length();

    if (l > 0) {
      normal.divideScalar(l).negate();
      plane.setFromNormalAndCoplanarPoint(normal, point);
      planes.push( plane.clone() );
    };
  };
  
  return planes;
}

/**
 *  Apply clipping planes on renderer of the default (first) canvas.
 *
 */
function applyClippingPlanes() {
  // availability of toggle button:  
  button.avail.toggleClipStation = (clipStation.planes.length > 0);
  
  var planes = [];

  if (clipStation.showing) {
    planes = clipStation.planes.slice();
  };
  
  if (clipInteract.showing) {
    // normal pointing into the hemisphere which is clipped away:
    var normal = aer2xyz( 
      {
        az : clipInteract.az * Math.PI/180,
        el : clipInteract.el * Math.PI/180,
        r : 1
      }
    );
    var point = new THREE.Vector3(clipInteract.x, clipInteract.y, clipInteract.z);
    var plane = new THREE.Plane();
    plane.setFromNormalAndCoplanarPoint(normal.normalize().negate(), point);
    planes.push( plane.clone() );
  };

  renderer[iCanvas0].clippingPlanes = planes; 
  
  console.log("applied " + String(renderer[iCanvas0].clippingPlanes.length) + " clipping plane(s) on renderer.");
}

/**
 *  Toggle clipping planes defined by the actual camera station.
 *
 */
function toggleClipStation() {
/*    
    // invisible everything above horizontal plane through z=4:
    //var globalPlane = new THREE.Plane( new THREE.Vector3( 0, 0, -1 ), 4 );
    // invisible everything below horizontal plane through z=4:
    //var globalPlane = new THREE.Plane( new THREE.Vector3( 0, 0, 1 ), -4 );
    // invisible everything below horizontal plane through z=4:
    //var globalPlane = new THREE.Plane( new THREE.Vector3( 0, 0, -1 ), -4 );
    // invisible everything at x<2 parallel to y,z plane:
    //var globalPlane = new THREE.Plane( new THREE.Vector3( 1, 0, 0 ), -2 );
    //var globalPlane = new THREE.Plane();
    //globalPlane.setFromNormalAndCoplanarPoint(new THREE.Vector3( 1, 0, 0 ),new THREE.Vector3( 2, 0, 0 )); 
    // invisible everything at x<-2 parallel to y,z plane:
    //var globalPlane = new THREE.Plane( new THREE.Vector3( 1, 0, 0 ), 2 );
    //var globalPlane = new THREE.Plane();
    //globalPlane.setFromNormalAndCoplanarPoint(new THREE.Vector3( 1, 0, 0 ),new THREE.Vector3( -2, 0, 0 )); 
    // invisible everything at x>2 parallel to y,z plane:
    //var globalPlane = new THREE.Plane( new THREE.Vector3( -1, 0, 0 ), 2 );
    //var globalPlane = new THREE.Plane();
    //globalPlane.setFromNormalAndCoplanarPoint(new THREE.Vector3( -1, 0, 0 ),new THREE.Vector3( 2, 0, 0 )); 
    // invisible everything at x>-2 parallel to y,z plane:
    //var globalPlane = new THREE.Plane( new THREE.Vector3( -1, 0, 0 ), -2 );
    //var globalPlane = new THREE.Plane();
    //globalPlane.setFromNormalAndCoplanarPoint(new THREE.Vector3( -1, 0, 0 ),new THREE.Vector3( -2, 0, 0 )); 
    //renderer[iCanvas0].clippingPlanes = [ globalPlane ];

    // invisible everything at x>3 parallel to y,z plane, and y>1.5 parallel to x,z plane:
    //var globalPlanes = [new THREE.Plane(), new THREE.Plane()];
    //globalPlanes[0].setFromNormalAndCoplanarPoint(new THREE.Vector3( -1, 0, 0 ),new THREE.Vector3( 3, 0, 0 )); 
    //globalPlanes[1].setFromNormalAndCoplanarPoint(new THREE.Vector3( 0, -1, 0 ),new THREE.Vector3( 0, 1.5, 0 )); 
    //renderer[iCanvas0].clippingPlanes = globalPlanes;
*/    

  // clipStation.planes is set by showNextStation() and contains zero or more THREE.Plane()'s.

  if (clipStation.planes.length > 0) {
    clipStation.showing = ! clipStation.showing;
    
    applyClippingPlanes();
  };

  console.log("new station clipping mode = " + String(clipStation.showing));

  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
  
  // render the scene of the default (first) canvas:
  render(iCanvas0);
}

/**
 *  Toggle clipping plane defined by interaction.
 *
 */
function toggleClipInteract() {
  clipInteract.showing = ! clipInteract.showing;
  
  applyClippingPlanes();
  
  console.log("new interactive clipping mode = " + String(clipInteract.showing));

  button.avail.increaseAz = clipInteract.showing;
  button.avail.decreaseAz = clipInteract.showing;
  button.avail.increaseEl = clipInteract.showing;
  button.avail.decreaseEl = clipInteract.showing;
  button.avail.increaseX = clipInteract.showing;
  button.avail.decreaseX = clipInteract.showing;
  button.avail.increaseY = clipInteract.showing;
  button.avail.decreaseY = clipInteract.showing;
  button.avail.increaseZ = clipInteract.showing;
  button.avail.decreaseZ = clipInteract.showing;
    
  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
  
  // render the scene of the default (first) canvas:
  render(iCanvas0);
}

/**
 *  In/decrease azimuth/elevation of normal of interactive clipping plane.
 *
 */
function increaseAz() {
  clipInteract.az += 5;
  clipInteract.az = (clipInteract.az >= 360) ? (clipInteract.az - 360) : (clipInteract.az);

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function decreaseAz() {
  clipInteract.az -= 5;
  clipInteract.az = (clipInteract.az < 0) ? (360 - clipInteract.az) : (clipInteract.az);

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function increaseEl() {
  clipInteract.el += 5;
  clipInteract.el = (clipInteract.el >= 360) ? (clipInteract.el - 360) : (clipInteract.el);

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function decreaseEl() {
  clipInteract.el -= 5;
  clipInteract.el = (clipInteract.el < 0) ? (360 - clipInteract.el) : (clipInteract.el);

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  

/**
 *  In/decrease x,y,z position of interactive clipping plane.
 *
 */
function increaseX() {
  clipInteract.x += spaceSteps[iSpaceStep];

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function decreaseX() {
  clipInteract.x -= spaceSteps[iSpaceStep];

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function increaseY() {
  clipInteract.y += spaceSteps[iSpaceStep];

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function decreaseY() {
  clipInteract.y -= spaceSteps[iSpaceStep];

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function increaseZ() {
  clipInteract.z += spaceSteps[iSpaceStep];

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  
function decreaseZ() {
  clipInteract.z -= spaceSteps[iSpaceStep];

  clipInteract.showing = ! clipInteract.showing; // trick to keep the same clipping mode again:
  toggleClipInteract();
}  

/**
 *  Show next face with its name. It is called by handleKeyStroke().
 *
 */
function showNextFace() {
  if (showingFaces) {
    // animation is on and should be put off now
    window.clearInterval(wShowingFaces);
    showingFaces = false;

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };

  // showing individual faces and timestepping are mutually exclusive:
  document.getElementById("timestep").innerHTML = "";
  if (showingTimesteps) {
    // animation is on and should be put off now
    window.clearInterval(wShowingTimesteps);
    showingTimesteps = false;

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };
  
  showNextFaceAnimated();
}

/**
 *  Show next face with its name. The function is called by window.setInterval() and showNextFace().
 *
 */
function showNextFaceAnimated() {
  iFace += 1;
  if (iFace > (nFace - 1)) {
    iFace = 0;
  };

  var i;
  var ifaces = wgeo[iCanvas0].iface;

  for (i = 0; i < wgeo[iCanvas0].faces.geometry.faces.length; i++) {
    if (ifaces[i] == iFace) {
      // show iFace with special color 1, i.e. very white 
      wgeo[iCanvas0].faces.geometry.faces[i].materialIndex = 0;

      // // show iFace with its ordinary color 
      // wgeo[iCanvas0].faces.geometry.faces[i].materialIndex = data[iCanvas0].facecolors[ (ifaces[i] * nTimestep[iCanvas0] + iTimestep) ];

    } else {
      // show any other face than iFace with special color (-1 + nspecialcolor), i.e. invisible 
      wgeo[iCanvas0].faces.geometry.faces[i].materialIndex = -1 + nspecialcolor;
    };
  };

  console.log("material updated for showing iFace=" + String(iFace));
  wgeo[iCanvas0].faces.material.needsUpdate = true;
  wgeo[iCanvas0].faces.geometry.elementsNeedUpdate = true;
  
  var facename = "";
  if (data[iCanvas0].facenames.length > 0) {
    facename = " called \"" + data[iCanvas0].facenames[iFace] + "\"";
  };

  document.getElementById("face").innerHTML = 
    "<span class=\"leftside\"><span class=\"contrast\">"
    + "<b>" + progressionBar(iFace,nFace) + "</b>  Highlighting face&nbsp;</span></span>"
    + "<span class=\"rightside\"><span class=\"contrast\">" 
    + String(iFace+1) + "/" + String(nFace) + facename + ".</span></span>";

  render(iCanvas0);
}

/**
 *  Show all faces.
 *
 */
function showAllFaces() {
  if (showingFaces) {
    // animation is on and should be put off now
    window.clearInterval(wShowingFaces);
    showingFaces = false;

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };

  // showing individual faces and timestepping are mutually exclusive:
  document.getElementById("timestep").innerHTML = "";
  if (showingTimesteps) {
    // animation is on and should be put off now
    window.clearInterval(wShowingTimesteps);
    showingTimesteps = false;

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };
  
  iTimestep = iTimestep - 1; // trick to show the colors of actual iTimestep, so redrawing the actual drawing
  showNextTimestepAnimated();
  
  document.getElementById("face").innerHTML = "";
}

/**
 *  A "spiky sine" function for showNextOrbit()
 *
 * quadrant   output
 *   I        -1*cos(x) + 1
 *   II       +1*cos(x) + 1
 *   III      -1*cos(x) - 1
 *   IV       +1*cos(x) - 1
 *
 */
function spiky(x) {
  var m = [-1,+1,-1,+1];
  var a = [+1,+1,-1,-1];
  var q = 1;
  var pi = Math.PI;
  var twopi = 2*Math.PI;
  
  // x = mod(x,2*pi);
  // x = x % (2*pi) yields NaN in javascript
  x = x - (twopi * Math.floor(x / twopi)); 

  q = Math.floor(2 * (x / pi)); // (quadrant 1..4) - 1
  
  return (a[q] + m[q]*Math.cos(x)); 
}

/**
 *  Calculate Cartesian coordinates from azimuth angle, elevation angle and radius.
 *    The azimuth is measured in the x,y-plane from the positive y-axis in clockwise 
 *    direction as seen from a far point on the the positive z-axis.
 *    The elevation is measured from the x,y-plane, and is positive when the vector
 *    may be projected onto the positive z-axis.
 *  Input: one record with fields "az" (in radians), "el" (in radians) and "r" (in length units).
 *  Output: one object of type THREE.Vector3.
 *
 */
function aer2xyz(aer) {
  var xyz = new THREE.Vector3(0,0,0);
  /* phi is measured in the x,y-plane from the positive x-axis in counter-clockwise 
   * direction as seen from a far point on the positive z-axis.
   */
  var phi = (Math.PI/2) - aer.az;
  var ce = Math.cos(aer.el);
  return xyz.set(aer.r * Math.cos(phi) * ce,
                 aer.r * Math.sin(phi) * ce,
                 aer.r * Math.sin(aer.el) );
}
  
/**
 *  Calculate azimuth angle, elevation angle and radius from Cartesian coordinates.
 *    The azimuth is measured in the x,y-plane from the positive y-axis in clockwise 
 *    direction as seen from a far point on the the positive z-axis.
 *    The elevation is measured from the x,y-plane, and is positive when the vector
 *    may be projected onto the positive z-axis.
 *  Input: one object of type THREE.Vector3.
 *  Output: one record with fields "az" (in radians), "el" (in radians) and "r" (in length units).
 *
 */
function xyz2aer(xyz) {
  var aer = {az: 0, el: 0, r: 0};
  aer.r = Math.sqrt( (xyz.x)*(xyz.x) + (xyz.y)*(xyz.y) + (xyz.z)*(xyz.z));
  /* phi is measured in the x,y-plane from the positive x-axis in counter-clockwise 
   * direction as seen from a far point on the positive z-axis.
   */
  var phi = Math.atan2( xyz.y, xyz.x );
  aer.az = (Math.PI/2) - phi;
  // we prefer values from 0 (inclusive) to 2*pi (exclusive):
  if (aer.az < 0) {
    aer.az = 2*Math.PI + aer.az;
  }; 
  aer.az = aer.az % (2*Math.PI);
  aer.el = Math.atan2( xyz.z, Math.sqrt( (xyz.x)*(xyz.x) + (xyz.y)*(xyz.y) ) );
  return aer;
}

/**
 *  Show next orbit position, depending on orbit mode.
 *
 */
function showNextOrbit() {
  var pi = Math.PI;
  var rad = pi/180;
  var pcam = new THREE.Vector3(0,0,0); // position of camera
  var ucam = new THREE.Vector3(0,0,1); // upright direction of camera
  var fcam = new THREE.Vector3(0,0,0); // focus of camera
  var s = '';

  // depending on orbit mode, new camera position and camera focus are calculated:
  //
  var orbitMode = orbitModes[iorbitmode];
  
  if (orbitMode == "spiral") {
    norbit = 10 * Math.round(360 / data[iCanvas0].cameraorbitmodes.spiral.deltaazimuth);

    iorbit += 1;
    if ((iorbit + 1) > norbit) {
      iorbit = 0;
    };

    // azimuth:
    var azcam = iorbit * data[iCanvas0].cameraorbitmodes.spiral.deltaazimuth * rad;  
    
    // elevation depends slowly from the azimuth:
    //elcam = (70*rad) * Math.sin( 0.25 * azcam );
    var elcam = (85*rad) * spiky( 0.25 * azcam);
    
    pcam = aer2xyz({az: azcam, el: elcam, r: envelope[iCanvas0].radius});
    
    pcam.add(envelope[iCanvas0].centre);
    fcam = envelope[iCanvas0].centre;
  };
  if (orbitMode == "swirl") {
    // 2018-10-01 not much success: abandoned
  
    var nturn = Math.floor(360 / data[iCanvas0].cameraorbitmodes.swirl.d);
    norbit = data[iCanvas0].cameraorbitmodes.swirl.n * nturn;

    iorbit += 1;
    if ((iorbit + 1) > norbit) {
      iorbit = 0;
    };
  
    // one turn:
    var iaz = iorbit % data[iCanvas0].cameraorbitmodes.swirl.n; 
    var azcam = iaz * 2*pi/data[iCanvas0].cameraorbitmodes.swirl.n;
    var racam = envelope[iCanvas0].radius;
    
    pcam.set( racam * Math.sin(2*azcam), racam * Math.sin(azcam), racam * (1/10)*Math.cos(azcam) );
    fcam.set( racam * (1/13)*Math.cos(5*azcam), 0, racam * (1/13)*Math.sin(5*azcam) );
  
    // rotate the x-y-z axis system a bit:
    var pcama = xyz2aer(pcam);
    var fcama = xyz2aer(fcam);
    var iturn = Math.floor( iorbit / nturn );
    pcama.az = pcama.az + iturn * (data[iCanvas0].cameraorbitmodes.swirl.d * rad);
    fcama.az = fcama.az + iturn * (data[iCanvas0].cameraorbitmodes.swirl.d * rad);
    pcam = aer2xyz( pcama );
    fcam = aer2xyz( fcama );

    pcam.add(envelope[iCanvas0].centre);
    fcam.add(envelope[iCanvas0].centre);
    
    s = " iaz=" + String(iaz) + " iturn=" + String(iturn);
  };
  if (orbitMode == "cardinal") {
    norbit = data[iCanvas0].cameraorbitmodes.cardinal.length/2;

    iorbit += 1;
    if ((iorbit + 1) > norbit) {
      iorbit = 0;
    };

    var i = iorbit * 2; // index in data[iCanvas0].cameraorbitmodes.cardinal; it is a one dimensional array
    pcam = aer2xyz({az: data[iCanvas0].cameraorbitmodes.cardinal[i] * rad, el: data[iCanvas0].cameraorbitmodes.cardinal[i+1] * rad, r: envelope[iCanvas0].radius});
    
    pcam.add(envelope[iCanvas0].centre);
    fcam = envelope[iCanvas0].centre;
  };
  if (orbitMode == "across") {
    norbit = data[iCanvas0].cameraorbitmodes.across.length/3;

    iorbit += 1;
    if ((iorbit + 1) > norbit) {
      iorbit = 0;
    };

    var i = iorbit * 3; // index in data[iCanvas0].cameraorbitmodes.across; it is a one dimensional array
    pcam = aer2xyz({az: data[iCanvas0].cameraorbitmodes.across[i] * rad, el: data[iCanvas0].cameraorbitmodes.across[i+1] * rad, r: envelope[iCanvas0].radius * data[iCanvas0].cameraorbitmodes.across[i+2]});
    
    pcam.add(envelope[iCanvas0].centre);
    fcam = envelope[iCanvas0].centre;
  };
  if (orbitMode == "tour") {
    norbit = data[iCanvas0].cameratour.length/9;

    iorbit += 1;
    if ((iorbit + 1) > norbit) {
      iorbit = 0;
    };

    var i = iorbit * 9; // index in data[iCanvas0].cameratour; it is a one dimensional array
    pcam.set(data[iCanvas0].cameratour[i],data[iCanvas0].cameratour[i+1],data[iCanvas0].cameratour[i+2]);
    ucam.set(data[iCanvas0].cameratour[i+3],data[iCanvas0].cameratour[i+4],data[iCanvas0].cameratour[i+5]);
    fcam.set(data[iCanvas0].cameratour[i+6],data[iCanvas0].cameratour[i+7],data[iCanvas0].cameratour[i+8]);
  };
  if (orbitMode == "station") {
    norbit = data[iCanvas0].camerastations.length / 10;

    iorbit += 1;
    if ((iorbit + 1) > norbit) {
      iorbit = 0;
    };

    var i = iorbit * 10; // index in data[iCanvas0].camerastations; it is a one dimensional array
    pcam.set(data[iCanvas0].camerastations[i],data[iCanvas0].camerastations[i+1],data[iCanvas0].camerastations[i+2]);
    ucam.set(data[iCanvas0].camerastations[i+3],data[iCanvas0].camerastations[i+4],data[iCanvas0].camerastations[i+5]);
    fcam.set(data[iCanvas0].camerastations[i+6],data[iCanvas0].camerastations[i+7],data[iCanvas0].camerastations[i+8]);
    
    // clipping planes:
    clipStation.planes = data[iCanvas0].camerastations[i+9].slice();
    clipStation.showing = (clipStation.planes.length > 0);
    applyClippingPlanes();
  };

  // set its new position, its view direction (towards the focus = centre of the surfaces), and
  // put it to upright:
  //
  putCamera(pcam,ucam,fcam);

  var acam = xyz2aer(pcam);
 
  console.log("camera position updated for showing orbit (\"" + orbitMode + "\", " + String(iorbit+1) + "/" + String(norbit) + "): " + s);
  console.log("camera position updated for showing orbit (\"" + orbitMode + "\", " + String(iorbit+1) + "/" + String(norbit) + "): x=" + pcam.x.toFixed(4) + " y=" + pcam.y.toFixed(4) + " z=" + pcam.z.toFixed(4));
  console.log("camera position updated for showing orbit (\"" + orbitMode + "\", " + String(iorbit+1) + "/" + String(norbit) + "): radius=" + acam.r.toFixed(4) + " azimuth=" + (acam.az/rad).toFixed(4) + "deg elevation=" + (acam.el/rad).toFixed(4) + "deg");

  // write the move on screen:
  //
  document.getElementById("move").innerHTML = "<span class=\"contrast\">Orbit mode \"" + orbitMode + "\" (" + String(iorbit+1) + "/" + String(norbit) + s + ").</span>";

  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
  
  // remake the picture on screen:
  //
  render(iCanvas0);
}

/**
 *  Show next tour position.
 *
 */
function showNextTour() {
  var ntour = data[iCanvas0].cameratour.length/9;
  
  itour += 1;
  if (itour > (ntour - 1)) {
    itour = 0;
  }; 

  var i = itour * 9; // index in data[iCanvas0].cameratour; it is a one dimensional array
  
  // see showNextOrbit():
  //
  putCamera(data[iCanvas0].cameratour[i],data[iCanvas0].cameratour[i+1],data[iCanvas0].cameratour[i+2],
            data[iCanvas0].cameratour[i+3],data[iCanvas0].cameratour[i+4],data[iCanvas0].cameratour[i+5],
            data[iCanvas0].cameratour[i+6],data[iCanvas0].cameratour[i+7],data[iCanvas0].cameratour[i+8]);
  
  console.log("camera position updated for showing tour with itour=" + String(itour) );
  
  // write the move on screen:
  //
  document.getElementById("move").innerHTML = 
    "<span class=\"contrast\"><b>" + progressionBar(itour,ntour) + "</b>  Touring (" 
    + String(itour+1) + "/" + String(data[iCanvas0].cameratour.length/9) + ").</span>";
  
  // remake the picture on screen:
  //
  render(iCanvas0);
}

/**
 *  Show next station.
 *
 */
function showNextStation() {
  var nstation = nStation[iCanvas0] + nExtraStation[iCanvas0];
  
  iStation += 1;
  if (iStation > (nstation - 1)) {
    iStation = 0;
  }; 

  // only if variable data[iCanvas0].camerastations has contents, we can put the camera somewhere:   
  if (nstation > 0) {
    var i = iStation * 10; // index in data[iCanvas0].camerastations; it is a one dimensional array
  
    // see showNextOrbit():
    //
    putCamera(data[iCanvas0].camerastations[i],
              data[iCanvas0].camerastations[i+1],
              data[iCanvas0].camerastations[i+2],
              data[iCanvas0].camerastations[i+3],
              data[iCanvas0].camerastations[i+4],
              data[iCanvas0].camerastations[i+5],
              data[iCanvas0].camerastations[i+6],
              data[iCanvas0].camerastations[i+7],
              data[iCanvas0].camerastations[i+8]);
    
    // clipping planes:
    clipStation.planes = data[iCanvas0].camerastations[i+9].slice();
    clipStation.showing = (clipStation.planes.length > 0);
    applyClippingPlanes();

    console.log("camera position updated for showing station with iStation=" + String(iStation));

    // write the move on screen:
    //
    //document.getElementById("move").innerHTML = "<span class=\"contrast\">Station (" + String(iStation+1) + "/" + String(nstation) + ").</span>";

    var stationname = "";
    if (data[iCanvas0].camerastationnames[iStation].length > 0) {
      stationname = " called \"" + data[iCanvas0].camerastationnames[iStation] + "\"";
    };

    document.getElementById("move").innerHTML = 
      "<span class=\"leftside\"><span class=\"contrast\">"
      + "<b>" + progressionBar(iStation,nstation) + "</b> Station&nbsp;</span></span>"
      + "<span class=\"rightside\"><span class=\"contrast\">" 
      + String(iStation+1) + "/" + String(nstation) + stationname + ".</span></span>";

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
      
    // remake the picture on screen:
    //
    render(iCanvas0);
  };
}


/**
 *  Calculate azimuth, elevation (in degrees) and radius of 
 *  a direction vector (or type THREE.Vector3).
 *
 */
function calcAzElR(vector) {
  var azelr = {az: 0, el: 0, radius: 0};

  azelr.radius = Math.sqrt((vector.x*vector.x + vector.y*vector.y + vector.z*vector.z));
  azelr.az = Math.atan2(vector.y,vector.x);
  azelr.el = Math.asin(vector.z / azelr.radius);

  azelr.az = azelr.az * 180/Math.PI;
  azelr.el = azelr.el * 180/Math.PI;

  return azelr;
}

/**
 *  Print camera position to console and on screen.
 *
 */
function printCameraPosition() {
  // variables for camera orientation:
  //
  var pcam = new THREE.Vector3(0,0,0); // position of camera
  var dcam = new THREE.Vector3(0,0,0); // view direction vector of camera

  // read the camera position and rotation:
  //
  camera[iCanvas0].getWorldPosition(pcam);
  camera[iCanvas0].getWorldDirection(dcam);
    
  // view direction in terms of azimuth, elevation and radius:
  //
  var dcama = calcAzElR(dcam); // mathematical definition for azimuth, angles in degrees
  var dcamm = xyz2aer(dcam); // meteorological definition for azimuth, angles in radians

  // -- https://stackoverflow.com/questions/35641875/three-js-how-to-find-world-orientation-vector-of-objects-local-up-vector
  // -- https://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm
  //    -- https://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/index.htm
  //    -- https://www.euclideanspace.com/maths/geometry/rotations/axisAngle/index.htm
  // -- https://www.andre-gaschler.com/rotationconverter/
  
  // rotation of the camera in terms of euler angles:
  //
  var ecam = new THREE.Euler();
  ecam.setFromRotationMatrix(camera[iCanvas0].matrixWorld,"XYZ");
  // same results with: ecam.setFromRotationMatrix(camera[iCanvas0].matrix,"XYZ");
  // if up means z==1, ecam.z, sometimes ecam.x, is roll angle
  // if up means y==1, ecam.y is roll angle

  //var ecami = new THREE.Euler();
  //ecami.setFromRotationMatrix(camera[iCanvas0].matrixWorldInverse,"XYZ");
  // if up means y==1, ecamw.z is roll angle
  // if up means z==1, ecamw.y is roll angle
  
  // the rotation of the camera in world coordinates in terms of a quaternion:
  //
  var qcam = new THREE.Quaternion();
  camera[iCanvas0].getWorldQuaternion(qcam);
  
  // the "roll" angle of the camera is the third angle
  // after azimuth and elevation:
  //
  //   !!! calculation results in ambiguous angle values due to dot product in function .angleTo()
  //
  var roll = NaN;
  if ( (dcama.el > -90) && (dcama.el < 90) ) { 
    // produce a reference quaternion for the camera 
    // with its actual azimuth and elevation (taken from dcama), 
    // but with a roll of zero degrees:
    var q = new THREE.Quaternion();
    THREE.MathUtils.setQuaternionFromProperEuler(q,
                                                 (dcama.az*Math.PI/180),
                                                 (-1*(90-dcama.el)*Math.PI/180),
                                                 0,
                                                 "ZYZ");
    // the actual roll angle of the camera is the difference in 
    // angle between the actual quaternion of the camera
    // and the reference one with no roll:
    roll = -1 * (qcam.angleTo(q) * 180/Math.PI - 90);
    // now, roll angle is zero if the model horizon is horizontal on screen
    //      roll angle is positive if leftside of picture is turned upwards
  } else {
    // thus, the elevation is +/- 90 degrees, and we have a special case:
    roll = ecam.z * 180/Math.PI;
    // now, roll angle is zero if the world y axis points to the top of the screen and world x axis to right
    //      in that case, roll angle is positive if leftside of picture is turned upwards
  };
  
  /*
  console.log("camera position changed to   (" + pcam.x.toFixed(4) + "," + pcam.y.toFixed(4) + "," + pcam.z.toFixed(4) + ")");
  console.log("       view direction angles (" + dcama.az.toFixed(1) + "deg," + dcama.el.toFixed(1) + "deg," + dcama.radius.toFixed(4) + ")");
  console.log("       view direction vector (" + dcam.x.toFixed(4) + "," + dcam.y.toFixed(4) + "," + dcam.z.toFixed(4) + ")");
  console.log("       euler angles          (" + (ecam.x * 180/Math.PI).toFixed(3) + "deg," + (ecam.y * 180/Math.PI).toFixed(3) + "deg," + (ecam.z * 180/Math.PI).toFixed(3) + "deg)");
  console.log("       roll angle             " + roll.toFixed(3) + "deg");  
  */
  
  document.getElementById("camera").innerHTML = "<span class=\"contrast\">"
                                                + "Camera position at x=" + pcam.x.toFixed(3) 
                                                + " y=" + pcam.y.toFixed(3) 
                                                + " z=" + pcam.z.toFixed(3) 
                                                + " and view direction with azimuth=" 
                                             // + dcama.az.toFixed(1) + "deg elevation=" 
                                                + (dcamm.az*180/Math.PI).toFixed(1) + "deg (clockwise from y axis) elevation=" 
                                                + dcama.el.toFixed(1) + "deg roll="
                                                + roll.toFixed(1) + "deg"
                                                + "</span>";

  // actual up direction vector of the camera,
  // calculated from two points in its normalized 
  // device coordinate (ndc):
  var p0 = new THREE.Vector3(0, 0, 0);
  var p1 = new THREE.Vector3(0, 1, 0);
    
  // project into world space:
  p0.unproject(camera[iCanvas0]);
  p1.unproject(camera[iCanvas0]);
    
  var ucam = new THREE.Vector3(p1.x, p1.y, p1.z);
  ucam.sub(p0);
  ucam.normalize();

  // write to the shadow clipboard in octave-compatible code:
  shadowClipboard = " " + 
    + pcam.x.toFixed(4) + ", " + pcam.y.toFixed(4) + ", " + pcam.z.toFixed(4) + ", "
    + ucam.x.toFixed(4) + ", " + ucam.y.toFixed(4) + ", " + ucam.z.toFixed(4) + ", "
    + dcam.x.toFixed(4) + ", " + dcam.y.toFixed(4) + ", " + dcam.z.toFixed(4) + ", "
    + (dcamm.az*180/Math.PI).toFixed(2) + ", " 
    + dcama.el.toFixed(2) + ", " 
    + roll.toFixed(2) + "; "
    + "% camera position, up direction, view direction, azimuth-elevation-roll angles;\n";
  // note that preferred data are:
  //   camera position, up direction, focus point, (azimuth-elevation-roll angles)
  // but the camera focus point cannot be well defined, therefore it is not calculated.

}

/**
 *  A function to toggle timestepping mode. It is called within handleKeyStroke() and by pressing the button.
 *
 */
function toggleTimestepping() {
  // only if there are more than one timesteps:
  if (nTimestep[iCanvas0] > 1) {
    
    if (showingTimesteps) {
      
      // animation is on and should be put off now
      window.clearInterval(wShowingTimesteps);
      showingTimesteps = false;
      
    } else {

      // showing individual faces and timestepping are mutually exclusive:
      document.getElementById("face").innerHTML = "";
      if (showingFaces) {
        // animation is on and should be put off now
        window.clearInterval(wShowingFaces);
        showingFaces = false;
      };

      // animation is off and should be put on now
      wShowingTimesteps = window.setInterval(showNextTimestepAnimated, timesteppingSpeed * timesteppingSpeeds[iTimesteppingSpeed]);
      showingTimesteps = true;
    };
    
    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();

  };    
}

/**
 *  Stop orbitting or touring. The function is called when the mouse or touch pad is used.
 *
 */
function stopOrbitTour(evt) {
  if (showingOrbitTour > 0) {
    // orbit or tour animation is on and should be put off now
    window.clearInterval(wShowingOrbitTour);
    showingOrbitTour = 0;

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
  };
}

/**
 *  A function to toggle orbit mode. It is called within handleKeyStroke() and by pressing the button.
 *
 */
function toggleOrbit() {
  if (showingOrbitTour > 0) {
    // orbit or tour animation is on and should be put off now
    window.clearInterval(wShowingOrbitTour);
  };

  if (showingOrbitTour == 1) {
    // orbit animation is on and should be put off now
    showingOrbitTour = 0;
  } else {
    // orbit animation is off and should be put on now
    wShowingOrbitTour = window.setInterval(showNextOrbit, orbitSpeed * orbitSpeeds[iorbitspeed]);
    showingOrbitTour = 1;
  };

  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
}

/**
 *  A function to toggle showing faces.
 *
 */
function toggleFaces() {
  // only if there are more than zero faces:
  if (data[iCanvas0].faces.length > 0) {
    
    if (showingFaces) {

      // animation is on and should be put off now
      window.clearInterval(wShowingFaces);
      showingFaces = false;

    } else {
    
      // showing individual faces and timestepping are mutually exclusive:
      document.getElementById("timestep").innerHTML = "";
      if (showingTimesteps) {
        // animation is on and should be put off now
        window.clearInterval(wShowingTimesteps);
        showingTimesteps = false;
      };
    
      // animation is off and should be put on now
      wShowingFaces = window.setInterval(showNextFaceAnimated, timesteppingSpeed * timesteppingSpeeds[iTimesteppingSpeed]);
      showingFaces = true;
    };

    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();
    
  };
}

/**
 *  A function to toggle tour mode. It is called within handleKeyStroke() and by pressing the button.
 *
 */
function toggleTour() {
  // only if variable data[iCanvas0].cameratour has contents, we can tour:   
  if (data[iCanvas0].cameratour.length > 0) {

    if (showingOrbitTour > 0) {
      // orbit or tour animation is on and should be put off now
      window.clearInterval(wShowingOrbitTour);
    };

    if (showingOrbitTour == 2) {
      // orbit animation is on and should be put off now
      showingOrbitTour = 0;
    } else {
      // tour animation is off and should be put on now
      wShowingOrbitTour = window.setInterval(showNextTour, orbitSpeed * orbitSpeeds[iorbitspeed]);
      showingOrbitTour = 2;
    };
      
    ibuttonrow -= 1; // trick to show the same row again:
    toggleButtonRow();

  };
}

/**
 *  A function to toggle station mode. It is called within handleKeyStroke() and by pressing the button.
 *
 */
function toggleStation() {
  // only if variable data[iCanvas0].camerastations has contents, we can put the camera somewhere:   
  if (data[iCanvas0].camerastations.length > 0) {

    if (showingOrbitTour > 0) {
      // orbit or tour animation is on and should be put off now
      window.clearInterval(wShowingOrbitTour);
      showingOrbitTour = 0;

      ibuttonrow -= 1; // trick to show the same row again:
      toggleButtonRow();
    };

    // show the next station:
    showNextStation();
  };
}

/**
 *  A function to toggle to the next orbit mode. It is called within handleKeyStroke() and by pressing the button.
 *  It only functions when orbitting is being shown.
 *
 */
function toggleOrbitMode() {
  if (showingOrbitTour > 0) {
    if (orbitModes.length > 0) {
      iorbitmode += 1;
      if ((iorbitmode + 1) > orbitModes.length) {
        iorbitmode = 0;
      };
    };
    
    // when iorbitmode is changed, we start the orbit at "start position" given
    // by iorbit = 0. Because in function showNextOrbit(), we always issue "iorbit += 1" 
    // we apply the following trick: 
    iorbit = -1;
    
    console.log("new orbit mode: iorbitmode=" + String(iorbitmode));
  };
}

/**
 *  A function to toggle to the next orbit speed level. It is called within handleKeyStroke() and by pressing the button.
 *  It only functions when orbitting is being shown.
 *
 */
function toggleOrbitSpeed() {
  if (showingOrbitTour > 0) {
    var iorbitspeed_previous = iorbitspeed;
    if (orbitSpeeds.length > 0) {
      iorbitspeed += 1;
      if ((iorbitspeed + 1) > orbitSpeeds.length) {
        iorbitspeed = 0;
      };
    };
    
    // when iorbitspeed is changed, we have to adapt window.setInterval:
    if (iorbitspeed != iorbitspeed_previous) {
      window.clearInterval(wShowingOrbitTour);
      wShowingOrbitTour = window.setInterval(showNextOrbit, orbitSpeed * orbitSpeeds[iorbitspeed]);
    };
    
    console.log("new orbit/tour speed: iorbitspeed=" + String(iorbitspeed));
  };
}

/**
 *  A function to toggle to the next timestepping speed level. It is called within handleKeyStroke() and by pressing the button.
 *  It only functions when timestepping or face highlighting is being shown.
 *
 */
function toggleTimesteppingSpeed() {
  if (showingTimesteps || showingFaces) {
    var itimesteppingspeed_previous = iTimesteppingSpeed;
    
    if (timesteppingSpeeds.length > 0) {
      iTimesteppingSpeed += 1;
      if ((iTimesteppingSpeed + 1) > timesteppingSpeeds.length) {
        iTimesteppingSpeed = 0;
      };
    };
    
    // when iTimesteppingSpeed is changed, we have to adapt window.setInterval:
    if (iTimesteppingSpeed != itimesteppingspeed_previous) {
      if (showingTimesteps) {
        window.clearInterval(wShowingTimesteps);
        wShowingTimesteps = window.setInterval(showNextTimestepAnimated, timesteppingSpeed * timesteppingSpeeds[iTimesteppingSpeed]);
      };
      if (showingFaces) {
        window.clearInterval(wShowingFaces);
        wShowingFaces = window.setInterval(showNextFaceAnimated, timesteppingSpeed * timesteppingSpeeds[iTimesteppingSpeed]);
      };
    };
    
    console.log("new timestepping/face-highlighting speed: iTimesteppingSpeed=" + String(iTimesteppingSpeed));
  };
}

/**
 *  A function to toggle full-screen mode. It is called within handleKeyStroke() and by pressing the button.
 *
 * The code is found in: http://www.css-jquery-design.com/2013/11/javascript-jquery-fullscreen-browser-window-html5-technology/
 * See also: https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
 *           https://stackoverflow.com/questions/1125084/how-to-make-the-window-full-screen-with-javascript-stretching-all-over-the-scre
 *
 */
function toggleFullScreen() {
  if (!document.fullscreenElement &&    // alternative standard method
      !document.mozFullScreenElement && !document.webkitFullscreenElement) {  // current working methods
    if (document.documentElement.requestFullscreen) {
      document.documentElement.requestFullscreen();
      showingFullScreen = true;
    } else if (document.documentElement.mozRequestFullScreen) {
      document.documentElement.mozRequestFullScreen();
      showingFullScreen = true;
    } else if (document.documentElement.webkitRequestFullscreen) {
      document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
      showingFullScreen = true;
    };
  } else {
    if (document.cancelFullScreen) {
      document.cancelFullScreen();
      showingFullScreen = false;
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
      showingFullScreen = false;
    } else if (document.webkitCancelFullScreen) {
      document.webkitCancelFullScreen();
      showingFullScreen = false;
    };
  };

  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();
}

/**
 *  A function to toggle stereo view. It is called within handleKeyStroke() and by pressing the button.
 *
 */
function toggleStereo() {
  // note: toggling the stereo effect off after it was on, does not have the desired effect:
  //       the screen remains divided into 2 and only one side shows a picture

  showingStereo = ! showingStereo;

  if (showingStereo) {
    console.log("rendering with stereo effect from now");
  } else {
    console.log("rendering with mono effect from now");
  };
    
  ibuttonrow -= 1; // trick to show the same row again:
  toggleButtonRow();

  render(iCanvas0);
}

/**
 *  An event listener for the window resize event. It is installed by the init() function.
 *
 */
function onWindowResize() {
  renderAll(false);
}

/**
 *  Render scenes on all canvasses:
 *  -- unconditionnally when force == true, 
 *  -- only after window resize when force == false. 
 *  But first: 
 *  -- if the window has been resized, recalculate extra stations and, 
 *  -- if an extra station has to be shown, put the camera at the recalculated station.
 *
 */
function renderAll(force) {
  var icanvas;

  detectWindowChange();

  for (icanvas = 0; icanvas < nCanvas; icanvas += 1) {

    if ( page.changed == true ) {
      camera[icanvas].aspect = canvasrelwidth[icanvas] * window.innerWidth /
                               ( canvasrelheight[icanvas] * window.innerHeight );
      camera[icanvas].updateProjectionMatrix();
    
      calcExtraStations(icanvas);

      // stereo effect is possible only on the default canvas:
      if (icanvas == iCanvas0) {
        if (showingStereo) {
          effect.setSize( canvasrelwidth[icanvas] * window.innerWidth, 
                          canvasrelheight[icanvas] * window.innerHeight );
        } else {
          renderer[icanvas].setSize( canvasrelwidth[icanvas] * window.innerWidth, 
                                     canvasrelheight[icanvas] * window.innerHeight );
        };
      
      } else {
        renderer[icanvas].setSize( canvasrelwidth[icanvas] * window.innerWidth, 
                                   canvasrelheight[icanvas] * window.innerHeight );
      };
    };

    if ( (force == true) || (page.changed == true) ) {
      // render the scene on the canvas (and reput the camera on an extra station if applicable):
      showExtraStation(icanvas);
    };

  };
  
  page.changed = false;
}

/**
 *  An event listener for the device orientation.  It is installed by the init() function.
 *
 */
function handleDeviceOrientation(evt) {

  // https://developer.mozilla.org/en-US/docs/Web/Events/devicemotion
  // https://developer.mozilla.org/en-US/docs/Web/Events/deviceorientation
  // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Orientation_and_motion_data_explained
  // https://developer.mozilla.org/en-US/docs/Web/API/Detecting_device_orientation

  // very illustrative:
  // https://www.html5rocks.com/en/tutorials/device/orientation/

  icount = icount + 1;

  if (icount > ncount) {
    icount = 0;
 
    var absolute = evt.absolute;
    var alpha    = evt.alpha;
    var beta     = evt.beta;
    var gamma    = evt.gamma;

    document.getElementById("device").innerHTML = "Device orientation: absolute=" + String(absolute) + 
                                                                     " alpha=" + alpha.toFixed(3) + 
                                                                     " beta=" + beta.toFixed(3) + 
                                                                     " gamma=" + gamma.toFixed(3);
  }
}


/**
 *  Function to calculate the relative mouse coordinates, needed for
 *  the raycaster.
 *  The coordinate values are within -1 and 1. 
 *  Thus, the (x,y) coordinates at the centre are (0,0). 
 */
function relativeMouseCoord(event) {
  var icanvas;
  var widthsum = 0;

  for (icanvas = 0; icanvas < nCanvas; icanvas++) {

    // calculate mouse position in normalized device coordinates
    // (-1 to +1) for both components
    //
    //mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
    //mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
  
    // assuming that canvasses are put next to each other:
    if (icanvas == 0) {
      widthsum = 0;
    } else {
      widthsum += canvasrelwidth[icanvas-1] * window.innerWidth;
    };
  
    mouse[icanvas].x =   ( (event.clientX - widthsum) / (canvasrelwidth[icanvas] * window.innerWidth) ) * 2 - 1;
    mouse[icanvas].y = - (  event.clientY             / (canvasrelheight[icanvas] * window.innerHeight) ) * 2 + 1;
  
    //console.log("mouse: ",icanvas,mouse[icanvas].x,mouse[icanvas].y);
  };
}

/**
 *  Function called by the event listener for mouse clicks.
 */
function onClick(event) {
  // Depending on the pick mode, pick a point by the mouse, 
  // only on the default (first) canvas:
  if (iPick > 0) {
    relativeMouseCoord(event);

    var icanvas = iCanvas0;
    
    if (    (mouse[icanvas].x >= -1) && (mouse[icanvas].x <= 1) 
         && (mouse[icanvas].y >= -1) && (mouse[icanvas].y <= 1) ) {
  
      pickPoint(icanvas);
      render(icanvas);
    };
  };
}

/**
 *  This function is called by the onload event so it will run after the
 *  page has loaded.  It creates the renderer, canvas, and scene objects,
 *  calls createWorld() to add objects to the scene, and renders the
 *  initial view of the scene.  If an error occurs, it is reported.
 */
function init() {
  var initFunctionTiming = performance.now();
  
  // create the necessary html elements, if not existing:
  //
  var info = document.getElementById("info");
  if (info == null) {
    info = document.createElement("div");
    info.setAttribute("id","info");
    info.setAttribute("class","info");
    document.body.appendChild(info);
  };
        
  // <div> elements within the info <div> element:
  var divelements = ["buttonrow", "longtitle", "camera", "move", "timestep", "face", "message"];
  var idiv, divelement;
  for (idiv = 0; idiv < divelements.length; idiv += 1) {
    divelement = document.getElementById(divelements[idiv]);
    if (divelement == null) {
      divelement = document.createElement("div");
      divelement.setAttribute("id",divelements[idiv]);
      info.appendChild(divelement);
     };
  };

  // the canvasses <div> element will contain all canvas elements:
  var canvasses = document.getElementById("canvasses");
  if (canvasses == null) {
    canvasses = document.createElement("div");
    canvasses.setAttribute("id","canvasses");
    document.body.appendChild(canvasses);
  };
  
  // check the user input (in global variable data) for how many canvasses:
  //
  if (! Array.isArray(data)) {
    // data is given as an record presumably.
    // convert it to an array of one element:
    var data0 = data;
    data = [];
    data[0] = data0;
  };
  nCanvas = data.length;
  
  if (nCanvas == 0) {

    document.getElementById("message").innerHTML += "No data are provided.";
    
  } else {
    
    // define the arrangement of the canvasses:
    //
    //   FIXME: for now, we accept only the following two configurations
    //          in future, also separate canvasses on the html page?
    //
    if (nCanvas == 1) {
      canvasrelwidth = [1];
      canvasrelheight = [1];
    } else if (nCanvas == 2) {
      // two canvases side-by-side, the second is on the right side and smaller:
      canvasrelwidth = [0.8, 0.2];
      canvasrelheight = [1, 1];
    } else {
      document.getElementById("message").innerHTML += "Sorry, the program accepts maximally two canvasses, provisionally.";
      return;
    };

    // create the html canvas element(s) 
    // and subsequently create the webgl renderer(s),
    // camera(s) and scene(s):
    //
    var canvas;
    var icanvas;
    for (icanvas = 0; icanvas < nCanvas; icanvas++) {
      
      // try to create a canvas (within the <div> element):
      //
      canvas = document.createElement("canvas");
      if ( (! canvas) || (! canvas.getContext) ) {
        document.getElementById("message").innerHTML += "Sorry, your browser doesn't support canvas graphics.";
        return;
      } else {
        canvas.setAttribute("id","canvas" + String(icanvas));
        canvasses.appendChild(canvas);
      };

      // try to create a THREE.WebGLRenderer:
      //
      if (window.WebGLRenderingContext) {
        renderer[icanvas] = new THREE.WebGLRenderer( { canvas: canvas, antialias: true } );
      } else {
        document.getElementById("message").innerHTML += "Sorry, your browser doesn't support WebGL graphics.";
        return;
      };

      renderer[icanvas].setPixelRatio( window.devicePixelRatio );

      renderer[icanvas].setSize( canvasrelwidth[icanvas] * window.innerWidth, 
                                 canvasrelheight[icanvas] * window.innerHeight );
      
      renderer[icanvas].setClearColor(0x220033); // dark violet background

      // stereo effect is only applied on the default (first) canvas:
      if (icanvas == iCanvas0) {
        effect = new THREE.StereoEffect( renderer[icanvas] );
        effect.setSize( canvasrelwidth[icanvas] * window.innerWidth, 
                        canvasrelheight[icanvas] * window.innerHeight );
      };
      
      // create the THREE.Scene object:
      //
      scene[icanvas] = new THREE.Scene();

      // create the camera:
      //
      camera[icanvas] = new THREE.PerspectiveCamera(45, 
                                                    canvasrelwidth[icanvas] * window.innerWidth /
                                                    ( canvasrelheight[icanvas] * window.innerHeight ), 
                                                    0.1, 
                                                    2000);
      camera[icanvas].position.z = zcaminit;

      // create the controls for the camera of the default (first) canvas:
      //
      if (icanvas == iCanvas0) {
        controls[icanvas] = new THREE.OrbitControls(camera[icanvas], canvas);
        controls[icanvas].target.set(0, 0, 0);
        controls[icanvas].update();
      };
       
      // get the mouse coordinates (needed for the raycaster):
      //
      mouse[icanvas] = new THREE.Vector2();
      if (icanvas == 0) {
        window.addEventListener("click", onClick, false);
      };

      // create the raycaster (for point picking by the mouse):
      //
      raycaster[icanvas] = new THREE.Raycaster();
      raycaster[icanvas].params.Line.threshold = 0.001; 
       
      // create the button row, by which mainly the default (first) canvas is controlled:
      //
      if (icanvas == iCanvas0) {
        toggleButtonRow();
      };
      
      // measure window size and page aspect after creation of the button row (!):
      //
      detectWindowChange();
      page.changed = false;

      // create all geometries given by the user input:
      //
      createGeometries(icanvas);

      // finaly, apply for the default (first) canvas:
      //
      if (icanvas == iCanvas0) {
        // create the button row:
        //toggleButtonRow();
      
        controls[icanvas].addEventListener('change', render_iCanvas0);
        controls[icanvas].enableKeys = false;

        document.addEventListener("keydown", handleKeyStroke, false);
        
        // unfortunately, interference with OrbitControls.js:
        // the function stopOrbitTour() is called by the eventlistener:
        // -- the animation is not stopped, but
        // -- the value of showingOrbitTour is changed  
        //document.addEventListener("touchstart", stopOrbitTour, false);
        //document.addEventListener("touchend", stopOrbitTour, false);
        
        window.addEventListener("resize", onWindowResize, false);
        
        /*
        if (window.DeviceOrientationEvent) {
          window.addEventListener("deviceorientation", handleDeviceOrientation, true);
        };
        */
      };
    };
    
    // render all the canvasses for the first time:
    //
    renderAll(true);
  };

  initFunctionTiming = performance.now() - initFunctionTiming;

  // print some timings:
  //
  console.log("web page load took " 
              + String(window.performance.timing.domComplete - window.performance.timing.navigationStart) 
              + " ms.");
  console.log("reading variable data took " + String(dataReadTiming) + " ms (not reliable ???).");
  console.log("init() function took " + String(initFunctionTiming) + " ms.");
  
  console.log("end of function init(). ===============================================================");
}
