diff --git a/build.sh b/build.sh index 99c8903..fd6e6d2 100755 --- a/build.sh +++ b/build.sh @@ -25,6 +25,9 @@ java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations - java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations --nomunge tracker.js >> mobile.js java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations --nomunge app.js >> mobile.js java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations --nomunge colour-map.js >> mobile.js +java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations --nomunge xdata.js >> mobile.js +java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations --nomunge station.js >> mobile.js +java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations --nomunge format.js >> mobile.js #compile plot lib and config java -jar "../tools/yuicompressor-2.4.8.jar" --type=js --disable-optimizations --nomunge _jquery.flot.js >> init_plot.js diff --git a/js/app.js b/js/app.js index 573f312..62a976d 100644 --- a/js/app.js +++ b/js/app.js @@ -860,7 +860,7 @@ $(window).ready(function() { // open map $('.nav .home').click(); // pan map to our current location - map.flyTo(new L.LatLng(currentPosition.lat, currentPosition.lon)); + map.panTo(new L.LatLng(currentPosition.lat, currentPosition.lon)); } else { alert("No position available"); } diff --git a/js/format.js b/js/format.js new file mode 100644 index 0000000..a1ee090 --- /dev/null +++ b/js/format.js @@ -0,0 +1,325 @@ +/* SondeHub Tracker Format Incoming Data + * + * Author: Luke Prior + */ + +function formatData(data, live) { + var response = {}; + response.positions = {}; + var dataTemp = []; + if (live) { // Websockets + if (!data.length) { // handle single sonde + data = {"entry": data}; + } + for (let entry in data) { + var dataTempEntry = {}; + var station = data[entry].uploader_callsign; + dataTempEntry.callsign = {}; + //check if other stations also received this packet + if (vehicles.hasOwnProperty(data[entry].serial)) { + if (data[entry].datetime == vehicles[data[entry].serial].curr_position.gps_time) { + for (let key in vehicles[data[entry].serial].curr_position.callsign) { + if (vehicles[data[entry].serial].curr_position.callsign.hasOwnProperty(key)) { + if (key != station) { + dataTempEntry.callsign[key] = {}; + if (vehicles[data[entry].serial].curr_position.callsign[key].hasOwnProperty("snr")) { + dataTempEntry.callsign[key].snr = vehicles[data[entry].serial].curr_position.callsign[key].snr; + } + if (vehicles[data[entry].serial].curr_position.callsign[key].hasOwnProperty("rssi")) { + dataTempEntry.callsign[key].rssi = vehicles[data[entry].serial].curr_position.callsign[key].rssi; + } + if (vehicles[data[entry].serial].curr_position.callsign[key].hasOwnProperty("frequency")) { + dataTempEntry.callsign[key].frequency = vehicles[data[entry].serial].curr_position.callsign[key].frequency; + } + } + } + } + } + } + dataTempEntry.callsign[station] = {}; + if (data[entry].snr) { + dataTempEntry.callsign[station].snr = data[entry].snr; + } + if (data[entry].rssi) { + dataTempEntry.callsign[station].rssi = data[entry].rssi; + } + if (data[entry].frequency) { + dataTempEntry.callsign[station].frequency = data[entry].frequency; + } + dataTempEntry.gps_alt = data[entry].alt; + dataTempEntry.gps_lat = data[entry].lat; + dataTempEntry.gps_lon = data[entry].lon; + if (data[entry].heading) { + dataTempEntry.gps_heading = data[entry].heading; + } + dataTempEntry.gps_time = data[entry].datetime; + dataTempEntry.server_time = data[entry].datetime; + dataTempEntry.vehicle = data[entry].serial; + dataTempEntry.position_id = data[entry].serial + "-" + data[entry].datetime; + dataTempEntry.data = {}; + if (data[entry].batt) { + dataTempEntry.data.batt = data[entry].batt; + } + if (data[entry].burst_timer) { + dataTempEntry.data.burst_timer = data[entry].burst_timer; + } + if (data[entry].frequency) { + dataTempEntry.data.frequency = data[entry].frequency; + } + if (data[entry].tx_frequency) { + dataTempEntry.data.frequency_tx = data[entry].tx_frequency; + } + if (data[entry].hasOwnProperty("humidity")) { + dataTempEntry.data.humidity = data[entry].humidity; + } + if (data[entry].manufacturer) { + dataTempEntry.data.manufacturer = data[entry].manufacturer; + } + if (data[entry].hasOwnProperty("pressure")) { + dataTempEntry.data.pressure = data[entry].pressure; + } + if (data[entry].sats) { + dataTempEntry.data.sats = data[entry].sats; + } + if (data[entry].hasOwnProperty("temp")) { + dataTempEntry.data.temperature_external = data[entry].temp; + } + if (data[entry].type) { + dataTempEntry.data.type = data[entry].type; + dataTempEntry.type = data[entry].type; + } + if (data[entry].subtype) { + dataTempEntry.data.type = data[entry].subtype; + dataTempEntry.type = data[entry].subtype; + } + if (data[entry].xdata) { + dataTempEntry.data.xdata = data[entry].xdata; + + if (data[entry].hasOwnProperty("pressure")) { + xdata_pressure = data[entry].pressure; + } else { + xdata_pressure = 1100.0; + } + + var tempXDATA = parseXDATA(data[entry].xdata, xdata_pressure); + for (let field in tempXDATA) { + if (tempXDATA.hasOwnProperty(field)) { + if (field == "xdata_instrument") { + dataTempEntry.data.xdata_instrument = tempXDATA.xdata_instrument.join(', '); + } else { + dataTempEntry.data[field] = tempXDATA[field]; + } + } + } + } + if (data[entry].serial.toLowerCase() != "xxxxxxxx") { + dataTemp.push(dataTempEntry); + } + } + } else if (data.length == null) { // Elasticsearch + for (let key in data) { + if (data.hasOwnProperty(key)) { + if (typeof data[key] === 'object') { + for (let i in data[key]) { + var dataTempEntry = {}; + var station = data[key][i].uploader_callsign; + dataTempEntry.callsign = {}; + dataTempEntry.callsign[station] = {}; + if (data[key][i].snr) { + dataTempEntry.callsign[station].snr = data[key][i].snr; + } + if (data[key][i].rssi) { + dataTempEntry.callsign[station].rssi = data[key][i].rssi; + } + if (data[key][i].frequency) { + dataTempEntry.callsign[station].frequency = data[key][i].frequency; + } + dataTempEntry.gps_alt = data[key][i].alt; + dataTempEntry.gps_lat = data[key][i].lat; + dataTempEntry.gps_lon = data[key][i].lon; + if (data[key][i].heading) { + dataTempEntry.gps_heading = data[key][i].heading; + } + dataTempEntry.gps_time = data[key][i].datetime; + dataTempEntry.server_time = data[key][i].datetime; + dataTempEntry.vehicle = data[key][i].serial; + dataTempEntry.position_id = data[key][i].serial + "-" + data[key][i].datetime; + dataTempEntry.data = {}; + if (data[key][i].batt) { + dataTempEntry.data.batt = data[key][i].batt; + } + if (data[key][i].burst_timer) { + dataTempEntry.data.burst_timer = data[key][i].burst_timer; + } + if (data[key][i].frequency) { + dataTempEntry.data.frequency = data[key][i].frequency; + } + if (data[key][i].tx_frequency) { + dataTempEntry.data.frequency_tx = data[key][i].tx_frequency; + } + if (data[key][i].hasOwnProperty("humidity")) { + dataTempEntry.data.humidity = data[key][i].humidity; + } + if (data[key][i].manufacturer) { + dataTempEntry.data.manufacturer = data[key][i].manufacturer; + } + if (data[key][i].hasOwnProperty("pressure")) { + dataTempEntry.data.pressure = data[key][i].pressure; + } + if (data[key][i].sats) { + dataTempEntry.data.sats = data[key][i].sats; + } + if (data[key][i].hasOwnProperty("temp")) { + dataTempEntry.data.temperature_external = data[key][i].temp; + } + if (data[key][i].type) { + dataTempEntry.data.type = data[key][i].type; + dataTempEntry.type = data[key][i].type; + } + if (data[key][i].subtype) { + dataTempEntry.data.type = data[key][i].subtype; + dataTempEntry.type = data[key][i].subtype; + } + if (data[key][i].xdata) { + dataTempEntry.data.xdata = data[key][i].xdata; + if (data[key][i].hasOwnProperty("pressure")) { + xdata_pressure = data[key][i].pressure; + } else { + xdata_pressure = 1100.0; + } + var tempXDATA = parseXDATA(data[key][i].xdata, xdata_pressure); + for (let field in tempXDATA) { + if (tempXDATA.hasOwnProperty(field)) { + if (field == "xdata_instrument") { + dataTempEntry.data.xdata_instrument = tempXDATA.xdata_instrument.join(', '); + } else { + dataTempEntry.data[field] = tempXDATA[field]; + } + } + } + } + if (data[key][i].serial.toLowerCase() != "xxxxxxxx") { + dataTemp.push(dataTempEntry); + } + } + } + } + } + } else { // AWS + for (var i = data.length - 1; i >= 0; i--) { + if (data[i].hasOwnProperty('subtype') && data[i].subtype == "SondehubV1") { // SondeHub V1 + var dataTempEntry = {}; + var station = data[i].uploader_callsign; + dataTempEntry.callsign = {}; + dataTempEntry.callsign[station] = {}; + dataTempEntry.gps_alt = parseFloat(data[i].alt); + dataTempEntry.gps_lat = parseFloat(data[i].lat); + dataTempEntry.gps_lon = parseFloat(data[i].lon); + dataTempEntry.gps_time = data[i].time_received; + dataTempEntry.server_time = data[i].time_received; + dataTempEntry.vehicle = data[i].serial; + dataTempEntry.position_id = data[i].serial + "-" + data[i].time_received; + dataTempEntry.data = {}; + if (data[i].humidity) { + dataTempEntry.data.humidity = parseFloat(data[i].humidity); + } + if (data[i].temp) { + dataTempEntry.data.temperature_external = parseFloat(data[i].temp); + } + dataTemp.push(dataTempEntry); + } else { // SondeHub V2 + var dataTempEntry = {}; + var station = data[i].uploader_callsign; + dataTempEntry.callsign = {}; + dataTempEntry.callsign[station] = {}; + if (data[i].snr) { + dataTempEntry.callsign[station].snr = data[i].snr; + } + if (data[i].rssi) { + dataTempEntry.callsign[station].rssi = data[i].rssi; + } + if (data[i].frequency) { + dataTempEntry.callsign[station].frequency = data[i].frequency; + } + dataTempEntry.gps_alt = data[i].alt; + dataTempEntry.gps_lat = data[i].lat; + dataTempEntry.gps_lon = data[i].lon; + if (data[i].heading) { + dataTempEntry.gps_heading = data[i].heading; + } + dataTempEntry.gps_time = data[i].datetime; + dataTempEntry.server_time = data[i].datetime; + dataTempEntry.vehicle = data[i].serial; + dataTempEntry.position_id = data[i].serial + "-" + data[i].datetime; + dataTempEntry.data = {}; + if (data[i].batt) { + dataTempEntry.data.batt = data[i].batt; + } + if (data[i].burst_timer) { + dataTempEntry.data.burst_timer = data[i].burst_timer; + } + if (data[i].frequency) { + dataTempEntry.data.frequency = data[i].frequency; + } + if (data[i].tx_frequency) { + dataTempEntry.data.frequency_tx = data[i].tx_frequency; + } + if (data[i].hasOwnProperty("humidity")) { + dataTempEntry.data.humidity = data[i].humidity; + } + if (data[i].manufacturer) { + dataTempEntry.data.manufacturer = data[i].manufacturer; + } + if (data[i].hasOwnProperty("pressure")) { + dataTempEntry.data.pressure = data[i].pressure; + } + if (data[i].sats) { + dataTempEntry.data.sats = data[i].sats; + } + if (data[i].hasOwnProperty("temp")) { + dataTempEntry.data.temperature_external = data[i].temp; + } + if (data[i].type && data[i].type == "payload_telemetry") { // SondeHub V1.5 data? + var comment = data[i].comment.split(" "); + if (v1types.hasOwnProperty(comment[0])) { + dataTempEntry.data.type = v1types[comment[0]]; + dataTempEntry.type = v1types[comment[0]]; + if (v1manufacturers.hasOwnProperty(dataTempEntry.type)) { + dataTempEntry.data.manufacturer = v1manufacturers[dataTempEntry.type]; + } + } + dataTempEntry.data.frequency = comment[2]; + } else if (data[i].type) { + dataTempEntry.data.type = data[i].type; + dataTempEntry.type = data[i].type; + } + if (data[i].subtype) { + dataTempEntry.data.type = data[i].subtype; + dataTempEntry.type = data[i].subtype; + } + if (data[i].xdata) { + dataTempEntry.data.xdata = data[i].xdata; + if (data[i].hasOwnProperty("pressure")) { + xdata_pressure = data[i].pressure; + } else { + xdata_pressure = 1100.0; + } + var tempXDATA = parseXDATA(data[i].xdata, xdata_pressure); + for (let field in tempXDATA) { + if (tempXDATA.hasOwnProperty(field)) { + if (field == "xdata_instrument") { + dataTempEntry.data.xdata_instrument = tempXDATA.xdata_instrument.join(', '); + } else { + dataTempEntry.data[field] = tempXDATA[field]; + } + } + } + } + dataTemp.push(dataTempEntry); + } + } + } + response.positions.position = dataTemp; + response.fetch_timestamp = Date.now(); + return response; +} \ No newline at end of file diff --git a/js/station.js b/js/station.js new file mode 100644 index 0000000..20ea504 --- /dev/null +++ b/js/station.js @@ -0,0 +1,963 @@ +/* SondeHub Tracker Station Popup Functions + * + * Author: Luke Prior + */ + +var launches_url = "https://api.v2.sondehub.org/sites"; +var sites = null; + +// Calculate the number of historical records for selected date range +// Set selectable months for selected year +function getSelectedNumber (station) { + var popup = $("#popup" + station); + var targetyear = popup.find("#yearList option:selected").val(); + var targetmonth = popup.find("#monthList option:selected").val(); + var count = 0; + var data = stationHistoricalData[station]; + + // Calculate count + for (let year in data) { + if (data.hasOwnProperty(year)) { + if (year == targetyear || targetyear == "all") { + for (let month in data[year]) { + if (data[year].hasOwnProperty(month)) { + if (month == targetmonth || targetmonth == "all" || targetyear == "all") { + count += data[year][month].length; + } + } + } + } + } + } + + // Update selected field & hide months if no data + var months = ["all", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; + popup.find('#yearList option').each(function() { + if ($(this).is(':selected')) { + $(this).attr("selected", "selected"); + var selectedYear = $(this).val(); + if (selectedYear != "all") { + months = Object.keys(data[selectedYear]); + months.push("all"); + } + } else { + $(this).attr("selected", false); + } + }); + + popup.find('#monthList option').each(function() { + if (!months.includes($(this).val())) { + $(this).hide(); + } else { + $(this).show(); + } + if ($(this).is(':selected')) { + $(this).attr("selected", "selected"); + } else { + $(this).attr("selected", false); + } + }); + + // Update popup + popup.find("#launchCount").text(count); +} + +// Get initial summary data for station, courtesy of TimMcMahon +function getHistorical (id, callback, continuation) { + var prefix = 'launchsites/' + id + '/'; + var params = { + Prefix: prefix, + }; + + if (typeof continuation !== 'undefined') { + params.ContinuationToken = continuation; + } else { + tempLaunchData = {}; + } + + s3.makeUnauthenticatedRequest('listObjectsV2', params, function(err, data) { + if (err) { + console.log(err, err.stack); + } else { + var tempSerials = []; + for (var i = 0; i < data.Contents.length; i++) { + // Sort data into year and month groups + var date = data.Contents[i].Key.substring(prefix.length).substring(0,10); + var year = date.substring(0,4); + var month = date.substring(5,7); + var serial = data.Contents[i].Key.substring(prefix.length+11).slice(0, -5); + if (tempLaunchData.hasOwnProperty(year)) { + if (tempLaunchData[year].hasOwnProperty(month)) { + if (!tempSerials.includes(serial)) { + tempSerials.push(serial) + tempLaunchData[year][month].push(data.Contents[i].Key); + } + } else { + tempLaunchData[year][month] = []; + tempSerials = []; + tempSerials.push(serial) + tempLaunchData[year][month].push(data.Contents[i].Key); + } + } else { + tempLaunchData[year] = {}; + tempLaunchData[year][month] = []; + tempSerials = []; + tempSerials.push(serial) + tempLaunchData[year][month].push(data.Contents[i].Key); + } + } + if (data.IsTruncated == true) { + // Requests are limited to 1000 entries so multiple may be required + getHistorical(id, callback, data.NextContinuationToken); + } else { + callback(tempLaunchData); + } + } + }); +} + +// Download summary data from AWS S3 +function downloadHistorical (suffix) { + var url = "https://sondehub-history.s3.amazonaws.com/" + suffix; + var ajaxReq = $.ajax({ + type: "GET", + url: url, + dataType: "json", + tryCount : 0, + retryLimit : 3, // Retry max of 3 times + error : function(xhr, textStatus, errorThrown ) { + if (textStatus == 'timeout') { + this.tryCount++; + if (this.tryCount <= this.retryLimit) { + //try again + $.ajax(this); + return; + } + return; + } + } + }); + historicalAjax.push(ajaxReq); + return ajaxReq; +} + +// Draw historic summaries to map +function drawHistorical (data, station) { + var landing = data[2]; + var serial = landing.serial; + var time = landing.datetime; + + if (!historicalPlots[station].sondes.hasOwnProperty(serial)) { + + historicalPlots[station].sondes[serial] = {}; + + // Using last known alt to detmine colour + var minAlt = 0; + var actualAlt = landing.alt; + var maxAlt = 10000; + + if (actualAlt > maxAlt) { + actualAlt = maxAlt; + } else if (actualAlt < minAlt) { + actualAlt = minAlt; + } + + var normalisedAlt = ((actualAlt-minAlt)/(maxAlt-minAlt)); + var iconColour = ConvertRGBtoHex(evaluate_cmap(normalisedAlt, 'turbo')); + + // Check if we have recovery data for it + var recovered = false; + if (historicalPlots[station].data.hasOwnProperty("recovered")) { + if (historicalPlots[station].data.recovered.hasOwnProperty(serial)) { + var recovery_info = historicalPlots[station].data.recovered[serial]; + recovered = true; + } + } + + var popup = L.popup(); + + html = "
"; + html += "
"+serial+" ("+time+")
"; + html += "
"; + html += "
 "+roundNumber(landing.lat, 5) + ', ' + roundNumber(landing.lon, 5)+"
"; + + var imp = offline.get('opt_imperial'); + var text_alt = Number((imp) ? Math.floor(3.2808399 * parseInt(landing.alt)) : parseInt(landing.alt)).toLocaleString("us"); + text_alt += " " + ((imp) ? 'ft':'m'); + + html += "
Altitude: "+text_alt+"
"; + html += "
Time: "+formatDate(stringToDateUTC(time))+"
"; + + if (landing.hasOwnProperty("type")) { + html += "
Sonde Type: " + landing.type + "
"; + }; + + html += "
"; + + if (recovered) { + html += "
"+(recovery_info.recovered ? "Recovered by " : "Not Recovered by ")+recovery_info.recovered_by+"
"; + html += "
Recovery time: "+formatDate(stringToDateUTC(recovery_info.datetime))+"
"; + html += "
Recovery location: "+recovery_info.position[1]+", "+recovery_info.position[0] + "
"; + html += "
Recovery notes: "+recovery_info.description+"
"; + + html += "
"; + } + + html += "
Show Full Flight Path: " + serial + "
"; + + html += "
"; + html += "
" + + if (landing.hasOwnProperty("uploader_callsign")) { + html += "
Last received by: " + landing.uploader_callsign.toLowerCase() + "
"; + }; + + popup.setContent(html); + + if (!recovered) { + var marker = L.circleMarker([landing.lat, landing.lon], {fillColor: "white", color: iconColour, weight: 3, radius: 5, fillOpacity:1}); + } else { + var marker = L.circleMarker([landing.lat, landing.lon], {fillColor: "grey", color: iconColour, weight: 3, radius: 5, fillOpacity:1}); + } + + marker.bindPopup(popup); + + marker.addTo(map); + marker.bringToBack(); + historicalPlots[station].sondes[serial].marker = marker; + } +} + +// Delete historic summaries from map +function deleteHistorical (station) { + var popup = $("#popup" + station); + var deleteHistorical = popup.find("#deleteHistorical"); + var historicalDelete = $("#historicalControlButton"); + + deleteHistorical.hide(); + + if (historicalPlots.hasOwnProperty(station)) { + for (let serial in historicalPlots[station].sondes) { + map.removeLayer(historicalPlots[station].sondes[serial].marker); + } + } + + delete historicalPlots[station]; + + var otherSondes = false; + + for (station in historicalPlots) { + if (historicalPlots.hasOwnProperty(station)) { + if (Object.keys(historicalPlots[station].sondes).length > 1) { + otherSondes = true; + } + } + } + + if (!otherSondes) historicalDelete.hide(); +} + +// Delete all historic sondes from map +function deleteHistoricalButton() { + var historicalDelete = $("#historicalControlButton"); + + for (station in historicalPlots) { + if (historicalPlots.hasOwnProperty(station)) { + historicalPlots[station].data.drawing = false; + for (let serial in historicalPlots[station].sondes) { + map.removeLayer(historicalPlots[station].sondes[serial].marker); + } + // Hide delete historical button from popup + var realpopup = launches.getLayer(stationMarkerLookup[station]).getPopup(); + var popup = $("#popup" + station); + var deleteHistorical = popup.find("#deleteHistorical"); + deleteHistorical.hide(); + // Required if popup is closed + if (!realpopup.isOpen()) { + var tempContent = $(realpopup.getContent()); + tempContent.find("#deleteHistorical").hide(); + realpopup.setContent("
" + tempContent.html() + "
"); + } + } + } + + // Cancel any queued or ongoing historical requests + for (i=0; i < historicalAjax.length; i++) { + historicalAjax[i].abort(); + } + + historicalAjax = []; + + historicalPlots = {}; + + historicalDelete.hide(); + +} + +// Delete all launch site predictions from map +function deletePredictionButton() { + var predictionDelete = $("#predictionControlButton"); + + for (var marker in launchPredictions) { + if (launchPredictions.hasOwnProperty(marker)) { + for (var prediction in launchPredictions[marker]) { + if (launchPredictions[marker].hasOwnProperty(prediction)) { + for (var object in launchPredictions[marker][prediction]) { + if (launchPredictions[marker][prediction].hasOwnProperty(object)) { + map.removeLayer(launchPredictions[marker][prediction][object]); + } + } + } + } + // Hide delete historical prediction button from popup + var realpopup = launches.getLayer(marker).getPopup(); + var popup = $("#popup" + markerStationLookup[marker]); + var deletePrediction = popup.find("#predictionDeleteButton"); + deletePrediction.hide(); + // Required if popup is closed + if (!realpopup.isOpen()) { + var tempContent = $(realpopup.getContent()); + tempContent.find("#predictionDeleteButton").hide(); + realpopup.setContent("
" + tempContent.html() + "
"); + } + } + } + + launchPredictions = {}; + + // Cancel any queued or ongoing launch site prediction requests + for (i=0; i < predictionAjax.length; i++) { + predictionAjax[i].abort(); + } + + predictionAjax = []; + + predictionDelete.hide(); + +} + +// Master function to display historic summaries +function showHistorical (station, marker) { + var popup = $("#popup" + station); + var realpopup = launches.getLayer(marker).getPopup(); + var submit = popup.find("#submit"); + var submitLoading = popup.find("#submitLoading"); + var deleteHistorical = popup.find("#deleteHistorical"); + var targetyear = popup.find("#yearList option:selected").val(); + var targetmonth = popup.find("#monthList option:selected").val(); + + submit.hide(); + submitLoading.show(); + deleteHistorical.hide(); + + var sondes = []; + var data = stationHistoricalData[station]; + + // Generate list of serial URLs + for (let year in data) { + if (data.hasOwnProperty(year)) { + if (year == targetyear || targetyear == "all") { + for (let month in data[year]) { + if (data[year].hasOwnProperty(month)) { + if (month == targetmonth || targetmonth == "all" || targetyear == "all") { + sondes = sondes.concat(data[year][month]); + } + } + } + } + } + } + + // Generate date range for station + dateNow = new Date(); + dateNow.setDate(dateNow.getDate() + 2); + + if (!historicalPlots.hasOwnProperty(station)) { + historicalPlots[station] = {}; + historicalPlots[station].sondes = {}; + historicalPlots[station].data = {}; + } + + // Get station location to fetch recoveries + if (!historicalPlots[station].data.hasOwnProperty("recovered")) { + historicalPlots[station].data.recovered = {}; + + var station_position = sites[station].position; + var data_str = "lat=" + station_position[0] + "&lon=" + station_position[1] + "&distance=400000&last=0"; + + $.ajax({ + type: "GET", + url: recovered_sondes_url, + data: data_str, + dataType: "json", + success: function(json) { + for (var i = 0; i < json.length; i++) { + historicalPlots[station].data.recovered[json[i].serial] = json[i]; + } + processHistorical() + }, + error: function() { + processHistorical(); + } + }); + } else { + processHistorical(); + } + + function processHistorical() { + var historicalDelete = $("#historicalControlButton"); + historicalDelete.show(); + + historicalPlots[station].data.drawing = true; + + for (let i = 0; i < sondes.length; i++) { + downloadHistorical(sondes[i]).done(handleData).fail(handleError); + } + + var completed = 0; + + function handleData(data) { + completed += 1; + try { + drawHistorical(data, station); + } catch(e) {}; + if (completed == sondes.length) { + submit.show(); + submitLoading.hide(); + if (historicalPlots[station].data.drawing) deleteHistorical.show(); + // If modal is closed the contents needs to be forced updated + if (!realpopup.isOpen()) { + realpopup.setContent("
" + popup.html() + "
"); + } + historicalPlots[station].data.drawing = false; + } + } + + function handleError(error) { + completed += 1; + if (completed == sondes.length) { + submit.show(); + submitLoading.hide(); + if (historicalPlots[station].data.drawing) deleteHistorical.show(); + // If modal is closed the contents needs to be forced updated + if (!realpopup.isOpen()) { + realpopup.setContent("
" + popup.html() + "
"); + } + historicalPlots[station].data.drawing = false; + } + } + } +} + +// Used to generate the content for station modal +function historicalLaunchViewer(station, marker) { + var realpopup = launches.getLayer(marker).getPopup(); + var popup = $("#popup" + station); + var historical = popup.find("#historical"); + function populateDropDown(data) { + // Save data + stationHistoricalData[station] = data; + + // Check if data exists + if (Object.keys(data).length == 0) { + historical.html("


No historical data
"); + historical.show(); + historicalButton.show(); + historicalButtonLoading.hide(); + // If modal is closed the contents needs to be forced updated + if (!realpopup.isOpen()) { + realpopup.setContent("
" + popup.html() + "
"); + } + return; + } + + // Find latest year + var latestYear = "0"; + var latestYears = Object.keys(data); + for (var i=0; i < latestYears.length; i++) { + if (parseInt(latestYears[i]) > parseInt(latestYear)) { + latestYear = latestYears[i]; + } + } + + // Generate year drop down + var yearList = document.createElement("select"); + yearList.name = "year" + yearList.id = "yearList"; + var option = document.createElement("option"); + option.value = "all"; + option.text = "All"; + yearList.appendChild(option); + for (let year in data) { + if (data.hasOwnProperty(year)) { + var option = document.createElement("option"); + option.value = year; + option.text = year; + if (year == latestYear) { + option.setAttribute("selected", "selected"); + } + yearList.appendChild(option); + } + } + + // Find latest month + var latestMonth = "0"; + var latestMonths = Object.keys(data[latestYear]); + for (var i=0; i < latestMonths.length; i++) { + if (parseInt(latestMonths[i]) > parseInt(latestMonth)) { + latestMonth = latestMonths[i]; + } + } + + // Generate month drop down + var monthList = document.createElement("select"); + monthList.name = "month" + monthList.id = "monthList"; + var option = document.createElement("option"); + option.value = "all"; + option.text = "All"; + monthList.appendChild(option); + var months = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; + var monthsText = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]; + for (var i=0; i < months.length; i++) { + var option = document.createElement("option"); + option.value = months[i]; + option.text = monthsText[i]; + if (months[i] == latestMonth) { + option.setAttribute("selected", "selected"); + } + monthList.appendChild(option); + } + + + // Calculate total launches + var totalLaunches = 0; + for (let year in data) { + if (data.hasOwnProperty(year)) { + for (let month in data[year]) { + if (data[year].hasOwnProperty(month)) { + totalLaunches += data[year][month].length; + } + } + } + } + + // Generate HTML + var popupContent = "


Launches Selected: " + totalLaunches + "
"; + popupContent += "
" + yearList.outerHTML; + popupContent += "" + monthList.outerHTML + "
"; + popupContent += "
"; + historical.html(popupContent); + historical.show(); + historicalButton.show(); + historicalButtonLoading.hide(); + // If modal is closed the contents needs to be forced updated + if (!realpopup.isOpen()) { + realpopup.setContent("
" + popup.html() + "
"); + } + getSelectedNumber(station); + } + if (historical.is(":visible")) { + // Don't regenerate if already in memory + historical.hide(); + } else { + if (stationHistoricalData.hasOwnProperty(station) && popup.find("#launchCount").length) { + // Don't regenerate if already in memory + historical.show(); + } else { + var historicalButton = popup.find("#historicalButton"); + var historicalButtonLoading = popup.find("#historicalButtonLoading"); + historicalButton.hide(); + historicalButtonLoading.show(); + getHistorical(station, populateDropDown); + } + } +} + +function launchSitePredictions(times, station, properties, marker, id) { + var realpopup = launches.getLayer(marker).getPopup(); + var popup = $("#popup" + id); + var predictionButton = popup.find("#predictionButton"); + var predictionButtonLoading = popup.find("#predictionButtonLoading"); + var predictionDeleteButton = popup.find("#predictionDeleteButton"); + + predictionButton.hide(); + predictionButtonLoading.show(); + + if (predictionDeleteButton.is(':visible')) { + deletePredictions(marker, id); + predictionDeleteButton.hide(); + } + position = station.split(","); + properties = properties.split(":"); + var now = new Date(); + if (times.length > 0) { + times = times.split(","); + var maxCount = 24 + var count = 0; + var day = 0; + var dates = []; + while (day < 8) { + for (var i = 0; i < times.length; i++) { + var date = new Date(); + var time = times[i].split(":"); + if (time[0] != 0) { + date.setDate(date.getDate() + (7 + time[0] - date.getDay()) % 7); + } + date.setUTCHours(time[1]); + date.setUTCMinutes(time[2]); + date.setSeconds(0); + date.setMilliseconds(0); + // launch time 45 minutes before target time + date.setMinutes( date.getMinutes() - 45 ); + while (date < now) { + if (time[0] == 0) { + date.setDate(date.getDate() + 1); + } else { + date.setDate(date.getDate() + 7); + } + } + if (day > 0) { + if (time[0] == 0) { + date.setDate(date.getDate() + day); + } else { + date.setDate(date.getDate() + (7*day)); + } + } + if (count < maxCount) { + if (((date - now) / 36e5) < 170) { + if (!dates.includes(date.toISOString().split('.')[0]+"Z")) { + dates.push(date.toISOString().split('.')[0]+"Z"); + count += 1; + } + } + } + } + day += 1; + } + dates.sort(); + } else { + var date = new Date(); + var dates = []; + dates.push(date.toISOString().split('.')[0]+"Z"); + } + var completed = 0; + var predictionDelete = $("#predictionControlButton"); + predictionDelete.show(); + for (var i = 0; i < dates.length; i++) { + var lon = ((360 + (position[1] % 360)) % 360); + //var url = "https://predict.cusf.co.uk/api/v1/?launch_latitude=" + position[0] + "&launch_longitude=" + lon + "&launch_datetime=" + dates[i] + "&ascent_rate=" + properties[0] + "&burst_altitude=" + properties[2] + "&descent_rate=" + properties[1]; + var url = "https://api.v2.sondehub.org/tawhiri?launch_latitude=" + position[0] + "&launch_longitude=" + lon + "&launch_datetime=" + dates[i] + "&ascent_rate=" + properties[0] + "&burst_altitude=" + properties[2] + "&descent_rate=" + properties[1]; + showPrediction(url).done(handleData).fail(handleError); + } + function handleData(data) { + completed += 1; + plotPrediction(data, dates, marker, properties); + if (completed == dates.length) { + if (Object.keys(launchPredictions).length != 0) predictionDeleteButton.show(); + predictionButton.show(); + predictionButtonLoading.hide(); + if (!realpopup.isOpen()) { + realpopup.setContent("
" + popup.html() + "
"); + } + } + } + function handleError(error) { + completed += 1; + if (completed == dates.length) { + if (Object.keys(launchPredictions).length != 0) predictionDeleteButton.show(); + predictionButton.show(); + predictionButtonLoading.hide(); + if (!realpopup.isOpen()) { + realpopup.setContent("
" + popup.html() + "
"); + } + } + } +} + +function plotPrediction (data, dates, marker, properties) { + if (!launchPredictions.hasOwnProperty(marker)) { + launchPredictions[marker] = {}; + } + launchPredictions[marker][dates.indexOf(data.request.launch_datetime)+1] = {}; + plot = launchPredictions[marker][dates.indexOf(data.request.launch_datetime)+1]; + + ascent = data.prediction[0].trajectory; + descent = data.prediction[1].trajectory; + var predictionPath = []; + for (var i = 0; i < ascent.length; i++) { + if (ascent[i].longitude > 180.0) { + var longitude = ascent[i].longitude - 360.0; + } else { + var longitude = ascent[i].longitude; + } + predictionPath.push([ascent[i].latitude, longitude]); + }; + for (var x = 0; x < descent.length; x++) { + if (descent[x].longitude > 180.0) { + var longitude = descent[x].longitude - 360.0; + } else { + var longitude = descent[x].longitude; + } + predictionPath.push([descent[x].latitude, longitude]); + }; + var burstPoint = ascent[ascent.length-1]; + var landingPoint = descent[descent.length-1]; + + plot.predictionPath = new L.polyline(predictionPath, {color: 'red'}).addTo(map); + + burstIconImage = host_url + markers_url + "balloon-pop.png"; + + burstIcon = new L.icon({ + iconUrl: burstIconImage, + iconSize: [20,20], + iconAnchor: [10, 10], + }); + + if (burstPoint.longitude > 180.0) { + var burstLongitude = burstPoint.longitude - 360.0; + } else { + var burstLongitude = burstPoint.longitude; + } + + plot.burstMarker = new L.marker([burstPoint.latitude, burstLongitude], { + icon: burstIcon + }).addTo(map); + + var burstTime = new Date(burstPoint.datetime); + var burstTooltip = "Time: " + burstTime.toLocaleString() + "
Altitude: " + Math.round(burstPoint.altitude) + "m"; + plot.burstMarker.bindTooltip(burstTooltip, {offset: [5,0]}); + + if (landingPoint.longitude > 180.0) { + var landingLongitude = landingPoint.longitude - 360.0; + } else { + var landingLongitude = landingPoint.longitude; + } + + plot.landingMarker = new L.marker([landingPoint.latitude, landingLongitude], { + icon: new L.NumberedDivIcon({number: dates.indexOf(data.request.launch_datetime)+1}) + }).addTo(map); + + var landingTime = new Date(landingPoint.datetime); + if (properties[3] != "" && properties[4] != "") { + var landingTooltip = "Time: " + landingTime.toLocaleString() + "
Model Dataset: " + data.request.dataset + + "
Model Assumptions:
- " + data.request.ascent_rate + "m/s ascent
- " + data.request.burst_altitude + "m burst altitude (" + properties[3] + " samples)
- " + data.request.descent_rate + "m/s descent (" + properties[4] + " samples)"; + } else { + var landingTooltip = "Time: " + landingTime.toLocaleString() + "
Model Dataset: " + data.request.dataset + + "
Model Assumptions:
- " + data.request.ascent_rate + "m/s ascent
- " + data.request.burst_altitude + "m burst altitude
- " + data.request.descent_rate + "m/s descent"; + } + plot.landingMarker.bindTooltip(landingTooltip, {offset: [13,-28]}); +} + +function showPrediction(url) { + var ajaxReq = $.ajax({ + type: "GET", + url: url, + dataType: "json", + }); + predictionAjax.push(ajaxReq); + return ajaxReq; +} + +function deletePredictions(marker, station) { + var predictionDelete = $("#predictionControlButton"); + if (launchPredictions.hasOwnProperty(marker)) { + for (var prediction in launchPredictions[marker]) { + if (launchPredictions[marker].hasOwnProperty(prediction)) { + for (var object in launchPredictions[marker][prediction]) { + if (launchPredictions[marker][prediction].hasOwnProperty(object)) { + map.removeLayer(launchPredictions[marker][prediction][object]); + } + } + } + } + delete launchPredictions[marker]; + } + var popup = $("#popup" + station); + var predictionDeleteButton = popup.find("#predictionDeleteButton"); + if (predictionDeleteButton.is(':visible')) { + predictionDeleteButton.hide(); + } + if (Object.keys(launchPredictions).length == 0) predictionDelete.hide(); +} + +function getLaunchSites() { + $.ajax({ + type: "GET", + url: launches_url, + dataType: "json", + success: function(json) { + sites = json; + generateLaunchSites(); + } + }); +} + +function generateLaunchSites() { + for (var key in sites) { + if (sites.hasOwnProperty(key)) { + var latlon = [sites[key].position[1], sites[key].position[0]]; + var sondesList = ""; + var popupContent = "
"; + var div = document.createElement('div'); + div.id = "popup" + key; + var ascent_rate = 5; + var descent_rate = 6; + var burst_altitude = 26000; + var burst_samples = ""; + var descent_samples = ""; + var marker = new L.circleMarker(latlon, {color: '#696969', fillColor: "white", radius: 8}); + var popup = new L.popup({ autoClose: false, closeOnClick: false }); + marker.title = key; + marker.bindPopup(popup); + launches.addLayer(marker); + + // Match sonde codes + if (sites[key].hasOwnProperty('rs_types')) { + var sondes = sites[key].rs_types; + for (var y = 0; y < sondes.length; y++) { + if (Array.isArray(sondes[y]) == false) { + sondes[y] = [sondes[y]]; + } + if (sondeCodes.hasOwnProperty(sondes[y][0])) { + sondesList += sondeCodes[sondes[y][0]] + if (sondes[y].length > 1) { + sondesList += " (" + sondes[y][1] + " MHz)"; + } + } else if (unsupportedSondeCodes.hasOwnProperty(sondes[y][0])) { + sondesList += unsupportedSondeCodes[sondes[y][0]]; + sondesList += " (cannot track)"; + } else { + sondesList += sondes[y][0] + " (unknown WMO code)"; + } + if (y < sondes.length-1) { + sondesList += ", "; + } + } + if (sondes.includes("11") || sondes.includes("82")) { //LMS6 + ascent_rate = 5; + descent_rate = 2.5; + burst_altitude = 33500; + } + popupContent += "" + sites[key].station_name + "

Sondes launched: " + sondesList; + } + + // Generate prefilled suggestion form + var popupLink = "https://docs.google.com/forms/d/e/1FAIpQLSfIbBSQMZOXpNE4VpK4BqUbKDPCWCDgU9QxYgmhh-JD-JGSsQ/viewform?usp=pp_url&entry.796606853=Modify+Existing+Site"; + popupLink += "&entry.749833526=" + key; + if (sites[key].hasOwnProperty('station_name')) { + popupLink += "&entry.675505431=" + sites[key].station_name.replace(/\s/g, '+'); + } + if (sites[key].hasOwnProperty('position')) { + popupLink += "&entry.1613779787=" + sites[key].position.reverse().toString(); + } + if (sites[key].hasOwnProperty('alt')) { + popupLink += "&entry.753148337=" + sites[key].alt; + } + if (sites[key].hasOwnProperty('ascent_rate')) { + popupLink += "&entry.509146334=" + sites[key]["ascent_rate"]; + } + if (sites[key].hasOwnProperty('burst_altitude')) { + popupLink += "&entry.1897602989=" + sites[key]["burst_altitude"]; + } + if (sites[key].hasOwnProperty('descent_rate')) { + popupLink += "&entry.267462486=" + sites[key]["descent_rate"]; + } + if (sites[key].hasOwnProperty('notes')) { + popupLink += "&entry.197384117=" + sites[key]["notes"].replace(/\s/g, '+'); + } + + // Update prediction data if provided + if (sites[key].hasOwnProperty('ascent_rate')) { + ascent_rate = sites[key]["ascent_rate"]; + } + if (sites[key].hasOwnProperty('descent_rate')) { + descent_rate = sites[key]["descent_rate"]; + } + if (sites[key].hasOwnProperty('burst_altitude')) { + burst_altitude = sites[key]["burst_altitude"]; + } + if (sites[key].hasOwnProperty('burst_samples')) { + burst_samples = sites[key]["burst_samples"]; + } + if (sites[key].hasOwnProperty('descent_samples')) { + descent_samples = sites[key]["descent_samples"]; + } + + // Process launch schedule if provided + if (sites[key].hasOwnProperty('times')) { + popupContent += "
Launch schedule:"; + for (var x = 0; x < sites[key]['times'].length; x++) { + popupContent += "
- "; + var day = sites[key]['times'][x].split(":")[0]; + if (day == 0) { + popupContent += "Everyday at "; + } else if (day == 1) { + popupContent += "Monday at "; + } else if (day == 2) { + popupContent += "Tuesday at "; + } else if (day == 3) { + popupContent += "Wednesday at "; + } else if (day == 4) { + popupContent += "Thursday at "; + } else if (day == 5) { + popupContent += "Friday at "; + } else if (day == 6) { + popupContent += "Saturday at "; + } else if (day == 7) { + popupContent += "Sunday at "; + } + popupContent += sites[key]['times'][x].split(":")[1] + ":" + sites[key]['times'][x].split(":")[2] + " UTC"; + } + } + + // Show notes if provided + if (sites[key].hasOwnProperty('notes')) { + popupContent += "
Notes: " + sites[key]["notes"]; + } + + popupContent += "
Know when this site launches? Contribute here"; + + // Generate view historical button + popupContent += "
"; + + // Create prediction button + if (sites[key].hasOwnProperty('times')) { + popupContent += ""; + } else { + popupContent += ""; + } + + popupContent += ""; + + div.innerHTML = popupContent; + + popup.setContent(div.innerHTML); + + var leafletID = launches.getLayerId(marker); + + stationMarkerLookup[key] = leafletID; + markerStationLookup[leafletID] = key; + } + } + if (focusID != 0) { + gotoSite(); + } +} + +// URL parameter redirect +function gotoSite() { + if (sites != null) { + if (sites.hasOwnProperty(focusID)) { + var site = sites[focusID]; + var latlng = new L.LatLng(site["position"][0], site["position"][1]); + map.setView(latlng, 9); + for (var i in launches._layers) { + marker = launches._layers + if (marker[i].title == focusID) { + marker[i].openPopup(); + } + } + } + } +} \ No newline at end of file diff --git a/js/tracker.js b/js/tracker.js index 1d04a06..5dba81c 100644 --- a/js/tracker.js +++ b/js/tracker.js @@ -6,7 +6,6 @@ var predictions_url = "https://api.v2.sondehub.org/predictions?vehicles="; var launch_predictions_url = "https://api.v2.sondehub.org/predictions/reverse"; var recovered_sondes_url = "https://api.v2.sondehub.org/recovered"; var recovered_sondes_stats_url = "https://api.v2.sondehub.org/recovered/stats"; -var launches_url = "https://api.v2.sondehub.org/sites"; var livedata = "wss://ws-reader.v2.sondehub.org/"; var clientID = "SondeHub-Tracker-" + Math.floor(Math.random() * 10000000000); @@ -31,6 +30,9 @@ var recovery_names = []; var recoveries = []; var launchPredictions = {}; +var stationMarkerLookup = {}; +var markerStationLookup = {}; +var predictionAjax = []; var stationHistoricalData = {}; var historicalPlots = {}; @@ -38,9 +40,7 @@ var historicalAjax = []; var skewtdata = []; -var sites = null; var launches = new L.LayerGroup(); -var showLaunches = false; var focusID = 0; var receiverCanvas = null; @@ -163,6 +163,154 @@ var v1manufacturers = { "M20": "Meteomodem" } +var globalKeys = { + "ascentrate": "Ascent Rate", + "battery_percent": "Battery", + "temperature_external": "Temperature, External", + "pressure_internal": "Pressure, Internal", + "voltage_solar_1": "Voltage, Solar 1", + "voltage_solar_2": "Voltage, Solar 2", + "light_red": "Light (Red)", + "light_green": "Light (Green)", + "light_blue": "Light (Blue)", + "gas_a": "Gas (A)", + "gas_b": "Gas (B)", + "gas_co2": "Gas (CO)", + "gas_combustible": "Gas (Combustible)", + "radiation": "Radiation (CPM)", + "temperature_radio": "Temperature, Radio", + "uplink_rssi": "Uplink RSSI", + "light_intensity": "Light Intensity", + "pred_lat": "Onboard Prediction (Lat)", + "pred_lon": "Onboard Prediction (Lon)", + "batt": "Battery Voltage", + "sats": "GNSS SVs Used", + "humidity": "Relative Humidity", + "subtype": "Sonde Sub-type", + "frequency": "Frequency", + "frequency_tx": "TX Frequency", + "manufacturer": "Manufacturer", + "type": "Sonde Type", + "burst_timer": "Burst Timer", + "xdata": "XDATA", + "xdata_instrument": "XDATA Instrument", + "oif411_instrument_number": "OIF411 Instrument Number", + "oif411_ext_voltage": "OIF411 External Voltage", + "oif411_ozone_battery_v": "OIF411 Battery", + "oif411_ozone_current_uA": "Ozone Current", + "oif411_ozone_pump_curr_mA": "Ozone Pump Current", + "oif411_ozone_pump_temp": "Ozone Pump Temperature", + "oif411_O3_partial_pressure": "Ozone Partial Pressure", + "oif411_serial": "OIF411 Serial Number", + "oif411_diagnostics": "OIF411 Diagnostics", + "oif411_version": "OIF411 Version", + "cfh_instrument_number": "CFH Instrument Number", + "cfh_mirror_temperature": "CFH Mirror Temperature", + "cfh_optics_voltage": "CFH Optics Voltage", + "cfh_optics_temperature": "CFH Optics Temperature", + "cfh_battery": "CFH Battery", + "cobald_instrument_number": "COBALD Instrument Number", + "cobald_sonde_number": "COBALD Sonde Number", + "cobald_internal_temperature": "COBALD Internal Temperature", + "cobald_blue_backscatter": "COBALD Blue Backscatter", + "cobald_red_backscatter": "COBALD Red Backscatter", + "cobald_blue_monitor": "COBALD Blue Monitor", + "cobald_red_monitor": "COBALD Red Monitor", + "pcfh_instrument_number": "PCFH Instrument Number", + "pcfh_6v_analog_supply_battery_voltage": "PCFH 6V Analog Battery Voltage", + "pcfh_45v_logic_supply_battery_voltage": "PCFH 4.5V Supply Battery Voltage", + "pcfh_45v_peltier_and_heater_supply_battery_voltage": "PCFH 4.5V Peltier Battery Voltage", + "pcfh_air_temperature_01": "PCFH Air Temperature #1", + "pcfh_air_temperature_02": "PCFH Air Temperature #2", + "pcfh_anticipated_frost_point_mirror_temperature_01": "PCFH Anticipated Frost Point Mirror Temperature #1", + "pcfh_anticipated_frost_point_mirror_temperature_02": "PCFH Anticipated Frost Point Mirror Temperature #2", + "pcfh_clean_frost_point_mirror_reflectance_01": "PCFH Clean Frost Point Mirror Reflectance #1", + "pcfh_clean_frost_point_mirror_reflectance_02": "PCFH Clean Frost Point Mirror Reflectance #2", + "pcfh_clean_reference_surface_reflectance_01": "PCFH Clean Reference Surface Reflectance #1", + "pcfh_clean_reference_surface_reflectance_02": "PCFH Clean Reference Surface Reflectance #2", + "pcfh_frost_point_mirror_reflectance_01": "PCFH Frost Point Mirror Reflectance #1", + "pcfh_frost_point_mirror_reflectance_02": "PCFH Frost Point Mirror Reflectance #2", + "pcfh_reference_surface_reflectance_01": "PCFH Reference Surface Reflectance #1", + "pcfh_reference_surface_reflectance_01": "PCFH Reference Surface Reflectance #2", + "pcfh_heat_sink_temperature_01": "PCFH Heat Sink Temperature #1", + "pcfh_heat_sink_temperature_02": "PCFH Heat Sink Temperature #2", + "pcfh_frost_point_mirror_temperature_01": "PCFH Frost Point Mirror Temperature #1", + "pcfh_frost_point_mirror_temperature_02": "PCFH Frost Point Mirror Temperature #2", + "pcfh_peltier_hot_side_temperature_01": "PCFH Peltier Hot Side Temperature #1", + "pcfh_peltier_hot_side_temperature_02": "PCFH Peltier Hot Side Temperature #2", + "pcfh_reference_surface_heating_current_01": "PCFH Reference Surface Heating Current #1", + "pcfh_reference_surface_heating_current_02": "PCFH Reference Surface Heating Current #2", + "pcfh_reference_surface_temperature_01": "PCFH Reference Surface Temperature #1", + "pcfh_reference_surface_temperature_02": "PCFH Reference Surface Temperature #2", + "pcfh_peltier_current_01": "PCFH Peltier Current #1", + "pcfh_peltier_current_02": "PCFH Peltier Current #2", + "pcfh_reserved_temperature": "PCFH Reserved Temperature", + "pcfh_thermocouple_reference_temperature": "PCFH Reference Thermocouple Temperature", + "pcfh_serial_number": "PCFH Serial Number", + "pcfh_controller_fw_date": "PCFH Controller Firmware Date", + "pcfh_fpga_fw_date": "PCFH FPGA Firmware Date", + "pcfh_temperature_pcb_date": "PCFH Temperature PCB Manufacture Date", + "pcfh_main_pcb_date": "PCFH Main PCB Manufacture Date" +}; + +var globalSuffixes = { + "current": " A", + "battery": " V", + "batt": " V", + "solar_panel": " V", + "temperature": "°C", + "temperature_internal": "°C", + "temperature_external": "°C", + "temperature_radio": "°C", + "pressure": " hPa", + "voltage_solar_1": " V", + "voltage_solar_2": " V", + "battery_percent": "%", + "uplink_rssi": " dBm", + "rssi_last_message": " dBm", + "rssi_floor": " dBm", + "bearing": "°", + "iss_azimuth": "°", + "iss_elevation": "°", + "light_intensity": " lx", + "humidity": " %", + "frequency": " MHz", + "frequency_tx": " MHz", + "spam": "", + "oif411_ext_voltage": " V", + "oif411_ozone_battery_v": " V", + "oif411_ozone_current_uA": " uA", + "oif411_ozone_pump_curr_mA": " mA", + "oif411_ozone_pump_temp": "°C", + "oif411_O3_partial_pressure": " mPa (+/- 1)", + "cfh_mirror_temperature": "°C", + "cfh_optics_voltage": " V", + "cfh_optics_temperature": "°C", + "cfh_battery": " V", + "cobald_internal_temperature": "°C", + "pcfh_6v_analog_supply_battery_voltage": " V", + "pcfh_45v_logic_supply_battery_voltage": " V", + "pcfh_45v_peltier_and_heater_supply_battery_voltage": " V", + "pcfh_air_temperature_01": "°C", + "pcfh_air_temperature_02": "°C", + "pcfh_anticipated_frost_point_mirror_temperature_01": "°C", + "pcfh_anticipated_frost_point_mirror_temperature_02": "°C", + "pcfh_heat_sink_temperature_01": "°C", + "pcfh_heat_sink_temperature_02": "°C", + "pcfh_frost_point_mirror_temperature_01": "°C", + "pcfh_frost_point_mirror_temperature_02": "°C", + "pcfh_peltier_hot_side_temperature_01": "°C", + "pcfh_peltier_hot_side_temperature_02": "°C", + "pcfh_reference_surface_heating_current_01": " A", + "pcfh_reference_surface_heating_current_02": " A", + "pcfh_reference_surface_temperature_01": "°C", + "pcfh_reference_surface_temperature_02": "°C", + "pcfh_peltier_current_01": " A", + "pcfh_peltier_current_02": " A", + "pcfh_reserved_temperature": "°C", + "pcfh_thermocouple_reference_temperature": "°C", +}; + // localStorage vars var ls_receivers = false; var ls_pred = false; @@ -670,6 +818,28 @@ function load() { L.control.historicalontrol({ position: 'topleft' }).addTo(map); + L.Control.PredictionControl = L.Control.extend({ + onAdd: function(map) { + var div = L.DomUtil.create('div'); + + div.innerHTML = ''; + div.id = "predictionControlButton"; + div.style.display = "none"; + + return div; + }, + + onRemove: function(map) { + // Nothing to do here + } + }); + + L.control.predictionontrol = function(opts) { + return new L.Control.PredictionControl(opts); + } + + L.control.predictionontrol({ position: 'topleft' }).addTo(map); + // update current position if we geolocation is available if(currentPosition) updateCurrentPosition(currentPosition.lat, currentPosition.lon); @@ -786,893 +956,57 @@ function load() { div.appendChild ( numdiv ); this._setIconStyles(div, 'icon'); return div; - }, - - createShadow: function () { - return null; - } - }); - - map.whenReady(callBack); - - // animate-in the timebox, - setTimeout(function() { - var elm = $("#timebox"); - - //if(is_mobile) $(".slickbox").css({left:'5px'}); - var origW = elm.width(); - var iconW = elm.find("svg").width(); - - if(offline.get('opt_hide_timebox')) { - elm.removeClass('animate').hide(); - $("#lookanglesbox").css({top:'7px'}); - } - - // prep for animation - $(".slickbox.animate").css({width:iconW}).find("span").hide(); - - if(!offline.get('opt_hide_timebox')) { - // animate timebox - elm.fadeIn(500,"easeOut").animate({width:origW},400,"easeOut", function() { - $("#timebox span").fadeIn(500, "easeOut"); - }); - } - - // animate lookanglesbox, delayed start by 300ms - $("#lookanglesbox").delay(200).fadeIn(500,"easeOut").animate({width:origW},400,"easeOut", function() { - if(GPS_ts === null) { - $("#lookanglesbox .nopos").fadeIn(500, "easeOut"); - } else if($("#lookanglesbox span:first").is(":hidden")) { - $("#lookanglesbox .nofollow").fadeIn(500, "easeOut"); - } - }); - - // if we there is enough screen space open aboutbox on startup - if(!is_mobile && !offline.get('opt_nowelcome') && $(window).width() > 900) $('.nav li.about').click(); - - }, 500); -} - -function setTimeValue() { - setTimeout(function() { - document.getElementById("timeperiod").value = wvar.mode; - }, 100); -} - -function getSelectedNumber (station) { - var popup = $("#popup" + station); - var targetyear = popup.find("#yearList option:selected").val(); - var targetmonth = popup.find("#monthList option:selected").val(); - var count = 0; - var data = stationHistoricalData[station]; - - // Calculate count - for (let year in data) { - if (data.hasOwnProperty(year)) { - if (year == targetyear || targetyear == "all") { - for (let month in data[year]) { - if (data[year].hasOwnProperty(month)) { - if (month == targetmonth || targetmonth == "all" || targetyear == "all") { - count += data[year][month].length; - } - } - } - } - } - } - - // Update selected field & hide months if no data - var months = ["all", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; - popup.find('#yearList option').each(function() { - if ($(this).is(':selected')) { - $(this).attr("selected", "selected"); - var selectedYear = $(this).val(); - if (selectedYear != "all") { - months = Object.keys(data[selectedYear]); - months.push("all"); - } - } else { - $(this).attr("selected", false); - } - }); - - popup.find('#monthList option').each(function() { - if (!months.includes($(this).val())) { - $(this).hide(); - } else { - $(this).show(); - } - if ($(this).is(':selected')) { - $(this).attr("selected", "selected"); - } else { - $(this).attr("selected", false); - } - }); - - // Update popup - popup.find("#launchCount").text(count); -} - -// Download summary data from AWS S3 -function downloadHistorical (suffix) { - var url = "https://sondehub-history.s3.amazonaws.com/" + suffix; - var ajaxReq = $.ajax({ - type: "GET", - url: url, - dataType: "json", - tryCount : 0, - retryLimit : 3, // Retry max of 3 times - error : function(xhr, textStatus, errorThrown ) { - if (textStatus == 'timeout') { - this.tryCount++; - if (this.tryCount <= this.retryLimit) { - //try again - $.ajax(this); - return; - } - return; - } - } - }); - historicalAjax.push(ajaxReq); - return ajaxReq; -} - -// Draw historic summaries to map -function drawHistorical (data, station) { - var landing = data[2]; - var serial = landing.serial; - var time = landing.datetime; - - if (!historicalPlots[station].sondes.hasOwnProperty(serial)) { - - historicalPlots[station].sondes[serial] = {}; - - // Using last known alt to detmine colour - var minAlt = 0; - var actualAlt = landing.alt; - var maxAlt = 10000; - - if (actualAlt > maxAlt) { - actualAlt = maxAlt; - } else if (actualAlt < minAlt) { - actualAlt = minAlt; - } - - var normalisedAlt = ((actualAlt-minAlt)/(maxAlt-minAlt)); - var iconColour = ConvertRGBtoHex(evaluate_cmap(normalisedAlt, 'turbo')); - - // Check if we have recovery data for it - var recovered = false; - if (historicalPlots[station].data.hasOwnProperty("recovered")) { - if (historicalPlots[station].data.recovered.hasOwnProperty(serial)) { - var recovery_info = historicalPlots[station].data.recovered[serial]; - recovered = true; - } - } - - var popup = L.popup(); - - html = "
"; - html += "
"+serial+" ("+time+")
"; - html += "
"; - html += "
 "+roundNumber(landing.lat, 5) + ', ' + roundNumber(landing.lon, 5)+"
"; - - var imp = offline.get('opt_imperial'); - var text_alt = Number((imp) ? Math.floor(3.2808399 * parseInt(landing.alt)) : parseInt(landing.alt)).toLocaleString("us"); - text_alt += " " + ((imp) ? 'ft':'m'); - - html += "
Altitude: "+text_alt+"
"; - html += "
Time: "+formatDate(stringToDateUTC(time))+"
"; - - if (landing.hasOwnProperty("type")) { - html += "
Sonde Type: " + landing.type + "
"; - }; - - html += "
"; - - if (recovered) { - html += "
"+(recovery_info.recovered ? "Recovered by " : "Not Recovered by ")+recovery_info.recovered_by+"
"; - html += "
Recovery time: "+formatDate(stringToDateUTC(recovery_info.datetime))+"
"; - html += "
Recovery location: "+recovery_info.position[1]+", "+recovery_info.position[0] + "
"; - html += "
Recovery notes: "+recovery_info.description+"
"; - - html += "
"; - } - - html += "
Show Full Flight Path: " + serial + "
"; - - html += "
"; - html += "
" - - if (landing.hasOwnProperty("uploader_callsign")) { - html += "
Last received by: " + landing.uploader_callsign.toLowerCase() + "
"; - }; - - popup.setContent(html); - - if (!recovered) { - var marker = L.circleMarker([landing.lat, landing.lon], {fillColor: "white", color: iconColour, weight: 3, radius: 5, fillOpacity:1}); - } else { - var marker = L.circleMarker([landing.lat, landing.lon], {fillColor: "grey", color: iconColour, weight: 3, radius: 5, fillOpacity:1}); - } - - marker.bindPopup(popup); - - marker.addTo(map); - marker.bringToBack(); - historicalPlots[station].sondes[serial].marker = marker; - } -} - -// Delete historic summaries from map -function deleteHistorical (station) { - var popup = $("#popup" + station); - var deleteHistorical = popup.find("#deleteHistorical"); - var historicalDelete = $("#historicalControlButton"); - - deleteHistorical.hide(); - - if (historicalPlots.hasOwnProperty(station)) { - for (let serial in historicalPlots[station].sondes) { - map.removeLayer(historicalPlots[station].sondes[serial].marker); - } - } - - delete historicalPlots[station]; - - var otherSondes = false; - - for (station in historicalPlots) { - if (historicalPlots.hasOwnProperty(station)) { - if (Object.keys(historicalPlots[station].sondes).length > 1) { - otherSondes = true; - } - } - } - - if (!otherSondes) historicalDelete.hide(); -} - -function deleteHistoricalButton() { - var historicalDelete = $("#historicalControlButton"); - - for (station in historicalPlots) { - if (historicalPlots.hasOwnProperty(station)) { - historicalPlots[station].data.drawing = false; - for (let serial in historicalPlots[station].sondes) { - map.removeLayer(historicalPlots[station].sondes[serial].marker); - } - var popup = $("#popup" + station); - var deleteHistorical = popup.find("#deleteHistorical"); - deleteHistorical.hide(); - } - } - - for (i=0; i < historicalAjax.length; i++) { - historicalAjax[i].abort(); - } - - historicalAjax = []; - - historicalPlots = {}; - - historicalDelete.hide(); - -} - -// Master function to display historic summaries -function showHistorical (station, marker) { - var popup = $("#popup" + station); - var realpopup = launches.getLayer(marker).getPopup(); - var submit = popup.find("#submit"); - var submitLoading = popup.find("#submitLoading"); - var deleteHistorical = popup.find("#deleteHistorical"); - var targetyear = popup.find("#yearList option:selected").val(); - var targetmonth = popup.find("#monthList option:selected").val(); - - submit.hide(); - submitLoading.show(); - deleteHistorical.hide(); - - var sondes = []; - var data = stationHistoricalData[station]; - - // Generate list of serial URLs - for (let year in data) { - if (data.hasOwnProperty(year)) { - if (year == targetyear || targetyear == "all") { - for (let month in data[year]) { - if (data[year].hasOwnProperty(month)) { - if (month == targetmonth || targetmonth == "all" || targetyear == "all") { - sondes = sondes.concat(data[year][month]); - } - } - } - } - } - } - - // Generate date range for station - // TODO make this reactive? - dateNow = new Date(); - dateNow.setDate(dateNow.getDate() + 2); - - if (!historicalPlots.hasOwnProperty(station)) { - historicalPlots[station] = {}; - historicalPlots[station].sondes = {}; - historicalPlots[station].data = {}; - } - - // Get station location to fetch recoveries - if (!historicalPlots[station].data.hasOwnProperty("recovered")) { - historicalPlots[station].data.recovered = {}; - - var station_position = sites[station].position; - var data_str = "lat=" + station_position[0] + "&lon=" + station_position[1] + "&distance=400000&last=0"; - - $.ajax({ - type: "GET", - url: recovered_sondes_url, - data: data_str, - dataType: "json", - success: function(json) { - for (var i = 0; i < json.length; i++) { - historicalPlots[station].data.recovered[json[i].serial] = json[i]; - } - processHistorical() - }, - error: function() { - processHistorical(); - } - }); - } else { - processHistorical(); - } - - function processHistorical() { - var historicalDelete = $("#historicalControlButton"); - historicalDelete.show(); - - historicalPlots[station].data.drawing = true; - - for (let i = 0; i < sondes.length; i++) { - downloadHistorical(sondes[i]).done(handleData).fail(handleError); - } - - var completed = 0; - - function handleData(data) { - completed += 1; - try { - drawHistorical(data, station); - } catch(e) {}; - if (completed == sondes.length) { - submit.show(); - submitLoading.hide(); - if (historicalPlots[station].data.drawing) deleteHistorical.show(); - // If modal is closed the contents needs to be forced updated - if (!realpopup.isOpen()) { - realpopup.setContent("
" + popup.html() + "
"); - } - historicalPlots[station].data.drawing = false; - } - } - - function handleError(error) { - completed += 1; - if (completed == sondes.length) { - submit.show(); - submitLoading.hide(); - if (historicalPlots[station].data.drawing) deleteHistorical.show(); - // If modal is closed the contents needs to be forced updated - if (!realpopup.isOpen()) { - realpopup.setContent("
" + popup.html() + "
"); - } - historicalPlots[station].data.drawing = false; - } - } - } -} - -// Used to generate the content for station modal -function historicalLaunchViewer(station, marker) { - var realpopup = launches.getLayer(marker).getPopup(); - var popup = $("#popup" + station); - var historical = popup.find("#historical"); - function populateDropDown(data) { - // Save data - stationHistoricalData[station] = data; - - // Check if data exists - if (Object.keys(data).length == 0) { - historical.html("


No historical data
"); - historical.show(); - historicalButton.show(); - historicalButtonLoading.hide(); - // If modal is closed the contents needs to be forced updated - if (!realpopup.isOpen()) { - realpopup.setContent("
" + popup.html() + "
"); - } - return; - } - - // Find latest year - var latestYear = "0"; - var latestYears = Object.keys(data); - for (var i=0; i < latestYears.length; i++) { - if (parseInt(latestYears[i]) > parseInt(latestYear)) { - latestYear = latestYears[i]; - } - } - - // Generate year drop down - var yearList = document.createElement("select"); - yearList.name = "year" - yearList.id = "yearList"; - var option = document.createElement("option"); - option.value = "all"; - option.text = "All"; - yearList.appendChild(option); - for (let year in data) { - if (data.hasOwnProperty(year)) { - var option = document.createElement("option"); - option.value = year; - option.text = year; - if (year == latestYear) { - option.setAttribute("selected", "selected"); - } - yearList.appendChild(option); - } - } - - // Find latest month - var latestMonth = "0"; - var latestMonths = Object.keys(data[latestYear]); - for (var i=0; i < latestMonths.length; i++) { - if (parseInt(latestMonths[i]) > parseInt(latestMonth)) { - latestMonth = latestMonths[i]; - } - } - - // Generate month drop down - var monthList = document.createElement("select"); - monthList.name = "month" - monthList.id = "monthList"; - var option = document.createElement("option"); - option.value = "all"; - option.text = "All"; - monthList.appendChild(option); - var months = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; - var monthsText = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]; - for (var i=0; i < months.length; i++) { - var option = document.createElement("option"); - option.value = months[i]; - option.text = monthsText[i]; - if (months[i] == latestMonth) { - option.setAttribute("selected", "selected"); - } - monthList.appendChild(option); - } - - - // Calculate total launches - var totalLaunches = 0; - for (let year in data) { - if (data.hasOwnProperty(year)) { - for (let month in data[year]) { - if (data[year].hasOwnProperty(month)) { - totalLaunches += data[year][month].length; - } - } - } - } - - // Generate HTML - var popupContent = "


Launches Selected: " + totalLaunches + "
"; - popupContent += "
" + yearList.outerHTML; - popupContent += "" + monthList.outerHTML + "
"; - popupContent += "
"; - historical.html(popupContent); - historical.show(); - historicalButton.show(); - historicalButtonLoading.hide(); - // If modal is closed the contents needs to be forced updated - if (!realpopup.isOpen()) { - realpopup.setContent("
" + popup.html() + "
"); - } - getSelectedNumber(station); - } - if (historical.is(":visible")) { - // Don't regenerate if already in memory - historical.hide(); - } else { - if (stationHistoricalData.hasOwnProperty(station) && popup.find("#launchCount").length) { - // Don't regenerate if already in memory - historical.show(); - } else { - var historicalButton = popup.find("#historicalButton"); - var historicalButtonLoading = popup.find("#historicalButtonLoading"); - historicalButton.hide(); - historicalButtonLoading.show(); - getHistorical(station, populateDropDown); - } - } -} - -function launchSitePredictions(times, station, properties, marker, id) { - var realpopup = launches.getLayer(marker).getPopup(); - var popup = $("#popup" + id); - var predictionButton = popup.find("#predictionButton"); - var predictionButtonLoading = popup.find("#predictionButtonLoading"); - var predictionDeleteButton = popup.find("#predictionDeleteButton"); - - predictionButton.hide(); - predictionButtonLoading.show(); - - if (predictionDeleteButton.is(':visible')) { - deletePredictions(marker, id); - predictionDeleteButton.hide(); - } - position = station.split(","); - properties = properties.split(":"); - var now = new Date(); - if (times.length > 0) { - times = times.split(","); - var maxCount = 24 - var count = 0; - var day = 0; - var dates = []; - while (day < 8) { - for (var i = 0; i < times.length; i++) { - var date = new Date(); - var time = times[i].split(":"); - if (time[0] != 0) { - date.setDate(date.getDate() + (7 + time[0] - date.getDay()) % 7); - } - date.setUTCHours(time[1]); - date.setUTCMinutes(time[2]); - date.setSeconds(0); - date.setMilliseconds(0); - // launch time 45 minutes before target time - date.setMinutes( date.getMinutes() - 45 ); - while (date < now) { - if (time[0] == 0) { - date.setDate(date.getDate() + 1); - } else { - date.setDate(date.getDate() + 7); - } - } - if (day > 0) { - if (time[0] == 0) { - date.setDate(date.getDate() + day); - } else { - date.setDate(date.getDate() + (7*day)); - } - } - if (count < maxCount) { - if (((date - now) / 36e5) < 170) { - if (!dates.includes(date.toISOString().split('.')[0]+"Z")) { - dates.push(date.toISOString().split('.')[0]+"Z"); - count += 1; - } - } - } - } - day += 1; - } - dates.sort(); - } else { - var date = new Date(); - var dates = []; - dates.push(date.toISOString().split('.')[0]+"Z"); - } - var completed = 0; - for (var i = 0; i < dates.length; i++) { - var lon = ((360 + (position[1] % 360)) % 360); - //var url = "https://predict.cusf.co.uk/api/v1/?launch_latitude=" + position[0] + "&launch_longitude=" + lon + "&launch_datetime=" + dates[i] + "&ascent_rate=" + properties[0] + "&burst_altitude=" + properties[2] + "&descent_rate=" + properties[1]; - var url = "https://api.v2.sondehub.org/tawhiri?launch_latitude=" + position[0] + "&launch_longitude=" + lon + "&launch_datetime=" + dates[i] + "&ascent_rate=" + properties[0] + "&burst_altitude=" + properties[2] + "&descent_rate=" + properties[1]; - showPrediction(url).done(handleData).fail(handleError); - } - function handleData(data) { - completed += 1; - plotPrediction(data, dates, marker, properties); - if (completed == dates.length) { - predictionDeleteButton.show(); - predictionButton.show(); - predictionButtonLoading.hide(); - if (!realpopup.isOpen()) { - realpopup.setContent("
" + popup.html() + "
"); - } - } - } - function handleError(error) { - completed += 1; - if (completed == dates.length) { - predictionDeleteButton.show(); - predictionButton.show(); - predictionButtonLoading.hide(); - if (!realpopup.isOpen()) { - realpopup.setContent("
" + popup.html() + "
"); - } - } - } -} - -function plotPrediction (data, dates, marker, properties) { - if (!launchPredictions.hasOwnProperty(marker)) { - launchPredictions[marker] = {}; - } - launchPredictions[marker][dates.indexOf(data.request.launch_datetime)+1] = {}; - plot = launchPredictions[marker][dates.indexOf(data.request.launch_datetime)+1]; - - ascent = data.prediction[0].trajectory; - descent = data.prediction[1].trajectory; - var predictionPath = []; - for (var i = 0; i < ascent.length; i++) { - if (ascent[i].longitude > 180.0) { - var longitude = ascent[i].longitude - 360.0; - } else { - var longitude = ascent[i].longitude; - } - predictionPath.push([ascent[i].latitude, longitude]); - }; - for (var x = 0; x < descent.length; x++) { - if (descent[x].longitude > 180.0) { - var longitude = descent[x].longitude - 360.0; - } else { - var longitude = descent[x].longitude; - } - predictionPath.push([descent[x].latitude, longitude]); - }; - var burstPoint = ascent[ascent.length-1]; - var landingPoint = descent[descent.length-1]; - - plot.predictionPath = new L.polyline(predictionPath, {color: 'red'}).addTo(map); - - burstIconImage = host_url + markers_url + "balloon-pop.png"; - - burstIcon = new L.icon({ - iconUrl: burstIconImage, - iconSize: [20,20], - iconAnchor: [10, 10], - }); - - if (burstPoint.longitude > 180.0) { - var burstLongitude = burstPoint.longitude - 360.0; - } else { - var burstLongitude = burstPoint.longitude; - } - - plot.burstMarker = new L.marker([burstPoint.latitude, burstLongitude], { - icon: burstIcon - }).addTo(map); - - var burstTime = new Date(burstPoint.datetime); - var burstTooltip = "Time: " + burstTime.toLocaleString() + "
Altitude: " + Math.round(burstPoint.altitude) + "m"; - plot.burstMarker.bindTooltip(burstTooltip, {offset: [5,0]}); - - if (landingPoint.longitude > 180.0) { - var landingLongitude = landingPoint.longitude - 360.0; - } else { - var landingLongitude = landingPoint.longitude; - } - - plot.landingMarker = new L.marker([landingPoint.latitude, landingLongitude], { - icon: new L.NumberedDivIcon({number: dates.indexOf(data.request.launch_datetime)+1}) - }).addTo(map); - - var landingTime = new Date(landingPoint.datetime); - if (properties[3] != "" && properties[4] != "") { - var landingTooltip = "Time: " + landingTime.toLocaleString() + "
Model Dataset: " + data.request.dataset + - "
Model Assumptions:
- " + data.request.ascent_rate + "m/s ascent
- " + data.request.burst_altitude + "m burst altitude (" + properties[3] + " samples)
- " + data.request.descent_rate + "m/s descent (" + properties[4] + " samples)"; - } else { - var landingTooltip = "Time: " + landingTime.toLocaleString() + "
Model Dataset: " + data.request.dataset + - "
Model Assumptions:
- " + data.request.ascent_rate + "m/s ascent
- " + data.request.burst_altitude + "m burst altitude
- " + data.request.descent_rate + "m/s descent"; - } - plot.landingMarker.bindTooltip(landingTooltip, {offset: [13,-28]}); -} - -function showPrediction(url) { - return $.ajax({ - type: "GET", - url: url, - dataType: "json", - }); -} - -function deletePredictions(marker, station) { - if (launchPredictions.hasOwnProperty(marker)) { - for (var prediction in launchPredictions[marker]) { - if (launchPredictions[marker].hasOwnProperty(prediction)) { - for (var object in launchPredictions[marker][prediction]) { - if (launchPredictions[marker][prediction].hasOwnProperty(object)) { - map.removeLayer(launchPredictions[marker][prediction][object]); - } - } - } - } - } - var popup = $("#popup" + station); - var predictionDeleteButton = popup.find("#predictionDeleteButton"); - if (predictionDeleteButton.is(':visible')) { - predictionDeleteButton.hide(); - } -} - -function getLaunchSites() { - $.ajax({ - type: "GET", - url: launches_url, - dataType: "json", - success: function(json) { - sites = json; - generateLaunchSites(); - } - }); -} - -function generateLaunchSites() { - for (var key in sites) { - if (sites.hasOwnProperty(key)) { - var latlon = [sites[key].position[1], sites[key].position[0]]; - var sondesList = ""; - var popupContent = "
"; - var div = document.createElement('div'); - div.id = "popup" + key; - var ascent_rate = 5; - var descent_rate = 6; - var burst_altitude = 26000; - var burst_samples = ""; - var descent_samples = ""; - var marker = new L.circleMarker(latlon, {color: '#696969', fillColor: "white", radius: 8}); - var popup = new L.popup({ autoClose: false, closeOnClick: false }); - marker.title = key; - marker.bindPopup(popup); - launches.addLayer(marker); - - // Match sonde codes - if (sites[key].hasOwnProperty('rs_types')) { - var sondes = sites[key].rs_types; - for (var y = 0; y < sondes.length; y++) { - if (Array.isArray(sondes[y]) == false) { - sondes[y] = [sondes[y]]; - } - if (sondeCodes.hasOwnProperty(sondes[y][0])) { - sondesList += sondeCodes[sondes[y][0]] - if (sondes[y].length > 1) { - sondesList += " (" + sondes[y][1] + " MHz)"; - } - } else if (unsupportedSondeCodes.hasOwnProperty(sondes[y][0])) { - sondesList += unsupportedSondeCodes[sondes[y][0]]; - sondesList += " (cannot track)"; - } else { - sondesList += sondes[y][0] + " (unknown WMO code)"; - } - if (y < sondes.length-1) { - sondesList += ", "; - } - } - if (sondes.includes("11") || sondes.includes("82")) { //LMS6 - ascent_rate = 5; - descent_rate = 2.5; - burst_altitude = 33500; - } - popupContent += "" + sites[key].station_name + "

Sondes launched: " + sondesList; - } - - // Generate prefilled suggestion form - var popupLink = "https://docs.google.com/forms/d/e/1FAIpQLSfIbBSQMZOXpNE4VpK4BqUbKDPCWCDgU9QxYgmhh-JD-JGSsQ/viewform?usp=pp_url&entry.796606853=Modify+Existing+Site"; - popupLink += "&entry.749833526=" + key; - if (sites[key].hasOwnProperty('station_name')) { - popupLink += "&entry.675505431=" + sites[key].station_name.replace(/\s/g, '+'); - } - if (sites[key].hasOwnProperty('position')) { - popupLink += "&entry.1613779787=" + sites[key].position.reverse().toString(); - } - if (sites[key].hasOwnProperty('alt')) { - popupLink += "&entry.753148337=" + sites[key].alt; - } - if (sites[key].hasOwnProperty('ascent_rate')) { - popupLink += "&entry.509146334=" + sites[key]["ascent_rate"]; - } - if (sites[key].hasOwnProperty('burst_altitude')) { - popupLink += "&entry.1897602989=" + sites[key]["burst_altitude"]; - } - if (sites[key].hasOwnProperty('descent_rate')) { - popupLink += "&entry.267462486=" + sites[key]["descent_rate"]; - } - if (sites[key].hasOwnProperty('notes')) { - popupLink += "&entry.197384117=" + sites[key]["notes"].replace(/\s/g, '+'); - } - - // Update prediction data if provided - if (sites[key].hasOwnProperty('ascent_rate')) { - ascent_rate = sites[key]["ascent_rate"]; - } - if (sites[key].hasOwnProperty('descent_rate')) { - descent_rate = sites[key]["descent_rate"]; - } - if (sites[key].hasOwnProperty('burst_altitude')) { - burst_altitude = sites[key]["burst_altitude"]; - } - if (sites[key].hasOwnProperty('burst_samples')) { - burst_samples = sites[key]["burst_samples"]; - } - if (sites[key].hasOwnProperty('descent_samples')) { - descent_samples = sites[key]["descent_samples"]; - } + }, + + createShadow: function () { + return null; + } + }); - // Process launch schedule if provided - if (sites[key].hasOwnProperty('times')) { - popupContent += "
Launch schedule:"; - for (var x = 0; x < sites[key]['times'].length; x++) { - popupContent += "
- "; - var day = sites[key]['times'][x].split(":")[0]; - if (day == 0) { - popupContent += "Everyday at "; - } else if (day == 1) { - popupContent += "Monday at "; - } else if (day == 2) { - popupContent += "Tuesday at "; - } else if (day == 3) { - popupContent += "Wednesday at "; - } else if (day == 4) { - popupContent += "Thursday at "; - } else if (day == 5) { - popupContent += "Friday at "; - } else if (day == 6) { - popupContent += "Saturday at "; - } else if (day == 7) { - popupContent += "Sunday at "; - } - popupContent += sites[key]['times'][x].split(":")[1] + ":" + sites[key]['times'][x].split(":")[2] + " UTC"; - } - } - - // Show notes if provided - if (sites[key].hasOwnProperty('notes')) { - popupContent += "
Notes: " + sites[key]["notes"]; - } - - popupContent += "
Know when this site launches? Contribute here"; + map.whenReady(callBack); - // Generate view historical button - popupContent += "
"; + // animate-in the timebox, + setTimeout(function() { + var elm = $("#timebox"); - // Create prediction button - if (sites[key].hasOwnProperty('times')) { - popupContent += ""; - } else { - popupContent += ""; - } + //if(is_mobile) $(".slickbox").css({left:'5px'}); + var origW = elm.width(); + var iconW = elm.find("svg").width(); - popupContent += ""; + if(offline.get('opt_hide_timebox')) { + elm.removeClass('animate').hide(); + $("#lookanglesbox").css({top:'7px'}); + } - div.innerHTML = popupContent; + // prep for animation + $(".slickbox.animate").css({width:iconW}).find("span").hide(); - popup.setContent(div.innerHTML); + if(!offline.get('opt_hide_timebox')) { + // animate timebox + elm.fadeIn(500,"easeOut").animate({width:origW},400,"easeOut", function() { + $("#timebox span").fadeIn(500, "easeOut"); + }); } - } - if (focusID != 0) { - gotoSite(); - } + + // animate lookanglesbox, delayed start by 300ms + $("#lookanglesbox").delay(200).fadeIn(500,"easeOut").animate({width:origW},400,"easeOut", function() { + if(GPS_ts === null) { + $("#lookanglesbox .nopos").fadeIn(500, "easeOut"); + } else if($("#lookanglesbox span:first").is(":hidden")) { + $("#lookanglesbox .nofollow").fadeIn(500, "easeOut"); + } + }); + + // if we there is enough screen space open aboutbox on startup + if(!is_mobile && !offline.get('opt_nowelcome') && $(window).width() > 900) $('.nav li.about').click(); + + }, 500); } -function gotoSite() { - if (sites != null) { - if (sites.hasOwnProperty(focusID)) { - var site = sites[focusID]; - var latlng = new L.LatLng(site["position"][0], site["position"][1]); - map.setView(latlng, 9); - for (var i in launches._layers) { - marker = launches._layers - if (marker[i].title == focusID) { - marker[i].openPopup(); - } - } - } - } +function setTimeValue() { + setTimeout(function() { + document.getElementById("timeperiod").value = wvar.mode; + }, 100); } function shareVehicle(callsign) { @@ -1730,75 +1064,27 @@ function guess_name(key) { } function habitat_data(jsondata, alternative) { - var keys = { - "ascentrate": "Ascent Rate", - "battery_percent": "Battery", - "temperature_external": "Temperature, External", - "pressure_internal": "Pressure, Internal", - "voltage_solar_1": "Voltage, Solar 1", - "voltage_solar_2": "Voltage, Solar 2", - "light_red": "Light (Red)", - "light_green": "Light (Green)", - "light_blue": "Light (Blue)", - "gas_a": "Gas (A)", - "gas_b": "Gas (B)", - "gas_co2": "Gas (CO)", - "gas_combustible": "Gas (Combustible)", - "radiation": "Radiation (CPM)", - "temperature_radio": "Temperature, Radio", - "uplink_rssi": "Uplink RSSI", - "light_intensity": "Light Intensity", - "pred_lat": "Onboard Prediction (Lat)", - "pred_lon": "Onboard Prediction (Lon)", - "batt": "Battery Voltage", - "sats": "GNSS SVs Used", - "humidity": "Relative Humidity", - "subtype": "Sonde Sub-type", - "frequency": "Frequency", - "frequency_tx": "TX Frequency", - "manufacturer": "Manufacturer", - "type": "Sonde Type", - "burst_timer": "Burst Timer", - "xdata": "XDATA" - }; + var keys = globalKeys; var tooltips = { "burst_timer": "If active, this indicates the time (HH:MM:SS) until the radiosonde will automatically power-off.", - "xdata": "Raw auxiliary data (as hexadecimal) from an external sensor package (often an Ozone sensor)." + "xdata": "Raw auxiliary data (as hexadecimal) from an external sensor package (often an Ozone sensor).", + "oif411_O3_partial_pressure": "Estimated O3 partial pressure, using nominal calibration values. +/- 1 mPa." } var hide_keys = { "spam": true, "battery_millivolts": true, "temperature_internal_x10": true, - "uplink_rssi_raw": true + "uplink_rssi_raw": true, + "oif411_instrument_number": true, + "oif411_ext_voltage": true, + "cfh_instrument_number": true, + "cobald_instrument_number": true, + "cobald_sonde_number": true }; - var suffixes = { - "current": " A", - "battery": " V", - "batt": " V", - "solar_panel": " V", - "temperature": "°C", - "temperature_internal": "°C", - "temperature_external": "°C", - "temperature_radio": "°C", - "pressure": " hPa", - "voltage_solar_1": " V", - "voltage_solar_2": " V", - "battery_percent": "%", - "uplink_rssi": " dBm", - "rssi_last_message": " dBm", - "rssi_floor": " dBm", - "bearing": "°", - "iss_azimuth": "°", - "iss_elevation": "°", - "light_intensity": " lx", - "humidity": " %", - "frequency": " MHz", - "frequency_tx": " MHz", - "spam": "" - }; + var suffixes = globalSuffixes; try { @@ -1808,6 +1094,7 @@ function habitat_data(jsondata, alternative) { var array = []; var output = ""; var txFreq = false + var xdataFound = false if(Object.keys(data).length === 0) return ""; @@ -1815,15 +1102,19 @@ function habitat_data(jsondata, alternative) { txFreq = true } + if ("xdata_instrument" in data) { + xdataFound = true + } + for(var key in data) { - if (key === "frequency" && txFreq) {} else { + if ((key === "frequency" && txFreq) || (key === "xdata" && xdataFound)) {} else { array.push([key, data[key]]); } } - array.sort(function(a, b) { - return a[0].localeCompare(b[0]); - }); + //array.sort(function(a, b) { + // return a[0].localeCompare(b[0]); + //}); for(var i = 0, ii = array.length; i < ii; i++) { var k = array[i][0]; // key @@ -3113,6 +2404,9 @@ function mapInfoBox_handle_path_new(data, vehicle, date) { if (data.hasOwnProperty("manufacturer")) { html += "
Manufacturer: " + data.manufacturer + "
"; }; + if (data.hasOwnProperty("pressure")) { + html += "
Pressure: " + data.pressure + " Pa
"; + }; if (data.hasOwnProperty("sats")) { html += "
Satellites: " + data.sats + "
"; }; @@ -3124,11 +2418,38 @@ function mapInfoBox_handle_path_new(data, vehicle, date) { } else if (data.hasOwnProperty("type")) { html += "
Sonde Type: " + data.type + "
"; }; - if (data.hasOwnProperty("pressure")) { - html += "
Pressure: " + data.pressure + " Pa
"; - }; if (data.hasOwnProperty("xdata")) { + html += "
"; + html += "
" html += "
XDATA: " + data.xdata + "
"; + if (data.hasOwnProperty("pressure")) { + xdata_pressure = data.pressure; + } else { + xdata_pressure = 1100.0; + } + var tempXDATA = parseXDATA(data.xdata, xdata_pressure); + for (let field in tempXDATA) { + if (tempXDATA.hasOwnProperty(field)) { + if (field == "xdata_instrument") { + html += "
XDATA Instrument: " + tempXDATA.xdata_instrument.join(', ') + "
"; + } else { + if (globalKeys.hasOwnProperty(field)) { + if (globalSuffixes.hasOwnProperty(field)) { + html += "
" + globalKeys[field] + ": " + tempXDATA[field] + globalSuffixes[field] + "
"; + } else { + html += "
" + globalKeys[field] + ": " + tempXDATA[field] + "
"; + } + + } else { + if (globalSuffixes.hasOwnProperty(field)) { + html += "
" + guess_name(field) + ": " + tempXDATA[field] + globalSuffixes[field] + "
"; + } else { + html += "
" + guess_name(field) + ": " + tempXDATA[field] + "
"; + } + } + } + } + } }; html += "
"; @@ -3851,7 +3172,7 @@ function addPosition(position) { // Graph Stuff -var graph_inhibited_fields = ['frequency', 'frequency_tx', 'burst_timer']; +var graph_fields = ['altitude', 'pred.alt', 'batt', 'humidity', 'pressure', 'sats', 'temperature_external', 'oif411_O3_partial_pressure']; function updateGraph(vcallsign, reset_selection) { if(!plot || !plot_open) return; @@ -4025,7 +3346,7 @@ function graphAddPosition(vcallsign, new_data) { i = (k in vehicle.graph_data_map) ? vehicle.graph_data_map[k] : data.length; // Disable plotting of a few fields. - if (graph_inhibited_fields.includes(k)){ + if (!(graph_fields.includes(k))){ return; } @@ -4097,366 +3418,6 @@ function graphAddPosition(vcallsign, new_data) { } } -function formatData(data, live) { - var response = {}; - response.positions = {}; - var dataTemp = []; - if (live) { - if (data.length) { - for (let entry in data) { - var dataTempEntry = {}; - var station = data[entry].uploader_callsign; - dataTempEntry.callsign = {}; - //check if other stations also received this packet - if (vehicles.hasOwnProperty(data[entry].serial)) { - if (data[entry].datetime == vehicles[data[entry].serial].curr_position.gps_time) { - for (let key in vehicles[data[entry].serial].curr_position.callsign) { - if (vehicles[data[entry].serial].curr_position.callsign.hasOwnProperty(key)) { - if (key != station) { - dataTempEntry.callsign[key] = {}; - if (vehicles[data[entry].serial].curr_position.callsign[key].hasOwnProperty("snr")) { - dataTempEntry.callsign[key].snr = vehicles[data[entry].serial].curr_position.callsign[key].snr; - } - if (vehicles[data[entry].serial].curr_position.callsign[key].hasOwnProperty("rssi")) { - dataTempEntry.callsign[key].rssi = vehicles[data[entry].serial].curr_position.callsign[key].rssi; - } - if (vehicles[data[entry].serial].curr_position.callsign[key].hasOwnProperty("frequency")) { - dataTempEntry.callsign[key].frequency = vehicles[data[entry].serial].curr_position.callsign[key].frequency; - } - } - } - } - } - } - dataTempEntry.callsign[station] = {}; - if (data[entry].snr) { - dataTempEntry.callsign[station].snr = data[entry].snr; - } - if (data[entry].rssi) { - dataTempEntry.callsign[station].rssi = data[entry].rssi; - } - if (data[entry].frequency) { - dataTempEntry.callsign[station].frequency = data[entry].frequency; - } - dataTempEntry.gps_alt = data[entry].alt; - dataTempEntry.gps_lat = data[entry].lat; - dataTempEntry.gps_lon = data[entry].lon; - if (data[entry].heading) { - dataTempEntry.gps_heading = data[entry].heading; - } - dataTempEntry.gps_time = data[entry].datetime; - dataTempEntry.server_time = data[entry].datetime; - dataTempEntry.vehicle = data[entry].serial; - dataTempEntry.position_id = data[entry].serial + "-" + data[entry].datetime; - dataTempEntry.data = {}; - if (data[entry].batt) { - dataTempEntry.data.batt = data[entry].batt; - } - if (data[entry].burst_timer) { - dataTempEntry.data.burst_timer = data[entry].burst_timer; - } - if (data[entry].frequency) { - dataTempEntry.data.frequency = data[entry].frequency; - } - if (data[entry].tx_frequency) { - dataTempEntry.data.frequency_tx = data[entry].tx_frequency; - } - if (data[entry].hasOwnProperty("humidity")) { - dataTempEntry.data.humidity = data[entry].humidity; - } - if (data[entry].manufacturer) { - dataTempEntry.data.manufacturer = data[entry].manufacturer; - } - if (data[entry].sats) { - dataTempEntry.data.sats = data[entry].sats; - } - if (data[entry].hasOwnProperty("temp")) { - dataTempEntry.data.temperature_external = data[entry].temp; - } - if (data[entry].type) { - dataTempEntry.data.type = data[entry].type; - dataTempEntry.type = data[entry].type; - } - if (data[entry].subtype) { - dataTempEntry.data.type = data[entry].subtype; - dataTempEntry.type = data[entry].subtype; - } - if (data[entry].hasOwnProperty("pressure")) { - dataTempEntry.data.pressure = data[entry].pressure; - } - if (data[entry].xdata) { - dataTempEntry.data.xdata = data[entry].xdata; - } - if (data[entry].serial.toLowerCase() != "xxxxxxxx") { - dataTemp.push(dataTempEntry); - } - } - } else { - var dataTempEntry = {}; - var station = data.uploader_callsign; - dataTempEntry.callsign = {}; - //check if other stations also received this packet - if (vehicles.hasOwnProperty(data.serial)) { - if (data.datetime == vehicles[data.serial].curr_position.gps_time) { - for (let key in vehicles[data.serial].curr_position.callsign) { - if (vehicles[data.serial].curr_position.callsign.hasOwnProperty(key)) { - if (key != station) { - dataTempEntry.callsign[key] = {}; - if (vehicles[data.serial].curr_position.callsign[key].hasOwnProperty("snr")) { - dataTempEntry.callsign[key].snr = vehicles[data.serial].curr_position.callsign[key].snr; - } - if (vehicles[data.serial].curr_position.callsign[key].hasOwnProperty("rssi")) { - dataTempEntry.callsign[key].rssi = vehicles[data.serial].curr_position.callsign[key].rssi; - } - if (vehicles[data.serial].curr_position.callsign[key].hasOwnProperty("frequency")) { - dataTempEntry.callsign[key].frequency = vehicles[data.serial].curr_position.callsign[key].frequency; - } - } - } - } - } - } - dataTempEntry.callsign[station] = {}; - if (data.snr) { - dataTempEntry.callsign[station].snr = data.snr; - } - if (data.rssi) { - dataTempEntry.callsign[station].rssi = data.rssi; - } - if (data.frequency) { - dataTempEntry.callsign[station].frequency = data.frequency; - } - dataTempEntry.gps_alt = data.alt; - dataTempEntry.gps_lat = data.lat; - dataTempEntry.gps_lon = data.lon; - if (data.heading) { - dataTempEntry.gps_heading = data.heading; - } - dataTempEntry.gps_time = data.datetime; - dataTempEntry.server_time = data.datetime; - dataTempEntry.vehicle = data.serial; - dataTempEntry.position_id = data.serial + "-" + data.datetime; - dataTempEntry.data = {}; - if (data.batt) { - dataTempEntry.data.batt = data.batt; - } - if (data.burst_timer) { - dataTempEntry.data.burst_timer = data.burst_timer; - } - if (data.frequency) { - dataTempEntry.data.frequency = data.frequency; - } - if (data.tx_frequency) { - dataTempEntry.data.frequency_tx = data.tx_frequency; - } - if (data.hasOwnProperty("humidity")) { - dataTempEntry.data.humidity = data.humidity; - } - if (data.manufacturer) { - dataTempEntry.data.manufacturer = data.manufacturer; - } - if (data.sats) { - dataTempEntry.data.sats = data.sats; - } - if (data.hasOwnProperty("temp")) { - dataTempEntry.data.temperature_external = data.temp; - } - if (data.type) { - dataTempEntry.data.type = data.type; - dataTempEntry.type = data.type; - } - if (data.subtype) { - dataTempEntry.data.type = data.subtype; - dataTempEntry.type = data.subtype; - } - if (data.hasOwnProperty("pressure")) { - dataTempEntry.data.pressure = data.pressure; - } - if (data.xdata) { - dataTempEntry.data.xdata = data.xdata; - } - if (data.serial.toLowerCase() != "xxxxxxxx") { - dataTemp.push(dataTempEntry); - } - } - } else if (data.length == null) { - for (let key in data) { - if (data.hasOwnProperty(key)) { - if (typeof data[key] === 'object') { - for (let i in data[key]) { - var dataTempEntry = {}; - var station = data[key][i].uploader_callsign; - dataTempEntry.callsign = {}; - dataTempEntry.callsign[station] = {}; - if (data[key][i].snr) { - dataTempEntry.callsign[station].snr = data[key][i].snr; - } - if (data[key][i].rssi) { - dataTempEntry.callsign[station].rssi = data[key][i].rssi; - } - if (data[key][i].frequency) { - dataTempEntry.callsign[station].frequency = data[key][i].frequency; - } - dataTempEntry.gps_alt = data[key][i].alt; - dataTempEntry.gps_lat = data[key][i].lat; - dataTempEntry.gps_lon = data[key][i].lon; - if (data[key][i].heading) { - dataTempEntry.gps_heading = data[key][i].heading; - } - dataTempEntry.gps_time = data[key][i].datetime; - dataTempEntry.server_time = data[key][i].datetime; - dataTempEntry.vehicle = data[key][i].serial; - dataTempEntry.position_id = data[key][i].serial + "-" + data[key][i].datetime; - dataTempEntry.data = {}; - if (data[key][i].batt) { - dataTempEntry.data.batt = data[key][i].batt; - } - if (data[key][i].burst_timer) { - dataTempEntry.data.burst_timer = data[key][i].burst_timer; - } - if (data[key][i].frequency) { - dataTempEntry.data.frequency = data[key][i].frequency; - } - if (data[key][i].tx_frequency) { - dataTempEntry.data.frequency_tx = data[key][i].tx_frequency; - } - if (data[key][i].hasOwnProperty("humidity")) { - dataTempEntry.data.humidity = data[key][i].humidity; - } - if (data[key][i].manufacturer) { - dataTempEntry.data.manufacturer = data[key][i].manufacturer; - } - if (data[key][i].sats) { - dataTempEntry.data.sats = data[key][i].sats; - } - if (data[key][i].hasOwnProperty("temp")) { - dataTempEntry.data.temperature_external = data[key][i].temp; - } - if (data[key][i].type) { - dataTempEntry.data.type = data[key][i].type; - dataTempEntry.type = data[key][i].type; - } - if (data[key][i].subtype) { - dataTempEntry.data.type = data[key][i].subtype; - dataTempEntry.type = data[key][i].subtype; - } - if (data[key][i].hasOwnProperty("pressure")) { - dataTempEntry.data.pressure = data[key][i].pressure; - } - if (data[key][i].xdata) { - dataTempEntry.data.xdata = data[key][i].xdata; - } - if (data[key][i].serial.toLowerCase() != "xxxxxxxx") { - dataTemp.push(dataTempEntry); - } - } - } - } - } - } else { - for (var i = data.length - 1; i >= 0; i--) { - if (data[i].hasOwnProperty('subtype') && data[i].subtype == "SondehubV1") { - var dataTempEntry = {}; - var station = data[i].uploader_callsign; - dataTempEntry.callsign = {}; - dataTempEntry.callsign[station] = {}; - dataTempEntry.gps_alt = parseFloat(data[i].alt); - dataTempEntry.gps_lat = parseFloat(data[i].lat); - dataTempEntry.gps_lon = parseFloat(data[i].lon); - dataTempEntry.gps_time = data[i].time_received; - dataTempEntry.server_time = data[i].time_received; - dataTempEntry.vehicle = data[i].serial; - dataTempEntry.position_id = data[i].serial + "-" + data[i].time_received; - dataTempEntry.data = {}; - if (data[i].humidity) { - dataTempEntry.data.humidity = parseFloat(data[i].humidity); - } - if (data[i].temp) { - dataTempEntry.data.temperature_external = parseFloat(data[i].temp); - } - dataTemp.push(dataTempEntry); - } else { - var dataTempEntry = {}; - var station = data[i].uploader_callsign; - dataTempEntry.callsign = {}; - dataTempEntry.callsign[station] = {}; - if (data[i].snr) { - dataTempEntry.callsign[station].snr = data[i].snr; - } - if (data[i].rssi) { - dataTempEntry.callsign[station].rssi = data[i].rssi; - } - if (data[i].frequency) { - dataTempEntry.callsign[station].frequency = data[i].frequency; - } - dataTempEntry.gps_alt = data[i].alt; - dataTempEntry.gps_lat = data[i].lat; - dataTempEntry.gps_lon = data[i].lon; - if (data[i].heading) { - dataTempEntry.gps_heading = data[i].heading; - } - dataTempEntry.gps_time = data[i].datetime; - dataTempEntry.server_time = data[i].datetime; - dataTempEntry.vehicle = data[i].serial; - dataTempEntry.position_id = data[i].serial + "-" + data[i].datetime; - dataTempEntry.data = {}; - if (data[i].batt) { - dataTempEntry.data.batt = data[i].batt; - } - if (data[i].burst_timer) { - dataTempEntry.data.burst_timer = data[i].burst_timer; - } - if (data[i].frequency) { - dataTempEntry.data.frequency = data[i].frequency; - } - if (data[i].tx_frequency) { - dataTempEntry.data.frequency_tx = data[i].tx_frequency; - } - if (data[i].hasOwnProperty("humidity")) { - dataTempEntry.data.humidity = data[i].humidity; - } - if (data[i].manufacturer) { - dataTempEntry.data.manufacturer = data[i].manufacturer; - } - if (data[i].sats) { - dataTempEntry.data.sats = data[i].sats; - } - if (data[i].hasOwnProperty("temp")) { - dataTempEntry.data.temperature_external = data[i].temp; - } - if (data[i].type && data[i].type == "payload_telemetry") { // SondeHub V1 data - var comment = data[i].comment.split(" "); - if (v1types.hasOwnProperty(comment[0])) { - dataTempEntry.data.type = v1types[comment[0]]; - dataTempEntry.type = v1types[comment[0]]; - if (v1manufacturers.hasOwnProperty(dataTempEntry.type)) { - dataTempEntry.data.manufacturer = v1manufacturers[dataTempEntry.type]; - } - } - dataTempEntry.data.frequency = comment[2]; - } else if (data[i].type) { - dataTempEntry.data.type = data[i].type; - dataTempEntry.type = data[i].type; - } - if (data[i].subtype) { - dataTempEntry.data.type = data[i].subtype; - dataTempEntry.type = data[i].subtype; - } - if (data[i].hasOwnProperty("pressure")) { - dataTempEntry.data.pressure = data[i].pressure; - } - if (data[i].xdata) { - dataTempEntry.data.xdata = data[i].xdata; - } - dataTemp.push(dataTempEntry); - } - } - } - response.positions.position = dataTemp; - response.fetch_timestamp = Date.now(); - return response; -} - var ajax_positions = null; var ajax_positions_single = null; var ajax_positions_single_new = null; @@ -4623,6 +3584,7 @@ function refreshSingle(serial) { success: function(data, textStatus) { response = formatData(data, false); update(response); + singleRecovery(serial); $("#stText").text(""); }, error: function() { @@ -4752,6 +3714,22 @@ function refreshNewReceivers(initial, serial) { }); } +function singleRecovery(serial) { + + var datastr = "serial=" + serial; + + $.ajax({ + type: "GET", + url: recovered_sondes_url, + data: datastr, + dataType: "json", + success: function(response, textStatus) { + updateRecoveries(response); + } + }); + +} + function refreshRecoveries() { $.ajax({ @@ -4829,60 +3807,6 @@ function refreshPredictions() { }); } -// Get initial summary data for station, courtesy of TimMcMahon -function getHistorical (id, callback, continuation) { - var prefix = 'launchsites/' + id + '/'; - var params = { - Prefix: prefix, - }; - - if (typeof continuation !== 'undefined') { - params.ContinuationToken = continuation; - } else { - tempLaunchData = {}; - } - - s3.makeUnauthenticatedRequest('listObjectsV2', params, function(err, data) { - if (err) { - console.log(err, err.stack); - } else { - var tempSerials = []; - for (var i = 0; i < data.Contents.length; i++) { - // Sort data into year and month groups - var date = data.Contents[i].Key.substring(prefix.length).substring(0,10); - var year = date.substring(0,4); - var month = date.substring(5,7); - var serial = data.Contents[i].Key.substring(prefix.length+11).slice(0, -5); - if (tempLaunchData.hasOwnProperty(year)) { - if (tempLaunchData[year].hasOwnProperty(month)) { - if (!tempSerials.includes(serial)) { - tempSerials.push(serial) - tempLaunchData[year][month].push(data.Contents[i].Key); - } - } else { - tempLaunchData[year][month] = []; - tempSerials = []; - tempSerials.push(serial) - tempLaunchData[year][month].push(data.Contents[i].Key); - } - } else { - tempLaunchData[year] = {}; - tempLaunchData[year][month] = []; - tempSerials = []; - tempSerials.push(serial) - tempLaunchData[year][month].push(data.Contents[i].Key); - } - } - if (data.IsTruncated == true) { - // Requests are limited to 1000 entries so multiple may be required - getHistorical(id, callback, data.NextContinuationToken); - } else { - callback(tempLaunchData); - } - } - }); -} - var periodical, periodical_focus, periodical_focus_new, periodical_receivers, periodical_listeners; var periodical_predictions = null; var timer_seconds = 5; diff --git a/js/xdata.js b/js/xdata.js new file mode 100644 index 0000000..a000467 --- /dev/null +++ b/js/xdata.js @@ -0,0 +1,638 @@ +/* SondeHub XDATA Parser Library + * + * Authors: Mark Jessop & Luke Prior + */ + +// Pump Efficiency Correction Parameters for ECC-6A Ozone Sensor, with 3.0cm^3 volume. +// We are using these as a nominal correction value for pump efficiency vs pressure +// +OIF411_Cef_Pressure = [ 0, 2, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500, 1000, 1100]; +OIF411_Cef = [ 1.171, 1.171, 1.131, 1.092, 1.055, 1.032, 1.022, 1.015, 1.011, 1.008, 1.006, 1.004, 1, 1]; + +function lerp(x, y, a){ + // Helper function for linear interpolation between two points + return x * (1 - a) + y * a +} + + +function get_oif411_Cef(pressure){ + // Get the Pump efficiency correction value for a given pressure. + + // Off-scale use bottom-end value + if (pressure <= OIF411_Cef_Pressure[0]){ + return OIF411_Cef[0]; + } + + // Off-scale top, use top-end value + if (pressure >= OIF411_Cef_Pressure[OIF411_Cef_Pressure.length-1]){ + return OIF411_Cef[OIF411_Cef.length-1]; + } + + + // Within the correction range, perform linear interpolation. + for(i= 1; i 0) { + _ozone_pump_temp = _ozone_pump_temp - 0x10000; + } + _ozone_pump_temp = _ozone_pump_temp*0.01; // Degrees C (5 - 35) + _output['oif411_ozone_pump_temp'] = Math.round(_ozone_pump_temp * 100) / 100; // 2 DP + + // Ozone Current + _ozone_current_uA = parseInt(xdata.substr(8,5),16)*0.0001; // micro-Amps (0.05 - 30) + _output['oif411_ozone_current_uA'] = Math.round(_ozone_current_uA * 10000) / 10000; // 4 DP + + // Battery Voltage + _ozone_battery_v = parseInt(xdata.substr(13,2),16)*0.1; // Volts (14 - 19) + _output['oif411_ozone_battery_v'] = Math.round(_ozone_battery_v * 10) / 10; // 1 DP + + // Ozone Pump Current + _ozone_pump_curr_mA = parseInt(xdata.substr(15,3),16); // mA (30 - 110) + _output['oif411_ozone_pump_curr_mA'] = Math.round(_ozone_pump_curr_mA * 10) / 10; // 1 DP + + // External Voltage + _ext_voltage = parseInt(xdata.substr(18,2),16)*0.1; // Volts + _output['oif411_ext_voltage'] = Math.round(_ext_voltage * 10) / 10; // 1 DP + + // Now attempt to calculate the O3 partial pressure + + // Calibration values + Ibg = 0.0; // The BOM appear to use a Ozone background current value of 0 uA + Cef = get_oif411_Cef(pressure); // Calculate the pump efficiency correction. + FlowRate = 28.5; // Use a 'nominal' value for Flow Rate (seconds per 100mL). + + _O3_partial_pressure = (4.30851e-4)*(_output['oif411_ozone_current_uA'] - Ibg)*(_output['oif411_ozone_pump_temp']+273.15)*FlowRate*Cef; // mPa + _output['oif411_O3_partial_pressure'] = Math.round(_O3_partial_pressure * 1000) / 1000; // 3 DP + } + + return _output +} + +function parseCOBALD(xdata) { + // Attempt to parse an XDATA string from a COBALD Compact Optical Backscatter Aerosol Detector + // Returns an object with parameters to be added to the sondes telemetry. + // + // References: + // https://hobbydocbox.com/Radio/83430839-Cobald-operating-instructions-imet-configuration.html + // + // Sample data: 190213fffe005fcf00359943912cca (length = 30 characters) + + // Run some checks over the input + if(xdata.length != 30){ + // Invalid COBALD dataset + return {}; + } + + if(xdata.substr(0,2) !== '19'){ + // Not a COBALD (shouldn't get here) + return {}; + } + + var _output = {}; + + // Instrument number is common to all XDATA types. + _output['cobald_instrument_number'] = parseInt(xdata.substr(2,2),16); + + // Sonde number + _output['cobald_sonde_number'] = parseInt(xdata.substr(4,3),16); + + // Internal temperature + _internal_temperature = parseInt(xdata.substr(7,3),16); + if ((_internal_temperature & 0x800) > 0) { + _internal_temperature = _internal_temperature - 0x1000; + } + _internal_temperature = _internal_temperature/8; // Degrees C (-40 - 50) + _output['cobald_internal_temperature'] = Math.round(_internal_temperature * 100) / 100; // 2 DP + + // Blue backscatter + _blue_backscatter = parseInt(xdata.substr(10,6),16); + if ((_blue_backscatter & 0x800000) > 0) { + _blue_backscatter = _blue_backscatter - 0x1000000; + } + _output['cobald_blue_backscatter'] = _blue_backscatter; // (0 - 1000000) + + // Red backckatter + _red_backscatter = parseInt(xdata.substr(16,6),16); + if ((_red_backscatter & 0x800000) > 0) { + _red_backscatter = _red_backscatter - 0x1000000; + } + _output['cobald_red_backscatter'] = _red_backscatter; // (0 - 1000000) + + // Blue monitor + _blue_monitor = parseInt(xdata.substr(22,4),16); + if ((_blue_monitor & 0x8000) > 0) { + _blue_monitor = _blue_monitor - 0x10000; + } + _output['cobald_blue_monitor'] = _blue_monitor; // (-32768 - 32767) + + // Red monitor + _red_monitor = parseInt(xdata.substr(26,4),16); + if ((_red_monitor & 0x8000) > 0) { + _red_monitor = _red_monitor - 0x10000; + } + _output['cobald_red_monitor'] = _red_monitor; // (-32768 - 32767) + + return _output +} + +function getPCFHdate(code) { + // months reference list + var PCFHmonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + // Get year from first character + var year = parseInt(code.charAt(0),16); + year = year + 2016; + + // Get month from second character + var month = parseInt(code.charAt(1),16); + month = PCFHmonths[month-1]; + + // Generate string + _part_date = month + " " + year; + return _part_date; +} + +function parsePCFH(xdata) { + // Attempt to parse an XDATA string from a Peltier Cooled Frost point Hygrometer (PCFH) + // Returns an object with parameters to be added to the sondes telemetry. + // + // References: + // Peltier Cooled Frost point Hygrometer (PCFH) Telemetry Interface PDF + // + // Sample data: 3c0101434a062c5cd4a5747b81486c93 (length = 32 characters) + // 3c0103456076175ec5fc9df9b1 (length = 26 characters) + // 3c0104a427104e203a9861a8ab6a65 (length = 30 characters) + // 3c010000011b062221 (length = 18 characters) + + // Run some checks over the input + if(xdata.length > 32){ + // Invalid PCFH dataset + return {}; + } + + if(xdata.substr(0,2) !== '3C'){ + // Not a PCFH (shouldn't get here) + return {}; + } + + var _output = {}; + + // Instrument number is common to all XDATA types. + _output['pcfh_instrument_number'] = parseInt(xdata.substr(2,2),16); + + // Packet ID + var packetID = xdata.substr(4,2); + + // Packet type + if (packetID == "00") { // Individual instrument identification (10 s) + // Serial number + _output["pcfh_serial_number"] = parseInt(xdata.substr(6,4)); + + // Temperature PCB date + _output["pcfh_temperature_pcb_date"] = getPCFHdate(xdata.substr(10,2)); + + // Main PCB date + _output["pcfh_main_pcb_date"] = getPCFHdate(xdata.substr(12,2)); + + // Controller FW date + _output["pcfh_controller_fw_date"] = getPCFHdate(xdata.substr(14,2)); + + // FPGA FW date + _output["pcfh_fpga_fw_date"] = getPCFHdate(xdata.substr(16,2)); + } else if (packetID == "01" || packetID == "02") { // Regular one second data, sub-sensor 1/2 + // Frost point mirror temperature + _frost_point_mirror_temperature = parseInt(xdata.substr(8,3),16); + _frost_point_mirror_temperature = (_frost_point_mirror_temperature*0.05) - 125; + _output['pcfh_frost_point_mirror_temperature_' + packetID] = Math.round(_frost_point_mirror_temperature * 100) / 100; // 2 DP + + // Peltier hot side temperature + _peltier_hot_side_temperature = parseInt(xdata.substr(11,3),16); + _peltier_hot_side_temperature = (_peltier_hot_side_temperature*0.05) - 125; + _output['pcfh_peltier_hot_side_temperature_' + packetID] = Math.round(_peltier_hot_side_temperature * 100) / 100; // 2 DP + + // Air temperature + _air_temperature = parseInt(xdata.substr(14,3),16); + _air_temperature = (_air_temperature*0.05) - 125; + _output['pcfh_air_temperature_' + packetID] = Math.round(_air_temperature * 100) / 100; // 2 DP + + // Anticipated frost point mirror temperature + _anticipated_frost_point_mirror_temperature = parseInt(xdata.substr(17,3),16); + _anticipated_frost_point_mirror_temperature = (_anticipated_frost_point_mirror_temperature*0.05) - 125; + _output['pcfh_anticipated_frost_point_mirror_temperature_' + packetID] = Math.round(_anticipated_frost_point_mirror_temperature * 100) / 100; // 2 DP + + // Frost point mirror reflectance + _frost_point_mirror_reflectance = parseInt(xdata.substr(20,4),16); + _frost_point_mirror_reflectance = _frost_point_mirror_reflectance/32768; + _output['pcfh_frost_point_mirror_reflectance_' + packetID] = Math.round(_frost_point_mirror_reflectance * 1000) / 1000; // 3 DP + + // Reference surface reflectance + _reference_surface_reflectance = parseInt(xdata.substr(24,4),16); + _reference_surface_reflectance = _reference_surface_reflectance/32768; + _output['pcfh_reference_surface_reflectance_' + packetID] = Math.round(_reference_surface_reflectance * 1000) / 1000; // 3 DP + + // Reference surface heating current + _reference_surface_heating_current = parseInt(xdata.substr(28,2),16); + _reference_surface_heating_current = _reference_surface_heating_current/2.56; + _output['pcfh_reference_surface_heating_current_' + packetID] = Math.round(_reference_surface_heating_current * 100) / 100; // 2 DP + + // Peltier current + _peltier_current = parseInt(xdata.substr(30,2),16); + if ((_peltier_current & 0x80) > 0) { + _peltier_current = _peltier_current - 0x100; + } + _peltier_current = _peltier_current/64; + _output['pcfh_peltier_current_' + packetID] = Math.round(_peltier_current * 1000) / 1000; // 3 DP + } else if (packetID == "03") { // Regular five second data + // Heat sink temperature 1 + _heat_sink_temperature = parseInt(xdata.substr(8,3),16); + _heat_sink_temperature = (_heat_sink_temperature*0.05) - 125; + _output['pcfh_heat_sink_temperature_01'] = Math.round(_heat_sink_temperature * 100) / 100; // 2 DP + + // Reference surface temperature 1 + _reference_surface_temperature = parseInt(xdata.substr(11,3),16); + _reference_surface_temperature = (_reference_surface_temperature*0.05) - 125; + _output['pcfh_reference_surface_temperature_01'] = Math.round(_reference_surface_temperature * 100) / 100; // 2 DP + + // Heat sink temperature 2 + _heat_sink_temperature = parseInt(xdata.substr(14,3),16); + _heat_sink_temperature = (_heat_sink_temperature*0.05) - 125; + _output['pcfh_heat_sink_temperature_02'] = Math.round(_heat_sink_temperature * 100) / 100; // 2 DP + + // Reference surface temperature 2 + _reference_surface_temperature = parseInt(xdata.substr(17,3),16); + _reference_surface_temperature = (_reference_surface_temperature*0.05) - 125; + _output['pcfh_reference_surface_temperature_02'] = Math.round(_reference_surface_temperature * 100) / 100; // 2 DP + + // Thermocouple reference temperature + _thermocouple_reference_temperature = parseInt(xdata.substr(20,3),16); + _thermocouple_reference_temperature = (_thermocouple_reference_temperature*0.05) - 125; + _output['pcfh_thermocouple_reference_temperature'] = Math.round(_thermocouple_reference_temperature * 100) / 100; // 2 DP + + // Reserved temperature + _reserved_temperature = parseInt(xdata.substr(23,3),16); + _reserved_temperature = (_reserved_temperature*0.05) - 125; + _output['pcfh_reserved_temperature'] = Math.round(_reserved_temperature * 100) / 100; // 2 DP + } else if (packetID == "04") { // Instrument status (10 s) + // Clean frost point mirror reflectance 1 + _clean_frost_point_mirror_reflectance = parseInt(xdata.substr(8,4),16); + _clean_frost_point_mirror_reflectance = _clean_frost_point_mirror_reflectance*0.001; + _output['pcfh_clean_frost_point_mirror_reflectance_01'] = Math.round(_clean_frost_point_mirror_reflectance * 1000) / 1000; // 3 DP + + // Clean reference surface reflectance 1 + _clean_reference_surface_reflectance = parseInt(xdata.substr(12,4),16); + _clean_reference_surface_reflectance = _clean_reference_surface_reflectance*0.001; + _output['pcfh_clean_reference_surface_reflectance_01'] = Math.round(_clean_reference_surface_reflectance * 1000) / 1000; // 3 DP + + // Clean frost point mirror reflectance 2 + _clean_frost_point_mirror_reflectance = parseInt(xdata.substr(16,4),16); + _clean_frost_point_mirror_reflectance = _clean_frost_point_mirror_reflectance*0.001; + _output['pcfh_clean_frost_point_mirror_reflectance_02'] = Math.round(_clean_frost_point_mirror_reflectance * 1000) / 1000; // 3 DP + + // Clean reference surface reflectance 2 + _clean_reference_surface_reflectance = parseInt(xdata.substr(20,4),16); + _clean_reference_surface_reflectance = _clean_reference_surface_reflectance*0.001; + _output['pcfh_clean_reference_surface_reflectance_02'] = Math.round(_clean_reference_surface_reflectance * 1000) / 1000; // 3 DP + + // 6V Analog supply battery voltage + _6v_analog_supply_battery_voltage = parseInt(xdata.substr(24,2),16); + _6v_analog_supply_battery_voltage = (_6v_analog_supply_battery_voltage*0.02) + 2.5; + _output['pcfh_6v_analog_supply_battery_voltage'] = Math.round(_6v_analog_supply_battery_voltage * 100) / 100; // 2 DP + + // 4.5V Logic supply battery voltage + _45v_logic_supply_battery_voltage = parseInt(xdata.substr(26,2),16); + _45v_logic_supply_battery_voltage = (_45v_logic_supply_battery_voltage*0.02) + 2.5; + _output['pcfh_45v_logic_supply_battery_voltage'] = Math.round(_45v_logic_supply_battery_voltage * 100) / 100; // 2 DP + + // 4.5V Peltier and heater supply battery voltage + _45v_peltier_and_heater_supply_battery_voltage = parseInt(xdata.substr(28,2),16); + _45v_peltier_and_heater_supply_battery_voltage = (_45v_peltier_and_heater_supply_battery_voltage*0.02) + 2.5; + _output['pcfh_45v_peltier_and_heater_supply_battery_voltage'] = Math.round(_45v_peltier_and_heater_supply_battery_voltage * 100) / 100; // 2 DP + } + + return _output +} + +function calculateFLASHBWaterVapour(S, B, P, T) { + // This code is incomplete as I don't have reference values + var K1 = 0; + var K2 = 0; + var U = 0; + + var F = S - B + K2*(S-B) + + if (P < 36) { + U = K1*F*0.956*(1+((0.00781*(T+273.16))/P)); + } else if (36 <= 36 < 300) { + U = K1*F*(1 + 0.00031*P) + } + + return U; +} + +function parseFLASHB(xdata, pressure, temperature) { + // Attempt to parse an XDATA string from a Fluorescent Lyman-Alpha Stratospheric Hygrometer for Balloon (FLASH-B) + // Returns an object with parameters to be added to the sondes telemetry. + // + // + // Sample data: 3D0204E20001407D00E4205DC24406B1012 (length = 35 characters) + + // Run some checks over the input + if(xdata.length != 35){ + // Invalid FLASH-B dataset + return {}; + } + + if(xdata.substr(0,2) !== '3D'){ + // Not a FLASH-B (shouldn't get here) + return {}; + } + + var _output = {}; + + // Instrument number is common to all XDATA types. + _output['flashb_instrument_number'] = parseInt(xdata.substr(2,2),16); + + _photomultiplier_counts = parseInt(xdata.substr(5,4),16); + + _photomultiplier_background_counts = parseInt(xdata.substr(9,4),16); + _output['flashb_photomultiplier_background_counts'] = _photomultiplier_background_counts + + //_photomultiplier_counts = calculateFLASHBWaterVapour(_photomultiplier_counts, _photomultiplier_background_counts, pressure, temperature); + _output['flashb_photomultiplier_counts'] = _photomultiplier_counts; + + _photomultiplier_temperature = parseInt(xdata.substr(13,4),16); + _photomultiplier_temperature = (-21.103*Math.log((_photomultiplier_temperature*0.0183)/(2.49856 - (_photomultiplier_temperature*0.00061)))) + 97.106; // Degrees C + _output['flashb_photomultiplier_temperature'] = Math.round(_photomultiplier_temperature * 100) / 100; // 2 DP + + _battery_voltage = parseInt(xdata.substr(17,4),16); + _battery_voltage = _battery_voltage*0.005185; // V + _output['flashb_battery_voltage'] = Math.round(_battery_voltage * 100) / 100; // 2 DP + + _yuv_current = parseInt(xdata.substr(21,4),16); + _yuv_current = _yuv_current*0.0101688; // mA + _output['flashb_yuv_current'] = Math.round(_yuv_current * 100) / 100; // 2 DP + + _pmt_voltage = parseInt(xdata.substr(25,4),16); + _pmt_voltage = _pmt_voltage*0.36966; // V + _output['flashb_pmt_voltage'] = Math.round(_pmt_voltage * 10) / 10; // 1 DP + + _firmware_version = parseInt(xdata.substr(29,2),16); + _firmware_version = _firmware_version*0.1; + _output['flashb_firmware_version'] = Math.round(_firmware_version * 10) / 10; // 1 DP + + _output['flashb_production_year'] = parseInt(xdata.substr(31,2),16); + + _output['flashb_hardware_version'] = parseInt(xdata.substr(33,2),16); + + return _output +} + +function parseSKYDEW(xdata) { + // Attempt to parse an XDATA string from a Peltier-based chilled-mirror hygrometer SKYDEW + // Returns an object with parameters to be added to the sondes telemetry. + // + // References: + // Peltier-based chilled-mirror hygrometer “SKYDEW” XDATA protocol (draft) + // + // Sample data: 3F0144A75446416100160ECAFFFF6EC8000006 (length = 38 characters) + + // Run some checks over the input + if(xdata.length != 38){ + // Invalid SKYDEW dataset + return {}; + } + + if(xdata.substr(0,2) !== '3F'){ + // Not a SKYDEW (shouldn't get here) + return {}; + } + + var _output = {}; + + // Instrument number is common to all XDATA types. + _output['skydew_instrument_number'] = parseInt(xdata.substr(2,2),16); + + // Mirror temperature value + // This requires the four coefficients to actually get a value + _output['skydew_mirror_temperature_value'] = parseInt(xdata.substr(4,4),16); + + // Scattered light level + _scattered_light_value = parseInt(xdata.substr(8,4),16); + _scattered_light_value = _scattered_light_value*0.0000625 // V + _output['skydew_scattered_light'] = Math.round(_scattered_light_value * 10000) / 10000; // 4 DP + + // Reference resistance + // Used to calculate mirror temperature + _reference_resistance = parseInt(xdata.substr(12,4),16); + + // Offset value + // Used to calculate mirror temperature + _offset_value = parseInt(xdata.substr(16,4),16); + + // Peltier current + _peltier_current_value = parseInt(xdata.substr(20,4),16); + _peltier_current_value = (_peltier_current_value*0.00040649414 - 1.5)*2; // A + _output['skydew_peltier_current'] = Math.round(_peltier_current_value * 10000) / 10000; // 4 DP + + // Heatsink temperature + _heatsink_temperature = parseInt(xdata.substr(24,2),16); + _heatsink_temperature = (Math.pow((((Math.log(((_heatsink_temperature/8192)*141.9)/(3.3-(_heatsink_temperature/8192)*3.3)/6))/3390)+1)/273.16, -1) -276.16); // Degrees C + _output['skydew_heatsink_temperature'] = Math.round(_heatsink_temperature * 100) / 100; // 2 DP + + // Circuit board temperature + _circuit_board_temperature = parseInt(xdata.substr(26,2),16); + _circuit_board_temperature = (Math.pow((((Math.log(((_circuit_board_temperature/8192)*39.6)/(3.3-(_circuit_board_temperature/8192)*3.3)/6))/3390)+1)/273.16, -1) -276.16); // Degrees C + _output['skydew_circuit_board_temperature'] = Math.round(_circuit_board_temperature * 100) / 100; // 2 DP + + // Battery + _output['skydew_battery'] = parseInt(xdata.substr(28,2),16); + + // PID + _output['skydew_pid'] = parseInt(xdata.substr(30,2),16); + + // Parameter + var parameter = parseInt(xdata.substr(32,4),16); + + // Coefficent type + var parameterType = parseInt(xdata.substr(36,2),16); + + // Parameter Type + switch(parameterType) { + case 0: + _output['skydew_serial_number'] = parameter; + case 1: + _output['skydew_coefficient_b'] = parameter; + case 2: + _output['skydew_coefficient_c'] = parameter; + case 3: + _output['skydew_coefficient_d'] = parameter; + case 4: + _output['skydew_coefficient_e'] = parameter; + case 5: + _output['skydew_firmware_version'] = parameter; + } + + return _output +} + +function parseXDATA(data, pressure, temperature){ + // Accept an XDATA string, or multiple XDATA entries, delimited by '#' + // Attempt to parse each one, and return an object + // Test datasets: + // "0501034F02C978A06300" + // "0501R20234850000006EI" + // "0501034F02CA08B06700#800261FCA6F80012F6F40A75" + // "800262358C080012FE6C0A70#0501035902BA08908400" + // "0501092C000000000000#190214f0df03e82e03660048d73683#0803DC5EF086C244078601A5#3F04475A4B0D415900160D510C270200465900" + + // Split apart any contatenated xdata. + if(data.includes('#')){ + data_split = data.split('#'); + } else { + data_split = [data]; + } + + _output = {"xdata_instrument": []}; + _instruments = []; + for(xdata_i = 0; xdata_i < data_split.length; xdata_i++){ + _current_xdata = data_split[xdata_i]; + + _current_xdata = String(_current_xdata).toUpperCase(); + + // Get Instrument ID + // https://gml.noaa.gov/aftp/user/jordan/XDATA%20Instrument%20ID%20Allocation.pdf + // https://www.gruan.org/gruan/editor/documents/gruan/GRUAN-TN-11_GruanToolRs92_v1.0_2020-10-01.pdf + _instrument = _current_xdata.substr(0,2); + + if (_instrument === '01') { + // V7 + // 0102 time=1001 cnt=0 rpm=0 + // 0102 time=1001 cnt=7 rpm=419 + if (!_instruments.includes("V7")) _instruments.push('V7'); + } else if (_instrument === '05'){ + // OIF411 + _xdata_temp = parseOIF411(_current_xdata, pressure); + _output = Object.assign(_output,_xdata_temp); + if (!_instruments.includes("OIF411")) _instruments.push('OIF411'); + } else if (_instrument === '08'){ + // CFH + // 0803B922067F1D0707CD0144 + // 0803ABD602800F7907D9015D + // 08038AB16A7FBF4908E50161 + // https://www.en-sci.com/cryogenic-frost-point-hygrometer/ + if (!_instruments.includes("CFH")) _instruments.push('CFH'); + } else if (_instrument === '10'){ + // FPH + if (!_instruments.includes("FPH")) _instruments.push('FPH'); + } else if (_instrument === '19'){ + // COBALD + _xdata_temp = parseCOBALD(_current_xdata); + _output = Object.assign(_output,_xdata_temp); + if (!_instruments.includes("COBALD")) _instruments.push('COBALD'); + } else if (_instrument === '28'){ + // SLW + if (!_instruments.includes("SLW")) _instruments.push('SLW'); + } else if (_instrument === '38'){ + // POPS + if (!_instruments.includes("POPS")) _instruments.push('POPS'); + } else if (_instrument === '39'){ + // OPC + if (!_instruments.includes("OPC")) _instruments.push('OPC'); + } else if (_instrument === '3C'){ + // PCFH + _xdata_temp = parsePCFH(_current_xdata); + _output = Object.assign(_output,_xdata_temp); + if (!_instruments.includes("PCFH")) _instruments.push('PCFH'); + } else if (_instrument === '3D'){ + // FLASH-B + _xdata_temp = parseFLASHB(_current_xdata, pressure, temperature); + _output = Object.assign(_output,_xdata_temp); + if (!_instruments.includes("FLASH-B")) _instruments.push('FLASH-B'); + } else if (_instrument === '3E'){ + // TRAPS + if (!_instruments.includes("TRAPS")) _instruments.push('TRAPS'); + } else if (_instrument === '3F'){ + // SKYDEW + _xdata_temp = parseSKYDEW(_current_xdata); + _output = Object.assign(_output,_xdata_temp); + if (!_instruments.includes("SKYDEW")) _instruments.push('SKYDEW'); + } else if (_instrument === '41'){ + // CICANUM + if (!_instruments.includes("CICANUM")) _instruments.push('CICANUM'); + } else if (_instrument === '45'){ + // POPS + if (!_instruments.includes("POPS")) _instruments.push('POPS'); + } else if (_instrument === '80'){ + // Unknown! + //console.log("Saw unknown XDATA instrument 0x80.") + }else { + // Unknown! + + } + } + + if (_instruments.length > 0) _output["xdata_instrument"] = _instruments; + + return _output + +}