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); }