function geo_complete = hgeo_to_html(geo,fn,outputformat,htmltemplate);

% Function hgeo_to_html(geo,fn)
%          hgeo_to_html(geo,fn,outputformat)
%          hgeo_to_html(geo,fn,outputformat,htmltemplate)
%          geo_complete = hgeo_to_html(geo,....)
% 
% writes the geometric data given by struct geo into a html/js file 
% with name fn.
% 
% The html/js file is based on the template file hgeo_example.html 
% residing in one of the Octave load paths (normally, the template
% file resides in the same directory as the m-file of this function).
% If another template file has to be used, its name is given by the
% fourth input argument htmltemplate.
% 
% The js file with javascript subroutines, hgeo_three_bundle.js, is 
% needed in companion with the html/js file for the web browser to
% show the given geometry and interact with it. The js file will be 
% written in the same directory as the html/js file, due to an internal
% call to function hgeo_collect_js_scripts().
% 
% The first input argument contains typically the struct geo, but instead,
% it can contain a row cellarray of two structs, {geo1, geo2}, when
% two geometries are to be shown next to each other in the web browser
% (e.g. the geometry proper and the legend).
% 
% Struct geo is organised in the following way:
% 
%    geo.shorttitle (optional) : a string for the window title (i.e. the 
%                   contents of the html <title> tag). 
% 
%    geo.longtitle (optional) : a string with a longer description printed 
%                   at the top of the page. 
% 
%    geo.camerastations (optional) : a cellarray of nC rows and 10 columns.
%                   Each row contains a steady camera position, i.e. x,y,z position
%                   of the camera, the x,y,z upright direction of the camera, 
%                   the x,y,z position of the focus point, and a matrix cp, 
%                   respectively. Matrix cp is empty, or contains data on 
%                   one or more clipping planes; matrix cp has 6 columns for
%                   the x,y,z components of the plane normal (pointing into
%                   the space to be clipped away) and the x,y,z components of
%                   a position on the plane, repectively.
% 
%                   For backward compatibility, geo.camerastations may consist
%                   of a matrix of nC rows and 9 columns (and no clipping planes
%                   can thus be defined).
% 
%    geo.camerastationnames (optional) : a cellarray of nC rows and 1 column of
%                   strings. Each row contains a name or description of the 
%                   camera station given to the same row in geo.camerastations.
% 
%    geo.cameratour (optional) : a matrix of nC rows and 9 columns.
%                   Each row contains a moment in the tour, i.e. x,y,z position
%                   of the camera, the x,y,z upright direction of the camera, 
%                   and the x,y,z position of the focus point, respectively.
% 
%    geo.cameraorbitmodes (optional) : a record with data on settings for 
%                   different orbit modes. See into the code of hgeo_subroutines.js
%                   for explanation.
% 
%    geo.colors (necessary) : a matrix of nC rows and 3 columns. At each
%                   row a color is defined by its red, green, blue channels (with 
%                   values from 0 to 1).
% 
%                   When the first input argument consists of a cellarray of
%                   structs {geo1, geo2}, one may omit geo2.colors. In that case
%                   the colors of the first geometry, geo1.colors, are used for 
%                   the second one too.
% 
%    geo.stepnames (optional) : a cellarray of nS rows and 1 column of strings,
%                   Each row contains a name or description of the time step.
% 
%    geo.facepoints (optional) : a matrix of nFP rows and 3 columns. 
%                   Each row contains the x,y,z coordinates of a point.
% 
%    geo.facepointnames (optional) : a cellarray with nFP rows and 1 column of strings.
%                   Each row contains a name or description of the point 
%                   given on the same row in geo.facepoints.
% 
%    geo.faces (optional) : a matrix of nF rows and 4 columns. 
%                   Each row contains either four indices to rows in geo.facepoints, 
%                   by which the points of a quadrangle are defined, or three indices 
%                   (to rows in geo.facepoints) and zero or NaN, by which the points
%                   of a triangle is defined. Points are listed counter-clockwise 
%                   as seen from a point away from the plane of the face in the 
%                   direction of the normal of the plane.
% 
%    geo.facecolors (optional) : a matrix of nF rows and 1 or nS columns.
%                   Each row contains one or more indices to rows in geo.colors, 
%                   and/or 0 or NaN when the face is invisible. Each row 
%                   corresponds to the same row in geo.faces. Each column
%                   corresponds to the row in geo.stepnames with the same index.
%     
%                   If geo.facecolors is not given (or if it is empty but geo.faces
%                   is not empty), then it will be automatically produced so that 
%                   each face has its own color.
% 
%    geo.facenames (optional) : a cellarray of nF rows and 1 column of strings.
%                   Each row contains a name or description of the face 
%                   given on the same row in geo.faces.
% 
%    geo.linepoints (optional) : a matrix of nLP rows and 3 columns.
%                   Each row contains the x,y,z coordinates of a point.
% 
%    geo.lines (optional) : a matrix of nL rows and 2 columns; 
%                   Each row contains two indices to rows in geo.linepoints, by
%                   which the begin and end points of a line segment are defined.
% 
%    geo.linecolors (optional) : a matrix of nL rows and 1 or nS columns.
%                   Each row contains one or more indices to rows in geo.colors, 
%                   and/or 0 or NaN when the line is invisible. Each row 
%                   corresponds to the same row in geo.lines. Each column
%                   corresponds to the row in geo.stepnames with the same index.
% 
%    geo.texts (optional) : a cellarray of nT rows and 4 columns.
%                   Each row contains the x,y,z coordinates of the anchor of a 
%                   text block, and a text string.
% 
%    geo.textcode (optional) : a string indicating the type of coding used for 
%                   the 4th column in geo.text. Possible values are:
%                     geo.textcode = "latex" (default)
%                     geo.textcode = "circumflex"
%                   The latter coding is the native coding:
%                     ^U...^E  superscript
%                     ^D...^E  subscript
%                     ^S...^E  slanted (italic)
%                     ^C...^E  character name; e.g. aunknown, ahere<-, ahere->, a<-, a->, acircum
%                     ^N       new line
%                     ^Oxy     y is overwritten by x
%                     ^Axy     diacritic x is put above or below y; x is to be replaced by:
%                                 a  for a acute 
%                                 g  for a grave
%                                 e  for a circumflex
%                                 U  for an umlaut or diaeresis
%                                 t  for a tilde
%                                 l  for a macron
%                                 p  for a dot above
%                                 u  for a breve
%                                 v  for a caron
%                                 H  for a double acute
%                                 c  for a cedilla
%                                 d  for a dot below
%                                 b  for a line below
%                                 r  for a ring above
%                                 x  for a stroke through
%                                 k  for a ogonek
%                   Combinations are possible, e.g.:
%                     ^O^Cacircum^E3   the digit 3 in a circle
%                     ^Aa^Agu          the lower-case letter u with a grave and an acute on top
% 
%                   If the latex coding is applied, the code of the texts is converted into
%                   the circumflex coding by function convert_to_circumflex(). 
%                   Unfortunately, the convertion is not complete (yet)! For the moment, 
%                   only super/subscripting and new lines are converted.
% 
%    geo.textcolors (optional) : a matrix of nT rows and 1 or nS columns.
%                   Each row contains one or more indices to rows in geo.textproperties, 
%                   and/or 0 or NaN when the text is invisible. Each row 
%                   corresponds to the same row in geo.texts. Each column
%                   corresponds to the row in geo.stepnames with the same index.
% 
%    geo.textproperties (optional) : a column cellarray, in which each element contains 
%                   either one row cellarray of property-value pairs, or a struct. These 
%                   property-value pairs must be given: 
%                   "v", [1, 0, 0] : 3-d direction of the baseline of the text
%                   "w", [0, 1, 0] : 3-d direction of the vertical of the text
%                   "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)
%                   The following property-value pairs 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 values of the fields of geo are not checked for mistakes! 
% 
% Note e.g. that geo.faces depends on geo.facepoints (because an integer in 
% geo.faces is the row index in geo.facepoints), and geo.facecolors depends on 
% geo.faces, geo.stepnames and geo.colors (because each row in geo.facecolors 
% corresponds to the same row in geo.faces, because each column in geo.facecolors 
% corresponds to the row in geo.stepnames, and because an integer in 
% geo.facecolors is the row index in geo.colors). The same principle applies to 
% the group of geo.linepoints, geo.lines, geo.linecolors, geo.stepnames and 
% geo.colors, and to the group of geo.texts, geo.textcolors, geo.stepnames and 
% geo.textproperties.
% 
% The output is a html/js file. It can be loaded into a browser capable of webgl 
% and javascript. The file with javascript subroutines, hgeo_three_bundle.js, 
% should be available (residing in the same directory as the html/js file), and it 
% contains javascript libraries three.js, OrbitControls.js and StereoEffect.js 
% together with dedicated subroutines. 
% 
% If fn is empty, no html/js file is written. The filename must not collide
% with hgeo_example.html nor with the filename given by input argument 
% htmltemplate.
% 
% If geo_complete is given as output argument, it will contain the completed
% version of struct geo, i.e. all the missing fields (optional or not) are 
% added, and some fields are converted or reorganised.
% 
% In the html/js file, the (completed) struct geo will be written as a
% javascript object. Optionally, its format can be controlled 
% by the third input argument outputformat, which may have one
% of the following values:
%   1 : in verbose javascript (by a call to a dedicated internal function).
%   2 : in compact javascript (by a call to the general function value2json()).
%   3 : as a json-string argument within JSON.parse("....")
%   4 : as a multiline json-string argument within JSON.parse(`....`)
%   5 : as a multiline json-string argument within JSON.parse("....")
% At default, outputformat = 5.
% 
% Internally, the following non-standard Octave m-files are called:
% -- value2json.m,
% -- convert_to_circumflex.m,
% -- hgeo_collect_js_scripts.m.
% 
% 
% (c) 2026 fabien van mook
% 
% 2026.08.20 release of this file within package "fvm-hgeo" under GNU GPLv3+

% This program is free software: you can redistribute it
% and/or modify it under the terms of the GNU General Public
% License as published by the Free Software Foundation, either
% version 3 of the License, or (at your option) any later
% version.
% 
% This program is distributed in the hope that it will be
% useful, but WITHOUT ANY WARRANTY; without even the implied
% warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
% PURPOSE. See the GNU General Public License for more
% details.
% 
% You can read the GNU General Public License on
% <https://www.gnu.org/copyleft/gpl.html>.


defaulthtmltemplate = "hgeo_example.html";

if (nargin < 2),
  fn = "";
endif;

[fn_d,fn_n,fn_e] = fileparts(fn); % returns [directory,name,extension]
fn_b = [fn_n fn_e];
if (length(fn_d) > 0),
  fn_d = [fn_d filesep];
endif;

if ( (nargin < 3) || isempty(outputformat) ),
  outputformat = 5;
endif;

if ( (nargin < 4) || isempty(htmltemplate) ),
  htmltemplate = defaulthtmltemplate;
endif;

[htmltemplate_d,htmltemplate_n,htmltemplate_e] = fileparts(htmltemplate);
htmltemplate_b = [htmltemplate_n htmltemplate_e];

% whether geo is a struct or a row cell array of struct's:
%  (a cell array of struct's is _not_ the same as a struct(ure) array,
%   see the help text of struct().)
isarray = iscell(geo);

if (~ isarray),
  % convert the struct into a row cell array of one struct temporarily:
  geo = {geo};
endif;

ngeo = length(geo);

% we add the optional members, if they were not given:

for igeo = [1:ngeo],

  if (! isfield(geo{igeo},"shorttitle") ),
    geo{igeo}.shorttitle = "";
  endif;

  if (! isfield(geo{igeo},"longtitle") ),
    geo{igeo}.longtitle = "";
  endif;

  if (! isfield(geo{igeo},"camerastations") ),
    geo{igeo}.camerastations = {};
  endif;
  
  % for backward compatibility:
  if (! iscell(geo{igeo}.camerastations)),
    geo{igeo}.camerastations = num2cell(geo{igeo}.camerastations);
  endif;
  if (size(geo{igeo}.camerastations,2) != 10),
    for ir = [1:size(geo{igeo}.camerastations,1)],
      geo{igeo}.camerastations{ir,10} = [];
    endfor;
  endif;

  if (! isfield(geo{igeo},"camerastationnames") ),
    geo{igeo}.camerastationnames = {};
  endif;
  
  if (! isfield(geo{igeo},"cameratour") ),
    geo{igeo}.cameratour = [];
  endif;

  if (! isfield(geo{igeo},"colors") ),
    igeocolors = 1; % at default, the color list is in igeo = 1
    
    if (igeo == igeocolors),
      geo{igeo}.colors = [];
    endif;
  else,
    igeocolors = igeo;
  endif;
  
  if (! isfield(geo{igeo},"stepnames") ),
    geo{igeo}.stepnames = {""};
  endif;

  if (! isfield(geo{igeo},"facepoints") ),
    geo{igeo}.facepoints = [];
  endif;
  
  if (! isfield(geo{igeo},"facepointnames") ),
    geo{igeo}.facepointnames = {};
  endif;

  if (! isfield(geo{igeo},"faces") ),
    geo{igeo}.faces = [];
  endif;

  if (! isfield(geo{igeo},"facecolors") ),
    geo{igeo}.facecolors = [];
  endif;

  nfaces = size(geo{igeo}.faces,1);
  if ((nfaces > 0) && (size(geo{igeo}.facecolors,1) == 0)),
    % thus, faces are defined but do not have colors yet.
    ncolors = size(geo{igeocolors}.colors,1);
    geo{igeocolors}.colors = [geo{igeocolors}.colors; jet(nfaces)];
    geo{igeo}.facecolors = ncolors + [1:nfaces]';
  endif; 

  if (! isfield(geo{igeo},"facenames") ),
    facenames = cellstr(char( "?"*ones(size(geo{igeo}.faces,1),1) ));
    geo{igeo}.facenames = {};
  else,
    facenames = geo{igeo}.facenames;
  endif;

  if (! isfield(geo{igeo},"facecolorscolumnlength") ),
    geo{igeo}.facecolorscolumnlength = size(geo{igeo}.facecolors,2);
    if (geo{igeo}.facecolorscolumnlength < 1),
      geo{igeo}.facecolorscolumnlength = 1;
    endif;
  endif;

  if (! isfield(geo{igeo},"linepoints") ),
    geo{igeo}.linepoints = [];
  endif;

  if (! isfield(geo{igeo},"lines") ),
    geo{igeo}.lines = [];
  endif;

  if (! isfield(geo{igeo},"linecolors") ),
    geo{igeo}.linecolors = [];
  endif;

  if (! isfield(geo{igeo},"linecolorscolumnlength") ),
    geo{igeo}.linecolorscolumnlength = size(geo{igeo}.linecolors,2);
    if (geo{igeo}.linecolorscolumnlength < 1),
      geo{igeo}.linecolorscolumnlength = 1;
    endif;
  endif;

  if (! isfield(geo{igeo},"texts") ),
    geo{igeo}.texts = {};
  endif;

  if (! isfield(geo{igeo},"textcode") ),
    geo{igeo}.textcode = "latex";
  endif;

  if (! isfield(geo{igeo},"textcolors") ),
    geo{igeo}.textcolors = [];
  endif;

  if (! isfield(geo{igeo},"textcolorscolumnlength") ),
    geo{igeo}.textcolorscolumnlength = size(geo{igeo}.textcolors,2);
    if (geo{igeo}.textcolorscolumnlength < 1),
      geo{igeo}.textcolorscolumnlength = 1;
    endif;
  endif;

  if (! isfield(geo{igeo},"textproperties") ),
    geo{igeo}.textproperties = {};
  endif;

endfor;

% geo is now complete with all the fields (optional or not):
if (isarray),
  geo_complete = geo;
else,
  % convert back into a struct:
  geo_complete = struct();
  geo_complete = geo{1};
endif;

% translate octave structure geo into a string with a json-like format:

for igeo = [1:ngeo],

  % firstly, face indices and color indices may be NaN, but should be zero:
  geo{igeo}.faces = setnantozero(geo{igeo}.faces);
  geo{igeo}.facecolors = setnantozero(geo{igeo}.facecolors);
  geo{igeo}.linecolors = setnantozero(geo{igeo}.linecolors);
  geo{igeo}.textcolors = setnantozero(geo{igeo}.textcolors);

  % secondly, indices in octave start at 1, whereas in javascript with 0:
  geo{igeo}.faces = settozerobased(geo{igeo}.faces);
  geo{igeo}.facecolors = settozerobased(geo{igeo}.facecolors);
  geo{igeo}.lines = settozerobased(geo{igeo}.lines);
  geo{igeo}.linecolors = settozerobased(geo{igeo}.linecolors);
  geo{igeo}.textcolors = settozerobased(geo{igeo}.textcolors);

endfor;

% string for javascript data in json-like format:
js = "";

if (outputformat == 1),
  % output in verbose javascript:
  
  if (isarray),
    js = [js "[\n"];
    for igeo = [1:ngeo],
      js = [js "// data for icanvas=" num2str(igeo-1) ":\n"];
      js = [js convert_to_js(geo{igeo})];
      if (igeo != ngeo),
        js = [js ",\n"];
      else,
        js = [js "\n"];
      endif;
    endfor;
    js = [js "]\n"];
  else,
    js = [js convert_to_js(geo{1}) "\n"];
  endif;
    
else,
  % output generated by value2json():
  
  opt = struct();
  opt.pre1 = "";
  opt.pre2 = "";
  opt.indent = 0;
  opt.two_to_one = 1;

  % convert the values of some fields in geo:
  for igeo = [1:ngeo],
  
    % remove data not used:
    geo{igeo}.facepointnames = {};

    % column cell array into row cell array:
    geo{igeo}.camerastationnames = geo{igeo}.camerastationnames';
    geo{igeo}.facenames = geo{igeo}.facenames';
    geo{igeo}.stepnames = geo{igeo}.stepnames';
    geo{igeo}.textproperties = geo{igeo}.textproperties';
  
    % convert textproperties (as key-value cell array) to struct,
    % if necessary:
    for k = [1:size(geo{igeo}.textproperties,2)],
      tp = geo{igeo}.textproperties{1,k};
      if iscell(tp),
        rtp = struct();
        for ip = [1:2:length(tp)],
          rtp.(tp{ip}) = tp{ip+1};
        endfor;
        geo{igeo}.textproperties{1,k} = rtp;
      endif;
    endfor;
    
    % convert text strings to circumflex notation:
    for k = [1:size(geo{igeo}.texts,1)],
      geo{igeo}.texts{k,4} = convert_to_circumflex(geo{igeo}.texts{k,4},geo{igeo}.textcode);
    endfor;
    
  endfor;

  if (outputformat == 2),
    % output in non-verbose javascript:
  
    if (isarray),
      js = [js value2json(geo,opt)];
    else,
      js = [js value2json(geo{1},opt)];
    endif;

  elseif (outputformat == 3),
    % output as single-line json-string argument within JSON.parse("...."):
    
    js0 = value2json(geo,opt);
    js0 = strrep(js0,"\n","");
    js0 = undo_string_escapes(js0);
    js = [js "JSON.parse(\"" js0 "\")"]; % using "..." (single-line string between double quotes)

  elseif (outputformat == 4),
    % output as multiline json-string argument within JSON.parse(`....`):

    js0 = value2json(geo,opt);
    js0 = strrep(js0,"\n","<__nl__>");
    js0 = undo_string_escapes(js0);
    js0 = strrep(js0,"<__nl__>","\n");
    js = [js "JSON.parse(\`" js0 "\`)"]; % using `...` (multiline string between backticks)

  elseif (outputformat == 5),
    % output as multiline json-string argument within JSON.parse("...."):

    js0 = value2json(geo,opt);
    js0 = strrep(js0,"\n","<__nl__>");
    js0 = undo_string_escapes(js0);
    js0 = strrep(js0,"<__nl__>","\\\n");
    js = [js "JSON.parse(\"" js0 "\")"]; % using "..." (multiline string between double quotes)

  endif;

endif;

if ( (length(fn) > 0) && (! strcmp(fn_b,htmltemplate_b)) && (! strcmp(fn_b,defaulthtmltemplate)) ),

  % composing the html file from the template:
  s = fileread(file_in_loadpath(htmltemplate));
  
  % replace links to js library files by the one bundle file "hgeo_three_bundle.js":
  t1 = "<!-- js_libraries_begin_here -->";
  t2 = "<!-- js_libraries_end_here -->";
  it1 = index(s,t1);
  it2 = index(s,t2);
  if ( (it1 > 0) && (it2 > 0) ),
    s = [s(1:(it1+length(t1))) "<script type=\"text/javascript\" src=\"hgeo_three_bundle.js\"></script>\n" s((it2-1):end)];
  endif;

  % remove "history_begins_here ... history_ends_here":
  t1 = "// history_begins_here";
  t2 = "// history_ends_here";
  it1 = index(s,t1);
  it2 = index(s,t2);
  if ( (it1 > 0) && (it2 > 0) ),
    s = [s(1:(it1-1)) s((it2+length(t2)):end)];
  endif;
  
  % replace the contents of variable data:
  t1 = "// data_begins_here";
  t2 = "// data_ends_here";
  it1 = index(s,t1);
  it2 = index(s,t2);
  if ( (it1 > 0) && (it2 > 0) ),
    s = [s(1:(it1+length(t1))) "\n  data = " js ";\n" s((it2-1):end)];
  endif;

  % write the html file:
  filewrite(fn,s);
  
  % bundling the js script files into "hgeo_three_bundle.js", if necessary:
  hgeo_collect_js_scripts(fn_d);
  
endif;

endfunction;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function y = setnantozero(x);

y = x;

for ic = [1:size(x,2)],
  ir = find( isnan(x(:,ic)) );
  if (length(ir) > 0),
    y(ir,ic) = 0;
  endif;
endfor;

endfunction;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function y = settozerobased(x);

if (! isempty(x)),
  y = x - 1;
else,
  y = x;
endif;

endfunction;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function js = convert_to_js(geo);

% geo is a struct, not a cellarray of struct's

js = "{\n";

js = [js "  shorttitle : \"" geo.shorttitle "\", // a string for the window title (i.e. the contents of the html <title> tag)"];

js = [js "\n  longtitle : \"" geo.longtitle "\", // a string with a longer description printed at the top of the page"];

if isempty(geo.camerastationnames),
  camerastationnames = cellstr(char( "?"*ones(size(geo.camerastations,1),1) ));
else,
  camerastationnames = geo.camerastationnames;
endif;

js = [js "\n  camerastations : [ // each row contains a steady camera position, i.e. x,y,z camera position, x,y,z camera up direction, x,y,z focus point and an array for clipping planes (defined by x,y,z normal and x,y,z position)\n"];
for k = [1:size(geo.camerastations,1)],
  if (size(geo.camerastations{k,10},1) == 0),
    cp = "[]";
  else,
    cp = ["[" sprintf("%e, ",ctranspose(geo.camerastations{k,10})) "]"];
  endif;
  js = [js sprintf("   %e, %e, %e,   %e, %e, %e,   %e, %e, %e,   %s, // station %3d with name \"%s\"\n",geo.camerastations(k,1:9){:},cp,k-1,camerastationnames{k,1})];
endfor;

js = [js "   ],\n  camerastationnames : [ // names or descriptions for each camera station\n"];
for k = [1:size(geo.camerastationnames,1)],
  js = [js sprintf("   \"%s\", // station %3d\n",geo.camerastationnames{k,1},k-1)];
endfor;

js = [js "   ],\n  cameratour : [ // each row contains a moment in the tour, i.e. x,y,z camera position, x,y,z camera up direction and x,y,z focus point\n"];
for k = [1:size(geo.cameratour,1)],
  js = [js sprintf("   %e, %e, %e,   %e, %e, %e,   %e, %e, %e,// tour moment %3d\n",geo.cameratour(k,1:9),k-1)];
endfor;

js = [js "   ],\n"];

if isfield(geo,"cameraorbitmodes"),
  opt = struct();
  opt.pre1 = "";
  opt.pre2 = "   ";
  opt.two_to_one = 1;
  js = [js "  cameraorbitmodes : " value2json(geo.cameraorbitmodes,opt) ",\n"];
endif;

if isfield(geo,"colors"),
  js = [js "  colors : [ // at each row, a color by its red,green,blue channels (with values from 0 to 1) is defined\n"];
  for k = [1:size(geo.colors,1)], 
    js = [js sprintf("   %5.4f, %5.4f, %5.4f, // color %3d\n",geo.colors(k,1:3),k-1)];
  endfor;
  js = [js "   ],\n"];
endif;

js = [js "  stepnames : [ // names or descriptions for each time step\n"];
for k = [1:size(geo.stepnames,1)],
  js = [js sprintf("   \"%s\", // step %3d\n",geo.stepnames{k,1},k-1)];
endfor;

if isempty(geo.facepointnames),
  facepointnames = cellstr(char( "?"*ones(size(geo.facepoints,1),1) ));
else,
  facepointnames = geo.facepointnames;
endif;

js = [js "   ],\n  facepoints : [ // each row contains the x,y,z coordinates of a point\n"];
for k = [1:size(geo.facepoints,1)],
  js = [js sprintf("   %e, %e, %e, // point %3d with name \"%s\"\n",geo.facepoints(k,1:3),k-1,facepointnames{k,1})];
endfor;

if isempty(geo.facenames),
  facenames = cellstr(char( "?"*ones(size(geo.faces,1),1) ));
else,
  facenames = geo.facenames;
endif;

js = [js "   ],\n  faces : [ // each row always contains four integers; each row contains the integers which refer to rows in data.facepoints, and form a quadrangle or a triangle; for a triangle the last integer in a row equals to -1; points are listed counterclockwise seen from outside\n"];
for k = [1:size(geo.faces,1)],
  js = [js sprintf("   %3d, %3d, %3d, %3d, // face %3d with name \"%s\"\n",geo.faces(k,1:4),k-1,facenames{k,1})];
endfor;
   
js = [js "   ],\n  facecolorscolumnlength : " num2str(geo.facecolorscolumnlength) ", // its value equals to 1 or the number of items in data.stepnames\n"];

js = [js "   \n  facecolors : [ // indices to row in data.colors (or -1 if invisible); each row corresponds to the same row in data.faces; there is 1 column or each column corresponds to item in data.stepnames\n"];
for k = [1:size(geo.facecolors,1)],
  js = [js "  " sprintf(" %3d,", geo.facecolors(k,:)) sprintf(" // face %3d with name \"%s\"\n",k-1,facenames{k,1})];
endfor;

js = [js "   ],\n  facenames : [ // string with face name\n"];
for k = [1:size(geo.facenames,1)], 
  js = [js sprintf("   \"%s\", // face %3d\n",geo.facenames{k,1},k-1)];
endfor;

js = [js "   ],\n  linepoints : [ // each row contains the x,y,z coordinates of a point\n"];
for k = [1:size(geo.linepoints,1)],
  js = [js sprintf("   %e, %e, %e, // point %3d\n",geo.linepoints(k,1:3),k-1)];
endfor;

js = [js "   ],\n  lines : [ // each row always contains two integers which refer to rows in data.linepoints and form a line segment\n"];
for k = [1:size(geo.lines,1)],
  js = [js sprintf("   %3d, %3d, // line %3d\n",geo.lines(k,1:2),k-1)];
endfor;

js = [js "   ],\n  linecolorscolumnlength : " num2str(geo.linecolorscolumnlength) ", // its value equals to 1 or the number of items in data.stepnames\n"];

js = [js "   \n  linecolors : [ // indices to row in data.colors (or -1 if invisible); each row corresponds to the same row in data.lines; there is 1 column or each column corresponds to item in data.stepnames\n"];
for k = [1:size(geo.linecolors,1)],
  js = [js "  " sprintf(" %3d,", geo.linecolors(k,:)) sprintf(" // line %3d\n",k-1)];
endfor;

js = [js "   ],\n  texts : [ // each row contains the x,y,z coordinates of the anchor of a text block, and a text string\n"];
for k = [1:size(geo.texts,1)],
  js = [js sprintf("   %e, %e, %e, \"%s\", // text %3d\n",geo.texts{k,1},geo.texts{k,2},geo.texts{k,3},convert_to_circumflex(geo.texts{k,4},geo.textcode),k-1)];
endfor;

js = [js "   ],\n  textcolorscolumnlength : " num2str(geo.textcolorscolumnlength) ", // its value equals to 1 or the number of items in data.stepnames\n"];

js = [js "   \n  textcolors : [ // indices to row in data.textproperties (or -1 if invisible); each row corresponds to the same row in data.text; there is 1 column or each column corresponds to item in data.stepnames\n"];
for k = [1:size(geo.textcolors,1)],
  js = [js "  " sprintf(" %3d,", geo.textcolors(k,:)) sprintf(" // text %3d\n",k-1)];
endfor;

js = [js "   ],\n   textproperties : [ // an array of records\n"];
for k = [1:size(geo.textproperties,1)],
  tp = geo.textproperties{k,1};
  if istruct(tp),
    % convert the struct to a row cell array of key-value pairs:
    ctp = {};
    for [v,k] = tp,
      ctp(1,end+[1 2]) = {k,v};
    endfor;
  else,
    % a row cell array of key-value pairs is assumed:
    ctp = tp;
  endif;
  js = [js sprintf("      { // color %3d\n",k-1)];
  for ip = [1:2:length(ctp)],
    if (    strcmp(ctp{ip},"v") ...
         || strcmp(ctp{ip},"w") ...
         || strcmp(ctp{ip},"color") ),
      js = [js sprintf("        %s: [%e, %e, %e],\n",ctp{ip},ctp{ip+1})];
    elseif (    strcmp(ctp{ip},"xheight") ...
             || strcmp(ctp{ip},"xwidthoverxheight") ...
             || strcmp(ctp{ip},"slant") ),
      js = [js sprintf("        %s: %e,\n",ctp{ip},ctp{ip+1})];
    elseif (    strcmp(ctp{ip},"lineskipconstant") ...
             || strcmp(ctp{ip},"includedylinedown") ),
      js = [js sprintf("        %s: %d,\n",ctp{ip},ctp{ip+1})];
    else,
      js = [js sprintf("        %s: \"%s\",\n",ctp{ip},ctp{ip+1})];
    endif;   
  endfor;
  js = [js sprintf("      },\n")];
endfor;

%js = [js "   ],\n   textproperties : [ // an array of records\n" ...
%         "      { // color 0\n" ...
%         "        v: [1, 0, 0],\n" ...
%         "        w: [0, 0, 1],\n" ...
%         "        xheight: 1, // height of lower-case letter x in absolute length units\n" ...
%         "        xwidthoverxheight: 1, // ratio of width to height of lower-case letter x\n" ...
%         "        color: [1, 1, 1], // r,g,b channels (value from 0 to 1)\n" ...
%         "        anchorvertical: "firstbaseline", // position of the text block anchor along its vertical; possible values are: top, halfxheightabovefirstbaseline, firstbaseline, centre, lastbaseline, bottom\n" ...
%         "        anchorhorizontal: "left", // position of the text block anchor along its horizontal (i.e. baselines); possible values are: left, centre, right\n" ...
%         "        alignment: "left", // horizontal alignment of lines within the text block; possible values are: left, centre, right\n" ...
%         "      },\n" ];

js = [js "   ],\n"];

js = [js "}"];

endfunction;

