/**
 * JavaScript code for working with Google maps
 *
 * map.js,v 1.45 2011/08/22 20:59:51 guym Exp
 */

/*global NASA_MARS_VISIBLE_MAP, NASA_MARS_ELEVATION_MAP, NASA_MARS_INFRARED_MAP, NASA_MARS_VISIBLE_MAP_NPOLAR, 
 NASA_MARS_ELEVATION_MAP_NPOLAR, NASA_MARS_VISIBLE_MAP_SPOLAR, NASA_MARS_ELEVATION_MAP_SPOLAR, 
 GLatLng, GMap2, GPolygon, GLatLngBounds, GEvent, GLargeMapControl3D, GScaleControl(), GOverviewMapControl*/

/* ========================================================================== */

/* Static methods */

/**
 * Coordinate conversion
 * 
 * @param lat
 * @param lon
 * @return map with keys x, y, z
 */
function latlon2xyz(lat, lon) 
{
    with (Math)
    {
        var theta = (90 - lat) * PI/180;
        var phi = lon * PI/180;

        var x = sin(theta) * cos(phi);
        var y = sin(theta) * sin(phi);
        var z = cos(theta);
    }
    
    return { x: x, y: y, z: z };
}

/**
 * Coordinate conversion
 * 
 * @param x
 * @param y
 * @param z
 * @return map with keys lat, lon
 */
function xyz2latlon(x, y, z) 
{
    with (Math)
    {
        var theta = atan2( sqrt(x*x + y*y), z );
        var phi = atan2( y,x );

        var lat = 90 - (theta * 180/PI);
        var lon = phi * 180/PI;
    }
    
    return { lat: lat, lon: lon };
}

/**
 * 
 * @param lat
 * @param lon
 * @return
 */
function get_rotation_pole(lat, lon)
{

    var rot_lat;
    var rot_lon;

    if ( lat < 0 )
    {
        rot_lon = lon;
        rot_lat = lat + 90;
    }
    else
    {
        rot_lon = (lon + 180) % 360;
        rot_lat = 90 - lat;
    }

    return latlon2xyz(rot_lat, rot_lon);
}

function rotate(x, y, z, u, v, w, a) 
{
    with (Math)
    {
        var a_rad = a * PI/180;

        var C = cos(a_rad);
        var S = sin(a_rad);

        // The following rotation math comes from
        // http://inside.mines.edu/~gmurray/ArbitraryAxisRotation/ArbitraryAxisRotation.html
        // Simplified for our u^2 + v^2 + w^2 = 1 case.

        var x_prime = x*( u*u + (v*v + w*w)*C ) + y*( u*v*(1-C)-w*S ) + z*( u*w*(1-C) + v*S );
        var y_prime = x*( u*v*(1-C) + w*S ) + y*( v*v +(u*u+w*w)*C ) + z*( v*w*(1-C) - u*S );
        var z_prime = x*( u*w*(1-C) - v*S ) + y*( v*w*(1-C) + u*S ) + z*( w*w +(u*u+v*v)*C );
        
    }
    
    return { x: x_prime, y: y_prime, z: z_prime };
}


/**
 * 
 * @param lat
 * @param lon
 * @param c1
 * @param c2
 * @param c3
 * @param a
 * @return
 */
function get_rotated_point(lat, lon, c1, c2, c3, a)
{
    var xyz_start = latlon2xyz(lat, lon);

    var xyz_rot = rotate(xyz_start.x, xyz_start.y, xyz_start.z, c1, c2, c3, a);

    var coord = xyz2latlon(xyz_rot.x, xyz_rot.y, xyz_rot.z);

    return { lat: coord.lat , lon: coord.lon };
}

/* ========================================================================== */

/**
 * Output a location as an HTML string with a given decimal precision
 * 
 * @param prec Precision (defaults to three if unspecified)
 */
GLatLng.prototype.toHTML = function(prec)
{
    var lng = this.lng();
    if (lng < 0) 
    {
    	lng += 360;
    }
    
    if (prec === undefined) 
    {
    	prec = 3;
    }
    return this.lat().toFixed(prec) + '&deg;' + (this.lat() >= 0 ? 'N' : 'S') +", " + lng.toFixed(prec) + '&deg;E';
};

/* ========================================================================== */

var BACKGROUNDS =
{
	nightir : 'Nighttime IR', // Themis	
    dayir   : 'Daytime IR', // Themis
    vis  : 'Visible',    // Viking MDIM
    elev : 'Elevation'   // MOLA
};

/* ========================================================================== */

function Body(radius)
{
    this.radius = Number(radius);
}

Body.prototype.metersPerDegree = function()
{
    return 2 * Math.PI * this.radius / 360.0;
};

var MARS_RADIUS_METERS = 3394500;

var MARS = new Body(MARS_RADIUS_METERS);

/* ========================================================================== */

function Instrument(body)
{
    this.body = body;
}

Instrument.prototype.getTargetWidthDegrees = function()
{
    return this.resolution * this.line_samples / this.body.metersPerDegree();
};

Instrument.prototype.getTargetHeightDegrees = function(lines, center)
{
    if (Math.abs(center.lat()) > 65) return this.getTargetWidthDegrees();
    
    return this.resolution * lines / this.body.metersPerDegree();
};

var MRO_HiRISE = new Instrument(MARS);
MRO_HiRISE.prototype = Instrument;

MRO_HiRISE.resolution =   0.3; // typical HiRISE resolution
MRO_HiRISE.inclination = 7; // degrees (science mapping orbit)
MRO_HiRISE.min_img_lines =  30000; // typical smallest image (unbinned)
MRO_HiRISE.max_img_lines = 200000; // typical largest image (unbinned)
MRO_HiRISE.avg_img_lines =  50000; // just under median (unbinned)
MRO_HiRISE.line_samples =  20000; // image width in pixels (unbinned)

var blueIcon = new GIcon(G_DEFAULT_ICON);
blueIcon.image = "/hiwish/include/blue-dot.png";

/* ========================================================================== */

/**
 * class SuggestionMap
 */
function SuggestionMap(canvas, center, zoom, editable)
{
	// only used in GMap2 constructor
    var MAPTYPES = 
    [
        NASA_MARS_DAYIR_MAP, 
        NASA_MARS_NIGHTIR_MAP,
        NASA_MARS_VISIBLE_MAP, 
        NASA_MARS_ELEVATION_MAP, 
        NASA_MARS_VISIBLE_MAP_NPOLAR, 
        NASA_MARS_ELEVATION_MAP_NPOLAR, 
        NASA_MARS_VISIBLE_MAP_SPOLAR, 
        NASA_MARS_ELEVATION_MAP_SPOLAR
    ];
    
    this.instrument = MRO_HiRISE;
    
    this.PADDING = 0.125; // fraction of the size of the map.getBounds()
    this.target = new GPolyline();
    this.marker = undefined;
    this.editable = editable;
    
    this.previousTargetCenter = undefined;
    this.numberLines = MRO_HiRISE.avg_img_lines;
    
    this.map = new GMap2(document.getElementById(canvas), { mapTypes: MAPTYPES });
    this.setCenter(center, zoom);
    
    if (center.lat() > 65)
    {    
        this.map.setMapType(NASA_MARS_ELEVATION_MAP_NPOLAR);
        this.bg = 'elev';
    }
    else if (center.lat() < -65)
    {
        this.map.setMapType(NASA_MARS_ELEVATION_MAP_SPOLAR);
        this.bg = 'elev';
    }
    else
    {
        this.map.setMapType(NASA_MARS_VISIBLE_MAP);
        this.bg = 'vis';
    }
    
    this.map.addControl(new GLargeMapControl3D());
    this.map.addControl(new GScaleControl());
    
    if (editable)
    {
    	this.map.addControl(new GOverviewMapControl());    
    }
    
    //this.map.addControl(new GHierarchicalMapTypeControl());
    // Minimum map zoom to display footprints
    this.footprintZoomMin = 6;

    // map of footprint managers, name => instance
    this.footprints =
	{
	    'MOC'   : new Footprints(MOC),
	    'CTX'   : new Footprints(CTX),
		'CRISM' : new Footprints(CRISM),
	    'RDR'   : new Footprints(RDR),
	    'HiT'   : new Footprints(HiT)
	}; 
}

SuggestionMap.prototype.mapMoved = function()
{
    this.changeMap(); 
    this.drawFootprints();
};

/**
 * Change the background (base map)
 * 
 * @param bg Should be one of the keys in the background array
 */
SuggestionMap.prototype.setBackground = function(bg)
{
    if (! BACKGROUNDS[bg])
    {
        return;
    }
    
    this.bg = bg;
    this.changeMap();
};

/**
 * Update the footprints
 * 
 * @param key key in the footprints map
 * @param state boolean to show (true) or hide
 */
SuggestionMap.prototype.updateFootprints = function(key, state)
{
    if (this.footprints[key] === undefined) 
    {
    	return;
    }
    
    var fps = this.footprints[key];
    
    if (state)
    {
        fps.show();
        
        if (fps.updated == false && this.map.getZoom() >= this.footprintZoomMin)
        {
            var bbox = this.getBoundingBox(this.PADDING);
            fps.update(bbox, this.map);
        }
    }
    else
    {
        fps.hide();
    }
};

/**
 Loads and draws all footprint objects
*/
SuggestionMap.prototype.drawFootprints = function()
{
    if (this.map.getZoom() < this.footprintZoomMin) 
    {
        return;
    }
    
    var bbox = this.getBoundingBox(this.PADDING);
    
    for (var key in this.footprints)
    {
        var fps = this.footprints[key];
        
        if (fps.enabled == true && fps.updated == false)
        {
            fps.update(bbox, this.map);  
        }
        else
        {
            fps.updated = false;
        }
    }    
};

/**
 * Returns the target region-of-interest serialized as a lng,lat,.... string.
 * If a target has not been defined, returns an empty string.
 * 
 * @return String in the form "lng,lat,lng,lat,..."
 */
SuggestionMap.prototype.getTargetAsString = function()
{    
    var num = this.target.getVertexCount() - 1;
    var str = '';

    if (num < 3) 
    {
    	return str;
    }
    
    var lat, lng;
    
    for (var i = 0 ; i < num ; i++)
    {
        var vtx = this.target.getVertex(i);
        lat = vtx.lat();
        lng = vtx.lng();
        
        if (lng < 0) 
        {
        	lng += 360;
        }
        
        str += lng.toFixed(3) + ',';
        str += lat.toFixed(3);
        
        if (i < num - 1) 
        {
        	str += ',';
        }
    }
    
    return str;
};

/**
 * Deserializes a lng,lat,.... string to set the target region-of-interest
 * 
 * @param str String in the form "lng,lat,lng,lat,..."
 */
SuggestionMap.prototype.setTargetFromString = function(str)
{
    var pts = str.split(/\s*,\s*/);
    
    if (pts.length < 2)
    {
        return;
    }
        
    var coords = [];
    
    for (var i = 0 ; i < pts.length - 1 ;  i += 2)
    {
        coords.push(new GLatLng(pts[i+1], pts[i]));
    }
    
    coords.push(coords[0]);
    
    this.drawTarget(this.map.getCenter(), coords);
};

/**
 * Return a bounding box for the current view
 * with a padding fraction (between 0 and 1)
 */
SuggestionMap.prototype.getBoundingBox = function(pad)
{
    var center = this.map.getCenter();
    
    if (Math.abs(center.lat()) > 65)
    {
        return this.getPolarBoundingBox(pad);
    }
    
    var bounds = this.map.getBounds();
    
    // In the polar projections, the values returned in lng() for E and W 
    // from getBounds() are sometimes backwards.
    var east = bounds.getNorthEast().lng();
    var west = bounds.getSouthWest().lng();
    
    if (east < 0) 
    {
    	east += 360;
    }
    
    if (west < 0) 
    {
    	west += 360;
    }

    var spanwidth  = (west > east) ? (360-west) + east : Math.abs(east - west);
    var spanheight = bounds.toSpan().lat();

    var latpad = spanheight * pad;
    var lonpad = spanwidth * pad;

    var ulat = bounds.getNorthEast().lat() + latpad;
    var llat = bounds.getSouthWest().lat() - latpad;
    
    var ulng = east + lonpad;
    var llng = west - lonpad;

    var clat = this.map.getCenter().lat();
    var clng = this.map.getCenter().lng();
/*    
    if ( Math.abs(clat) > 65 )
    {
       // Additional polar projection hacking, ensure a minimum width
       var halfwidth = 10;
       
       if ( Math.abs(ulng - llng) < 2 * halfwidth )
       {
          llng = clng - halfwidth;
          ulng = clng + halfwidth;
       }
    }
*/    
    if ( ulat > 90 ) 
    {
    	ulat = 90;
    }
    
    if ( llat < -90 ) 
    {
    	llat = -90;
    }

    if (ulng > 180) 
    {
    	ulng -= 360;
    }
    
    if (llng > 180) 
    {
    	llng -= 360;
    }

    return new GLatLngBounds(new GLatLng(llat, llng), new GLatLng(ulat, ulng));
}; 

SuggestionMap.prototype.getPolarBoundingBox = function(pad)
{
   var north = -90;
   var south = +90;
   var east = 0;
   var west = 360;
   var size = this.map.getSize();
   var self = this;

   /* this is a temporary hack. The corners may not be the most north, south,
    * east, west.
    */
   for (var i = 0 ; i < 1200 ; i++)
   {
      [ new GPoint(i, 0), new GPoint(size.width, i), new GPoint(i, size.height), new GPoint(0, i) ].forEach(
            function(pt) {
               var gll = self.map.fromDivPixelToLatLng(pt);
               var lat = gll.lat();
               var lng = gll.lng();

               if (lat > north) 
               {
            	   north = lat;
               }
               
               if (lat < south) 
               {
            	   south = lat;
               }
               
               if (lng > east) 
               {
            	   east = lng;
               }
               
               if (lng < west) 
               {
            	   west = lng;
               }
            }
            );
   }

    var nw = new GLatLng(north, west);
    var ne = new GLatLng(north, east);
    var se = new GLatLng(south, east);
    var sw = new GLatLng(south, west);

    //this.map.addOverlay(new GPolyline([ nw, ne, se, sw, nw ], '#bcbcbc'));
    //console.log("[N,S,E,W] = " + nmost.lat().toFixed(1) + "," + smost.lat().toFixed(1) + "," + emost.lng().toFixed(1) + "," + wmost.lng().toFixed(1));
    //console.log("A,B="+ay.toFixed(1)+","+ax.toFixed(1)+","+by.toFixed(1)+","+bx.toFixed(1));
    return new GLatLngBounds(sw, ne);
};

/**
 * Re-centers the view on the target, if it is defined, otherwise does nothing.
 * Used in the 'goto' links.
 * 
 * @param zoomin If true, zoom into the default target zoom level
 */
SuggestionMap.prototype.recenter = function(zoomin)
{
    if (this.target.getVertexCount() < 3)
    {
        return;
    }
    
    var center = this.target.getBounds().getCenter();

    var zoom = this.map.getZoom();

    if (zoomin === true && zoom < 7)
    {
        zoom = 7;
    }
    
    this.setCenter( center, zoom );
};

/**
 * Change the map so it is centered on a particular location and zoom level.
 * 
 * @param center A GLatLng point to center on
 * @param zoom level to zoom to
 */
SuggestionMap.prototype.setCenter = function(center, zoom)
{ 
    this.map.setCenter(center, zoom);    
    this.changeMap();
    
    //GEvent.trigger(this.map, 'moveend', center);    
};

/**
 * 
 */
SuggestionMap.prototype.changeMap = function()
{
    var center = this.map.getCenter();
    
    var proj = 'eq';
    
    if (center.lat() > 65)
    {
        proj = 'np';
    }
    else if (center.lat() < -65)
    {
        proj = 'sp';
    }

    var type = proj + '_' + this.bg;
    var mapType = this.map.getCurrentMapType();
    
    switch (type)
    {
	    case 'eq_vis':
	        mapType = NASA_MARS_VISIBLE_MAP;
	        break;
	    case 'eq_elev':
	    	mapType = NASA_MARS_ELEVATION_MAP;
	        break;
	    case 'eq_dayir':
	    	mapType = NASA_MARS_DAYIR_MAP;
	        break;
	    case 'eq_nightir':
	    	mapType = NASA_MARS_NIGHTIR_MAP;
	        break;        
	    case 'np_vis':
	    	mapType = NASA_MARS_VISIBLE_MAP_NPOLAR;
	        break;
	    case 'np_elev':
	    	mapType = NASA_MARS_ELEVATION_MAP_NPOLAR;
	        break;
	    case 'sp_vis':
	    	mapType = NASA_MARS_VISIBLE_MAP_SPOLAR;
	        break;
	    case 'sp_elev':
	    	mapType = NASA_MARS_ELEVATION_MAP_SPOLAR;
	        break;       
	    case 'np_dayir':
	    	mapType = NASA_MARS_ELEVATION_MAP_NPOLAR;
	        GEvent.trigger(this.map, 'bgchanged', 'elev');
	        break;
	    case 'sp_dayir':
	    	mapType = NASA_MARS_ELEVATION_MAP_SPOLAR;
	        GEvent.trigger(this.map, 'bgchanged', 'elev');
	        break;
	    case 'np_nightir':
	    	mapType = NASA_MARS_ELEVATION_MAP_NPOLAR;
	        GEvent.trigger(this.map, 'bgchanged', 'elev');
	        break;
	    case 'sp_nightir':
	    	mapType = NASA_MARS_ELEVATION_MAP_SPOLAR;
	        GEvent.trigger(this.map, 'bgchanged', 'elev');
	        break;               
    }
    
	if (this.map.getCurrentMapType() != mapType)
    {
    	try
    	{
    		this.map.setMapType(mapType);
    	}
    	catch (ex)
    	{
    		console.log(ex);
    	}
    }
};

SuggestionMap.prototype.calculateCoords = function(point, lines)
{
    var half_height = 0.5 * this.instrument.getTargetHeightDegrees(lines, point);    
    var half_width  = 0.5 * this.instrument.getTargetWidthDegrees();

    upper_lat = point.lat() + half_height;
    lower_lat = point.lat() - half_height;

    var upper_rotation_pole = get_rotation_pole( upper_lat, point.lng() );
    var lower_rotation_pole = get_rotation_pole( lower_lat, point.lng() );

    var ul = get_rotated_point( upper_lat, point.lng(), upper_rotation_pole.x, upper_rotation_pole.y, upper_rotation_pole.z, half_width );
    var ur = get_rotated_point( upper_lat, point.lng(), upper_rotation_pole.x, upper_rotation_pole.y, upper_rotation_pole.z, (-1 * half_width) );    
    var ll = get_rotated_point( lower_lat, point.lng(), lower_rotation_pole.x, lower_rotation_pole.y, lower_rotation_pole.z, half_width );
    var lr = get_rotated_point( lower_lat, point.lng(), lower_rotation_pole.x, lower_rotation_pole.y, lower_rotation_pole.z, (-1 * half_width) );

    if (ul.lat == lr.lat || ul.lon == lr.lon) 
    {
    	return null;
    }
    
    // Now rotate this footprint for the orbital inclination
    var inc_rot_pole = latlon2xyz( point.lat(), point.lng() );
    var ulr = get_rotated_point( ul.lat, ul.lon, inc_rot_pole.x, inc_rot_pole.y, inc_rot_pole.z, this.instrument.inclination );
    var urr = get_rotated_point( ur.lat, ur.lon, inc_rot_pole.x, inc_rot_pole.y, inc_rot_pole.z, this.instrument.inclination );
    var llr = get_rotated_point( ll.lat, ll.lon, inc_rot_pole.x, inc_rot_pole.y, inc_rot_pole.z, this.instrument.inclination );
    var lrr = get_rotated_point( lr.lat, lr.lon, inc_rot_pole.x, inc_rot_pole.y, inc_rot_pole.z, this.instrument.inclination );

    var corners =
    [
        new GLatLng( ulr.lat, ulr.lon ),
        new GLatLng( urr.lat, urr.lon ),
        new GLatLng( lrr.lat, lrr.lon ),
        new GLatLng( llr.lat, llr.lon ),
        new GLatLng( ulr.lat, ulr.lon )
    ];
    
    return corners;
};

/**
 * 
 * @param point
 * @param lines
 * @return
 */
SuggestionMap.prototype.placeTarget = function(point)
{   
    this.drawTarget(point, this.calculateCoords(point,this.numberLines));
};

/**
 * 
 * @param coords
 */
SuggestionMap.prototype.drawTarget = function(point, coords)
{
    var center;
    
    if (coords)
    {
        this.map.removeOverlay( this.target );
        this.target = new GPolyline( coords, "#fbfbf5", 3, 1 );
        center = point;
        this.map.addOverlay(this.target);
        
        GEvent.trigger(this.map, 'nudge', center);
    }
    else
    {
        return;
    }
    
    if (this.editable)
    {
        if (this.marker !== undefined) 
        {
        	this.map.removeOverlay( this.marker );
        }
        
        this.marker = new GMarker(center, { title: 'Your new target', draggable: true, icon: blueIcon });    
        GEvent.bind(this.marker, 'drag', this, this.nudgeTarget);
        GEvent.bind(this.marker, 'dragstart', this, this.startNudgeTarget);
        this.map.addOverlay(this.marker);  
    }   
};

SuggestionMap.prototype.stretchTarget = function(lines)
{
    var coords = this.calculateCoords(this.marker.getLatLng(), lines);
    this.map.removeOverlay( this.target );
    this.target = new GPolyline( coords, "#fbfbf5", 3, 1 );
    this.map.addOverlay(this.target);
    this.numberLines = lines;
    
    // fire click event so the page knows we've updated
    GEvent.trigger(this.map, 'nudge', this.target.getBounds().getCenter());    
};

/**
 * Handle a drag on the marker by nudging the target. Fires a 'nudged' event
 * on the map with the new GLatLon of the target's center.
 * 
 * @param gll New center for target
 */
SuggestionMap.prototype.nudgeTarget = function(gll)
{
    //console.log("Nudging");
    var bounds = this.target.getBounds();
    
    if (bounds.containsLatLng(new GLatLng(90, 0)) || bounds.containsLatLng(new GLatLng(-90, 0))) 
    {
    	return;
    }
    
    var center = bounds.getCenter();
    var latstep = gll.lat() - center.lat();
    var lngstep = gll.lng() - center.lng();    
    var latlngs = [];
    
    for ( var i = 0 ; i < this.target.getVertexCount() ; i++ )
    {
        var vtx = this.target.getVertex(i);
        latlngs.push( new GLatLng( vtx.lat() + latstep, vtx.lng() + lngstep ) );
    }
        
    this.map.removeOverlay( this.target );
    this.target = new GPolyline( latlngs, "#fbfbf5", 3, 1 );
    this.map.addOverlay(this.target);
    
    // fire click event so the page knows we've updated
    GEvent.trigger(this.map, 'nudge', gll);
};

/**
 * Store the current location of the target, so that 
 * if the user accidentally moves it to overlap with an existing 
 * target, they can revert to the old location
 * 
 * @param gll New center for target
 */
SuggestionMap.prototype.startNudgeTarget = function(gll)
{
   this.previousTargetCenter = gll;
};

/*
 * takes an array of coordinates, makes a polyLine,
 * then calls getBounds() on that line, and 
 * recomputes an array of coordinates based on 
 * that bounds object.
 */
SuggestionMap.prototype.boundingBoxCoords = function(coords)
{
    
    var poly = new GPolyline( coords, "#fbfbf5", 3, 1 );
    var bounds = poly.getBounds();
    var southWest = bounds.getSouthWest();
    var northEast = bounds.getNorthEast();
    
    var myCorners =
    [
        new GLatLng( southWest.lat(), southWest.lng() ),
        new GLatLng( northEast.lat(), southWest.lng() ),
        new GLatLng( northEast.lat(), northEast.lng() ),
        new GLatLng( southWest.lat(), northEast.lng() ),
        new GLatLng( southWest.lat(), southWest.lng() )
    ];
    
    return myCorners;
};

/*
 * Takes a GLatLngBounds and returns a GPolyline
 * which matches that bounds.
 */
SuggestionMap.prototype.boundingBox2gPolyline = function(bounds)
{
    var southWest = bounds.getSouthWest();
    var northEast = bounds.getNorthEast();
    var myCorners =
    [
        new GLatLng( southWest.lat(), southWest.lng() ),
        new GLatLng( northEast.lat(), southWest.lng() ),
        new GLatLng( northEast.lat(), northEast.lng() ),
        new GLatLng( southWest.lat(), northEast.lng() ),
        new GLatLng( southWest.lat(), southWest.lng() )
    ];
    var poly = new GPolyline(myCorners, "#fbfbf5", 3, 1 );
    return poly;
};

