<?xml version="1.0" encoding="UTF-8"?>
<Module>

<!-- Copyright (C) 2007 Michael Kosowsky  All rights reserved. -->

<ModulePrefs
    title="Path Profiler"
    description="Draw an arbitrary path on the map and see its elevation profile"
    author="Michael Kosowsky"
    author_email="profiler_mapplet+nospam@heywhatsthat.com"
    author_location="Lincolnville, Maine, USA"
    author_affiliation="heywhatsthat.com"
    screenshot="http://www.heywhatsthat.com/mapplets/profiler-screenshot.png"
    thumbnail="http://www.heywhatsthat.com/mapplets/profiler-thumbnail.png"
    scrolling="true"
    >
  <Require feature="sharedmap"/>
  <Require feature="setprefs" />
  <Require feature="analytics"/>
  <Require feature="dynamic-height"/>
</ModulePrefs>
<UserPref name="use_metric"     datatype="hidden" default_value="0" />
<UserPref name="latlon_mode"    datatype="hidden" default_value="0" />
<UserPref name="curved_earth"   datatype="hidden" default_value="1" />
<UserPref name="show_scale"     datatype="hidden" default_value="1" />
<UserPref name="show_grade"     datatype="hidden" default_value="0" />


<Content type="html"><![CDATA[

<style type="text/css">
  .button { color: blue; cursor: pointer; text-decoration: underline; -moz-user-select: none }
  .button:hover { color: black }
  .rbutton { text-align: right; color: blue; cursor: pointer; text-decoration: underline; -moz-user-select: none }
  .rbutton:hover { color: black }
  .rbutton:visited { color: blue }
  table { font-size: 12px }
</style>


<div style="font-size:12px">

<div class="rbutton"><a class="rbutton" target="_blank" href="http://www.heywhatsthat.com">HeyWhatsThat</a></div>
<div class="rbutton"><a class="rbutton" target="_blank" href="http://www.heywhatsthat.com/mappletfaq.html">FAQ</a></div>
<div class="rbutton" title="Sign up for HeyWhatsThat email updates" onclick="signup()">Sign up</div>
<script>
document.write('<div class="rbutton"><a class="rbutton" target="_blank" href="mailto:comments-profiler-mapplet@hey' + '' + 'what' + 'sthat.com">Comments?</a></div>');
</script>

<p>
<form name="f_settings" onsubmit="return false">
<center>
<table style="font-size:10px">
  <tr>
    <td><input type="radio" name="curved_earth" value="0" onclick="set_curved_earth(0)">flat Earth</input></td>
    <td>&nbsp;&nbsp;&nbsp;</td>
    <td title="Include axes on the profile"><input type="checkbox" name="show_scale" onclick="set_show_scale(this.checked)">show scale</input></td>
  </tr><tr>
    <td><input type="radio" name="curved_earth" value="1" onclick="set_curved_earth(1)">curved Earth</input></td>
    <td>&nbsp;&nbsp;&nbsp;</td>
    <td title="Show elevation change along each leg"><input type="checkbox" name="show_grade" onclick="set_show_grade(this.checked)">show grade</input></td>
  </tr>
  </tr><tr>
    <td>&nbsp;&nbsp;&nbsp;</td>
    <td>&nbsp;&nbsp;&nbsp;</td>
    <td title="Calculate driving route as you add points"><input type="checkbox" name="show_route">follow roads</input></td>
  </tr>
  <tr><td>&nbsp;</td></tr>
  <tr>
    <td><input type="radio" name="units" value="0" onclick="set_units(0)">English</input></td>
    <td>&nbsp;&nbsp;&nbsp;</td>
    <td><input type="radio" name="latlon_mode" value="0" onclick="set_latlon_mode(this.value)">DD.DDDDDD&deg;</input></td>
  </tr><tr>
    <td><input type="radio" name="units" value="1" onclick="set_units(1)">Metric</input></td>
    <td></td>
    <td><input type="radio" name="latlon_mode" value="1" onclick="set_latlon_mode(this.value)">DD&deg; MM.MMMM'</input></td>
  </tr><tr>
    <td></td>
    <td></td>
    <td><input type="radio" name="latlon_mode" value="2" onclick="set_latlon_mode(this.value)">DD&deg; MM' SS.SS"</input></td>
  </tr>
</table>
</center>
</form>

<p>
<div id="listbutton_div" style="text-align:right">
  <form name="f_points" onsubmit="return false">
  <span class="button" onclick="handle_location()" title="Add this address as next point">Find:</span> <input name=location size=30 onchange="handle_location()"><br>
  <div class="rbutton" onclick="backspace()" title="Remove last point">Backspace</div>
  <div class="rbutton" onclick="clearx()" title="Remove all points">Clear</div>
  <div class="rbutton" onclick="popup_transect()" title="Open profile in its own window">Popup</div>
  </form>
</div>
<div id="list_div" style="white-space: pre"></div>
<p>
<center><i>Note: bearings are true, not magnetic</i></center>
<p>
<center>
<img id="transect_img" onclick="popup_transect()"></img>
</center>

<!--
<p>
Click on a few points on the map and hit 'Draw.'
Click on the image to open it in a popup window.
-->
<p>
<i>
Visit <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>
where we show you the summits you can see from almost anywhere in the world.
<p>
This mapplet runs most reliably on <a href="http://www.mozilla.com/firefox" target="_blank">Firefox 1.5 or later</a> at this time.
</i>
</div>

<script>
_IG_Analytics("UA-2222064-1", "/mapplets/profiler");
_IG_AdjustIFrameHeight();  // need this and dynamic-height feature as of about 10/20/07

var is_msie = navigator.userAgent.toLowerCase().indexOf('msie') != -1;

Array.prototype._foreach = function(f) { for (var _i = 0; _i < this.length; _i++) f(this[_i]); };

var current = {
  magnetic: false,
  points: [],
  legs: [],
  latlon_mode: 0,
  show_grade: 0,
  use_metric: 0,
  img_src: null
};

var map = new GMap2();
var prefs = new _IG_Prefs(__MODULE_ID__);

var transect_img = document.getElementById('transect_img');
var list_div     = document.getElementById("list_div");

GEvent.addListener(map, "click", handle_map_click);
init_transect();
clearx();


var colors = [ 'ff0000', '00b000', '0000ff' ];

function encodeURI_more(s) {
  s = encodeURI(s);
  s = s.replace(/\&/g, '%26');
  s = s.replace(/\=/g, '%3D');
  s = s.replace(/\+/g, '%2B');
  return s;
}

function signup() {
  var email = prompt("Give us an email address and\nwe'll send you news about HeyWhatsThat", '');
  if (email)
    _IG_FetchContent('http://www.heywhatsthat.com/bin/subscribe.cgi?src=profilermapplet&email=' + encodeURI_more(email),
      function(s) {
        var a = s.replace(/\n/g,'');
	if (a)
          alert(a + ' subscribed.  Thanks.');
        else
	  alert('Subscription failed for ' + email);
      }
    );
}


function backspace() {
  if (current.points.length == 0)
    return;
  if (current.points.length == 1) {
    clearx();
    return;
  }
  var p = current.legs.pop();
  if (p) {
    map.removeOverlay(p.line);
    current.total_dist -= p.dist;
  }
  p = current.points.pop();
  map.removeOverlay(p.marker);
  write_list_div();
  draw_transect();
}

function clearx() {
  current.legs._foreach(function(p) { map.removeOverlay(p.line); });
  current.points._foreach(function(p) { map.removeOverlay(p.marker); });
  current.points = [];
  current.legs = [];
  current.total_dist = 0;
  current.total_dist_is_true = true;
  write_list_div();
  draw_transect();
}


function handle_map_click(overlay, point) {
  if (!point)
    return;
  add_point(point.y, point.x);
  write_list_div();
  draw_transect();
}

function add_elev_to_point(p, elev) {
  p.elev = elev;
  set_point_html(p);
}

function add_steps_to_leg(l, steps) {
  l.steps = steps;
  var dist = route_length(steps);
  l.total_dist += dist - l.dist;
  l.dist = dist;
  set_leg_html(l);
  update_legs(l);

  map.removeOverlay(l.line);
  l.line = new GPolyline(l.steps, '#' + l.color, 5, .5);
  map.addOverlay(l.line);
}

function settings_changed() {
  // take this opportunity to recalculate total_dist too
  current.points._foreach(function(p) { set_point_html(p); });
  update_legs();
}

function update_legs(start) {
  var i;

  if (start) {
    current.total_dist = start.total_dist;
    current.total_dist_is_true = start.total_dist_is_true;
    for (i = 0; i < current.legs.length && current.legs[i] != start; i++) ;
    i++;
  } else {
    current.total_dist = 0;
    current.total_dist_is_true = true;
    i = 0;
  }
  for (; i < current.legs.length; i++) {
    var l = current.legs[i];
    current.total_dist += l.dist;
    current.total_dist_is_true &= l.dist_is_true;
    l.total_dist = current.total_dist;
    l.total_dist_is_true = current.total_dist_is_true;
    set_leg_html(l);
  }
  write_list_div();
}


function add_point(lat, lon, elev, address) {
  lat = round6(lat);
  lon = round6(lon);

  var latlng = new GLatLng(lat, lon);

  if (elev == null)
    elev = get_elev(lat, lon);

  var p = {address: address,
           lat:  lat,
	   lon:  lon,
	   latlng: latlng,
	   elev: elev,
	   marker: new GMarker(latlng, { icon: icon_plus, clickable: false, title: lat_lon_to_string(lat, lon, 0) })
	  };

  set_point_html(p);
  current.points.push(p);
  map.addOverlay(p.marker);

  if (current.points.length > 1) {
    var pp = current.points[current.points.length - 2];

    var b  = bearing_and_range(pp.lat, pp.lon, lat, lon);
    var rb = bearing_and_range(lat, lon, pp.lat, pp.lon);
    current.total_dist += b[1];
    // current.total_dist_is_true 

    var l = { prev: pp,
	      next: p,
              smoothed: document.f_settings.show_route.checked,
	      color:    colors[(current.points.length - 2) % colors.length],
              steps:    [ pp.latlng, p.latlng ],
	      dist_is_true: true,

	      dist: b[1],
	      total_dist: current.total_dist,
	      total_dist_is_true: current.total_dist_is_true,
	      bearing: b[0],
	      reverse_bearing: rb[0]
            };
    set_leg_html(l);
    current.legs.push(l);
    l.line = new GPolyline(l.steps, '#' + l.color, 5, .5);
    map.addOverlay(l.line);

    if (l.smoothed)
      get_route(l);
  }
}


function set_point_html(p) {
  p.html =   (p.address? '<tr><td colspan=3>' + p.address + '</td></tr>' : '')
           + '<tr align=right>'
           + '<td>' + format_angle(p.lat, current.latlon_mode, 'N', 'S', 1) + '</td>'
           + '<td>' + format_angle(p.lon, current.latlon_mode, 'E', 'W', 1) + '</td>'
	   + (p.elev != null? '<td>' + feet_or_meters(p.elev) + '</td>' : '')
           + '</tr>';
}


function set_leg_html(l) {
  var start_elev = current.points[0].elev;
  var prev_elev  = l.prev.elev;
  var next_elev  = l.next.elev;

  l.html =   '<tr align=right style="color: #' + l.color + '">'
           + '<td title="reverse bearing ' + round0(adjust_for_declination(l.reverse_bearing)) + '">'
		+ round0(adjust_for_declination(l.bearing)) + '&deg;</td>'
           + '<td>' + miles_or_km1(l.dist) + '</td>'
           + '<td>' + miles_or_km0_no_label(l.total_dist) + '&nbsp;total</td></tr>'
	   + (current.show_grade && start_elev != null && prev_elev != null && next_elev != null?
	          '<tr align=right style="color: #' + l.color + '">'
                + '<td>' + feet_or_meters(next_elev - prev_elev) + ' (' + percent1((next_elev - prev_elev)/l.dist) + ')</td>'
	        + '<td>' + feet_or_meters_no_label(next_elev - start_elev) + '&nbsp;total</td></tr>'
              : '');
}


function write_list_div() {
  if (!current.points.length) {
    list_div.innerHTML = '';
    return;
  }

  var s = '<table>';
  for (var i = 0; i < current.points.length - 1; i++)
    s += current.points[i].html + current.legs[i].html;
  s += current.points[current.points.length - 1].html + '</table>';
  list_div.innerHTML = s;
}


var geocoder = null;
function geocode(s, f) {
  if (!geocoder)
    geocoder = new GClientGeocoder();
  geocoder.getLatLngAsync(s, function(l) { if (l) f(l); else alert(s + " not found"); });
}
  

function handle_location() {
  if (!document.f_points.location.value)
    return;
  geocode(document.f_points.location.value, function(l) {
      add_point(l.lat(), l.lng(), null, document.f_points.location.value);
      map.setCenter(l, 12);
      write_list_div();
      draw_transect();
  });
}

function get_route(l) {
		// we can only fit about 300 points on the URL line (apache has compiled limit of 8190), so limit # of points we get here
    _IG_FetchContent('http://www.heywhatsthat.com/bin/directions.cgi?max=50&q=from+' + l.prev.lat + ',' + l.prev.lon + '+to+' + l.next.lat + ',' + l.next.lon,
      function(s) {
        var a = s.split(/\n/);
	a.pop();  // trailing NL
	steps = [];
	for (var i = 0; i < a.length && i < 1000; i++) {
	  var b = a[i].split(/\s+/);
		// b[2] is elev if you add &elevs=1 to request
	  steps.push(new GLatLng(b[0], b[1]));
        }
        if (steps.length > 2) {
	  add_steps_to_leg(l, steps);
	  draw_transect();
	} else {
	  alert('No driving route available between last two points');
        }
      });
}


					//***************************** TRANSECTS
function init_transect() {
    document.getElementById('transect_img').src = 'http://www.heywhatsthat.com/images/click-r.png';
			// 'http://www.heywhatsthat.com/images/profile-hit-calculate-r.png';
}

function draw_transect(warn) {
  if (current.points.length < 2) {
    if (warn)
      alert("Click on at least two points,\nthen hit 'Draw profile'");
    init_transect();
    return;
  }

	// consider URL length limits: Apache 8190 and IE 2083
	// 110 bytes address and args
	// 37 for each anchor point
	// 27 for each intermediate point
	// reduce by 5 for safety

  var limit = ((is_msie? 2083 : 8190) - 110 - 37 * current.points.length) / 27 - 5;
  if (limit <= 0) {
    alert("Too many points to draw profile");
    return;
  }
  var t = 0;
  current.legs._foreach(function(l) { t += l.steps.length - 2; });
  var inc = t < limit? 1 : 1 + Math.floor((t - 1)/limit);

  var s = '';
  var i = 0;
  current.legs._foreach(function(l) {
    for (var j = 0; j < l.steps.length - 1; j += inc) {
      s += '&pt' + i + '=' + l.steps[j].lat() + ',' + l.steps[j].lng();
      if (j == 0)
        s += ',' + l.color;
      i++;
    }
  });
  s += '&pt' + i + '=' + current.points[current.points.length - 1].lat + ',' + current.points[current.points.length - 1].lon;
  current.img_src = 'http://www.heywhatsthat.com/bin/profile.cgi?src=profilermapplet&curvature=' + radio_get(document.f_settings.curved_earth) + '&metric=' + current.use_metric + '&axes=' + (document.f_settings.show_scale.checked? 1 : 0) + s;
  transect_img.src = current.img_src + '&rotate=1&inc=' + inc;
}


function popup_transect() {
  if (current.points.length < 2) {
    alert("Click on a few spots on the map, then hit 'Popup' again");
    return;
  }
  var w = window.open('', '');
  w.document.write('<html><body><center><img src="' + current.img_src + '"><p>');
  var s = '';
  for (var i = 0; i < current.points.length - 1; i++)
    s += current.points[i].html + current.legs[i].html;
  w.document.write('<table style="font-size:small">' + s + current.points[current.points.length - 1].html + '</table></center></body></html>\n');
  w.document.close();
  w.focus();

// window.open(current.img_src, '', 'height=200,width=840');
}



					//**************************** ELEVATION DATA CACHE
var elevs = new Object;

function set_elev(lat, lon, elev) {
  elevs[round6(lat) + ' ' + round6(lon)] = elev;
}

function get_elev(lat, lon, callback) {
  var lat0 = round6(lat);
  var lon0 = round6(lon);

  if (elevs[lat0 + ' ' + lon0] != null)
    return elevs[lat0 + ' ' + lon0];

    _IG_FetchContent('http://www.heywhatsthat.com/bin/points.cgi?lat0=' + lat + '&lon0=' + lon,
      function(s) {
        var a = s.split(' ');
	elevs[round6(a[0]) + ' ' + round6(a[1])] = a[2];
	check_elevs();
      }
    );
}

function check_elevs() {
  var got_one = 0;
  current.points._foreach(function(p) {
    var e;
    if (p.elev == null && (e = elevs[p.lat + ' ' + p.lon]) != null) {
      add_elev_to_point(p, e);
      got_one = 1; }
    });

	// if show_grade, adding an elev only affects the adjacent legs, unless it's the first one
	// but we'll take the cowards way out and redo the whole thing
  if (got_one)
    if (current.show_grade)
      settings_changed();
    else
      write_list_div();
}


					//***************************** MISCELLANEOUS

function round0(x) {
  return Math.round(x);
}

function round1(x) {
  return Math.round(x * 10)/10;
}

function round2(x) {
  return Math.round(x * 100)/100;
}

function round4(x) {
  return Math.round(x * 10000)/10000;
}

function round6(x) {
  return Math.round(x * 1000000)/1000000;
}

function miles_or_km0_no_label(x) {
  return round0(current.use_metric? x/1000 : x/1609.344);
}

function miles_or_km1(x) {
  return current.use_metric?  round1(x/1000).toFixed(1)     + '&nbsp;km'
	                    : round1(x/1609.344).toFixed(1) + '&nbsp;miles';
}

var feet_per_meter = 1 / .3048;

function feet_or_meters_no_label(x) {
  return current.use_metric? round0(x) : round0(x * feet_per_meter);
}

function feet_or_meters(x) {
  return current.use_metric? round0(x) + ' m' : round0(x * feet_per_meter) + ' ft';
}

function percent1(x) {
  return round1(x * 100) + '%';
}


var DEGREES_TO_RADIANS = Math.PI / 180.;
var EARTH_RADIUS = 6367447;

function bearing_and_range(lat0, lon0, lat1, lon1) {
	// rotate so viewer at zero longitude
  lon1 -= lon0;

	// about 11cm
        // prevents NaN values below
  if (Math.abs(lat1 - lat0) + Math.abs(lon1) < .000001)
     return [0, 0];

  lat0 *= DEGREES_TO_RADIANS;
  lat1 *= DEGREES_TO_RADIANS;
  lon1 *= DEGREES_TO_RADIANS;

  var sinlon1 = Math.sin(lon1);
  var coslon1 = Math.cos(lon1);
  var sinlat0 = Math.sin(lat0);
  var coslat0 = Math.cos(lat0);
  var sinlat1 = Math.sin(lat1);
  var coslat1 = Math.cos(lat1);

  var x = coslat1 * coslon1;
  var y = coslat1 * sinlon1;
  var z = sinlat1;

	// rotate about y axis so viewer at north pole
  var xx =  x * sinlat0 - z * coslat0;
  var yy =  y;
  var zz =  x * coslat0 + z * sinlat0;
  var theta = Math.PI - Math.atan2(yy, xx);
  var phi   = Math.acos(zz);

  return [ theta/DEGREES_TO_RADIANS, phi * EARTH_RADIUS];
}


function adjust_for_declination(d) {
  if (current.magnetic) {
    d -= current.declination;
    if (d < 0)
       d += 360;
    else if (d > 360)
       d -= 360;
  }
  return d;
}

function route_length(a) {
  var t = 0;
  for (var i = 0; i < a.length - 1; i++) {
    var b = bearing_and_range(a[i].lat(), a[i].lng(), a[i+1].lat(), a[i+1].lng());
    t += b[1];
  }
  return t;
}


function radio_set(r, v) {
  for (var i = 0; i < r.length; i++) if (r[i].value == v) { r[i].checked = 1; return 1; }
  return 0;
}
function radio_get(r) {
  for (var i = 0; i < r.length; i++) if (r[i].checked) return r[i].value;
  return null;
}

// mode is number of parts - 1, e.g. 0 for DD, 1 for 'DDMM', 2 for 'DDMMSS'
function format_angle(x, mode, pos_char, neg_char, is_html) {
  var a = expand_angle(Math.abs(x), mode);
  var s = '';
  var symbols = [is_html? '&deg;' : '', "'", '"'];
  for (var i = 0; i <= mode; i++)
    s += a[i] + symbols[i] + ' ';
  if (x < 0)
    if (neg_char)
      s += neg_char;
    else
      s = '-' + s;
  else
   s += pos_char;

  return s;
}
function expand_angle(x, mode) {
  if (mode == 0)
    return [round6(x)];
  var d = Math.floor(x);
  x = 60 * (x - d);
  if (mode == 1)
    return [d, round4(x)];
  var m = Math.floor(x);
  x = 60 * (x - m);
  return [d, m, round2(x)];
}
function lat_lon_to_string(lat, lon, is_html) {
  return    format_angle(lat, current.latlon_mode, 'N', 'S', is_html)
          + (is_html? '&nbsp;&nbsp;' : '  ')
          + format_angle(lon, current.latlon_mode, 'E', 'W', is_html);
}


function set_latlon_mode(d) {
  current.latlon_mode = d;
  radio_set(document.f_settings.latlon_mode, d);
  prefs.set('latlon_mode', d);
  settings_changed();
}
function set_units(u) {
  current.use_metric = u;
  radio_set(document.f_settings.units, u);
  prefs.set('use_metric', u);
  settings_changed();
  draw_transect();
}
function set_show_scale(s) {
  //radio_set(document.f_settings.show_scale, s);
  document.f_settings.show_scale.checked = s;
  prefs.set('show_scale', s);
  draw_transect();
}
function set_show_grade(s) {
  current.show_grade = s;
  document.f_settings.show_grade.checked = s;
  prefs.set('show_grade', s);
  settings_changed();
}
function set_curved_earth(c) {
  radio_set(document.f_settings.curved_earth, c);
  prefs.set('curved_earth', c);
  draw_transect();
}

set_latlon_mode(prefs.getInt("latlon_mode"));
set_units(prefs.getInt("use_metric"));
set_show_scale(prefs.getInt("show_scale"));
set_show_grade(prefs.getInt("show_grade"));
set_curved_earth(prefs.getInt("curved_earth"));

var icon_plus              = new GIcon();
icon_plus.image            = "http://www.heywhatsthat.com/images/black-plus.png";
icon_plus.shadow           = null;
icon_plus.iconSize         = new GSize(24, 24);
icon_plus.shadowSize       = new GSize(0, 0);
icon_plus.iconAnchor       = new GPoint(12, 12);
icon_plus.infoWindowAnchor = new GPoint(12, 12);

</script>

]]></Content>
</Module>
