/**
   * 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;
  }
  /**
   * 1. Votingverfahren fuer Strahl-Auswahl: Verfahren testet, ob der getroffene Strahl und der
   * Ausgangsstrahl die gleiche Richtung besitzen
   *
   * @param currentHit Treffer-Instanz, deren Strahlenrichtung ueberprueft wird
   * @param currentRay Strahlen-Instanz, fuer die ein Nachfolger gesucht wird
   * @return True, falls der Ausgangsstrahl die gleiche Richtung besitzt, wie der getroffene Strahl,
   *     False sonst
   */
  private boolean hasSameDirection(Hit currentHit, Ray currentRay) {

    MyVector3f currentHitDirection = currentHit.getHitRay().getDirection();
    MyVector3f currentRayDirection = currentRay.getDirection();

    // teste, ob die Strahlen parallel sind
    return mMathHelper.isParallel(currentHitDirection, currentRayDirection);
  }
  /**
   * Methode testet die Abbruchkriterien fuer die Footprintberechnung. Ein Abbruch findet statt,
   * falls der aktuelle Ray mit dem dem StartRay uebereinstimmt und der Treffer nicht hinter dem
   * ersten berechneten Treffer fuer den StartRay liegt.
   *
   * @param startRay Ausgangsstrahl, von dem ausgehend der Berechnungsalgorithmus ermittelt wird
   * @param firstHit Erster Treffer auf dem Ausgangsstrahl, wird als Abbruchkriterium verwendet
   * @param currentRay Ausgangsstrahl der naechsten Iteration
   * @param currentHit Ermittelter Treffer auf dem aktuellen Strahl
   * @return True, falls Start- und Current-Ray nicht identisch sind oder der aktuelle Treffer
   *     hinter dem ersten Treffer auf dem Startstrahl liegt, False sonst
   */
  private Boolean checkTermination(
      Ray startRay, MyVector3f firstHit, Ray currentRay, MyVector3f currentHit) {

    // sind Start- und Current-Ray identisch, teste, ob der Treffer hinter
    // dem ersten Treffer auf dem Strahl liegt
    if (startRay.equals(currentRay)) {
      Double firstHitParameter = mMathHelper.calculateParameterOnRayForPoint(firstHit, startRay);
      Double currentHitParameter =
          mMathHelper.calculateParameterOnRayForPoint(currentHit, currentRay);

      // wenn der Parameter des aktuellen Hits groesser ist, liegt der
      // Treffer hinter dem ersten Treffer
      if (currentHitParameter > firstHitParameter) return true;
      else return false;
    }
    // wenn der aktuelle Strahl vom Startstrahl abweicht, rechne weiter
    else return true;
  }
    /**
     * Methode prueft, ob der gesetzte Schnittpunkt Startpunkt des getroffenen Strahls ist. Sofern
     * dies der Fall ist, wird das Start-Flag gesetzt.
     */
    private void isPointStart() {

      // teste, ob der Schnittpunkt dem Startpunkt entspricht
      MyVector3f rayStart = mHitRay.getStart();
      if (rayStart.equals(mIntersection)) {
        mIsStart = true;
        return;
      }

      MyVectormath mathHelper = MyVectormath.getInstance();

      // teste nun, ob die Distanz zwischen den Punkten innerhalb eines
      // Toleranzbereichs liegt
      // sofern dies der Fall ist, werden die Punkte als "gleich"
      // angesehen
      float distance = mathHelper.calculatePointPointDistance(rayStart, mIntersection);
      if (mathHelper.isWithinTolerance(distance, 0.0f, 0.01f)) {
        mIsStart = true;
        return;
      }

      mIsStart = false;
    }
  /**
   * Methode sucht nach Strahlen, die den uebergebenen Schnittpunkt enthalten. Ziel ist es,
   * Verlaengerungen des Eingabestrahls zu entdecken, die durch die Schnittpunkttests nicht bestimmt
   * werden koennen, da es fuer die Gleichungssysteme keine gueltige Loesung gibt.
   *
   * @param intersection Schnittpunkt, der im aktuellen Durchlauf bestimmt wurde
   * @param current Strahl, fuer den nach Schnittpunkten gesucht wird
   * @param allRays Vector mit allen Strahlen, die im aktuell verarbeiteten Bucket enthalten sind
   * @return Vector mit allen Strahlen, die den Schnittpunkt enthalten
   */
  private List<Ray> searchForRaysContainingGivenPoint(
      MyVector3f intersection, Ray current, List<Ray> allRays) {

    List<Ray> result = new Vector<Ray>();
    Iterator<Ray> rayIter = allRays.iterator();
    Ray currentRay = null;

    while (rayIter.hasNext()) {
      currentRay = rayIter.next();
      // skippe den Eingabestrahl
      if (currentRay.equals(current)) continue;
      if (mMathHelper.isPointOnRay(intersection, currentRay)) {
        if (mMathHelper.isPointOnLineSegment(intersection, currentRay)) {
          // sortiere solche Hits aus, bei denen der Schnittpunkt der
          // Endpunkt der getroffenen Kante ist
          double rayParameter =
              mMathHelper.calculateParameterOnRayForPoint(intersection, currentRay);
          if (mMathHelper.isWithinTolerance((float) rayParameter, 1.0f, 0.01f)) continue;
          result.add(currentRay);
        }
      }
    }
    return result;
  }
/**
 * @author Patrick Gunia Klasse implementiert ein Verfahren zur Berechnung von Grundrissen aus einer
 *     Menge komplexer Objekte. Das Verfahren durchlaeuft die Kanten aller Eingabegrundrisse und
 *     berechnet einen Polygonzug, der saemtliche eingegebenen Grundrisse enthaelt.
 */
public class FootprintMerger {

  /** Logging-Instanz */
  protected static Logger LOGGER = Logger.getLogger(FootprintMerger.class);

  /** Instanz der Mathebibliothek */
  private MyVectormath mMathHelper = MyVectormath.getInstance();

  // ------------------------------------------------------------------------------------------

  /**
   * 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 testet die Abbruchkriterien fuer die Footprintberechnung. Ein Abbruch findet statt,
   * falls der aktuelle Ray mit dem dem StartRay uebereinstimmt und der Treffer nicht hinter dem
   * ersten berechneten Treffer fuer den StartRay liegt.
   *
   * @param startRay Ausgangsstrahl, von dem ausgehend der Berechnungsalgorithmus ermittelt wird
   * @param firstHit Erster Treffer auf dem Ausgangsstrahl, wird als Abbruchkriterium verwendet
   * @param currentRay Ausgangsstrahl der naechsten Iteration
   * @param currentHit Ermittelter Treffer auf dem aktuellen Strahl
   * @return True, falls Start- und Current-Ray nicht identisch sind oder der aktuelle Treffer
   *     hinter dem ersten Treffer auf dem Startstrahl liegt, False sonst
   */
  private Boolean checkTermination(
      Ray startRay, MyVector3f firstHit, Ray currentRay, MyVector3f currentHit) {

    // sind Start- und Current-Ray identisch, teste, ob der Treffer hinter
    // dem ersten Treffer auf dem Strahl liegt
    if (startRay.equals(currentRay)) {
      Double firstHitParameter = mMathHelper.calculateParameterOnRayForPoint(firstHit, startRay);
      Double currentHitParameter =
          mMathHelper.calculateParameterOnRayForPoint(currentHit, currentRay);

      // wenn der Parameter des aktuellen Hits groesser ist, liegt der
      // Treffer hinter dem ersten Treffer
      if (currentHitParameter > firstHitParameter) return true;
      else return false;
    }
    // wenn der aktuelle Strahl vom Startstrahl abweicht, rechne weiter
    else return true;
  }

  // ------------------------------------------------------------------------------------------

  /**
   * 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;
  }

  // ------------------------------------------------------------------------------------------
  /**
   * Methode waehlt den Ergebnisstrahl aus dem uebergebenen Vektor aus und gibt diesen zurueck.
   * Dadurch kapselt die Methode die gesamte Auswahllogik.
   *
   * @param hits Liste mit saemtlichen Trefferinstanzen
   * @param current Strahl, fuer den nach Schnitten mit allen anderen Strahlen gesucht wird
   * @param bucket Eimer, fuer den aktuell ein gemergter Grundriss erstellt wird
   * @return Hit-Datenstruktur, die als Ausgangspunkt der naechsten Iteration dient
   */
  private Hit chooseResultRay(
      final List<Hit> hits, final Ray current, final FootprintBucket bucket) {

    Hit currentHit = null;

    // kommen mehrere Hits mit gleicher Distanz vor, fuellt man diese
    // zunaechst in einen Buffer-Vector um
    List<Hit> buffer = new ArrayList<Hit>();

    sortHitsByDistance(hits);
    float distance = Float.MAX_VALUE;

    // befuelle den Buffer-Vector mit allen Hits, die die minimale Distanz
    // besitzen
    // 1. Hit nehmen und distance initialisieren
    buffer.add(hits.get(0));
    distance = hits.get(0).getDistance();

    for (int i = 1; i < hits.size(); i++) {
      currentHit = hits.get(i);
      if (currentHit.getDistance() > distance) break;
      else buffer.add(currentHit);
    }

    hits.clear();

    // wurde nur ein Hit mit dieser Distanz gefunden, gebe diesen als
    // Ergebnis zurueck
    if (buffer.size() == 1) return buffer.get(0);

    // an diesem Punkt hat man mehrere Strahlen mit gleicher Distanz
    // fuehre einen Votingansatz durch, bei dem fuer jeden Strahl Tests
    // gerechnet werden => je nach Ausgang dieser Tests wird der Vote-Count
    // fuer den jeweiligen Treffer erhoeht, am Ende waehlt man den Strahl
    // mit den meisten Stimmen
    Iterator<Hit> hitIter = buffer.iterator();

    while (hitIter.hasNext()) {
      currentHit = hitIter.next();
      // bevorzuge Strahlen mit unterschiedlicher Richtung
      if (!hasSameDirection(currentHit, current)) currentHit.vote(1);

      // bevorzuge Strahlen, die nicht zum gleichen Objekt gehoeren, wie
      // der Ausgangsstrahl
      // es ist wichtiger, dass der Strahl nicht zum gleichen Objekt
      // gehoert, darum 2 Stimmen!
      if (!isPartOfSameObject(currentHit, current, bucket)) {
        currentHit.vote(2);
      }
    }

    // sortiere die Strahlen anhand der Stimmen, die sie durch die
    // Votingmethoden bekommen haben
    sortHitsByVoteCount(buffer);

    /*
     * logger.debug("Gevotete Hits: "); for (int i = 0; i < buffer.size();
     * i++) logger.debug(buffer.get(i));
     */
    return buffer.get(buffer.size() - 1);
  }

  // ------------------------------------------------------------------------------------------
  /**
   * Methode sucht nach Strahlen, die den uebergebenen Schnittpunkt enthalten. Ziel ist es,
   * Verlaengerungen des Eingabestrahls zu entdecken, die durch die Schnittpunkttests nicht bestimmt
   * werden koennen, da es fuer die Gleichungssysteme keine gueltige Loesung gibt.
   *
   * @param intersection Schnittpunkt, der im aktuellen Durchlauf bestimmt wurde
   * @param current Strahl, fuer den nach Schnittpunkten gesucht wird
   * @param allRays Vector mit allen Strahlen, die im aktuell verarbeiteten Bucket enthalten sind
   * @return Vector mit allen Strahlen, die den Schnittpunkt enthalten
   */
  private List<Ray> searchForRaysContainingGivenPoint(
      MyVector3f intersection, Ray current, List<Ray> allRays) {

    List<Ray> result = new Vector<Ray>();
    Iterator<Ray> rayIter = allRays.iterator();
    Ray currentRay = null;

    while (rayIter.hasNext()) {
      currentRay = rayIter.next();
      // skippe den Eingabestrahl
      if (currentRay.equals(current)) continue;
      if (mMathHelper.isPointOnRay(intersection, currentRay)) {
        if (mMathHelper.isPointOnLineSegment(intersection, currentRay)) {
          // sortiere solche Hits aus, bei denen der Schnittpunkt der
          // Endpunkt der getroffenen Kante ist
          double rayParameter =
              mMathHelper.calculateParameterOnRayForPoint(intersection, currentRay);
          if (mMathHelper.isWithinTolerance((float) rayParameter, 1.0f, 0.01f)) continue;
          result.add(currentRay);
        }
      }
    }
    return result;
  }

  // ------------------------------------------------------------------------------------------
  /**
   * 1. Votingverfahren fuer Strahl-Auswahl: Verfahren testet, ob der getroffene Strahl und der
   * Ausgangsstrahl die gleiche Richtung besitzen
   *
   * @param currentHit Treffer-Instanz, deren Strahlenrichtung ueberprueft wird
   * @param currentRay Strahlen-Instanz, fuer die ein Nachfolger gesucht wird
   * @return True, falls der Ausgangsstrahl die gleiche Richtung besitzt, wie der getroffene Strahl,
   *     False sonst
   */
  private boolean hasSameDirection(Hit currentHit, Ray currentRay) {

    MyVector3f currentHitDirection = currentHit.getHitRay().getDirection();
    MyVector3f currentRayDirection = currentRay.getDirection();

    // teste, ob die Strahlen parallel sind
    return mMathHelper.isParallel(currentHitDirection, currentRayDirection);
  }

  // ------------------------------------------------------------------------------------------
  /**
   * 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 sortiert alle gefundenen Treffer des Ausgangsstrahls mit allen anderen Strahlen
   * aufgrund ihrer Distanz zum Start der getroffenen Strahlen
   *
   * @param hits Liste mit allen Treffern
   */
  private void sortHitsByDistance(List<Hit> hits) {
    Collections.sort(
        hits,
        new Comparator<Hit>() {
          @Override
          public int compare(Hit o1, Hit o2) {
            return o1.getDistance().compareTo(o2.getDistance());
          }
        });
  }

  // ------------------------------------------------------------------------------------------

  /**
   * Methode sortiert alle gefundenen Treffer des Ausgangsstrahls mit allen anderen Strahlen
   * aufgrund der Anzahl der Stimmen, die sie waehrend der Strahlauswahl erhalten haben
   *
   * @param hits Vector mit allen Treffern
   */
  private void sortHitsByVoteCount(List<Hit> hits) {
    Collections.sort(
        hits,
        new Comparator<Hit>() {
          @Override
          public int compare(Hit o1, Hit o2) {
            return o1.getVoteCount().compareTo(o2.getVoteCount());
          }
        });
  }

  // ------------------------------------------------------------------------------------------

  /**
   * 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 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;
  }

  // ------------------------------------------------------------------------------------------

  /**
   * @author Patrick Gunia
   *     <p>Instanzen dieser Klasse speichern alle Informationen ueber gefundene Treffer waehrend
   *     der Schnittpunkberechnungen zwischen Strahlen waehrend der Footprintbestimmung
   */
  private class Hit {

    /** Strahl, mit dem der Schnittpunkt berechnet wurde */
    private Ray mHitRay = null;

    /**
     * Entfernung des gefundenen Schnittpunkts vom Ausgangsstrahl, also des Strahls, fuer den
     * Schnitte gesucht werden
     */
    private Float mDistance = null;

    /** Speichert, ob der gefundene Schnittpunkt Startpunkt des gespeicherten Strahls ist */
    private boolean mIsStart = false;

    /** Berechneter Schnittpunkt */
    private MyVector3f mIntersection = null;

    /**
     * Fuer die Treffer werden unterschiedliche Tests durchgefuehrt, um zu entscheiden, welcher
     * Treffer Ausgnagspunkt fuer die naechste Iteration ist. Bei jedem Test, der fuer einen Hit
     * entscheided, inkrementiert man den Hit-Count und waehlt anschliessend den Strahl mit den
     * meisten Stimmen
     */
    private int mVoteCount = 0;

    // ------------------------------------------------------------------------------------------

    /**
     * @param mHitRay
     * @param mDistance
     * @param mIsStart
     */
    public Hit(Ray mHitRay, float mDistance, MyVector3f intersection) {
      super();
      this.mHitRay = mHitRay;
      this.mDistance = mDistance;
      this.mIntersection = intersection;

      // stelle fest, ob der gefundene Schnittpunkt Startpunkt des
      // getroffenen Strahls ist
      isPointStart();
    }

    // ------------------------------------------------------------------------------------------
    /** @return the mHitRay */
    public Ray getHitRay() {
      return mHitRay;
    }

    // ------------------------------------------------------------------------------------------

    /** @return the mDistance */
    public Float getDistance() {
      return mDistance;
    }

    // ------------------------------------------------------------------------------------------

    /** @return the mIsStart */
    public boolean isStart() {
      return mIsStart;
    }

    // ------------------------------------------------------------------------------------------
    /** @return the mIntersection */
    public MyVector3f getIntersection() {
      return mIntersection;
    }

    // ------------------------------------------------------------------------------------------
    /**
     * Methode prueft, ob der gesetzte Schnittpunkt Startpunkt des getroffenen Strahls ist. Sofern
     * dies der Fall ist, wird das Start-Flag gesetzt.
     */
    private void isPointStart() {

      // teste, ob der Schnittpunkt dem Startpunkt entspricht
      MyVector3f rayStart = mHitRay.getStart();
      if (rayStart.equals(mIntersection)) {
        mIsStart = true;
        return;
      }

      MyVectormath mathHelper = MyVectormath.getInstance();

      // teste nun, ob die Distanz zwischen den Punkten innerhalb eines
      // Toleranzbereichs liegt
      // sofern dies der Fall ist, werden die Punkte als "gleich"
      // angesehen
      float distance = mathHelper.calculatePointPointDistance(rayStart, mIntersection);
      if (mathHelper.isWithinTolerance(distance, 0.0f, 0.01f)) {
        mIsStart = true;
        return;
      }

      mIsStart = false;
    }

    // ------------------------------------------------------------------------------------------
    /**
     * Inkemenentiert den Vote-Count fuer den aktuellen Hit um den uebergebenen Wert
     *
     * @param voteCount Anzahl der Stimmen, um die der Count geaendert wird, kann auch negativ sein
     */
    public void vote(int voteCount) {
      mVoteCount += voteCount;
    }

    // ------------------------------------------------------------------------------------------

    /** @return the mVoteCount */
    public Integer getVoteCount() {
      return mVoteCount;
    }

    // ------------------------------------------------------------------------------------------

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
      return "Hit: Strahl: "
          + mHitRay
          + ", Distanz:"
          + mDistance
          + ", Schnittpunkt:"
          + mIntersection
          + ", Votes: "
          + mVoteCount;
    }

    // ------------------------------------------------------------------------------------------

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + getOuterType().hashCode();
      result = prime * result + ((mDistance == null) ? 0 : mDistance.hashCode());
      result = prime * result + ((mHitRay == null) ? 0 : mHitRay.hashCode());
      result = prime * result + ((mIntersection == null) ? 0 : mIntersection.hashCode());
      return result;
    }

    // ------------------------------------------------------------------------------------------

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (obj == null) return false;
      if (getClass() != obj.getClass()) return false;
      Hit other = (Hit) obj;
      if (!getOuterType().equals(other.getOuterType())) return false;
      if (mDistance == null) {
        if (other.mDistance != null) return false;
      } else if (!mDistance.equals(other.mDistance)) return false;
      if (mHitRay == null) {
        if (other.mHitRay != null) return false;
      } else if (!mHitRay.equals(other.mHitRay)) return false;
      if (mIntersection == null) {
        if (other.mIntersection != null) return false;
      } else if (!mIntersection.equals(other.mIntersection)) return false;
      return true;
    }

    private FootprintMerger getOuterType() {
      return FootprintMerger.this;
    }
  }
  // ------------------------------------------------------------------------------------------
}
  /**
   * 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;
  }