Files
eacp_webapp/htdocs/js/statistics_range.js
T
2026-06-16 11:24:50 +08:00

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