diff --git a/htdocs/includes/repositories/packetpathrepository.class.php b/htdocs/includes/repositories/packetpathrepository.class.php index 1cb5d02..cadeae5 100644 --- a/htdocs/includes/repositories/packetpathrepository.class.php +++ b/htdocs/includes/repositories/packetpathrepository.class.php @@ -95,4 +95,36 @@ class PacketPathRepository extends ModelRepository $stmt = $pdo->prepareAndExec($sql, $args); return $stmt->fetchAll(PDO::FETCH_ASSOC); } + + /** + * Get latest data list by receiving station id + * + * @param int $stationId + * @param int $hours + * @param int $limit + * @return array + */ + public function getLatestDataListByReceivingStationId($stationId, $hours, $limit) + { + if (!isInt($stationId) || !isInt($hours)) { + return []; + } + $minTimestamp = time() - (60*60*$hours); + + $sql = 'select pp.* + from packet_path pp + where pp.station_id = ? + and pp.timestamp >= ? + and pp.number = 0 + and pp.sending_latitude is not null + and pp.sending_longitude is not null + order by pp.timestamp + limit ?'; + + $arg = [$stationId, $minTimestamp, $limit]; + + $pdo = PDOConnection::getInstance(); + $stmt = $pdo->prepareAndExec($sql, $arg); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } } diff --git a/htdocs/public/data/coverage.php b/htdocs/public/data/coverage.php new file mode 100644 index 0000000..c1ea3b8 --- /dev/null +++ b/htdocs/public/data/coverage.php @@ -0,0 +1,24 @@ +getObjectById($_GET['id'] ?? null); +if ($station->isExistingObject()) { + $response['station_id'] = $station->id; + $response['coverage'] = []; + + $numberOfHours = 10*24; // latest 10 days should be enough + $limit = 10000; // Limit number of packets to reduce load on server (and browser) + $packetPaths = PacketPathRepository::getInstance()->getLatestDataListByReceivingStationId($_GET['id'] ?? null, $numberOfHours, $limit); + foreach ($packetPaths as $path) { + $row = []; + $row['latitude'] = $path['sending_latitude']; + $row['longitude'] = $path['sending_longitude']; + $row['distance'] = $path['distance']; + $response['coverage'][] = $row; + } +} + +header('Content-type: application/json'); +echo json_encode($response); diff --git a/htdocs/public/index.php b/htdocs/public/index.php index 9d5ef77..50bf9dc 100755 --- a/htdocs/public/index.php +++ b/htdocs/public/index.php @@ -20,6 +20,7 @@ + @@ -56,6 +57,7 @@ var options = {}; options['isMobile'] = false; options['useImperialUnit'] = ; + options['coverageDataUrl'] = 'data/coverage.php';; var md = new MobileDetect(window.navigator.userAgent); if (md.mobile() !== null) { diff --git a/htdocs/public/js/convex-hull.js b/htdocs/public/js/convex-hull.js new file mode 100644 index 0000000..376cb7f --- /dev/null +++ b/htdocs/public/js/convex-hull.js @@ -0,0 +1,87 @@ +/* + * Convex hull algorithm - Library (compiled from TypeScript) + * + * Copyright (c) 2021 Project Nayuki + * https://www.nayuki.io/page/convex-hull-algorithm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ +"use strict"; +var convexhull; +(function (convexhull) { + // Returns a new array of points representing the convex hull of + // the given set of points. The convex hull excludes collinear points. + // This algorithm runs in O(n log n) time. + function makeHull(points) { + var newPoints = points.slice(); + newPoints.sort(convexhull.POINT_COMPARATOR); + return convexhull.makeHullPresorted(newPoints); + } + convexhull.makeHull = makeHull; + // Returns the convex hull, assuming that each points[i] <= points[i + 1]. Runs in O(n) time. + function makeHullPresorted(points) { + if (points.length <= 1) + return points.slice(); + // Andrew's monotone chain algorithm. Positive y coordinates correspond to "up" + // as per the mathematical convention, instead of "down" as per the computer + // graphics convention. This doesn't affect the correctness of the result. + var upperHull = []; + for (var i = 0; i < points.length; i++) { + var p = points[i]; + while (upperHull.length >= 2) { + var q = upperHull[upperHull.length - 1]; + var r = upperHull[upperHull.length - 2]; + if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) + upperHull.pop(); + else + break; + } + upperHull.push(p); + } + upperHull.pop(); + var lowerHull = []; + for (var i = points.length - 1; i >= 0; i--) { + var p = points[i]; + while (lowerHull.length >= 2) { + var q = lowerHull[lowerHull.length - 1]; + var r = lowerHull[lowerHull.length - 2]; + if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) + lowerHull.pop(); + else + break; + } + lowerHull.push(p); + } + lowerHull.pop(); + if (upperHull.length == 1 && lowerHull.length == 1 && upperHull[0].x == lowerHull[0].x && upperHull[0].y == lowerHull[0].y) + return upperHull; + else + return upperHull.concat(lowerHull); + } + convexhull.makeHullPresorted = makeHullPresorted; + function POINT_COMPARATOR(a, b) { + if (a.x < b.x) + return -1; + else if (a.x > b.x) + return +1; + else if (a.y < b.y) + return -1; + else if (a.y > b.y) + return +1; + else + return 0; + } + convexhull.POINT_COMPARATOR = POINT_COMPARATOR; +})(convexhull || (convexhull = {})); \ No newline at end of file diff --git a/htdocs/public/js/trackdirect.min.js b/htdocs/public/js/trackdirect.min.js index 3e3ba9d..d730ec1 100755 --- a/htdocs/public/js/trackdirect.min.js +++ b/htdocs/public/js/trackdirect.min.js @@ -1,4 +1,4 @@ -var trackdirect={services:{},models:{},_time:null,_timetravel:null,_center:null,_zoom:null,_maptype:null,_mid:null,_rulers:[],_filterTimeoutId:null,_waitForFilterResponse:false,_doNotChangeLocationOnFilterResponse:false,_doNotChangeLocationOnFilterResponseTmp:false,_filters:{},_defaultLatitude:null,_defaultLongitude:null,_eventListeners:{},_eventListenersOnce:{},_cordinatesContainerElementId:null,_statusContainerElementId:"td-status-text",_mapElementId:null,_wsServerUrl:null,_map:null,_websocket:null,_mapCreated:false,_trackdirectInitDone:false,isMobile:false,settings:{},init:function(wsServerUrl,mapElementId,options){this._initSettings();this._wsServerUrl=wsServerUrl;this._mapElementId=mapElementId;if($("#"+mapElementId).length<=0){console.log("ERROR: Specified map element missing");return;} +var trackdirect={services:{},models:{},_time:null,_timetravel:null,_center:null,_zoom:null,_maptype:null,_mid:null,_rulers:[],_filterTimeoutId:null,_waitForFilterResponse:false,_doNotChangeLocationOnFilterResponse:false,_doNotChangeLocationOnFilterResponseTmp:false,_filters:{},_defaultLatitude:null,_defaultLongitude:null,_eventListeners:{},_eventListenersOnce:{},_cordinatesContainerElementId:null,_statusContainerElementId:"td-status-text",_mapElementId:null,_wsServerUrl:null,_map:null,_websocket:null,_mapCreated:false,_trackdirectInitDone:false,isMobile:false,coverageDataUrl:null,settings:{},init:function(wsServerUrl,mapElementId,options){this._initSettings();this._wsServerUrl=wsServerUrl;this._mapElementId=mapElementId;if($("#"+mapElementId).length<=0){console.log("ERROR: Specified map element missing");return;} if(typeof google==="object"&&typeof google.maps==="object"){this.settings.defaultMinZoomForMarkerLabel=12;this.settings.minZoomForMarkerLabel=12;} this._parseOptions(options);var me=this;this.addListener("map-created",function(){me._initTime();me._websocket=new trackdirect.Websocket(me._wsServerUrl);me._initWebsocketListeners();me._handleWebsocketStateChange();me._initMapListeners();if(!me._initFilterUrlRequest()){trackdirect.services.callbackExecutor.add(me,me._sendPositionRequest,[]);} me._setWebsocketStateIdle();if(inIframe()){var parentUrl="";try{parentUrl=window.location!=window.parent.location?document.referrer:document.location.href;}catch(e){parentUrl="Unknown";}} @@ -15,7 +15,8 @@ if(stationId!==null){var trackLinkElementClass="trackStationLink"+stationId;$(". if(this._map.state.trackStationId!==null){var trackLinkElementClass="trackStationLink"+this._map.state.trackStationId;$("."+trackLinkElementClass).html("Track");} this._map.state.onlyTrackRecentPackets=onlyTrackRecentPackets;this._map.state.trackStationId=stationId;this._emitEventListeners("track-changed",[stationId,stationName]);},focusOnStation:function(stationId,openInfoWindow){var map=this._map;openInfoWindow=typeof openInfoWindow!=="undefined"?openInfoWindow:false;var marker=map.markerCollection.getStationLatestMarker(stationId);if(marker!==null){marker.show();marker.showLabel();if(openInfoWindow){map.openMarkerInfoWindow(marker,false);}else{this.setCenter(marker.packet.latitude,marker.packet.longitude);} marker.hide(5000,true);}},focusOnMarkerId:function(markerId,zoom){var map=this._map;var markerIdKey=map.markerCollection.getMarkerIdKey(markerId);if(map.markerCollection.isExistingMarker(markerIdKey)){var marker=map.markerCollection.getMarker(markerIdKey);if(map.markerCollection.hasRelatedDashedPolyline(marker)){newerMarker=map.markerCollection.getMarker(marker._relatedMarkerOriginDashedPolyLine.ownerMarkerIdKey);if(newerMarker.packet.hasConfirmedMapId()){return this.focusOnMarkerId(newerMarker.packet.marker_id);}} -marker.show();marker.showLabel();this.setCenter(marker.packet.latitude,marker.packet.longitude,zoom);map.openMarkerInfoWindow(marker);marker.hide(5000,true);}},setMapType:function(mapType){if(this._map!==null){this._map.setMapType(mapType);}},getMapType:function(){if(this._map!==null){return this._map.getMapType();}},setMapDefaultLocation:function(setDefaultZoom){this._map.setMapDefaultLocation(setDefaultZoom);},setMapLocationByGeoLocation:function(failCallBack,successCallBack,timeout){var me=this;if(navigator&&navigator.geolocation){navigator.geolocation.getCurrentPosition(function(position){var pos={lat:position.coords.latitude,lng:position.coords.longitude,};me._map.setCenter(pos,12);if(successCallBack!==null){successCallBack();}},function(error){if(failCallBack!==null){failCallBack(error.message);}},{enableHighAccuracy:false,timeout:timeout,maximumAge:5000,});}else{if(failCallBack!==null){failCallBack();}}},openStationInformationDialog:function(stationId){var packet=this._map.markerCollection.getStationLatestPacket(stationId);if(packet==null){packet={station_id:stationId,id:null};} +marker.show();marker.showLabel();this.setCenter(marker.packet.latitude,marker.packet.longitude,zoom);map.openMarkerInfoWindow(marker);marker.hide(5000,true);}},toggleStationCoverage:function(stationId,coverageLinkElementClass){coverageLinkElementClass=typeof coverageLinkElementClass!=="undefined"?coverageLinkElementClass:null;var coveragePolygon=this._map.markerCollection.getStationCoverage(stationId);if(coveragePolygon!==null&&coveragePolygon.isRequestedToBeVisible()){coveragePolygon.hide();if(coverageLinkElementClass!==null){$("."+coverageLinkElementClass).html("Coverage");}}else{if(coveragePolygon!==null){coveragePolygon.show();if(!coveragePolygon.hasContent()){alert("Currently we do not have enough data to create a max range coverage plot for this station. Try again later!");}else{if(coverageLinkElementClass!==null){$("."+coverageLinkElementClass).html("Hide coverage");}}}else{var packet=this._map.markerCollection.getStationLatestPacket(stationId);var center={lat:parseFloat(packet.latitude),lng:parseFloat(packet.longitude),};var coveragePolygon=new trackdirect.models.StationCoveragePolygon(center,this._map,true);this._map.markerCollection.addStationCoverage(stationId,coveragePolygon);coveragePolygon.showWhenDone();if(coverageLinkElementClass!==null){$("."+coverageLinkElementClass).html('Loading ');coveragePolygon.addTdListener("visible",function(){if(!coveragePolygon.hasContent()){coveragePolygon.hide();alert("Currently we do not have enough data to create a max range coverage plot for this station. Try again later!");$("."+coverageLinkElementClass).html("Coverage");}else{$("."+coverageLinkElementClass).html("Hide coverage");}},true);} +var me=this;$.getJSON(this.coverageDataUrl+"?id="+stationId,function(data){if("station_id"in data&&"coverage"in data){coveragePolygon.setData(data["coverage"]);var marker=me._map.markerCollection.getStationLatestMarker(stationId);if(marker.isVisible()){if(coveragePolygon.isRequestedToBeVisible()){coveragePolygon.show();}}}}).fail(function(){coveragePolygon.hide();alert("Failed to fetch coverage data. Try again later!");$("."+coverageLinkElementClass).html("Coverage");}).always(function(){});}}},setMapType:function(mapType){if(this._map!==null){this._map.setMapType(mapType);}},getMapType:function(){if(this._map!==null){return this._map.getMapType();}},setMapDefaultLocation:function(setDefaultZoom){this._map.setMapDefaultLocation(setDefaultZoom);},setMapLocationByGeoLocation:function(failCallBack,successCallBack,timeout){var me=this;if(navigator&&navigator.geolocation){navigator.geolocation.getCurrentPosition(function(position){var pos={lat:position.coords.latitude,lng:position.coords.longitude,};me._map.setCenter(pos,12);if(successCallBack!==null){successCallBack();}},function(error){if(failCallBack!==null){failCallBack(error.message);}},{enableHighAccuracy:false,timeout:timeout,maximumAge:5000,});}else{if(failCallBack!==null){failCallBack();}}},openStationInformationDialog:function(stationId){var packet=this._map.markerCollection.getStationLatestPacket(stationId);if(packet==null){packet={station_id:stationId,id:null};} this._emitEventListeners("station-name-clicked",packet);},openMarkerInfoWindow:function(markerId){var markerIdKey=this._map.markerCollection.getMarkerIdKey(markerId);if(this._map.markerCollection.isExistingMarker(markerIdKey)){var marker=this._map.markerCollection.getMarker(markerIdKey);this._map.openMarkerInfoWindow(marker);}},closeAnyOpenInfoWindow:function(){if(this._map!==null){var state=this._map.state;if(state.isInfoWindowOpen()){state.openInfoWindow.hide();}}},setTimeTravelTimestamp:function(ts,sendRequestToServer){if(ts!=0||this._map.state.endTimeTravelTimestamp!=null){sendRequestToServer=typeof sendRequestToServer!=="undefined"?sendRequestToServer:true;if(this._map.state.endTimeTravelTimestamp!=ts){if(ts!=null&&ts!=0&&ts!=""){this._map.state.endTimeTravelTimestamp=ts;}else{this._map.state.endTimeTravelTimestamp=null;} if(sendRequestToServer){trackdirect.services.callbackExecutor.add(this,this._handleTimeChange,[]);}} this._emitEventListeners("time-travel-changed",ts);this._emitEventListeners("mode-changed");}},getTimeTravelTimestamp:function(){return this._map.state.endTimeTravelTimestamp;},setTimeLength:function(time,sendRequestToServer){sendRequestToServer=typeof sendRequestToServer!=="undefined"?sendRequestToServer:true;if(this._map.state.getTimeLength()/60!=time){this._map.state.setTimeLength(time*60);if(sendRequestToServer){trackdirect.services.callbackExecutor.add(this,this._handleTimeChange,[]);}} @@ -35,6 +36,7 @@ var packet=this._map.markerCollection.getStationLatestPacket(stationId);if(packe this.trackStation(stationId,stationName,true);}}},_initSettings:function(){this.settings={animate:true,defaultMinZoomForMarkerLabel:11,defaultMinZoomForMarkerPrevPosition:11,defaultMinZoomForMarkerTail:9,minZoomForMarkerLabel:11,minZoomForMarkerPrevPosition:11,minZoomForMarkerTail:9,minZoomForMarkers:8,markerSymbolBaseDir:"/symbols/",imagesBaseDir:"/images/",defaultCurrentZoom:11,defaultCurrentZoomMobile:11,dateFormat:"L LTSZ",dateFormatNoTimeZone:"L LTS",host:"www.aprsdirect.com",baseUrl:"https://www.aprsdirect.com",defaultTimeLength:60,symbolsToScale:[],primarySymbolWithNoDirectionPolyline:[87,64,95],alternativeSymbolWithNoDirectionPolyline:[40,42,64,74,84,85,96,98,101,102,112,116,119,121,123,],};},_parseOptions:function(options){if(typeof options["cordinatesContainerElementId"]!==undefined){this._cordinatesContainerElementId=options["cordinatesContainerElementId"];} if(typeof options["statusContainerElementId"]!==undefined){this._statusContainerElementId=options["statusContainerElementId"];} if(typeof options["isMobile"]!==undefined){this.isMobile=options["isMobile"];} +if(typeof options["coverageDataUrl"]!==undefined){this.coverageDataUrl=options["coverageDataUrl"];} if(typeof options["time"]!==undefined){this._time=options["time"];} if(typeof options["timetravel"]!==undefined){this._timetravel=options["timetravel"];} if(typeof options["center"]!==undefined){this._center=options["center"];} @@ -257,6 +259,36 @@ trackdirect.models.TailPolyline.prototype.constructor=trackdirect.models.TailPol return null;};trackdirect.models.TailPolyline.prototype.pushPathItem=function(latLng){if(typeof google==="object"&&typeof google.maps==="object"){var path=google.maps.Polyline.prototype.getPath.call(this);path.push(latLng);}else if(typeof L==="object"){this.addLatLng(latLng);}};trackdirect.models.TailPolyline.prototype.removePathItem=function(index){if(typeof google==="object"&&typeof google.maps==="object"){var path=google.maps.Polyline.prototype.getPath.call(this);path.removeAt(index);}else if(typeof L==="object"){var list=this.getLatLngs();if(typeof list[index]!=="undefined"){list.splice(index,1);this.setLatLngs(list);}}};trackdirect.models.TailPolyline.prototype.getPathLength=function(index){if(typeof google==="object"&&typeof google.maps==="object"){var path=google.maps.Polyline.prototype.getPath.call(this);return path.getLength();}else if(typeof L==="object"){var list=this.getLatLngs();return list.length;}};trackdirect.models.TailPolyline.prototype.getPath=function(){if(typeof google==="object"&&typeof google.maps==="object"){return google.maps.Polyline.prototype.getPath.call(this);}else if(typeof L==="object"){return this.getLatLngs();} return[];};trackdirect.models.TailPolyline.prototype.getMap=function(){if(typeof google==="object"&&typeof google.maps==="object"){var map=google.maps.Polyline.prototype.getMap.call(this);if(typeof map!=="undefined"){return map;}}else if(typeof L==="object"){if(this._defaultMap.hasLayer(this)){return this._defaultMap;}} return null;};trackdirect.models.TailPolyline.prototype.setMarkerIdKey=function(markerIdKey){this.markerIdKey=markerIdKey;this.ownerMarkerIdKey=markerIdKey;this._addInfoWindowListener(markerIdKey);};trackdirect.models.TailPolyline.prototype.setRelatedMarkerIdKey=function(markerIdKey){this.relatedMarkerIdKey=markerIdKey;};trackdirect.models.TailPolyline.prototype.show=function(){if(typeof google==="object"&&typeof google.maps==="object"){if(typeof this.getMap()==="undefined"||this.getMap()===null){this.setMap(this._defaultMap);}}else if(typeof L==="object"){if(!this._defaultMap.hasLayer(this)){this.addTo(this._defaultMap);}}};trackdirect.models.TailPolyline.prototype.hide=function(){if(typeof google==="object"&&typeof google.maps==="object"){if(this.getMap()!==null){this.setMap(null);}}else if(typeof L==="object"){if(this._defaultMap.hasLayer(this)){this._defaultMap.removeLayer(this);}}};trackdirect.models.TailPolyline.prototype.addMarker=function(marker){if(typeof google==="object"&&typeof google.maps==="object"){var latLng=new google.maps.LatLng(parseFloat(marker.packet.latitude),parseFloat(marker.packet.longitude));latLng.marker=marker;this.pushPathItem(latLng);}else if(typeof L==="object"){var latLng=new L.latLng(parseFloat(marker.packet.latitude),parseFloat(marker.packet.longitude));latLng.marker=marker;this.addLatLng(latLng);}};trackdirect.models.TailPolyline.prototype._addInfoWindowListener=function(markerIdKey){var me=this;if(typeof google==="object"&&typeof google.maps==="object"){google.maps.event.addListener(this,"click",function(event){var marker=me._defaultMap.markerCollection.getMarker(markerIdKey);me._defaultMap.openPolylineInfoWindow(marker,event.latLng);});}else if(typeof L==="object"){this.on("click",function(event){var marker=me._defaultMap.markerCollection.getMarker(markerIdKey);me._defaultMap.openPolylineInfoWindow(marker,event.latlng);});}};trackdirect.models.TailPolyline.prototype._getGooglePolylineOptions=function(color){return{geodesic:false,strokeOpacity:0.6,strokeWeight:4,strokeColor:color,map:null,zIndex:100,};};trackdirect.models.TailPolyline.prototype._getLeafletPolylineOptions=function(color){return{opacity:0.7,weight:4,color:color,};}; +trackdirect.models.StationCoveragePolygon=function(center,map,tryToShowCoveragePolygon){tryToShowCoveragePolygon=typeof tryToShowCoveragePolygon!=="undefined"?tryToShowCoveragePolygon:true;this._showPolygon=tryToShowCoveragePolygon;this._map=map;this._center=center;this._isRequestedToBeVisible=false;this._polygon=null;this._polygonCoordinates=null;this._heatmapCoordinates=null;this._heatmap=null;this._tdEventListeners={};this._tdEventListenersOnce={};this._upperMaxRangeInMeters=1000*1000;this._percentile=95;this._paddingInPercentOfMaxRange=10;this._paddingMinInMeters=1000;};trackdirect.models.StationCoveragePolygon.prototype.setData=function(data){this._addParametersToData(data);this._heatmapCoordinates=this._getCoordinates(data);if(this._showPolygon){var maxRange=this._getCoveragePolygonMaxRange(data);if(maxRange<=0){this._showPolygon=false;}else{this._polygonCoordinates=this._getConvexHullCoordinates(data,maxRange);}} +if(typeof google==="object"&&typeof google.maps==="object"){this._googleMapsInit();}else if(typeof L==="object"){this._leafletInit();}};trackdirect.models.StationCoveragePolygon.prototype.addTdListener=function(event,handler,execOnce){execOnce=typeof execOnce!=="undefined"?execOnce:false;if(execOnce){if(!(event in this._tdEventListenersOnce)){this._tdEventListenersOnce[event]=[];} +this._tdEventListenersOnce[event].push(handler);}else{if(!(event in this._tdEventListeners)){this._tdEventListeners[event]=[];} +this._tdEventListeners[event].push(handler);}};trackdirect.models.StationCoveragePolygon.prototype.hasContent=function(){if(this._heatmapCoordinates!==null&&this._heatmapCoordinates.length>0){return true;} +return false;};trackdirect.models.StationCoveragePolygon.prototype.isRequestedToBeVisible=function(){return this._isRequestedToBeVisible;};trackdirect.models.StationCoveragePolygon.prototype.showWhenDone=function(){this._isRequestedToBeVisible=true;};trackdirect.models.StationCoveragePolygon.prototype.show=function(){if(typeof google==="object"&&typeof google.maps==="object"){if(this._polygon!==null){this._polygon.setMap(this._map);} +if(this._heatmap!==null){this._heatmap.setMap(this._map);}}else if(typeof L==="object"){if(this._polygon!==null){this._polygon.addTo(this._map);} +if(this._heatmap!==null){this._heatmap.addTo(this._map);}} +this._isRequestedToBeVisible=true;if(this._showPolygon&&this._polygonCoordinates!==null){this._emitTdEventListeners("visible");}else if(this._heatmapCoordinates!==null){this._emitTdEventListeners("visible");}};trackdirect.models.StationCoveragePolygon.prototype.hide=function(stillMarkAsVisible){stillMarkAsVisible=typeof stillMarkAsVisible!=="undefined"?stillMarkAsVisible:false;if(typeof google==="object"&&typeof google.maps==="object"){if(this._polygon!==null){this._polygon.setMap(null);} +if(this._heatmap!==null){this._heatmap.setMap(null);}}else if(typeof L==="object"){if(this._polygon!==null){this._map.removeLayer(this._polygon);} +if(this._heatmap!==null){this._map.removeLayer(this._heatmap);}} +if(!stillMarkAsVisible){this._isRequestedToBeVisible=false;} +this._emitTdEventListeners("hidden");};trackdirect.models.StationCoveragePolygon.prototype._googleMapsInit=function(){if(this._polygonCoordinates!==null&&this._polygonCoordinates.length>0){this._polygon=new google.maps.Polygon({paths:this._polygonCoordinates,strokeColor:"#0000FF",strokeOpacity:0,strokeWeight:0,fillColor:"#0000FF",fillOpacity:0.2,});} +if(this._heatmapCoordinates!==null&&this._heatmapCoordinates.length>0){var data=[];for(var i=0;i0){this._polygon=new L.polygon(this._polygonCoordinates,{color:"#0000FF",opacity:0,weight:0,fillColor:"#0000FF",fillOpacity:0.2,});} +if(this._heatmapCoordinates!==null&&this._heatmapCoordinates.length>0){var data=[];for(var i=0;imaxRange){continue;} +result.push(data[i].latLngLiteral);} +return result;};trackdirect.models.StationCoveragePolygon.prototype._getCoveragePolygonMaxRange=function(data){var maxRange=this._getDistancePercentile(data,this._percentile,this._upperMaxRangeInMeters);if(isNaN(maxRange)){maxRange=0;} +return maxRange;};trackdirect.models.StationCoveragePolygon.prototype._getDistancePercentile=function(data,percentile,upperMaxRange){var values=[];for(var i=0;imaxRange){continue;} +counter++;} +return counter;};trackdirect.models.StationCoveragePolygon.prototype._convertToXYPos=function(positions){var result=[];for(var i=0;i99999){if(this._map.state.useImperialUnit){distance=Math.round(trackdirect.services.imperialConverter.convertKilometerToMile(distance/1000)).toString()+" miles";}else{distance=Math.round(distance/1000).toString()+" km";}}else if(distance>999){if(this._map.state.useImperialUnit){distance=(Math.round(trackdirect.services.imperialConverter.convertKilometerToMile(distance/1000)*10)/10).toString()+" miles";}else{distance=(Math.round(distance/100)/10).toString()+" km";}}else{distance=distance.toString()+" m";} var bearing=Math.round(trackdirect.services.distanceCalculator.getBearing(p2,p1),0).toString();return bearing+"º "+distance;};trackdirect.models.Ruler.prototype._getPositionLiteral=function(marker){if(typeof google==="object"&&typeof google.maps==="object"){var latLng=marker.getPosition();if(typeof latLng!=="undefined"&&typeof latLng.lat==="function"){return{lat:latLng.lat(),lng:latLng.lng()};}else{return latLng;}}else if(typeof L==="object"){var latLng=marker.getLatLng();if(typeof latLng!=="undefined"){return{lat:latLng.lat,lng:latLng.lng};}else{return latLng;}} @@ -381,7 +413,7 @@ if(marker.overwrite==true&&packet.overwrite==0){return false;}else{if(marker.pac if(marker.packet.reported_timestamp!==null&&packet.reported_timestamp!==null&&Math.abs(marker.packet.reported_timestamp-packet.reported_timestamp)<600){if(marker.packet.reported_timestamp>packet.reported_timestamp){return true;}}}} if(packet.is_moving==1){var list=this._map.markerCollection.getStationMarkerIdKeys(packet.station_id);for(var relatedMarkerIdKey in list){var relatedMarker=this._map.markerCollection.getMarker(relatedMarkerIdKey);if(relatedMarker!==null&&relatedMarker.overwrite==false&&relatedMarker.packet.timestamp>packet.timestamp){return true;}}} return false;}; -trackdirect.models.MarkerCollection=function(){this._markerKeys={};this._markers=[];this._stationMarkers={};this._stationLastMovingMarkerIdKey={};this._stationLastMarker={};this._senderLastMarker={};this._positionMarkersIdKeys={};this._dotMarkers=[];this._markerPolyLines=[];this._markerOriginDashedPolyLines=[];this._mapSectorMarkerIdKeys={};this.resetAllMarkers();};trackdirect.models.MarkerCollection.prototype.getMarkerIdKey=function(markerId){if(!(markerId in this._markerKeys)){this._markers.push(null);this._markerPolyLines.push(null);this._dotMarkers.push(null);this._markerOriginDashedPolyLines.push(null);var markerIdKey=this._markers.length-1;this._markerKeys[markerId]=markerIdKey;return markerIdKey;}else{return this._markerKeys[markerId];}};trackdirect.models.MarkerCollection.prototype.isExistingMarker=function(markerIdKey){if(markerIdKey in this._markers&&this._markers[markerIdKey]!==null){return true;} +trackdirect.models.MarkerCollection=function(){this._markerKeys={};this._markers=[];this._stationMarkers={};this._stationLastMovingMarkerIdKey={};this._stationLastMarker={};this._senderLastMarker={};this._positionMarkersIdKeys={};this._dotMarkers=[];this._markerPolyLines=[];this._markerOriginDashedPolyLines=[];this._mapSectorMarkerIdKeys={};this._stationCoverage={};this.resetAllMarkers();};trackdirect.models.MarkerCollection.prototype.getMarkerIdKey=function(markerId){if(!(markerId in this._markerKeys)){this._markers.push(null);this._markerPolyLines.push(null);this._dotMarkers.push(null);this._markerOriginDashedPolyLines.push(null);var markerIdKey=this._markers.length-1;this._markerKeys[markerId]=markerIdKey;return markerIdKey;}else{return this._markerKeys[markerId];}};trackdirect.models.MarkerCollection.prototype.isExistingMarker=function(markerIdKey){if(markerIdKey in this._markers&&this._markers[markerIdKey]!==null){return true;} return false;};trackdirect.models.MarkerCollection.prototype.setMarker=function(markerIdKey,marker){if(marker!==null&&typeof marker.packet!=="undefined"){var packet=marker.packet;this._markers[markerIdKey]=marker;this._addStationMarkerId(markerIdKey,packet);this._addStationLastMarker(packet,marker);this.addPostionMarkerId(markerIdKey,packet);}};trackdirect.models.MarkerCollection.prototype.getMarker=function(markerIdKey){if(markerIdKey!==null&&markerIdKey in this._markers){return this._markers[markerIdKey];} return null;};trackdirect.models.MarkerCollection.prototype.getAllMarkers=function(){return this._markers;};trackdirect.models.MarkerCollection.prototype.removeMarker=function(markerIdKey){if(markerIdKey!==null&&markerIdKey in this._markers){this._markers.splice(markerIdKey,1);}};trackdirect.models.MarkerCollection.prototype.getNumberOfMarkers=function(){return this._markers.length;};trackdirect.models.MarkerCollection.prototype.setMarkerLabel=function(markerIdKey,label){if(markerIdKey in this._markers&&this._markers[markerIdKey]!==null){this._markers[markerIdKey].label=label;}};trackdirect.models.MarkerCollection.prototype.getMarkerLabel=function(markerIdKey){if(markerIdKey in this._markers&&this._markers[markerIdKey]!==null){return this._markers[markerIdKey].label;} return null;};trackdirect.models.MarkerCollection.prototype.hasMarkerLabel=function(markerIdKey){if(markerIdKey in this._markers&&this._markers[markerIdKey]!==null&&this._markers[markerIdKey].label!==null){return true;} @@ -390,6 +422,9 @@ for(var markerIdKey in this.getPositionMarkerIdKeys(packet.latitude,packet.longi return false;};trackdirect.models.MarkerCollection.prototype.addDotMarker=function(markerIdKey,dotMarker){if(markerIdKey in this._dotMarkers){if(this._dotMarkers[markerIdKey]===null){this._dotMarkers[markerIdKey]=[];} this._dotMarkers[markerIdKey].push(dotMarker);}};trackdirect.models.MarkerCollection.prototype.getDotMarkers=function(markerIdKey){if(markerIdKey in this._dotMarkers&&this._dotMarkers[markerIdKey]!==null){return this._dotMarkers[markerIdKey];} return[];};trackdirect.models.MarkerCollection.prototype.hasDotMarkers=function(markerIdKey){if(markerIdKey in this._dotMarkers&&this._dotMarkers[markerIdKey]!==null&&this._dotMarkers[markerIdKey].length>0){return true;} +return false;};trackdirect.models.MarkerCollection.prototype.addStationCoverage=function(stationId,stationCoveragePolygon){this._stationCoverage[stationId]=stationCoveragePolygon;};trackdirect.models.MarkerCollection.prototype.getStationCoverage=function(stationId){if(stationId in this._stationCoverage&&this._stationCoverage[stationId]!==null){return this._stationCoverage[stationId];} +return null;};trackdirect.models.MarkerCollection.prototype.getStationIdListWithVisibleCoverage=function(){var result=[];for(var stationId in this._stationCoverage){if(this._stationCoverage[stationId].isRequestedToBeVisible()){result.push(stationId);}} +return result;};trackdirect.models.MarkerCollection.prototype.hasCoveragePolygon=function(stationId){if(stationId in this._stationCoverage&&this._stationCoverage[stationId]!==null){return true;} return false;};trackdirect.models.MarkerCollection.prototype.resetDotMarkers=function(markerIdKey){if(markerIdKey in this._dotMarkers&&this._dotMarkers[markerIdKey]!==null){this._dotMarkers[markerIdKey]=[];}};trackdirect.models.MarkerCollection.prototype.removeOldestDotMarker=function(markerIdKey){if(this.hasDotMarkers(markerIdKey)){latestMarker=this.getMarker(markerIdKey);var latestMarkerIndex=this.getDotMarkerIndex(markerIdKey,latestMarker);var dotMarkers=this.getDotMarkers(markerIdKey);var maxNumberOfPolyLinePoints=dotMarkers.length;if(latestMarkerIndex>-1){maxNumberOfPolyLinePoints=maxNumberOfPolyLinePoints-1;} var removedItems=this._dotMarkers[markerIdKey].splice(0,1);if(removedItems.length==1){var removedMarker=removedItems[0];removedMarker.hide();if(this.hasPolyline(removedMarker.markerIdKey)){var polyline=this.getMarkerPolyline(removedMarker.markerIdKey);while(polyline.getPathLength()>maxNumberOfPolyLinePoints){polyline.removePathItem(0);}} removedMarker=null;return true;}} @@ -409,7 +444,7 @@ return{};};trackdirect.models.MarkerCollection.prototype.getPositionMarkerIdKeys return{};};trackdirect.models.MarkerCollection.prototype.addPostionMarkerId=function(markerIdKey,packet){var key=this._getCompareablePosition(packet.latitude,packet.longitude);if(!(key in this._positionMarkersIdKeys)){this._positionMarkersIdKeys[key]={};} this._positionMarkersIdKeys[key][markerIdKey]=true;};trackdirect.models.MarkerCollection.prototype.removePostionMarkerId=function(latitude,longitude,markerIdKey){var key=this._getCompareablePosition(latitude,longitude);if(key in this._positionMarkersIdKeys){if(markerIdKey in this._positionMarkersIdKeys[key]){this._positionMarkersIdKeys[key][markerIdKey]=false;}}};trackdirect.models.MarkerCollection.prototype.addMarkerToMapSector=function(markerIdKey,markerMapSector){if(!(markerMapSector in this._mapSectorMarkerIdKeys)){this._mapSectorMarkerIdKeys[markerMapSector]=[];} if(this._mapSectorMarkerIdKeys[markerMapSector].indexOf(markerIdKey)<0){this._mapSectorMarkerIdKeys[markerMapSector].push(markerIdKey);}};trackdirect.models.MarkerCollection.prototype.getMapSectorMarkerKeys=function(mapSector){if(mapSector in this._mapSectorMarkerIdKeys){return this._mapSectorMarkerIdKeys[mapSector];} -return[];};trackdirect.models.MarkerCollection.prototype.resetAllMarkers=function(){this._markerKeys={};this._markers=[];this._markerPolyLines=[];this._dotMarkers=[];this._markerOriginDashedPolyLines=[];this._stationMarkers={};this._stationLastMovingMarkerIdKey={};this._stationLastMarker={};this._senderLastMarker={};this._positionMarkersIdKeys={};this._mapSectorMarkerIdKeys={};};trackdirect.models.MarkerCollection.prototype.resetMarker=function(markerIdKey){if(this.isExistingMarker(markerIdKey)){var marker=this._markers[markerIdKey];this._markers[markerIdKey]=null;this._markerPolyLines[markerIdKey]=null;this._dotMarkers[markerIdKey]=null;this._markerOriginDashedPolyLines[markerIdKey]=null;if(typeof marker.packet.latitude!="undefined"&&typeof marker.packet.longitude!="undefined"){this.removePostionMarkerId(marker.packet.latitude,marker.packet.longitude,markerIdKey);}}};trackdirect.models.MarkerCollection.prototype.hasRelatedDashedPolyline=function(marker){if(typeof marker._relatedMarkerOriginDashedPolyLine!=="undefined"&&marker._relatedMarkerOriginDashedPolyLine!==null){return true;} +return[];};trackdirect.models.MarkerCollection.prototype.resetAllMarkers=function(){this._markerKeys={};this._markers=[];this._markerPolyLines=[];this._dotMarkers=[];this._markerOriginDashedPolyLines=[];this._stationMarkers={};this._stationLastMovingMarkerIdKey={};this._stationLastMarker={};this._senderLastMarker={};this._positionMarkersIdKeys={};this._mapSectorMarkerIdKeys={};this._stationCoverage={};};trackdirect.models.MarkerCollection.prototype.resetMarker=function(markerIdKey){if(this.isExistingMarker(markerIdKey)){var marker=this._markers[markerIdKey];this._markers[markerIdKey]=null;this._markerPolyLines[markerIdKey]=null;this._dotMarkers[markerIdKey]=null;this._markerOriginDashedPolyLines[markerIdKey]=null;if(typeof marker.packet.latitude!="undefined"&&typeof marker.packet.longitude!="undefined"){this.removePostionMarkerId(marker.packet.latitude,marker.packet.longitude,markerIdKey);}}};trackdirect.models.MarkerCollection.prototype.hasRelatedDashedPolyline=function(marker){if(typeof marker._relatedMarkerOriginDashedPolyLine!=="undefined"&&marker._relatedMarkerOriginDashedPolyLine!==null){return true;} return false;};trackdirect.models.MarkerCollection.prototype.hasNonRelatedMovingMarkerId=function(packet){var latestStationMovingMarkerIdKey=this.getStationLatestMovingMarkerIdKey(packet.station_id);var newMarkerIdKey=this.getMarkerIdKey(packet.marker_id);if(latestStationMovingMarkerIdKey!==null&&latestStationMovingMarkerIdKey!==newMarkerIdKey){var latestStationMovingMarker=this.getMarker(latestStationMovingMarkerIdKey);if(latestStationMovingMarker.packet.hasConfirmedMapId()){return true;}} return false;};trackdirect.models.MarkerCollection.prototype.isPacketReplacingMarker=function(packet){var markerIdKey=this.getMarkerIdKey(packet.marker_id);if(this.isExistingMarker(markerIdKey)){var marker=this.getMarker(markerIdKey);if(marker!==null){if(packet.map_id==14){return true;} if(packet.is_moving==0){return true;} @@ -562,7 +597,7 @@ return 0;};trackdirect.models.Map.prototype.getSouthWestLng=function(){if(this.g return 0;};trackdirect.models.Map.prototype.isMapReady=function(){if(this.getBounds()!=null){return true;} return false;};trackdirect.models.Map.prototype.setMapType=function(mapType){if(mapType in this._supportedMapTypes){this._mapType=mapType;if(typeof google==="object"&&typeof google.maps==="object"){this._updateGoogleMapTileLayer();}else if(typeof L==="object"){this._updateLeafletTileLayer();} this._emitTdEventListeners("change");}};trackdirect.models.Map.prototype.getMapType=function(){return this._mapType;};trackdirect.models.Map.prototype.getLeafletTileLayer=function(){return this._leafletTileLayer;};trackdirect.models.Map.prototype.getMid=function(){if(typeof this._tdMapOptions.mid!=="undefined"){return this._tdMapOptions.mid;} -return null;};trackdirect.models.Map.prototype.resetAllMarkers=function(){while(this.markerCollection.getNumberOfMarkers()>0){var i=this.markerCollection.getNumberOfMarkers();while(i--){var marker=this.markerCollection.getMarker(i);if(marker!==null){marker.stopToOldTimeout();marker.stopDirectionPolyline();marker.hide();marker.hideMarkerPrevPosition();marker.hideMarkerTail();} +return null;};trackdirect.models.Map.prototype.resetAllMarkers=function(){while(this.markerCollection.getNumberOfMarkers()>0){var i=this.markerCollection.getNumberOfMarkers();while(i--){var marker=this.markerCollection.getMarker(i);if(marker!==null){marker.stopToOldTimeout();marker.stopDirectionPolyline();marker.hide();marker.hideMarkerPrevPosition();marker.hideMarkerTail();var stationCoverage=this.markerCollection.getStationCoverage(marker.packet.station_id);if(stationCoverage){stationCoverage.hide();}} this.markerCollection.removeMarker(i);}} if(this.state.openInfoWindow!==null){this.state.openInfoWindow.hide();} if(this.oms){this.oms.clearMarkers();} @@ -590,7 +625,8 @@ this.showTopLabelOnPosition(marker.packet.latitude,marker.packet.longitude);}}}} marker.hideLabel();marker.hasLabel=false;}} if(topMarkerIdKey!=-1){var topMarker=this.markerCollection.getMarker(topMarkerIdKey);topMarker.hasLabel=true;var topMarkerMapSector=trackdirect.services.MapSectorCalculator.getMapSector(topMarker.getPositionLiteral().lat,topMarker.getPositionLiteral().lng);if(this.state.isFilterMode){topMarker.showLabel();}else if(this.isMapSectorVisible(topMarkerMapSector)&&this.getZoom()>=trackdirect.settings.minZoomForMarkerLabel){topMarker.showLabel();}}}};trackdirect.models.Map.prototype._hideMarkersInPreviousVisibleMapSectors=function(previousVisibleMapSectors){if(this._currentContentZoom>=trackdirect.settings.minZoomForMarkers){var markerIdKeyListToMaybeHide={};var markerIdKeyListNotToHide={};for(var i=0;i=trackdirect.settings.minZoomForMarkers){var mapSectorMarkerKeys=this.markerCollection.getMapSectorMarkerKeys(mapSector);for(var j=0;j=trackdirect.settings.minZoomForMarkers){for(var i=0;i=trackdirect.settings.minZoomForMarkers){for(var i=0;i' + ); + coveragePolygon.addTdListener( + "visible", + function () { + if (!coveragePolygon.hasContent()) { + coveragePolygon.hide(); + alert( + "Currently we do not have enough data to create a max range coverage plot for this station. Try again later!" + ); + $("." + coverageLinkElementClass).html("Coverage"); + } else { + $("." + coverageLinkElementClass).html("Hide coverage"); + } + }, + true + ); + } + + var me = this; + $.getJSON(this.coverageDataUrl + "?id=" + stationId, function (data) { + if ("station_id" in data && "coverage" in data) { + coveragePolygon.setData(data["coverage"]); + var marker = + me._map.markerCollection.getStationLatestMarker(stationId); + if (marker.isVisible()) { + if (coveragePolygon.isRequestedToBeVisible()) { + coveragePolygon.show(); + } + } + } + }) + .fail(function () { + coveragePolygon.hide(); + alert("Failed to fetch coverage data. Try again later!"); + $("." + coverageLinkElementClass).html("Coverage"); + }) + .always(function () {}); + } + } + }, + /** * Set map type * @param {string} mapType @@ -938,6 +1035,9 @@ var trackdirect = { if (typeof options["isMobile"] !== undefined) { this.isMobile = options["isMobile"]; } + if (typeof options["coverageDataUrl"] !== undefined) { + this.coverageDataUrl = options["coverageDataUrl"]; + } if (typeof options["time"] !== undefined) { this._time = options["time"]; } diff --git a/jslib/src/trackdirect/models/InfoWindow.js b/jslib/src/trackdirect/models/InfoWindow.js index 085d23c..32dc03c 100755 --- a/jslib/src/trackdirect/models/InfoWindow.js +++ b/jslib/src/trackdirect/models/InfoWindow.js @@ -1224,6 +1224,14 @@ trackdirect.models.InfoWindow.prototype._getMenuDiv = function ( menuUl.append(this._getMenuDivCenterLink(isInfoWindowOpen)); } menuUl.append(this._getMenuDivZoomLink(isInfoWindowOpen)); + if ( + !trackdirect.isEmbedded && + !inIframe() && + !this._marker.isMovingStation() && + this._marker.packet.source_id != 2 + ) { + menuUl.append(this._getMenuDivCoverageLink()); + } return menuWrapperDiv; }; @@ -1360,6 +1368,41 @@ trackdirect.models.InfoWindow.prototype._getMenuDivFilterLink = function () { return menuLi; }; +/** + * Get the info window menu coverage link + * @return {object} + */ +trackdirect.models.InfoWindow.prototype._getMenuDivCoverageLink = function () { + var coverageLinkElementClass = + "stationCoverageLink" + this._marker.packet.station_id; + var menuLi = $(document.createElement("li")); + menuLi.css(this._getMenuDivLinkCss()); + var menuLink = $(document.createElement("a")); + menuLink.css("color", "#337ab7"); + menuLink.css("white-space", "nowrap"); + menuLink.attr("href", "#"); + menuLink.addClass(coverageLinkElementClass); + menuLink.attr( + "onclick", + "trackdirect.toggleStationCoverage(" + + this._marker.packet.station_id + + ', "' + + coverageLinkElementClass + + '"); return false;' + ); + + var coveragePolygon = this._defaultMap.markerCollection.getStationCoverage( + this._marker.packet.station_id + ); + if (coveragePolygon !== null && coveragePolygon.isRequestedToBeVisible()) { + menuLink.html("Hide Coverage"); + } else { + menuLink.html("Coverage"); + } + menuLi.append(menuLink); + return menuLi; +}; + /** * Get the info window menu center link * @param {boolean} isInfoWindowOpen diff --git a/jslib/src/trackdirect/models/Map.js b/jslib/src/trackdirect/models/Map.js index ead6619..eb702fa 100755 --- a/jslib/src/trackdirect/models/Map.js +++ b/jslib/src/trackdirect/models/Map.js @@ -446,6 +446,11 @@ trackdirect.models.Map.prototype.resetAllMarkers = function () { marker.hide(); marker.hideMarkerPrevPosition(); marker.hideMarkerTail(); + + var stationCoverage = this.markerCollection.getStationCoverage(marker.packet.station_id); + if (stationCoverage) { + stationCoverage.hide(); + } } this.markerCollection.removeMarker(i); } @@ -1132,6 +1137,20 @@ trackdirect.models.Map.prototype._showMarkersInNewVisibleMapSectors = function ( } } } + + // Also make sure all stations with a visible coverage is shown + var stationIdList = + this.markerCollection.getStationIdListWithVisibleCoverage(); + for (var i = 0; i < stationIdList.length; i++) { + var latestMarker = this.markerCollection.getStationLatestMarker( + stationIdList[i] + ); + if (latestMarker !== null) { + if (latestMarker.shouldMarkerBeVisible() && latestMarker.showAsMarker) { + latestMarker.show(); + } + } + } } }; diff --git a/jslib/src/trackdirect/models/MarkerCollection.js b/jslib/src/trackdirect/models/MarkerCollection.js index ef6c1e5..2f8b5b5 100755 --- a/jslib/src/trackdirect/models/MarkerCollection.js +++ b/jslib/src/trackdirect/models/MarkerCollection.js @@ -35,6 +35,9 @@ trackdirect.models.MarkerCollection = function () { // Contains arrays of markerKeys and is indexed by mapSectorId this._mapSectorMarkerIdKeys = {}; + // Contains coverage values indexed by stationId + this._stationCoverage = {}; + this.resetAllMarkers(); }; @@ -280,6 +283,67 @@ trackdirect.models.MarkerCollection.prototype.hasDotMarkers = function ( return false; }; +/* + * Add dot marker for specified markerIdKey + * @param {int} markerIdKey + * @param {object} dotMarker + */ +trackdirect.models.MarkerCollection.prototype.addStationCoverage = function ( + stationId, + stationCoveragePolygon +) { + this._stationCoverage[stationId] = stationCoveragePolygon; +}; + +/** + * Returns the station coverage + * @param {int} stationId + * @return {StationCoveragePolygon} + */ +trackdirect.models.MarkerCollection.prototype.getStationCoverage = function ( + stationId +) { + if ( + stationId in this._stationCoverage && + this._stationCoverage[stationId] !== null + ) { + return this._stationCoverage[stationId]; + } + return null; +}; + +/** + * Returns an array of stations the has a visible coverage + * @return {array} + */ +trackdirect.models.MarkerCollection.prototype.getStationIdListWithVisibleCoverage = + function () { + var result = []; + for (var stationId in this._stationCoverage) { + if (this._stationCoverage[stationId].isRequestedToBeVisible()) { + result.push(stationId); + } + } + return result; + }; + +/** + * Returns true if specified station has a coverage polygon + * @param {int} stationId + * @return {boolean} + */ +trackdirect.models.MarkerCollection.prototype.hasCoveragePolygon = function ( + stationId +) { + if ( + stationId in this._stationCoverage && + this._stationCoverage[stationId] !== null + ) { + return true; + } + return false; +}; + /** * Reset the dot markers for a specified marker * @param {int} markerIdKey @@ -727,6 +791,7 @@ trackdirect.models.MarkerCollection.prototype.resetAllMarkers = function () { this._senderLastMarker = {}; this._positionMarkersIdKeys = {}; this._mapSectorMarkerIdKeys = {}; + this._stationCoverage = {}; }; /** diff --git a/jslib/src/trackdirect/models/StationCoveragePolygon.js b/jslib/src/trackdirect/models/StationCoveragePolygon.js new file mode 100755 index 0000000..6f0e482 --- /dev/null +++ b/jslib/src/trackdirect/models/StationCoveragePolygon.js @@ -0,0 +1,506 @@ +/** + * Class trackdirect.models.StationCoveragePolygon + * @param {LatLngLiteral} center + * @param {trackdirect.models.Map} map + * @param {boolean} tryToShowCoveragePolygon + */ +trackdirect.models.StationCoveragePolygon = function ( + center, + map, + tryToShowCoveragePolygon +) { + tryToShowCoveragePolygon = + typeof tryToShowCoveragePolygon !== "undefined" + ? tryToShowCoveragePolygon + : true; + this._showPolygon = tryToShowCoveragePolygon; + + this._map = map; + this._center = center; + this._isRequestedToBeVisible = false; + + this._polygon = null; + this._polygonCoordinates = null; + + this._heatmapCoordinates = null; + this._heatmap = null; + + this._tdEventListeners = {}; + this._tdEventListenersOnce = {}; + + // I recommend ignoring positions with a very long distance. + // Ignoring everything with a distance longer than 1000km is reasonable. + this._upperMaxRangeInMeters = 1000 * 1000; + + // Percentile affects how many packets we include in the coverage polygon. + this._percentile = 95; + + // To get a smooth ploygon we add som padding to the convex hull positions. + this._paddingInPercentOfMaxRange = 10; + this._paddingMinInMeters = 1000; +}; + +/** + * Set coverage data + * @param {array} data + */ +trackdirect.models.StationCoveragePolygon.prototype.setData = function (data) { + this._addParametersToData(data); + this._heatmapCoordinates = this._getCoordinates(data); + + if (this._showPolygon) { + var maxRange = this._getCoveragePolygonMaxRange(data); + if (maxRange <= 0) { + this._showPolygon = false; + } else { + this._polygonCoordinates = this._getConvexHullCoordinates(data, maxRange); + } + } + + if (typeof google === "object" && typeof google.maps === "object") { + this._googleMapsInit(); + } else if (typeof L === "object") { + this._leafletInit(); + } +}; + +/** + * Add listener to events + * @param {string} event + * @param {string} handler + */ +trackdirect.models.StationCoveragePolygon.prototype.addTdListener = function ( + event, + handler, + execOnce +) { + execOnce = typeof execOnce !== "undefined" ? execOnce : false; + + if (execOnce) { + if (!(event in this._tdEventListenersOnce)) { + this._tdEventListenersOnce[event] = []; + } + this._tdEventListenersOnce[event].push(handler); + } else { + if (!(event in this._tdEventListeners)) { + this._tdEventListeners[event] = []; + } + this._tdEventListeners[event].push(handler); + } +}; + +/** + * Returns true if polygon has an area + * @return {boolean} + */ +trackdirect.models.StationCoveragePolygon.prototype.hasContent = function () { + if ( + this._heatmapCoordinates !== null && + this._heatmapCoordinates.length > 0 + ) { + return true; + } + return false; +}; + +/** + * Returns true if polygon is visible + * @return {boolean} + */ +trackdirect.models.StationCoveragePolygon.prototype.isRequestedToBeVisible = + function () { + return this._isRequestedToBeVisible; + }; + +/** + * Request polygon to be shown when complete + */ +trackdirect.models.StationCoveragePolygon.prototype.showWhenDone = function () { + this._isRequestedToBeVisible = true; +}; + +/** + * Show coverage + */ +trackdirect.models.StationCoveragePolygon.prototype.show = function () { + if (typeof google === "object" && typeof google.maps === "object") { + if (this._polygon !== null) { + this._polygon.setMap(this._map); + } + if (this._heatmap !== null) { + this._heatmap.setMap(this._map); + } + } else if (typeof L === "object") { + if (this._polygon !== null) { + this._polygon.addTo(this._map); + } + if (this._heatmap !== null) { + this._heatmap.addTo(this._map); + } + } + this._isRequestedToBeVisible = true; + + // show will be called again when data has been updated + if (this._showPolygon && this._polygonCoordinates !== null) { + this._emitTdEventListeners("visible"); + } else if (this._heatmapCoordinates !== null) { + this._emitTdEventListeners("visible"); + } +}; + +/** + * Hide coverage + * @param {boolean} stillMarkAsVisible + */ +trackdirect.models.StationCoveragePolygon.prototype.hide = function ( + stillMarkAsVisible +) { + stillMarkAsVisible = + typeof stillMarkAsVisible !== "undefined" ? stillMarkAsVisible : false; + + if (typeof google === "object" && typeof google.maps === "object") { + if (this._polygon !== null) { + this._polygon.setMap(null); + } + if (this._heatmap !== null) { + this._heatmap.setMap(null); + } + } else if (typeof L === "object") { + if (this._polygon !== null) { + this._map.removeLayer(this._polygon); + } + if (this._heatmap !== null) { + this._map.removeLayer(this._heatmap); + } + } + if (!stillMarkAsVisible) { + this._isRequestedToBeVisible = false; + } + this._emitTdEventListeners("hidden"); +}; + +/** + * Init function for Gogle Maps + */ +trackdirect.models.StationCoveragePolygon.prototype._googleMapsInit = + function () { + if ( + this._polygonCoordinates !== null && + this._polygonCoordinates.length > 0 + ) { + this._polygon = new google.maps.Polygon({ + paths: this._polygonCoordinates, + strokeColor: "#0000FF", + strokeOpacity: 0, + strokeWeight: 0, + fillColor: "#0000FF", + fillOpacity: 0.2, + }); + } + + if ( + this._heatmapCoordinates !== null && + this._heatmapCoordinates.length > 0 + ) { + var data = []; + for (var i = 0; i < this._heatmapCoordinates.length; i++) { + data.push({ location: this._heatmapCoordinates[i], weight: 1 }); + } + + this._heatmap = new google.maps.visualization.HeatmapLayer({ + data: data, + radius: 8, + maxIntensity: 5, + gradient: [ + "rgba(0, 255, 255, 0)", + "rgba(0, 255, 255, 1)", + "rgba(0, 191, 255, 1)", + "rgba(0, 127, 255, 1)", + "rgba(0, 63, 255, 1)", + "rgba(0, 0, 255, 1)", + "rgba(0, 0, 223, 1)", + "rgba(0, 0, 191, 1)", + "rgba(0, 0, 159, 1)", + "rgba(0, 0, 127, 1)", + "rgba(63, 0, 91, 1)", + "rgba(127, 0, 63, 1)", + "rgba(191, 0, 31, 1)", + "rgba(255, 0, 0, 1)", + ], + map: null, + }); + } + }; + +/** + * Init function for Leaflet + */ +trackdirect.models.StationCoveragePolygon.prototype._leafletInit = function () { + if ( + this._polygonCoordinates !== null && + this._polygonCoordinates.length > 0 + ) { + this._polygon = new L.polygon(this._polygonCoordinates, { + color: "#0000FF", + opacity: 0, + weight: 0, + fillColor: "#0000FF", + fillOpacity: 0.2, + }); + } + + if ( + this._heatmapCoordinates !== null && + this._heatmapCoordinates.length > 0 + ) { + var data = []; + for (var i = 0; i < this._heatmapCoordinates.length; i++) { + data.push([ + this._heatmapCoordinates[i].lat, + this._heatmapCoordinates[i].lng, + 10, + ]); + } + this._heatmap = L.heatLayer(this._heatmapCoordinates, { + minOpacity: 0.35, + radius: 6, + blur: 4, + }); + } +}; + +/** + * Get convex hull coordinates + * @param {array} data + * @param {int} maxRange + * @return {array} + */ +trackdirect.models.StationCoveragePolygon.prototype._getConvexHullCoordinates = + function (data, maxRange) { + var positions = this._getFilteredPositions(data, maxRange); + positions.push(this._center); + + var xyPositions = this._convertToXYPos(positions); + var convexHullXYPositions = convexhull.makeHull(xyPositions); + + // Calc padding + var latLngPadding = + this._paddingInPercentOfMaxRange * 0.01 * maxRange * 0.000009; + var latLngPaddingMin = this._paddingMinInMeters * 0.000009; + if (isNaN(latLngPadding) || latLngPadding < latLngPaddingMin) { + latLngPadding = latLngPaddingMin; + } + + // Add padding + var xyPositionsWithPadding = []; + for (var i = 0; i < convexHullXYPositions.length; i++) { + xyPositionsWithPadding.push(convexHullXYPositions[i]); + + for (var angle = 0; angle < 360; angle += 10) { + var x = + convexHullXYPositions[i]["x"] + + latLngPadding * Math.cos((angle * Math.PI) / 180); + var y = + convexHullXYPositions[i]["y"] + + latLngPadding * Math.sin((angle * Math.PI) / 180) * 2; + if (!isNaN(x) && !isNaN(y)) { + xyPositionsWithPadding.push({ x: x, y: y }); + } + } + } + var convexHullXYPositionsWithPadding = convexhull.makeHull( + xyPositionsWithPadding + ); + + // Convert to LatLng and return + return this._convertToLatLngPos(convexHullXYPositionsWithPadding); + }; + +/** + * Get an array with valid positions + * @param {array} data + * @param {int} maxRange + * @return {array} + */ +trackdirect.models.StationCoveragePolygon.prototype._getFilteredPositions = + function (data, maxRange) { + var result = []; + + for (var i = 0; i < data.length; i++) { + if (typeof maxRange !== "undefined" && data[i].distance > maxRange) { + continue; + } + + result.push(data[i].latLngLiteral); + } + + return result; + }; + +/** + * Calculate coverage polygon max range + * @param {array} data + * @return {int} + */ +trackdirect.models.StationCoveragePolygon.prototype._getCoveragePolygonMaxRange = + function (data) { + var maxRange = this._getDistancePercentile( + data, + this._percentile, + this._upperMaxRangeInMeters + ); + if (isNaN(maxRange)) { + maxRange = 0; + } + + return maxRange; + }; + +/** + * Calculate the specified percentile + * @param {array} data + * @param {int} percentile + * @param {int} upperMaxRange + * @return {int} + */ +trackdirect.models.StationCoveragePolygon.prototype._getDistancePercentile = + function (data, percentile, upperMaxRange) { + var values = []; + for (var i = 0; i < data.length; i++) { + if (data[i].distance + 0 < upperMaxRange) { + values.push(data[i].distance); + } + } + + values.sort(function (a, b) { + return a - b; + }); + + var index = (percentile / 100) * values.length; + var result; + if (Math.floor(index) == index) { + result = (values[index - 1] + values[index]) / 2; + } else { + result = values[Math.floor(index)]; + } + + return result; + }; + +/** + * Calculate number of values + * @param {array} data + * @param {int} maxRange + * @return {int} + */ +trackdirect.models.StationCoveragePolygon.prototype._getNumberOfValues = + function (data, maxRange) { + var counter = 0; + for (var i = 0; i < data.length; i++) { + if (data[i].distance > maxRange) { + continue; + } + + counter++; + } + + return counter; + }; + +/** + * Convert to xy positions + * @param {array} data + * @return {array} + */ +trackdirect.models.StationCoveragePolygon.prototype._convertToXYPos = function ( + positions +) { + var result = []; + for (var i = 0; i < positions.length; i++) { + result.push({ x: positions[i].lat, y: positions[i].lng }); + } + return result; +}; + +/** + * Convert to lat/lng positions + * @param {array} data + * @return {array} + */ +trackdirect.models.StationCoveragePolygon.prototype._convertToLatLngPos = + function (positions) { + var result = []; + for (var i = 0; i < positions.length; i++) { + result.push({ lat: positions[i].x, lng: positions[i].y }); + } + return result; + }; + +/** + * Get an array of all coordinates for the specified move type + * @param {array} data + * @return {array} + */ +trackdirect.models.StationCoveragePolygon.prototype._getCoordinates = function ( + data +) { + var result = []; + + for (var j = 0; j < data.length; j++) { + if (typeof google === "object" && typeof google.maps === "object") { + var position = new google.maps.LatLng( + parseFloat(data[j]["latitude"]), + parseFloat(data[j]["longitude"]) + ); + } else { + var position = { + lat: parseFloat(data[j]["latitude"]), + lng: parseFloat(data[j]["longitude"]), + }; + } + result.push(position); + } + + return result; +}; + +/** + * Add the angle paramter for each coverage position + * @param {array} data + */ +trackdirect.models.StationCoveragePolygon.prototype._addParametersToData = + function (data) { + for (var j = 0; j < data.length; j++) { + var latLngLiteral = { + lat: parseFloat(data[j].latitude), + lng: parseFloat(data[j].longitude), + }; + data[j].latLngLiteral = latLngLiteral; + data[j].angle = trackdirect.services.distanceCalculator.getBearing( + this._center, + latLngLiteral + ); + } + }; + +/** + * Emit all event listeners for a specified event + * @param {string} event + * @param {object} arg + */ +trackdirect.models.StationCoveragePolygon.prototype._emitTdEventListeners = + function (event, arg) { + if (event in this._tdEventListeners) { + for (var i = 0; i < this._tdEventListeners[event].length; i++) { + this._tdEventListeners[event][i](arg); + } + } + + if (event in this._tdEventListenersOnce) { + var eventListenersOnce = this._tdEventListenersOnce[event].splice(0); + this._tdEventListenersOnce[event] = []; + for (var i = 0; i < eventListenersOnce.length; i++) { + eventListenersOnce[i](arg); + } + } + }; diff --git a/misc/database/tables/packet_path.sql b/misc/database/tables/packet_path.sql index e1c680e..824d728 100644 --- a/misc/database/tables/packet_path.sql +++ b/misc/database/tables/packet_path.sql @@ -1,13 +1,15 @@ create table packet_path ( "id" bigserial not null, "packet_id" bigint not null, - "sending_station_id" bigint not null, "station_id" bigint not null, "latitude" double precision null, "longitude" double precision null, "timestamp" bigint null, "distance" int null, "number" smallint, + "sending_station_id" bigint not null, + "sending_latitude" double precision null, + "sending_longitude" double precision null, primary key (id), foreign key(station_id) references station(id), foreign key(sending_station_id) references station(id) diff --git a/server/trackdirect/collector/PacketBatchInserter.py b/server/trackdirect/collector/PacketBatchInserter.py index 1daa50e..788b2c4 100644 --- a/server/trackdirect/collector/PacketBatchInserter.py +++ b/server/trackdirect/collector/PacketBatchInserter.py @@ -232,7 +232,7 @@ class PacketBatchInserter(): distance = packet.getTransmitDistance() pathTuples.append( - (packet.id, stationId, packet.stationId, latitude, longitude, packet.timestamp, distance, number)) + (packet.id, stationId, latitude, longitude, packet.timestamp, distance, number, packet.stationId, packet.latitude, packet.longitude)) number += 1 i += 1 @@ -240,9 +240,9 @@ class PacketBatchInserter(): if pathTuples: try: argString = ','.join(cur.mogrify( - "(%s, %s, %s, %s, %s, %s, %s, %s)", x) for x in pathTuples) + "(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", x) for x in pathTuples) cur.execute("insert into " + packetPathTable + - "(packet_id, station_id, sending_station_id, latitude, longitude, timestamp, distance, number) values " + argString) + "(packet_id, station_id, latitude, longitude, timestamp, distance, number, sending_station_id, sending_latitude, sending_longitude) values " + argString) except psycopg2.InterfaceError as e: # Connection to database is lost, better just exit raise e