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