Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
396 lines (351 sloc) 15.6 KB
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, height=device-height" />
<title>WGS84-travel</title>
<style type="text/css">
/* <bootstrap> */
*,
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 62.5%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.428571429;
color: #333333;
background-color: #ffffff;
}
/* </bootstrap> */
/* <layout> */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body { /* first-level flex-container */
display: flex;
flex-direction: column;
}
body > * {
flex: 0 0 auto;
margin: 0 0 10px 0;
}
body > main { /* second-level flex-container */
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
body > main > textarea {
width: 300px;
flex: none;
margin: 0 10px;
}
body > main > section:last-child { /* third-level flex-container */
flex: 1 1 auto;
display: flex;
flex-direction: column;
margin-right: 10px;
}
body > main > section:last-child > .output-map {
flex: 1 1 auto;
}
body > main > section:last-child > textarea {
height: 150px;
flex: none;
margin-top: 10px;
}
/* <layout> */
/* <controls> */
h1 {
background: #000;
color: #999;
padding: 15px 10px;
font-size: 18px;
line-height: 20px;
font-weight: normal;
}
textarea {
display: block;
font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
font-size: 10px;
margin: 0;
}
p {
padding: 0 10px;
}
.output-map {
border: 1px solid gray;
/*border-width: 1px 1px 1px 2px; *//* looks better on Chrome */
width: 100%;
height: 100%;
}
/* </controls> */
</style>
</head>
<body>
<h1>WGS84-travel</h1>
<p>
Takes a starting point latitude/longitude, and a list of north/east offsets in meters,
and produces a standard <a href="http://en.wikipedia.org/wiki/GPS_eXchange_Format">GPX file</a> for the resulting track.
Try <a href="#sample1">simple sample</a> input, or one with <a href="#sample2">more fields</a> (non-standard ones will be added as comments).
The source is on <a href="https://github.com/jareware/jareware.github.com/blob/master/wgs84-travel.html">GitHub</a>.
</p>
<main>
<textarea name="input"></textarea>
<section>
<div class="output-map"></div>
<textarea name="output-text" readonly></textarea>
</section>
</main>
<script src="http://maps.googleapis.com/maps/api/js?sensor=false&extension=.js&key=AIzaSyD7jr5xQ9SJogIDH_fqN1yKgfkAf8R_cMc"></script>
<script>
(function() { "use strict";
var DEBOUNCE = 1000;
var SAMPLE_DATA_SIMPLE = JSON.stringify([{"lat":60.136918333333334,"lon":24.426893333333332},{"travelN":29,"travelE":-12},{"travelN":5,"travelE":34},{"travelN":7,"travelE":36},{"travelN":25,"travelE":35},{"travelN":16,"travelE":35},{"travelN":4,"travelE":45},{"travelN":-7,"travelE":30},{"travelN":-14,"travelE":25},{"travelN":-18,"travelE":7},{"travelN":-50,"travelE":-22}], undefined, 4);
var SAMPLE_DATA_COMPLEX = JSON.stringify([{"lat":60.12375,"lon":24.441499,"time":"2005-05-04T14:36:56.000Z","alt":58,"speed":4.2,"heartRate":96,"temp":23.2},{"travelN":-800,"travelE":140,"time":"2005-05-04T14:36:57.000Z","alt":58,"speed":4.2,"heartRate":97,"temp":23.2},{"travelN":-80,"travelE":1000,"time":"2005-05-04T14:36:58.000Z","alt":58,"speed":4.3,"distance":2,"heartRate":97,"temp":23.2},{"travelN":110,"travelE":500,"time":"2005-05-04T14:37:00.000Z"},{"travelN":300,"travelE":500,"time":"2005-05-04T14:37:01.000Z"},{"travelN":530,"travelE":500}], undefined, 4);
var txtInput = document.querySelector('textarea[name=input]');
var txtOutput = document.querySelector('textarea[name=output-text]');
var mapOutput = document.querySelector('.output-map');
var lnkSample1 = document.querySelector('a[href="#sample1"]');
var lnkSample2 = document.querySelector('a[href="#sample2"]');
var mapAPI = initMap(mapOutput);
var currentPolyline;
var reactionTimeout;
var lastInput;
txtInput.addEventListener('change', notice);
txtInput.addEventListener('paste', notice);
txtInput.addEventListener('keyup', notice);
function notice() {
if (lastInput !== txtInput.value) { // input actually changed
lastInput = txtInput.value;
txtOutput.style.color = '#ccc';
window.clearTimeout(reactionTimeout);
reactionTimeout = window.setTimeout(react, DEBOUNCE);
}
}
function react() {
try {
var input = JSON.parse(txtInput.value);
var output = convert(input);
txtOutput.value = getGPX(output);
updateMap(output, mapAPI);
} catch (e) {
txtOutput.value = 'ERROR: ' + e;
}
txtOutput.style.color = 'inherit';
}
function initMap(mapEl) {
return new google.maps.Map(mapEl, {
zoom: 15,
center: new google.maps.LatLng(60.136918333333334, 24.426893333333332),
mapTypeId: google.maps.MapTypeId.SATELLITE
});
}
function updateMap(input, mapAPI) {
if (currentPolyline) {
currentPolyline.setMap(null); // remove previous line
}
var coords = input.map(function(d) {
return new google.maps.LatLng(d.lat, d.lon);
});
currentPolyline = new google.maps.Polyline({
path: coords,
geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
});
currentPolyline.setMap(mapAPI);
var bounds = new google.maps.LatLngBounds();
coords.forEach(function(c) {
bounds.extend(c);
});
mapAPI.fitBounds(bounds);
}
function convert(input) {
var prev;
return input.map(function(d) {
if (prev) {
var t = travel(prev.lat || 0, prev.lon || 0, d.travelN || 0, d.travelE || 0);
d.lat = t[0];
d.lon = t[1];
}
return prev = d;
});
}
function getGPX(input) {
/**
* Returns a node with the given name. The rest are var-args, so that:
*
* - an object sets attributes as key/value-pairs
* - a string/number/boolean sets the text content of the node
* - an array is treated as a list of child nodes
*
* As a special case, if the node name is "<!", a comment node is created,
* with the following string as its content.
*
* For convenience, falsy values in the list of children are ignored.
*
* @todo https://developer.mozilla.org/en-US/docs/Web/API/document.createTextNode
*
* @example el('p', [
* el('a', 'Click here', {
* href: '#some-location'
* })
* ]);
*
* @returns https://developer.mozilla.org/en-US/docs/Web/API/element
*
* @link https://gist.github.com/jareware/8dc0cc1a948c122edce0
* @author Jarno Rantanen <jarno@jrw.fi>
* @license Do whatever you want with it
*/
function el(name) {
var attributes = {}, text, children = [];
Array.prototype.slice.call(arguments, 1).forEach(function(arg) {
if (arg instanceof Array) {
children = arg;
} else if (typeof arg === 'object') {
attributes = arg;
} else if ([ 'string', 'number', 'boolean' ].indexOf(typeof arg) >= 0) {
text = arg;
}
});
if (name === '<!') {
return document.createComment(text);
}
var node = document.createElement(name);
Object.keys(attributes).forEach(function(key) {
node.setAttribute(key, attributes[key]);
});
if (text) {
node.textContent = text;
}
children.forEach(function(child) {
if (child) {
node.appendChild(child);
}
});
return node;
}
function getNonStandardFields(d) {
var IGNORE_FIELDS = [ 'lat', 'lon', 'time', 'alt', 'travelN', 'travelE' ];
var o = {};
Object.keys(d).forEach(function(key) {
if (IGNORE_FIELDS.indexOf(key) === -1) {
o[key] = d[key];
}
});
return o;
}
var HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n';
var ROOT_ATTRS = {
'xmlns': 'http://www.topografix.com/GPX/1/1',
'creator': 'http://jrw.fi/wgs84-travel.html',
'version': '1.1',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation': 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
};
var gpx = el('gpx', ROOT_ATTRS, [
el('metadata', [
input[0].time ? el('time', input[0].time) : undefined
]),
el('trk', [
el('name', 'WGS84-travel export'),
el('trkseg', input.map(function(d) {
var nsFields = getNonStandardFields(d);
nsFields = Object.keys(nsFields).length ? JSON.stringify(nsFields) : false;
return el('trkpt', {
lat: d.lat,
lon: d.lon
}, [
d.time ? el('time', d.time) : undefined,
d.alt ? el('ele', d.alt) : undefined,
nsFields ? el('<!', nsFields) : undefined
]);
}))
])
]);
return HEADER + gpx.outerHTML;
}
/**
* Returns new coordinates based on the given original coordinates, adjusted by traveling the given amount
* of meters on the surface of a sphere (approximating the WGS-84 geoid).
*
* Based on the Java implementation by aol@IRCnet, with permission.
*
* @param fromLatDeg Original latitude, in degrees
* @param fromLonDeg Original longitude, in degrees
* @param metersNorth Surface-meters to travel north
* @param metersEast Surface-meters to travel east
* @returns Adjusted coordinates as lat/lon-tuple, in degrees
*/
function travel(fromLatDeg, fromLonDeg, metersNorth, metersEast) {
var EARTH_RADIUS = 6371000;
if (metersNorth === 0 && metersEast === 0) {
return [ fromLatDeg, fromLonDeg ];
}
var distanceRad = geoidSurfaceDistanceToRad(metersToVectorLengthMeters(metersNorth, metersEast));
var bearingRad = metersToBearingRad(metersNorth, metersEast);
var travel = travelByRad(degToRad(fromLatDeg), degToRad(fromLonDeg), distanceRad, bearingRad);
return [ radToDeg(travel[0]), radToDeg(travel[1]) ];
function degToRad(degrees) {
return degrees * Math.PI / 180;
}
function radToDeg(radians) {
return radians * 180 / Math.PI;
}
function travelByRad(fromLatRad, fromLonRad, distanceRad, bearingRad) {
var t1, t2, t3, t4, t5;
t1 = Math.sin(fromLatRad);
t2 = Math.cos(distanceRad);
t3 = Math.cos(fromLatRad);
t4 = Math.sin(distanceRad);
t5 = Math.sin(bearingRad);
var adjustedLatRad = Math.asin(t1*t2 + t3*t4*Math.cos(bearingRad));
var adjustedLonRad = Math.atan2(t5*t4*t3, t2-t1*Math.sin(adjustedLatRad)) + fromLonRad;
return [ adjustedLatRad, adjustedLonRad ];
}
function metersToBearingRad(metersNorth, metersEast) {
return Math.atan(metersEast / metersNorth) + (metersNorth < 0 ? Math.PI : 0);
}
function metersToVectorLengthMeters(metersNorth, metersEast) {
return Math.sqrt(Math.pow(metersNorth, 2) + Math.pow(metersEast, 2));
}
function geoidSurfaceDistanceToRad(aDistanceMeters) {
return aDistanceMeters / EARTH_RADIUS;
}
}
lnkSample1.addEventListener('click', function(e) {
e.preventDefault();
txtInput.value = SAMPLE_DATA_SIMPLE;
react();
});
lnkSample2.addEventListener('click', function(e) {
e.preventDefault();
txtInput.value = SAMPLE_DATA_COMPLEX;
react();
});
})();
</script>
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-42176157-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>