405 lines
13 KiB
JavaScript
405 lines
13 KiB
JavaScript
/********************************************************************
|
|
* Dynamic Receiver Statistic for Radarcape
|
|
********************************************************************
|
|
* Guenter Koellner Embedded Development GmbH
|
|
*
|
|
* Am Rain 24
|
|
* 85256 Vierkirchen
|
|
* Geschaeftsfuehrer Guenter Koellner
|
|
* http://www.jetvision.de
|
|
*
|
|
* References:
|
|
* https://www.w3schools.com/jsref/default.asp
|
|
* http://www.chartjs.org/docs/latest/
|
|
* http://www.chartjs.org/samples/latest/charts/radar.html
|
|
* https://github.com/arian/pngjs
|
|
* https://stackoverflow.com/questions/33902299/using-jquery-ajax-to-download-a-binary-file
|
|
* https://stackoverflow.com/questions/43010404/chart-js-radar-scaleshowline-for-each-scaleline
|
|
* https://stackoverflow.com/questions/5422265/how-can-i-pre-populate-html-form-input-fields-from-url-parameters
|
|
*
|
|
********************************************************************/
|
|
|
|
var cookie_settings_name = "statististics_range";
|
|
var PRODUCT_NAME = "";
|
|
var verbose = false;
|
|
|
|
var nofBuckets = 40;
|
|
|
|
var settings = {
|
|
'maxDistance' : 25,
|
|
'rangeMode' : 'Auto',
|
|
'rangeBucket' : nofBuckets -1
|
|
};
|
|
|
|
var myChart = null;
|
|
var myChartMaxDistance = 25;
|
|
var myChartRangeMode = "Auto";
|
|
var containers; // will store [0..2] locssources 0..39 times altitude 360 distance values
|
|
|
|
var refresh_time = 5; // seconds
|
|
|
|
var rangeSlider = null;
|
|
var altitudeSlider = null;
|
|
|
|
var labels360 = [];
|
|
for( var deg=0; deg<360; deg++ ) {
|
|
if ( deg%15 == 0) { labels360.push(deg); }
|
|
else { labels360.push(''); }
|
|
}
|
|
|
|
var isContinue = true; // set when the current integration is part of the time interval
|
|
|
|
function bucketToAltitude(bucket) {
|
|
var alt_ft = bucket*1000 + 500;
|
|
return alt_ft;
|
|
}
|
|
|
|
// https://stackoverflow.com/questions/7434371/image-onload-function-with-return
|
|
// https://stackoverflow.com/questions/5004829/how-to-call-a-member-function-from-another-member-function-in-javascript
|
|
class PngContainer {
|
|
|
|
constructor(url, locsource) {
|
|
//console.log('PngContainer::constructor url='+url);
|
|
this.url = url;
|
|
this.locsource = locsource;
|
|
var self = this;
|
|
var oReq = new XMLHttpRequest();
|
|
oReq.open("GET", rcd_globals.fcgipath+url, true);
|
|
oReq.responseType = "arraybuffer";
|
|
|
|
oReq.onload = function() {
|
|
var bytes = oReq.response;
|
|
var pngReader = new PNGReader(bytes);
|
|
pngReader.parse(function(err, png){
|
|
if( !err ) {
|
|
self.initialize(png);
|
|
updateContainers(self);
|
|
}
|
|
});
|
|
}
|
|
oReq.send();
|
|
}
|
|
|
|
initialize (png) {
|
|
this.width = ( png.width==360 ) ? 360 : 0;
|
|
this.height = ( png.height==40 ) ? 40 : 0;
|
|
if( (this.width==0 ) || (this.height==0) ) {
|
|
console.log ("invalid PNG size");
|
|
return;
|
|
}
|
|
// expected width = 360, expected height is 40
|
|
//console.log('file='+this.url, 'width='+png.width, 'height='+png.height, 'colorType='+png.colorType, 'bitDepth='+png.bitDepth, 'colors='+png.colors);
|
|
this.data = [];
|
|
for( var alt_bucket=0; alt_bucket<png.height; alt_bucket++) {
|
|
var rangedata = [];
|
|
for( var az=0; az<png.width; az++ ) {
|
|
var pixel = png.getPixel(az,alt_bucket);
|
|
var dist_km = pixel[0]/64;
|
|
rangedata.push(dist_km);
|
|
}
|
|
this.data.push(rangedata);
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearContainers() {
|
|
myChartMaxDistance = 25;
|
|
containers=[];
|
|
for( var locsource=0; locsource<3; locsource++ ) {
|
|
containers.push([]);
|
|
for( var alt=0; alt<nofBuckets; alt++ ) {
|
|
containers[locsource].push([]);
|
|
for( var az=0; az<360; az++ ) {
|
|
containers[locsource][alt].push(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateContainers(png) {
|
|
//console.log('updateContainers url='+png.url, 'locsource='+png.locsource);
|
|
for( var alt=0; alt<nofBuckets; alt++ ) {
|
|
for( var deg=0; deg<360; deg++) {
|
|
var png_dist = png.data[alt][deg];
|
|
if( png_dist ) {
|
|
var dist = Math.max(containers[png.locsource][alt][deg], png_dist);
|
|
containers[png.locsource][alt][deg] = dist;
|
|
myChartMaxDistance = Math.max(myChartMaxDistance, dist);
|
|
}
|
|
}
|
|
}
|
|
updateChart(png.locsource);
|
|
}
|
|
|
|
function updateChart( locsource ) {
|
|
var data = [];
|
|
for( az=0; az<360; az++) {
|
|
var dist = -1.0;
|
|
for( bucket=settings.rangeBucket; bucket<nofBuckets; bucket++ )
|
|
{
|
|
dist = Math.max(dist, containers[locsource][bucket][az]);
|
|
}
|
|
data.push(dist);
|
|
}
|
|
|
|
if( myChartRangeMode==='MaxHold' ) {
|
|
myChart.options.scale.ticks.max = Math.ceil(myChartMaxDistance/25)*25;
|
|
console.log('Maxhold='+myChartMaxDistance);
|
|
}
|
|
|
|
myChart.data.datasets[locsource].data = data;
|
|
myChart.update();
|
|
}
|
|
|
|
|
|
// create a list of filenames for each hourly file between begin_ts and end_ts
|
|
function updatePngContainers(start, end)
|
|
{
|
|
var T_start = start + 3600; // recording saves with timestamp at the begin of next hour
|
|
var T_end = isContinue ? end-3600 : end; // if continue, the last element will be the one dynamically loaded
|
|
|
|
var infotext = 'Begin: ' + moment.unix(start).toISOString() + '<br/>'
|
|
+ ' End: ' + moment.unix(end ).toISOString() + '<br/>'
|
|
+ ' First file: ' + moment.unix(T_start).utc().format("YYYYMMDDHH") + '<br/>'
|
|
+ ' Last file: ' + moment.unix(T_end ).utc().format("YYYYMMDDHH") + '<br/>'
|
|
+ ' Continued update: ' + ( isContinue ? 'Yes' : 'No' );
|
|
|
|
document.getElementById('infotext').innerHTML = infotext;
|
|
|
|
clearContainers();
|
|
|
|
for( var T=T_start; T<=T_end; T+=3600 ) { // filename is the end time of accumulation
|
|
var fn = moment.unix(T).utc().format("YYYYMMDDHH");
|
|
var arr = ['ranges/'+fn+'-v1-ADSB.png', 'ranges/'+fn+'-v1-MLAT.png', 'ranges/'+fn+'-v1-FLARM.png'];
|
|
arr.forEach( function(url, locsource_index) {
|
|
var png = new PngContainer(url, locsource_index);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
function updateRangeSlider() {
|
|
var manual_range = parseInt(rangeSlider.value);
|
|
//console.log ("rangeMode="+myChartRangeMode,"manualRange="+manual_range,"myChartMaxDistance="+myChartMaxDistance,"ticks.max="+myChart.options.scale.ticks.max);
|
|
document.getElementById("OutputRangeSlider").innerHTML = manual_range +" km";
|
|
|
|
switch( myChartRangeMode ) {
|
|
case "Auto":
|
|
myChart.options.scale.ticks.max = undefined;
|
|
myChart.update();
|
|
break;
|
|
case "MaxHold":
|
|
myChart.options.scale.ticks.max = Math.ceil(myChartMaxDistance/25)*25;
|
|
myChart.update();
|
|
break;
|
|
case "Manual":
|
|
myChart.options.scale.ticks.max = manual_range;
|
|
myChart.update();
|
|
break;
|
|
default:
|
|
throw "unexpected value of myChartRangeMode";
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
function updateAltitudeSlider() {
|
|
var bucket = parseInt(altitudeSlider.value);
|
|
document.getElementById("OutputAltitudeSlider").innerHTML = bucketToAltitude(bucket) +" ft";
|
|
settings.rangeBucket = nofBuckets - bucket - 1;
|
|
|
|
for(var locsource=0; locsource<3; locsource++) updateChart(locsource);
|
|
}
|
|
|
|
function initializeDateRangePicker(){
|
|
var end = moment().utc().add('hour', 1).startOf('hour');
|
|
var start = end.clone().add('hour', -24);
|
|
|
|
function cb(start, end) {
|
|
var fullmonthFormat = 'MMMM D YYYY, HH:mm UTC';
|
|
$('#timerange-selector span').html(start.format(fullmonthFormat) + ' — ' + end.format(fullmonthFormat));
|
|
}
|
|
|
|
$('#timerange-selector').daterangepicker({
|
|
startDate: start,
|
|
endDate: end,
|
|
//minDate: undefined,
|
|
//maxDate: undefined,
|
|
//maxSpan: undefined,
|
|
timePicker: true,
|
|
timePickerIncrement: 60,
|
|
alwaysShowCalendars: true,
|
|
timePicker24Hour: true,
|
|
locale: {
|
|
format: 'YYYY-MM-DD HH:mm UTC'
|
|
},
|
|
ranges: {
|
|
'Today': [moment().utc().startOf("day"), moment().utc().endOf("day")]
|
|
// further possible
|
|
}
|
|
}, cb);
|
|
|
|
// function called on clicking apply
|
|
$('#timerange-selector').on('apply.daterangepicker', function(ev, picker) {
|
|
|
|
// daterangepicker does not have utc support: https://github.com/dangrossman/bootstrap-daterangepicker/issues/343
|
|
var startDateOffset = picker.startDate.utcOffset()
|
|
var utcStartDate = moment.utc(picker.startDate).utcOffset(-startDateOffset, true).utc()
|
|
|
|
var endDateOffset = picker.endDate.utcOffset()
|
|
var utcEndDate = moment.utc(picker.endDate).utcOffset(-endDateOffset, true).utc()
|
|
|
|
cb(utcStartDate, utcEndDate); // update label of <input>
|
|
var begin_ts = utcStartDate.unix();
|
|
var end_ts = utcEndDate.unix();
|
|
|
|
var now = moment().utc().unix();
|
|
isContinue = now < end_ts;
|
|
|
|
updatePngContainers(begin_ts, end_ts);
|
|
});
|
|
|
|
isContinue = true;
|
|
updatePngContainers(start/1000, end/1000);
|
|
|
|
cb(start, end);
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
|
|
|
|
getJSON( rcd_globals.fcgipath + "param.json?param=productname", function(params) {
|
|
PRODUCT_NAME = params.productname;
|
|
if( PRODUCT_NAME==='MLAT_SERVER' ) {
|
|
document.title = "Network Range";
|
|
} else {
|
|
document.title = "Receiver Range";
|
|
}
|
|
document.getElementById("page_title").innerHTML = document.title;
|
|
});
|
|
|
|
// read cookies
|
|
// ------------
|
|
var tmp = [];
|
|
var cookie = getCookie(cookie_settings_name);
|
|
if( cookie ) {
|
|
$.extend( tmp, JSON.parse(cookie) );
|
|
}
|
|
for (var key in tmp) {
|
|
if( key in settings ) {
|
|
settings[key] = tmp[key];
|
|
}
|
|
}
|
|
|
|
altitudeSlider = document.getElementById("AltitudeSlider");
|
|
document.getElementById("AltitudeSliderDown").onclick = function() { altitudeSlider.stepDown(); updateAltitudeSlider(); }
|
|
document.getElementById("AltitudeSliderUp").onclick = function() { altitudeSlider.stepUp(); updateAltitudeSlider(); }
|
|
altitudeSlider.onchange = function() {
|
|
updateAltitudeSlider();
|
|
}
|
|
|
|
rangeSlider = document.getElementById("RangeSlider");
|
|
document.getElementById("RangeSliderDown").onclick = function() { rangeSlider.stepDown(); updateRangeSlider(); }
|
|
document.getElementById("RangeSliderUp").onclick = function() { rangeSlider.stepUp(); updateRangeSlider(); }
|
|
rangeSlider.onchange = function() {
|
|
updateRangeSlider();
|
|
}
|
|
|
|
var rangeModeSelectButtons = document.getElementById("RangeModeSelectButtons");
|
|
rangeModeSelectButtons.onchange = function() {
|
|
myChartRangeMode = $("input[type='radio'][name='RangeModeSelectButtons']:checked").val();
|
|
updateRangeSlider();
|
|
}
|
|
|
|
var color = Chart.helpers.color;
|
|
clearContainers();
|
|
|
|
const options25 = {
|
|
plugins: [{
|
|
afterInit: function() { console.log("Chart is ready");
|
|
}
|
|
}],
|
|
responsive: true, // true: scales with responsiveness, false: scales with pixel size
|
|
animation: false,
|
|
legend: {
|
|
position: 'bottom',
|
|
},
|
|
scale: {
|
|
// https://www.chartjs.org/docs/latest/axes/radial/linear.html#angle-line-options
|
|
// https://www.chartjs.org/docs/latest/axes/radial/linear.html#linear-radial-axis
|
|
// angleLines: { display: true, color: "#00ff00ff", lineWidth: 0.1 },
|
|
// https://www.chartjs.org/docs/latest/axes/styling.html
|
|
// display: true,
|
|
ticks: { min:0,
|
|
stepSize: 25,
|
|
callback: function(value, index, values) {
|
|
//console.log (value, index, values);
|
|
return value;
|
|
}
|
|
} // this is the maximum distance to be shown
|
|
}
|
|
}
|
|
|
|
var dataset_adsb = {
|
|
label: 'ADS-B',
|
|
fill: false,
|
|
borderColor: "#000000ff",
|
|
borderWidth: 2,
|
|
pointRadius: 0
|
|
};
|
|
var dataset_mlat = {
|
|
label: 'MLAT',
|
|
fill: false,
|
|
borderColor: "#ff0000ff",
|
|
borderWidth: 2,
|
|
pointRadius: 0
|
|
};
|
|
var dataset_flarm = {
|
|
label: 'Flarm',
|
|
fill: false,
|
|
borderColor: "#ff00ffff", //window.chartColors.black,
|
|
borderWidth: 2,
|
|
pointRadius: 0
|
|
};
|
|
|
|
myChart = new Chart(document.getElementById('range-overall'), { type:'radar', data:{labels:labels360, datasets:[dataset_adsb, dataset_mlat, dataset_flarm]}, options:options25 } );
|
|
|
|
$("input[type='radio'][name='RangeModeSelectButtons'][value="+myChartRangeMode+"]").prop('checked', true);
|
|
updateRangeSlider();
|
|
updateAltitudeSlider();
|
|
|
|
setTimeout ( initializeDateRangePicker, 1000);
|
|
|
|
var initalized = false;
|
|
var delay = 100;
|
|
var hour_tm1 = moment().startOf("hour").unix();
|
|
|
|
function loop() {
|
|
if( rcd_globals.initialized ) {
|
|
if( isContinue )
|
|
{
|
|
var hour = moment().startOf("hour").unix();
|
|
if( hour!= hour_tm1 ) {
|
|
console.log('permanently add last hour');
|
|
hour_tm1 = hour;
|
|
var fn = moment.unix(hour).utc().format("YYYYMMDDHH");
|
|
var arr = ['ranges/'+fn+'-v1-ADSB.png', 'ranges/'+fn+'-v1-MLAT.png', 'ranges/'+fn+'-v1-FLARM.png'];
|
|
arr.forEach( function(url, locsource_index) {
|
|
var png = new PngContainer(url, locsource_index);
|
|
});
|
|
document.getElementById('infotext').innerHTML += '<br/>added hourly: ' + moment.unix(hour).utc().format("YYYYMMDDHH");
|
|
}
|
|
|
|
['range-v0-ADSB.png', 'range-v0-MLAT.png', 'range-v0-FLARM.png'].forEach( function(url, locsource_index) {
|
|
var png = new PngContainer(url, locsource_index);
|
|
});
|
|
}
|
|
delay = Math.max(1000, refresh_time*1000);
|
|
}
|
|
|
|
setTimeout( loop, delay );
|
|
}
|
|
|
|
loop();
|
|
});
|