/*
 * Copyright (c) 2006, Swedish Institute of Computer Science.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $Id: ChannelModel.java,v 1.3 2007/03/23 21:13:43 fros4943 Exp $
 */

package se.sics.mrm;

import java.awt.geom.*;
import java.util.*;
import javax.swing.tree.DefaultMutableTreeNode;
import org.apache.log4j.Logger;
import org.jdom.Element;
import statistics.GaussianWrapper;

/**
 * The channel model object in MRM is responsible for calulating propagation
 * impact on packets being sent in the radio medium.
 * 
 * By registering as a settings observer on this channel model, other parts will
 * be notified if the settings change.
 * 
 * TODO Add better support for different signal strengths
 * 
 * @author Fredrik Osterlind
 */
public class ChannelModel {
  private static Logger logger = Logger.getLogger(ChannelModel.class);

  enum TransmissionData { SIGNAL_STRENGTH, SIGNAL_STRENGTH_VAR, SNR, SNR_VAR, PROB_OF_RECEPTION, DELAY_SPREAD, DELAY_SPREAD_RMS}
  
  private Properties parameters = new Properties();
  private Properties parameterDescriptions = new Properties();
  
  // Parameters used for speeding up calculations
  private boolean needToPrecalculateFSPL = true;
  private static double paramFSPL = 0;
  private boolean needToPrecalculateOutputPower = true;
  private static double paramOutputPower = 0;
  
  private ObstacleWorld myObstacleWorld = new ObstacleWorld();
  
  // Ray tracing components temporary vector
  private boolean inLoggingMode = false;
  private Vector<Line2D> savedRays = null;
  private Vector<Vector<Line2D>> calculatedVisibleSides = new Vector<Vector<Line2D>>();
  private Vector<Point2D> calculatedVisibleSidesSources = new Vector<Point2D>();
  private Vector<Line2D> calculatedVisibleSidesLines = new Vector<Line2D>();
  private Vector<AngleInterval> calculatedVisibleSidesAngleIntervals = new Vector<AngleInterval>();
  private static int maxSavedVisibleSides = 30; // Max size of lists above
  
  /**
   * Notifies observers when this channel model has changed settings.
   */
  private class SettingsObservable extends Observable {
    private void notifySettingsChanged() {
      setChanged();
      notifyObservers();
    }
  }
  private SettingsObservable settingsObservable = new SettingsObservable();
  
  // A random number generator
  private Random random = new Random();
  
  public ChannelModel() {
    // - Set initial parameter values -
    
    // Using random variables
    parameters.put("apply_random", new Boolean(false)); // TODO Should not use random variables as default
    parameterDescriptions.put("apply_random", "Apply random values immediately");
    
    // Signal to noise reception threshold
    parameters.put("snr_threshold", new Double(6));
    parameterDescriptions.put("snr_threshold", "SNR reception threshold (dB)");

    // Background noise mean
    parameters.put("bg_noise_mean", new Double(-150));
    parameterDescriptions.put("bg_noise_mean", "Background noise mean (dBm)");
    
    // Background noise variance
    parameters.put("bg_noise_var", new Double(1));
    parameterDescriptions.put("bg_noise_var", "Background noise variance (dB)");
    
    // Extra system gain mean
    parameters.put("system_gain_mean", new Double(0));
    parameterDescriptions.put("system_gain_mean", "Extra system gain mean (dB)");
    
    // Extra system gain variance
    parameters.put("system_gain_var", new Double(4)); // TODO Should probably be default 0 or 1
    parameterDescriptions.put("system_gain_var", "Extra system gain variance (dB)");
    
    // Transmission wavelength
    parameters.put("wavelength", new Double(0.346)); // ~868 MHz (RFM TR1001)
    parameterDescriptions.put("wavelength", "Wavelength w (m)");

    // Transmitter output power
    parameters.put("tx_power", new Double(1.5)); // dBm (deciBel milliwatts)
    parameterDescriptions.put("tx_power", "Transmitter output power (dBm)");
    
    // Transmitter antenna gain
    parameters.put("tx_antenna_gain", new Double(0)); // TODO Should use angle
    parameterDescriptions.put("tx_antenna_gain", "Transmitter antenna gain (dB)");
    
    // Receiver sensitivity
    parameters.put("rx_sensitivity", new Double(-100));
    parameterDescriptions.put("rx_sensitivity", "Receiver sensitivity (dBm)");
    
    // Receiver antenna gain
    parameters.put("rx_antenna_gain", new Double(0)); // TODO Should use angle
    parameterDescriptions.put("rx_antenna_gain", "Receiver antenna gain (dB)");
    
    // Ray Tracer - Disallow direct path
    parameters.put("rt_disallow_direct_path", new Boolean(false));
    parameterDescriptions.put("rt_disallow_direct_path", "Disallow direct path");

    // Ray Tracer - If direct path exists, ignore the non-direct (used for debugging)
    parameters.put("rt_ignore_non_direct", new Boolean(false));
    parameterDescriptions.put("rt_ignore_non_direct", "If existing, only use direct path");

    // Ray Tracer - Use FSPL on total length only
    parameters.put("rt_fspl_on_total_length", new Boolean(true));
    parameterDescriptions.put("rt_fspl_on_total_length", "Use FSPL on total path lengths only");

    // Ray Tracer - Max number of subrays
    parameters.put("rt_max_rays", new Integer(1));
    parameterDescriptions.put("rt_max_rays", "Max path rays");

    // Ray Tracer - Max number of refractions
    parameters.put("rt_max_refractions", new Integer(1));
    parameterDescriptions.put("rt_max_refractions", "Max refractions");

    // Ray Tracer - Max number of reflections
    parameters.put("rt_max_reflections", new Integer(1));
    parameterDescriptions.put("rt_max_reflections", "Max reflections");

    // Ray Tracer - Max number of diffractions
    parameters.put("rt_max_diffractions", new Integer(0));
    parameterDescriptions.put("rt_max_diffractions", "Max diffractions");

    // Ray Tracer - Use scattering
    //parameters.put("rt_use_scattering", new Boolean(false)); // TODO Not used yet
    //parameterDescriptions.put("rt_use_scattering", "Use simple scattering");

    // Ray Tracer - Refraction coefficient
    parameters.put("rt_refrac_coefficient", new Double(-3));
    parameterDescriptions.put("rt_refrac_coefficient", "Refraction coefficient (dB)");

    // Ray Tracer - Reflection coefficient
    parameters.put("rt_reflec_coefficient", new Double(-5));
    parameterDescriptions.put("rt_reflec_coefficient", "Reflection coefficient (dB)");

    // Ray Tracer - Diffraction coefficient
    parameters.put("rt_diffr_coefficient", new Double(-10));
    parameterDescriptions.put("rt_diffr_coefficient", "Diffraction coefficient (dB)");

    // Ray Tracer - Scattering coefficient
    //parameters.put("rt_scatt_coefficient", new Double(-20)); // TODO Not used yet
    //parameterDescriptions.put("rt_scatt_coefficient", "!! Scattering coefficient (dB)");

    // Shadowing - Obstacle Attenuation constant
    parameters.put("obstacle_attenuation", new Double(-3));
    parameterDescriptions.put("obstacle_attenuation", "Obstacle attenuation (dB/m)");
  }
  
  /**
   * Adds a settings observer to this channel model.
   * Every time the settings are changed all observers
   * will be notified.
   * 
   * @param obs New observer
   */
  public void addSettingsObserver(Observer obs) {
    settingsObservable.addObserver(obs);
  }
  
  /**
   * Deletes an earlier registered setting observer.
   * 
   * @param osb
   *          Earlier registered observer
   */
  public void deleteSettingsObserver(Observer obs) {
    settingsObservable.deleteObserver(obs);
  }
  
  /**
   * Remove all previously registered obstacles
   */
  public void removeAllObstacles() {
    myObstacleWorld.removeAll();
    settingsObservable.notifySettingsChanged();
  }
  
  /**
   * Add new obstacle with a rectangle shape.
   * Notifies observers of the new obstacle.
   * 
   * @param startX Low X coordinate
   * @param startY Low Y coordinate
   * @param width Width of obstacle
   * @param height Height of obstacle
   */
  public void addRectObstacle(double startX, double startY, double width, double height) {
    addRectObstacle(startX, startY, width, height, true);
  }
  
  /**
   * Add new obstacle with a rectangle shape.
   * Notifies observers depending on given notify argument.
   * 
   * @param startX Low X coordinate
   * @param startY Low Y coordinate
   * @param width Width of obstacle
   * @param height Height of obstacle
   * @param notify If true, notifies all observers of this new obstacle
   */
  public void addRectObstacle(double startX, double startY, double width, double height, boolean notify) {
    myObstacleWorld.addObstacle(startX, startY, width, height);
    
    if (notify)
      settingsObservable.notifySettingsChanged();
  }
  
  /**
   * @return Number of registered obstacles
   */
  public int getNumberOfObstacles() {
    return myObstacleWorld.getNrObstacles();
  }
  
  /**
   * Returns an obstacle at given position
   * @param i Obstacle position
   * @return Obstacle
   */
  public Rectangle2D getObstacle(int i) {
    return myObstacleWorld.getObstacle(i);
  }
  
  /**
   * Returns a parameter value
   * 
   * @param identifier Parameter identifier
   * @return Current parameter value
   */
  public Object getParameterValue(String id) {
    Object value = parameters.get(id);
    if (value == null) {
      logger.fatal("No parameter with id:" + id + ", aborting");
      return null;
    }
    return value;
  }
  
  /**
   * Returns a double parameter value
   * 
   * @param identifier Parameter identifier
   * @return Current parameter value
   */
  public double getParameterDoubleValue(String id) {
    return ((Double) getParameterValue(id)).doubleValue();
  }
  
  /**
   * Returns an integer parameter value
   * 
   * @param identifier Parameter identifier
   * @return Current parameter value
   */
  public int getParameterIntegerValue(String id) {
    return ((Integer) getParameterValue(id)).intValue();
  }
  
  /**
   * Returns a boolean parameter value
   * 
   * @param identifier Parameter identifier
   * @return Current parameter value
   */
  public boolean getParameterBooleanValue(String id) {
    return ((Boolean) getParameterValue(id)).booleanValue();
  }
  
  /**
   * Saves a new parameter value
   * 
   * @param id Parameter identifier
   * @param newValue New parameter value
   */
  public void setParameterValue(String id, Object newValue) {
    if (!parameters.containsKey(id)) {
      logger.fatal("No parameter with id:" + id + ", aborting");
      return;
    }
    parameters.put(id, newValue);
    
    // Guessing we need to recalculate input to FSPL+Output power
    needToPrecalculateFSPL = true;
    needToPrecalculateOutputPower = true;
    
    settingsObservable.notifySettingsChanged();
  }
  
  /**
   * Returns a parameter description
   * 
   * @param identifier Parameter identifier
   * @return Current parameter description
   */
  public String getParameterDescription(String id) {
    Object value = parameterDescriptions.get(id);
    if (value == null) {
      logger.fatal("No parameter description with id:" + id + ", aborting");
      return null;
    }
    return ((String) value);
  }
  
  /**
   * When this method is called all settings observers
   * will be notified.
   */
  public void notifySettingsChanged() {
    settingsObservable.notifySettingsChanged();
  }
  
  /**
   * Returns the Free Space Path Loss factor (in dB), by using
   * parts of the Friis equation. (FSPL <= 0)
   * 
   * @param distance Distance from transmitter to receiver
   * @return FSPL factor
   */
  protected double getFSPL(double distance) {
    // From Friis equation:
    //  Pr(d) = Pt * (Gt * Gr * w2) / ( (4*PI)2 * d2 * L)
    // For FSPL, ignoring Pt, Gt, Gr, L:
    //  Pr(d) = 1 * (1 * 1 * w2) / ( (4*PI)2 * d2 * 1)
    //  Pr(d) = w2 / ( (4*PI)2 * d2)
    //  Pr_dB(d) = 20*log10(w) - 20*log10(4*PI) - 20*log10(d)
    
    if (needToPrecalculateFSPL) {
      double w = getParameterDoubleValue("wavelength");
      paramFSPL = 20*Math.log10(w) - 20*Math.log10(4*Math.PI);
      needToPrecalculateFSPL = false;
    }
    
    return Math.min(0.0, paramFSPL - 20*Math.log10(distance));
  }
  
  
  /**
   * Returns the subset of a given line, that is intersecting the given rectangle.
   * This method returns null if the line does not intersect the rectangle.
   * The given line is defined by the given (x1, y1) -> (x2, y2).
   * 
   * @param x1 Line start point X
   * @param y1 Line start point Y
   * @param x2 Line end point X
   * @param y2 Line epoint Y
   * @param rectangle Rectangle which line may intersect
   * @return Intersection line of given line and rectangle (or null)
   */
  private Line2D getIntersectionLine(double x1, double y1, double x2, double y2, Rectangle2D rectangle) {
    
    // Check if entire line is inside rectangle
    if (rectangle.contains(x1, y1) && rectangle.contains(x2, y2)) {
      return new Line2D.Double(x1, y1, x2, y2);
    }
    
    // Get rectangle and test lines
    Line2D rectangleLower = new Line2D.Double(rectangle.getMinX(), rectangle.getMinY(), rectangle.getMaxX(), rectangle.getMinY());
    Line2D rectangleUpper = new Line2D.Double(rectangle.getMinX(), rectangle.getMaxY(), rectangle.getMaxX(), rectangle.getMaxY());
    Line2D rectangleLeft = new Line2D.Double(rectangle.getMinX(), rectangle.getMinY(), rectangle.getMinX(), rectangle.getMaxY());
    Line2D rectangleRight = new Line2D.Double(rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxX(), rectangle.getMaxY());
    Line2D testLine = new Line2D.Double(x1, y1, x2, y2);
    
    // Check which sides of the rectangle the test line passes through
    Vector<Line2D> intersectedSides = new Vector<Line2D>();
    
    if (rectangleLower.intersectsLine(testLine))
      intersectedSides.add(rectangleLower);
    
    if (rectangleUpper.intersectsLine(testLine))
      intersectedSides.add(rectangleUpper);
    
    if (rectangleLeft.intersectsLine(testLine))
      intersectedSides.add(rectangleLeft);
    
    if (rectangleRight.intersectsLine(testLine))
      intersectedSides.add(rectangleRight);
    
    // If no sides are intersected, return null (no intersection)
    if (intersectedSides.isEmpty()) {
      return null;
    }
    
    // Calculate all resulting line points (should be 2)
    Vector<Point2D> intersectingLinePoints = new Vector<Point2D>();
    
    for (int i=0; i < intersectedSides.size(); i++) {
      intersectingLinePoints.add(
          getIntersectionPoint(testLine, intersectedSides.get(i))
      );
    }
    
    // If only one side was intersected, one point must be inside rectangle
    if (intersectingLinePoints.size() == 1) {
      if (rectangle.contains(x1, y1)) {
        intersectingLinePoints.add(new Point2D.Double(x1, y1));
      } else if (rectangle.contains(x2, y2)) {
        intersectingLinePoints.add(new Point2D.Double(x2, y2));
      } else {
        // Border case, no intersection line
        return null;
      }
    }
    
    if (intersectingLinePoints.size() != 2) {
      // We should have 2 line points!
      logger.warn("Intersecting points != 2");
      return null;
    }
    
    if (intersectingLinePoints.get(0).distance(intersectingLinePoints.get(1)) < 0.001)
      return null;
    
    return new Line2D.Double(
        intersectingLinePoints.get(0),
        intersectingLinePoints.get(1)
    );
  }
  
  /**
   * Returns the intersection point of the two given lines.
   * 
   * @param firstLine First line
   * @param secondLine Second line
   * @return Intersection point of the two lines or null
   */
  private Point2D getIntersectionPoint(Line2D firstLine, Line2D secondLine) {
    double dx1 = firstLine.getX2() - firstLine.getX1();
    double dy1 = firstLine.getY2() - firstLine.getY1();
    double dx2 = secondLine.getX2() - secondLine.getX1();
    double dy2 = secondLine.getY2() - secondLine.getY1();
    double det = (dx2*dy1-dy2*dx1);
    
    if (det == 0.0)
      // Lines parallell, not intersecting
      return null;
    
    double mu = ((firstLine.getX1() - secondLine.getX1())*dy1 - (firstLine.getY1() - secondLine.getY1())*dx1)/det;
    if (mu >= 0.0  &&  mu <= 1.0) {
      Point2D.Double intersectionPoint = new Point2D.Double((secondLine.getX1() + mu*dx2),
          (secondLine.getY1() + mu*dy2));
      
      return intersectionPoint;
    }
    
    // Lines not intersecting withing segments
    return null;
  }
  
  /**
   * Returns the intersection point of the two given lines when streched to infinity.
   * 
   * @param firstLine First line
   * @param secondLine Second line
   * @return Intersection point of the two infinite lines or null if parallell
   */
  private Point2D getIntersectionPointInfinite(Line2D firstLine, Line2D secondLine) {
    double dx1 = firstLine.getX2() - firstLine.getX1();
    double dy1 = firstLine.getY2() - firstLine.getY1();
    double dx2 = secondLine.getX2() - secondLine.getX1();
    double dy2 = secondLine.getY2() - secondLine.getY1();
    double det = (dx2*dy1-dy2*dx1);
    
    if (det == 0.0)
      // Lines parallell, not intersecting
      return null;
    
    double mu = ((firstLine.getX1() - secondLine.getX1())*dy1 - (firstLine.getY1() - secondLine.getY1())*dx1)/det;
    Point2D.Double intersectionPoint = new Point2D.Double((secondLine.getX1() + mu*dx2),
        (secondLine.getY1() + mu*dy2));
    
    return intersectionPoint;
  }

  /**
   * This method builds a tree structure with all visible lines from a given source.
   * It is recursive and depends on the given ray data argument, which holds information
   * about maximum number of recursions.
   * Each element in the tree is either produced from a refraction, reflection or a diffraction
   * (except for the absolute source which is neither), and holds a point and a line.
   * 
   * @param rayData Holds information about the incident ray
   * @return Tree of all visibles lines 
   */
  private DefaultMutableTreeNode buildVisibleLinesTree(RayData rayData) {
    DefaultMutableTreeNode thisTree = new DefaultMutableTreeNode();
    thisTree.setUserObject(rayData);

    // If no more rays may be produced there if no need to search for visible lines
    if (rayData.getSubRaysLimit() <= 0) {
      return thisTree;
    }
    
    Point2D source = rayData.getSourcePoint();
    Line2D line = rayData.getLine();
    
    // Find all visible lines
    Vector<Line2D> visibleSides = getAllVisibleSides(
        source.getX(),
        source.getY(), 
        null, 
        line
    );
    
    // Create refracted subtrees
    if (rayData.getRefractedSubRaysLimit() > 0 && visibleSides != null) {
      Enumeration<Line2D> visibleSidesEnum = visibleSides.elements();
      while (visibleSidesEnum.hasMoreElements()) {
        Line2D refractingSide = visibleSidesEnum.nextElement();
        
        // Keeping old source, but looking through this line to see behind it
        
        // Recursively build and add subtrees
        RayData newRayData = new RayData(
            RayData.RayType.REFRACTION, 
            source, 
            refractingSide,
            rayData.getSubRaysLimit() - 1,
            rayData.getRefractedSubRaysLimit() - 1,
            rayData.getReflectedSubRaysLimit(),
            rayData.getDiffractedSubRaysLimit()            
        );
        DefaultMutableTreeNode subTree = buildVisibleLinesTree(newRayData);
        
        thisTree.add(subTree);
      }
    }
    
    // Create reflection subtrees
    if (rayData.getReflectedSubRaysLimit() > 0 && visibleSides != null) {
      Enumeration<Line2D> visibleSidesEnum = visibleSides.elements();
      while (visibleSidesEnum.hasMoreElements()) {
        Line2D reflectingSide = visibleSidesEnum.nextElement();
        
        // Create new pseudo-source
        Rectangle2D bounds = reflectingSide.getBounds2D();
        double newPsuedoSourceX = source.getX();
        double newPsuedoSourceY = source.getY();
        if (bounds.getHeight() > bounds.getWidth())
          newPsuedoSourceX = 2*reflectingSide.getX1() - newPsuedoSourceX;
        else
          newPsuedoSourceY = 2*reflectingSide.getY1() - newPsuedoSourceY;
        
        // Recursively build and add subtrees
        RayData newRayData = new RayData(
            RayData.RayType.REFLECTION, 
            new Point2D.Double(newPsuedoSourceX, newPsuedoSourceY), 
            reflectingSide,
            rayData.getSubRaysLimit() - 1,
            rayData.getRefractedSubRaysLimit(),
            rayData.getReflectedSubRaysLimit() - 1,
            rayData.getDiffractedSubRaysLimit()            
        );
        DefaultMutableTreeNode subTree = buildVisibleLinesTree(newRayData);
        
        thisTree.add(subTree);
      }
    }
    
    // Get possible diffraction sources
    Vector<Point2D> diffractionSources = null;
    if (rayData.getDiffractedSubRaysLimit() > 0) {
      diffractionSources = getAllDiffractionSources(visibleSides);
    }

    // Create diffraction subtrees
    if (rayData.getDiffractedSubRaysLimit() > 0 && diffractionSources != null) {
      Enumeration<Point2D> diffractionSourcesEnum = diffractionSources.elements();
      while (diffractionSourcesEnum.hasMoreElements()) {
        Point2D diffractionSource = diffractionSourcesEnum.nextElement();
        
        // Recursively build and add subtrees
        RayData newRayData = new RayData(
            RayData.RayType.DIFFRACTION, 
            diffractionSource, 
            null,
            rayData.getSubRaysLimit() - 1,
            rayData.getRefractedSubRaysLimit(),
            rayData.getReflectedSubRaysLimit(),
            rayData.getDiffractedSubRaysLimit() - 1            
        );
        DefaultMutableTreeNode subTree = buildVisibleLinesTree(newRayData);
        
        thisTree.add(subTree);
      }
    }
    
    return thisTree;
  }
  
  /**
   * Returns a vector of ray paths from given origin to given destination.
   * Each ray path consists of a vector of points (including source and destination).
   * 
   * @param origin Ray paths origin
   * @param dest Ray paths destination
   * @param visibleLinesTree Information about all visible lines generated by buildVisibleLinesTree()
   * @see #buildVisibleLinesTree(RayData)
   * @return All ray paths from origin to destnation
   */
  private Vector<RayPath> getConnectingPaths(Point2D origin, Point2D dest, DefaultMutableTreeNode visibleLinesTree) {
    Vector<RayPath> allPaths = new Vector<RayPath>();
    
    // Analyse the possible paths to find which actually reached destination
    Enumeration treeEnum = visibleLinesTree.breadthFirstEnumeration();
    while (treeEnum.hasMoreElements()) {
      // For every element, 
      //  check if it is the origin, a diffraction, refraction or a reflection source
      DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) treeEnum.nextElement();
      RayData rayData = (RayData) treeNode.getUserObject();
      Point2D sourcePoint = rayData.getSourcePoint();
      Line2D line = rayData.getLine();
      RayData.RayType type = rayData.getType();
      
      Line2D pseudoSourceToDest = new Line2D.Double(sourcePoint, dest);
      boolean directPathExists = false;
      Point2D justBeforeDestination = null;
      
      // Get ray path point just before destination (if path exists at all)
      if (type == RayData.RayType.ORIGIN) {
        
        // Check if direct path exists
        justBeforeDestination = sourcePoint;
        
        if (!getParameterBooleanValue("rt_disallow_direct_path"))
          directPathExists = isDirectPath(justBeforeDestination, dest);
        else
          directPathExists = false;
        
      } else if (type == RayData.RayType.REFRACTION && pseudoSourceToDest.intersectsLine(line)) {
        
        // Destination is inside refraction interval
        justBeforeDestination = getIntersectionPoint(pseudoSourceToDest, line);
        
        // Check if direct path exists (but ignore when leaving obstacle)
        directPathExists = isDirectPath(justBeforeDestination, dest);
        
      } else if (type == RayData.RayType.REFLECTION && pseudoSourceToDest.intersectsLine(line)) {
        
        // Destination is inside reflection interval
        justBeforeDestination = getIntersectionPoint(pseudoSourceToDest, line);
        
        // Check if direct path exists (ignore reflection line)
        directPathExists = isDirectPath(justBeforeDestination, dest);

      } else if (type == RayData.RayType.DIFFRACTION) {
        
        // Check if direct path exists (travelling through object not allowed
        justBeforeDestination = sourcePoint;
        directPathExists = isDirectPath(justBeforeDestination, dest);

      }
      
      // If a direct path exists, traverse up tree to find entire path
      if (directPathExists) {

        // Create new empty ray path
        boolean pathBroken = false;
        RayPath currentPath = new RayPath();
        
        // Add those parts we already know
        currentPath.addPoint(dest, RayData.RayType.DESTINATION);
        currentPath.addPoint(justBeforeDestination, type);

        Point2D lastPoint = dest;
        Point2D newestPoint = justBeforeDestination;
        
        // Check that this ray subpath is long enough to be considered
        if (newestPoint.distance(lastPoint) < 0.01 && type != RayData.RayType.ORIGIN) {
          pathBroken = true;
        }

        // Subpath must be double-direct if from diffraction
        if (type == RayData.RayType.DIFFRACTION && !isDirectPath(lastPoint, newestPoint)) {
          pathBroken = true;
        }

        // Data used when traversing path
        DefaultMutableTreeNode currentlyTracedNode = treeNode;
        RayData currentlyTracedRayData = (RayData) currentlyTracedNode.getUserObject();
        RayData.RayType currentlyTracedNodeType = currentlyTracedRayData.getType();
        Point2D currentlyTracedSource = currentlyTracedRayData.getSourcePoint();
        Line2D currentlyTracedLine = currentlyTracedRayData.getLine();
        

        // Traverse upwards until origin found
        while (!pathBroken && currentlyTracedNodeType != RayData.RayType.ORIGIN) {
          
          // Update new ray data
          currentlyTracedNode = (DefaultMutableTreeNode) currentlyTracedNode.getParent();
          currentlyTracedRayData = (RayData) currentlyTracedNode.getUserObject();
          currentlyTracedNodeType = currentlyTracedRayData.getType();
          currentlyTracedSource = currentlyTracedRayData.getSourcePoint();
          currentlyTracedLine = currentlyTracedRayData.getLine();
          
          if (currentlyTracedNodeType == RayData.RayType.ORIGIN) {
            // We finally found the path origin, path ends here
            lastPoint = newestPoint;
            newestPoint = origin;

            currentPath.addPoint(newestPoint, currentlyTracedNodeType);

            // Check that this ray subpath is long enough to be considered
            if (newestPoint.distance(lastPoint) < 0.01)
              pathBroken = true;

          } else {
            // Trace further up in the tree
            
            if (currentlyTracedNodeType == RayData.RayType.REFRACTION || currentlyTracedNodeType == RayData.RayType.REFLECTION) {
              // Traced tree element is a reflection/refraction - get intersection point and keep climbing
              lastPoint = newestPoint;
              
              Line2D newToOldIntersection = new Line2D.Double(currentlyTracedSource, lastPoint);
              newestPoint = getIntersectionPointInfinite(newToOldIntersection, currentlyTracedLine);
              
            } else {
              // Traced tree element is a diffraction - save point and keep climbing
              lastPoint = newestPoint;
              newestPoint = currentlyTracedSource;
            }

            currentPath.addPoint(newestPoint, currentlyTracedNodeType);

            // Check that this ray subpath is long enough to be considered
            if (newestPoint == null || lastPoint == null || newestPoint.distance(lastPoint) < 0.01)
              pathBroken = true;
          }
          
          // Subpath must be double-direct if from diffraction
          if (currentlyTracedNodeType == RayData.RayType.DIFFRACTION && !isDirectPath(lastPoint, newestPoint)) {
            pathBroken = true;
          }
          
          if (pathBroken)
            break;
        }

        // Save ray path
        if (!pathBroken) {
          allPaths.add(currentPath);

          // Stop here if no other paths should be considered
          if (type == RayData.RayType.ORIGIN && getParameterBooleanValue("rt_ignore_non_direct")) {
            return allPaths;
          }

        }
        
      }
    }
      
    return allPaths;
  }
  
  /**
   * True if a line drawn from the given source and given destination does
   * not intersect with any obstacle outer lines in the current obstacle world.
   * This method only checks for intersection with the obstacles lines "visible"
   * from source. Hence, if source is inside an obstacle, that obstacles will
   * not cause this method to return false. (Note that method is not symmetric)
   * 
   * @param source Source
   * @param dest Destination
   * @return True if no obstacles between source and destination
   */
  private boolean isDirectPath(Point2D source, Point2D dest) {
    Line2D sourceToDest = new Line2D.Double(source, dest);
    
    // Get angle
    double deltaX = dest.getX() - source.getX(); 
    double deltaY = dest.getY() - source.getY(); 
    double angleSourceToDest = Math.atan2(deltaY, deltaX);
      
    // Get all visible sides near angle
    Vector<Line2D> visibleSides = getAllVisibleSides(
        source.getX(),
        source.getY(), 
        new AngleInterval(angleSourceToDest - 0.1, angleSourceToDest + 0.1), 
        null
    );
    
    // Check for intersections
    if (visibleSides != null) {
      for (int i=0; i < visibleSides.size(); i++) {
        if (visibleSides.get(i).intersectsLine(sourceToDest)) {
          // Check that intersection point is not destination
          Point2D intersectionPoint = getIntersectionPointInfinite(visibleSides.get(i), sourceToDest);
          if (dest.distance(intersectionPoint) > 0.01)
            return false;
        }
      }
    }

    return true;
  }
  
  /**
   * Returns the Fast fading factor (in dB), which depends on
   * the multiple paths from source to destination via reflections
   * on registered obstacles.
   * TODO Only first-order multipath...
   * 
   * @param sourceX Transmitter X coordinate
   * @param sourceY Transmitter Y coordinate
   * @param destX Receiver X coordinate
   * @param destY Receiver Y coordinate
   * @return Slow fading factor
   */
  protected double getFastFading(double sourceX, double sourceY, double destX, double destY) {
    Point2D dest = new Point2D.Double(destX, destY);
    Point2D source = new Point2D.Double(sourceX, sourceY);
    
    // Destination inside an obstacle? => no reflection factor
    for (int i=0; i < myObstacleWorld.getNrObstacles(); i++) {
      if (myObstacleWorld.getObstacle(i).contains(dest)) {
        //logger.debug("Destination inside obstacle, aborting fast fading");
        return 0;
      }
    }
    
    return 0;
  }
  
  
  /**
   * Returns all possible diffraction sources, by checking which
   * of the endpoints of the given visible lines that are on a corner
   * of a obstacle structure.
   * 
   * @param allVisibleLines Lines which may hold diffraction sources
   * @return All diffraction sources
   */
  private Vector<Point2D> getAllDiffractionSources(Vector<Line2D> allVisibleLines) {
    Vector<Point2D> allDiffractionSources = new Vector<Point2D>();
    Enumeration<Line2D> allVisibleLinesEnum = allVisibleLines.elements();
    
    while (allVisibleLinesEnum.hasMoreElements()) {
      Line2D visibleLine = allVisibleLinesEnum.nextElement();
      
      // Check both end points of line for possible diffraction point
      if (myObstacleWorld.pointIsNearCorner(visibleLine.getP1())) {
        allDiffractionSources.add(visibleLine.getP1());
      }
      if (myObstacleWorld.pointIsNearCorner(visibleLine.getP2())) {
        allDiffractionSources.add(visibleLine.getP2());
      }
    }
    
    return allDiffractionSources;
  }
  
  /**
   * Return all obstacle sides visible from given source when looking
   * in the given angle interval.
   * The sides may partly be shadowed by other obstacles.
   * If the angle interval is null, it will be regarded as the entire interval
   * If the line argument is non-null, all returned lines will be on the far side
   * of this line, as if one was looking through that line.
   * 
   * @param sourceX Source X
   * @param sourceY Source Y
   * @param angleInterval Angle interval (or null)
   * @param lookThrough Line to look through (or null)
   * @return All visible sides
   */
  private Vector<Line2D> getAllVisibleSides(double sourceX, double sourceY, AngleInterval angleInterval, Line2D lookThrough) {
    Point2D source = new Point2D.Double(sourceX, sourceY);    

    // Check if results were already calculated earlier
    for (int i=0; i < calculatedVisibleSidesSources.size(); i++) {
      if (
          // Compare sources
          source.equals(calculatedVisibleSidesSources.get(i)) &&
          
          // Compare angle intervals
          (angleInterval == calculatedVisibleSidesAngleIntervals.get(i) ||
              angleInterval != null && angleInterval.equals(calculatedVisibleSidesAngleIntervals.get(i)) ) &&
              
              // Compare lines
              (lookThrough == calculatedVisibleSidesLines.get(i) ||
                  lookThrough != null && lookThrough.equals(calculatedVisibleSidesLines.get(i)) )
      ) {
        // Move to top of list
        Point2D oldSource = calculatedVisibleSidesSources.remove(i);
        Line2D oldLine = calculatedVisibleSidesLines.remove(i);
        AngleInterval oldAngleInterval = calculatedVisibleSidesAngleIntervals.remove(i);
        Vector<Line2D> oldVisibleLines = calculatedVisibleSides.remove(i);
        
        calculatedVisibleSidesSources.add(0, oldSource);
        calculatedVisibleSidesLines.add(0, oldLine);
        calculatedVisibleSidesAngleIntervals.add(0, oldAngleInterval);
        calculatedVisibleSides.add(0, oldVisibleLines);
        
        // Return old results
        return oldVisibleLines;
      }
    }

    Vector<Line2D> visibleLines = new Vector<Line2D>();
    Vector<AngleInterval> unhandledAngles = new Vector<AngleInterval>();
    
    if (lookThrough != null) {
      if (angleInterval == null)
        unhandledAngles.add(AngleInterval.getAngleIntervalOfLine(source, lookThrough));
      else
        unhandledAngles.add(AngleInterval.getAngleIntervalOfLine(source, lookThrough).intersectWith(angleInterval));
    } else {
      if (angleInterval == null)
        unhandledAngles.add(new AngleInterval(0, 2*Math.PI));
      else
        unhandledAngles.add(angleInterval);
    }
    
    // Do forever (will break when no more unhandled angles exist)
    while (!unhandledAngles.isEmpty()) {
      
      // While unhandled angles still exist, keep searching for visible lines
      while (!unhandledAngles.isEmpty()) {
        //logger.info("Beginning of while-loop, unhandled angles left = " + unhandledAngles.size());
        AngleInterval angleIntervalToCheck = unhandledAngles.firstElement();
        
        // Check that interval is not empty or "infinite small"
        if (angleIntervalToCheck == null || angleIntervalToCheck.isEmpty()) {
          //logger.info("Angle interval (almost) empty, ignoring");
          unhandledAngles.remove(angleIntervalToCheck);
          break;
        }
        
        // <<<< Get visible obstacle candidates inside this angle interval >>>>
        Vector<Rectangle2D> visibleObstacleCandidates = 
          myObstacleWorld.getAllObstaclesInAngleInterval(source, angleIntervalToCheck);
        
        //logger.info("Obstacle candidates count = " + visibleObstacleCandidates.size());
        if (visibleObstacleCandidates.isEmpty()) {
          //logger.info("Visible obstacles candidates empty");
          unhandledAngles.remove(angleIntervalToCheck);
          break; // Restart without this angle
        }
        
        // <<<< Get visible line candidates of these obstacles >>>>
        Vector<Line2D> visibleLineCandidates = new Vector<Line2D>();
        for (int i=0; i < visibleObstacleCandidates.size(); i++) {
          Rectangle2D obstacle = visibleObstacleCandidates.get(i);
          int outcode = obstacle.outcode(source);
          
          if ((outcode & Rectangle2D.OUT_BOTTOM) != 0)
            visibleLineCandidates.add(
                new Line2D.Double(obstacle.getMinX(), obstacle.getMaxY(), obstacle.getMaxX(), obstacle.getMaxY()));
          
          if ((outcode & Rectangle2D.OUT_TOP) != 0)
            visibleLineCandidates.add(
                new Line2D.Double(obstacle.getMinX(), obstacle.getMinY(), obstacle.getMaxX(), obstacle.getMinY()));
          
          if ((outcode & Rectangle2D.OUT_LEFT) != 0)
            visibleLineCandidates.add(
                new Line2D.Double(obstacle.getMinX(), obstacle.getMinY(), obstacle.getMinX(), obstacle.getMaxY()));
          
          if ((outcode & Rectangle2D.OUT_RIGHT) != 0)
            visibleLineCandidates.add(
                new Line2D.Double(obstacle.getMaxX(), obstacle.getMinY(), obstacle.getMaxX(), obstacle.getMaxY()));
        }
        //logger.info("Line candidates count = " + visibleLineCandidates.size());
        if (visibleLineCandidates.isEmpty()) {
          //logger.info("Visible line candidates empty");
          unhandledAngles.remove(angleIntervalToCheck);
          break; // Restart without this angle
        }
        
        // <<<< Get cropped visible line candidates of these lines >>>>
        Vector<Line2D> croppedVisibleLineCandidates = new Vector<Line2D>();
        for (int i=0; i < visibleLineCandidates.size(); i++) {
          Line2D lineCandidate = visibleLineCandidates.get(i);
          
          // Create angle interval of this line
          AngleInterval lineAngleInterval = AngleInterval.getAngleIntervalOfLine(source, lineCandidate);
          
          AngleInterval intersectionInterval = null;

          // Add entire line if it is fully inside our visible angle interval
          if (angleIntervalToCheck.contains(lineAngleInterval)) {

            if (lookThrough != null) {
              // Check if the candidate is "equal" to the see through line
              if (Math.abs(lineCandidate.getX1() - lookThrough.getX1()) +
                  Math.abs(lineCandidate.getY1() - lookThrough.getY1()) +
                  Math.abs(lineCandidate.getX2() - lookThrough.getX2()) +
                  Math.abs(lineCandidate.getY2() - lookThrough.getY2()) < 0.01) {
                // See through line and candidate line are the same - skip this candidate
              }
              
              // Check if the candidate is on our side of the see through line
              else if (new Line2D.Double(
                  lineCandidate.getBounds2D().getCenterX(), 
                  lineCandidate.getBounds2D().getCenterY(),
                  sourceX,
                  sourceY
              ).intersectsLine(lookThrough)) {
                croppedVisibleLineCandidates.add(lineCandidate);
              } // else Skip line
            } else croppedVisibleLineCandidates.add(lineCandidate);
            
          } 
          
          // Add part of line if it is partly inside our visible angle interval
          else if ((intersectionInterval = lineAngleInterval.intersectWith(angleIntervalToCheck)) != null) {
            
            // Get lines towards the visible segment
            Line2D lineToStartAngle = AngleInterval.getDirectedLine(
                source,
                intersectionInterval.getStartAngle(),
                1.0
            );
            Line2D lineToEndAngle = AngleInterval.getDirectedLine(
                source,
                intersectionInterval.getEndAngle(),
                1.0
            );
            
            // Calculate intersection points
            Point2D intersectionStart = getIntersectionPointInfinite(
                lineCandidate,
                lineToStartAngle
            );
            Point2D intersectionEnd = getIntersectionPointInfinite(
                lineCandidate,
                lineToEndAngle
            );
            
            if (
                intersectionStart != null &&
                intersectionEnd != null &&
                intersectionStart.distance(intersectionEnd) > 0.001 // Rounding error limit (1 mm)
            ) {

              Line2D newCropped = new Line2D.Double(intersectionStart, intersectionEnd);

              if (lookThrough != null) {
                // Check if the candidate is "equal" to the see through line
                if (Math.abs(newCropped.getX1() - lookThrough.getX1()) +
                    Math.abs(newCropped.getY1() - lookThrough.getY1()) +
                    Math.abs(newCropped.getX2() - lookThrough.getX2()) +
                    Math.abs(newCropped.getY2() - lookThrough.getY2()) < 0.01) {
                  // See through line and candidate line are the same - skip this candidate
                }
                
                // Check if the candidate is on our side of the see through line
                else if (new Line2D.Double(
                    newCropped.getBounds2D().getCenterX(), 
                    newCropped.getBounds2D().getCenterY(),
                    sourceX,
                    sourceY
                ).intersectsLine(lookThrough)) {
                  croppedVisibleLineCandidates.add(newCropped);
                } // else Skip line
              } else croppedVisibleLineCandidates.add(newCropped);

            }
          }
          
          // Skip line completely if not in our visible angle interval
          else {
          }
        }
        //logger.info("Cropped line candidates count = " + croppedVisibleLineCandidates.size());
        if (croppedVisibleLineCandidates.isEmpty()) {
          //logger.info("Cropped visible line candidates empty");
          unhandledAngles.remove(angleIntervalToCheck);
          break; // Restart without this angle
        }        
        
        // <<<< Get visible lines from these line candidates >>>>
        for (int i=0; i < croppedVisibleLineCandidates.size(); i++) {
          Line2D visibleLineCandidate = croppedVisibleLineCandidates.get(i);
          AngleInterval visibleLineCandidateAngleInterval = 
            AngleInterval.getAngleIntervalOfLine(source, visibleLineCandidate).intersectWith(angleIntervalToCheck);
          
          //logger.info("Incoming angle interval " + angleIntervalToCheck);
          //logger.info(". => line interval " + visibleLineCandidateAngleInterval);

          // Area to test for shadowing objects
          GeneralPath testArea = new GeneralPath();
          testArea.moveTo((float) sourceX, (float) sourceY);
          testArea.lineTo((float) visibleLineCandidate.getX1(), (float) visibleLineCandidate.getY1());
          testArea.lineTo((float) visibleLineCandidate.getX2(), (float) visibleLineCandidate.getY2());
          testArea.closePath();
          
          // Does any other line shadow this line?
          boolean unshadowed = true;
          boolean unhandledAnglesChanged = false;
          for (int j=0; j < croppedVisibleLineCandidates.size(); j++) {
            
            // Create shadow rectangle
            Line2D shadowLineCandidate = croppedVisibleLineCandidates.get(j);
            Rectangle2D shadowRectangleCandidate = shadowLineCandidate.getBounds2D();
            double minDelta = 0.01*Math.max(
                shadowRectangleCandidate.getWidth(), 
                shadowRectangleCandidate.getHeight()
            );
            shadowRectangleCandidate.add(
                shadowRectangleCandidate.getCenterX() + minDelta,
                shadowRectangleCandidate.getCenterY() + minDelta
            );

            // Find the shortest of the two
            double shadowDistance =
              shadowLineCandidate.getP1().distance(source) + 
              shadowLineCandidate.getP2().distance(source);
            
            double visibleDistance =
              visibleLineCandidate.getP1().distance(source) + 
              visibleLineCandidate.getP2().distance(source);

            double shadowCloseDistance =
              Math.min(
                  shadowLineCandidate.getP1().distance(source),
                  shadowLineCandidate.getP2().distance(source));
            
            double visibleFarDistance =
              Math.max(
                  visibleLineCandidate.getP1().distance(source),
                  visibleLineCandidate.getP2().distance(source));
            
            // Does shadow rectangle intersect test area?
            if (visibleLineCandidate != shadowLineCandidate &&
                testArea.intersects(shadowRectangleCandidate) &&
                shadowCloseDistance <= visibleFarDistance) {
              
              // Shadow line candidate seems to shadow (part of) our visible candidate
              AngleInterval shadowLineCandidateAngleInterval = 
                AngleInterval.getAngleIntervalOfLine(source, shadowLineCandidate).intersectWith(angleIntervalToCheck);
              
              if (shadowLineCandidateAngleInterval.contains(visibleLineCandidateAngleInterval)) {
                // Covers us entirely, do nothing
                
                // Special case, both shadow and visible candidate have the same interval
                if (visibleLineCandidateAngleInterval.contains(shadowLineCandidateAngleInterval)) {
                  
                  if (visibleDistance > shadowDistance) {
                    unshadowed = false;
                    break;
                  }
                } else {
                  unshadowed = false;
                  break;
                }
                
              } else if (visibleLineCandidateAngleInterval.intersects(shadowLineCandidateAngleInterval)) {
                // Covers us partly, split angle interval
                Vector<AngleInterval> newIntervalsToAdd = new Vector<AngleInterval>();
                
                // Create angle interval of intersection between shadow and visible candidate
                AngleInterval intersectedInterval = 
                  visibleLineCandidateAngleInterval.intersectWith(shadowLineCandidateAngleInterval);
                if (intersectedInterval != null) {
                  Vector<AngleInterval> tempVector1 =
                    AngleInterval.intersect(unhandledAngles, intersectedInterval);
                  
                  if (tempVector1 != null) 
                    for (int k=0; k < tempVector1.size(); k++) 
                      if (tempVector1.get(k) != null && !tempVector1.get(k).isEmpty()) {
                        newIntervalsToAdd.add(tempVector1.get(k));
                      }
                }
                
                // Add angle interval of visible candidate without shadow candidate
                Vector<AngleInterval> tempVector2 = 
                  visibleLineCandidateAngleInterval.subtract(shadowLineCandidateAngleInterval);
                if (tempVector2 != null) 
                  for (int k=0; k < tempVector2.size(); k++) 
                    if (tempVector2.get(k) != null && !tempVector2.get(k).isEmpty())
                      newIntervalsToAdd.addAll(AngleInterval.intersect(unhandledAngles, tempVector2.get(k)));
                
                // Subtract angle interval of visible candidate
                unhandledAngles = AngleInterval.subtract(unhandledAngles, visibleLineCandidateAngleInterval);
                unhandledAnglesChanged = true;
                
                // Add new angle intervals
                //logger.info("Split angle interval: " + visibleLineCandidateAngleInterval);
                for (int k=0; k < newIntervalsToAdd.size(); k++) {
                  if (newIntervalsToAdd.get(k) != null && !newIntervalsToAdd.get(k).isEmpty()) {
                    //logger.info("> into: " + newIntervalsToAdd.get(k));
                    unhandledAngles.add(newIntervalsToAdd.get(k));
                    unhandledAnglesChanged = true;
                  }
                }
                
                unshadowed = false;
                break;
              } else {
                // Not intersecting after all, just ignore this
              }
            }
           
            if (!unshadowed)
              break;
          }
          
          if (unhandledAnglesChanged) {
            //logger.info("Unhandled angles changed, restarting..");
            break;
          }
          
          if (unshadowed) {
            // No other lines shadow this line => this line must be visible!
            
            unhandledAngles = AngleInterval.subtract(unhandledAngles, visibleLineCandidateAngleInterval);
            visibleLines.add(visibleLineCandidate);
            
            //logger.info("Added visible line and removed angle interval: " + visibleLineCandidateAngleInterval);
            //logger.info("Number of visible lines sofar: " + visibleLines.size());
            break;
          }
          
        }
        
      }
      
    } // End of outer loop
    
    // Save results in order to speed up later calculations
    int size = calculatedVisibleSides.size();
    // Crop saved sides vectors
    if (size >= maxSavedVisibleSides) {
      calculatedVisibleSides.remove(size-1);
      calculatedVisibleSidesSources.remove(size-1);
      calculatedVisibleSidesAngleIntervals.remove(size-1);
      calculatedVisibleSidesLines.remove(size-1);
    }
    
    calculatedVisibleSides.add(0, visibleLines);
    calculatedVisibleSidesSources.add(0, source);
    calculatedVisibleSidesAngleIntervals.add(0, angleInterval);
    calculatedVisibleSidesLines.add(0, lookThrough);

    return visibleLines;
  }
  
  /**
   * Calculates and returns the received signal strength (dBm) of a signal sent
   * from the given source position to the given destination position as a
   * random variable. This method uses current parameters such as transmitted
   * power, obstacles, overall system loss etc.
   * 
   * @param sourceX
   *          Source position X
   * @param sourceY
   *          Source position Y
   * @param destX
   *          Destination position X
   * @param destY
   *          Destination position Y
   * @return Received signal strength (dBm) random variable. The first value is
   *         the random variable mean, and the second is the variance.
   */
  public double[] getReceivedSignalStrength(double sourceX, double sourceY, double destX, double destY) {
    return getTransmissionData(sourceX, sourceY, destX, destY, TransmissionData.SIGNAL_STRENGTH);
  }
   
  // TODO Fix better data type support
  private double[] getTransmissionData(double sourceX, double sourceY, double destX, double destY, TransmissionData dataType) {
    Point2D source = new Point2D.Double(sourceX, sourceY);
    Point2D dest = new Point2D.Double(destX, destY);
    double accumulatedVariance = 0;

    // - Get all ray paths from source to destination -
    RayData originRayData = new RayData(
        RayData.RayType.ORIGIN,
        source,
        null,
        getParameterIntegerValue("rt_max_rays"), 
        getParameterIntegerValue("rt_max_refractions"), 
        getParameterIntegerValue("rt_max_reflections"), 
        getParameterIntegerValue("rt_max_diffractions") 
    );

    // TODO Current (changing) signal strength should be built into 'build visible lines' to speed up things!
    
    // Check if origin tree is already calculated and saved
    DefaultMutableTreeNode visibleLinesTree = null;
    visibleLinesTree =
      buildVisibleLinesTree(originRayData); 

    // Calculate all paths from source to destination, using above calculated tree
    Vector<RayPath> allPaths = getConnectingPaths(source, dest, visibleLinesTree);
    
    if (inLoggingMode) {
      logger.info("Saved rays:");
      Enumeration<RayPath> pathsEnum = allPaths.elements();
      while (pathsEnum.hasMoreElements()) {
        RayPath currentPath = pathsEnum.nextElement();
        logger.info("* " + currentPath);
        for (int i=0; i < currentPath.getSubPathCount(); i++) {
          savedRays.add(currentPath.getSubPath(i));
        }
      }
    }
        
    // - Extract length and losses of each path -
    double[] pathLengths = new double[allPaths.size()];
    double[] pathGain = new double[allPaths.size()];
    int bestSignalNr = -1;
    double bestSignalPathLoss = 0;
    for (int i=0; i < allPaths.size(); i++) {
      RayPath currentPath = allPaths.get(i);
      double accumulatedStraightLength = 0;

      for (int j=0; j < currentPath.getSubPathCount(); j++) {
        Line2D subPath = currentPath.getSubPath(j);
        double subPathLength = subPath.getP1().distance(subPath.getP2());
        RayData.RayType subPathStartType = currentPath.getType(j);
        
        // Type specific losses
        // TODO Type specific losses depends on angles as well!
        if (subPathStartType == RayData.RayType.REFRACTION) {
          pathGain[i] += getParameterDoubleValue("rt_refrac_coefficient");
        } else if (subPathStartType == RayData.RayType.REFLECTION) {
          pathGain[i] += getParameterDoubleValue("rt_reflec_coefficient");

          // Add FSPL from last subpaths (if FSPL on individual rays)
          if (!getParameterBooleanValue("rt_fspl_on_total_length") && accumulatedStraightLength > 0) {
            pathGain[i] += getFSPL(accumulatedStraightLength);
          }
          accumulatedStraightLength = 0; // Reset straight length
        } else if (subPathStartType == RayData.RayType.DIFFRACTION) {
          pathGain[i] += getParameterDoubleValue("rt_diffr_coefficient");

          // Add FSPL from last subpaths (if FSPL on individual rays)
          if (!getParameterBooleanValue("rt_fspl_on_total_length") && accumulatedStraightLength > 0) {
            pathGain[i] += getFSPL(accumulatedStraightLength);
          }
          accumulatedStraightLength = 0; // Reset straight length
        }
        accumulatedStraightLength += subPathLength; // Add length, FSPL should be calculated on total straight length

        // If ray starts with a refraction, calculate obstacle attenuation
        if (subPathStartType == RayData.RayType.REFRACTION) {
          // Ray passes through a wall, calculate distance through that wall

          // Fetch attenuation constant
          double attenuationConstant = getParameterDoubleValue("obstacle_attenuation");

          Vector<Rectangle2D> allPossibleObstacles = myObstacleWorld.getAllObstaclesNear(subPath.getP1());
          
          for (int k=0; k < allPossibleObstacles.size(); k++) {
            Rectangle2D obstacle = allPossibleObstacles.get(k);
            
            // Calculate the intersection distance
            Line2D line = getIntersectionLine(
                subPath.getP1().getX(), 
                subPath.getP1().getY(), 
                subPath.getP2().getX(), 
                subPath.getP2().getY(), 
                obstacle
            );
            
            if (line != null) {
              pathGain[i] += attenuationConstant * line.getP1().distance(line.getP2());
              break;
            }
            
          }
            
        }

        // Add to total path length
        pathLengths[i] += subPathLength;
      }
 
      // Add FSPL from last rays (if FSPL on individual rays)
      if (!getParameterBooleanValue("rt_fspl_on_total_length") && accumulatedStraightLength > 0) {
        pathGain[i] += getFSPL(accumulatedStraightLength);
      }
      
      // Free space path loss on total path length?
      if (getParameterBooleanValue("rt_fspl_on_total_length")) {
        pathGain[i] += getFSPL(pathLengths[i]);
      }
        
      if (bestSignalNr < 0 || pathGain[i] > bestSignalPathLoss) {
        bestSignalNr = i;
        bestSignalPathLoss = pathGain[i];
      }
    }
    
    // - Calculate total path loss (using simple Rician) -
    double[] pathModdedLengths = new double[allPaths.size()];
    double delaySpread = 0;
    double delaySpreadRMS = 0;
    double wavelength = getParameterDoubleValue("wavelength");
    double totalPathGain = 0;
    double delaySpreadTotalWeight = 0;
    double speedOfLight = 300; // Approximate value (m/us)
    for (int i=0; i < pathModdedLengths.length; i++) {
      // Ignore insignificant interfering signals
      if (pathGain[i] > pathGain[bestSignalNr] - 30) {
        double pathLengthDiff = Math.abs(pathLengths[i] - pathLengths[bestSignalNr]);

        // Update delay spread TODO Now considering best signal, should be first or mean?
        if (pathLengthDiff > delaySpread)
          delaySpread = pathLengthDiff;


        // Update root-mean-square delay spread TODO Now considering best signal time, should be mean delay?
        delaySpreadTotalWeight += pathGain[i]*pathGain[i];
        double rmsDelaySpreadComponent = pathLengthDiff/speedOfLight;
        rmsDelaySpreadComponent *= rmsDelaySpreadComponent * pathGain[i]*pathGain[i];
        delaySpreadRMS += rmsDelaySpreadComponent;

        // OK since cosinus is even function
        pathModdedLengths[i] = pathLengthDiff % wavelength;
        
        // Using Rician fading approach, TODO Only one best signal considered - combine these? (need two limits)
        totalPathGain += Math.pow(10, pathGain[i]/10.0)*Math.cos(2*Math.PI * pathModdedLengths[i]/wavelength);
        if (inLoggingMode) {
          logger.info("Adding ray path with gain " + pathGain[i] + " and phase " + (2*Math.PI * pathModdedLengths[i]/wavelength));
        }
      } else if (inLoggingMode) {
        pathModdedLengths[i] = (pathLengths[i] - pathLengths[bestSignalNr]) % wavelength;
        logger.info("Not adding ray path with gain " + pathGain[i] + " and phase " + (2*Math.PI * pathModdedLengths[i]/wavelength));
      }
      
    }

    // Calculate resulting RMS delay spread
    delaySpread /= speedOfLight;
    delaySpreadRMS /= delaySpreadTotalWeight;
    
    
    // Convert back to dB
    totalPathGain = 10*Math.log10(Math.abs(totalPathGain));

    if (inLoggingMode) {
      logger.info("Total path gain:\t" + totalPathGain);
      logger.info("Delay spread:\t" + delaySpread);
      logger.info("RMS Delay spread:\t" + delaySpreadRMS);
    }

    // - Calculate received power -
    // Using formula (dB)
    //  Received power = Output power + System gain + Transmitter gain + Path Loss + Receiver gain
    // TODO Update formulas
    Random random = new Random();
    double outputPower = getParameterDoubleValue("tx_power");
    double systemGain = getParameterDoubleValue("system_gain_mean");
    if (getParameterBooleanValue("apply_random")) {
      systemGain += Math.sqrt(getParameterDoubleValue("system_gain_var")) * random.nextGaussian();
    } else {
      accumulatedVariance += getParameterDoubleValue("system_gain_var");
    }
    double transmitterGain = getParameterDoubleValue("tx_antenna_gain"); // TODO Should depend on angle
    
    double receivedPower = outputPower + systemGain + transmitterGain + totalPathGain;
    if (inLoggingMode) {
      logger.info("Resulting received signal strength:\t" + receivedPower + " (" + accumulatedVariance + ")");
    }
    
    if (dataType == TransmissionData.DELAY_SPREAD || dataType == TransmissionData.DELAY_SPREAD_RMS)
      return new double[] {delaySpread, delaySpreadRMS};

    return new double[] {receivedPower, accumulatedVariance};
  }
  
  /**
   * Returns all rays from given source to given destination if a transmission
   * were to be made. The resulting rays depend on the current settings and may
   * include rays through obstacles, reflected rays or scattered rays.
   * 
   * @param sourceX Source position X
   * @param sourceY Source position Y
   * @param destX Destination position X
   * @param destY Destination position Y
   * @return All resulting rays of a simulated transmission from source to destination
   */
  public Vector<Line2D> getRaysOfTransmission(double sourceX, double sourceY, double destX, double destY) {
    
    // Reset current rays vector
    inLoggingMode = true;
    savedRays = new Vector<Line2D>();
    
    // Calculate rays, ignore power
    getProbability(sourceX, sourceY, destX, destY, -Double.MAX_VALUE);
    
    inLoggingMode = false;
    
    return savedRays;
  }

  /**
   * Calculates and returns the signal to noise ratio (dB) of a signal sent from
   * the given source position to the given destination position as a random
   * variable. This method uses current parameters such as transmitted power,
   * obstacles, overall system loss etc.
   * 
   * @param sourceX
   *          Source position X
   * @param sourceY
   *          Source position Y
   * @param destX
   *          Destination position X
   * @param destY
   *          Destination position Y
   * @return Received SNR (dB) random variable. The first value is the random
   *         variable mean, and the second is the variance. The third value is the received signal strength which may be used in comparison with interference etc.
   */
  public double[] getSINR(double sourceX, double sourceY, double destX, double destY, double interference) {

    // Calculate received signal strength
    double[] signalStrength = getReceivedSignalStrength(sourceX, sourceY, destX, destY);

    double[] snrData = 
      new double[] { signalStrength[0], signalStrength[1], signalStrength[0] };

    // Add antenna gain TODO Should depend on angle
    snrData[0] += getParameterDoubleValue("rx_antenna_gain"); 
    
    double noiseVariance = getParameterDoubleValue("bg_noise_var");
    double noiseMean = getParameterDoubleValue("bg_noise_mean");

    if (interference > noiseMean)
      noiseMean = interference;
    
    if (getParameterBooleanValue("apply_random")) {
      noiseMean += Math.sqrt(noiseVariance) * random.nextGaussian();
      noiseVariance = 0;
    }

    // Applying noise to calculate SNR
    snrData[0] -= noiseMean;
    snrData[1] += noiseVariance;

    if (inLoggingMode) {
      logger.info("SNR at receiver:\t" + snrData[0] + " (" + snrData[1] + ")");
    }
    return snrData;
  }


  /**
   * Calculates and returns probability that a receiver at given destination receives a packet from a transmitter at given source.
   * This method uses current parameters such as transmitted power,
   * obstacles, overall system loss, packet size etc. TODO Packet size?! TODO Interfering signal strength
   * 
   * @param sourceX
   *          Source position X
   * @param sourceY
   *          Source position Y
   * @param destX
   *          Destination position X
   * @param destY
   *          Destination position Y
   * @param interference
   *          Current interference at destination (dBm)
   * @return [Probability of reception, signal strength at destination]
   */
  public double[] getProbability(double sourceX, double sourceY, double destX, double destY, double interference) {
    double[] snrData = getSINR(sourceX, sourceY, destX, destY, interference);
    double snrMean = snrData[0];
    double snrVariance = snrData[1];
    double signalStrength = snrData[2];
    double threshold = getParameterDoubleValue("snr_threshold");
    double rxSensitivity = getParameterDoubleValue("rx_sensitivity");

    // Check signal strength against receiver sensitivity and interference
    if (rxSensitivity > signalStrength - snrMean && threshold < rxSensitivity + snrMean - signalStrength) {
      if (inLoggingMode) {
        logger.info("Signal to low for receiver sensitivity, increasing threshold");
      }
      
      // Keeping snr variance but increasing theshold to sensitivity
      threshold = rxSensitivity + snrMean - signalStrength;
    }
    
    // If not random varianble, probability is either 1 or 0
    if (snrVariance == 0)
      return new double[] {
        threshold - snrMean > 0 ? 0:1, signalStrength
    };
    double snrStdDev = Math.sqrt(snrVariance);
    
    
    // "Missing" signal strength in order to receive packet is probability that
    // random variable with mean snrMean and standard deviance snrStdDev is above
    // current threshold.
    
    // (Using error algorithm method, much faster than taylor approximation!)
    double probReception = 1 - GaussianWrapper.cdfErrorAlgo(
        threshold, snrMean, snrStdDev);
    
    if (inLoggingMode) {
      logger.info("Probability of reception: " + probReception);
    }
    
    // Returns probabilities
    return new double[] { probReception, signalStrength };
  }
  
  /**
   * Calculates and returns root-mean-square delay spread when given destination receives a packet from a transmitter at given source.
   * This method uses current parameters such as transmitted power,
   * obstacles, overall system loss, packet size etc. TODO Packet size?!
   * 
   * @param sourceX
   *          Source position X
   * @param sourceY
   *          Source position Y
   * @param destX
   *          Destination position X
   * @param destY
   *          Destination position Y
   * @return RMS delay spread
   */
  public double getRMSDelaySpread(double sourceX, double sourceY, double destX, double destY) {
    return getTransmissionData(sourceX, sourceY, destX, destY, TransmissionData.DELAY_SPREAD)[1];
  }

  /**
   * Returns XML elements representing the current configuration.
   * 
   * @see #setConfigXML(Collection)
   * @return XML element collection
   */
  public Collection<Element> getConfigXML() {
    Vector<Element> config = new Vector<Element>();
    Element element;

    Enumeration paramEnum = parameters.keys();
    while (paramEnum.hasMoreElements()) {
      String name = (String) paramEnum.nextElement();
      element = new Element(name);
      element.setText(parameters.get(name).toString());
      config.add(element);
    }
    
    element = new Element("obstacles");
    element.addContent(myObstacleWorld.getConfigXML());
    config.add(element);

    return config;
  }

  /**
   * Sets the configuration depending on the given XML elements.
   * 
   * @see #getConfigXML()
   * @param configXML
   *          Config XML elements
   * @return True if config was set successfully, false otherwise
   */
  public boolean setConfigXML(Collection<Element> configXML) {
    for (Element element : configXML) {
      if (element.getName().equals("obstacles")) {
        myObstacleWorld = new ObstacleWorld();
        myObstacleWorld.setConfigXML(element.getChildren());
      } else {
        // Assuming parameter value

        // Fetch current class before applying saved value
        Object obj = parameters.get(element.getName());
        Class paramClass = obj.getClass();

        if (paramClass == Double.class) {
          parameters.put(element.getName(), new Double(Double.parseDouble(element.getText())));
        } else if (paramClass == Boolean.class) {
          parameters.put(element.getName(), Boolean.parseBoolean(element.getText()));
        } else if (paramClass == Integer.class) {
          parameters.put(element.getName(), Integer.parseInt(element.getText()));
        } else {
          logger.fatal("Unsupported class type: " + paramClass);
        }
      }
    }
    needToPrecalculateFSPL = true;
    needToPrecalculateOutputPower = true;
    settingsObservable.notifySettingsChanged();
    return true;    
  }
}
