NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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> <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">×</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|°)/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 + '°</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>×</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 + '°</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);
}