var map; var map_id = 0; var position; var lastPosition; var stations = {}; var station_markers = {}; var labelled_markers = {}; var timeoutId; var coverageOverlay; var start = '2018-01-01'; var end = '2100-01-01'; var lasthash = ''; var maxTimeout, minTimeout; var processingUrl = true; var defaultPointer = 'crosshair'; // help, cursor, crosshair, text, wait, pointer, progress //var defaultColor = '#00990000:#009900ff'; var defaultColour = '#80000040:#008000ff'; var minZoomLevel = 8; var maxZoomLevel = 11; var selected = { 'what':'max', 'when':'lastweek', 'station':'', 'center':'', 'zoom':'', 'airspace':'', 'airports':'', 'circles':'', 'ambiguity':'', 'colour': defaultColour }; function initialize() { airspaceLayer = new ol.layer.Tile({ source: new ol.source.XYZ({ tileUrlFunction: function(tileCoord, projection) { var z = tileCoord[0]; var x = tileCoord[1]; var y = (1 << z) + tileCoord[2]; return 'http://1.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_approved_airspaces_geometries@png/' + z + '/' + x + '/' + y + '.png'; }, wrapx: false }), opacity: 1, visible: false }); airportsLayer = new ol.layer.Tile({ source: new ol.source.XYZ({ tileUrlFunction: function(tileCoord, projection) { var z = tileCoord[0]; var x = tileCoord[1]; var y = (1 << z) + tileCoord[2]; return 'http://1.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_approved_airports@png/' + z + '/' + x + '/' + y + '.png'; }, wrapx: false }), opacity: 1, visible: false }); OSMLayer = new ol.layer.Tile({ source: new ol.source.OSM(), visible: true }); stationLayerSource = new ol.source.Vector({ features: [] }); stationLayer = new ol.layer.Vector({ source: stationLayerSource, zIndex: 50, opacity: 1, visible: true }); circleLayerSource = new ol.source.Vector({ features: [] }); circleLayer = new ol.layer.Vector({ source: circleLayerSource, zIndex: 50, opacity: 1, visible: false }); AmbiguitySquareLayerSource = new ol.source.Vector({ features: [] }); AmbiguitySquareLayer = new ol.layer.Vector({ source: AmbiguitySquareLayerSource, zIndex: 51, opacity: 1, visible: true }); ambiguityOverlay = new AmbiguityMapType(); // ratio: 1.2, +/-10% instead of default +/-25% ambiguityLayer = new ol.layer.Image({ source: new ol.source.ImageCanvas({ canvasFunction: ambiguityOverlay.canvasFunctionAmbiguity, projection: "EPSG:3857", ratio: 1.2, visible: false }) }); // create overlay before reading hash coverageOverlay = new CoverageMapType(); // ratio: 1.2, +/-10% instead of default +/-25% coverageLayer = new ol.layer.Image({ source: new ol.source.ImageCanvas({ canvasFunction: coverageOverlay.canvasFunctionCoverage, projection: "EPSG:3857", ratio: 1.2 }) }); // read hash before creating map with intial settings if ( location.hash.substr(1) != '' ) { readhash(); } else { setOptions('circles', 'circles'); // show by default setColour( defaultColour ); processingUrl = false; } // re-readhash if user changed hash and then redraw $(window).bind( 'hashchange', reReadhash ); attribution = new ol.control.Attribution({ collapsed: true, tipLabel: 'Show credits' }); scaleLineControl = new ol.control.ScaleLine(); map = new ol.Map({ layers: [ OSMLayer, circleLayer, airportsLayer, airspaceLayer, coverageLayer, ambiguityLayer, stationLayer, ], target: 'map_canvas', controls: ol.control.defaults({ attributionOptions: { collapsible: true } }).extend([ scaleLineControl, attribution ]), view: new ol.View({ projection: 'EPSG:3857', center: ol.proj.fromLonLat([dLon,dLat]), zoom: dZoom }), }); myresize(); $(window).resize(function () { myresize(); }); // displays credits over the list credits = document.getElementsByClassName('ol-attribution ol-unselectable ol-control')[0]; credits.style.zIndex = "1000"; var tooltip = document.getElementById('tooltip'); var overlay = new ol.Overlay({ element: tooltip, offset: [10, 0], positioning: 'bottom-left' }); map.addOverlay(overlay); function genericDisplayTooltip(evt) { var pixel = evt.pixel; var feature = map.forEachFeatureAtPixel(pixel, function(feature) { return feature; }); tooltip.style.display = feature ? '' : 'none'; if (feature) { overlay.setPosition(evt.coordinate); tooltip.innerHTML = feature.get('info'); } }; // marker tooltip function displayTooltip(event) { var hit = map.hasFeatureAtPixel(event.pixel); var pixel = event.pixel; tooltip.style.display = 'none'; map.getTargetElement().style.cursor = defaultPointer; map.forEachFeatureAtPixel(pixel, function(feature) { if (typeof(feature.get('mark'))!='undefined'){ if ( feature.get('mark').substring(0,1) == "S" ) { overlay.setPosition(event.coordinate); tooltip.innerText = feature.get('info'); tooltip.style.display = ''; // also change cursor map.getTargetElement().style.cursor = 'pointer'; return; } } }) }; map.on('pointermove', displayTooltip); map.on('singleclick', function(event) { var stationName = ''; map.forEachFeatureAtPixel(event.pixel, function(feature,layer) { if (typeof(feature.get('mark'))!='undefined'){ if ( feature.get('mark').substring(0,1) == "S" ) { stationName = feature.get('name'); } } }); if (stationName != '') { getStationData( stationName ); } else { // no 'S' feature // map click - display details updateDetails('loading...'); if ( timeoutId ) { clearTimeout(timeoutId); } position = ol.proj.transform(event.coordinate, 'EPSG:3857', 'EPSG:4326');; timeoutId = setTimeout( displayDetails, 500 ); } }); map.on('moveend', function(event) { var zoom = map.getView().getZoom(); if (zoom != dZoom) { setZoom(zoom); updateURL( 'zoom' ); } var nc = ol.proj.toLonLat( map.getView().getCenter() ); if ( (dLon != round(nc[0],6)) || (dLat != round(nc[1],6)) ) { setCentre( nc ); updateURL( 'center' ); } }); // display the stations displayStations( true ); updateDetails("Blue icons represent UP old stations, Mauve ones DOWN
Green is 0.2.1 or above and UP, Red is DOWN new stations"); timeoutId = null; }; function myresize() { var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); var t = $('#top_row').height(); var b = $('#bottom_row').height(); $('#map_canvas').height((h - t - b) + 'px'); map.updateSize(); } function addLabel( location, labelText ) { var marker = labelled_markers[location.s] = new ol.Feature({ geometry: new ol.geom.Point(ol.proj.fromLonLat([location.lg,location.lt])), mark: "L" }); marker.setStyle(new ol.style.Style({ text: new ol.style.Text({ font: '12px Verdana', text: labelText, offsetX: 0, offsetY: -22, fill: new ol.style.Fill({color: 'black'}), stroke: new ol.style.Stroke({color: 'black', width: 0.5}) }) })); stationLayerSource.addFeature(marker); } // custom marker var angleMarker = 30; var radiusMarker = 11; var sinMarker = Math.sin(degreesToRadians(angleMarker)); var cosMarker = Math.cos(degreesToRadians(angleMarker)); var offsetMarker = radiusMarker/sinMarker; var styleCache = {}; var styleFunction = function(colour) { colour = '#'+colour; var style = styleCache[colour]; if (!style) { canvasM = document.createElement("canvas"); var canvasWidth = radiusMarker*2+3, canvasHeight = radiusMarker+offsetMarker+3; canvasM.setAttribute("width", canvasWidth); canvasM.setAttribute("height", canvasHeight); var contextM = canvasM.getContext("2d"); // erase the canvas before re-drawing contextM.clearRect(0, 0, canvasWidth, canvasHeight); // Draw contextM.save(); contextM.beginPath(); var x1=radiusMarker*cosMarker, y2=radiusMarker+offsetMarker; var y1=offsetMarker-radiusMarker*sinMarker, x2=x1*y2/y1; contextM.moveTo(radiusMarker+1, canvasHeight-0); contextM.lineTo(radiusMarker+1-x1, canvasHeight-y1); contextM.arcTo(radiusMarker+1-x2, canvasHeight-y2, radiusMarker+1, canvasHeight-y2, radiusMarker); contextM.arcTo(radiusMarker+1+x2, canvasHeight-y2, radiusMarker+1+x1, canvasHeight-y1, radiusMarker); contextM.closePath(); contextM.fillStyle = colour; contextM.fill(); contextM.lineWidth = 1; contextM.strokeStyle = '#000000'; contextM.stroke(); contextM.restore(); style = new ol.style.Style({ image: new ol.style.Icon({ img: canvasM, imgSize: [canvasWidth, canvasHeight], anchor: [0.5, 1.0], }) }); styleCache[colour] = style; } return style; }; function addStation( location, colour ) { var marker = station_markers[location.s] = new ol.Feature({ geometry: new ol.geom.Point(ol.proj.fromLonLat([location.lg,location.lt])), lat: location.lt, lon: location.lg, info: location.s + "\n" + (location.u == "U" ? "Last heartbeat at:\n" : "Last point at ") + location.ut + "Z\n" + "Version " + location.v, mark: "S", name: location.s, }); marker.setStyle(styleFunction(colour)); // custom /* marker.setStyle(new ol.style.Style({ image: new ol.style.Icon({ anchor: [0.5, 1], opacity: 1, src: "//www.googlemapsmarkers.com/v1/" + colour + "/", }) })); */ stationLayerSource.addFeature(marker); } function addCircle( location, radius, colour ) { var rangeCircle = new ol.Feature({ // geometry: new ol.geom.Circle(ol.proj.fromLonLat([location.lg, location.lt]), radius / Math.cos( degreesToRadians(location.lt))) geometry: new ol.geom.Polygon.circular( // WGS84 Sphere new ol.Sphere(6378137), [location.lg, location.lt], radius, // Number of verticies 64).transform('EPSG:4326', 'EPSG:3857') }); rangeCircle.setStyle(new ol.style.Style({ stroke: new ol.style.Stroke({ color: colour, width: 1 }), })); circleLayerSource.addFeature(rangeCircle); } var dsTimeOut = null; function displayStations(first) { // Ajax is async and timeout runs twice unless cleared until AJAX done // so need a clearTimeOut if (dsTimeOut) { clearTimeout(dsTimeOut); } $.ajax( { type: "GET", url: OgR+"/perl/stations2-filtered.pl?start="+start+"&end="+end, timeout:20000, cache: true, error: function (xhr, ajaxOptions, thrownError) { }, success: function(json) { var checked = selected['circles'] == 'circles'; stationLayerSource.clear() station_markers = {}; stations = []; circleLayerSource.clear() // add markers json.stations.forEach( function(entry) { stations[entry.s] = entry; var old = (entry.v == 'old' || entry.v == '?' || entry.v == "undefined" || entry.v == "" || entry.v == '0.1.3' ); var colour = '00ff00'; if ( entry.u == "U" ) { if ( old ) { colour = '0000ff'; } } else { if ( old ) { colour = 'aa00aa'; } else { colour = 'aa0000'; } } addStation( entry, colour ); addCircle( entry, 10000, 'rgba(48, 48, 48, 0.7)' ); addCircle( entry, 20000, 'rgba(48, 48, 48, 0.5)' ); addCircle( entry, 30000, 'rgba(48, 48, 48, 0.3)' ); } ); if ( first ) { setupSearch(); var stationName = selected['station']; stationName = matchStation(stationName); if ( stationName != "" && stations[stationName] ) { setStation(stationName); // as per match setCentre( [ stations[stationName].lg, stations[stationName].lt ] ); map.getView().setCenter(ol.proj.fromLonLat([dLon, dLat])); } } } }); // and update every 5 minutes dsTimeOut = setTimeout( displayStations, 5*60*1000 ); } function degreesToRadians(deg) { return deg * (Math.PI / 180); } function radiansToDegrees(rad) { return rad / (Math.PI / 180); } function round(value, decimals) { return Number(Math.round(value+'e'+decimals)+'e-'+decimals); } const DEFAULT_RADIUS = 6371008.8; function getDistance(c1, c2, opt_radius) { const radius = opt_radius || DEFAULT_RADIUS; const lat1 = degreesToRadians(c1[1]); const lat2 = degreesToRadians(c2[1]); const deltaLatBy2 = (lat2 - lat1) / 2; const deltaLonBy2 = degreesToRadians(c2[0] - c1[0]) / 2; const a = Math.sin(deltaLatBy2) * Math.sin(deltaLatBy2) + Math.sin(deltaLonBy2) * Math.sin(deltaLonBy2) * Math.cos(lat1) * Math.cos(lat2); return 2 * radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function clearLabels() { // remove old labels // use Object.keys for array with keys instead of numeric indexes Object.keys(labelled_markers).forEach(function(key, index) { stationLayerSource.removeFeature(labelled_markers[key]); }); labelled_markers = []; } function getStationData( stationName ) { clearLabels(); updateDetails(''); adjustMap( null, null, stationName ); }; function updateDetails(newDetails) { if (newDetails != '') { $('#details').show(); } else { $('#details').hide(); } $('#details').html(newDetails); myresize(); // adjust, details DIV may change size } function displayDetails() { if ( position && position != lastPosition ) { $.ajax( { type: "GET", url: OgR+"/perl/details-mgrs.pl", data: { start: start, end: end, position: forward([position[0],position[1]],2) }, timeout:20000, cache: true, error: function (xhr, ajaxOptions, thrownError) { }, success: function(json) { console.log( json.toString() ); var pos = inverse( json.query.ref ); var bounds = ol.extent.boundingExtent([[pos[0],pos[1]],[pos[2],pos[3]]]) var point = ol.extent.getCenter(bounds); clearLabels(); var stationList = ""; var id = 1; var currentFilter = selected['station']; json.position.forEach( function(station) { var distance = stations[station.s] ? Math.round( getDistance([stations[station.s].lg, stations[station.s].lt],point) /100)/10 : '?'; if ( currentFilter != '' && station.s != currentFilter ) { stationList += ""; } stationList += ""+id+":"+station.s+": "+distance+"km, "+station.l+"-"+station.h+"m, Avg Max:"+(station.a/10)+"db, Samples:"+station.c + ", Gliders:"+station.g+ ' ('+station.first; if ( station.first != station.last ) { stationList += " to " + station.last; } stationList += ')
'; if ( currentFilter != '' && station.s != currentFilter ) { stationList += "
"; } if ( station_markers[station.s] ) { addLabel( stations[station.s], id.toString() ); } id++; }); console.log( stationList ); updateDetails(stationList); }, }); lastPosition = position; } timeoutId = null; } var substringMatcher = function(strs) { return function findMatches(q, cb) { var matches, substringRegex; matches = []; substrRegex = new RegExp(q, 'i'); for( var key in stations ) { if (substrRegex.test(key)) { matches.push({ value: key }); } } cb(matches.sort(function(a,b){ return a.value < b.value ? -1 : (a.value == b.value ? 0 : 1); })); cb(matches); }; }; function setupSearch() { $('.stationlist #typeahead').typeahead({ hint: true, highlight: true, minLength: 1 }, { name: 'stations', displayKey: 'value', source: substringMatcher('') }); } function toggleOptions(option) { var newOption = selected[option] ? '' : option; setOptions(option, newOption); updateURL(option); return false; } function setOptions(option, newOption) { switch(option) { case 'circles': circleLayer.setVisible(newOption); break; case 'ambiguity': ambiguityLayer.setVisible(newOption); break; case 'airports': airportsLayer.setVisible(newOption); break; case 'airspace': airspaceLayer.setVisible(newOption); break; default: { } } tick( option, newOption ); } function setMinColour(c,a) { var s = selected['colour'].split(':'); if ( s[0] != c ) { s[0] = c+pad2(a.toString(16)); adjustMap( null, null, null, s[0]+':'+s[1] ); } return false; } function setMaxColour(c,a) { var s = selected['colour'].split(':'); if ( s[1] != c ) { s[1] = c+pad2(a.toString(16)); adjustMap( null, null, null, s[0]+':'+s[1] ); } return false; } function setZoom(mapZoom) { console.log( "Zoom level: "+mapZoom ); dZoom = mapZoom; selected['zoom'] = mapZoom; if (mapZoom > maxZoomLevel) { $('#zoomOut_msg').show(); } else { $('#zoomOut_msg').hide(); } if (mapZoom < minZoomLevel) { $('#zoomIn_msg').show(); } else { $('#zoomIn_msg').hide(); } return false; } function setCentre(coords) { dLon = coords[0]; dLat = coords[1]; selected['center'] = coords[1].toFixed(5) + "_" + coords[0].toFixed(5); } function setColour(colour) { selected['colour'] = colour; coverageOverlay.setColourScheme( colour ); } function setStation(where) { selected['station'] = where; coverageOverlay.setStation(where); $('#typeahead').val( where ); updateTitle(); } function setSource(what) { tick( 'what', what ); coverageOverlay.setSource(what); updateTitle(); } function setDates(when) { switch(when) { case 'today': start = end = new Date().toISOString().substr(0,10); break; case 'yesterday': start = new Date(new Date().getTime() - (24*3600*2*1000)).toISOString().substr(0,10); end = new Date(new Date().getTime() - (24*3600*1*1000)).toISOString().substr(0,10); break; case 'lastweek': start = new Date(new Date().getTime() - (24*3600*7*1000)).toISOString().substr(0,10); end = new Date().toISOString().substr(0,10); break; case 'recent': start = new Date(new Date().getFullYear(),0,1).toISOString().substr(0,10); end = new Date().toISOString().substr(0,10); break; case 'all': start = '2015-03-31'; end = new Date().toISOString().substr(0,10); break; default: { switch(when.substr(0,1)) { case 'd': var ndays = parseInt(when.substr(1,10)); start = new Date(new Date().getTime() - (24*3600*ndays*1000)).toISOString().substr(0,10); end = new Date().toISOString().substr(0,10); break; case 'D': var s = when.substr(1,21).split('#'); start = s[0]; end = s[1]; break; default: { } } } } tick( 'when', when ); coverageOverlay.setDates( start, end ); updateTitle(); } function setWhen(when) { adjustMap(null, when); return false; } function setWhat(what) { adjustMap(what); return false; } function matchStation(where) { if (where != '') { // match in list for( var key in stations ) { if (where.toUpperCase() == stations[key].s.toUpperCase()) { where = stations[key].s; break; } } } return where; } var nostation = { 'receivers':1, 'coverage':1 }; function adjustMap( what, when, where, colour ) { if ( what ) { setSource(what); if ( nostation[what] ) { where = ''; } updateURL(what); } if ( when ) { setDates(when); updateURL(when); } if ( where != null && where != undefined ) { if (where == '') { // i.e all stations setStation(where); updateURL('station'); } else { if (where.includes('%')) { // i.e contains wildcard setStation(where); updateURL('station'); } else { where = matchStation(where); var LngLat; if ( stations[ where ] ) { LngLat = [stations[where].lg,stations[where].lt]; if (map.getView().getZoom() < minZoomLevel ) { setZoom(minZoomLevel); map.getView().setZoom(dZoom); } setCentre( LngLat ); map.getView().setCenter(ol.proj.fromLonLat([dLon, dLat])); setStation(where); updateURL('station'); } else { $('#typeahead').val(''); } } } } if ( colour != null && colour != '' ) { setColour(colour); updateURL('colour'); } // update the coverage layer coverageLayer.getSource().changed(); } var hrefOrder = [ 'station', 'what', 'when', 'center', 'zoom', 'colour' ]; var updateHistory = { 'what':1, 'when':1, 'station':1 }; var titleOrder = [ 'what', 'station', 'when' ]; var options = [ 'airports', 'airspace', 'circles', 'ambiguity' ]; function tick( whattype, newItem ) { // clear current tick if ( selected[whattype] && selected[whattype] !== '' && whattype !== 'station' ) { $('#'+selected[whattype]+' span').attr('class',''); } selected[whattype] = newItem; // set new tick if ( selected[whattype] && selected[whattype] !== '' && whattype !== 'station' ) { $('#'+selected[whattype]+' span').attr('class','glyphicon glyphicon-ok'); } } function updateTitle() { // update the title to reflect the contents var title = ''; for( var i = 0; i < titleOrder.length; i++ ) { var v = selected[titleOrder[i]]; if ( v && v != '' ) { if ( titleOrder[i] == 'station' ) { title += ' - ' + v; } else { title += ' - ' + $('#'+v).text(); } } } window.document.title = 'Onglide Range' + title; $('#description').html( title.substr(2) ); } var coloursInitialised = false; function initColour( newColour ) { var s = newColour.split(':'); var startColor = tinycolor( s[1] ); var endColor = tinycolor( s[0] ); if (coloursInitialised) { // update $("#maxc").trigger("colorpickersliders.updateColor", startColor.toRgbString()); $("#minc").trigger("colorpickersliders.updateColor", endColor.toRgbString()); return; } $("#maxc").ColorPickerSliders({ color: startColor.toRgbString(), flat: true, size: 'sm', placement: 'left', customswitches: false, order: { hsl: 1, opacity: 2 }, onchange: function( container, colour ) { if ( maxTimeout ) { clearTimeout( maxTimeout ); } maxTimeout = setTimeout( function() { setMaxColour( colour.tiny.toHexString(), Math.round(colour.rgba.a*255) ); }, 500 ); } }); $("#minc").ColorPickerSliders({ color: endColor.toRgbString(), flat: true, size: 'sm', placement: 'left', customswitches: false, order: { hsl: 1, opacity: 2 }, onchange: function( container, colour ) { if ( minTimeout ) { clearTimeout( minTimeout ); } minTimeout = setTimeout( function() { setMinColour( colour.tiny.toHexString(), Math.round(colour.rgba.a*255) ); }, 500 ); } }); coloursInitialised = true; } function updateURL( whattype ) { // and if we aren't processing a URL hash at the moment then // update the URL as well if ( ! processingUrl ) { var url = '#'; for( var i = 0; i < hrefOrder.length; i++, url += ',' ) { if ( selected[hrefOrder[i]] ) { url += selected[hrefOrder[i]]; } } for( i = 0; i < options.length; i++ ) { if ( selected[options[i]] ) { url += options[i] + ";"; } } // we changed it, this will stop us doing anything with it lasthash = url; // if ( history.pushState && updateHistory[whattype] ) { // history.pushState( null, null, url ); // } else { location.replace( url ); // } } } function reReadhash() { // make sure the hash isn't the same as we think it is // this should stop us parsing our own changes if ( location.hash === lasthash ) { return; } readhash(); // update map map.getView().setZoom(dZoom); map.getView().setCenter(ol.proj.fromLonLat([dLon, dLat])); // make sure canvas redrawn if zoom and center stays same !!! // update the coverage layer coverageLayer.getSource().changed(); } function readhash() { lasthash = location.hash; processingUrl = true; var vals = location.hash.substr(1).split(','); console.log( location.hash.substr(1) + "|%%%%|" + vals[0] ); // do options first as adjustMap will force the redraw if ( vals[6] ) { var userOptions = vals[6].split(';'); var newOption = ~(userOptions.indexOf('airports')) ? 'airports' : ''; setOptions('airports', newOption); newOption = ~(userOptions.indexOf('airspace')) ? 'airspace' : ''; setOptions('airspace', newOption); newOption = ~(userOptions.indexOf('circles')) ? 'circles' : ''; setOptions('circles', newOption); newOption = ~(userOptions.indexOf('ambiguity')) ? 'ambiguity' : ''; setOptions('ambiguity', newOption); } // set colour if ( vals[5] ) { setColour( vals[5] ); } else { setColour( defaultColour ); } initColour(selected['colour']); // set date if (vals[2]) { setDates(vals[2]); } else { setDates('all'); } // set graph source if ( vals[1] ) { setSource(vals[1]); } // set station if ( vals[0] ) { setStation(vals[0]); } // set center (lat_lon) if ( vals[3] ) { var coords = vals[3].split('_'); if ( coords.length ) { setCentre( [ parseFloat(coords[1]), parseFloat(coords[0]) ] ); } } // set zoom if ( vals[4] ) { setZoom(parseInt(vals[4])); } processingUrl = false; } // Force a hex value to have 2 characters function pad2(c) { return c.length == 1 ? '0' + c : '' + c; }