##  Copyright (C) 2014  Daniel Kraft <d@domob.eu>
##  GNU Octave level-set package.
##
##  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 should have received a copy of the GNU General Public License
##  along with this program.  If not, see <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn  {Function File} {@var{geom} =} ls_find_geometry (@var{phi}, @var{h} = 1, @var{fractol} = 1e-3)
## 
## Interpret the level-set function @var{phi} and extract geometrical
## properties.  A rectangular grid with spacing @var{h} uniform in each
## dimension is assumed.  This function assumes the 2D situation.
##
## The function assumes that @var{phi} does not contain values which are
## exact zeros.  It is a good idea to use @code{ls_normalise} before
## calling @code{ls_find_geometry}.
## Intersection points with edges very
## close to a grid point may be pushed away from it, governed
## by @var{fractol}.
##
## The following terms will be used to denote certain elements
## of the geometrical description returned by this function:
##
## @table @dfn
## @item node
## A point on the grid.  @var{phi} is the value of the level-set function
## at each node.  They are numbered from 1 to @var{l} x @var{m} in the
## same way as the entries in the matrix @var{phi} are numbered.
##
## @item element
## One rectangular cell of the grid.  Since we work in 2D,
## each element is made up
## of four nodes (which are the vertices of the rectangle).  If the grid
## has @var{l} x @var{m} nodes, then it contains (@var{l} - 1) x (@var{m} - 1)
## elements.
##
## @item inner element
## An element for which each node is inside the described domain.
##
## @item outer element
## An element for which each node is outside of the described domain.
##
## @item boundary element
## An element which has both inside and outside nodes.  These elements are
## intersected by the boundary of the domain and have intersection points
## on some of their edges.
##
## It can be the case that it has two intersection
## points (when one boundary edge intersects it) or four (if we have
## a ``narrow pair'').  The latter case is that of @emph{two} intersecting
## (parallel) boundary edges, which happens when diagonally opposing nodes
## of the element have the same sign of @var{phi}.
##
## A narrow pair is ambiguous as to which intersection points should be
## connected together.  This function is implemented such that it considers
## the ``narrow channel'' between the parallel boundary edges to be
## @strong{outside} of the domain.  In other words, at a narrow pair, the
## domain is considered to be disconnected instead of connected with a narrow
## channel.
##
## @item intersection point
## A point on the @emph{edge} of a boundary element where the boundary of the
## described domain (approximately) intersects the grid (i. e., @var{phi}
## has a zero).  It can be characterised by a pair of inside/outside
## nodes at each end of the edge it intersects and by the fraction of the
## side that lies inside the domain.
##
## @item gamma
## The boundary of the described domain.  I. e., the curve given by
## the zero level-set of @var{phi}.  It may consist of several components,
## which are each a closed curve.
##
## @item boundary edge
## A single edge of the domain's boundary, which is a line connecting
## two intersection points and cutting through a boundary element.
## It is ordered in such a way that the domain is on the left, if the
## grid is interpreted as coordinate system.  (On the right in matrix
## ordering.)
## @end table
##
## The returned variable @var{geom} will be a struct with these
## fields:
##
## @table @code
## @item dim
## The dimension of the grid, equal to @code{size (@var{phi})}.
##
## @item elem
## Itself a struct describing the elements of the grid.  It contains:
##
## @table @code
## @item n
## Number of elements on the grid.  Equal to (@var{l} - 1) x (@var{m} - 1)
## if @var{phi} is @var{l} x @var{m}.
##
## @item nodelist
## @code{n} x 4 matrix containing in its rows the indices of nodes
## making up each element.  According to the ordering in the matrix @var{phi},
## the nodes are stored in the order SE, SW, NW, NE.  Interpreted in a
## (x, y) grid constructed via @code{meshgrid}, the order is instead
## NE, NW, SW, SE matching the usual numbering of quadrants in the plane.
## (This is the case because the y axis points down when numbering rows
## of a matrix, but up in the usual coordinate system.)
##
## @item type
## A vector giving the type of each element.  It is -1, 0 and 1
## for inner, boundary and outer elements, respectively.  (This convention
## mimics the sign of @var{phi} somewhat.)
##
## @item index
## Itself a struct with the fields @code{inner}, @code{bdry}
## and @code{outer}, containing a list of indices for inner, boundary
## and outer elements.
##
## These equal the result of @code{find (@var{geom}.elem.type == @var{x})}
## with @var{x} being -1, 0 and 1.
## @end table
##
## @item bdryel
## A struct with information about the boundary elements.  They have their
## own index here, which can be mapped to the global element index
## via @code{@var{geom}.elem.index.bdry}.  Its fields are:
##
## @table @code
## @item n
## Number of boundary elements.
##
## @item edges
## @code{n} x 4 matrix, whose columns correspond to the four edges of
## the boundary elements.  For each edge where an intersection point lies,
## the respective matrix entry is set to the intersection point's index
## into @code{@var{geom}.ispts}.
## If there is no intersection point, the value is @code{NA}.
##
## The edges are ordered similarly to the order in
## @code{@var{geom}.elem.nodelist}.  In matrix interpretation, the
## edges are in the columns as S, W, N, E.  Interpreted as a coordinate
## grid, the order is N, W, S, E.
## @end table
##
## @item ispts
## Struct containing details about the intersection points with the fields:
##
## @table @code
## @item n
## Number of intersection points.
##
## @item inout
## @code{n} x 2 matrix containing the node indices of the inner and
## outer nodes neighbouring each intersection point (in this order).
##
## @item frac
## Fraction of the edge that is inside the domain (connecting
## the intersection point to the inner neighbour node).  This value
## is always in the range 0--1, independent of @var{h}.
##
## @item incoord
## Coordinates of the intersection points relative to the neighbouring
## inner nodes as a @code{n} x 2 matrix.  This takes @var{h} into account.
##
## @item onedge
## @code{n} x 2 x 3 array that indicates on which edges of which
## boundary elements this intersection point occurs.  The first dimension
## is indexed by intersection point number, the second dimension numbers
## the (two) boundary elements each intersection point is part of, and
## the third dimension contains the tuple (boundary element index, edge index,
## boundary edge index) in the ranges 1--@code{@var{geom}.bdryel.n}, 1--4
## and 1--@code{@var{geom}.bedges.n}.
##
## The occurances (second dimension) are ordered such that the first
## one is where the boundary edge ``enters'' the boundary element,
## while the second one is where it ``leaves'' a boundary element.
##
## @item gammachain
## @code{n} x 2 matrix that contains for each intersection point
## the next and previous intersection point indices when
## following gamma.  See also @code{@var{geom}.gamma.ispts}.
## @end table
##
## @item gamma
## Struct with information about the components of the domain's boundary.
## It contains these fields:
##
## @table @code
## @item n
## Number of gamma components.
##
## @item ispts
## Cell-array with @code{n} elements.  Each element is a list of indices
## of intersection points (i. e., indices into @code{@var{geom}.ispts}).
## In the order given, they can be joined together to form a closed curve
## (when also the last one is connected back to the first).
## @end table
##
## @item bedges
## Information about the boundary edges:
##
## @table @code
## @item n
## Number of boundary edges in total.
##
## @item comp
## A vector giving the gamma component this edge is part of.  The
## components are in the range 1--@code{@var{geom}.gamma.n}.
##
## @item ispts
## @code{n} x 2 matrix containing the intersection point indices that
## make up each boundary edge.
## @end table
##
## @item internal
## Data field with additional information for internal use, in particular
## in @code{ls_build_mesh}.  The precise format may be subject to change
## and should not be used.
## @end table
##
## @seealso{ls_absolute_geom, ls_normalise, ls_build_mesh, ls_inside}
## @end deftypefn

% Currently contained information in "internal":
%
% bdryelSegments:
%   Information about the (one or two) "segments" of each boundary element
%   that belong to the domain.  This is the polygon that will be divided
%   into triangles during mesh building, and it is a by-product of
%   locating the gamma components.  Thus it makes sense to collect the
%   information already here instead of redoing everything later.
%
%   It is a cell-array indexed by boundary element number.  Each entry
%   is itself a cell-array with one or two struct entries (for each single
%   segment).  These structs contain the fields:
%     startEdge: edge index (1--4) where the startPt intersects.
%     endEdge: edge index (1--4) where the endPt lies.
%     startPt: ispt index of the "start" of the intersecting edge.
%     endPt: ispt index of the "end" point.
%     inners: Node indices of inner points "enclosed" (one to three).
%   The corresponding gamma component piece will be [startPt, endPt],
%   and when using [startPt, inners, endPt, startPt], one gets an enumeration
%   of the polygon's vertices *in clockwise order* (with coordinate grid
%   interpretation of the matrices).

% Additional information that could be added as needed:
% - more about nodes (elements that contain a node, neighbours)
% - connected components of the domain / exterior

function geom = ls_find_geometry (phi, h = 1, fractol = 1e-3)
  if (nargin () < 1 || nargin () > 3)
    print_usage ();
  endif

  sz = size (phi);
  if (length (sz) ~= 2 || any (sz < 2))
    error ("ls_find_geometry is only implemented for 2D");
  endif

  if (!all (abs (sign (phi(:))) == 1))
    error ("PHI is not normalised and contains exact zeros or NaN's");
  endif

  % Use internal routines.
  elem = __levelset_geomElements (phi);
  [ispts, edges] = __levelset_geomBoundary (phi, h, fractol, ...
                                            elem.index.bdry, elem.nodelist);
  bdryel = struct ("n", length (elem.index.bdry), "edges", edges);
  [gammaComp, ispts.onedge, bdryelSeg] ...
    = __levelset_geomGamma (phi, elem.nodelist, elem.index.bdry, ...
                            edges, ispts.inout);
  gamma = struct ("n", length (gammaComp), "ispts", {gammaComp});
  internal = struct ("bdryelSegments", {bdryelSeg});

  % Build the bedges struct and ispts.gammachain from gammaComp.
  bedges = struct ("n", 0, "comp", [], "ispts", []);
  ispts.gammachain = NA (ispts.n, 2);
  for i = 1 : gamma.n
    comp = gamma.ispts{i};
    nCurPts = length (gamma.ispts{i});
    comp(end + 1) = comp(1);

    bedges.n += nCurPts;
    bedges.comp = [bedges.comp, repmat(i, 1, nCurPts)];

    block = [comp(1 : end - 1), comp(2 : end)]; 
    assert (size (block), [nCurPts, 2]);
    bedges.ispts = [bedges.ispts; block];

    ispts.gammachain(comp(1 : end - 1), 1) = comp(2 : end);
    ispts.gammachain(comp(2 : end), 2) = comp(1 : end - 1);
  endfor
  assert (length (bedges.comp), bedges.n);
  assert (size (bedges.ispts), [bedges.n, 2]);
  assert (all (!isna (ispts.gammachain)));

  % Fill in onedge(:, :, 3) from bedges.
  ispts.onedge(bedges.ispts(:, 1), 1, 3) = 1 : bedges.n;
  ispts.onedge(bedges.ispts(:, 2), 2, 3) = 1 : bedges.n;
  assert (all (!isna (ispts.onedge(:))));

  % Construct result.
  geom = struct ("dim", sz, "elem", elem, ...
                 "bdryel", bdryel, "ispts", ispts, ...
                 "gamma", gamma, "bedges", bedges, ...
                 "internal", internal);
endfunction

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Tests.

% Test for error conditions.
%!error <Invalid call to>
%!  ls_find_geometry ()
%!error <Invalid call to>
%!  ls_find_geometry (1, 2, 3, 4)
%!error <ls_find_geometry is only implemented for 2D>
%!  ls_find_geometry (resize (zeros (8, 1), [2, 2, 2]))
%!error <ls_find_geometry is only implemented for 2D>
%!  ls_find_geometry ([1, -1, 1])
%!error <PHI is not normalised>
%!  ls_find_geometry ([0, 1; -1, -1])
%!error <PHI is not normalised>
%!  ls_find_geometry ([-1, NA; -1, -1])

% A function to verify certain expected properties / postconditions
% for the resulting geometry structure.
%!function verifyGeometry (phi, geom)
%!  sz = size (phi);
%!  nNode = prod (sz);
%!  L = sz(1);
%!  M = sz(2);
%!  nElem = (L - 1) * (M - 1);
%!  assert (geom.dim, sz);
%!
%!  % Basic properties of the elem field.
%!  assert (geom.elem.n, nElem);
%!  assert (size (geom.elem.nodelist), [nElem, 4]);
%!  assert (all (geom.elem.type(geom.elem.index.inner) == -1));
%!  assert (all ((geom.elem.type(geom.elem.index.bdry) == 0)));
%!  assert (all ((geom.elem.type(geom.elem.index.outer) == 1)));
%!  % Order of the nodes in nodelist.
%!  assert (geom.elem.nodelist(1, :), [L+2, 2, 1, L+1]);
%!  % Correct classification of inner/outer/bdry elements.
%!  assert (all (phi(geom.elem.nodelist(geom.elem.index.inner, :))(:) < 0));
%!  assert (all (phi(geom.elem.nodelist(geom.elem.index.outer, :))(:) > 0));
%!  bdryPhis = phi(geom.elem.nodelist(geom.elem.index.bdry, :));
%!  bdryPhis = sort (bdryPhis, 2);
%!  assert (all (bdryPhis(:, 1) < 0) && all (bdryPhis(:, end) > 0));
%!
%!  % Each boundary element should have two or four intersected edges.
%!  % Also verify that the start/endEdge fields of the internal information
%!  % match up to the points in question.
%!  for i = 1 : geom.bdryel.n
%!    empty = length (find (isna (geom.bdryel.edges(i, :))));
%!    assert (empty == 2 || empty == 0);
%!    for j = 1 : length (geom.internal.bdryelSegments{i})
%!      s = geom.internal.bdryelSegments{i}{j};
%!      assert (geom.bdryel.edges(i, s.startEdge) == s.startPt);
%!      assert (geom.bdryel.edges(i, s.endEdge) == s.endPt);
%!    endfor
%!  endfor
%!
%!  % Run through gamma components and verify that geom.ispts.onedge matches.
%!  for i = 1 : geom.gamma.n
%!    c = geom.gamma.ispts{i};
%!    c(end + 1) = c(1);
%!    for j = 1 : length (c) - 1
%!      cur = c(j);
%!      next = c(j + 1);
%!      bdryel = geom.ispts.onedge(cur, 1, 1);
%!      assert (geom.ispts.onedge(next, 2, 1), bdryel);
%!      assert (geom.bdryel.edges(bdryel, geom.ispts.onedge(cur, 1, 2)), cur);
%!      assert (geom.bdryel.edges(bdryel, geom.ispts.onedge(next, 2, 2)), next);
%!      
%!      edgeForward = geom.ispts.onedge(cur, 1, 3);
%!      assert (geom.bedges.ispts(edgeForward, 1), cur);
%!      assert (geom.bedges.ispts(edgeForward, 2), next);
%!
%!      assert (geom.ispts.gammachain(cur, 1), next);
%!      assert (geom.ispts.gammachain(next, 2), cur);
%!    endfor
%!  endfor
%!
%!  % Verify that the ordering of points in bedges matches the onedge
%!  % information about start / end points in each boundary element.
%!  for i = 1 : geom.bedges.n
%!    assert (geom.ispts.onedge(geom.bedges.ispts(i, 1), 1, 1), ...
%!            geom.ispts.onedge(geom.bedges.ispts(i, 2), 2, 1));
%!    assert (geom.ispts.onedge(geom.bedges.ispts(i, 1), 1, 3), i);
%!    assert (geom.ispts.onedge(geom.bedges.ispts(i, 2), 2, 3), i);
%!  endfor
%!endfunction

% Test the postconditions against all test phis.
%!test
%!  phis = ls_get_tests ();
%!  hs = [0.1, 0.5, 1, 1.5];
%!  for i = 1 : length (phis)
%!    for h = hs
%!      for s = [-1, 1]
%!        cur = phis{i} * s;
%!        phi = ls_normalise (cur, h);
%!        geom = ls_find_geometry (phi, h);
%!        verifyGeometry (phi, geom);
%!      endfor
%!    endfor
%!  endfor

% Test pushing away of intersection points from nodes and the
% fraction calculation in general.  The constructed example
% is a narrow-pair with four intersection points.
%!test
%!  phi = ones (4, 4);
%!  phi(2 : 3, 2 : 3) = [Inf, -1; -Inf, 2];
%!  g = ls_find_geometry (phi, 0.1, 0.01);
%!
%!  ind = find (g.elem.index.bdry == 5);
%!  assert (size (ind), [1, 1]);
%!  edges = g.bdryel.edges(ind, :);
%!  assert (all (~isna (edges)));
%!
%!  assert (g.ispts.inout(edges, :), [7, 11; 7, 6; 10, 6; 10, 11]);
%!  assert (g.ispts.frac(edges), [0.99; 0.5; 0.01; 1/3], sqrt (eps));
%!  assert (g.ispts.incoord(edges, :), ...
%!          [0.099, 0; 0, -0.05; -0.001, 0; 0, 1/30], sqrt (eps));

% Test that cutting through the hold-all domain is correctly identified.
%!error <boundary cuts through hold-all domain>
%!  phi = [-1, -1; 1, 1];
%!  ls_find_geometry (phi);

% Very simple test for gamma components.
%!test
%!  phi = ones (3, 3);
%!  phi(2, 2) = -1;
%!  geom = ls_find_geometry (phi);
%!
%!  % Permute the component array such that it starts with the north point
%!  % and then compare coordinates.  Note that the "north" point is "south"
%!  % in matrix interpretation and thus has "outindex" of 6.
%!  assert (geom.gamma.n, 1);
%!  c = geom.gamma.ispts{1}';
%!  while (geom.ispts.inout(c(1), 2) != 6)
%!    c = [c(2 : end), c(1)];
%!  endwhile
%!  assert (geom.ispts.incoord(c, :), ...
%!          [0, 1/2; -1/2, 0; 0, -1/2; 1/2, 0], sqrt (eps));

% Helper function to check that the given coordinates all have the
% given direction with respect to the origin.  +1 means counter-clockwise
% and -1 is clockwise.
%!function checkBoundaryDirection (pts, dir)
%!  pts(end + 1, :) = pts(1, :);
%!  for i = 1 : length (pts) - 1
%!    a = [pts(i, :), 0];
%!    b = [pts(i + 1, :), 0];
%!    z = cross (a, b);
%!    assert (z(1 : 2), [0, 0]);
%!    assert (sign (z(3)), dir);
%!  endfor
%!endfunction

% Check that both "outside" and "inside" boundaries get the correct
% ordering in gamma components.  The test case here is a ring, where we
% identify the larger (in terms of count of ispts) gamma component with
% the outer and smaller component with the inner boundary.  We expect
% that the coordiantes of the outer and inner intersection points
% have (counter-)clockwise ordering with respect to the origin, respectively.
%!test
%!  n = 50;
%!  x = linspace (-10, 10, n);
%!  h = x(2) - x(1);
%!  [XX, YY] = meshgrid (x, x);
%!
%!  phi = ls_setdiff (ls_genbasic (XX, YY, "sphere", [0, 0], 8), ...
%!                    ls_genbasic (XX, YY, "sphere", [0, 0], 4));
%!  phi = ls_normalise (phi, h);
%!  geom = ls_find_geometry (phi, h);
%!  geom = ls_absolute_geom (geom, XX, YY);
%!
%!  assert (geom.gamma.n, 2);
%!  len1 = length (geom.gamma.ispts{1});
%!  len2 = length (geom.gamma.ispts{2});
%!  if (len1 < len2)
%!    outer = geom.gamma.ispts{2};
%!    inner = geom.gamma.ispts{1};
%!  else
%!    assert (len1 > len2);
%!    outer = geom.gamma.ispts{1};
%!    inner = geom.gamma.ispts{2};
%!  endif
%!
%!  checkBoundaryDirection (geom.ispts.coord(outer, :), 1);
%!  checkBoundaryDirection (geom.ispts.coord(inner, :), -1);

% Test that narrow bands are interpreted in the documented way.
%!test
%!  phi = ones (4, 4);
%!  phi(2, 2) = -1;
%!  phi(3, 3) = -1;
%!
%!  geom = ls_find_geometry (phi);
%!  assert (geom.gamma.n, 2);
%!  geom = ls_find_geometry (-phi);
%!  assert (geom.gamma.n, 1);
%!
%!  % Now also try with other "kind" of narrow pair.
%!  phi = phi(:, end : -1 : 1);
%!  geom = ls_find_geometry (phi);
%!  assert (geom.gamma.n, 2);
%!  geom = ls_find_geometry (-phi);
%!  assert (geom.gamma.n, 1);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Demo.

% Plot a grid, colour the cells according to the element type and
% mark the intersection points.
%!demo
%!  n = 15;
%!  x = linspace (-10, 10, n);
%!  h = x(2) - x(1);
%!  [XX, YY] = meshgrid (x, x);
%!
%!  r1 = sqrt (2) * 2.9;
%!  r2 = 3;
%!  phi = ls_union (ls_genbasic (XX, YY, "sphere", [-3, -3], r1), ...
%!                  ls_genbasic (XX, YY, "sphere", [3, 3], r1), ...
%!                  ls_genbasic (XX, YY, "sphere", [-5, 5], r2), ...
%!                  ls_genbasic (XX, YY, "sphere", [5, -5], r2));
%!
%!  phi = ls_normalise (phi, h);
%!  g = ls_find_geometry (phi, h, 0.2);
%!  g = ls_absolute_geom (g, XX, YY);
%!
%!  figure ();
%!  hold ("on");
%!
%!  for i = 1 : size (g.elem.nodelist, 1)
%!    nodes = g.elem.nodelist(i, :);
%!    switch (g.elem.type(i))
%!      case -1
%!        colour = [0.5, 0.5, 1];
%!      case 0
%!        colour = [1, 0.5, 0.5];
%!      case 1
%!        colour = [0.8, 0.8, 0.8];
%!    endswitch
%!    patch (XX(nodes), YY(nodes), colour);
%!  endfor
%!
%!  plot (g.ispts.coord(:, 1), g.ispts.coord(:, 2), "k.", "MarkerSize", 8);
%!  for i = 1 : g.gamma.n
%!    pts = g.gamma.ispts{i};
%!    pts(end + 1) = pts(1);
%!    plot (g.ispts.coord(pts, 1), g.ispts.coord(pts, 2), "k-", "LineWidth", 2);
%!  endfor
%!
%!  contour (XX, YY, phi, [0, 0], "g");
%!
%!  axis ([min(x), max(x), min(x), max(x)]);
%!  axis ("equal");
%!  grid ("on");
%!  set (gca (), "xtick", x, "ytick", x, "xticklabel", "", "yticklabel", "");
%!  hold ("off");
