/* Description: arribasail.js - common Javascript for arribasail.com web apps License: Copyright (C) 2016-2017 Alan Noble arribasail.js is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. arribasail.js is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details (http://www.gnu.org/licenses/). */ const EARTH_RADIUS = 6373000; // handy Math functions Math.radians = function(deg) { return deg * Math.PI / 180; }; Math.degrees = function(rad) { return rad * 180 / Math.PI; }; Math.square = function(num) { return num * num; }; // utility functions function isNumber(value) { // return true if value is a number return !isNaN(value); } function isAngle(value, range) { // return true if value is an angle within the given range, e.g., [-90,+90] if (isNaN(value)) return false; var angle = parseFloat(value); return (angle >= range[0] && angle <= range[1]); } function item(arr, prop, value) { // return the array item for which the property has a given value for (var ii = 0; ii < arr.length; ii++) { if (arr[ii][prop] == value) { return arr[ii]; } } } function getParam(name) { // returns part of a query string var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); var match = regex.exec(window.location.href); if(match == null) { return ''; } else { return decodeURIComponent(match[1].replace(/\+/g, " ")); } } function getUrlPart(name) { // returns part of current location URL, given blah.com/foo/bar and "foo", returns "bar" var regex = new RegExp("/" + name + "/([^/]*)"); var match = regex.exec(window.location.href); if(match == null) { return ''; } else { return decodeURIComponent(match[1].replace(/\+/g, " ")); } } function hasLabel(labels, label) { // returns true if comma-separated labels contains a given label var regex = new RegExp("(^|,)" + label + "($|,)"); return regex.test(labels); } function quote(str) { // quote a string using single quotes return "'" + str.replace(/'/g,"\\'") + "'"; } // DOM accessors/mutators function getInput(name) { // get an input value return document.getElementById(name).value; } function setInput(name, value) { // set an input value if (value != null && value != '') { document.getElementById(name).value = value; } } function getChecked(name) { return document.getElementById(name).checked; } function setChecked(name, value) { if (value == 'true') { document.getElementById(name).checked = true; } else { document.getElementById(name).checked = false; } } function getSelected(name) { var menu= document.getElementById(name); return menu.options[menu.selectedIndex].value; } function setSelected(name, value) { var menu = document.getElementById(name); for (var ii = 0; ii < menu.options.length; ii++) { if (menu.options[ii].value === value) { menu.selectedIndex = ii; return; } } } // latlng functions // a latlng object is simply { lat:lat_value, lng:lng_value } function parseLatlng(latlng_str) { // returns a lat,lng object given a string representing a latlng in either decdeg or decmin format var lat = null; var lng = null; var regex_decdeg = /([-\.\d]+)[\s°]?\s*,\s*([-\.\d]+)[\s°]?/; // NB: comma is required for decimal degree format var match = regex_decdeg.exec(latlng_str); if (match) { lat = parseFloat(match[1]); lng = parseFloat(match[2]); } else { var regex_decmin = /([\d]+)[\s°]?([\d]*\.?[\d]*)?[’']?\s*([NS])\s*,?\s*([\d]+)[\s°]?([\d]*\.?[\d]*)?[’']?\s*([EW])/; match = regex_decmin.exec(latlng_str.toUpperCase()); if (match) { var sign = (match[3] == 'S') ? -1 : 1; if (!match[2]) match[2] = '0'; lat = sign * (parseInt(match[1]) + parseFloat(match[2])/60); sign = (match[6] == 'W') ? -1 : 1; if (!match[5]) match[5] = '0'; lng = sign * (parseInt(match[4]) + parseFloat(match[5])/60); } } if (isAngle(lat, [-90, 90]) && isAngle(lng, [-180, 180])) { return { lat:lat, lng:lng }; } return null; } function splitLatlng(latlng) { // split latlng object into 4 components: [abs(lat),lat_hemisphere,abs(lng),lng_hemisphere] var ll; if (typeof latlng.lat == 'function' && typeof latlng.lng == 'function') { ll = [latlng.lat(),'N',latlng.lng(),'E']; } else { ll = [latlng.lat,'N',latlng.lng,'E']; } if (ll[0] < 0) { ll[0] = Math.abs(ll[0]); ll[1] = 'S'; } if (ll[2] < 0) { ll[2] = Math.abs(ll[2]); ll[3] = 'W'; } return ll; } function formatLatlng(latlng, fmt, decimals) { // formats latlng object as decimal degrees (decdeg), decimal minutes (decmin) or minutes/seconds (minsec) // convert google.maps.LatLng if necessary if (typeof latlng.lat == 'function' && typeof latlng.lng == 'function') { latlng = { lat:latlng.lat(), lng:latlng.lng() } } if (fmt == 'decdeg') { if (typeof decimals === 'undefined') decimals = 6; var mm = Math.pow(10, decimals); var lat = Math.round(latlng.lat * mm)/mm; var lng = Math.round(latlng.lng * mm)/mm; return lat + ', ' + lng; } else if(fmt == 'decmin') { if (typeof decimals === 'undefined') decimals = 3; var mm = Math.pow(10, decimals); var ll = splitLatlng(latlng); var latmin = Math.round(60 * (ll[0] % 1) * mm)/mm; var lngmin = Math.round(60 * (ll[2] % 1) * mm)/mm; return ~~ll[0] + '°' + latmin + "'" + ll[1] + ' ' + ~~ll[2] + '°' + lngmin + "'" + ll[3]; } else if(fmt == 'minsec') { var ll = splitLatlng(latlng); var latmin = 60 * (ll[0] % 1); var lngmin = 60 * (ll[2] % 1); var latsec = Math.round(60 * (latmin % 1)); if (latsec == 60) { latsec = 0; latmin++; } var lngsec = Math.round(60 * (lngmin % 1)); if (lngsec == 60) { lngsec = 0; lngmin++; } return ~~ll[0] + '°' + ~~latmin + "'" + latsec + '"' + ll[1] + ' ' + ~~ll[2] + '°' + ~~lngmin + "'" + lngsec + '"' + ll[3]; } else { return ''; } } function latlngDistance(latlng1, latlng2) { // calculate great-circle distance between 2 latlngs in meters, using Haversine Formula // convert google.maps.LatLng if necessary if (typeof latlng1.lat == 'function' && typeof latlng1.lng == 'function') { latlng1 = { lat:latlng1.lat(), lng:latlng1.lng() } } if (typeof latlng2.lat == 'function' && typeof latlng2.lng == 'function') { latlng2 = { lat:latlng2.lat(), lng:latlng2.lng() } } var lat1 = Math.radians(latlng1.lat); var lng1 = Math.radians(latlng1.lng); var lat2 = Math.radians(latlng2.lat); var lng2 = Math.radians(latlng2.lng); var dlng = lng2 - lng1; var dlat = lat2 - lat1; var aa = Math.square(Math.sin(dlat/2)) + Math.cos(lat1) * Math.cos(lat2) * Math.square(Math.sin(dlng/2)); cc = 2 * Math.atan2(Math.sqrt(aa), Math.sqrt(1 - aa)); return EARTH_RADIUS * cc; } function latlngBearing(latlng1, latlng2) { // caculate bearing from latlng1 to latlng2 if (typeof latlng1.lat == 'function' && typeof latlng1.lng == 'function') { latlng1 = { lat:latlng1.lat(), lng:latlng1.lng() } } if (typeof latlng2.lat == 'function' && typeof latlng2.lng == 'function') { latlng2 = { lat:latlng2.lat(), lng:latlng2.lng() } } var lat1 = Math.radians(latlng1.lat); var lat2 = Math.radians(latlng2.lat); var dlng = Math.radians(latlng2.lng - latlng1.lng); var bearing = Math.degrees(Math.atan2(Math.sin(dlng)*Math.cos(lat2), Math.cos(lat1)*Math.sin(lat2)-Math.sin(lat1)*Math.cos(lat2)*Math.cos(dlng))); return (bearing + 360) % 360 } function latlngCrossTrackDistance(pos, orig, dest) { // calculate cross-track distance (cross-track error) for position on a track between orig and dest var dist = latlngDistance(orig, pos); // how far along the track we are var dangle = latlngBearing(orig, pos) - latlngBearing(orig, dest); // relative angle off center return Math.asin(Math.sin(dist/EARTH_RADIUS) * Math.sin(Math.radians(dangle))) * EARTH_RADIUS; } function groundResolution(lat, zoom) { // groundResolution is the distance in m on the ground per Google Maps pixel // # pixels for the width of the (square) world map in Web Mercator. // For zoom level 0, this is 256 pixels. var pixels = Math.pow(2, 8 + zoom); // divide earth's circumference (at given latitude) by # pixels return Math.cos(lat * Math.PI / 180) * 2 * Math.PI * EARTH_RADIUS / pixels; }