/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Wait for the deviceready event before using any of Cordova's device APIs. // See https://cordova.apache.org/docs/en/latest/cordova/events/events.html#deviceready document.addEventListener('deviceready', onDeviceReady, false); // map from sondeid to marker (and path) var markers = {}; var ready = 0; var map = null; /////var lastObj = { obj: null, marker: null, /*no longer used: */pred: null, land: null }; var lastMarker = null; var mypos = {lat: 48.56, lon: 13.43, hdop: 25, alt: 480}; var myposMarker = null; var ballonIcon, landIcon, burstIcon; var infobox = null; //var checkMark = "✅"; var checkMark = "✔"; var crossMark = "❌"; var offlineMap = localStorage.getItem("mapstorage"); if(!offlineMap) offlineMap="file:///sdcard/Android/data/de.dl9rdz.files/"; console.log("Map storage location: "+offlineMap); // add "top center" and "bottom center" to leaflet (function (L) { L.Map.prototype._initControlPos = function(_initControlPos) { return function() { _initControlPos.apply(this, arguments); // original function this._controlCorners['bottomcenter'] = L.DomUtil.create('div', 'leaflet-bottom leaflet-center', L.DomUtil.create('div', 'leaflet-control-bottomcenter', this._controlContainer) ); this._controlCorners['topcenter'] = L.DomUtil.create('div', 'leaflet-top leaflet-center', L.DomUtil.create('div', 'leaflet-control-topcenter', this._controlContainer) ); }; } (L.Map.prototype._initControlPos); }(L, this, document)); // Let's add bearing calculation to latLngs... L.LatLng.prototype.bearingTo = function(target) { var lat1 = this.lat * Math.PI / 180; var lat2 = target.lat * Math.PI / 180; var dLon = (target.lng-this.lng) * Math.PI / 180; //console.log("b2: "+lat1+", "+lat2+", "+dLon+" -- "+JSON.stringify(target)); var y = Math.sin(dLon) * Math.cos(lat2); var x = Math.cos(lat1)*Math.sin(lat2) - Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon); var bearing = Math.atan2(y, x) * 180 / Math.PI; bearing = ( parseInt( bearing ) + 360 ) % 360; console.log("bearing : "+x+", "+y+", => "+bearing); return bearing; }; function onDeviceReady() { // Cordova is now initialized. Have fun! console.log('Running cordova-' + cordova.platformId + '@' + cordova.version); // Check for updates fetch("https://raw.githubusercontent.com/dl9rdz/rdzwx-go/main/version.json") .then(response => response.json()) .then(data => { console.log('Success:', data); if(data.version > "1.0.7") { if(window.confirm("New version "+ data.version + " available! Download?")) { console.log("opening "+data.url); cordova.InAppBrowser.open(data.url, "_system"); } } }) .catch((error) => { console.error('Error:', error); }); // Some map tile sources var tfapikey = "01be52efbdc14d38beac233a870c8d4f"; var tfland = L.tileLayer('https://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png?apikey=' + tfapikey, {attribution: '© Thunderforest, © OpenStreetMap'}), tftrans = L.tileLayer('https://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png?apikey=' + tfapikey, {attribution: '© Thunderforest, © OpenStreetMap'}), tfout = L.tileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey=' + tfapikey, {attribution: '© Thunderforest, © OpenStreetMap'}), tfcycle = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=' + tfapikey, {attribution: '© Thunderforest, © OpenStreetMap'}), tfatlas = L.tileLayer('https://{s}.tile.thunderforest.com/mobile-atlas/{z}/{x}/{y}.png?apikey=' + tfapikey, {attribution: '© Thunderforest, © OpenStreetMap'}), opentopo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {attribution: 'Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © OpenTopoMap (CC-BY-SA)'}), sat = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'}) L.OfflineTileLayer = L.TileLayer.extend({ getTileUrl: function(tilePoint, tile, done) { var tilesrc; var z = tilePoint.z, x = tilePoint.x, y = tilePoint.y; console.log("Coord: "+x+","+y+","+z); console.log("this: " + this); tile.thethis = this; RdzWx.gettile(x, y, z, function(result) { if(result.tile) { console.log("gettile: success: " + result.tile); tile.onload = L.Util.bind(tile.thethis._tileOnLoad, this, done, tile); tile.src = result.tile; //done(tile); } else { console.log("gettile: success but no tile"); tile.src = "img/MapTileUnavailable.png"; done(tile); } }, function(error) { console.log("gettile: error: " + error); tile.src = "img/MapTileUnavailable.png"; done(tile); }); console.log("getTileUrl returning..."); //return tilestr; }, createTile: function (coords, done) { var tile = document.createElement('img'); //DomEvent.on(tile, 'load', Util.bind(this._tileOnLoad, this, done, tile)); //DomEvent.on(tile, 'error', Util.bind(this._tileOnError, this, done, tile)); if (this.options.crossOrigin || this.options.crossOrigin === '') { tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin; } /* Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons http://www.w3.org/TR/WCAG20-TECHS/H67 */ tile.alt = ''; /* Set role="presentation" to force screen readers to ignore this https://www.w3.org/TR/wai-aria/roles#textalternativecomputation */ tile.setAttribute('role', 'presentation'); //tile.src = this.getTileUrl(coords); this.getTileUrl(coords, tile, done); return tile; } /* _loadTile: function(tile, tilePoint) { tile._layer = this; tile.onload = this._tileOnLoad; tile.onerror = this._tileOnError; this._adjustTilePoint(tilePoint); this.getTileURL(tilePoint, tile); this.fire("tileloadstart", { tile: tile, url: tile.src } ); }, */ }); L.offlineTileLayer = function(url, options) { return new L.OfflineTileLayer(url, options); }; var offline = L.offlineTileLayer("http://NOWHERE", {attribution: 'Map data © OpenStreetMap contributors', maxZoom: 22}); Stamen_TonerHybrid = L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner-hybrid/{z}/{x}/{y}{r}.{ext}', { attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', subdomains: 'abcd', minZoom: 0, maxZoom: 18, ext: 'png' }), osm = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { maxZoom: 20, attribution: '© OpenStreetMap contributors' }); var hybrid = new L.layerGroup([sat, Stamen_TonerHybrid]); map = L.map('map', { layers: [osm], contextmenu: true, zoomControl: false} ).setView([48,13],12); var baseMaps = { 'Offline': offline, "Openstreetmap": osm, "Landscape": tfland, "Transport": tftrans, "Outdoors": tfout, "Atlas": tfatlas, "OpenCycleMap": tfcycle, "OpenTopoMap" : opentopo, "Sat": sat, "Sat/Hybrid": hybrid }; var baseMapControl = new L.control.layers(baseMaps, {}, { collapsed: true, position: 'topright' } ).addTo(map); baseMaps["Openstreetmap"].addTo(map); // not working.......... map.addEventListener('baselayerchange', baseMapControl.collapse() ); L.control.scale({metric: true, imperial: false, position: "bottomright"}).addTo(map); // main menu L.easyButton('≡', function(btn, map) { toolbar = L.DomUtil.get("toolbar"); L.DomUtil.addClass(toolbar, "open"); toolbarclose = L.DomUtil.get("toolbarclose"); L.DomEvent.on(toolbarclose, 'click', function(e) {L.DomUtil.removeClass(toolbar, "open")}); }).addTo(map); new L.Control.Zoom({position: "topleft" }).addTo(map); // prediction tbtn = L.easyButton('⌖', function(btn, map) { getPrediction(); }).addTo(map); L.DomEvent.on(tbtn.button, 'contextmenu', function(e) { tawhiriCtl.toggle(); } ); map.locate({setView: true, maxZoom: 16}); var TawhiriCtl = L.Control.extend({ options: { position: 'bottomcenter' }, onAdd: function(map) { var tawhiriContainer = L.DomUtil.create('div', 'leaflet-control-layers leaflet-control'); var tawhiriBody = L.DomUtil.create('div', 'leaflet-popup-content-wrapper'); tawhiriContainer.appendChild(tawhiriBody); var tawhiriContent = L.DomUtil.create('div', 'leaflet-popup-content tawhiridiv'); tawhiriBody.appendChild(tawhiriContent); var infoCloseButton = L.DomUtil.create('a', 'leaflet-popup-close-button'); tawhiriContainer.appendChild(infoCloseButton); infoCloseButton.innerHTML = 'x'; infoCloseButton.setAttribute('style', 'cursor: pointer'); var infoContent = L.DomUtil.create('div', 'tawhiricontent'); infoContent.innerHTML = '
RS41 | R1234567 |
403.012 MHz | + 1.2 kHz |
" + sym + obj.type + " | " + obj.ser + " |
" + (1*obj.freq).toFixed(3) + " MHz | " + (0.001*obj.afc).toFixed(2) + " kHz |
" + ll2str(obj.lat,false) + " | " + ll2str(obj.lon,true) + " |
" + obj.alt.toFixed(0) + "m | " + obj.vs + "m/s | " + (obj.hs*3.6).toFixed(1) + "km/h |
RSSI: " + -0.5*obj.rssi + " | " + distance + " |
'; l2 += ' | ';
l2 += distance + ' '; l2 += ''+ obj.alt.toFixed(0) + "m "+obj.vs+'m/s '; l2 += ''+ (obj.hs*3.6).toFixed(1)+' km/h '; l2 += 'RSSI: '+ -0.5*obj.rssi + ' |
Lat: ' + lay.getLatLng().lat.toFixed(5) + '
' +
'Lon: ' + lay.getLatLng().lng.toFixed(5) + '
' +
'Altutide: ' + alt + '
HDOP: ' + (lay.hdop>0 ? lay.hdop : 'no GPS fix') + '
'; }); document.addEventListener("pause", onPause); document.addEventListener("resume", onResume); document.addEventListener("backbutton", onBackButton); } function ll2str(l,islon) { var res; if(islon) { res = l<0 ? "W":"E"; } else { res = l<0 ? "S":"N"; } if(l<0) l=-l; return res + l.toFixed(5); } // so let's try this approach for state management // - "back" button on main screen -> close app (state is lost)? // - "pause" event with TTGO connected: keep connection and all running in background, create notification entry // without TTGO connected: stop background thread // - "resume" event: if stopped, start background thread function onPause() { if(ttgoStatus.state() == 'offline') { console.log("onPause(): TTGO is offline, stopping all activities"); window.localStorage.setItem('lastgps', JSON.stringify(mypos)); RdzWx.stop("", function(){}); } else { console.log("onPause(): TTGO is online, keeping activities running in background"); } } function onResume() { console.log("onResume()"); //if(ttgoStatus.state() == 'offline') { // if already started (not stopped in onPause()), start will do nothign.... RdzWx.start("testarg", callBack); //} } function onBackButton() { console.log("onBackButton(): Exit"); window.localStorage.setItem('lastgps', JSON.stringify(mypos)); RdzWx.stop("", function(){}); navigator.app.exitApp(); // note: this will also call onPause() } function formatParams(params) { return '?' + Object.keys(params).map( function(key) { return key+"="+encodeURIComponent(params[key]) }).join('&'); } // borrowed from wetterson.de/karte ..... function calc_drag(drag,alt,desc){ if (alt < 1000 ){ drag = drag * 1; } else if (alt < 2000){ dragfak = (( alt - 1000 ) * ( 0.98 - 1) / ( 2000 - 1000)) + 1; drag = drag * dragfak; } else if (alt < 3000){ dragfak = (( alt - 2000 ) * ( 0.95 - 0.98) / ( 3000 - 2000)) + 0.98; drag = drag * dragfak; } else if (alt < 6000){ dragfak = (( alt - 3000 ) * ( 0.75 - 0.95) / ( 6000 - 3000)) + 0.95; drag = drag * dragfak; } else if (alt < 8000){ dragfak = (( alt - 6000 ) * ( 0.62- 0.75) / ( 8000 - 6000)) + 0.75; drag = drag * dragfak; } else if (alt < 10000){ dragfak = (( alt - 8000 )* ( 0.55 - 0.62) / ( 10000 - 8000)) + 0.62; drag = drag * dragfak; } else if (alt < 20000){ dragfak = (( alt - 10000 )* ( 0.3 - 0.55) / ( 20000 - 10000)) + 0.55; drag = drag * dragfak; } else { drag = desc; } return drag; } function removePrediction(marker) { if(marker.pred) { marker.pred.remove(map); } if(marker.land) { marker.land.remove(map); } if(marker.burst) { marker.burst.remove(map); } } function getPrediction(refobj) { // going out of service soon... TAWHIRI = 'http://predict.cusf.co.uk/api/v1'; TAWHIRI = 'https://api.v2.sondehub.org/tawhiri'; if(refobj == null) { refobj = lastMarker; } if(refobj == null) { alert("no object available"); return; } // lookup parameters from form var burst = document.getElementById("tawhiri-burst").value; if(burst) burst= parseInt(burst); else burst=35000; if(refobj.obj.alt > burst) burst = refobj.obj.alt; var asc = document.getElementById("tawhiri-ascent").value; if(asc) asc=parseFloat(asc); else asc=5.0; var desc = document.getElementById("tawhiri-descent").value; if(desc) desc=parseFloat(desc); else desc=5.0; var usecurrent = document.getElementById("tawhiri-current").checked; var lon = refobj.obj.lon; if(lon<0) lon+=360; // tawhiri api needs 0..360 var tParams = { "launch_latitude": refobj.obj.lat, "launch_longitude": lon, "launch_altitude": refobj.obj.alt.toFixed(1), "launch_datetime": new Date().toISOString().split('.')[0] + 'Z', "ascent_rate": asc, "descent_rate": desc, "burst_altitude": (refobj.obj.alt+2).toFixed(1), "profile": "standard_profile", } var vs = refobj.obj.vs; if( refobj.vsavg ) { vs = refobj.vsavg; if(vs*refobj.obj.vs < 0) vs=refobj.obj.vs; } if(vs > 0) { // still climbing up tParams["ascent_rate"] = usecurrent ? vs : asc; if(burst > refobj.obj.alt+2) { tParams["burst_altitude"] = burst; } } else { tParams["descent_rate"] = usecurrent ? calc_drag( -vs, refobj.obj.alt, desc ) : desc; } const xhr = new XMLHttpRequest(); const url = TAWHIRI + formatParams(tParams); xhr.onreadystatechange = function() { if(xhr.readyState === 4) { if( (xhr.status/100)!=2 ) { alert("Request failed: "+xhr.statusText); return; } var pred = JSON.parse(xhr.response); var traj0 = pred.prediction[0].trajectory; // 0 is ascent, 1 is descent... var traj1 = pred.prediction[1].trajectory; // 0 is ascent, 1 is descent... var latlons = []; traj0.forEach( p => latlons.push( [p.latitude, wrap(p.longitude)] ) ); traj1.forEach( p => latlons.push( [p.latitude, wrap(p.longitude)] ) ); //alert("path: "+JSON.stringify(traj)); poly = L.polyline(latlons, { opacity: 0.7, color: '#EE0000', dashArray: '8, 6'} ); poly.addTo(map); if( refobj.pred ) { refobj.pred.remove(map); } refobj.pred = poly; if( refobj.land ) { refobj.land.remove(map); } refobj.land = new L.marker(latlons.slice(-1)[0], {icon: landingIcon, contextmenu: true, contextmenuItems: [{ text: "Zoom to location", callback: function(e) { b=new L.LatLngBounds([refobj.land.getLatLng()]); map.fitBounds(b, {maxZoom: 16}); } }, { separator: true }, { text: "Export to map app", callback: function(e) { ll=refobj.land.getLatLng(); uri="geo:0:0?q="+ll.lat+","+ll.lng+"(X-"+refobj.obj.id+")"; RdzWx.showmap(uri, function(){}); } }] }); refobj.land.addTo(map); if( refobj.burst ) { refobj.burst.remove(map); } if( vs>0 ) { // still climbing, so add burst mark var b = traj0.slice(-1)[0]; refobj.burst = new L.marker( [b.latitude, b.longitude], {icon: burstIcon}); refobj.burst.addTo(map); } var lastpt = traj1.splice(-1)[0]; lastpt.datetime = new Date(lastpt.datetime).toISOString().split(".")[0] + "Z"; var popup = 'Altitude: ' + lastpt.altitude.toFixed(1) + ' m'+ 'Asc. Rate: ' + tParams["ascent_rate"].toFixed(2) + ' m/s'+ 'Burst: ' + tParams["burst_altitude"] + ' m'+ 'Desc. Rate: ' + tParams["descent_rate"].toFixed(2) + ' m/s
' + ''; refobj.land.bindPopup(popup); } } xhr.open('GET', url, true); xhr.send(null); } function callBack(arg) { var obj; try { console.log("callback: "+arg); obj = JSON.parse(arg); } catch(err) { console.log("callBack: JSON error: "+arg+": "+err.message); return; } update(obj); // for now, only for electron (does not support keepCallback) //RdzWx.next(callBack); } function updateMypos(obj) { console.log("updateMypos"); infobox._updateMypos(obj); if(obj.hdop<0) { // GPS fix lost console.log("gps fix lost"); if(myposMarker.hdop) myposMarker.hdop = 0; if(myposMarker.hdopCircle) { map.removeLayer(myposMarker.hdopCircle); myposMarker.hdopCircle = null; } return; } mypos = obj; var pos = [obj.lat, obj.lon, obj.alt]; myposMarker.setLatLng(pos); myposMarker.update(); if(myposMarker.hdop) { myposMarker.hdopCircle.setLatLng(pos) if(obj.hdop != myposMarker.hdop) { myposMarker.hdopCircle.setRadius(obj.hdop); myposMarker.hdop = obj.hdop; } } else { if(obj.hdop) { myposMarker.hdopCircle = L.circle(pos, {radius: obj.hdop, dashArray: "2 2" }).addTo(map); myposMarker.hdop = obj.hdop; } } } function periodicStatusCheck() { now = new Date(); if( lastMsgTS && (now-lastMsgTS) > 10000 ) { // handle connection broken (if still connnected) //alert("Closing conn: "+now+" vs "+lastMsgTS); console.log("no data for 10 seconds, closing connection to rdzTTGOsonde"); lastMsgTS = 0; RdzWx.closeconn("", function(){}); } } function update(obj) { if(!ready || !map) { console.log("not ready"); return; } lastMsgTS = new Date(); if(obj.msgtype) { if(obj.msgtype == "ttgostatus") { ttgoStatus.ttgourl = 'http://' + obj.ip; ttgoStatus.state(obj.state) if(obj.state=="offline") { infobox.setStatus(1); } } if(obj.msgtype == "gps") { updateMypos(obj); } console.log("update: type="+obj.msgtype); return; } // position update //console.log("Pos update: "+JSON.stringify(obj)); if(obj.egmdiff && obj.alt) { obj.alt -= obj.egmdiff; } infobox.setContent(obj); infobox.setStatus(obj.res); var isValidPos = true; if( ((obj.validPos&0x03) != 0x03) || ((obj.validPos&0x80)!=0) ) { // latitude and longitude are invalid isValidPos = false; } var marker; if( (!obj.validId) || (!isValidPos) || (obj.res!=0) ) { // no valid pos... // res: 1=Timeout, 2=CRC error, 3=unknown, 4=no position // Check if it is an object marked "old" from TTGO which we do not yet have on the map if( ((obj.validPos&0x3)==0x3) && obj.validId && !markers[obj.id]) { console.log("pos update: Adding old TTGO pos: "+JSON.stringify(obj)); marker = createNewMarker(obj); updateMarkerTooltip(marker, obj); markers[obj.id] = marker; lastMarker = marker } else { console.log("pos update: No valid update: "+JSON.stringify(obj)); } return; } console.log("pos update: Good update! "+JSON.stringify(obj)); var pos = new L.LatLng(obj.lat, obj.lon); if(markers[obj.id]) { marker = markers[obj.id]; if(pos.equals(marker.getLatLng())) { console.log("update: position unchanged"); } else { marker.path.addLatLng(pos); console.log("update: appending new position"); } marker.vsavg = 0.9 * marker.vsavg + 0.1 * obj.vs; } else { marker = createNewMarker(obj); markers[obj.id] = marker; } lastMarker = marker; updateMarkerTooltip(marker, obj); } function updateMarkerTooltip(marker, obj) { var tt = 'Serial: '+ lay.obj.ser + '
' +
''+(new Date(1000*lay.obj.time)).toString().split(" (")[0] + '
' +
'(' + formathms( new Date().valueOf() / 1000 - lay.obj.time ) + ' ago)
' +
'Frame #'+lay.obj.frame+', Sats='+lay.obj.sats + '
' +
'burstKT='+formathms(lay.obj.burstKT)+'
launchKT='+formathms(lay.obj.launchKT)+'
countdown='+formathms(lay.obj.countKT+lay.obj.crefKT-lay.obj.frame)+'
' +
'