Merge branch 'main' of https://github.com/qvarforth/trackdirect into qvarforth-main

This commit is contained in:
Peter Buchegger 2022-01-31 18:34:50 +01:00
commit 6e2a6cb0d8
33 changed files with 1575 additions and 169 deletions

View File

@ -1,6 +1,6 @@
# APRS Track Direct
APRS Track Direct is a collection of tools that can be used to run an APRS website. You can use data from APRS-IS, CWOP-IS, OGN, HABHUB, CBAPRS or any other source that uses the APRS specification.
APRS Track Direct is a collection of tools that can be used to run an APRS website. You can use data from APRS-IS, CWOP-IS, OGN or any other source that uses the APRS specification.
Tools included are an APRS data collector, a websocket server, a javascript library (websocket client and more), a heatmap generator and a website example (which can of course be used as is).
@ -61,7 +61,7 @@ sudo python2 setup.py install
```
### Set up aprsc
You should not to connect to a public APRS server (APRS-IS, CWOP-IS or OGN server). The collector will use a full feed connection and each websocket client will use a filtered feed connection. To not cause extra load on public servers it is better to run your own aprsc server and let your collector and all websocket connections connect to that instead (will result in only one full feed connection to a public APRS server).
You should not to connect your collector and websocket server directly to a public APRS server (APRS-IS, CWOP-IS or OGN server). The collector will use a full feed connection and each websocket client will use a filtered feed connection (through the websocket server). To not cause extra load on public servers it is better to run your own aprsc server and let your collector and all websocket connections connect to that instead (will result in only one full feed connection to a public APRS server).
Note that it seems like aprsc needs to run on a server with a public ip, otherwise uplink won't work.
@ -114,7 +114,7 @@ ALTER ROLE {USER} WITH SUPERUSER;
GRANT ALL PRIVILEGES ON DATABASE "trackdirect" to {USER};
```
Remember to add password to password-file:
Might be good to add password to password-file:
```
vi ~/.pgpass
```

View File

@ -12,11 +12,15 @@ password="foobar"
port="5432"
;; Settings for the remover script
days_to_save_position_data="31"
days_to_save_station_data="90"
days_to_save_position_data="10"
days_to_save_station_data="30"
days_to_save_weather_data="10"
days_to_save_telemetry_data="10"
;; If this setting is enabled, OGN stations that we are not allowed to reveal the identity of will be given a random name similar to "UNKNOWN123"
;; If disabled we will drop all packets regarding stations that we should not reveal the identity of.
save_ogn_stations_with_missing_identity="0"
[websocket_server]
@ -29,10 +33,10 @@ port="9000"
;; Websocket server log output
error_log="~/trackdirect/server/log/wsserver_aprs.log"
;; Frequency limit
;; Packets received more frequently than the configured frequency limit will be dropped
;; Frequency limit is specified in seconds, and 0 means that the limit is disabled.
;; This setting is very useful when frequency is very high (when receiving data from the OGN network this needs to be at least 15s)
;; Packets received more frequently than the configured frequency limit will be dropped (limit is specified in seconds)
;; This frequency limit is only refering to pakets that is received in real time from the filtered feed used by the websocket server
;; This frequency limit may be a bit more forgiving than the frequence limit on the collector.
;; When receiving data from the OGN network this needs to be about 15s or more.
frequency_limit="0"
;; First APRS IS server for the websocket server to connect to.
@ -40,6 +44,7 @@ frequency_limit="0"
aprs_host1="127.0.0.1"
aprs_port1="14580"
;; Important that you set the correct source, otherwise it might be handled incorrect
;; - Source Id 1: APRS-IS
;; - Source Id 2: CWOP
;; - Source Id 3: CBAPRS
@ -54,18 +59,18 @@ aprs_source_id1="1"
;aprs_source_id2="2"
;; Allow time travel
;; Use this settings to disable data requests with a time interval
;; Useful when it is not allowed to show data older than 24h (like when data comes from the OGN network)
;; Note that you need to configure the remover script to delete data after 24h as well (if the source require you to do so)
;; Use this settings to disable/enable data requests with a time interval (this must be disabled for the OGN network)
allow_time_travel="1"
;; Max default time in seconds (how old packets that will be included in the response)
;; Max default time in minutes (how old packets that will be included in the response)
;; This setting should be no more than 1440 for for the OGN network.
max_default_time="1440"
;; Max time in seconds when filtering (how old packets that will be included in the response)
max_filter_time="14400"
;; Max time in minutes when filtering (how old packets that will be included in the response)
;; This setting should be no more than 1440 for for the OGN network.
max_filter_time="1440"
;; Time in seconds until idle client is disconnected
;; Time in minutes until idle client is disconnected
max_client_idle_time="60"
;; Max age in seconds for real time packets waiting to be sent to client (dropping packets if limit is excceded)
@ -78,6 +83,7 @@ host="aprsc"
port_full="10152"
port_filtered="14580"
;; Important that you set the correct source, otherwise it might be handled incorrect
;; - Source Id 1: APRS-IS
;; - Source Id 2: CWOP
;; - Source Id 3: CBAPRS
@ -92,10 +98,8 @@ passcode="-1"
;; Database inserts is done in batches
numbers_in_batch="50"
;; Frequency limit
;; Packets received more frequently than the configured frequency limit will not be shown on map.
;; Frequency limit is specified per station in seconds, and 0 means that the limit is disabled.
;; This setting is very useful when frequency is very high (when receiving data from the OGN network this needs to be at least 20s).
;; Packets received more frequently than the configured frequency limit will not be shown on map (limit is specified in seconds)
;; When receiving data from the OGN network this needs to be 20s or more.
;; If setting save_fast_packets to "0", packets that is received to frequently will not be save (useful for OGN, but not for APRS-IS).
frequency_limit="5"
save_fast_packets="1"

View File

@ -1744,4 +1744,35 @@ function getSymbolDescription($symbolTable, $symbol, $includeUndefinedOverlay)
}
}
}
}
/**
* Returnes true if the time travel feature works
*
* @return boolean
*/
function isTimeTravelAllowed() {
$isTimeTravelAllowed = false;
$config = parse_ini_file(ROOT . '/../config/trackdirect.ini', true);
if (isset($config['websocket_server'])) {
if (isset($config['websocket_server']['allow_time_travel'])) {
if ($config['websocket_server']['allow_time_travel'] == '1') {
$isTimeTravelAllowed = true;
}
}
if (isset($config['websocket_server']['aprs_source_id1']) && $config['websocket_server']['aprs_source_id1'] == 5) {
// Data source is OGN, disable time travel (server will block it anyway)
$isTimeTravelAllowed = false;
}
if (isset($config['websocket_server']['aprs_source_id2']) && $config['websocket_server']['aprs_source_id2'] == 5) {
// Data source is OGN, disable time travel (server will block it anyway)
$isTimeTravelAllowed = false;
}
}
return $isTimeTravelAllowed;
}

View File

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

View File

@ -148,15 +148,14 @@ class PacketRepository extends ModelRepository
* Get latest packet data list by station id (useful for creating a chart)
*
* @param int $stationId
* @param int $startTimestamp
* @param int $endTimestamp
* @param int $numberOfHours
* @param array $columns
* @return Array
*/
public function getLatestDataListByStationId($stationId, $startTimestamp, $endTimestamp, $columns)
public function getLatestDataListByStationId($stationId, $numberOfHours, $columns)
{
$result = Array();
if (!isInt($stationId) || !isInt($startTimestamp) || !isInt($endTimestamp)) {
if (!isInt($stationId) || !isInt($numberOfHours)) {
return $result;
}
@ -165,27 +164,13 @@ class PacketRepository extends ModelRepository
$columns[] = 'timestamp';
}
if ($endTimestamp < time()-60) {
// Fetching old values
$sql = 'select ' . implode(',', $columns) . ', position_timestamp from packet
where station_id = ?
and timestamp >= ?
and (timestamp <= ? or (position_timestamp is not null and position_timestamp <= ?))
and (speed is not null or altitude is not null)
and map_id in (1,12,5,7,9)
order by timestamp';
$arg = [$stationId, $startTimestamp, $endTimestamp, $endTimestamp];
} else {
// Fetching recent values
$sql = 'select ' . implode(',', $columns) . ', position_timestamp from packet
where station_id = ?
and timestamp >= ?
and timestamp <= ?
and (speed is not null or altitude is not null)
and map_id in (1,12,5,7,9)
order by timestamp';
$arg = [$stationId, $startTimestamp, $endTimestamp];
}
$sql = 'select ' . implode(',', $columns) . ', position_timestamp from packet
where station_id = ?
and timestamp >= ?
and (speed is not null or altitude is not null)
and map_id in (1,12,5,7,9)
order by timestamp';
$arg = [$stationId, time() - $numberOfHours*60*60];
$pdo = PDOConnection::getInstance();
$stmt = $pdo->prepareAndExec($sql, $arg);

140
htdocs/public/about.php Normal file
View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>About</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="/css/main.css">
<style type="text/css">
h2:not(:first-child) {
padding-top: 20px;
}
h2 {
font-weight: normal;
}
h3 {
padding-top: 15px;
}
</style>
</head>
<body>
<div class="modal-inner-content" style="padding-bottom: 30px;">
<h2>1. I have a question. Who may I contact?</h2>
<p>
Maintainer of this website is <a href="mailto:no@name.com">Unknown</a>.
</p>
<h2>2. What is APRS?</h2>
<p>
APRS (Automatic Packet Reporting System) is a digital communications system that uses packet radio to send real time tactical information. The APRS network is used by ham radio operators all over the world.
</p>
<p>
Information shared over the APRS network is for example coordinates, altitude, speed, heading, text messages, alerts, announcements, bulletins and weather data.
</p>
<h2>3. What is APRS Track Direct</h2>
<p>
This website is based on the APRS Track Direct tools. Read more on <a href="https://github.com/qvarforth/trackdirect" target="_blank">GitHub</a>. But please note that the maintainer of APRS Track Direct has nothing to do with this website.
</p>
<h2>4. I have a map created in <a target="_blank" href="https://mymaps.google.com/">Google My Maps</a>, can I render it on top of this map?</h2>
<p>
Sure! Follow the following instructions...
</p>
<ol>
<li>Open your map at <a target="_blank" href="https://mymaps.google.com/">Google My Maps</a>.</li>
<li>Look at the current URL and try to find a value named <b>mid</b>. Copy that value!</li>
<li>Open this website. Add <b>"&mid={the mid value}"</b> to the end of the URL.</li>
<li>Press Enter!</li>
</ol>
<h2>5. My latest packet seems to be using different path's depending on what website I look at (APRS-IS related). Why?</h2>
<p>
The websites you compare are not collecting packets from the same APRS-IS servers. Each APRS-IS server performes duplicate filtering, and which packet that is considered to be a duplicate may differ depending on which APRS-IS server you ask.
</p>
<h2>6. Where does the displayed data come from?</h2>
<p>On each station you can see the specified source. APRS data can be received from <a href="http://www.aprs-is.net" target="_blank">APRS-IS</a>, <a href="http://www.wxqa.com" target="_blank">CWOP-IS</a> or <a href="https://www.glidernet.org" target="_blank">OGN</a> (and more).
<h2>7. How do I prevent my data from being displayed on websites such as this?</h2>
<h3>A. Answer for APRS-IS/CWOP-IS and more</h3>
<p>
If you do not want your APRS data to be publiched on APRS-websites you can append <b>NOGATE</b> to the end of your path (or use <b>RFONLY</b>). If your digipeater path is <b>WIDE1-1,WIDE2-1</b>, you just change it to <b>WIDE1-1,WIDE2-1,NOGATE</b>.
</p>
<h3>B. Answer for OGN (Open Glider Network)</h3>
<p>
Not all information that is sent to the Open Glider Network is published.
</p>
<p>
Aircrafts that meet any of the following condition is not shown at all.<br/>
&rarr; Has the "no-tracking" flag in FLARM device configuration set.<br/>
&rarr; Has activated the setting "I don't want this device to be tracked" in the the <a target="_blank" href="http://wiki.glidernet.org/ddb">OGN Devices DataBase</a>.
</p>
<p>
This website will only display information that can be used to identify an aircraft if the aircraft device details exists in the <a target="_blank" href="http://wiki.glidernet.org/ddb">OGN Devices DataBase</a>, and if the setting "I don't want this device to be identified" is deactivated. If the website is configured to also render aircrafts that does not exists in the OGN database, the aircraft is given a temporary name that is only used for a maximum of 24h (to make sure it can not be identified).
</p>
<p>
Read more about how to "Opt In" or "Opt Out" <a target="_blank" href="http://wiki.glidernet.org/opt-in-opt-out">here</a>.
</p>
<h2>8. How is the coverage map created?</h2>
<p>
Note that the coverage map is only available for receiving stations, it tries to show from which area the station is able to receive packets.
</p>
<p>
The coverage map consists of two parts:
<ul>
<li><span>The heatmap that shows all recorded coordinates.</span></li>
<li><span>The interpolated max range plot polygon that shows the coverage.</span></li>
</ul>
</p>
<p>
The max range plot is created by:
<ol>
<li><span>We exclude positions that have a distance that is further away than the 95th percentile.</span></li>
<li><span>We use a convex hull algorithm to get a polygon of the covered area.</span></li>
<li><span>We add some padding to the area received in the previous step. This step is just used to make the polygon look a bit nicer.</span></li>
</ol>
</p>
<h2>9. Can you tell me how the marker logic works?</h2>
<p>
Okay, let me tell you more about our brilliant marker logic :-)
</p>
<ul>
<li>We have a speed limit filter and other filters that sorts out packets that has a faulty position.</li>
<li>If a moving station sends a packet that is sorted out by our speed limit filter the packet will be marked as unconfirmed, if we later receive a packet that confirmes that the station is moving in that direction, the previous packet will be confirmed then.</li>
<li>If a station moves in one area and suddently appear in another area the two tails will be connected by a dashed polyline.</li>
<li>A moving station that reports it's speed and direction will have an animated direction polyline (will be hidden after 15min).</li>
<li>The dotted polyline shows the packet transmit path, will be shown when you hover over a marker or a "dotmarker". If a station in the path hasn't sent a position packet in a long time it will show up for some seconds and than disappear again.</li>
<li>Note that the time-interval specified in the station info-window (on the map) is how long a station has been on that location <u>without any downtime longer than 24h</u>.</li>
</ul>
<h2>10. What browsers do this website support?</h2>
<p>
Our goal is that APRS Direct should work on all broswers that supports websockets (an HTML5 feature). The following browser version (and newer) supports the websocket-protocol.
</p>
<ul>
<li>Internet Explorer 10 (released 4/9 2012)</li>
<li>Edge 12 (released 30/3 2015)</li>
<li>Firefox 11 (released 31/1 2012)</li>
<li>Chrome 16 (released 25/10 2012)</li>
<li>Safari 7 (released 22/10 2013)</li>
<li>Opera 6.1 (released 5/11 2012)</li>
<li>iOS Safari 6.1 (released 28/1 2013)</li>
<li>Android Browser 4.4 (released 9/12 2013)</li>
<li>Blackberry browser 7 (released 1/1 2012)</li>
<li>Opera Mobile 12.1 (released 9/10 2012)</li>
<li>Chrome 53 for Android (released 8/9 2016)</li>
<li>Firefox 49 for Android (released 20/9 2016)</li>
<li>IE Mobile 10 (released 20/6 2012)</li>
<li>UC Browser 11 (released 17/8 2016)</li>
<li>Samsung Internet 4 (released 19/4 2016)</li>
</ul>
</div>
</body>
</html>

View File

@ -11,7 +11,7 @@ body {
height: 100%;
font-family: "Sans-serif", "Helvetica" !important;
font-size: 14px;
font-size: 12px;
background: #fff;
}
@ -93,6 +93,10 @@ a {
background-image: url(/images/checked.png);
}
.dropdown-content-checkbox-hidden {
display: none !important;
}
/* Hide the link that should open and close the topnav on small screens */
.topnav .icon {
display: none;
@ -264,7 +268,7 @@ a {
overflow: hidden;
}
#modal-station-info-iframe, #modal-station-search-iframe {
#modal-station-info-iframe, #modal-station-search-iframe, #modal-about-iframe {
width: 100%;
height: 100%;
border: none;

View File

@ -0,0 +1,24 @@
<?php
require "../../includes/bootstrap.php";
$response = [];
$station = StationRepository::getInstance()->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);

View File

@ -1,9 +1,9 @@
<?php
require "../includes/bootstrap.php";
require "../../includes/bootstrap.php";
if (isset($_GET['sid']) && isInt($_GET['sid'])) {
$station = StationRepository::getInstance()->getObjectById($_GET['sid']);
if (isset($_GET['id']) && isInt($_GET['id'])) {
$station = StationRepository::getInstance()->getObjectById($_GET['id']);
} else {
$station = new Station(null);
}

View File

@ -0,0 +1,55 @@
<?php
require "../../includes/bootstrap.php";
$response = [];
$station = StationRepository::getInstance()->getObjectById($_GET['id'] ?? null);
if ($station->isExistingObject()) {
$numberOfHours = $_GET['hours'] ?? 1;
error_log($numberOfHours);
$columns = ['timestamp'];
$type = 'speed';
if ($_GET['type'] == 'speed') {
$type = 'speed';
if ($_GET['imperialUnits'] ?? '0' == '1') {
$response[] = array('Time', 'Speed (mph)');
} else {
$response[] = array('Time', 'Speed (kmh)');
}
} else {
$type = 'altitude';
if ($_GET['imperialUnits'] ?? '0' == '1') {
$response[] = array('Time', 'Altitude (ft)');
} else {
$response[] = array('Time', 'Altitude (m)');
}
}
$columns[] = $type;
$packets = PacketRepository::getInstance()->getLatestDataListByStationId($_GET['id'] ?? null, $numberOfHours, $columns);
foreach($packets as $packet) {
$value = floatval($packet[$type]);
if ($_GET['imperialUnits'] ?? '0' == '1') {
if ($type == 'speed') {
$value = convertKilometerToMile($value);
} else if ($type == 'altitude') {
$value = convertMeterToFeet($value);
}
}
if ($type == 'speed' && count($response) > 1) {
if (isset($response[count($response) - 1])) {
$prevTimestamp = $response[count($response) - 1][0];
if ($prevTimestamp < ($timestamp - 60*60)) {
// Previous value is old, make sure we have a break in graph
$response[] = array($prevTimestamp + 1, null);
}
}
}
$response[] = [$packet['timestamp'], $value];
}
}
header('Content-type: application/json');
echo json_encode($response);

View File

@ -20,11 +20,13 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" integrity="sha512-jGsMH83oKe9asCpkOVkBnUrDDTp8wl+adkB2D+//JtlxO4SrLoJdhbOysIFQJloQFD+C4Fl1rMsQZF76JjV0eQ==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js" integrity="sha512-LGXaggshOkD/at6PFNcp2V2unf9LzFq6LE+sChH7ceMTDP0g2kn6Vxwgg7wkPP7AAtX+lmPqPdxB47A0Nz0cMQ==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/autolinker/3.14.2/Autolinker.min.js" integrity="sha512-qyoXjTIJ69k6Ik7CxNVKFAsAibo8vW/s3WV3mBzvXz6Gq0yGup/UsdZBDqFwkRuevQaF2g7qhD3E4Fs+OwS4hw==" crossorigin="anonymous"></script>
<script src="/js/convex-hull.js" crossorigin="anonymous"></script>
<!-- Map api javascripts and related dependencies -->
<?php $mapapi = $_GET['mapapi'] ?? 'leaflet'; ?>
<?php if ($mapapi == 'google') : ?>
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?key=<insert map key here>&libraries=visualization,geometry"></script>
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?libraries=visualization,geometry"></script>
<!-- <script type="text/javascript" src="//maps.googleapis.com/maps/api/js?key=<insert map key here>&libraries=visualization,geometry"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier/1.0.3/oms.min.js" integrity="sha512-/3oZy+rGpR6XGen3u37AEGv+inHpohYcJupz421+PcvNWHq2ujx0s1QcVYEiSHVt/SkHPHOlMFn5WDBb/YbE+g==" crossorigin="anonymous"></script>
<?php elseif ($mapapi == 'leaflet' || $mapapi == 'leaflet-vector'): ?>
@ -52,12 +54,11 @@
<script>
// Start everything!!!
$(document).ready(function() {
var wsServerUrl = 'ws://<?php echo $_SERVER['HTTP_HOST']; ?>:9000/ws'; // When using this in production you probably need to change this!!
var mapElementId = 'map-container';
var options = {};
options['isMobile'] = false;
options['useImperialUnit'] = <?php echo (isImperialUnitUser() ? 'true': 'false'); ?>;
options['coverageDataUrl'] = 'data/coverage.php';;
options['defaultTimeLength'] = 60; // In minutes
var md = new MobileDetect(window.navigator.userAgent);
if (md.mobile() !== null) {
@ -69,6 +70,7 @@
options['zoom'] = "<?php echo $_GET['zoom'] ?? '' ?>"; // Zoom level
options['timetravel'] = "<?php echo $_GET['timetravel'] ?? '' ?>"; // Unix timestamp to travel to
options['maptype'] = "<?php echo $_GET['maptype'] ?? '' ?>"; // May be "roadmap", "terrain" or "satellite"
options['mid'] = "<?php echo $_GET['mid'] ?? '' ?>"; // Render map from "Google My Maps" (requires https)
options['filters'] = {};
options['filters']['sid'] = "<?php echo $_GET['sid'] ?? '' ?>"; // Station id to filter on
@ -88,8 +90,8 @@
options['defaultLatitude'] = '59.30928';
options['defaultLongitude'] = '18.08830';
// Tip: request position from some ip->location service (here using freegeoip as an example)
$.getJSON('https://freegeoip.app/json/', function(data) {
// Tip: request position from some ip->location service (https://freegeoip.app/json and https://ipapi.co/json is two examples)
$.getJSON('https://ipapi.co/json', function(data) {
if (data.latitude && data.longitude) {
options['defaultLatitude'] = data.latitude;
options['defaultLongitude'] = data.longitude;
@ -124,6 +126,13 @@
var supportsWebSockets = 'WebSocket' in window || 'MozWebSocket' in window;
if (supportsWebSockets) {
<?php if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') : ?>
var wsServerUrl = 'wss://<?php echo $_SERVER['HTTP_HOST']; ?>:9000/ws';
<?php else : ?>
var wsServerUrl = 'ws://<?php echo $_SERVER['HTTP_HOST']; ?>:9000/ws';
<?php endif; ?>
var mapElementId = 'map-container';
trackdirect.init(wsServerUrl, mapElementId, options);
} else {
alert('This service require HTML 5 features to be able to feed you APRS data in real-time. Please upgrade your browser.');
@ -165,12 +174,12 @@
<div class="dropdown-content" id="tdTopnavTimelength">
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(10); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">10 minutes</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(30); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">30 minutes</a>
<a href="javascript:void(0);" id="tdTopnavTimelength60" onclick="trackdirect.setTimeLength(60); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox dropdown-content-checkbox-active">1 hour</a>
<a href="javascript:void(0);" id="tdTopnavTimelengthDefault" onclick="trackdirect.setTimeLength(60); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">1 hour</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(180); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">3 hours</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(360); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">6 hours</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(720); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">12 hours</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(1080); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">18 hours</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(1440); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox">24 hours</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(720); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox dropdown-content-checkbox-only-filtering dropdown-content-checkbox-hidden">12 hours</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(1080); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox dropdown-content-checkbox-only-filtering dropdown-content-checkbox-hidden">18 hours</a>
<a href="javascript:void(0);" onclick="trackdirect.setTimeLength(1440); $('#tdTopnavTimelength>a').removeClass('dropdown-content-checkbox-active'); $(this).addClass('dropdown-content-checkbox-active');" class="dropdown-content-checkbox dropdown-content-checkbox-only-filtering dropdown-content-checkbox-hidden">24 hours</a>
</div>
</div>
@ -242,7 +251,11 @@
</div>
</div>
<a href="javascript:void(0);"onclick="$('#modal-about').show();">
<a href="javascript:void(0);"
onclick="
$('#modal-about-iframe').attr('src', '/about.php');
$('#modal-about').show();"
title="More about this website!">
About
</a>
@ -287,62 +300,70 @@
<span class="modal-title">Travel in time</h2>
</div>
<div class="modal-content-body" style="margin: 0px 20px 20px 20px;">
<p>Select date and time to show map data for (enter time for your locale time zone). The regular time length select box can still be used to select how old data that should be shown (relative to selected date and time).</p>
<p>*Note that the heatmap will still based on data from the latest hour (not the selected date and time).</p>
<p>Date and time:</p>
<?php if (!isTimeTravelAllowed()) : ?>
<div style="text-align: center;">
<p style="max-width: 800px; display: inline-block; color: red;">
The time travel feature that allows you to see the map as it looked like an earlier date is disabled on this website. The reason is probably that it is a requirement from the data source used.
</p>
</div>
<?php else : ?>
<p>Select date and time to show map data for (enter time for your locale time zone). The regular time length select box can still be used to select how old data that should be shown (relative to selected date and time).</p>
<p>*Note that the heatmap will still based on data from the latest hour (not the selected date and time).</p>
<p>Date and time:</p>
<form id="timetravel-form">
<select id="timetravel-date" class="timetravel-select form-control">
<option value="0" selected>Select date</option>
<?php for($i=0; $i <= 10; $i++) : ?>
<?php $date = date('Y-m-d', strtotime("-$i days")); ?>
<option value="<?php echo $date; ?>"><?php echo $date; ?></option>
<?php endfor; ?>
</select>
<form id="timetravel-form">
<select id="timetravel-date" class="timetravel-select form-control"
<option value="0" selected>Select date</option>
<?php for($i=0; $i <= 10; $i++) : ?>
<?php $date = date('Y-m-d', strtotime("-$i days")); ?>
<option value="<?php echo $date; ?>"><?php echo $date; ?></option>
<?php endfor; ?>
</select>
<select id="timetravel-time" class="timetravel-select form-control">
<option value="0" selected>Select time</option>
<option value="00:00">00:00</option>
<option value="01:00">01:00</option>
<option value="02:00">02:00</option>
<option value="03:00">03:00</option>
<option value="04:00">04:00</option>
<option value="05:00">05:00</option>
<option value="06:00">06:00</option>
<option value="07:00">07:00</option>
<option value="08:00">08:00</option>
<option value="09:00">09:00</option>
<option value="10:00">10:00</option>
<option value="11:00">11:00</option>
<option value="12:00">12:00</option>
<option value="13:00">13:00</option>
<option value="14:00">14:00</option>
<option value="15:00">15:00</option>
<option value="16:00">16:00</option>
<option value="17:00">17:00</option>
<option value="18:00">18:00</option>
<option value="19:00">19:00</option>
<option value="20:00">20:00</option>
<option value="21:00">21:00</option>
<option value="22:00">22:00</option>
<option value="23:00">23:00</option>
</select>
<input type="submit"
value="Ok"
onclick="
if ($('#timetravel-date').val() != '0' && $('#timetravel-time').val() != '0') {
trackdirect.setTimeLength(60, false);
var ts = moment($('#timetravel-date').val() + ' ' + $('#timetravel-time').val(), 'YYYY-MM-DD HH:mm').unix();
trackdirect.setTimeTravelTimestamp(ts);
$('#right-container-timetravel-content').html('Time travel to ' + $('#timetravel-date').val() + ' ' + $('#timetravel-time').val());
$('#right-container-timetravel').show();
} else {
trackdirect.setTimeTravelTimestamp(0, true);
$('#right-container-timetravel').hide();
}
$('#modal-timetravel').hide();
return false;"/>
</form>
<select id="timetravel-time" class="timetravel-select form-control">
<option value="0" selected>Select time</option>
<option value="00:00">00:00</option>
<option value="01:00">01:00</option>
<option value="02:00">02:00</option>
<option value="03:00">03:00</option>
<option value="04:00">04:00</option>
<option value="05:00">05:00</option>
<option value="06:00">06:00</option>
<option value="07:00">07:00</option>
<option value="08:00">08:00</option>
<option value="09:00">09:00</option>
<option value="10:00">10:00</option>
<option value="11:00">11:00</option>
<option value="12:00">12:00</option>
<option value="13:00">13:00</option>
<option value="14:00">14:00</option>
<option value="15:00">15:00</option>
<option value="16:00">16:00</option>
<option value="17:00">17:00</option>
<option value="18:00">18:00</option>
<option value="19:00">19:00</option>
<option value="20:00">20:00</option>
<option value="21:00">21:00</option>
<option value="22:00">22:00</option>
<option value="23:00">23:00</option>
</select>
<input type="submit"
value="Ok"
onclick="
if ($('#timetravel-date').val() != '0' && $('#timetravel-time').val() != '0') {
trackdirect.setTimeLength(60, false);
var ts = moment($('#timetravel-date').val() + ' ' + $('#timetravel-time').val(), 'YYYY-MM-DD HH:mm').unix();
trackdirect.setTimeTravelTimestamp(ts);
$('#right-container-timetravel-content').html('Time travel to ' + $('#timetravel-date').val() + ' ' + $('#timetravel-time').val());
$('#right-container-timetravel').show();
} else {
trackdirect.setTimeTravelTimestamp(0, true);
$('#right-container-timetravel').hide();
}
$('#modal-timetravel').hide();
return false;"/>
</form>
<?php endif; ?>
</div>
</div>
</div>
@ -360,28 +381,13 @@
</div>
<div id="modal-about" class="modal">
<div class="modal-content">
<div class="modal-long-content">
<div class="modal-content-header">
<span class="modal-close" onclick="$('#modal-about').hide();">&times;</span>
<span class="modal-title">About</h2>
</div>
<div class="modal-content-body" style="margin: 0px 20px 20px 20px;">
<p>
Maintainer of this website: <a href="mailto:no@name.com">No Name</a>
</p>
<h4>What is APRS?</h4>
<p>
APRS (Automatic Packet Reporting System) is a digital communications system that uses packet radio to send real time tactical information. The APRS network is used by ham radio operators all over the world.
</p>
<p>
Information shared over the APRS network is for example coordinates, altitude, speed, heading, text messages, alerts, announcements, bulletins and weather data.
</p>
<h4>APRS Track Direct</h4>
<p>
This website is based on the APRS Track Direct tools. Read more on <a href="https://github.com/qvarforth/trackdirect" target="_blank">GitHub</a>. But please note that the maintainer of APRS Track Direct has nothing to do with this website.
</p>
<div class="modal-content-body">
<iframe id="modal-about-iframe" src=""></iframe>
</div>
</div>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
"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 = {}));

View File

@ -19,6 +19,11 @@ if (!inIframe()) {
$("#tdTopnav").hide();
}
// Set correct time length option to active
jQuery(document).ready(function ($) {
$("#tdTopnavTimelengthDefault").addClass("dropdown-content-checkbox-active");
});
// Open station dialog if user clicked on station name
jQuery(document).ready(function ($) {
trackdirect.addListener("station-name-clicked", function (data) {
@ -86,9 +91,10 @@ jQuery(document).ready(function ($) {
// Time travel is stopped when filtering is stopped
$("#right-container-timetravel").hide();
// Tail length is reset to 60 minutes when filtering is stopped
$("#tdTopnavTimelength>a").removeClass("active");
$("#tdTopnavTimelength60").addClass("active");
// Reset tail length to default when filtering is stopped
$("#tdTopnavTimelength>a").removeClass("dropdown-content-checkbox-active");
$("#tdTopnavTimelengthDefault").addClass("dropdown-content-checkbox-active");
$(".dropdown-content-checkbox-only-filtering").addClass("dropdown-content-checkbox-hidden");
} else {
var counts = {};
for (var i = 0; i < packets.length; i++) {
@ -102,6 +108,7 @@ jQuery(document).ready(function ($) {
"Filtering on " + Object.keys(counts).length + " station(s)"
);
$("#right-container-filtered").show();
$(".dropdown-content-checkbox-only-filtering").removeClass("dropdown-content-checkbox-hidden");
}
});
});

View File

@ -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 <i class="fa fa-spinner fa-spin" style="font-size:12px"></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(){});}}},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;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,});}};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,});}};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);var latLngPadding=(this._paddingInPercentOfMaxRange*0.01)*maxRange*0.000009;var latLngPaddingMin=this._paddingMinInMeters*0.000009;if(isNaN(latLngPadding)||latLngPadding<latLngPaddingMin){latLngPadding=latLngPaddingMin;}
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);return this._convertToLatLngPos(convexHullXYPositionsWithPadding);};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;};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;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;};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;};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;};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;};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;};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);}};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);}}};
trackdirect.models.Ruler=function(defaultLength,map){this._map=map;if(typeof google==="object"&&typeof google.maps==="object"){this._googleMapsInit(defaultLength);this._addGoolgeMapsListeners();}else if(typeof L==="object"){this.leafletInit(defaultLength);this._addLeafletListeners();}};trackdirect.models.Ruler.prototype._googleMapsInit=function(defaultLength){this.marker1=new google.maps.Marker({position:trackdirect.services.distanceCalculator.getPositionByDistance(this._map.getCenterLiteral(),270,defaultLength/2),draggable:true,map:this._map,});this.marker2=new google.maps.Marker({position:trackdirect.services.distanceCalculator.getPositionByDistance(this._map.getCenterLiteral(),90,defaultLength/2),draggable:true,map:this._map,});this.marker1.markerLabel=new trackdirect.models.Label({position:this.marker1.getPosition(),text:this._getDistance(this.marker1,this.marker2),},this._map);this.marker1.markerLabel.show();this.marker2.markerLabel=new trackdirect.models.Label({position:this.marker2.getPosition(),text:this._getDistance(this.marker2,this.marker1),},this._map);this.marker2.markerLabel.show();this.line=new google.maps.Polyline({path:[this.marker1.getPosition(),this.marker2.getPosition()],strokeColor:"#ffff00",strokeOpacity:0.7,strokeWeight:6,});this.line.setMap(this._map);};trackdirect.models.Ruler.prototype.leafletInit=function(defaultLength){this.marker1=new L.Marker(trackdirect.services.distanceCalculator.getPositionByDistance(this._map.getCenterLiteral(),270,defaultLength/2),{draggable:true});this.marker1.addTo(this._map);this.marker2=new L.Marker(trackdirect.services.distanceCalculator.getPositionByDistance(this._map.getCenterLiteral(),90,defaultLength/2),{draggable:true});this.marker2.addTo(this._map);this.marker1.markerLabel=new trackdirect.models.Label({position:this.marker1.getLatLng(),text:this._getDistance(this.marker1,this.marker2),},this._map);this.marker1.markerLabel.show();this.marker2.markerLabel=new trackdirect.models.Label({position:this.marker2.getLatLng(),text:this._getDistance(this.marker2,this.marker1),},this._map);this.marker2.markerLabel.show();this.line=new L.Polyline([this.marker1.getLatLng(),this.marker2.getLatLng()],{color:"#ffff00",opacity:0.8,weight:6,});this.line.addTo(this._map);};trackdirect.models.Ruler.prototype.show=function(){this.marker1.markerLabel.show();this.marker2.markerLabel.show();if(typeof google==="object"&&typeof google.maps==="object"){this.marker1.setMap(this._map);this.marker2.setMap(this._map);this.line.setMap(this._map);}else if(typeof L==="object"){this.marker1.addTo(this._map);this.marker2.addTo(this._map);this.line.addTo(this._map);}};trackdirect.models.Ruler.prototype.hide=function(){this.marker1.markerLabel.hide();this.marker2.markerLabel.hide();if(typeof google==="object"&&typeof google.maps==="object"){this.marker1.setMap(null);this.marker2.setMap(null);this.line.setMap(null);}else if(typeof L==="object"){this._map.removeLayer(this.marker1);this._map.removeLayer(this.marker2);this._map.removeLayer(this.line);}};trackdirect.models.Ruler.prototype._addGoolgeMapsListeners=function(){var me=this;google.maps.event.addListener(this.marker1,"drag",function(){me.line.setPath([me.marker1.getPosition(),me.marker2.getPosition()]);me._updateLabels();});google.maps.event.addListener(this.marker2,"drag",function(){me.line.setPath([me.marker1.getPosition(),me.marker2.getPosition()]);me._updateLabels();});};trackdirect.models.Ruler.prototype._addLeafletListeners=function(){var me=this;this.marker1.on("drag",function(e){me.line.setLatLngs([me.marker1.getLatLng(),me.marker2.getLatLng()]);me._updateLabels();});this.marker2.on("drag",function(e){me.line.setLatLngs([me.marker1.getLatLng(),me.marker2.getLatLng()]);me._updateLabels();});};trackdirect.models.Ruler.prototype._updateLabels=function(){this.marker1.markerLabel.hide();this.marker2.markerLabel.hide();if(typeof google==="object"&&typeof google.maps==="object"){this.marker1.markerLabel=new trackdirect.models.Label({position:this.marker1.getPosition(),text:this._getDistance(this.marker1,this.marker2),},this._map);this.marker2.markerLabel=new trackdirect.models.Label({position:this.marker2.getPosition(),text:this._getDistance(this.marker2,this.marker1),},this._map);}else if(typeof L==="object"){this.marker1.markerLabel=new trackdirect.models.Label({position:this.marker1.getLatLng(),text:this._getDistance(this.marker1,this.marker2),},this._map);this.marker2.markerLabel=new trackdirect.models.Label({position:this.marker2.getLatLng(),text:this._getDistance(this.marker2,this.marker1),},this._map);}
this.marker1.markerLabel.show();this.marker2.markerLabel.show();};trackdirect.models.Ruler.prototype._getDistance=function(marker1,marker2){var p1=this._getPositionLiteral(marker1);var p2=this._getPositionLiteral(marker2);var distance=Math.round(trackdirect.services.distanceCalculator.getDistance(p1,p2),0);if(distance>99999){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+"&ordm; "+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<previousVisibleMapSectors.length;i++){var mapSector=previousVisibleMapSectors[i];if(!this.isMapSectorVisible(mapSector)||this.getZoom()<trackdirect.settings.minZoomForMarkers){var mapSectorMarkerKeys=this.markerCollection.getMapSectorMarkerKeys(mapSector);for(var j=0;j<mapSectorMarkerKeys.length;j++){var markerIdKey=mapSectorMarkerKeys[j];markerIdKeyListToMaybeHide[markerIdKey]=markerIdKey;}}else if(this.getZoom()>=trackdirect.settings.minZoomForMarkers){var mapSectorMarkerKeys=this.markerCollection.getMapSectorMarkerKeys(mapSector);for(var j=0;j<mapSectorMarkerKeys.length;j++){var markerIdKey=mapSectorMarkerKeys[j];markerIdKeyListNotToHide[markerIdKey]=markerIdKey;}}}
for(var markerIdKey in markerIdKeyListToMaybeHide){if(markerIdKey in markerIdKeyListNotToHide){continue;}
var marker=this.markerCollection.getMarker(markerIdKey);if(marker!==null){trackdirect.services.callbackExecutor.addWithPriority(marker,marker.hideCompleteMarker,[]);}}}};trackdirect.models.Map.prototype._showMarkersInNewVisibleMapSectors=function(previousVisibleMapSectors){if(this.getZoom()>=trackdirect.settings.minZoomForMarkers){for(var i=0;i<this._visibleMapSectors.length;i++){var mapSector=this._visibleMapSectors[i];if(previousVisibleMapSectors.indexOf(mapSector)==-1||this._currentContentZoom<trackdirect.settings.minZoomForMarkers){var mapSectorMarkerKeys=this.markerCollection.getMapSectorMarkerKeys(mapSector);for(var j=0;j<mapSectorMarkerKeys.length;j++){var markerIdKey=mapSectorMarkerKeys[j];var marker=this.markerCollection.getMarker(markerIdKey);if(marker!==null){trackdirect.services.callbackExecutor.addWithPriority(marker,marker.showCompleteMarker,[]);}}}}}};trackdirect.models.Map.prototype._setMapInitialLocation=function(){var zoom=this._getInitialZoom();if(typeof this._tdMapOptions.initCenter!=="undefined"&&this._tdMapOptions.initCenter!==null){var pos=this._tdMapOptions.initCenter;this.setCenter(pos,zoom);}else{this.setMapDefaultLocation();this.setZoom(zoom);}
var marker=this.markerCollection.getMarker(markerIdKey);if(marker!==null){trackdirect.services.callbackExecutor.addWithPriority(marker,marker.hideCompleteMarker,[]);}}}};trackdirect.models.Map.prototype._showMarkersInNewVisibleMapSectors=function(previousVisibleMapSectors){if(this.getZoom()>=trackdirect.settings.minZoomForMarkers){for(var i=0;i<this._visibleMapSectors.length;i++){var mapSector=this._visibleMapSectors[i];if(previousVisibleMapSectors.indexOf(mapSector)==-1||this._currentContentZoom<trackdirect.settings.minZoomForMarkers){var mapSectorMarkerKeys=this.markerCollection.getMapSectorMarkerKeys(mapSector);for(var j=0;j<mapSectorMarkerKeys.length;j++){var markerIdKey=mapSectorMarkerKeys[j];var marker=this.markerCollection.getMarker(markerIdKey);if(marker!==null){trackdirect.services.callbackExecutor.addWithPriority(marker,marker.showCompleteMarker,[]);}}}}
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();}}}}};trackdirect.models.Map.prototype._setMapInitialLocation=function(){var zoom=this._getInitialZoom();if(typeof this._tdMapOptions.initCenter!=="undefined"&&this._tdMapOptions.initCenter!==null){var pos=this._tdMapOptions.initCenter;this.setCenter(pos,zoom);}else{this.setMapDefaultLocation();this.setZoom(zoom);}
this._emitTdEventListeners("change");};trackdirect.models.Map.prototype.setMapDefaultLocation=function(setDefaultZoom){setDefaultZoom=typeof setDefaultZoom!=="undefined"?setDefaultZoom:false;var defaultLatitude=typeof this._tdMapOptions.defaultLatitude!=="undefined"?this._tdMapOptions.defaultLatitude:59.35;var defaultLongitude=typeof this._tdMapOptions.defaultLongitude!=="undefined"?this._tdMapOptions.defaultLongitude:18.05;var pos={lat:parseFloat(defaultLatitude),lng:parseFloat(defaultLongitude),};if(setDefaultZoom){if(trackdirect.isMobile){this.setCenter(pos,trackdirect.settings.defaultCurrentZoomMobile);}else{this.setCenter(pos,trackdirect.settings.defaultCurrentZoom);}}else{this.setCenter(pos);}};trackdirect.models.Map.prototype.addMarkerToMapSectorInterval=function(markerIdKey,startLatLng,endLatLng){var minLat=startLatLng.lat;var maxLat=endLatLng.lat;var minLng=startLatLng.lng;var maxLng=endLatLng.lng;if(endLatLng.lat<minLat){minLat=endLatLng.lat;maxLat=startLatLng.lat;}
if(endLatLng.lng<minLng){minLng=endLatLng.lng;maxLng=startLatLng.lng;}
for(var lat=Math.floor(minLat);lat<=Math.ceil(maxLat);lat++){for(var lng=Math.floor(minLng);lng<=Math.ceil(maxLng);lng++){var markerMapSector=trackdirect.services.MapSectorCalculator.getMapSector(lat,lng);this.markerCollection.addMarkerToMapSector(markerIdKey,markerMapSector);if(this.isMapSectorVisible(markerMapSector)){if(this._newMarkersToShow.indexOf(markerIdKey)<0){this._newMarkersToShow.push(markerIdKey);}}}}};trackdirect.models.Map.prototype.addMarkerToMapSectors=function(markerIdKey,packet,tryToShowPacket){var markerMapSectors=[];markerMapSectors.push(packet.map_sector);if(typeof packet.related_map_sectors!=="undefined"&&packet.related_map_sectors!==null){for(var i=0;i<packet.related_map_sectors.length;i++){markerMapSectors.push(packet.related_map_sectors[i]);}}
@ -753,7 +789,8 @@ return weatherDiv;};trackdirect.models.InfoWindow.prototype._getTelemetryDiv=fun
if(comment==""){return null;}
var commentDiv=$(document.createElement("div"));commentDiv.css("clear","both");commentDiv.css("font-weight","bold");if(!trackdirect.isMobile){commentDiv.css("font-size","11px");}else{commentDiv.css("font-size","10px");}
commentDiv.html(comment);return commentDiv;};trackdirect.models.InfoWindow.prototype._addPhgLinkListeners=function(){var marker=this._marker;$("#half-phg-"+marker.packet.station_id+"-"+marker.packet.id).click(function(e){marker.showPHGCircle(true);return false;});$("#full-phg-"+marker.packet.station_id+"-"+marker.packet.id).click(function(e){marker.showPHGCircle(false);return false;});$("#none-phg-"+this._marker.packet.station_id+"-"+marker.packet.id).click(function(e){marker.hidePHGCircle();return false;});if($("#phglinks-"+marker.packet.station_id+"-"+marker.packet.id).length){$("#phglinks-"+marker.packet.station_id+"-"+marker.packet.id).show();}};trackdirect.models.InfoWindow.prototype._addRngLinkListeners=function(){var marker=this._marker;$("#half-rng-"+marker.packet.station_id+"-"+marker.packet.id).click(function(e){marker.showRNGCircle(true);return false;});$("#full-rng-"+marker.packet.station_id+"-"+marker.packet.id).click(function(e){marker.showRNGCircle(false);return false;});$("#none-rng-"+marker.packet.station_id+"-"+marker.packet.id).click(function(e){marker.hideRNGCircle();return false;});if($("#rnglinks-"+marker.packet.station_id+"-"+marker.packet.id).length){$("#rnglinks-"+marker.packet.station_id+"-"+marker.packet.id).show();}};trackdirect.models.InfoWindow.prototype._getMenuDiv=function(isInfoWindowOpen){var menuWrapperDiv=this._getMenuDivWrapperDiv();var menuDiv=this._getMenuDivMainDiv();menuWrapperDiv.append(menuDiv);var menuUl=this._getMenuDivUlDiv();menuDiv.append(menuUl);menuUl.append(this._getMenuDivTrackLink());menuUl.append(this._getMenuDivFilterLink());if(!trackdirect.isMobile){menuUl.append(this._getMenuDivCenterLink(isInfoWindowOpen));}
menuUl.append(this._getMenuDivZoomLink(isInfoWindowOpen));return menuWrapperDiv;};trackdirect.models.InfoWindow.prototype._getMenuDivWrapperDiv=function(){var menuWrapperDiv=$(document.createElement("div"));menuWrapperDiv.addClass("infowindow-menu-wrapper");menuWrapperDiv.css("clear","both");menuWrapperDiv.css("width","100%");menuWrapperDiv.css("padding-top","8px");return menuWrapperDiv;};trackdirect.models.InfoWindow.prototype._getMenuDivMainDiv=function(){var menuDiv=$(document.createElement("div"));menuDiv.addClass("infowindow-menu");menuDiv.css("width","100%");menuDiv.css("border-top","1px solid #cecece");return menuDiv;};trackdirect.models.InfoWindow.prototype._getMenuDivUlDiv=function(){var menuUl=$(document.createElement("ul"));menuUl.css("list-style-type","none");menuUl.css("list-style","none");menuUl.css("text-align","center");menuUl.css("margin","0");menuUl.css("padding","0");menuUl.css("display","table");return menuUl;};trackdirect.models.InfoWindow.prototype._getMenuDivLinkCss=function(){var liLinkCss={"list-style":"none",display:"table-cell","text-align":"center","padding-right":"10px",width:"auto",};return liLinkCss;};trackdirect.models.InfoWindow.prototype._getMenuDivTrackLink=function(){var trackLinkElementClass="trackStationLink"+this._marker.packet.station_id;var menuLi=$(document.createElement("li"));menuLi.css(this._getMenuDivLinkCss());var menuLink=$(document.createElement("a"));menuLink.css("color","#337ab7");menuLink.attr("href","#");menuLink.addClass(trackLinkElementClass);menuLink.attr("onclick","trackdirect.handleTrackStationRequest("+
menuUl.append(this._getMenuDivZoomLink(isInfoWindowOpen));if(!trackdirect.isEmbedded&&!inIframe()&&!this._marker.isMovingStation()&&this._marker.packet.source_id!=2){menuUl.append(this._getMenuDivCoverageLink());}
return menuWrapperDiv;};trackdirect.models.InfoWindow.prototype._getMenuDivWrapperDiv=function(){var menuWrapperDiv=$(document.createElement("div"));menuWrapperDiv.addClass("infowindow-menu-wrapper");menuWrapperDiv.css("clear","both");menuWrapperDiv.css("width","100%");menuWrapperDiv.css("padding-top","8px");return menuWrapperDiv;};trackdirect.models.InfoWindow.prototype._getMenuDivMainDiv=function(){var menuDiv=$(document.createElement("div"));menuDiv.addClass("infowindow-menu");menuDiv.css("width","100%");menuDiv.css("border-top","1px solid #cecece");return menuDiv;};trackdirect.models.InfoWindow.prototype._getMenuDivUlDiv=function(){var menuUl=$(document.createElement("ul"));menuUl.css("list-style-type","none");menuUl.css("list-style","none");menuUl.css("text-align","center");menuUl.css("margin","0");menuUl.css("padding","0");menuUl.css("display","table");return menuUl;};trackdirect.models.InfoWindow.prototype._getMenuDivLinkCss=function(){var liLinkCss={"list-style":"none",display:"table-cell","text-align":"center","padding-right":"10px",width:"auto",};return liLinkCss;};trackdirect.models.InfoWindow.prototype._getMenuDivTrackLink=function(){var trackLinkElementClass="trackStationLink"+this._marker.packet.station_id;var menuLi=$(document.createElement("li"));menuLi.css(this._getMenuDivLinkCss());var menuLink=$(document.createElement("a"));menuLink.css("color","#337ab7");menuLink.attr("href","#");menuLink.addClass(trackLinkElementClass);menuLink.attr("onclick","trackdirect.handleTrackStationRequest("+
this._marker.packet.station_id+
', "'+
trackLinkElementClass+
@ -768,6 +805,11 @@ Number.parseFloat(center.lat).toFixed(5)+
Number.parseFloat(center.lng).toFixed(5)+
"/zoom/"+
this._defaultMap.getZoom());}else{menuLink.html("Filter");menuLink.attr("href","/sid/"+this._marker.packet.station_id);}
menuLi.append(menuLink);return menuLi;};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;};trackdirect.models.InfoWindow.prototype._getMenuDivCenterLink=function(isInfoWindowOpen){var menuLi=$(document.createElement("li"));menuLi.css(this._getMenuDivLinkCss());var menuLink=$(document.createElement("a"));menuLink.css("color","#337ab7");if(isInfoWindowOpen){menuLink.attr("href","/center/"+
this._marker.packet.latitude.toFixed(5)+
","+

View File

@ -52,6 +52,7 @@
<div class="modal-inner-content-menu">
<span>Overview</span>
<a title="Statistics" href="/station/statistics.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Statistics</a>
<a title="Trail Chart" href="/station/trail.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Trail Chart</a>
<a title="Weather" href="/station/weather.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Weather</a>
<a title="Telemetry" href="/station/telemetry.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Telemetry</a>
<a title="Raw packets" href="/station/raw.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Raw packets</a>
@ -436,7 +437,7 @@
<?php endif; ?>
<?php endif; ?>
<div>Export <a href="/kml.php?sid=<?php echo $station->id; ?>"><?php echo htmlspecialchars($station->name); ?></a> data to KML</div>
<div>Export <a href="/data/kml.php?id=<?php echo $station->id; ?>"><?php echo htmlspecialchars($station->name); ?></a> data to KML</div>
</div>
<div style="clear: both;"></div>
<?php endif; ?>

View File

@ -84,6 +84,7 @@
<div class="modal-inner-content-menu">
<a title="Overview" href="/station/overview.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Overview</a>
<a title="Statistics" href="/station/statistics.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Statistics</a>
<a title="Trail Chart" href="/station/trail.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Trail Chart</a>
<a title="Weather" href="/station/weather.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Weather</a>
<a title="Telemetry" href="/station/telemetry.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Telemetry</a>
<span>Raw packets</span>

View File

@ -44,6 +44,7 @@
<div class="modal-inner-content-menu">
<a title="Overview" href="/station/overview.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Overview</a>
<span>Statistics</span>
<a title="Trail Chart" href="/station/trail.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Trail Chart</a>
<a title="Weather" href="/station/weather.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Weather</a>
<a title="Telemetry" href="/station/telemetry.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Telemetry</a>
<a title="Raw packets" href="/station/raw.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Raw packets</a>

View File

@ -73,6 +73,7 @@
<div class="modal-inner-content-menu">
<a title="Overview" href="/station/overview.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Overview</a>
<a title="Statistics" href="/station/statistics.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Statistics</a>
<a title="Trail Chart" href="/station/trail.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Trail Chart</a>
<a title="Weather" href="/station/weather.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Weather</a>
<span>Telemetry</span>
<a title="Raw packets" href="/station/raw.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Raw packets</a>

View File

@ -0,0 +1,210 @@
<?php require "../../includes/bootstrap.php"; ?>
<?php $station = StationRepository::getInstance()->getObjectById($_GET['id'] ?? null); ?>
<?php if ($station->isExistingObject()) : ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title><?php echo $station->name; ?> Trail Chart</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="mobile-web-app-capable" content="yes">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" integrity="sha512-jGsMH83oKe9asCpkOVkBnUrDDTp8wl+adkB2D+//JtlxO4SrLoJdhbOysIFQJloQFD+C4Fl1rMsQZF76JjV0eQ==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js" integrity="sha512-LGXaggshOkD/at6PFNcp2V2unf9LzFq6LE+sChH7ceMTDP0g2kn6Vxwgg7wkPP7AAtX+lmPqPdxB47A0Nz0cMQ==" crossorigin="anonymous"></script>
<script type="text/javascript" src="//www.gstatic.com/charts/loader.js"></script>
<link rel="stylesheet" href="/css/main.css">
<script>
$(document).ready(function() {
var locale = window.navigator.userLanguage || window.navigator.language;
moment.locale(locale);
$('.raw-packet-timestamp').each(function() {
if ($(this).html().trim() != '' && !isNaN($(this).html().trim())) {
$(this).html(moment(new Date(1000 * $(this).html())).format('L LTSZ'));
}
});
if (window.parent && window.parent.trackdirect) {
<?php if ($station->latestConfirmedLatitude != null && $station->latestConfirmedLongitude != null) : ?>
window.parent.trackdirect.focusOnStation(<?php echo $station->id ?>, true);
<?php endif; ?>
}
function loadTrailChart(stationId, hours, type, imperialUnits) {
$('#trail-curve-chart').html('Loading!');
$.ajax({
type: 'GET',
url: '/data/trail.php?id=' + stationId + '&hours=' + hours + '&type=' + type + '&imperialUnits=' + imperialUnits,
dataType: 'json'
}).done(function( result ) {
var onlyZeroValues = true;
for(i=1; i < result.length; i++) {
if (result[i][0] === parseInt(result[i][0], 10)) {
result[i][0] = new Date(result[i][0] * 1000);
}
if (result[i][1] != 0 && result[i][1] != null) {
onlyZeroValues = false;
}
}
var endTimestamp = new Date();
var startTimestamp = new Date(endTimestamp.getTime() - 1000*60*60*hours);
if (result != null && result.length > 1) {
google.charts.setOnLoadCallback(
function () {
var data = google.visualization.arrayToDataTable(result);
var pointSize = 0; // default
var dataOpacity = 1;
var trigger = 'selection';
var series = {
0: { lineWidth: 2},
1: { lineWidth: 1, color: 'darkgreen'},
};
var legend = {position: 'none'};
var title = result[0][1];
var curveType = 'none'; // can be 'function' or 'none'
var vAxis = {};
var hAxis = {
minValue: startTimestamp,
maxValue: endTimestamp
};
var explorer = {
axis: 'horizontal',
keepInBounds:true,
maxZoomIn: 50,
maxZoomOut: 1,
actions: ['dragToPan', 'rightClickToReset']
};
explorer = null;
if (result[0].length > 2) {
// We need to show legend if we plot more than one thing
legend = {position: 'top'};
title = null;
}
if (onlyZeroValues) {
// dot chart with big dots
var series = {
0: { lineWidth: 0, pointsVisible: true, pointSize: 4 },
}
} else if (hours < 24) {
// line chart
var series = {
0: { lineWidth: 2, pointsVisible: false},
}
} else {
// dot chart
var series = {
0: { lineWidth: 0, pointsVisible: true, pointSize: 1 },
}
}
if (type == 'speed') {
// I'm pretty sure we won't have negative speed
var vAxis = {
viewWindow : {
min: 0
}
};
}
var chartArea = {'width': '90%', 'height': '80%', 'left': '8%'};
var options = {
title: title,
curveType: curveType,
tooltip : {
trigger: trigger
},
pointsVisible : false,
pointSize: pointSize,
dataOpacity: dataOpacity,
series: series,
chartArea: chartArea,
legend: legend,
hAxis: hAxis,
vAxis: vAxis,
interpolateNulls: false,
crosshair: {
trigger: 'focus',
opacity: 0.5
},
explorer: explorer
};
var chart = new google.visualization.LineChart(document.getElementById('trail-curve-chart'));
chart.draw(data, options);
});
} else {
$('#trail-curve-chart').html('<br/><p><i><b>No trail data to create chart from.</b></i></p>');
}
});
}
google.charts.load('current', {'packages':['corechart', 'timeline']});
$('#trail-hours').change(function() {
loadTrailChart(<?php echo $station->id; ?>, $('#trail-hours').val(), $('#trail-type').val(), <?php echo $_GET['imperialUnits'] ?? 0; ?>);
});
$('#trail-type').change(function() {
loadTrailChart(<?php echo $station->id; ?>, $('#trail-hours').val(), $('#trail-type').val(), <?php echo $_GET['imperialUnits'] ?? 0; ?>);
});
loadTrailChart(<?php echo $station->id; ?>, $('#trail-hours').val(), $('#trail-type').val(), <?php echo $_GET['imperialUnits'] ?? 0; ?>);
});
</script>
</head>
<body>
<div class="modal-inner-content">
<div class="modal-inner-content-menu">
<a title="Overview" href="/station/overview.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Overview</a>
<a title="Statistics" href="/station/statistics.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Statistics</a>
<span>Trail Chart</span>
<a title="Weather" href="/station/weather.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Weather</a>
<a title="Telemetry" href="/station/telemetry.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Telemetry</a>
<a title="Raw packets" href="/station/raw.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Raw packets</a>
</div>
<div class="horizontal-line">&nbsp;</div>
<p>
Show chart for
</p>
<div class="form-container">
<select id="trail-type" style="float:left; margin-right: 5px;">
<option <?php echo (($_GET['type'] ?? 'speed') == 'speed' ? 'selected' : ''); ?> value="speed">Speed</option>
<option <?php echo (($_GET['type'] ?? 'speed') == 'altitude' ? 'selected' : ''); ?> value="altitude">Altitude</option>
</select>
<select id="trail-hours" style="float:left; margin-right: 5px;">
<option <?php echo (($_GET['hours'] ?? 1) == 1 ? 'selected' : ''); ?> value="1">Latest hour</option>
<option <?php echo (($_GET['hours'] ?? 1) == 3 ? 'selected' : ''); ?> value="3">Latest 3 hours</option>
<option <?php echo (($_GET['hours'] ?? 1) == 24 ? 'selected' : ''); ?> value="24">Latest 24 hours</option>
</select>
</div>
<div style="clear: both;"></div>
<div id="trail-curve-chart" style="width:850px; height: 350px;"></div>
<p>
* chart x-axis is based on your timezone (not the timezone of the station).
</p>
</div>
</body>
</html>
<?php endif; ?>

View File

@ -68,6 +68,7 @@
<div class="modal-inner-content-menu">
<a title="Overview" href="/station/overview.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Overview</a>
<a title="Statistics" href="/station/statistics.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Statistics</a>
<a title="Trail Chart" href="/station/trail.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Trail Chart</a>
<span>Weather</span>
<a title="Telemetry" href="/station/telemetry.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Telemetry</a>
<a title="Raw packets" href="/station/raw.php?id=<?php echo $station->id ?>&imperialUnits=<?php echo $_GET['imperialUnits'] ?? 0; ?>">Raw packets</a>

View File

@ -37,6 +37,7 @@ var trackdirect = {
_trackdirectInitDone: false,
isMobile: false,
coverageDataUrl: null,
settings: {},
/**
@ -184,9 +185,12 @@ var trackdirect = {
* @return None
*/
setCenter: function (latitude, longitude, zoom) {
latitude = typeof latitude !== "undefined" ? latitude : this._defaultLatitude;
longitude = typeof longitude !== "undefined" ? longitude : this._defaultLongitude;
zoom = typeof zoom !== "undefined" ? zoom : this.settings.defaultCurrentZoom;
latitude =
typeof latitude !== "undefined" ? latitude : this._defaultLatitude;
longitude =
typeof longitude !== "undefined" ? longitude : this._defaultLongitude;
zoom =
typeof zoom !== "undefined" ? zoom : this.settings.defaultCurrentZoom;
if (this._map !== null) {
this._map.setCenter({ lat: latitude, lng: longitude }, zoom);
@ -421,6 +425,99 @@ var trackdirect = {
}
},
/**
* Toggle station coverage
* @param {int} stationId
* @param {string} coverageLinkElementClass
*/
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 <i class="fa fa-spinner fa-spin" style="font-size:12px"></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"];
}

View File

@ -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

View File

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

View File

@ -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 = {};
};
/**

View File

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

View File

@ -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)

View File

@ -26,6 +26,10 @@ if __name__ == '__main__':
collectorNumber = int(sys.argv[2])
collectorOptions = config.collector[collectorNumber]
saveOgnStationsWithMissingIdentity = False
if (config.saveOgnStationsWithMissingIdentity) :
saveOgnStationsWithMissingIdentity = True
fh = logging.handlers.RotatingFileHandler(filename=os.path.expanduser(
collectorOptions['error_log']), mode='a', maxBytes=1000000, backupCount=10)
fh.setLevel(logging.WARNING)
@ -47,7 +51,8 @@ if __name__ == '__main__':
try:
trackDirectDataCollector = trackdirect.TrackDirectDataCollector(
collectorOptions)
collectorOptions,
saveOgnStationsWithMissingIdentity)
trackDirectDataCollector.run()
except Exception as e:
trackDirectLogger.error(e, exc_info=1)

View File

@ -48,6 +48,15 @@ class TrackDirectConfig(Singleton):
self.daysToSaveTelemetryData = int(configParser.get(
'database', 'days_to_save_telemetry_data').strip('"'))
self.saveOgnStationsWithMissingIdentity = False
try:
saveOgnStationsWithMissingIdentity = configParser.get(
'database', 'save_ogn_stations_with_missing_identity').strip('"')
if (saveOgnStationsWithMissingIdentity == "1"):
self.saveOgnStationsWithMissingIdentity = True
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
pass
# Websocket server
self.websocketHostname = configParser.get(
'websocket_server', 'host').strip('"')
@ -69,11 +78,11 @@ class TrackDirectConfig(Singleton):
allowTimeTravel = configParser.get(
'websocket_server', 'allow_time_travel').strip('"')
self.allowTimeTravel = True
if (allowTimeTravel == "0"):
self.allowTimeTravel = False
self.allowTimeTravel = False
if (allowTimeTravel == "1"):
self.allowTimeTravel = True
# Websocket server APRS-IS connection
# Websocket server APRS connection (we support 2 different sources, more can be added...)
try:
self.websocketAprsHost1 = configParser.get(
'websocket_server', 'aprs_host1').strip('"')
@ -98,6 +107,14 @@ class TrackDirectConfig(Singleton):
self.websocketAprsHost2 = None
self.websocketAprsPort2 = None
if (self.websocketAprsSourceId1 == 5 or self.websocketAprsSourceId2 == 5) :
# At least one source is of type OGN, disable display of older data
self.allowTimeTravel = False
if (self.maxDefaultTime > 1440) :
self.maxDefaultTime = 1440
if (self.maxFilterTime > 1440) :
self.maxDefaultTime = 1440
# Collectors
for collectorNumber in range(0, 5):
self.collector[collectorNumber] = {}
@ -129,6 +146,13 @@ class TrackDirectConfig(Singleton):
self.collector[collectorNumber]['error_log'] = configParser.get(
'collector' + str(collectorNumber), 'error_log').strip('"')
if (self.websocketAprsSourceId1 == 5 or self.websocketAprsSourceId2 == 5) :
# source is of type OGN, make sure we do not save to many packets (will cause to high load on db)
if (self.collector[collectorNumber]['frequency_limit'] < 10) :
self.collector[collectorNumber]['frequency_limit'] = 10
self.collector[collectorNumber]['save_fast_packets'] = False
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
self.collector[collectorNumber]['source_id'] = None
self.collector[collectorNumber]['host'] = None

View File

@ -19,7 +19,6 @@ from trackdirect.database.DatabaseConnection import DatabaseConnection
from trackdirect.repositories.StationRepository import StationRepository
from trackdirect.objects.Packet import Packet
class TrackDirectDataCollector():
"""An TrackDirectDataCollector instance connects to the data source and saves all received packets to the database
@ -28,12 +27,14 @@ class TrackDirectDataCollector():
This is useful if you want one connection to the regular APRS-IS network and one connection to the CWOP network.
"""
def __init__(self, collectorOptions):
def __init__(self, collectorOptions, saveOgnStationsWithMissingIdentity):
"""The __init__ method.
Args:
collectorOptions (dict): Contains data like host, port, callsign, passcode, source id
collectorOptions (dict): Contains data like host, port, callsign, passcode, source id
saveOgnStationsWithMissingIdentity (boolean): True if we should not ignore stationss with a missing identity
"""
self.saveOgnStationsWithMissingIdentity = saveOgnStationsWithMissingIdentity
self.sourceHostname = collectorOptions['host']
self.sourcePort = collectorOptions['port_full']
self.numbersInBatch = collectorOptions['numbers_in_batch']
@ -142,7 +143,7 @@ class TrackDirectDataCollector():
'Collector has a delay on %s seconds', self.delay)
packetDict = aprslib.parse(line)
parser = AprsPacketParser(self.db)
parser = AprsPacketParser(self.db, self.saveOgnStationsWithMissingIdentity)
parser.setSourceId(self.sourceId)
packet = parser.getPacket(packetDict, timestamp)
@ -177,7 +178,7 @@ class TrackDirectDataCollector():
try:
line = line.decode('utf-8', 'ignore')
packetDict = self.basicParse(line)
parser = AprsPacketParser(self.db)
parser = AprsPacketParser(self.db, self.saveOgnStationsWithMissingIdentity)
parser.setSourceId(self.sourceId)
packet = parser.getPacket(packetDict, timestamp, True)
packet.markerId = 1

View File

@ -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

View File

@ -51,13 +51,15 @@ class AprsPacketParser():
"""AprsPacketParser tackes a aprslib output and converts it to a Trackdirect Packet
"""
def __init__(self, db):
def __init__(self, db, saveOgnStationsWithMissingIdentity):
"""The __init__ method.
Args:
db (psycopg2.Connection): Database connection
db (psycopg2.Connection): Database connection
saveOgnStationsWithMissingIdentity (boolean): True if we should not ignore stationss with a missing identity
"""
self.db = db
self.saveOgnStationsWithMissingIdentity = saveOgnStationsWithMissingIdentity
self.logger = logging.getLogger('trackdirect')
self.databaseWriteAccess = True
@ -315,6 +317,9 @@ class AprsPacketParser():
return
elif (not ognDataPolicy.isAllowedToIdentify):
if (not self.saveOgnStationsWithMissingIdentity) :
self.packet.mapId = 15
return
self.isHiddenStation = True
self.data["from"] = self._getHiddenStationName()
self.data["object_name"] = None

View File

@ -36,7 +36,7 @@ class PacketCommentPolicy():
return self._formatComment(comment)
def _formatComment(self, comment):
"""Remove junk from comment (probably characters that should have been picked up during earlier parsing)
"""Remove junk from comment
Args:
comment (string): Comment from packet
@ -54,13 +54,13 @@ class PacketCommentPolicy():
comment = self._rchop(comment, "_%")
comment = self._lchop(comment, "_#")
comment = self._rchop(comment, "_#")
comment = self._rchop(comment, "_\"")
comment = self._lchop(comment, "_\"")
comment = self._rchop(comment, "_\"")
comment = self._lchop(comment, "_$")
comment = self._rchop(comment, "_$")
comment = self._lchop(comment, "_)")
comment = self._lchop(comment, "_(")
comment = self._rchop(comment, "_)")
comment = self._lchop(comment, "_(")
comment = self._rchop(comment, "_(")
comment = self._lchop(comment, "()")
comment = self._rchop(comment, "()")
@ -75,8 +75,8 @@ class PacketCommentPolicy():
comment = self._lchop(comment, "1}")
comment = self._rchop(comment, "1}")
comment = self._lchop(comment, "_1")
comment = self._lchop(comment, "_1")
comment = self._rchop(comment, "\"(}")
comment = self._rchop(comment, "_1")
comment = self._lchop(comment, "\"(}")
comment = self._rchop(comment, "\"(}")
comment = self._rchop(comment, "=")
comment = self._lchop(comment, "]")

View File

@ -42,6 +42,10 @@ class AprsISPayloadCreator():
self.config = TrackDirectConfig()
self.stationHashTimestamps = {}
self.saveOgnStationsWithMissingIdentity = False
if (self.config.saveOgnStationsWithMissingIdentity) :
self.saveOgnStationsWithMissingIdentity = True
def getPayloads(self, line, sourceId):
"""Takes a raw packet and returnes a dict with the parsed result
@ -85,7 +89,7 @@ class AprsISPayloadCreator():
Packet
"""
basicPacketDict = aprslib.parse(line)
parser = AprsPacketParser(self.db)
parser = AprsPacketParser(self.db, self.saveOgnStationsWithMissingIdentity)
parser.setDatabaseWriteAccess(False)
parser.setSourceId(sourceId)
try :