content: /*
   * Copyright 2013 Hannes Janetzek
   * Copyright 2017-2019 Gustl22
   *
   * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
   *
   * This program is free software: you can redistribute it and/or modify it under the
   * terms of the GNU Lesser 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 Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License along with
   * this program. If not, see <http://www.gnu.org/licenses/>.
   */
  package org.oscim.layers.tile.buildings;
  
  import org.oscim.backend.canvas.Color;
  import org.oscim.core.GeometryBuffer;
  import org.oscim.core.Tag;
  import org.oscim.utils.ColorsCSS;
  import org.oscim.utils.Tessellator;
  import org.oscim.utils.geom.GeometryUtils;
  import org.oscim.utils.math.MathUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import java.util.*;
  
  /**
   * Provides utils for S3DB layers.
   */
  public final class S3DBUtils {
      private static final Logger log = LoggerFactory.getLogger(S3DBUtils.class);
  
      // Toggle this to debug and improve ridge calculation, you can see the faults in map then.
      private static final boolean IMPROVE_RIDGE_CALCULATION = false;
      private static final int SNAP_THRESHOLD = 70; // Threshold for ridge snap calculation (maybe should depend on map scale)
  
      private static final float[][] PROFILE_DOME = new float[][]{
              {1, 0},
              {0.966f, 0.259f},
              {0.866f, 0.5f},
              {0.707f, 0.707f},
              {0.5f, 0.866f},
              {0.259f, 0.966f},
              {0, 1}};
      private static final float[][] PROFILE_HIPPED = new float[][]{
              {1, 0},
              {0, 1}};
      private static final float[][] PROFILE_MANSARD = new float[][]{
              {1, 0},
              {0.75f, 0.75f},
              {0, 1}};
      private static final float[][] PROFILE_ONION = new float[][]{
              {1, 0},
              {0.2f, 0.01f},
              {0.875f, 0.1875f},
              {1, 0.375f},
              {0.875f, 0.5625f},
              {0.5f, 0.75f},
              {0.2f, 0.8125f},
              {0, 1}};
      private static final float[][] PROFILE_SALTBOX = new float[][]{
              {1, 0},
              {0.5f, 1},
              {0, 1}};
  
      /**
       * Adds point to ridgePoints and snaps it to a point which is in radius of SNAP_THRESHOLD.
       */
      private static void addSnapRidgePoint(int id, float[] point, TreeMap<Integer, float[]> ridgePoints) {
          // Simplify ridgePoints
          if (point == null) return;
          for (float[] ridPoint : ridgePoints.values()) {
              if (ridPoint == null) {
                  log.debug("Ridge point not found!");
                  continue;
              }
              if (GeometryUtils.distance2D(ridPoint, point) < SNAP_THRESHOLD) {
                  ridgePoints.put(id, ridPoint);
                  return;
              }
          }
          ridgePoints.put(id, point);
      }
  
      /**
       * Calculates a circle mesh of a Poly-GeometryBuffer.
       *
       * @param element the GeometryBuffer which is used to write the 3D mesh
       * @return true if calculation succeeded, false otherwise
       */
      public static boolean calcCircleMesh(GeometryBuffer element, float minHeight, float maxHeight, String roofShape) {
          float[] points = element.points;
          int[] index = element.index;
  
          boolean outerProcessed = false;
          for (int i = 0, pointPos = 0; i < index.length && !outerProcessed; i++) {
              if (index[i] < 0) {
                  break;
              }
  
              int numSections = index[i] / 2;
              if (numSections < 0) continue;
  
              outerProcessed = true;
  
              // Init mesh
              GeometryBuffer mesh;
              mesh = initCircleMesh(getProfile(roofShape), numSections);
  
              // Calculate center and load points
              float centerX = 0;
              float centerY = 0;
              float radius = 0;
  
              List<float[]> point3Fs = new ArrayList<>();
  
              for (int j = 0; j < (numSections * 2); j += 2, pointPos += 2) {
                  float x = points[pointPos];
                  float y = points[pointPos + 1];
  
                  point3Fs.add(new float[]{x, y, minHeight});
  
                  centerX += x;
                  centerY += y;
              }
  
              centerX = centerX / numSections;
              centerY = centerY / numSections;
  
              // Calc max radius
              for (float[] point3F : point3Fs) {
                  float difX = point3F[0] - centerX;
                  float difY = point3F[1] - centerY;
                  float tmpR = (float) Math.sqrt(difX * difX + difY * difY);
                  if (tmpR > radius) {
                      radius = tmpR;
                  }
              }
  
              element.points = mesh.points;
  
              // Calc radius and adjust angle
              int numPointsPerSection = (element.points.length / (3 * numSections));
              float heightRange = maxHeight - minHeight;
              for (int k = 0, j = 0; k < numSections; k++) {
                  float px = point3Fs.get(k)[0] - centerX;
                  float py = point3Fs.get(k)[1] - centerY;
  
                  float phi = (float) Math.atan2(py, px);
                  int sectionLimit = (numPointsPerSection + numPointsPerSection * k) * 3;
  
                  for (boolean first = true; j < sectionLimit; j = j + 3) {
                      float r = element.points[j + 0] * radius; // Set radius to initial x (always positive) stretched with individual element radius
                      px = (float) (r * Math.cos(phi));
                      py = (float) (r * Math.sin(phi));
  
                      if (!first) {
                          element.points[j + 0] = centerX + px;
                          element.points[j + 1] = centerY + py;
                          element.points[j + 2] = minHeight + element.points[j + 2] * heightRange;
                      } else {
                          // Set lowest points to outline points.
                          first = false;
                          element.points[j + 0] = point3Fs.get(k)[0];
                          element.points[j + 1] = point3Fs.get(k)[1];
                          element.points[j + 2] = minHeight;
                      }
                  }
              }
  
              element.index = mesh.index;
              element.pointNextPos = element.points.length;
          }
  
          element.type = GeometryBuffer.GeometryType.TRIS;
          return true;
      }
  
      /**
       * Calculates a flat mesh of a Poly-GeometryBuffer.
       *
       * @param element the GeometryBuffer which is used to write the 3D mesh
       * @return true if calculation succeeded, false otherwise
       */
      public static boolean calcFlatMesh(GeometryBuffer element, float maxHeight) {
  
          if (Tessellator.tessellate(element, element) == 0) return false;
  
          float[] points = element.points;
          List<float[]> point3Fs = new ArrayList<>();
  
          // Calculate center and load points
          for (int pointPos = 0; pointPos < points.length; pointPos += 2) {
              float x = points[pointPos];
              float y = points[pointPos + 1];
              point3Fs.add(new float[]{x, y, maxHeight});
          }
  
          points = new float[3 * point3Fs.size()];
          for (int i = 0; i < point3Fs.size(); i++) {
              int pPos = 3 * i;
              float[] point3D = point3Fs.get(i);
              points[pPos + 0] = point3D[0];
              points[pPos + 1] = point3D[1];
              points[pPos + 2] = point3D[2];
          }
  
          element.points = points;
          element.pointNextPos = element.points.length;
          element.type = GeometryBuffer.GeometryType.TRIS;
          return true;
      }
  
      /**
       * Calculates a mesh for the outlines of a Poly-GeometryBuffer.
       *
       * @param element the GeometryBuffer which is used to write the 3D mesh
       * @return true if calculation succeeded, false otherwise
       */
      public static boolean calcOutlines(GeometryBuffer element, float minHeight, float maxHeight) {
          float[] points = element.points;
          int[] index = element.index;
  
          element.points = null;
          element.index = null;
  
          for (int i = 0, pointPos = 0; i < index.length; i++) {
              if (index[i] < 0) {
                  break;
              }
  
              int numPoints = index[i] / 2;
              if (numPoints < 0) continue;
  
              List<float[]> point3Fs = new ArrayList<>();
  
              // load points
              for (int j = 0; j < numPoints; j++, pointPos += 2) {
                  float x = points[pointPos];
                  float y = points[pointPos + 1];
  
                  point3Fs.add(new float[]{x, y, minHeight});
                  point3Fs.add(new float[]{x, y, maxHeight});
              }
  
              // Write index: index gives the first point of triangle mesh (divided 3)
              int[] meshIndex = new int[numPoints * 6]; // 3 vertices and each side needs 2 triangles
              for (int j = 0; j < point3Fs.size(); j = j + 2) {
                  int pos = 3 * j; // triangle mesh
                  meshIndex[pos + 2] = j;
                  meshIndex[pos + 1] = (j + 1) % point3Fs.size();
                  meshIndex[pos + 0] = (j + 3) % point3Fs.size();
  
                  meshIndex[pos + 5] = (j + 3) % point3Fs.size();
                  meshIndex[pos + 4] = (j + 2) % point3Fs.size();
                  meshIndex[pos + 3] = (j);
              }
  
              // Write points
              float[] meshPoints = new float[point3Fs.size() * 3];
              for (int j = 0; j < point3Fs.size(); j++) {
                  int pos = 3 * j;
                  meshPoints[pos + 0] = point3Fs.get(j)[0];
                  meshPoints[pos + 1] = point3Fs.get(j)[1];
                  meshPoints[pos + 2] = point3Fs.get(j)[2];
              }
  
              // Init points and indices or add more polygons (e.g. inner rings)
              if (element.points == null) {
                  element.points = meshPoints;
              } else {
                  float[] tmpPoints = element.points;
                  element.points = new float[tmpPoints.length + meshPoints.length];
                  System.arraycopy(tmpPoints, 0, element.points, 0, tmpPoints.length);
                  System.arraycopy(meshPoints, 0, element.points, tmpPoints.length, meshPoints.length);
              }
  
              if (element.index == null) {
                  element.index = meshIndex;
              } else {
                  int[] tmpIndex = element.index;
                  element.index = new int[tmpIndex.length + meshIndex.length];
                  System.arraycopy(tmpIndex, 0, element.index, 0, tmpIndex.length);
                  // Shift all indices with previous element.points.length
                  for (int k = 0; k < meshIndex.length; k++) {
                      element.index[k + tmpIndex.length] = meshIndex[k] + (element.pointNextPos / 3);
                  }
              }
              element.pointNextPos = element.points.length;
          }
  
          if (element.points == null) {
              return false;
          }
  
          //element.indexCurrentPos = 0;
          element.type = GeometryBuffer.GeometryType.TRIS;
          return true;
      }
  
      /**
       * Calculates a pyramidal mesh of a Poly-GeometryBuffer.
       *
       * @param element the GeometryBuffer which is used to write the 3D mesh
       * @return true if calculation succeeded, false otherwise
       */
      public static boolean calcPyramidalMesh(GeometryBuffer element, float minHeight, float maxHeight) {
          float[] points = element.points;
          int[] index = element.index;
          float[] topPoint = new float[3];
          topPoint[2] = maxHeight;
  
          for (int i = 0, pointPos = 0; i < index.length; i++) {
              if (index[i] < 0) {
                  break;
              }
              if (i > 0) break; // May add inner rings
  
              int numPoints = index[i] / 2;
              if (numPoints < 0) continue;
  
              // Init top of roof (attention with pointPos)
              GeometryUtils.center(points, pointPos, numPoints << 1, topPoint);
  
              List<float[]> point3Fs = new ArrayList<>();
              // Load points
              for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2)
                  point3Fs.add(new float[]{points[pointPos], points[pointPos + 1], minHeight});
  
              // Write index: index gives the first point of triangle mesh (divided 3)
              int[] meshIndex = new int[numPoints * 3];
              for (int j = 0; j < point3Fs.size(); j++) {
                  int pos = 3 * j; // triangle mesh
                  meshIndex[pos + 0] = j;
                  meshIndex[pos + 1] = (j + 1) % point3Fs.size();
                  meshIndex[pos + 2] = point3Fs.size();
              }
  
              // Write points
              point3Fs.add(topPoint);
              float[] meshPoints = new float[point3Fs.size() * 3];
              for (int j = 0; j < point3Fs.size(); j++) {
                  int pos = 3 * j;
                  float[] point3D = point3Fs.get(j);
                  meshPoints[pos + 0] = point3D[0];
                  meshPoints[pos + 1] = point3D[1];
                  meshPoints[pos + 2] = point3D[2];
              }
  
              element.points = meshPoints;
              element.index = meshIndex;
              //element.indexCurrentPos = 0;
              element.pointNextPos = meshPoints.length;
          }
  
          element.type = GeometryBuffer.GeometryType.TRIS;
          return true;
      }
  
      /**
       * Calculates a ridge mesh of a Poly-GeometryBuffer.
       *
       * @param element           the GeometryBuffer which is used to write the 3D mesh
       * @param minHeight         the minimum height
       * @param maxHeight         the maximum height
       * @param orientationAcross indicates if ridge is parallel to short side
       * @param roofShape         the roof shape
       * @param specialParts      element to add missing parts of underlying element
       * @return true if calculation succeeded, false otherwise
       */
      public static boolean calcRidgeMesh(GeometryBuffer element, float minHeight, float maxHeight, boolean orientationAcross, String roofShape, GeometryBuffer specialParts) {
          float[] points = element.points;
          int[] index = element.index;
  
          boolean isGabled = isGabled(roofShape);
  
          for (int i = 0, pointPos = 0; i < index.length; i++) {
              if (index[i] < 0) {
                  break;
              }
              if (i > 0) break; // Handle only first polygon
  
              int numPoints = index[i] / 2;
              if (numPoints < 0) continue;
  
              if (numPoints < 4 || (!isGabled && orientationAcross)) {
                  calcPyramidalMesh(element, minHeight, maxHeight);
                  return true;
              }
  
              List<float[]> point3Fs = new ArrayList<>();
  
              // Calculate center and load points
              for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2) {
                  float x = points[pointPos];
                  float y = points[pointPos + 1];
  
                  point3Fs.add(new float[]{x, y, minHeight});
              }
  
              // Number of ground points
              int groundSize = point3Fs.size();
  
              // Calc vectors
              List<Float> lengths = new ArrayList<>();
              List<float[]> normVectors = GeometryUtils.normalizedVectors2D(point3Fs, lengths);
  
              List<Byte> simpleAngles = getSimpleAngles(normVectors);
  
              Integer indexStart = getIndexStart(simpleAngles, lengths, orientationAcross);
  
              int countConcavAngles = 0;
              for (Byte simpleAngle : simpleAngles) {
                  if (simpleAngle < -1)
                      countConcavAngles++;
              }
  
              // Calc different mesh, if roof has no nearly right angle
              if (indexStart == null) {
                  if (isGabled)
                      return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts);
                  else
                      return calcPyramidalMesh(element, minHeight, maxHeight);
              }
  
              List<float[]> bisections = getBisections(normVectors);
              List<float[]> intersections = new ArrayList<>();
  
              // Calc intersection of bisection
              for (int k = 0; k < groundSize; k++) {
                  int nextTurn = getIndexNextTurn(k, simpleAngles);
                  float[] pA = point3Fs.get(nextTurn);
                  float[] pB = point3Fs.get(k);
                  intersections.add(GeometryUtils.intersectionLines2D(pA, bisections.get(nextTurn), pB, bisections.get(k)));
              }
  
              // Calc ridge points
              TreeMap<Integer, float[]> ridgePoints = new TreeMap<>();
              TreeMap<Integer, float[]> ridgeLines = new TreeMap<>();
              HashSet<Integer> gablePoints = new HashSet<>(); // Only used if gabled
              Integer currentRidgeInd = null;
              boolean isOdd = false;
              for (int k = 0; k < groundSize; k++) {
                  int shift = (k + indexStart) % groundSize;
                  byte direction = simpleAngles.get(shift);
                  if (direction == 0) {
                      continue; // direction is similar to last one
                  } else if (direction < 0) {
                      // If shape turns left (concave)
                      float[] positionRidgeA = null;
                      float[] positionRidgeB = null;
  
                      // Check two previous corners
                      Integer indexPrevious = getIndexPreviousConvexTurn(shift, simpleAngles);
                      Integer indexPrevious2 = getIndexPreviousConvexTurn(indexPrevious == null ? shift - 1 : indexPrevious, simpleAngles);
  
                      if (indexPrevious != null && indexPrevious2 != null) {
                          // Write two previous
                          if (!ridgeLines.containsKey(indexPrevious2)) {
                              ridgeLines.put(indexPrevious2, normVectors.get(indexPrevious));
                          }
  
                          positionRidgeA = intersections.get(indexPrevious2);
                          currentRidgeInd = indexPrevious2;
                          if (isGabled) {
                              positionRidgeA = GeometryUtils.intersectionLines2D(positionRidgeA, ridgeLines.get(indexPrevious2), point3Fs.get(indexPrevious2), normVectors.get(indexPrevious2));
                              gablePoints.add(indexPrevious2);
                          }
                          ridgePoints.put(indexPrevious2, positionRidgeA);
  
                          // Remove previous ridge, if exists
                          gablePoints.remove(indexPrevious);
                          ridgePoints.remove(indexPrevious);
                          ridgeLines.remove(indexPrevious);
                      }
  
                      // Check two next corners
                      Integer indexNext = getIndexNextConvexTurn(shift, simpleAngles);
                      Integer indexNext2 = getIndexNextConvexTurn(indexNext == null ? shift + 1 : indexNext, simpleAngles);
  
                      if (indexNext != null && indexNext2 != null) {
                          if (ridgePoints.get(indexNext) == null) {
                              // Write both next
                              if (!ridgeLines.containsKey(indexNext)) {
                                  ridgeLines.put(indexNext, normVectors.get(indexNext2));
                              }
                              positionRidgeB = intersections.get(indexNext);
  
                              if (isGabled) {
                                  positionRidgeB = GeometryUtils.intersectionLines2D(positionRidgeB, ridgeLines.get(indexNext), point3Fs.get(indexNext), normVectors.get(indexNext));
                                  gablePoints.add(indexNext);
                              }
                              ridgePoints.put(indexNext, positionRidgeB);
                          } else {
                              positionRidgeB = ridgePoints.get(indexNext);
                          }
                      }
  
                      // Handle multiple concaves
                      if (positionRidgeA == null || positionRidgeB == null) {
                          if (positionRidgeA == null && positionRidgeB == null && currentRidgeInd != null) {
                              positionRidgeA = ridgePoints.get(currentRidgeInd);
                          }
                          if (positionRidgeA != null && positionRidgeB == null) { // Next index is concave
                              positionRidgeA = GeometryUtils.intersectionLines2D(positionRidgeA, ridgeLines.get(currentRidgeInd), point3Fs.get(shift), bisections.get(shift));
                              currentRidgeInd = shift;
                              addSnapRidgePoint(shift, positionRidgeA, ridgePoints);
                              ridgeLines.put(shift, normVectors.get(shift)); // Add ridgeLine, if concave
                              isOdd = false;
                              continue;
                          } else if (positionRidgeA == null && positionRidgeB != null) { // Previous index is concave
                              positionRidgeA = GeometryUtils.intersectionLines2D(positionRidgeB, ridgeLines.get(indexNext), point3Fs.get(shift), bisections.get(shift));
                              addSnapRidgePoint(shift, positionRidgeA, ridgePoints);
                              currentRidgeInd = null;
                              isOdd = false;
                              continue;
                          } else {
                              log.debug("Should never happen, because positionRidge wouldn't be null then");
                              currentRidgeInd = null;
                              continue;
                          }
                      }
  
                      // Calc actual concave
                      if (currentRidgeInd == null || indexNext == null || ridgeLines.get(currentRidgeInd) == null || ridgeLines.get(indexNext) == null) {
                          log.debug("Concave shape not calculated correctly: " + element.toString());
                          currentRidgeInd = null;
                          continue;
                      }
  
                      float[] intersection = GeometryUtils.intersectionLines2D(positionRidgeA, ridgeLines.get(currentRidgeInd), positionRidgeB, ridgeLines.get(indexNext));
                      addSnapRidgePoint(shift, intersection, ridgePoints);
  
                      // Set opposite ridge, if only one concave corner
                      if (countConcavAngles == 1) {
                          Integer opposite = getIndexNextConvexTurn(indexNext2, simpleAngles);
                          if (opposite != null) {
                              if (isGabled)
                                  gablePoints.remove(opposite);
                              ridgePoints.put(opposite, intersection);
                          }
                      }
  
                      // Reset ridges
                      currentRidgeInd = null;
                      isOdd = false;
                      continue;
                  }
                  // Regular right angle (convex turn)
                  if (isOdd) {
                      isOdd = false;
                      continue;
                  }
                  if (simpleAngles.get(shift) > 1) {
                      isOdd = true;
                  }
                  if (ridgePoints.containsKey(shift) && ridgeLines.containsKey(shift)) {
                      currentRidgeInd = shift;
                      continue;
                  }
                  if (currentRidgeInd != null) {
                      float[] intersection;
                      // If is gabled, then use the normal line as intersection instead of bisection, but if the angle is not right, this is usually not a gable point
                      if (isGabled && direction > 1) {
                          if (ridgePoints.get(currentRidgeInd) == null) {
                              log.debug("Gabled intersection calc failed");
                              currentRidgeInd = null;
                              continue;
                          }
                          intersection = GeometryUtils.intersectionLines2D(ridgePoints.get(currentRidgeInd), ridgeLines.get(currentRidgeInd), point3Fs.get(shift), normVectors.get(shift));
                          if (intersection == null) {
                              log.debug("Gabled intersection calc failed");
                              currentRidgeInd = null;
                              continue;
                          }
                          gablePoints.add(shift);
                          ridgePoints.put(shift, intersection);
                      } else {
                          intersection = GeometryUtils.intersectionLines2D(ridgePoints.get(currentRidgeInd), ridgeLines.get(currentRidgeInd), point3Fs.get(shift), bisections.get(shift));
                          addSnapRidgePoint(shift, intersection, ridgePoints);
                      }
                      if (isOdd) {
                          currentRidgeInd = null;
                      } else {
                          ridgeLines.put(shift, normVectors.get(shift));
                          currentRidgeInd = shift;
                      }
                  } else {
                      Integer indexNext = getIndexNextConvexTurn(shift, simpleAngles);
                      if (indexNext == null) continue;
                      if (!ridgeLines.containsKey(shift)) {
                          ridgeLines.put(shift, normVectors.get(indexNext));
                      }
                      currentRidgeInd = shift;
  
                      float[] ridgePos = intersections.get(shift);
                      if (isGabled) {
                          ridgePos = GeometryUtils.intersectionLines2D(ridgePos, ridgeLines.get(currentRidgeInd), point3Fs.get(shift), normVectors.get(shift));
                          gablePoints.add(shift);
                      }
                      addSnapRidgePoint(shift, ridgePos, ridgePoints);
                  }
              }
  
              if (ridgePoints.isEmpty()) {
                  calcPyramidalMesh(element, minHeight, maxHeight);
                  return true;
              }
  
              Iterator<Map.Entry<Integer, float[]>> ridgeIt = ridgePoints.entrySet().iterator();
              while (ridgeIt.hasNext()) {
                  Map.Entry<Integer, float[]> ridgeEntry = ridgeIt.next();
                  Integer key = ridgeEntry.getKey();
                  if (ridgeEntry.getValue() == null) {
                      log.debug("Ridge calculation failed at point " + key);
                      ridgeIt.remove();
                      continue;
                  }
  
                  // Only remove ridgePoint at concave corners
                  if (!isGabled || simpleAngles.get(key) < 0) {
                      boolean isIn = GeometryUtils.pointInPoly(ridgeEntry.getValue()[0], ridgeEntry.getValue()[1], points, points.length, 0);
                      if (!isIn) {
                          // FIXME can improve shapes with concaves that intersect each other and remove shapes which have ridgepoints outside the outline
                          if (!IMPROVE_RIDGE_CALCULATION) {
                              if (isGabled) {
                                  return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts);
                              } else return calcFlatMesh(element, minHeight);
                          }
                      }
                  }
              }
  
              float[][] profile = getProfile(roofShape);
              int profileSize = profile.length - 2;
              int profileSizePlus = profile.length - 1; // profile roof shape size + ground size (+1)
  
              // Allocate the indices to the points
              int ridgePointSize = ridgePoints.size();
              float[] meshPoints = new float[(groundSize * profileSizePlus + ridgePointSize) * 3]; //(ridgePoints * 3 = 6)
              List<Integer> meshVarIndex = new ArrayList<>();
  
              // Add special building parts
              List<Integer> meshPartVarIndex = null;
              if (isGabled && specialParts != null) {
                  meshPartVarIndex = new ArrayList<>();
              }
  
              float heightRange = maxHeight - minHeight;
  
              // Number of 3D-points for the roof (groundSize * inner profile + groundSize)
              int grRsSize = groundSize * profileSizePlus;
  
              // WRITE MESH
  
              for (int l = 0; l < groundSize; l++) {
                  // l: #ground points
                  // k: #(shape points + ground points)
                  int k = l * profileSizePlus;
  
                  // Add first face
                  float[] p = point3Fs.get(l);
                  int ridgePointIndex1 = l;
                  while (!ridgePoints.containsKey(ridgePointIndex1)) {
                      ridgePointIndex1 = (ridgePointIndex1 + groundSize - 1) % groundSize; // Decrease ridgePointIndex until a ridge point is found for the k point.
                  }
                  int ridgeIndex1 = ridgePoints.headMap(ridgePointIndex1).size(); // set ridgeIndex to shift in ridgePoints
                  boolean isGable = false;
                  if (meshPartVarIndex != null && gablePoints.contains(ridgePointIndex1) && getIndexNextTurn(ridgePointIndex1, simpleAngles).equals(getIndexNextTurn(l, simpleAngles))) {
                      isGable = true;
                      // Add missing parts to building
                      meshPartVarIndex.add(k + profileSize);
                      meshPartVarIndex.add((k + profileSizePlus + profileSize) % grRsSize);
                      meshPartVarIndex.add(ridgeIndex1 + grRsSize);
                  } else {
                      // Add first roof face
                      meshVarIndex.add(k + profileSize);
                      meshVarIndex.add((k + profileSizePlus + profileSize) % grRsSize);
                      meshVarIndex.add(ridgeIndex1 + grRsSize);
                  }
  
                  // Add second face, if necessary
                  int ridgePointIndex2 = (l + 1) % groundSize;
                  while (!ridgePoints.containsKey(ridgePointIndex2)) {
                      ridgePointIndex2 = (ridgePointIndex2 + groundSize - 1) % groundSize; // Decrease ridgePointIndex
                  }
  
                  if (ridgePointIndex2 != ridgePointIndex1) {
                      int ridgeIndex2 = ridgePoints.headMap(ridgePointIndex2).size(); // Set ridgeIndex to position in ridgePoints
                      meshVarIndex.add(ridgeIndex1 + grRsSize);
                      meshVarIndex.add((k + profileSizePlus + profileSize) % grRsSize);
                      meshVarIndex.add(ridgeIndex2 + grRsSize);
                  }
  
                  // Write ground points
                  int offset = 3 * k;
                  meshPoints[offset + 0] = p[0];
                  meshPoints[offset + 1] = p[1];
                  meshPoints[offset + 2] = p[2];
  
                  // Write profile roof shape points, skip ground points and ridge points of profile
                  float[] rp1 = ridgePoints.get(ridgePointIndex1);
                  float[] dif = GeometryUtils.diffVec(p, rp1); // Vector from ridge point to ground point
                  float phi = (float) Math.atan2(dif[0], dif[1]); // direction of diff
                  float r = (float) GeometryUtils.length(dif);
                  for (int m = 1; m < profileSizePlus; m++) {
                      offset = 3 * (k + m); // (+ 1 - 1 = 0)
                      int o = k + m - 1; // the ground + actual point of profile (m = 0 is the ground point)
  
                      // Add profile roof faces (2 per side and profile point)
                      if (isGable) {
                          // Add missing parts to building
                          meshPartVarIndex.add(o); // ground
                          meshPartVarIndex.add((o + profileSizePlus) % grRsSize); // ground
                          meshPartVarIndex.add((o + 1) % grRsSize); // profile
  
                          meshPartVarIndex.add((o + profileSizePlus) % grRsSize); // ground + 1
                          meshPartVarIndex.add((o + 1 + profileSizePlus) % grRsSize); // profile
                          meshPartVarIndex.add((o + 1) % grRsSize); // profile
                      } else {
                          meshVarIndex.add(o); // ground
                          meshVarIndex.add((o + profileSizePlus) % grRsSize); // ground + 1
                          meshVarIndex.add((o + 1) % grRsSize); // profile
  
                          meshVarIndex.add((o + profileSizePlus) % grRsSize); // ground + 1
                          meshVarIndex.add((o + 1 + profileSizePlus) % grRsSize); // profile + 1
                          meshVarIndex.add((o + 1) % grRsSize); // profile
                      }
  
                      // Calculate position of profile point.
                      // profile[m][0] is same length for x and y
                      meshPoints[offset + 0] = rp1[0] + (float) (r * profile[m][0] * Math.sin(phi));
                      meshPoints[offset + 1] = rp1[1] + (float) (r * profile[m][0] * Math.cos(phi));
                      meshPoints[offset + 2] = p[2] + heightRange * profile[m][1];
                  }
              }
  
              // Tessellate top, if necessary (can be used to improve wrong rendered roofs)
              if (ridgePointSize > 2) {
                  HashSet<Integer> ridgeSkipFaceIndex = new HashSet<>();
                  boolean isTessellateAble = true;
                  for (int k = 0; k < groundSize; k++) {
                      if (!isTessellateAble || ridgePoints.get(k) == null) continue;
                      Integer middle = null;
                      for (int m = k + 1; m <= k + groundSize; m++) {
                          int secIndex = m % groundSize;
                          if (ridgePoints.get(secIndex) == null) continue;
                          if (middle == null) {
                              middle = secIndex;
                          } else {
                              float isClockwise = GeometryUtils.isTrisClockwise(ridgePoints.get(k), ridgePoints.get(middle), ridgePoints.get(secIndex));
                              if (Math.abs(isClockwise) < 0.001) {
                                  ridgeSkipFaceIndex.add(middle);
                                  if (Arrays.equals(ridgePoints.get(k), ridgePoints.get(secIndex)))
                                      ridgeSkipFaceIndex.add(k);
                              }
                              if (isClockwise > 0 && IMPROVE_RIDGE_CALCULATION) {
                                  // TODO Improve handling of counter clockwise faces and support multiple faces
                                  isTessellateAble = false;
                                  break;
                                  //return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts);
                              }
                              break;
                          }
                      }
                  }
                  int faceLength = ridgePointSize - ridgeSkipFaceIndex.size();
                  if (isTessellateAble && faceLength > 0) {
                      float[] gbPoints = new float[2 * faceLength];
                      int k = 0;
                      List<Integer> faceIndex = new ArrayList<>(); // Store used indices
                      for (int m = 0; m < groundSize; m++) {
                          float[] point = ridgePoints.get(m);
                          if (ridgeSkipFaceIndex.contains(m) || point == null) {
                              continue;
                          }
                          faceIndex.add(m);
                          gbPoints[2 * k] = point[0];
                          gbPoints[2 * k + 1] = point[1];
                          k++;
                      }
                      GeometryBuffer buffer = new GeometryBuffer(gbPoints, new int[]{2 * faceLength});
                      if (Tessellator.tessellate(buffer, buffer) != 0) {
                          for (int ind : buffer.index) {
                              // Get position in ridgePoints, considering skipped points
                              meshVarIndex.add(ridgePoints.headMap(faceIndex.get(ind)).size() + grRsSize);
                          }
                      } else {
                          // TODO Improve wrong or not tessellated faces
                          if (!IMPROVE_RIDGE_CALCULATION) {
                              if (isGabled) {
                                  return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts);
                              } else return calcFlatMesh(element, minHeight);
                          }
                      }
                  }
              }
  
  
              // Replace in Java 8 / min API 24
              int[] meshIndex = new int[meshVarIndex.size()]; // new int[(groundSize + ridgePointSize) * 3]; // 3 vertices per point + 6 vertices for left and right roof
              for (int k = 0; k < meshIndex.length; k++)
                  meshIndex[k] = meshVarIndex.get(k);
  
              for (int k = 0, l = 0; k < groundSize; k++) {
                  // Add ridge points
                  float[] tmp = ridgePoints.get(k);
                  if (tmp != null) {
                      int ppos = 3 * (l + grRsSize);
                      meshPoints[ppos + 0] = tmp[0];
                      meshPoints[ppos + 1] = tmp[1];
                      meshPoints[ppos + 2] = maxHeight;
                      l++;
                  }
              }
  
              // Add special parts e.g. for gabled roofs
              if (specialParts != null && meshPartVarIndex != null) {
                  // Replace in Java 8 / min API 24
                  int[] meshPartsIndex = new int[meshPartVarIndex.size()];
                  for (int k = 0; k < meshPartsIndex.length; k++)
                      meshPartsIndex[k] = meshPartVarIndex.get(k);
  
                  specialParts.points = meshPoints;
                  specialParts.index = meshPartsIndex;
                  specialParts.pointNextPos = meshPoints.length;
                  specialParts.type = GeometryBuffer.GeometryType.TRIS;
              }
  
              element.points = meshPoints;
              element.index = meshIndex;
              element.pointNextPos = meshPoints.length;
              element.type = GeometryBuffer.GeometryType.TRIS;
          }
  
          return element.isTris();
      }
  
      /**
       * Calculates a simple gabled mesh of a Poly-GeometryBuffer.
       *
       * @param element the GeometryBuffer which is used to write the 3D mesh
       * @return true if calculation succeeded, false otherwise
       */
      private static boolean calcSimpleGabledMesh(GeometryBuffer element, float minHeight, float maxHeight, boolean orientationAcross, GeometryBuffer specialParts) {
          float[] points = element.points;
          int[] index = element.index;
  
          for (int i = 0, pointPos = 0; i < index.length; i++) {
              if (index[i] < 0) {
                  break;
              }
              if (i > 0) break; // Handle only first polygon
  
              int numPoints = index[i] / 2;
              if (numPoints < 0) continue;
  
              if (numPoints < 4) {
                  calcPyramidalMesh(element, minHeight, maxHeight);
                  return true;
              }
  
              List<float[]> point3Fs = new ArrayList<>();
  
              // Calculate center and load points
              for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2) {
                  float x = points[pointPos];
                  float y = points[pointPos + 1];
  
                  point3Fs.add(new float[]{x, y, minHeight});
              }
  
              // Calc vectors
              int groundSize = point3Fs.size();
  
              List<Float> lengths = new ArrayList<>();
              List<float[]> normVectors = GeometryUtils.normalizedVectors2D(point3Fs, lengths);
  
              List<Byte> simpleAngles = getSimpleAngles(normVectors);
  
              int indexStart = getIndicesLongestSide(simpleAngles, lengths, null)[0];
              if (orientationAcross) {
                  Integer tmp = getIndexPreviousConvexTurn(indexStart, simpleAngles);
                  if (tmp == null) {
                      tmp = getIndexNextTurn(indexStart, simpleAngles);
                  }
                  indexStart = tmp;
              }
              float[] vL = normVectors.get(indexStart);
              float[] pL = point3Fs.get(indexStart);
              float[] splitLinePoint = null;
              float maxDist = 0;
              for (float[] point : point3Fs) {
                  float curDist = GeometryUtils.distancePointLine2D(point, pL, vL);
                  if (curDist > maxDist) {
                      maxDist = curDist;
                      splitLinePoint = point; // Farthest point from line
                  }
              }
              // Scale of normal vec
              maxDist = Math.signum(GeometryUtils.isTrisClockwise(
                      pL,
                      GeometryUtils.sumVec(pL, vL),
                      splitLinePoint)) * (maxDist / 2);
  
              float[] normL = new float[]{-vL[1], vL[0]}; // Normal vec to line
              normL = GeometryUtils.scale(normL, (float) (maxDist / Math.sqrt(GeometryUtils.dotProduct(normL, normL)))); // normalize vec
              splitLinePoint = GeometryUtils.sumVec(pL, normL);
              float degreeNormL = (float) Math.atan2(normL[0], -normL[1]) * MathUtils.radiansToDegrees;
  
              // Split polygon
              int sideChange = 0;
              List<float[]> elementPoints1 = new ArrayList<>();
              List<float[]> elementPoints2 = new ArrayList<>();
              float[] secSplitPoint = GeometryUtils.sumVec(splitLinePoint, vL);
              float sideLastPoint = Math.signum(GeometryUtils.isTrisClockwise(splitLinePoint, secSplitPoint, point3Fs.get(groundSize - 1)));
              degreeNormL = sideLastPoint > 0 ? degreeNormL : (degreeNormL + 180f) % 360; // Correct angle
              List<Integer> intersection1 = new ArrayList<>(), intersection2 = new ArrayList<>();
              for (int k = 0; k < groundSize; k++) {
                  // If point is not on the same side as the previous point, the split line intersect and can calc split point
                  float sideCurPoint = Math.signum(GeometryUtils.isTrisClockwise(splitLinePoint, secSplitPoint, point3Fs.get(k)));
                  if (sideCurPoint != sideLastPoint) {
                      if (sideChange > 2 && !IMPROVE_RIDGE_CALCULATION)
                          return calcFlatMesh(element, minHeight); // TODO Improve multiple side changes
                      int indexPrev = (k + groundSize - 1) % groundSize;
                      float[] intersection = GeometryUtils.intersectionLines2D(splitLinePoint, vL, point3Fs.get(indexPrev), normVectors.get(indexPrev));
                      elementPoints1.add(intersection);
                      elementPoints2.add(intersection);
                      intersection1.add(elementPoints1.size() - 1);
                      intersection2.add(elementPoints2.size() - 1);
                      sideChange++;
                  }
                  if (sideChange % 2 == 0) {
                      elementPoints1.add(point3Fs.get(k));
                  } else {
                      elementPoints2.add(point3Fs.get(k));
                  }
                  sideLastPoint = sideCurPoint;
              }
  
              GeometryBuffer geoEle1 = new GeometryBuffer(elementPoints1.size(), 1);
              for (int k = 0; k < elementPoints1.size(); k++) {
                  geoEle1.points[2 * k] = elementPoints1.get(k)[0];
                  geoEle1.points[2 * k + 1] = elementPoints1.get(k)[1];
              }
              geoEle1.index[0] = geoEle1.points.length;
              geoEle1.pointNextPos = geoEle1.points.length;
  
              GeometryBuffer geoEle2 = new GeometryBuffer(elementPoints2.size(), 1);
              for (int k = 0; k < elementPoints2.size(); k++) {
                  geoEle2.points[2 * k] = elementPoints2.get(k)[0];
                  geoEle2.points[2 * k + 1] = elementPoints2.get(k)[1];
              }
              geoEle2.index[0] = geoEle2.points.length;
              geoEle2.pointNextPos = geoEle2.points.length;
  
              GeometryBuffer specialParts1 = new GeometryBuffer(geoEle1);
              GeometryBuffer specialParts2 = new GeometryBuffer(geoEle2);
              if (!(calcSkillionMesh(geoEle1, minHeight, maxHeight, degreeNormL, specialParts1)
                      && calcSkillionMesh(geoEle2, minHeight, maxHeight,
                      degreeNormL + 180, specialParts2))) {
                  return false;
              }
  
              // Adapt gable intersections to max height
              for (Integer integer : intersection1) {
                  geoEle1.points[integer * 3 + 2] = maxHeight;
                  specialParts1.points[6 * integer + 5] = maxHeight;
              }
              for (Integer integer : intersection2) {
                  geoEle2.points[integer * 3 + 2] = maxHeight;
                  specialParts2.points[6 * integer + 5] = maxHeight;
              }
  
              // Merge buffers
              mergeMeshGeometryBuffer(geoEle1, geoEle2, element);
              mergeMeshGeometryBuffer(specialParts1, specialParts2, specialParts);
              return true;
          }
          return false;
      }
  
      /**
       * Calculates a skillion mesh of a Poly-GeometryBuffer.
       *
       * @param element      the GeometryBuffer which is used to write the 3D mesh
       * @param roofDegree   the direction of slope
       * @param specialParts the GeometryBuffer which is used to write the additional building parts
       * @return true if calculation succeeded, false otherwise
       */
      public static boolean calcSkillionMesh(GeometryBuffer element, float minHeight, float maxHeight, float roofDegree, GeometryBuffer specialParts) {
          float[] points = element.points;
          int[] index = element.index;
  
          for (int i = 0, pointPos = 0; i < index.length; i++) {
              if (index[i] < 0) {
                  break;
              }
              if (i > 0) break; // May add inner rings
  
              int numPoints = index[i] / 2;
              if (numPoints < 0) continue;
  
              List<float[]> point3Fs = new ArrayList<>();
              for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2) {
                  float x = points[pointPos];
                  float y = points[pointPos + 1];
  
                  point3Fs.add(new float[]{x, y, minHeight});
              }
  
              boolean hasOutlines = calcOutlines(specialParts, minHeight, maxHeight);
  
              // Use 3 points that match the angle the best and use as plane
              float[] min1 = null, min2 = null, max1 = null, max2 = null;
              float minDif1, minDif2, maxDif1, maxDif2;
              minDif1 = minDif2 = Float.MAX_VALUE;
              maxDif1 = maxDif2 = 0;
  
              if (calcFlatMesh(element, maxHeight)) {
                  // To positive radian
                  roofDegree = ((MathUtils.degreesToRadians * roofDegree) + MathUtils.PI2) % MathUtils.PI2;
                  float[] vRidge = new float[2];
                  vRidge[0] = (float) Math.sin(roofDegree);
                  vRidge[1] = (float) -Math.cos(roofDegree);
                  vRidge = GeometryUtils.scale(vRidge, 100000000); // Use very large value, so the distances are nearly parallel
  
                  for (int k = 0; k < point3Fs.size(); k++) {
                      float[] point = point3Fs.get(k);
                      float vx = vRidge[0] - point[0];
                      float vy = vRidge[1] - point[1];
                      float currentDiff = (float) Math.sqrt(vx * vx + vy * vy);
                      if (max1 == null || currentDiff > maxDif1) {
                          if (max1 != null) {
                              max2 = max1;
                              maxDif2 = maxDif1;
                          }
                          max1 = point;
                          maxDif1 = currentDiff;
                      } else if (max2 == null || currentDiff > maxDif2) {
                          max2 = point;
                          maxDif2 = currentDiff;
                      }
                      if (min1 == null || currentDiff < minDif1) {
                          if (min1 != null) {
                              min2 = min1;
                              minDif2 = minDif1;
                          }
                          min1 = point;
                          minDif1 = currentDiff;
                      } else if (min2 == null || currentDiff < minDif2) {
                          min2 = point;
                          minDif2 = currentDiff;
                      }
                  }
                  if (min1 == max1) return false;
  
                  float[] normal, zVector = new float[]{0, 0, 1}; // height intersection
                  min1[2] = minHeight;
                  max1[2] = maxHeight;
  
                  // Use lower two points if they promise better results (e.g. at triangles)
                  if (Math.abs(minDif2 - minDif1) < Math.abs(maxDif2 - maxDif1)) {
                      min2[2] = minHeight; // Note: min2 == max2 is possible
                      normal = GeometryUtils.normalOfPlane(min1, max1, min2);
                  } else {
                      max2[2] = maxHeight;
                      normal = GeometryUtils.normalOfPlane(min1, max1, max2);
                  }
  
                  // Calc intersection points of ground points with plane
                  for (int k = 0; k < point3Fs.size(); k++) {
                      float[] intersection = GeometryUtils.intersectionLinePlane(point3Fs.get(k), zVector, min1, normal);
                      if (intersection == null) return false;
                      intersection[2] = intersection[2] > (2 * maxHeight) ? maxHeight : (intersection[2] < minHeight ? minHeight : intersection[2]);
                      element.points[3 * k + 2] = intersection[2];
                      if (hasOutlines) {
                          specialParts.points[6 * k + 5] = intersection[2]; // Every sixth point is height of k
                      }
                  }
  
                  return true;
              }
          }
  
          return false;
      }
  
      /**
       * @return the bisections of vectors
       */
      private static List<float[]> getBisections(List<float[]> normVectors) {
          int size = normVectors.size();
          List<float[]> bisections = new ArrayList<>();
          // Calc bisections
          for (int k = 0; k < size; k++) {
              float[] vBC = normVectors.get((k + size - 1) % size);
              float[] vBA = normVectors.get(k);
  
              // Change direction to get correct angle
              vBC = Arrays.copyOf(vBC, vBC.length);
              vBC[0] = -vBC[0];
              vBC[1] = -vBC[1];
  
              // Calc bisection
              bisections.add(GeometryUtils.bisectionNorm2D(vBC, vBA));
          }
          return bisections;
      }
  
      /**
       * @param color    the color as string (see http://wiki.openstreetmap.org/wiki/Key:colour)
       * @param hsv      the HSV color values to modify given color
       * @param relative declare if colors are modified relative to their values
       * @return the color as integer (8 bit each a, r, g, b)
       */
      public static int getColor(String color, Color.HSV hsv, boolean relative) {
          if ("transparent".equals(color))
              return Color.get(0, 1, 1, 1);
  
          int c;
          if (color.charAt(0) == '#')
              c = Color.parseColor(color, Color.CYAN);
          else {
              Integer css = ColorsCSS.get(color);
              if (css == null) {
                  log.debug("unknown color:{}", color);
                  c = Color.CYAN;
              } else
                  c = css;
          }
  
          return hsv.mod(c, relative);
      }
  
      /**
       * @return the index of convex turn after specified index or null, if it's concave.
       */
      private static Integer getIndexNextConvexTurn(int index, List<Byte> simpleAngles) {
          for (int i = index + 1; i < simpleAngles.size() + index; i++) {
              int iMod = i % simpleAngles.size();
              if (simpleAngles.get(iMod) > 0) {
                  return iMod;
              } else if (simpleAngles.get(iMod) < 0) {
                  return null;
              }
          }
          return (index + 1) % simpleAngles.size();
      }
  
      /**
       * @return the index of next turn after specified index
       */
      private static Integer getIndexNextTurn(int index, List<Byte> simpleAngles) {
          for (int i = index + 1; i < simpleAngles.size() + index; i++) {
              int iMod = i % simpleAngles.size();
              if (simpleAngles.get(iMod) != 0) {
                  return iMod;
              }
          }
          return (index + 1) % simpleAngles.size();
      }
  
      /**
       * @return the index of previous convex turn at specified index
       */
      private static Integer getIndexPreviousConvexTurn(int index, List<Byte> simpleAngles) {
          for (int i = simpleAngles.size() + index - 1; i >= 0; i--) {
              int iMod = i % simpleAngles.size();
              if (simpleAngles.get(iMod) > 0) {
                  return iMod;
              } else if (simpleAngles.get(iMod) < 0) {
                  return null;
              }
          }
          return (simpleAngles.size() + index - 1) % simpleAngles.size();
      }
  
      /**
       * @return the best index to begin a calculation
       */
      private static Integer getIndexStart(List<Byte> simpleAngles, List<Float> lengths, boolean directionAcross) {
          int size = simpleAngles.size();
          Integer indexStart = null;
          Integer concaveStart = null;
          for (int i = 0; i < size; i++) {
              if (indexStart != null && concaveStart != null) break;
              if (indexStart == null && simpleAngles.get(i) > 1) {
                  // Use first angle as start index;
                  indexStart = i;
              } else if (concaveStart == null && simpleAngles.get(i) < -1) {
                  // A real concave corner
                  concaveStart = i;
              }
          }
  
          if (indexStart == null) {
              return null;
          }
  
          if (concaveStart != null) {
              // look for next convex shape (point)
              for (int i = concaveStart; i < size + indexStart; i++) {
                  if (simpleAngles.get(i % size) < 0) {
                      return i % size;
                  }
              }
          }
  
          // Calculate longest side with right angle next to it.
          int[] iLongSide = getIndicesLongestSide(simpleAngles, lengths, indexStart);
          if (simpleAngles.get(iLongSide[1]) < 2) {
              // If angle is not good to start a ridge use previous
              indexStart = getIndexPreviousConvexTurn(iLongSide[0], simpleAngles);
          } else {
              indexStart = iLongSide[1]; // Get side next to longest one
          }
  
          // Direction across only possible if no concave shapes are present
          if (directionAcross) {
              return iLongSide[0];
          }
  
          return indexStart;
      }
  
      /**
       * @param indexStart the start index, if already calculated (can be null)
       * @return int[0] = start index, int[1] = end index
       */
      private static int[] getIndicesLongestSide(List<Byte> simpleAngles, List<Float> lengths, Integer indexStart) {
          int[] iLongSide = new int[2];
          int size = simpleAngles.size();
          if (indexStart == null) {
              for (int i = 0; i < size; i++) {
                  if (simpleAngles.get(i) > 0) {
                      // Use first convex angle as start index;
                      indexStart = i;
                      break;
                  }
              }
          }
  
          float longestSideLength = 0, currentLength = 0;
          int indexCurrentSide = indexStart;
          int loopSize = size + indexStart;
          for (int i = indexStart; i < loopSize; i++) {
              if (i >= size) {
                  i -= size;
                  loopSize -= size;
              }
  
              if (simpleAngles.get(i) != 0) {
                  // Right angle
                  currentLength = lengths.get(i);
                  indexCurrentSide = i;
              } else {
                  currentLength += lengths.get(i);
              }
  
              if (currentLength > longestSideLength) {
                  longestSideLength = currentLength;
                  iLongSide[0] = indexCurrentSide;
                  iLongSide[1] = (i + 1) % size;
              }
          }
          return iLongSide;
      }
  
      /**
       * @param material the material as string (see http://wiki.openstreetmap.org/wiki/Key:material and following pages)
       * @param hsv      the HSV color values to modify given material
       * @return the color as integer (8 bit each a, r, g, b)
       */
      public static int getMaterialColor(String material, Color.HSV hsv, boolean relative) {
  
          int c;
          if (material.charAt(0) == '#') {
              c = Color.parseColor(material, Color.CYAN);
          } else {
              switch (material) {
                  case "roof_tiles":
                      c = Color.get(216, 167, 111);
                      break;
                  case "tile":
                      c = Color.get(216, 167, 111);
                      break;
                  case "concrete":
                  case "cement_block":
                      c = Color.get(210, 212, 212);
                      break;
                  case "metal":
                      c = 0xFFC0C0C0;
                      break;
                  case "tar_paper":
                      c = 0xFF969998;
                      break;
                  case "eternit":
                      c = Color.get(216, 167, 111);
                      break;
                  case "tin":
                      c = 0xFFC0C0C0;
                      break;
                  case "asbestos":
                      c = Color.get(160, 152, 141);
                      break;
                  case "glass":
                      c = Color.fade(Color.get(130, 224, 255), 0.6f);
                      break;
                  case "slate":
                      c = 0xFF605960;
                      break;
                  case "zink":
                      c = Color.get(180, 180, 180);
                      break;
                  case "gravel":
                      c = Color.get(170, 130, 80);
                      break;
                  case "copper":
                      // same as roof color:green
                      c = Color.get(150, 200, 130);
                      break;
                  case "wood":
                      c = Color.get(170, 130, 80);
                      break;
                  case "grass":
                      c = 0xFF50AA50;
                      break;
                  case "stone":
                      c = Color.get(206, 207, 181);
                      break;
                  case "plaster":
                      c = Color.get(236, 237, 181);
                      break;
                  case "brick":
                      c = Color.get(255, 217, 191);
                      break;
                  case "stainless_steel":
                      c = Color.get(153, 157, 160);
                      break;
                  case "gold":
                      c = 0xFFFFD700;
                      break;
                  default:
                      c = Color.CYAN;
                      log.debug("unknown material:{}", material);
                      break;
              }
          }
  
          return hsv.mod(c, relative);
      }
  
      /**
       * Get the profile (half cross section) of roof shape.
       * profile[i][0]: x-coordinate
       * profile[i][1]: z-coordinate
       *
       * @param roofShape the roof shape value
       * @return the profile (read-only!)
       */
      public static float[][] getProfile(String roofShape) {
          switch (roofShape) {
              case Tag.VALUE_ONION:
                  return PROFILE_ONION;
              case Tag.VALUE_ROUND:
              case Tag.VALUE_DOME:
                  return PROFILE_DOME;
              case Tag.VALUE_SALTBOX:
                  return PROFILE_SALTBOX;
              case Tag.VALUE_MANSARD:
              case Tag.VALUE_GAMBREL:
                  return PROFILE_MANSARD;
              case Tag.VALUE_GABLED:
              case Tag.VALUE_HIPPED:
              default:
                  return PROFILE_HIPPED;
          }
      }
  
      /**
       * @param normVectors the normalized vectors
       * @return a list of simple angles:
       * 0           straight
       * (+/-) 1     (convex/concave) obtuse angle
       * (+/-) 2     (convex/concave) right angle (or acute angle)
       * <p>
       * Note lhs coordinate system.
       * convex: turns right
       * concave: turns left
       */
      private static List<Byte> getSimpleAngles(List<float[]> normVectors) {
          int size = normVectors.size();
          // List<Float> angles = new ArrayList<>();
          List<Byte> simpAngls = new ArrayList<>();
          float tmpAnlgeSum = 0;
          float threshold = MathUtils.PI / 12;
          for (int k = 0; k < size; k++) {
              // Check angle between next and this vector
              float[] v2 = normVectors.get(k);
              float[] v1 = normVectors.get((k - 1 + size) % size);
              float val = v1[0] * v2[0] + v1[1] * v2[1];
              float angle = (float) Math.acos(Math.abs(val) > 1 ? Math.signum(val) : val);
              // angles.add(angle);
  
              // Positive turns right (convex), negative turns left (concave)
              byte simpAngle = (byte) Math.signum(v1[0] * v2[1] - v1[1] * v2[0]);
              if (angle > (MathUtils.PI / 2) - threshold) {
                  // Right angle
                  simpAngle *= 2;
                  tmpAnlgeSum = 0;
              } else if (angle < threshold) {
                  tmpAnlgeSum += simpAngle * angle; // Many small angles, indicate a corner
                  if (Math.abs(tmpAnlgeSum) > threshold) {
                      // Can improve sum of concave/convex shapes
                      simpAngle = (byte) Math.signum(tmpAnlgeSum);
                      tmpAnlgeSum = 0;
                  } else
                      simpAngle = 0;
              } else {
                  // Angle which is not right, and not straight
                  tmpAnlgeSum = 0;
              }
  
              simpAngls.add(simpAngle);
          }
  
          return simpAngls;
      }
  
      private static GeometryBuffer initCircleMesh(float[][] profile, int numSections) {
          int indexSize = numSections * (profile.length - 1) * 2 * 3; // * 2 faces * 3 vertices
          int[] meshIndex = new int[indexSize];
  
          int meshSize = numSections * profile.length;
          float[] meshPoints = new float[meshSize * 3];
          for (int i = 0; i < numSections; i++) {
              for (int j = 0; j < profile.length; j++) {
                  // Write point mesh
                  int pPos = 3 * (i * profile.length + j);
                  meshPoints[pPos + 0] = profile[j][0];
                  meshPoints[pPos + 1] = 0f;
                  meshPoints[pPos + 2] = profile[j][1];
  
                  // Write point indices
                  if (j != profile.length - 1) {
                      int iPos = 6 * (i * (profile.length - 1) + j); // 6 = 2 * Mesh * 3PointsPerMesh
                      pPos = pPos / 3;
                      meshIndex[iPos + 2] = pPos + 0;
                      meshIndex[iPos + 1] = pPos + 1;
                      meshIndex[iPos + 0] = (pPos + profile.length) % meshSize;
  
                      // FIXME if is last point, only one tris is needed, if top shape is closed
                      meshIndex[iPos + 5] = pPos + 1;
                      meshIndex[iPos + 4] = (pPos + profile.length + 1) % meshSize;
                      meshIndex[iPos + 3] = (pPos + profile.length) % meshSize;
                  }
              }
  
          }
  
          return new GeometryBuffer(meshPoints, meshIndex);
      }
  
      private static boolean isGabled(String roofShape) {
          switch (roofShape) {
              case Tag.VALUE_ROUND:
              case Tag.VALUE_SALTBOX:
              case Tag.VALUE_GABLED:
              case Tag.VALUE_GAMBREL:
                  return true;
              case Tag.VALUE_MANSARD:
              case Tag.VALUE_HALF_HIPPED:
              case Tag.VALUE_HIPPED:
              default:
                  return false;
          }
      }
  
      private static void mergeMeshGeometryBuffer(GeometryBuffer gb1, GeometryBuffer gb2, GeometryBuffer out) {
          if (!(gb1.isTris() && gb2.isTris())) return;
          int gb1PointSize = gb1.points.length;
          float[] mergedPoints = new float[gb1PointSize + gb2.points.length];
          System.arraycopy(gb1.points, 0, mergedPoints, 0, gb1PointSize);
          System.arraycopy(gb2.points, 0, mergedPoints, gb1PointSize, gb2.points.length);
          out.points = mergedPoints;
          out.pointNextPos = mergedPoints.length;
  
          int gb1IndexSize = gb1.index.length;
          int[] mergedIndices = new int[gb1IndexSize + gb2.index.length];
          System.arraycopy(gb1.index, 0, mergedIndices, 0, gb1IndexSize);
          gb1PointSize /= 3; // (x,y,z)
          for (int k = 0; k < gb2.index.length; k++) {
              mergedIndices[gb1IndexSize + k] = gb2.index[k] + gb1PointSize;
          }
          out.index = mergedIndices;
          out.type = gb1.type;
      }
  
      private S3DBUtils() {
      }
  }
  