/**
   * Methode analysiert die Grundrissebenen und verwendet deren Normalenvektoren, um zu bestimmen,
   * welcher Koordinatenebene deren Ausrichtungen am naechsten kommen, indem die Komponente mit dem
   * groessten absoluten Wert des Normalenvektors ermittelt wird. Diese wird zurueckgereicht
   *
   * @param bucket Bucket mit Grundrissen, die innerhalb der Methode gemerged werden
   * @return Achse, die fuer eine Projektion ignoriert werden koennte
   */
  private Axis findRelevantComponent(FootprintBucket bucket) {

    Footprint currentFootprint = null;
    MyVector3f planeNormal = null;
    List<Footprint> footprints = bucket.getFootprints();
    Iterator<Footprint> footprintIter = footprints.iterator();
    Axis relevantComponent = Axis.UNKNOWN;
    Axis resultComponent = Axis.UNKNOWN;
    while (footprintIter.hasNext()) {
      currentFootprint = footprintIter.next();

      // hole die Normale und suche eine Komponente mit Wert 0
      planeNormal = currentFootprint.getFootprintPoly().getNormal();

      // bestimme die Koordinatenkomponente mit dem groessten absoluten
      // Wert
      relevantComponent = mMathHelper.getIgnorableAxis(planeNormal, false);

      // validiere, dass alle Grundrisse die gleiche Ausrichtung besitzen
      if (resultComponent != Axis.UNKNOWN && relevantComponent != resultComponent) {
        assert false
            : "Planes mit unterschiedlichen Ausrichtungen der Normalenvektoren im gleichen Bucket";
      } else resultComponent = relevantComponent;
    }
    return resultComponent;
  }
  /**
   * 2. Votingverfahren fuer Strahlauswahl: Methode testet, ob der getroffene Strahl und der
   * Ausgangsstrahl Teil des gleichen Ausgangspolygons sind
   *
   * @param currentHit Treffer-Instanz, fuer deren Strahl getestet wird, ob dieser zum gleichen
   *     Polygon gehoert, wie der Ausgangsstrahl
   * @param currentRay Strahlen-Instanz, fuer die ein Nachfolger gesucht wird
   * @param bucket Eimer, fuer den aktuell ein gemergter Grundriss errechnet wird
   * @return True, falls beide Strahlen Kanten des gleichen Polygons sind, False sonst
   */
  private boolean isPartOfSameObject(
      final Hit currentHit, final Ray currentRay, final FootprintBucket bucket) {

    // bestimme die Quellpolygone fuer den Trefferstrahl und den Teststrahl
    List<Footprint> footprints = bucket.getFootprints();
    MyPolygon currentPolygon = null;
    Ray hitRay = currentHit.getHitRay();

    LOGGER.debug("CURRENT RAY: " + currentRay + " HITRAY: " + hitRay);

    for (int i = 0; i < footprints.size(); i++) {
      currentPolygon = footprints.get(i).getFootprintPoly();

      // wenn beide Strahlen im Polygon enthalten sind, gebe True zurueck
      if (currentPolygon.isRayInPolygon(hitRay) && currentPolygon.isRayInPolygon(currentRay)) {
        LOGGER.debug("SAME POLY");
        LOGGER.debug("CUR POLY: " + currentPolygon);
        return true;
      }
    }
    return false;
  }
  /**
   * Methode sucht innerhalb der Footprintstrukturen nach einem Strahl, von dem aus der
   * Schnittalgorithmus starten kann. Der Startpunkt des Verfahrens muss ausserhalb aller anderen
   * Grundrisse liegen. Ein solcher Punkt muss innerhalb der Ebene, in der der Grundriss liegt,
   * einen Extremwert aufweisen.
   *
   * @param bucket Eimer mit einer Menge von Grundrissen, fuer die ein gemergter Grundriss erstellt
   *     werden soll
   * @return Strahl, von dem ausgehend die Schnittberechnung erfolgt
   */
  private Ray findStartRay(FootprintBucket bucket) {

    Axis ignorableAxis = findRelevantComponent(bucket);

    // sammele alle Rays in einer grossen Liste
    List<Ray> allRays = new ArrayList<Ray>();
    List<Footprint> allFootprints = bucket.getFootprints();
    Iterator<Footprint> footprintIter = allFootprints.iterator();
    while (footprintIter.hasNext()) {
      allRays.addAll(footprintIter.next().getRays());
    }

    List<Ray> rayBuffer = new ArrayList<Ray>();

    // initialisiere auf maximal moeglichen Float-Wert
    float minValueForRelevantComponent = Float.MAX_VALUE;
    Ray currentStart = null, currentRay = null;
    MyVector3f currentPos = null;

    // suche jetzt den Strahl, der bezueglich der relevanten Komponente den
    // kleinsten Wert besitzt, dies ist der Start-Strahl
    Iterator<Ray> rayIter = allRays.iterator();
    while (rayIter.hasNext()) {
      currentRay = rayIter.next();
      currentPos = currentRay.getStartPtr();

      // durchlaufe alle Rays und suche denjenigen, der zunaechst in einer
      // Komponente den kleinsten Wert besitzt
      // die Entscheidung, welche Komponente getestet wird, haengt von der
      // Achse ab, die vorab als "ignorierbar" bestimmt wurde
      switch (ignorableAxis) {
          // x
        case X:
          if (currentPos.z <= minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.z;
            // adde den Ray am Start des Buffers => dadurch befinden
            // sich die Rays mit den kleinsten Werten am Ende vorne
            rayBuffer.add(0, currentRay);
          }
          break;
          // y
        case Y:
          if (currentPos.x <= minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.x;
            rayBuffer.add(0, currentRay);
          }
          break;
          // z
        case Z:
          if (currentPos.y <= minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.y;
            rayBuffer.add(0, currentRay);
          }
          break;
          // Fehler
        default:
          assert false : "Die berechnete relevante Komponente ist ungueltig: " + ignorableAxis;
          break;
      }
    }

    // durchlaufe den Buffer und pruefe, ob mehrere Rays mit gleicher
    // Komponente vorkommen
    rayIter = rayBuffer.iterator();
    minValueForRelevantComponent = Float.MAX_VALUE;

    // entferne nun alle Strahlen, deren Wert ueber dem aktuellen Minimum
    // liegt
    while (rayIter.hasNext()) {
      currentRay = rayIter.next();
      currentPos = currentRay.getStartPtr();

      switch (ignorableAxis) {
          // x
        case X:
          if (currentPos.z <= minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.z;
          }
          // Komponentenwert liegt ueber dem aktuellen Minimum =>
          // entfernen
          else rayIter.remove();
          break;
          // y
        case Y:
          if (currentPos.x <= minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.x;
          } else rayIter.remove();
          break;
          // z
        case Z:
          if (currentPos.y <= minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.y;
          } else rayIter.remove();
          break;
      }
    }

    // wenn es nur noch einen Strahl gibt, hat man sein Ergebnis
    if (rayBuffer.size() == 1) return rayBuffer.get(0);

    // Kontrollwert zuruecksetzen
    minValueForRelevantComponent = Float.MAX_VALUE;

    // sonst muss noch eine zweite Komponente geprueft werden
    // erneuter Switch mit Test auf die verbleibende Komponente:
    rayIter = rayBuffer.iterator();
    while (rayIter.hasNext()) {
      currentRay = rayIter.next();
      currentPos = currentRay.getStartPtr();
      switch (ignorableAxis) {
          // x
        case X:
          if (currentPos.y < minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.y;
            currentStart = currentRay;
          }
          break;
          // y
        case Y:
          if (currentPos.z < minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.z;
            currentStart = currentRay;
          }
          break;
          // z
        case Z:
          if (currentPos.x < minValueForRelevantComponent) {
            minValueForRelevantComponent = currentPos.x;
            currentStart = currentRay;
          }
          break;
      }
    }

    // jetzt hat man einen Strahl, der in beiden Komponenten minimal ist
    return currentStart;
  }
  /**
   * Methode berechnet fuer den uebergebenen Grundriss-Bucket einen gemergeten Grundriss und gibt
   * diesen als Vektor von Vertex3d-Instanzen zurueck.
   *
   * @param bucket Bucket mit Grundrissen, die innerhalb der Methode gemerged werden
   * @return Vektor mit Vertex3d-Instanzen, die den gemergten Grundriss beschreiben
   */
  public List<Vertex3d> computeMergedFootprint(FootprintBucket bucket) {

    LOGGER.info("Footprintbucket enthaelt: " + bucket.getFootprints().size() + " Eingabepolygone!");
    Ray startRay = findStartRay(bucket);

    LOGGER.trace("Startray fuer Schnittberechnungen: " + startRay);
    assert startRay != null : "FEHLER: Es konnte kein Anfangsstrahl ermittelt werden!";

    List<Vertex3d> resultVertices = new ArrayList<Vertex3d>();

    // sammele erneut alle Strahlen aller Grundrisse ein
    List<Ray> allRays = new ArrayList<Ray>();
    List<Footprint> allFootprints = bucket.getFootprints();
    Iterator<Footprint> footprintIter = allFootprints.iterator();
    while (footprintIter.hasNext()) {
      allRays.addAll(footprintIter.next().getRays());
    }

    Iterator<Ray> rayIter = allRays.iterator();
    LOGGER.trace("Insgesamt befinden sich " + allRays.size() + " Rays im Eimer");

    while (rayIter.hasNext()) {
      LOGGER.trace(rayIter.next());
    }

    Ray currentRay = null, lastRay = null, intersectionRay = null;
    MyVector3f intersectionPoint = null;
    Vertex3d resultVertex = null;

    // fuege Startpunkt des Startstrahls als erstes Vertex hinzu
    resultVertex = new Vertex3d(startRay.getStart());
    resultVertices.add(resultVertex);

    // Steuervariable fuer Iterationen
    Boolean doContinue = true;

    // erster Treffer des Startstrahls, wird fuer Abbruchkriterium benoetigt
    MyVector3f firstHit = null;

    // Iteration laeuft so lange, bis Abbruchkriterium erfuellt wurde
    while (doContinue) {
      // 1. Iteration
      if (currentRay == null) {
        currentRay = startRay;
      }

      Hit resultHit = findIntersectingRay(currentRay, bucket, lastRay, intersectionPoint);

      assert resultHit != null : "Es konnte kein Schnittstrahl ermittelt werden";
      // System.out.println("Gefundener Strahl: " + intersectionRay);

      intersectionRay = resultHit.getHitRay();
      intersectionPoint = resultHit.getIntersection();

      // speichere den ersten gefundenen Treffer
      if (firstHit == null) firstHit = intersectionPoint;

      // erzeuge eine Vertex3d-Instanz fuer den Schnittpunkt und fuege sie
      // zum Result-Vektor hinzu
      resultVertex = new Vertex3d(intersectionPoint);

      // breche ab, wenn ein Vertex geadded werden soll, das bereits
      // vorhanden ist
      LOGGER.trace("Adde Vertex: " + resultVertex);
      if (resultVertices.contains(resultVertex)) break;
      else resultVertices.add(resultVertex);

      // naechste Iteration vorbereiten
      lastRay = currentRay;
      currentRay = intersectionRay;

      // Abbruchkriterium pruefen
      doContinue = checkTermination(startRay, firstHit, currentRay, intersectionPoint);
    }

    return resultVertices;
  }
  /**
   * Methode durchlaeuft alle Strahlen im uebergebenen Vektor und testet auf Schnitte mit dem
   * uebergebenen Strahl. Hierbei wird immer der Strahl zurueckgegeben, dessen Schnitt mit dem
   * uebergebenen Strahl dem Ausgangspunkt am naechsten liegt (darum kann man auch nicht abbrechen,
   * sobald man den ersten Schnitt gefunden hat)
   *
   * @param current Strahl, fuer den im Strahlenvektor nach Schnitten gesucht wird
   * @param bucket Eimer, fuer den der gemergte Grundriss berechnet wird und der alle Strahlen fuer
   *     diese Rechnung enthaelt
   * @param lastIntersected Im letzten Durchlauf geschnittener Strahl. So wird ein Ping-Pong-Effekt
   *     zwischen Iterationen vermieden
   * @param lastIntersection Vektor, der den letzten berechneten Schnittpunkt beschreibt
   * @return Hit-Datenstruktur, die den getroffenen Strahl sowie den Schnittpunkt enthaelt
   */
  private Hit findIntersectingRay(
      final Ray current,
      final FootprintBucket bucket,
      final Ray lastIntersected,
      final MyVector3f lastIntersection) {

    Ray currentTestRay = null;
    float currentIntersectionDistance = Float.MAX_VALUE,
        lastIntersectionDistance = -Float.MAX_VALUE;
    MyVector3f currentIntersection = null;

    List<Hit> hits = new ArrayList<Hit>();
    Hit currentHit = null;

    LOGGER.debug("Aktueller Teststrahl: " + current);
    LOGGER.debug("Letzter Schnittpunkt: " + lastIntersection);

    Iterator<Ray> allRayIter = bucket.getAllRays().iterator();
    while (allRayIter.hasNext()) {
      currentTestRay = allRayIter.next();

      // sich selber nicht testen
      if (currentTestRay.equals(current)) continue;

      // den zuletzt geschnittenen Strahl ebenfalls ueberspringen
      if (currentTestRay.equals(lastIntersected)) continue;

      // sonst Schnittpunkt berechnen
      currentIntersection =
          mMathHelper.calculateRay2RayIntersectionApproximation(currentTestRay, current);

      // Entfernung des letzten Schnittpunkts zum Startpunkt bestimmen
      if (lastIntersection != null)
        lastIntersectionDistance =
            mMathHelper.calculatePointPointDistance(lastIntersection, current.getStart());

      if (currentIntersection != null) {

        // das Verfahren wird einen Strahl finden, der den Startpunkt
        // des aktuellen Strahls als Endpunkt besitzt, diesen
        // ueberspringen
        if (currentIntersection.equals(current.getStart())) continue;

        // wenn Vertices auf Strahlen liegen, kann der Fall auftreten,
        // dass der Schnittpunkt des letzten Durchlaufs erneut gefunden
        // wird
        if (currentIntersection.equals(lastIntersection)) continue;

        // logger.trace("CurrentIntersection: " + currentIntersection);

        // befindet sich der Strahl auf den Liniensegmenten beider
        // Teststrahlen?
        if (mMathHelper.isPointOnLineSegment(currentIntersection, currentTestRay)
            && mMathHelper.isPointOnLineSegment(currentIntersection, current)) {

          // sortiere solche Hits aus, bei denen der Schnittpunkt der
          // Endpunkt der getroffenen Kante ist
          double rayParameter =
              mMathHelper.calculateParameterOnRayForPoint(currentIntersection, currentTestRay);
          if (mMathHelper.isWithinTolerance(rayParameter, 1.0d, 0.001d)) {
            continue;
          }

          // Entfernung bestimmen
          currentIntersectionDistance =
              mMathHelper.calculatePointPointDistance(currentIntersection, current.getStart());

          // wenn der neue Schnittpunkt naeher am Startpunkt liegt,als
          // der im vorheringen Startpunkt berechnete, breche ab!
          // Dieser Schnittpunkt liegt auf dem Eingabestrahl und muss
          // nicht mit dem Startpunkt identisch sein, darum muss
          // jeder neue Schnittpunkt, der auf dem Strahl ermittelt
          // wird, zwingend weiter vom Start des Eingabestrahls
          // entfernt liegen
          if (lastIntersectionDistance > currentIntersectionDistance) {
            continue;
          }

          // erzeuge eine Treffer-Instanz
          currentHit = new Hit(currentTestRay, currentIntersectionDistance, currentIntersection);
          if (!hits.contains(currentHit)) {
            hits.add(currentHit);
            // logger.info("Kandidat: " + currentHit);
          }

          // suche nach Strahlen, die eine Verlaengerung des
          // Ausgangsstrahls sind und somit durch Schnittpunkttests
          // nicht gefunden werden koennen
          List<Ray> additionalRays =
              searchForRaysContainingGivenPoint(currentIntersection, current, bucket.getAllRays());

          // wenn Strahlen gefunden wurden, adde sie zum Hit-Vetor
          if (additionalRays.size() > 0) {
            Ray additionalRay = null;
            Iterator<Ray> additionalRayIter = additionalRays.iterator();
            while (additionalRayIter.hasNext()) {
              additionalRay = additionalRayIter.next();
              currentHit = new Hit(additionalRay, currentIntersectionDistance, currentIntersection);
              if (!hits.contains(currentHit)) {
                hits.add(currentHit);
                // logger.info("Kandidat: " + currentHit);
              }
            }
          }
        }
      }
    }

    LOGGER.debug("Insgesamt wurden " + hits.size() + " potentielle Kandidaten bestimmt.");

    // wenn kein Strahl gefunden wurde, werfe eine Exception
    assert hits.size() > 0
        : "Fehler: Es konnte kein Strahl gefunden werden, der sich mit dem Eingabestrahl schneidet! Eingabe: "
            + current;

    /*
     * logger.info("Potentielle Treffer: "); for(int i = 0; i < hits.size();
     * i++) logger.info(hits.get(i));
     */
    // es wurde nur ein Strahl getroffen
    if (hits.size() == 1) {
      return hits.get(0);
    }

    currentHit = chooseResultRay(hits, current, bucket);
    if (currentHit == null) return hits.get(0);
    else return currentHit;
  }