strozzascotte / Find SpaceX Ships

// ==UserScript==
// @name        Find SpaceX Ships
// @namespace   SpaceX
// @description Ease the finding of SpaceX ships on marinetraffic.com.
// @license     GNU GPLv3
// @icon        https://imgur.com/85vQaF9.png
// @include     http://www.marinetraffic.com/en/ais/embed/*
// @version     1.3.1
// @grant       none
// @updateURL   https://openuserjs.org/meta/strozzascotte/Find_SpaceX_Ships.meta.js
// @run-at document-end
// ==/UserScript==

// NOTE: Use URL like: http://www.marinetraffic.com/en/ais/embed/maptype:3/showmenu:false/shownames:false/

// NOTE: Replace the offshore_ids in the array below
//       as they change almost every time a ship reaches coastal zone
//       go offshore again.
var ships = [
  {name: 'Elsbeth III', id: '434560', offshore_id: '76547322145', color: 'yellow'},
  {name: 'Go Searcher', id: '426008', offshore_id: '76547313593', color: 'red'},
  {name: 'Go Quest', id: '450521', offshore_id: '76547338106', color: 'green'}
];

// Define ASDS marker coordinates and icon
var asds = {
  title: 'OCISLY expected position at landing (with 10NM range)',
  position: {lat: 28.114722, lng: -73.642222},
  image: {
    url: 'https://imgur.com/85vQaF9.png',
    scaledSize: {width: 35, height: 18},
    anchor: {x: 17.5, y: 9}
  },
  range: {radius: 10, color: 'black'} 
};

// Styles for the table
var css = "\
  #shipsPanel {z-index: 10000; padding:10px; border-radius: 5px; position: absolute; top: 5px; left: 5px; background-color: rgba(255,255,255,0.5); text-align: left;}\
  .shipBullet, .shipName, .shipLat, .shipLong, .shipSpeed, .shipHeading, .shipTime, .shipLog ,.logButton, .delLogButton {display: inline-block; vertical-align: top}\
  .shipBullet { width: 10px; }\
  .shipName { width: 100px; font-weight: bold;}\
  .shipLat, .shipLong { width: 80px; }\
  .shipSpeed, .shipHeading { width: 60px; }\
  .shipTime { width: 220px; }\
  .shiplog { width: 20px; }\
  .bullet {width: 6px; height: 6px; margin: 2px 0px; border-radius: 6px; background-color: white; display: inline-block;}\
  .logButton {color: gray; width: 16px; height: 16px; margin: 1px; padding: 2px 0; border-radius: 3px; background-color: rgba(255,255,255,0.75); text-align: center; font-size: smaller !important; }\
  .logButton:hover{color: black; cursor: pointer; text-decoration: none; background-color: white;}\
  .delLogButton {color: gray; width: 16px; height: 16px; margin: 1px; border-radius: 3px; border: 1px solid gray; text-align: center;}\
  .delLogButton span {display: block; position: relative; top: -5px; font-size: 16px;}\
  .delLogButton:hover{color: black; cursor: pointer; text-decoration: none !important;}\
  #modalBox { display: none; position: fixed; z-index: 10001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgb(0,0,0); background-color: rgba(0,0,0,0.4); }\
  .modal-content {background-color: #fefefe; margin: 15% auto; padding: 20px; border: 1px solid #888; width: 80%; }\
  #closeButton { color: #aaa; float: right; font-size: 20px; font-weight: bold; }\
  #closeButton:hover, #closeButton:focus { color: black; text-decoration: none; cursor: pointer; }\
  #logsBox table { margin: auto;}\
  #logsBox td, #logsBox th { padding: 2px 10px; text-align: center; }\
  .adsbygoogle {display: none !important;}\
  #configBar {text-align: center; padding-top: 5px;}\
  .toggleButton {color: gray; background-color: rgba(255,255,255,0.75); padding: 8px 10px 3px 10px; margin: 5px; border-radius: 5px;}\
  .toggleButton:hover{color: black;cursor: pointer; background-color: white; text-decoration: none;}\
  .toggleButton::after {content: ' \u2610'; font-size: 18px;}\
  .toggleOn::after {content: ' \u2611'; font-size: 18px;}\
  #countDownSecs {width: 35px; display: inline-block; text-align: right;}\
";
addGlobalStyle(css);

// Wait for AJAX map to be loaded and creates main panel
// and call for findShip function after that 
waitForKeyElements('div#map_canvas', function () {
  //Create panel
  var div = document.createElement('div');
  div.setAttribute('id', 'shipsPanel');
  document.body.insertBefore(div, document.body.firstChild);

  var div2 = document.createElement('div');
  div2.style = "font-weight: bold";
  div2.innerHTML  = '<span class="shipBullet"></span>';
  div2.innerHTML += '<span class="shipName">Ship\'s Name</span>';
  div2.innerHTML += '<span class="shipLat">Latitude</span>';
  div2.innerHTML += '<span class="shipLong">Longitude</span>';
  div2.innerHTML += '<span class="shipSpeed">Speed</span>';
  div2.innerHTML += '<span class="shipHeading">Heading</span>';
  div2.innerHTML += '<span class="shipTime">Last info</span>';
  div2.innerHTML += '<span class="shipLog">Log</span></div>';
  div.appendChild(div2);

  var div3 = document.createElement('div');
  div3.id = 'configBar';
  div3.innerHTML  = '<a class="toggleButton toggleOn" id="toggleTracks" title="Toggle tracks\' visibility">Tracks</a>';
  div3.innerHTML += '<a class="toggleButton toggleOn" id="toggleWaypoints" title="Toggle waypoints\' visibility">Waypoints</a>';
  div3.innerHTML += '<a class="toggleButton toggleOn" id="toggleProjections" title="Toggle projected positions\' visibility">Projected positions</a>';
  div3.innerHTML += '<a class="toggleButton toggleOn" id="toggleASDS" title="Toggle expected landing position marker visibility">ASDS</a>';
  div3.innerHTML += '<a title="Time to refresh" style="text-decoration:none; float:right;"><span id="countDownSecs"><span class="infin">∞</span></span>&nbsp;<i class="fa fa-refresh"></i></a>';
  div.appendChild(div3);
  
  //Create hidden modal box for logs
  var div2 = document.createElement('div');
  div2.setAttribute('id', 'modalBox');
  div2.innerHTML = '<div class="modal-content"><span id="closeButton">&#215;</span><div id="logsBox">Some text in the Modal..</div></div>';
  document.body.insertBefore(div2, document.body.firstChild);

  //Load logs from local storage if any.
  ships.forEach(loadLog);

  //Try to find ships everytime an AJAX call is completed
  $( document ).ajaxComplete(function() {
    ships.forEach(findShip);
    window.$.mtdata.refreshInterval = 120 + Math.max((14 - Math.max(window.map.zoom, 8)), 0) * 30;  // Set refresh interval
    window.$.mtdata.restartCountDown();
  });
  
  //Toggle options code
  $(document).ready(function(){
    $('a.toggleButton').click(function(){
        $(this).toggleClass("toggleOn");
        var state = $(this).hasClass("toggleOn");
        var target = $(this).attr('id').slice(6);
        if (target == 'Tracks') setTracksVisible(state);
        if (target == 'Waypoints') setWaypointsVisible(state);
        if (target == 'Projections') setProjectionsVisible(state);
        if (target == 'ASDS') setMarkerVisible(asds,state);
        if (target == 'Ads') setAdsVisible(asds,state);
    });
  },
  true);
   
  //Wait for the map to be completely loaded
  waitForKeyElements('#map-copyright', function () {
    setMarker(asds); //Place ASDS marker
  }, true); 
  

});

//Load log from local storage if any.
function loadLog(ship) {
  if (typeof(ship.log) == 'undefined') ship.log = [];
  if (typeof(Storage) !== "undefined") {
    if (typeof(localStorage.spaceXshipsLogs) !== "undefined") {
      var storedLogs = JSON.parse(localStorage.spaceXshipsLogs);
      var i = storedLogs.ids.indexOf(ship.id);
      if (i >= 0) {
        ship.log = storedLogs.logs[i];
        ship.log.forEach(function(log) {
          log.time = new Date(log.time);
        })
      } 
    }
  } 
};

//Save log to local storage if available.
function saveLog(ship) {
  if(typeof(Storage) !== "undefined") {
    if (typeof(localStorage.spaceXshipsLogs) !== "undefined") {
      var storedLogs = JSON.parse(localStorage.spaceXshipsLogs);
    } else {
      var storedLogs = {ids: [], logs: []};
    }
    var i = storedLogs.ids.indexOf(ship.id);
    if (i < 0) {
      i = storedLogs.ids.length;
      storedLogs.ids[i] = ship.id;
    } 
    storedLogs.logs[i] = ship.log;
    localStorage.spaceXshipsLogs = JSON.stringify(storedLogs);
  }
};


function findShip(ship)
{
    harvestShipsData(ship, ship.id);
    harvestShipsData(ship, ship.offshore_id);
};
                     
function harvestShipsData(ship, id) {
  
  if (document.getElementById(('ship' + id))) { // There is a marker for the ship 

    // Harvest data from DIV tag
    var divShip = document.getElementById(('ship' + id));

    var long = Number(divShip.getAttribute('data-x'));
    var lat = Number(divShip.getAttribute('data-y'));
    var info = divShip.getAttribute('data-title');
     
  } else if (typeof window.shipbounds[id] !== 'undefined') { // Ship is outlined in cyan...
    
    // Harvest data form shipbounds array.
    var data = window.shipbounds[id];

    var long = Math.round(data.latlng.lng() * 100000) / 100000;
    var lat = Math.round(data.latlng.lat() * 100000) / 100000;
    var info = data.tooltip;
    
  } else return;

  // Speed
  var patt = /([0-9\.]+) knots/gi;
  var res = patt.exec(info);
  var speed = Number(res[1]);

  // Heading
  patt = /([0-9\.]+)(\u00B0|&deg;)/gi;
  res = patt.exec(info);
  var head = Number(res[1]);
  
  // Date and Time
  patt = /Position received: (([0-9]+) hr)?(?:, )?(([0-9]+) min)? ago/gi;
  res = patt.exec(info);
  var hours = res[2];
  if (typeof (hours) == 'undefined') {
    hours = 0
  };
  var mins = res[4];
  var d = new Date();
  d.setHours(d.getHours() - hours);
  d.setMinutes(d.getMinutes() - mins);
 
  // Add or update actual location waypoint to chart
  if (typeof ship.locationPoint == 'undefined') {
    ship.locationPoint = new google.maps.Marker({
      strokeOpacity: 1.0,
      position: {lat: lat, lng: long},
      icon: {
        path: google.maps.SymbolPath.CIRCLE,
        scale: 12, 
        strokeColor: ship.color,
        strokeOpacity: 1,
        strokeWeight: 2,
        fillColor: ship.color,
        fillOpacity: 0
      },
      map: map,
      clickable: false
    });
  } else {
    ship.locationPoint.setPosition({lat: lat, lng: long});
  }

  // Add record to ship's log only if it's 2+ minutes newer. This is needed because the whole "x hours, y minutes ago" thing sucks.
  if (ship.log.length == 0) {
    ship.log.push({lat: lat, long: long, speed: speed, head: head, time: d}); // Add data to log
  } else {
    if ((d.getTime() > (ship.log[ship.log.length - 1].time.getTime() + 30000)) && (lat != ship.log[ship.log.length - 1].lat) && (long != ship.log[ship.log.length - 1].long)) {
      ship.log.push({lat: lat, long: long, speed: speed, head: head, time: d}); // Add data to log
    } else if (d.getTime() > (ship.log[ship.log.length - 1].time.getTime() + 120000)) {
      ship.log.push({lat: lat, long: long, speed: speed, head: head, time: d}); // Add data to log
    }
  } 

  saveLog(ship); // Save log to local storage

      // Update table row
  updateTable(ship); 

  if (ship.log.length > 1) {
    drawWaypoints(ship);  // Add last waypoint to chart
    drawTrack(ship); // Update track line
  }
 
  if (ship.log[ship.log.length - 1].speed >= 0.5) { 
    drawProjection(ship);  // Draw projection line and marker only if speed is significant
  } else {
    deleteProjection(ship);
  }

  // Set timer for client update of projected location
  if (ship.timer) clearInterval(ship.timer);
  ship.timer = setInterval(
    function() {
      updateTable(ship);
      if (ship.log[ship.log.length - 1].speed >= 0.5) { 
        drawProjection(ship);  // Draw projection line and marker only if speed is significant
      }
    }, 
    10000
  );

};

function updateTable(ship) {
  
  // Create label element in the table if it doesn't exist
  var panel = document.getElementById('shipsPanel');
  if (document.getElementById('label_' + ship.id)) {
    var label = document.getElementById('label_' + ship.id);
  } else {
    var label = document.createElement('div');
    label.setAttribute('id', 'label_' + ship.id);
    panel.insertBefore(label, panel.lastChild);
  }

  var data = ship.log[ship.log.length-1];
  var dTime = Math.round((new Date().getTime() - data.time.getTime()) / 60000); // in minutes
  var hours = Math.floor((dTime / 60));
  var minutes = dTime - hours * 60;

  // Update label
  label.innerHTML = '<span class="shipBullet"><span class="bullet" style="background-color: ' + ship.color + ';"><span></span>';
  label.innerHTML += '<span class="shipName"><a onclick="map.setCenter({lat:' + data.lat + ',lng:' + data.long + '});" title="Center map on ' + ship.name +'">' + ship.name + '</a></span>';
  label.innerHTML += '<span class="shipLat">' + data.lat + '</span>';
  label.innerHTML += '<span class="shipLong">' + data.long + '</span>';
  label.innerHTML += '<span class="shipSpeed">' + data.speed + ' kn</span>';
  label.innerHTML += '<span class="shipHeading">' + data.head + '&deg;</span>';
  label.innerHTML += '<span class="shipTime">' + formatUTC(data.time) + ' (' + ("00" + hours).slice(-2) + ':' + ("00" + minutes).slice(-2) + ' ago)</span>';
  label.innerHTML += '<span class="shipLog"><a class="logButton" id="logButton_' + ship.id + '" onclick="displayLog(' + ship.id +');" title="Display ' + ship.name + '\'s log">' + ship.log.length + '</a><a class="delLogButton" onclick="deleteLog(' + ship.id +');" title="Delete ' + ship.name + '\'s log"><span>&#215;</span></a></span>';

};

  
function drawWaypoints(ship) {
  if (typeof ship.routePoints == 'undefined') ship.routePoints = [];
  for (i = 0; i < (ship.log.length - 1); i++) { 
    if (typeof ship.routePoints[i] == 'undefined') {
      ship.routePoints[i] =  new google.maps.Marker({
        strokeOpacity: 1.0,
        position: {lat: ship.log[i].lat, lng: ship.log[i].long},
        icon: {
          path: google.maps.SymbolPath.CIRCLE,
          scale: 2, 
          strokeColor: ship.color,
          strokeOpacity: 1,
          strokeWeight: 1,
          fillColor: ship.color,
          fillOpacity: 1
        },
        map: map,
        title: ship.name + ' (' + formatUTC(ship.log[i].time) + ')',
        visible: getWaypointsVisible()
      });
    } 
  }
};

function drawTrack(ship) {
  var route = [];
  for (i = 0; i < ship.log.length; i++) { 
    route.push({lat: ship.log[i].lat, lng: ship.log[i].long});
  }      
  if (typeof ship.routeLine == 'undefined') {
    ship.routeLine = new google.maps.Polyline({
      path: route,
      geodesic: true,
      strokeColor: ship.color,
      strokeWeight: 2,
      map: map,
      visible: getTracksVisible()
    });
  } else {
    ship.routeLine.setPath(route);
  }
};
  
      
function drawProjection(ship) {
      
  // Calculate projected position based on speed and heading
  var data = ship.log[ship.log.length-1];
  var dt = Math.round((new Date().getTime() - data.time.getTime()) / 1000); // in seconds
  var radCourse = (90 - data.head) / 180 * Math.PI; // in radians
  var distance = data.speed * dt / 3600; // in nautical miles
  var projectedLat = (Math.sin(radCourse) * distance) / 60 + data.lat; // in degrees
  var projectedLong = (Math.cos(radCourse) * distance) / 60 + data.long; //in degrees

  // Update porjection line
  if (typeof ship.projectionLine == 'undefined') {
    ship.projectionLine = new google.maps.Polyline({
      path : Array({lat: data.lat, lng: data.long}, {lat: projectedLat, lng: projectedLong}),
      geodesic: true,
      strokeColor: ship.color,
      strokeOpacity: 0.25,
      strokeWeight: 1,
      map: map,
      visible: getProjectionsVisible()
    });
  } else {
    ship.projectionLine.setPath(Array({lat: data.lat, lng: data.long}, {lat: projectedLat, lng: projectedLong}));
  }
    
  // Update projection marker
  if (typeof ship.projectedMarker == 'undefined') {
    ship.projectedMarker = new google.maps.Marker({
      position: {lat: projectedLat, lng: projectedLong},
      icon: {
        path: google.maps.SymbolPath.CIRCLE,
        scale: 6, 
        strokeColor: ship.color,
        strokeOpacity: 0.5,
        strokeWeight: 1,
        fillColor: ship.color,
        fillOpacity: 0.25,
      },
      map: map,
      title: ship.name + ' (Projected current position)',
      visible: getProjectionsVisible()         
    });
  } else {
    ship.projectedMarker.setPosition({lat: projectedLat, lng: projectedLong});
  }
};

function deleteProjection(ship) {
  if (typeof ship.projectedMarker !== 'undefined') {
    ship.projectedMarker.setMap(null);
    delete ship.projectedMarker;
  }
  if (typeof ship.projectionLine !== 'undefined') {
    ship.projectionLine.setMap(null);
    delete ship.projectionLine;
  }
};
    

// Rounds seconds to minute and format date object to custom UTC time
function formatUTC(d)
{
  if (typeof (d) != 'object') {
    return typeof (d);
  };
  t = d.getTime();
  m = t / 60000;
  m = Math.round(m);
  t = m * 60000;
  d.setTime(t);
  
  var s;
  s = d.getUTCFullYear() + '-' + (d.getUTCMonth() + 1) + '-' + d.getUTCDate();
  s += ' ' + ("00" + d.getUTCHours()).slice(-2) + ':' + ("00" + d.getUTCMinutes()).slice(-2) + ' UTC';
  return s;
};

window.displayLog = function(id)
{
  var ship = getShipById(id);
  var log = Object;
  var div = document.getElementById('logsBox');
  div.innerHTML = '<h3>' + ship.name + '\'s Log</h3>';
  var tbl = document.createElement('table');
  var tbdy = document.createElement('tbody');  
  var tr = document.createElement('tr');
    tr.innerHTML = '<th>Latitude</th><th>Longitude</th><th>Speed</th><th>Heading</th><th>Time</th>';
    tbdy.appendChild(tr);
  for (i = 0; i < ship.log.length; i++) {
    log = ship.log[i];
    var tr = document.createElement('tr');
      tr.innerHTML += '<td>' + log.lat + '</td><td>' + log.long + '</td><td>' + log.speed + '</td><td>' + log.head + '&deg;</td><td>' + formatUTC(log.time) + '</td>';
      tbdy.appendChild(tr);
  }
  tbl.appendChild(tbdy);
  div.appendChild(tbl);

  
  var modalBox = document.getElementById('modalBox');
  modalBox.style.display = "block";
  var closeButton = document.getElementById('closeButton');
  closeButton.onclick = function() {
    modalBox.style.display = "none";
  }
};

window.deleteLog = function(id) {
  var ship = getShipById(id);
  if (ship.log.length > 1) {
    var r = confirm('You\'re about to permanently delete ' + ship.name + '\'s log. Please be advised that this operation cannot be undone.');
    if (r == true) {
      ship.log.splice(0, (ship.log.length - 1));
      saveLog(ship);
      ship.routeLine.setMap(null);
      delete ship.routeLine;
      ship.routePoints.forEach(function(marker) {
        marker.setMap(null);
      })
      delete ship.routePoints;
      if (document.getElementById('logButton_' + id)) {
        var logButton = document.getElementById('logButton_' + id);
        logButton.innerHTML = ship.log.length;
      }
    }
  } else {
    alert(ship.name + '\'s log has only one entry. Nothing to do.');
  }
};

function getShipById(id) {
  var ship = false;
  ships.forEach(function(s) {
    if (s.id == id) ship = s;
  });
  return ship;
}

getTracksVisible = function() {
  return $('#toggleTracks').hasClass("toggleOn");
};
getWaypointsVisible = function() {
  return $('#toggleWaypoints').hasClass("toggleOn");
};
getProjectionsVisible = function() {
  return $('#toggleProjections').hasClass("toggleOn");
};

setTracksVisible = function(state) {
  ships.forEach(function(ship){
    if (typeof ship.routeLine !== 'undefined') ship.routeLine.setVisible(state);
  }) 
};

setWaypointsVisible = function(state) {
  ships.forEach(function(ship){
    if (typeof ship.routePoints !== 'undefined') {
      ship.routePoints.forEach(function(marker) {
        marker.setVisible(state);
      })    
    }
  }) 
};

setProjectionsVisible = function(state) {
  ships.forEach(function(ship){
    if (typeof ship.projectedMarker !== 'undefined') ship.projectedMarker.setVisible(state);
    if (typeof ship.projectionLine !== 'undefined') ship.projectionLine.setVisible(state);
  }) 
};

setMarker = function(m) {
  if (typeof m.marker === 'undefined') {
    m.marker = new google.maps.Marker({
      position: m.position,
      icon: m.image,
      map: map,
      title: m.title,
      visible: true         
    });
  }
  if (typeof m.circle === 'undefined') {
    m.circle = new google.maps.Circle({
      strokeColor: m.range.color,
      strokeOpacity: 0.5,
      strokeWeight: 1,
      fillColor: m.range.color,
      fillOpacity: 0.1,
      map: map,
      center: m.position,
      clickable: false,
      radius: (1852 * m.range.radius) // 10 nautical milesº
    });
  }
};

setMarkerVisible = function(m,state) {
  if (typeof m.marker !== 'undefined') m.marker.setVisible(state);
  if (typeof m.circle !== 'undefined') m.circle.setVisible(state);
};


/*--- waitForKeyElements():  A utility function, for Greasemonkey scripts,
    that detects and handles AJAXed content.

    IMPORTANT: This function requires your script to have loaded jQuery.
*/
function waitForKeyElements(selectorTxt, /* Required: The jQuery selector string that
                        specifies the desired element(s).
                    */
actionFunction, /* Required: The code to run when elements are
                        found. It is passed a jNode to the matched
                        element.
                    */
bWaitOnce, /* Optional: If false, will continue to scan for
                        new elements even after the first match is
                        found.
                    */
iframeSelector /* Optional: If set, identifies the iframe to
                        search.
                    */
) {
  var targetNodes,
  btargetsFound;
  if (typeof iframeSelector == 'undefined')
  targetNodes = $(selectorTxt);
   else
  targetNodes = $(iframeSelector).contents().find(selectorTxt);
  if (targetNodes && targetNodes.length > 0) {
    btargetsFound = true;
    /*--- Found target node(s).  Go through each and act if they
            are new.
        */
    targetNodes.each(function () {
      var jThis = $(this);
      var alreadyFound = jThis.data('alreadyFound') || false;
      if (!alreadyFound) {
        //--- Call the payload function.
        var cancelFound = actionFunction(jThis);
        if (cancelFound)
        btargetsFound = false;
         else
        jThis.data('alreadyFound', true);
      }
    });
  } 
  else {
    btargetsFound = false;
  } //--- Get the timer-control variable for this selector.

  var controlObj = waitForKeyElements.controlObj || {
  };
  var controlKey = selectorTxt.replace(/[^\w]/g, '_');
  var timeControl = controlObj[controlKey];
  //--- Now set or clear the timer as appropriate.
  if (btargetsFound && bWaitOnce && timeControl) {
    //--- The only condition where we need to clear the timer.
    clearInterval(timeControl);
    delete controlObj[controlKey]
  } 
  else {
    //--- Set a timer, if needed.
    if (!timeControl) {
      timeControl = setInterval(function () {
        waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector
        );
      }, 300
      );
      controlObj[controlKey] = timeControl;
    }
  }
  waitForKeyElements.controlObj = controlObj;
};

function addGlobalStyle(css) {
    var head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
}