
// Copyright (c) 2001
package uk.co.patrickhaston.mars;

import java.awt.*;
import javax.swing.*;
import java.math.*;
import java.util.Observer;
import java.util.Observable;

import oracle.jdeveloper.layout.XYConstraints;
import oracle.jdeveloper.layout.XYLayout;

/**
 * A Class class.
 * <P>
 * @author Patrick Haston
 */
public class MarsMap extends MPanel implements Observer
{
  private MarsFrame theFrame = null;
  private MarsMapPanel mapPanel = null;

  // Contents
  protected double scale;
  protected LatLong viewpoint;
  protected XYZcoord viewCoord;
  private double smallCircle;
  private double bigCircle;
  private double verticalOffset;
  private LatLong viewAngle = new LatLong();
  private Rectangle screenSize = new Rectangle();
  private double sinAngleA;
  private double cosAngleA;
  private double sinAngleB;
  private double cosAngleB;
  private double sinAngleN;
  private double cosAngleN;

  // x measures across the way, y measures vertically
  // and z measures back (depth).
  protected double radius = 8000000;

  // Display settings
  protected boolean showFeaturesFlag;
  protected boolean showGridlinesFlag;
  protected boolean showFeatureNamesFlag;
  protected boolean showSettlementsFlag;
  protected boolean showSettlementNamesFlag;
  protected boolean showLinksFlag;
  protected boolean showWaypointsFlag;

//  public MarsModel mars = null; // pointer to the data

  /**
   * Constructs a new instance.
   */
  public MarsMap(MarsMapPanel parent, MarsFrame frame) 
  {
    super();
    try  
    {
      theFrame = frame;
      mapPanel = parent;
      jbInit();
    }
    catch (Exception e) 
    {
      e.printStackTrace();
    }
  }

  /**
   * Initializes the state of this instance.
   */
  private void jbInit() throws Exception 
  {
    scale = 500001;
    viewpoint = new LatLong();
    viewpoint.east = 0;
    viewpoint.north = 0;
    viewCoord = new XYZcoord();
//    updateViewCoords();
//    Dimension d = new Dimension();
//    JPanel parent = (JPanel) this.getParent();
//    d = parent.getSize(d);
//    this.setSize(d);
    showFeaturesFlag = true;
    showGridlinesFlag = true;
    showFeatureNamesFlag = true;
    showSettlementsFlag = true;
    showSettlementNamesFlag = true;
    showLinksFlag = true;
    showWaypointsFlag = true;
  }

  public void update(Observable o, Object obj)
  {
    // no code yet
  }

  public void showFeatures(boolean flag)
    { showFeaturesFlag = flag; }

  public void showGridlines(boolean flag)
    { showGridlinesFlag = flag; }

  public void showFeatureNames(boolean flag)
    { showFeatureNamesFlag = flag; }

  public void showSettlements(boolean flag)
    { showSettlementsFlag = flag; }

  public void showSettlementNames(boolean flag)
    { showSettlementNamesFlag = flag; }

  public void showLinks(boolean flag)
    { showLinksFlag = flag; }

  public void setScale(double s)
    { scale = s; }
    
  // This is called every time the viewpoint is updated or the scale changes
  private void updateVariables()
  {
    // update the "constants" used frequently in the mapping routines
    screenSize = this.getBounds();
    
    if(viewpoint != null)
    {
      viewAngle.east = viewpoint.east * 3.14159 / 180;
      viewAngle.north = viewpoint.north * 3.14159 / 180;
      
      sinAngleA = Math.sin(viewAngle.east + 3.14159/2);;
      cosAngleA = Math.cos(viewAngle.east + 3.14159/2);
      sinAngleB = Math.sin(viewAngle.north + 3.14159/2);
      cosAngleB = Math.cos(viewAngle.north + 3.14159/2);
      sinAngleN = Math.sin(viewAngle.north);
      cosAngleN = Math.cos(viewAngle.north);
    }
  }
  // Convert from map coordinates to real world coordinates
  public LatLong viewToWorld(Point p)
  {
    // debug messages
    //theFrame.mars.systemMessage("Mouse click at (" + p.x + ", " + p.y + ")");
    
    p.x = p.x - screenSize.width / 2;
    p.y = screenSize.height / 2 - p.y;

    // debug messages
    //theFrame.mars.systemMessage("Relative click at (" + p.x + ", " + p.y + ")");
    
    LatLong l = new LatLong();
    // calculate the distance to the clicked point
    double dist = p.x * scale * p.x * scale + p.y * p.y * scale * scale;
    // check to see if it is on the planet
    if (dist > radius*radius) return null; // off the horizon...
    
    XYZcoord A = new XYZcoord();
    XYZcoord B = new XYZcoord();
    XYZcoord C = new XYZcoord();
    XYZcoord D = new XYZcoord();
    
    // work out the location of the point in cartesian coordinates using 3d trigometry
    
    A.x = 0;
    A.y = p.x * scale;
    A.z = 0;
    
    // debug messages
    //theFrame.mars.systemMessage("A =  (" + A.x + ", " + A.y + ", " + A.z + ")");
    
    B.x = -sinAngleN * p.y * scale;
    B.y = 0;
    B.z = cosAngleN * p.y * scale;

    // debug messages
    //theFrame.mars.systemMessage("B =  (" + B.x + ", " + B.y + ", " + B.z + ")");
    
    double z = Math.sqrt(Math.abs(radius*radius - dist));

    // debug messages
    //theFrame.mars.systemMessage("z =  (" + z + ")");

    C.x = cosAngleN * z;
    C.y = 0;
    C.z = sinAngleN * z;

    // debug messages
    //theFrame.mars.systemMessage("C =  (" + C.x + ", " + C.y + ", " + C.z + ")");
    
    // Add A + B + C
    D.x = A.x + B.x + C.x;
    D.y = A.y + B.y + C.y;
    D.z = A.z + B.z + C.z;
    
    // check for D.x == 0 as this causes problems for ATAN function
    if (D.x == 0) D.x = 0.00000001;
    
    // Convert cartesian coordinates to polar coordinates
    l.east = 90 - Math.atan2(D.x, D.y)*180/3.14159;
    l.north = 90 - Math.acos(D.z / radius)*180/3.14159;
    
    // offset the east coordinate by the view angle
    l.east += viewpoint.east;

    // debug messages
    //theFrame.mars.systemMessage("l.north =  (" + l.north + ")" + " C.z / radius = " + C.z / radius);
    
    return l;
  }

  // Convert from real world coordinates to map coordinates
  public Point worldToView(LatLong l)
  {
    double smallCircle, bigCircle;
    double verticalOffset, relativeAngle;
    LatLong v = new LatLong();
    Point p = new Point();
    Rectangle s = this.getBounds();

    v.east = l.east * 3.14159 / 180;
    v.north = l.north * 3.14159 / 180;

    // Calculate the difference in longitude between
    // the viewpoint and lat long "l"
    relativeAngle = (viewpoint.east - l.east) * 3.14159 / 180;

    // Calculate how far up to the centre of the elliptical
    // this line of latitude is
    verticalOffset = Math.cos(viewAngle.north) * radius
      * Math.sin(l.north * 3.14159 / 180);

    // Calculate the big radius of the ellipse
    bigCircle = Math.cos(v.north) * radius;

    // Calculate the small radius of the ellipse
    smallCircle = Math.sin(viewAngle.north) * bigCircle;

    p.x = (int) ( (s.width/2) - (Math.sin(relativeAngle)
      * bigCircle / scale) );
    p.y = (int) ( (s.height/2) + ((Math.cos(relativeAngle)
      * smallCircle - verticalOffset) / scale) );

    return p;
  }

  public void paintComponent(Graphics g)
  {
    g.drawString(this.getName(),0,15);
    updateViewCoords();

    if (showGridlinesFlag) drawGridlines(g);
    if (showFeaturesFlag) drawFeatures(g);
    if (showLinksFlag) drawLinks(g);
    if (showWaypointsFlag) drawWaypoints(g);
    if (showSettlementsFlag) drawSettlements(g);
    
  }

  public void drawGridlines(Graphics g)
  {
    int e, n;
    int step = 1;
    Point a, b, c;
    LatLong X = new LatLong();
    LatLong Y = new LatLong();
    LatLong Z = new LatLong();

    g.setColor(Color.darkGray);
    // draw Gridlines
    if (showGridlinesFlag)
    {
      // draw them according to the view scale
      if (scale > 200000)
      {
        // show only 30 degree grid lines
        step = 30;
      }
      else
      {
        if (scale > 5000)
        {
          // show 10 degree grid lines
          step = 10;
        }
        else
        {
          // show 1 degree grid lines
          step = 1;
        }
      }
    }

    // now draw the lines
    e = 0;
    while (e < 360)
    {
      n = -90;
      while (n < 90)
      {
        X.east = e;
        X.north = n;
        Y.east = e;
        Y.north = n + step;
        Z.east = e + step;
        Z.north = n;

        if (isLatLongVisible(X))
        {
          if (isLatLongVisible(Y))
          {
            // draw a line from X to Y
            a = worldToView(X);
            b = worldToView(Y);
            g.drawLine(a.x, a.y, b.x, b.y);
          }
          if (isLatLongVisible(Z))
          {
            // draw a line from X to Z
            a = worldToView(X);
            c = worldToView(Z);
            g.drawLine(a.x, a.y, c.x, c.y);
          }
        }

        n = n + step;
      }
      e = e + step;
    }
  }

  public void drawSettlements(Graphics g)
  {
    Rectangle rect = this.getBounds();
    Point centre = null;
    double size;
    int screenRadius;

    if (theFrame.mars.Settlements != null)
    {
      for (int i=1; i< theFrame.mars.Settlements.size(); i++)
      {
        if (theFrame.mars.Settlements.elementAt(i) != null)
        {
          MarsSettlement s = new MarsSettlement( (MarsSettlement) theFrame.mars.Settlements.elementAt(i));
          // check to see if this feature is on this side of the world
          if ( this.isLatLongVisible(s.getLocation()) )
          {
            centre = worldToView(s.getLocation());
            // Check to see if the feature will be visible on the screen
            if ( (centre.x) < rect.width &&
                 (centre.x) > 0 &&
                 (centre.y) < rect.height &&
                 (centre.y) > 0 )
            {
              if (s.getName() != null)
              {
                // draw this settlement
                screenRadius = 6;
                g.setColor(Color.white);
                g.fillOval(centre.x - screenRadius/2, centre.y - screenRadius/2, screenRadius, screenRadius);
                g.setColor(Color.yellow);
                g.drawString(s.getName(), centre.x, centre.y + 15);
              }
            }
          }
        }
      }
    }
    
  }

  public void zoomOut()
  {
    scale = scale * 1.414213562373;
    //updateVariables();
  }

  public void zoomIn()
  {
    scale = scale * 0.7071067811865475244;
    //updateVariables();
  }

  public void panNorth()
  {
    // amount to pan is a factor of scale
    if (viewpoint.north < 90)
    {
      viewpoint.north += 1;
    }
    if (viewpoint.north > 90)
    {
      viewpoint.north = 90;
    }
    //updateVariables();
  }

  public void panSouth()
  {
    // amount to pan is a factor of scale
    if (viewpoint.north > -90)
    {
      viewpoint.north -= 1;
    }
    if (viewpoint.north < -90)
    {
      viewpoint.north = -90;
    }
    //updateVariables();
  }

  public void panEast()
  {
    // amount to pan is a factor of scale
    viewpoint.east += 1;

    if (viewpoint.east > 360)
    {
      viewpoint.east -= 360;
    }
    //updateVariables();
  }

  public void panWest()
  {
    // amount to pan is a factor of scale

    viewpoint.east -= 1;

    if (viewpoint.east < 0)
    {
      viewpoint.east += 360;
    }
    //updateVariables();
  }

  public void fitToView()
  {
  }

  private void updateViewCoords()
  {
    /*
    double smallRadius;

    smallRadius = Math.cos(viewpoint.north * 3.14159 / 180);

    viewCoord.y = Math.sin(viewpoint.north * 3.14159 / 180);
    viewCoord.z = Math.cos(viewpoint.east * 3.14159 / 180) * smallRadius;
    viewCoord.x = Math.sin(viewpoint.east * 3.14159 / 180) * smallRadius;
    */
    viewCoord.set(viewpoint);
    updateVariables();
  }

  public boolean isLatLongVisible(LatLong l)
  {
    double smallRadius;
    XYZcoord p = new XYZcoord(l);
    XYZcoord d = new XYZcoord();
    /*
    smallRadius = Math.cos(l.north * 3.14159 / 180);

    p.y = Math.sin(l.north * 3.14159 / 180);
    p.z = Math.cos(l.east * 3.14159 / 180) * smallRadius;
    p.x = Math.sin(l.east * 3.14159 / 180) * smallRadius;
    */
    p.set(l);

    d.x = p.x - viewCoord.x;
    d.y = p.y - viewCoord.y;
    d.z = p.z - viewCoord.z;

    if( (d.x*d.x) + (d.y*d.y) + (d.z*d.z) < 2 ) return true;

    return false;
  }

  public void drawFeatures(Graphics g)
  {
    Rectangle rect = this.getBounds();
    Point centre = null;
    double size;

    if (theFrame.mars.Features != null)
    {
      for (int i=1; i< theFrame.mars.Features.size(); i++)
      {
        if (theFrame.mars.Features.elementAt(i) != null)
        {
          MarsFeature f = new MarsFeature( (MarsFeature) theFrame.mars.Features.elementAt(i));
          // check to see if this feature is on this side of the world
          if ( this.isLatLongVisible(f.location) )
          {
            centre = worldToView(f.location);
            size = (f.radius * 1000 / scale);
            // Check to see if the feature will be visible on the screen
            if ( (centre.x - size) < rect.width &&
                 (centre.x + size) > 0 &&
                 (centre.y - size) < rect.height &&
                 (centre.y + size) > 0 )
            {
              // draw this feature
              // draw craters and depressions
              if (f.featureType == 2 ||
                f.featureType == 3 ||
                //f.featureType == 5 ||
                f.featureType == 8 ||
                f.featureType == 22)// ||
                //f.featureType == 35)
                  drawCrater(g, f, Color.lightGray, Color.darkGray);
              // draw mensa, hill or mountain
              if (f.featureType == 6 ||
                f.featureType == 20 ||
                //f.featureType == 21 ||
                f.featureType == 33)
                  drawCrater(g, f, Color.darkGray, Color.lightGray);

            }
          }
        }
      }
    }
  }

  public void drawCrater(Graphics g, MarsFeature f, Color highlight, Color shadow)
  {
    double size;
    int shadowSize = 1;
    int shadowAngle;
    XYcoord p, s, a, b, c, d;
    double ri, ro;
    boolean notVisible;
    Point centre = worldToView(f.location);
    int screenRadius;
    Color iColor, oColor;

    size = (f.radius * 1000 / scale);
    if (size < 10) shadowSize = 1;
    if (size >= 10 && size < 20) shadowSize = 5;
    if (size >= 20 && size < 100) shadowSize = 10;
    if (size >= 100 && size < 1000) shadowSize = 30;
    if (size >= 10000) shadowSize = 90;

    screenRadius = (int) size;



    switch (shadowSize)
    {
      case 1: // tiny - just draw a circle
        g.setColor(highlight);
        g.drawOval(centre.x, centre.y, screenRadius, screenRadius);
        break;
      case 5: // draw shadows as arcs
        // draw the highlights
        g.setColor(highlight);
        g.drawArc(centre.x, centre.y, screenRadius+1, screenRadius+1, 135, 180);
        g.drawArc(centre.x, centre.y, screenRadius-1, screenRadius-1, 135, -180);
        // draw the shadows
        g.setColor(shadow);
        g.drawArc(centre.x, centre.y, screenRadius-1, screenRadius-1, 135, 180);
        g.drawArc(centre.x, centre.y, screenRadius+1, screenRadius+1, 135, -180);

        break;
      case 10:
      case 30:
      case 90:
        // draw the shadows as lines radiating from the crater rim
        for (shadowAngle=0; shadowAngle<360; shadowAngle += (360/size) )
        {
          // set the color according to the angle
          iColor = highlight;
          oColor = shadow;
          if (shadowAngle >= 135 && shadowAngle <= 315)
          {
            iColor = shadow;
            oColor = highlight;
          }
          // draw the inside line
          g.setColor(iColor);
          g.drawLine( (int) (centre.x + Math.cos(shadowAngle * 3.14159 / 180) * screenRadius * 0.9),
            (int) (centre.y + Math.sin(shadowAngle * 3.14159 / 180) * screenRadius * 0.9),
            (int) (centre.x + Math.cos(shadowAngle * 3.14159 / 180) * screenRadius * 0.98),
            (int) (centre.y + Math.sin(shadowAngle * 3.14159 / 180) * screenRadius * 0.98));
          // draw the outside line
          g.setColor(oColor);
          g.drawLine( (int) (centre.x + Math.cos(shadowAngle * 3.14159 / 180) * screenRadius * 1.02),
            (int) (centre.y + Math.sin(shadowAngle * 3.14159 / 180) * screenRadius * 1.02),
            (int) (centre.x + Math.cos(shadowAngle * 3.14159 / 180) * screenRadius * 1.1),
            (int) (centre.y + Math.sin(shadowAngle * 3.14159 / 180) * screenRadius * 1.1));
        }
    }
  }

  public void drawWaypoints(Graphics g)
  {
    MarsWaypoints wps = theFrame.mars.Waypoints;
    LatLong loc = null;
    Point centre = null;
    if(wps == null) return;
    if(wps.size() <= 1) return;
    for (int i=1; i<wps.size(); i++)
    {
      loc = wps.getLocation(i);
      if(isLatLongVisible(loc))
      {
        if(i == mapPanel.selectedWaypoint)
        {
           // draw a bigger, higlighted dot for this waypoint
          g.setColor(Color.yellow);
          centre = worldToView(loc);
          g.fillOval(centre.x-2, centre.y-2, 4, 4);
        }
        else
        {
          // draw a small red dot for this waypoint
          g.setColor(Color.red);
          centre = worldToView(loc);
          g.fillOval(centre.x-1, centre.y-1, 2, 2);
        }
      }
    }
  }
  
  public void drawLinks(Graphics g)
  {
    MarsLinks links = theFrame.mars.Links;
    MarsWaypoints wps = theFrame.mars.Waypoints;
    int[] wp = null;
    LatLong loc1, loc2;
    Point p1 = null;
    Point p2 = null;
    if(links == null) return;
    if(links.size() <= 1) return;
    if(wps == null) return;
    if(wps.size() <= 1) return;
    for (int i=1; i<links.size(); i++)
    {
      wp = links.getEnds(i);
      if (wp != null)
      {
        if (wp[0] > 0 && wp[1] > 0)
        {
          loc1 = wps.getLocation(wp[0]);
          loc2 = wps.getLocation(wp[1]);
          if(isLatLongVisible(loc1) && isLatLongVisible(loc2))
          {
             // draw a line for this link
            g.setColor(theFrame.mars.LinkTypes.getColour(links.getType(i)));
            p1 = worldToView(loc1);
            p2 = worldToView(loc2);
            g.drawLine(p1.x, p1.y, p2.x, p2.y);
          }
        }
      }
    }
  }


}


