Compare commits

...

10 Commits

38 changed files with 1007 additions and 504 deletions

View File

@ -26,6 +26,10 @@ In der Übersicht nur Daten aus der Datenbank, vorerst keine Daten aus Wetris.
2) der Zeitpunkt des letzten Reports innerhalb der Zone
3) keine Referenz sondern Kopie der Position/Timestamp?
- Alarm / Zuordnung sind unabhängig von der Gruppe. Die Gruppe / der Zulauf ergibt sich erst in der Webanwendung. (Ist das sinnvoll?)
- Für die Abfrage in der Gruppe der Zonen Außenweser/Binnenweser: Jeweils "neuester" Alarm:
- wenn kein weiterer Alarm: o : unspezifisch
- wenn vorh. Alarm in Zone mit niedrigere Seq : <- eingehend
- wenn vorh. Alarm in Zone mit höherer Seq : -> abgehend
## Stand Dez 22

View File

@ -28,6 +28,7 @@ namespace bsmd.AIS2Service
private static Timer _staleTargetTimer; // cleanup sitrep
private static Timer _stalePosReportTimer; // clean db
private static IDisposable _restAPISelfHost = null;
private static AIS_SQLiteStorage _sqliteStorage = null;
#endregion
@ -38,16 +39,17 @@ namespace bsmd.AIS2Service
_tasks.Add(new SerialTCPReader(Properties.Settings.Default.DataSourceHost, Properties.Settings.Default.DataSourcePort, _inputLines));
_tasks.Add(new AISDecoder(_inputLines, _decodedClasses));
_tasks.Add(new SitRep(_decodedClasses, _sitRepList, _dbSaveTargets));
AIS_SQLiteStorage sqliteStorage = new AIS_SQLiteStorage(_dbSaveTargets);
_tasks.Add(sqliteStorage);
_tasks.Add(new AISZoneMonitor(_sitRepList, sqliteStorage));
_sqliteStorage = new AIS_SQLiteStorage(_dbSaveTargets);
_tasks.Add(_sqliteStorage);
_tasks.Add(new AISZoneMonitor(_sitRepList, _sqliteStorage));
// preload sit rep
Dictionary<int, AIS_Target> targets = await sqliteStorage.LoadTargets();
Dictionary<int, AIS_Target> targets = await _sqliteStorage.LoadTargets();
foreach(int key in targets.Keys)
{
_sitRepList.TryAdd(key, targets[key]);
}
_log.InfoFormat("preloaded {0} targets", _sitRepList.Count);
foreach (var task in _tasks)
{
@ -58,7 +60,7 @@ namespace bsmd.AIS2Service
// init timer tasks
_staleTargetTimer = new Timer(StaleTargetTimerCheck, null, 0, 60000); // check every minute, start immediately
_stalePosReportTimer = new Timer(StalePosReportCheck, sqliteStorage, 0, 60000 * 10); // every ten minutes,
_stalePosReportTimer = new Timer(StalePosReportCheck, _sqliteStorage, 0, 60000 * 10); // every ten minutes,
// if required start self-hosted owin endpoint
if(Properties.Settings.Default.EnableRestAPIEndpoint)
@ -87,10 +89,9 @@ namespace bsmd.AIS2Service
#region Properties
public static ConcurrentDictionary<int, AIS_Target> SitRep
{
get { return _sitRepList; }
}
public static ConcurrentDictionary<int, AIS_Target> SitRep { get { return _sitRepList; } }
public static AIS_SQLiteStorage SQLiteStorage { get { return _sqliteStorage; } }
#endregion

View File

@ -21,8 +21,8 @@ namespace bsmd.AIS2Service
#region Fields
ConcurrentDictionary<int, AIS_Target> _sitRepDict;
AIS_SQLiteStorage _storage;
private readonly ConcurrentDictionary<int, AIS_Target> _sitRepDict;
private readonly AIS_SQLiteStorage _storage;
private Thread _thread;
private bool _stopFlag = false;
private static readonly ILog _log = LogManager.GetLogger(typeof(AISZoneMonitor));
@ -45,18 +45,24 @@ namespace bsmd.AIS2Service
{
// load zones from storage
List<MonitorZone> allZones = _storage.LoadMonitorZones();
Dictionary<int, List<MonitorZone>> testZones = new Dictionary<int, List<MonitorZone>>();
Dictionary<int, List<AlarmAssignmentZone>> testDict = new Dictionary<int, List<AlarmAssignmentZone>>();
// letzte Alarme
// Dictionary<int, Alarm> currentAlarms = _storage.LoadAlarms();
foreach(MonitorZone zone in allZones)
{
// load up Zone list for all assignment mmsi's (to check)
foreach(MonitorAssignment ma in zone.Assignments)
{
if(!testZones.ContainsKey(ma.MMSI)) testZones[ma.MMSI] = new List<MonitorZone>();
testZones[ma.MMSI].Add(zone);
if(!testDict.ContainsKey(ma.MMSI)) testDict[ma.MMSI] = new List<AlarmAssignmentZone>();
ma.Alarms.AddRange(_storage.LoadAlarms(ma));
AlarmAssignmentZone aazone = new AlarmAssignmentZone();
aazone.Zone = zone;
aazone.Assignment = ma;
testDict[ma.MMSI].Add(aazone);
}
}
// loop
while(!_stopFlag)
{
@ -64,13 +70,41 @@ namespace bsmd.AIS2Service
foreach(int mmsi in _sitRepDict.Keys)
{
if(testZones.ContainsKey(mmsi))
if(testDict.ContainsKey(mmsi))
{
if (!_sitRepDict.ContainsKey(mmsi)) continue;
AIS_Target target = _sitRepDict[mmsi];
foreach(MonitorZone zone in testZones[mmsi]) {
if(zone.IsPointInPolygon4(target.Position))
foreach(AlarmAssignmentZone aazone in testDict[mmsi]) {
if(aazone.Zone.IsPointInPolygon4(target.Position))
{
_log.InfoFormat("{0} is in zone {1}", target.ToString(), zone.Name);
_log.DebugFormat("{0} is in zone {1}", target.ToString(), aazone.Zone.Name);
Alarm alarm;
if (aazone.Assignment.Alarms.Count > 0)
{
alarm = aazone.Assignment.Alarms[0];
if(alarm.Timestamp_Last.HasValue &&
((DateTime.Now - alarm.Timestamp_Last.Value).TotalMinutes > Properties.Settings.Default.MinAlarmIntervalMins))
{
// this "old" alarm will be triggered again (overwritten)
alarm.Acknowledged = null;
alarm.Timestamp_Last = null;
alarm.Timestamp_First = DateTime.Now;
}
else
{
// update the last time the target registered in the zone
alarm.Timestamp_Last = DateTime.Now;
}
}
else
{
// create new alarm
alarm = new Alarm(-1, aazone.Assignment);
alarm.Timestamp_First = DateTime.Now;
alarm.ZoneMonitorType = aazone.Assignment.MonitorType;
aazone.Assignment.Alarms.Add(alarm);
}
_storage.Save(alarm);
}
}
}
@ -113,6 +147,15 @@ namespace bsmd.AIS2Service
#endregion
#region class AssignmentZoneAlarm composition
private class AlarmAssignmentZone
{
public MonitorZone Zone { get; set; }
public MonitorAssignment Assignment { get; set; }
}
#endregion
}
}

View File

@ -1,13 +1,12 @@
using System;
using log4net;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using System.Threading;
using System.Data.SQLite;
using log4net;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace bsmd.AIS2Service
{
@ -16,13 +15,14 @@ namespace bsmd.AIS2Service
/// past track. It is just intended to function as a "saving the state" of the AIS situation.
/// Attention: Alarm zones / alarms are also stored here. This might or might not be such a great idea.
/// </summary>
public class AIS_SQLiteStorage : IAISThread
public class AIS_SQLiteStorage : IAISThread, IDisposable
{
#region Fields
private readonly SQLiteConnection _connection;
private Thread _thread;
private bool _stopFlag = false;
private bool disposedValue;
private static readonly ILog _log = LogManager.GetLogger(typeof(AIS_SQLiteStorage));
private readonly ConcurrentQueue<AIS_Target> _inputQueue;
private readonly Dictionary<int, AIS_Target> _storageTargets = new Dictionary<int, AIS_Target>();
@ -54,7 +54,7 @@ namespace bsmd.AIS2Service
/// monitor zone loader func for the zone alarm watchdog (doesn't need groups or such)
/// </summary>
/// <returns></returns>
public List<MonitorZone> LoadMonitorZones()
public List<MonitorZone> LoadMonitorZones(bool loadInnerCollections = true)
{
List<MonitorZone> monitorZones = new List<MonitorZone>();
if ((_connection == null) || (_connection.State != ConnectionState.Open)) return monitorZones; // can't load but return nothing in a friendly way
@ -79,51 +79,54 @@ namespace bsmd.AIS2Service
reader.Close();
lzCmd.Dispose();
// load vertices for each zone
string loadVertexString = "SELECT Id, latitude, longitude FROM zone_vertex WHERE monitor_zone_id = @ID";
SQLiteCommand lvCmd = new SQLiteCommand(loadVertexString, _connection);
foreach(MonitorZone mz in monitorZones)
if (loadInnerCollections)
{
lvCmd.Parameters.Clear();
lvCmd.Parameters.AddWithValue("@ID", mz.Id);
reader = lvCmd.ExecuteReader();
if(reader.HasRows)
// load vertices for each zone
string loadVertexString = "SELECT Id, latitude, longitude FROM zone_vertex WHERE monitor_zone_id = @ID";
SQLiteCommand lvCmd = new SQLiteCommand(loadVertexString, _connection);
foreach (MonitorZone mz in monitorZones)
{
while(reader.Read())
lvCmd.Parameters.Clear();
lvCmd.Parameters.AddWithValue("@ID", mz.Id);
reader = lvCmd.ExecuteReader();
if (reader.HasRows)
{
int id = reader.GetInt32(0);
GeoPoint gp = new GeoPoint(id);
gp.Lat = reader.GetDouble(1);
gp.Lon = reader.GetDouble(2);
mz.Vertices.Add(gp);
while (reader.Read())
{
int id = reader.GetInt32(0);
GeoPoint gp = new GeoPoint(id);
gp.Lat = reader.GetDouble(1);
gp.Lon = reader.GetDouble(2);
mz.Vertices.Add(gp);
}
}
reader.Close();
}
reader.Close();
}
lvCmd.Dispose();
lvCmd.Dispose();
// load mmsi / zone assignments for each zone
string loadAssignmentsString = "SELECT id, mmsi, type FROM zone_assignment WHERE monitor_zone_id = @ID";
SQLiteCommand laCmd = new SQLiteCommand(loadAssignmentsString, _connection);
foreach (MonitorZone mz in monitorZones)
{
laCmd.Parameters.Clear();
laCmd.Parameters.AddWithValue("@ID", mz.Id);
reader = laCmd.ExecuteReader();
if (reader.HasRows)
// load mmsi / zone assignments for each zone
string loadAssignmentsString = "SELECT id, mmsi, type FROM zone_assignment WHERE monitor_zone_id = @ID";
SQLiteCommand laCmd = new SQLiteCommand(loadAssignmentsString, _connection);
foreach (MonitorZone mz in monitorZones)
{
while (reader.Read())
laCmd.Parameters.Clear();
laCmd.Parameters.AddWithValue("@ID", mz.Id);
reader = laCmd.ExecuteReader();
if (reader.HasRows)
{
int id = reader.GetInt32(0);
MonitorAssignment ma = new MonitorAssignment(id);
ma.MMSI = reader.GetInt32(1);
ma.MonitorType = (MonitorAssignment.ZoneMonitorType)reader.GetInt32(2);
mz.Assignments.Add(ma);
while (reader.Read())
{
int id = reader.GetInt32(0);
MonitorAssignment ma = new MonitorAssignment(id);
ma.MMSI = reader.GetInt32(1);
ma.MonitorType = (MonitorAssignment.ZoneMonitorType)reader.GetInt32(2);
mz.Assignments.Add(ma);
}
}
reader.Close();
}
reader.Close();
laCmd.Dispose();
}
laCmd.Dispose();
return monitorZones;
}
@ -180,7 +183,8 @@ namespace bsmd.AIS2Service
int id = reader.GetInt32(0);
Alarm alarm = new Alarm(id, assignment);
alarm.Timestamp_First = reader.GetDateTime(1);
alarm.Timestamp_Last = reader.GetDateTime(2);
if(!reader.IsDBNull(2))
alarm.Timestamp_Last = reader.GetDateTime(2);
alarm.ZoneMonitorType = (MonitorAssignment.ZoneMonitorType)reader.GetInt32(3);
if(!reader.IsDBNull(4))
alarm.Acknowledged = reader.GetDateTime(4);
@ -207,8 +211,10 @@ namespace bsmd.AIS2Service
if (alarm.Id <= 0)
{
// insert
string saveAlarmString = $"INSERT INTO alarm (zone_assignment_id, timestamp_first, timestamp_last, type, acknowledged) VALUES ({alarm.Assignment.Id}, '{alarm.Timestamp_First}', '{alarm.Timestamp_Last}', {alarm.ZoneMonitorType}, {alarm.Acknowledged})";
string saveAlarmString = $"INSERT INTO alarm (zone_assignment_id, timestamp_first, timestamp_last, type) VALUES ({alarm.Assignment.Id}, @TS_FIRST, @TS_LAST, {(int) alarm.ZoneMonitorType})";
SQLiteCommand cmd = new SQLiteCommand(saveAlarmString, _connection);
cmd.Parameters.AddWithValue("@TS_FIRST", alarm.Timestamp_First);
cmd.Parameters.AddWithValue("@TS_LAST", alarm.Timestamp_Last);
int insertedRows = cmd.ExecuteNonQuery();
cmd.Dispose();
alarm.Id = GetLastInsertId();
@ -217,8 +223,14 @@ namespace bsmd.AIS2Service
else
{
// update
string updateAlarmString = $"UPDATE alarm SET acknowledged = {alarm.Acknowledged}, timestamp_last = '{alarm.Timestamp_Last}' WHERE id = {alarm.Id}";
string updateAlarmString = $"UPDATE alarm SET acknowledged = @TS_ACK, timestamp_first = @TS_FIRST, timestamp_last = @TS_LAST WHERE id = {alarm.Id}";
SQLiteCommand cmd = new SQLiteCommand(updateAlarmString, _connection);
cmd.Parameters.AddWithValue("@TS_FIRST", alarm.Timestamp_First);
cmd.Parameters.AddWithValue("@TS_LAST", alarm.Timestamp_Last);
if(alarm.Acknowledged.HasValue)
cmd.Parameters.AddWithValue("@TS_ACK", alarm.Acknowledged);
else
cmd.Parameters.AddWithValue("@TS_ACK", DBNull.Value);
int updatedRows = cmd.ExecuteNonQuery();
cmd.Dispose();
return (updatedRows == 1);
@ -247,6 +259,8 @@ namespace bsmd.AIS2Service
groups.Add(mGroup);
}
}
reader.Close();
laCmd.Dispose();
return groups;
}
@ -373,6 +387,42 @@ namespace bsmd.AIS2Service
#endregion
#region ShipLocationReport
/// <summary>
/// Helper method to allow the web interface to load all ship alarms in a group
/// (this makes sense since zones appear als columns in the overview)
/// </summary>
public List<ShipLocationReport> GetShipLocationReports(long groupId)
{
List<ShipLocationReport> slrs = new List<ShipLocationReport>();
string loadSLRString = "SELECT a.timestamp_first, a.timestamp_last, za.mmsi, mz.id FROM alarm a " +
"INNER JOIN zone_assignment za ON a.zone_assignment_id = za.id " +
"INNER JOIN monitor_zone mz ON za.monitor_zone_id = mz.id " +
$"WHERE mz.monitor_group_id = {groupId} ORDER BY mz.sequence";
SQLiteCommand laCmd = new SQLiteCommand(loadSLRString, _connection);
SQLiteDataReader reader = laCmd.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
ShipLocationReport slr = new ShipLocationReport();
slr.Timestamp_First = reader.GetDateTime(0);
if (reader.IsDBNull(1)) continue; // we dont want very new alarms
slr.Timestamp_Last = reader.GetDateTime(1);
slr.MMSI = reader.GetInt32(2);
slr.MonitorZoneId = reader.GetInt64(3);
slrs.Add(slr);
}
}
reader.Close();
return slrs;
}
#endregion
#endregion
#region private methods
@ -620,5 +670,32 @@ namespace bsmd.AIS2Service
#endregion
#region IDisposable implementation
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if(_connection.State == ConnectionState.Open)
{
_connection.Close();
}
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@ -50,7 +50,7 @@
<value>60</value>
</setting>
<setting name="SQLiteDBConnectionString" serializeAs="String">
<value>Data Source=ais_initial.db;Version=3;</value>
<value>Data Source=ais_initial.db;Version=3;Synchronous=OFF;Journal Mode=MEMORY;</value>
</setting>
<setting name="PosReportDBCleanupDays" serializeAs="String">
<value>7</value>
@ -67,6 +67,12 @@
<setting name="MonitorTargetSaturationSecs" serializeAs="String">
<value>120</value>
</setting>
<setting name="MinAlarmIntervalMins" serializeAs="String">
<value>60</value>
</setting>
<setting name="AutoAlarmExpiryHours" serializeAs="String">
<value>24</value>
</setting>
</bsmd.AIS2Service.Properties.Settings>
</applicationSettings>
<runtime>

View File

@ -1,311 +0,0 @@
// Copyright (c) 2022 - schick Informatik
// bsmd.AIS2Service [MonitorZone.cs]: %UserDisplayName%
// Description: Represents a geographical area that should be checked against
// ship positions
using log4net;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace bsmd.AIS2Service
{
#region DBEntity
public class DBEntity
{
protected long _id; // PK from database
public DBEntity(long id)
{
_id = id;
}
public long Id { get { return _id; } set { _id = value; } }
}
#endregion
#region class MonitorGroup
public class MonitorGroup : DBEntity
{
private static readonly ILog _log = LogManager.GetLogger(typeof(MonitorGroup));
#region fields
private string _name;
private readonly List<MonitorZone> _zones = new List<MonitorZone>();
#endregion
#region Construction
public MonitorGroup(long id, string name) : base( id )
{
_name = name;
}
#endregion
#region Properties
public List<MonitorZone> Zones { get { return _zones; } }
public string Name { get { return _name; } set { _name = value; } }
#endregion
#region public static methods
/// <summary>
/// Da Basti nun eine Datei mit allen Elementen exportiert gibt es eine einzelne Funktion, die alles
/// importiert
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
public static List<MonitorGroup> LoadGroups(string filename)
{
List<MonitorGroup> groups = new List<MonitorGroup>();
try
{
if (File.Exists(filename))
{
XDocument kml = XDocument.Load(filename);
XNamespace ns = "http://www.opengis.net/kml/2.2";
foreach(XElement rootFolderNode in kml.Root.Element(ns + "Document").Element(ns + "Folder").Elements(ns + "Folder"))
{
MonitorGroup mg = new MonitorGroup(-1, rootFolderNode.Element(ns + "name").Value);
int sequence = 1;
foreach(XElement placemark in rootFolderNode.Elements(ns + "Placemark"))
{
MonitorZone mz = new MonitorZone(-1, placemark.Element(ns + "name").Value);
mz.Active = true;
mz.Sequence = sequence;
// now add all vertices
string[] vertices = placemark.Element(ns + "Polygon").Element(ns + "outerBoundaryIs").Element(ns + "LinearRing").Element(ns + "coordinates").Value.Split(' ');
for (int i = 0; i < vertices.Length - 1; i++)
{
string[] pointElems = vertices[i].Trim().Split(',');
if (pointElems.Length != 3) continue;
GeoPoint gp = new GeoPoint(-1);
gp.Lon = Double.Parse(pointElems[0], System.Globalization.NumberFormatInfo.InvariantInfo);
gp.Lat = Double.Parse(pointElems[1], System.Globalization.NumberFormatInfo.InvariantInfo);
mz.Vertices.Add(gp);
}
mg.Zones.Add(mz);
sequence++;
}
groups.Add(mg);
}
}
}
catch(Exception ex)
{
_log.Error(ex.ToString());
}
return groups;
}
#endregion
#region public methods
public override string ToString()
{
return this.Name;
}
#endregion
}
#endregion
#region class MonitorZone
public class MonitorZone : DBEntity, IComparable<MonitorZone>
{
#region fields
private readonly List<GeoPoint> _vertices = new List<GeoPoint>();
private readonly List<MonitorAssignment> _assignments = new List<MonitorAssignment>();
private readonly string _name;
#endregion
#region Construction
public MonitorZone(long id, string name) : base (id)
{
_name = name;
}
#endregion
#region Properties
public string Name { get { return _name; } }
public List<GeoPoint> Vertices { get { return _vertices; } }
public List<MonitorAssignment> Assignments { get { return _assignments; } }
public bool Active { get; set; }
public int Sequence { get; set; }
public long MonitorGroupId { get; set; }
#endregion
#region public static methods
public static MonitorZone ImportFromKML(string filename)
{
MonitorZone result = null;
if (File.Exists(filename))
{
XDocument kml = XDocument.Load(filename);
XNamespace ns = "http://www.opengis.net/kml/2.2";
string name = kml.Root.Element(ns + "Document").Element(ns + "name").Value;
if (name.EndsWith(".kml")) name = name.Substring(0, name.Length - 4);
result = new MonitorZone(-1, name);
// now find all vertices
string[] vertices = kml.Root.Element(ns + "Document").Element(ns + "Placemark").Element(ns + "Polygon").Element(ns + "outerBoundaryIs").Element(ns + "LinearRing").Element(ns + "coordinates").Value.Split(' ');
for (int i = 0; i < vertices.Length - 1; i++)
{
string[] pointElems = vertices[i].Trim().Split(',');
if (pointElems.Length != 3) continue;
GeoPoint gp = new GeoPoint(-1);
gp.Lon = Double.Parse(pointElems[0], System.Globalization.NumberFormatInfo.InvariantInfo);
gp.Lat = Double.Parse(pointElems[1], System.Globalization.NumberFormatInfo.InvariantInfo);
result.Vertices.Add(gp);
}
}
return result;
}
#endregion
#region public methods
public bool IsPointInPolygon4(GeoPoint testPoint)
{
bool result = false;
int j = _vertices.Count() - 1;
for (int i = 0; i < _vertices.Count(); i++)
{
if (_vertices[i].Lat < testPoint.Lat && _vertices[j].Lat >= testPoint.Lat || _vertices[j].Lat < testPoint.Lat && _vertices[i].Lat >= testPoint.Lat)
{
if (_vertices[i].Lon + (testPoint.Lat - _vertices[i].Lat) / (_vertices[j].Lat - _vertices[i].Lat) * (_vertices[j].Lon - _vertices[i].Lon) < testPoint.Lon)
{
result = !result;
}
}
j = i;
}
return result;
}
public int CompareTo(MonitorZone other)
{
return this.Sequence.CompareTo(other.Sequence);
}
public override string ToString()
{
return String.Format("{0} (Seq.:{1} #Vert.:{2}", this.Name, this.Sequence, this.Vertices.Count);
}
#endregion
}
#endregion
#region class GeoPoint
public class GeoPoint : DBEntity
{
public GeoPoint(long id) : base (id)
{}
public double Lat { get; set; }
public double Lon { get; set; }
public long MonitorZoneId { get; set; }
}
#endregion
#region class MonitorAssignment
public class MonitorAssignment : DBEntity
{
public MonitorAssignment(long id) : base(id)
{}
[Flags]
public enum ZoneMonitorType
{
INACTIVE = 0,
ENTER = 1,
EXIT = 2,
PASSTHROUGH = 4, // outside - enter - inside - exit - outside
LEAVE_AND_RETURN = 8 // inside - exit - outside - enter - inside
}
public int MMSI { get; set; }
public ZoneMonitorType MonitorType { get; set; } = ZoneMonitorType.INACTIVE;
public long MonitorZoneId { get; set; }
public override string ToString()
{
return String.Format("{0} {1}", MMSI, MonitorType);
}
}
#endregion
#region class Alarm
public class Alarm : DBEntity
{
private readonly MonitorAssignment _assignment;
public Alarm(long id, MonitorAssignment assignment) : base(id) { _assignment = assignment; }
public MonitorAssignment Assignment { get { return _assignment; } }
public DateTime Timestamp_First { get; set; }
public DateTime Timestamp_Last { get; set; }
public DateTime? Acknowledged { get; set; }
public MonitorAssignment.ZoneMonitorType ZoneMonitorType { get; set; }
}
#endregion
}

View File

@ -70,7 +70,7 @@ namespace bsmd.AIS2Service.Properties {
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Data Source=ais_initial.db;Version=3;")]
[global::System.Configuration.DefaultSettingValueAttribute("Data Source=ais_initial.db;Version=3;Synchronous=OFF;Journal Mode=MEMORY;")]
public string SQLiteDBConnectionString {
get {
return ((string)(this["SQLiteDBConnectionString"]));
@ -121,5 +121,23 @@ namespace bsmd.AIS2Service.Properties {
return ((int)(this["MonitorTargetSaturationSecs"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("60")]
public int MinAlarmIntervalMins {
get {
return ((int)(this["MinAlarmIntervalMins"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("24")]
public int AutoAlarmExpiryHours {
get {
return ((int)(this["AutoAlarmExpiryHours"]));
}
}
}
}

View File

@ -18,7 +18,7 @@
<Value Profile="(Default)">60</Value>
</Setting>
<Setting Name="SQLiteDBConnectionString" Type="System.String" Scope="Application">
<Value Profile="(Default)">Data Source=ais_initial.db;Version=3;</Value>
<Value Profile="(Default)">Data Source=ais_initial.db;Version=3;Synchronous=OFF;Journal Mode=MEMORY;</Value>
</Setting>
<Setting Name="PosReportDBCleanupDays" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">7</Value>
@ -35,5 +35,11 @@
<Setting Name="MonitorTargetSaturationSecs" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">120</Value>
</Setting>
<Setting Name="MinAlarmIntervalMins" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">60</Value>
</Setting>
<Setting Name="AutoAlarmExpiryHours" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">24</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -94,25 +94,30 @@
<DependentUpon>AIS2_Service.cs</DependentUpon>
</Compile>
<Compile Include="AISClass.cs" />
<Compile Include="AISController.cs" />
<Compile Include="AISDecoder.cs" />
<Compile Include="webservice\AISController.cs" />
<Compile Include="decoding\AISDecoder.cs" />
<Compile Include="AISManager.cs" />
<Compile Include="AISZoneMonitor.cs" />
<Compile Include="AIS_BaseStation.cs" />
<Compile Include="AIS_BaseStationReport.cs" />
<Compile Include="AIS_ClassB.cs" />
<Compile Include="AIS_ClassBExt.cs" />
<Compile Include="AIS_ClassBStatic.cs" />
<Compile Include="AIS_PosReport.cs" />
<Compile Include="decoding\AIS_BaseStation.cs" />
<Compile Include="decoding\AIS_BaseStationReport.cs" />
<Compile Include="decoding\AIS_ClassB.cs" />
<Compile Include="decoding\AIS_ClassBExt.cs" />
<Compile Include="decoding\AIS_ClassBStatic.cs" />
<Compile Include="decoding\AIS_PosReport.cs" />
<Compile Include="AIS_SQLiteStorage.cs" />
<Compile Include="AIS_StaticData.cs" />
<Compile Include="decoding\AIS_StaticData.cs" />
<Compile Include="AIS_Target.cs" />
<Compile Include="IAISThread.cs" />
<Compile Include="Lookup.cs" />
<Compile Include="MonitorZone.cs" />
<Compile Include="NMEA.cs" />
<Compile Include="NMEA_AIS_Sentence.cs" />
<Compile Include="NMEA_PNMLS_Sentence.cs" />
<Compile Include="zone_alarm\Alarm.cs" />
<Compile Include="zone_alarm\DBEntity.cs" />
<Compile Include="zone_alarm\GeoPoint.cs" />
<Compile Include="zone_alarm\MonitorAssignment.cs" />
<Compile Include="zone_alarm\MonitorGroup.cs" />
<Compile Include="zone_alarm\MonitorZone.cs" />
<Compile Include="decoding\NMEA.cs" />
<Compile Include="decoding\NMEA_AIS_Sentence.cs" />
<Compile Include="decoding\NMEA_PNMLS_Sentence.cs" />
<Compile Include="Program.cs" />
<Compile Include="ProjectInstaller.cs">
<SubType>Component</SubType>
@ -127,9 +132,12 @@
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
<Compile Include="SerialTCPReader.cs" />
<Compile Include="webservice\ShipLocationReport.cs" />
<Compile Include="SitRep.cs" />
<Compile Include="StartupWebAPI.cs" />
<Compile Include="webservice\SLRController.cs" />
<Compile Include="webservice\StartupWebAPI.cs" />
<Compile Include="Util.cs" />
<Compile Include="webservice\ZonesController.cs" />
</ItemGroup>
<ItemGroup>
<None Include="..\SQL\ais_initial.db">

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Zonen Übersicht</title>
<script type="text/javascript" src="zonen.js"></script>
</head>
<body onload="createAreas()">
<div class="table_group" id="root-div" />
<div id="bottomcenter">
(c) <a href="http://www.textbausteine.net" target="#">Informatikbüro Daniel Schick</a>
</div>
</body>
</html>

View File

@ -6,123 +6,50 @@
body{
font-family: Helvetica;
-webkit-font-smoothing: antialiased;
background: rgba( 71, 147, 227, 1);
}
h2{
text-align: center;
font-size: 18px;
text-transform: uppercase;
letter-spacing: 1px;
color: white;
padding: 30px 0;
}
/* Table Styles */
.table-wrapper{
margin: 10px 70px 70px;
box-shadow: 0px 35px 50px rgba( 0, 0, 0, 0.2 );
}
.fl-table {
border-radius: 5px;
font-size: 12px;
font-weight: normal;
border: none;
.styled-table {
border-collapse: collapse;
width: 100%;
max-width: 100%;
white-space: nowrap;
background-color: white;
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
.fl-table td, .fl-table th {
text-align: center;
padding: 8px;
}
.fl-table td {
border-right: 1px solid #f8f8f8;
font-size: 12px;
}
.fl-table thead th {
.styled-table thead tr {
background-color: #009879;
color: #ffffff;
background: #4FC3A1;
text-align: left;
}
.styled-table th,
.styled-table td {
padding: 12px 15px;
}
.styled-table tbody tr {
border-bottom: 1px solid #dddddd;
}
.styled-table tbody tr:nth-of-type(even) {
background-color: #f3f3f3;
}
.styled-table tbody tr:last-of-type {
border-bottom: 2px solid #009879;
}
.styled-table tbody tr.active-row {
font-weight: bold;
color: #009879;
}
.fl-table thead th:nth-child(odd) {
color: #ffffff;
background: #324960;
}
.fl-table tr:nth-child(even) {
background: #F8F8F8;
}
/* Responsive */
@media (max-width: 767px) {
.fl-table {
display: block;
width: 100%;
}
.table-wrapper:before{
content: "Scroll horizontally >";
display: block;
text-align: right;
font-size: 11px;
color: white;
padding: 0 0 10px;
}
.fl-table thead, .fl-table tbody, .fl-table thead th {
display: block;
}
.fl-table thead th:last-child{
border-bottom: none;
}
.fl-table thead {
float: left;
}
.fl-table tbody {
width: auto;
position: relative;
overflow-x: auto;
}
.fl-table td, .fl-table th {
padding: 20px .625em .625em .625em;
height: 60px;
vertical-align: middle;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
width: 120px;
font-size: 13px;
text-overflow: ellipsis;
}
.fl-table thead th {
text-align: left;
border-bottom: 1px solid #f7f7f9;
}
.fl-table tbody tr {
display: table-cell;
}
.fl-table tbody tr:nth-child(odd) {
background: none;
}
.fl-table tr:nth-child(even) {
background: transparent;
}
.fl-table tr td:nth-child(odd) {
background: #F8F8F8;
border-right: 1px solid #E6E4E4;
}
.fl-table tr td:nth-child(even) {
border-right: 1px solid #E6E4E4;
}
.fl-table tbody td {
display: block;
text-align: center;
}
}
#bottomcenter {
position: fixed;
left: 50%;
bottom: 20px;
transform: translate(-50%, -50%);
margin: 0 auto;
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Web.Http;
using log4net;
namespace bsmd.AIS2Service
{
public class SLRController : ApiController
{
private static readonly ILog _log = LogManager.GetLogger(typeof(SLRController));
[HttpGet]
public IEnumerable<ShipLocationReport> Get([FromUri] int? id)
{
if (!id.HasValue) return null;
List<ShipLocationReport> result = AISManager.SQLiteStorage.GetShipLocationReports(id.Value);
// remove results not in SitRep
int obscureTargetCnt = result.RemoveAll(x => !AISManager.SitRep.ContainsKey(x.MMSI));
_log.InfoFormat("removed {0} obscure targets", obscureTargetCnt);
// remove targets w/o name (i.e. static data)
int unnamedPlayerCnt = result.RemoveAll(x => string.IsNullOrEmpty(AISManager.SitRep[x.MMSI].Name));
_log.InfoFormat("removed {0} unnamed targets", unnamedPlayerCnt);
// Class B targets entfernen
int classBReportCnt = result.RemoveAll(x => AISManager.SitRep[x.MMSI].IsClassB ?? false);
_log.InfoFormat("removed {0} class B alarms from list", classBReportCnt); // tut des?
// auch alles entfernen was "abgelaufen" ist und nicht im SitRep enthalten ist
int expiredCnt = result.RemoveAll(x => (DateTime.Now - x.Timestamp_Last).TotalMinutes > 1440);
_log.InfoFormat("removed {0} expired (> 1 day) alarms from list", expiredCnt);
Dictionary<int, List<ShipLocationReport>> mmsiDict = new Dictionary<int, List<ShipLocationReport>>();
foreach(ShipLocationReport report in result) {
if (!mmsiDict.ContainsKey(report.MMSI)) mmsiDict[report.MMSI] = new List<ShipLocationReport>();
mmsiDict[report.MMSI].Add(report);
if (AISManager.SitRep.ContainsKey(report.MMSI))
{
report.Destination = AISManager.SitRep[report.MMSI].Destination;
report.Name = AISManager.SitRep[report.MMSI].Name;
report.NavStatus = AIS_PosReport.GetNavStatus(AISManager.SitRep[report.MMSI].NavStatus);
report.IMO = AISManager.SitRep[report.MMSI].IMO;
}
}
// determine "state" of vessel through alarm comparison. Possible values are:
// 0 = stationary (= equals)
// 1 = incoming
// 2 = outgoing
// 3 = expired
foreach(int key in mmsiDict.Keys)
{
bool expired = true;
DateTime? lastDate= null;
bool? incoming = null;
// first run through alarm list to determine state
foreach(ShipLocationReport slr in mmsiDict[key])
{
if((DateTime.Now - slr.Timestamp_Last).TotalHours < Properties.Settings.Default.AutoAlarmExpiryHours) expired = false;
if (lastDate == null)
{
lastDate = slr.Timestamp_Last;
}
else
{
incoming = slr.Timestamp_Last < lastDate;
}
}
// second run through alarm list to set state flag in all entries
foreach (ShipLocationReport slr in mmsiDict[key])
{
if (expired)
{
slr.VoyageDirection = 3;
}
else if (incoming.HasValue)
{
if (incoming.Value) { slr.VoyageDirection = 1; }
else { slr.VoyageDirection = 2; }
}
else
{
slr.VoyageDirection = 0; // stationary / no comparison value
}
}
}
return result;
}
}
}

View File

@ -0,0 +1,28 @@
using System;
namespace bsmd.AIS2Service
{
/// <summary>
/// Helper class to report a ship position / entry into a zone (aka alarm connected to zone)
/// </summary>
public class ShipLocationReport
{
public DateTime Timestamp_First { get; set; }
public DateTime Timestamp_Last { get; set; }
public long MonitorZoneId { get; set; }
public int MMSI { get; set; }
public int? IMO { get; set; }
public string Name { get; set; }
public string Destination { get; set; }
public string NavStatus { get; set; }
public int VoyageDirection { get; set; }
}
}

View File

@ -16,6 +16,19 @@ namespace bsmd.AIS2Service
routeTemplate: "api/{Controller}",
defaults: new { id = RouteParameter.Optional, Controller = "AIS"}
);
config.Routes.MapHttpRoute(
name: "ZonesList",
routeTemplate: "api/{Controller}",
defaults: new { id = RouteParameter.Optional, Controller = "Zones" }
);
config.Routes.MapHttpRoute(
name: "SLRList",
routeTemplate: "api/{Controller}",
defaults: new { id = RouteParameter.Optional, Controller = "SLR" }
);
config.EnableCors(cors);
appBuilder.UseWebApi(config);

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Web.Http;
using System.Web.Http.Cors;
namespace bsmd.AIS2Service
{
public class ZonesController : ApiController
{
[HttpGet]
public IEnumerable<MonitorGroup> Get() // Get([FromUri] int? id)
{
List<MonitorZone> allZones = AISManager.SQLiteStorage.LoadMonitorZones(false);
List<MonitorGroup> groups = AISManager.SQLiteStorage.LoadGroups();
foreach(MonitorGroup group in groups)
{
foreach(MonitorZone zone in allZones)
{
if(group.Id == zone.MonitorGroupId)
{
group.Zones.Add(zone);
}
}
group.Zones.Sort();
}
return groups;
/*
if (!id.HasValue)
{
List<MonitorGroup> groups = AISManager.SQLiteStorage.LoadGroups();
return groups;
}
else
{
return null;
}
*/
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) 2023 - schick Informatik
// bsmd.AIS2Service [Alarm.cs]: Daniel Schick
// Description: alarm created if vessel interacts with zone
//
using System;
namespace bsmd.AIS2Service
{
#region class Alarm
public class Alarm : DBEntity
{
private readonly MonitorAssignment _assignment;
public Alarm(long id, MonitorAssignment assignment) : base(id) { _assignment = assignment; }
public MonitorAssignment Assignment { get { return _assignment; } }
public DateTime Timestamp_First { get; set; }
public DateTime? Timestamp_Last { get; set; }
public DateTime? Acknowledged { get; set; }
public MonitorAssignment.ZoneMonitorType ZoneMonitorType { get; set; }
}
#endregion
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2023 - schick Informatik
// bsmd.AIS2Service [DBEntity.cs]: Daniel Schick
// Description: Root class for SQlite database entities
//
namespace bsmd.AIS2Service
{
#region DBEntity
public class DBEntity
{
protected long _id; // PK from database
public DBEntity(long id)
{
_id = id;
}
public long Id { get { return _id; } set { _id = value; } }
}
#endregion
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2023 - schick Informatik
// bsmd.AIS2Service.zone_alarm [GeoPoint.cs]: Daniel Schick
// Description: Single vertex of a zone
//
namespace bsmd.AIS2Service
{
#region class GeoPoint
public class GeoPoint : DBEntity
{
public GeoPoint(long id) : base(id)
{ }
public double Lat { get; set; }
public double Lon { get; set; }
public long MonitorZoneId { get; set; }
}
#endregion
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2023 - schick Informatik
// bsmd.AIS2Service.zone_alarm [MonitorAssignment.cs]: Daniel Schick
// Description: Mapping between targets (MMSI) and zones
//
using System;
using System.Collections.Generic;
namespace bsmd.AIS2Service
{
#region class MonitorAssignment
public class MonitorAssignment : DBEntity
{
public MonitorAssignment(long id) : base(id)
{ }
[Flags]
public enum ZoneMonitorType
{
INACTIVE = 0,
ENTER = 1,
EXIT = 2,
PASSTHROUGH = 4, // outside - enter - inside - exit - outside
LEAVE_AND_RETURN = 8 // inside - exit - outside - enter - inside
}
public int MMSI { get; set; }
public ZoneMonitorType MonitorType { get; set; } = ZoneMonitorType.INACTIVE;
public long MonitorZoneId { get; set; }
public List<Alarm> Alarms { get; } = new List<Alarm>();
public override string ToString()
{
return String.Format("{0} {1}", MMSI, MonitorType);
}
}
#endregion
}

View File

@ -0,0 +1,112 @@
// Copyright (c) 2023 - schick Informatik
// bsmd.AIS2Service [MonitorGroup.cs]: Daniel Schick
// Description: Group container for zones
//
using log4net;
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
namespace bsmd.AIS2Service
{
#region class MonitorGroup
public class MonitorGroup : DBEntity
{
private static readonly ILog _log = LogManager.GetLogger(typeof(MonitorGroup));
#region fields
private string _name;
private readonly List<MonitorZone> _zones = new List<MonitorZone>();
#endregion
#region Construction
public MonitorGroup(long id, string name) : base(id)
{
_name = name;
}
#endregion
#region Properties
public List<MonitorZone> Zones { get { return _zones; } }
public string Name { get { return _name; } set { _name = value; } }
#endregion
#region public static methods
/// <summary>
/// Da Basti nun eine Datei mit allen Elementen exportiert gibt es eine einzelne Funktion, die alles
/// importiert
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
public static List<MonitorGroup> LoadGroups(string filename)
{
List<MonitorGroup> groups = new List<MonitorGroup>();
try
{
if (File.Exists(filename))
{
XDocument kml = XDocument.Load(filename);
XNamespace ns = "http://www.opengis.net/kml/2.2";
foreach (XElement rootFolderNode in kml.Root.Element(ns + "Document").Element(ns + "Folder").Elements(ns + "Folder"))
{
MonitorGroup mg = new MonitorGroup(-1, rootFolderNode.Element(ns + "name").Value);
int sequence = 1;
foreach (XElement placemark in rootFolderNode.Elements(ns + "Placemark"))
{
MonitorZone mz = new MonitorZone(-1, placemark.Element(ns + "name").Value);
mz.Active = true;
mz.Sequence = sequence;
// now add all vertices
string[] vertices = placemark.Element(ns + "Polygon").Element(ns + "outerBoundaryIs").Element(ns + "LinearRing").Element(ns + "coordinates").Value.Split(' ');
for (int i = 0; i < vertices.Length - 1; i++)
{
string[] pointElems = vertices[i].Trim().Split(',');
if (pointElems.Length != 3) continue;
GeoPoint gp = new GeoPoint(-1);
gp.Lon = Double.Parse(pointElems[0], System.Globalization.NumberFormatInfo.InvariantInfo);
gp.Lat = Double.Parse(pointElems[1], System.Globalization.NumberFormatInfo.InvariantInfo);
mz.Vertices.Add(gp);
}
mg.Zones.Add(mz);
sequence++;
}
groups.Add(mg);
}
}
}
catch (Exception ex)
{
_log.Error(ex.ToString());
}
return groups;
}
#endregion
#region public methods
public override string ToString()
{
return this.Name;
}
#endregion
}
#endregion
}

View File

@ -0,0 +1,121 @@
// Copyright (c) 2022 - schick Informatik
// bsmd.AIS2Service [MonitorZone.cs]: %UserDisplayName%
// Description: Represents a geographical area that should be checked against
// ship positions
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace bsmd.AIS2Service
{
#region class MonitorZone
public class MonitorZone : DBEntity, IComparable<MonitorZone>
{
#region fields
private readonly List<GeoPoint> _vertices = new List<GeoPoint>();
private readonly List<MonitorAssignment> _assignments = new List<MonitorAssignment>();
private readonly string _name;
#endregion
#region Construction
public MonitorZone(long id, string name) : base (id)
{
_name = name;
}
#endregion
#region Properties
public string Name { get { return _name; } }
public List<GeoPoint> Vertices { get { return _vertices; } }
public List<MonitorAssignment> Assignments { get { return _assignments; } }
public bool Active { get; set; }
public int Sequence { get; set; }
public long MonitorGroupId { get; set; }
#endregion
#region public static methods
public static MonitorZone ImportFromKML(string filename)
{
MonitorZone result = null;
if (File.Exists(filename))
{
XDocument kml = XDocument.Load(filename);
XNamespace ns = "http://www.opengis.net/kml/2.2";
string name = kml.Root.Element(ns + "Document").Element(ns + "name").Value;
if (name.EndsWith(".kml")) name = name.Substring(0, name.Length - 4);
result = new MonitorZone(-1, name);
// now find all vertices
string[] vertices = kml.Root.Element(ns + "Document").Element(ns + "Placemark").Element(ns + "Polygon").Element(ns + "outerBoundaryIs").Element(ns + "LinearRing").Element(ns + "coordinates").Value.Split(' ');
for (int i = 0; i < vertices.Length - 1; i++)
{
string[] pointElems = vertices[i].Trim().Split(',');
if (pointElems.Length != 3) continue;
GeoPoint gp = new GeoPoint(-1);
gp.Lon = Double.Parse(pointElems[0], System.Globalization.NumberFormatInfo.InvariantInfo);
gp.Lat = Double.Parse(pointElems[1], System.Globalization.NumberFormatInfo.InvariantInfo);
result.Vertices.Add(gp);
}
}
return result;
}
#endregion
#region public methods
public bool IsPointInPolygon4(GeoPoint testPoint)
{
bool result = false;
int j = _vertices.Count() - 1;
for (int i = 0; i < _vertices.Count(); i++)
{
if (_vertices[i].Lat < testPoint.Lat && _vertices[j].Lat >= testPoint.Lat || _vertices[j].Lat < testPoint.Lat && _vertices[i].Lat >= testPoint.Lat)
{
if (_vertices[i].Lon + (testPoint.Lat - _vertices[i].Lat) / (_vertices[j].Lat - _vertices[i].Lat) * (_vertices[j].Lon - _vertices[i].Lon) < testPoint.Lon)
{
result = !result;
}
}
j = i;
}
return result;
}
public int CompareTo(MonitorZone other)
{
return this.Sequence.CompareTo(other.Sequence);
}
public override string ToString()
{
return String.Format("{0} (Seq.:{1} #Vert.:{2}", this.Name, this.Sequence, this.Vertices.Count);
}
#endregion
}
#endregion
}

View File

@ -0,0 +1,175 @@
/* startup, load groups from database */
var groupIds = [];
var groupZones = [];
async function loadAlarms(groupId)
{
let data;
try {
const res = await fetch('http://localhost:9050/api/slr?id=' + groupId);
data = await res.json();
updateData(data, groupId);
}
catch(error) {
console.error(error);
}
}
function updateData(data, groupId) {
var table = document.getElementById('table-' + groupId);
for (var i = 0; i < data.length; i++) {
let row_id = "row_" + groupId + "_" + data[i].MMSI;
row = document.getElementById(row_id);
if(row == null) { // not found, create new row
row = document.createElement('tr');
row.setAttribute("id", row_id);
// add leading cells
var td1 = document.createElement('td');
//td1.innerHTML = data[i].Name;
row.appendChild(td1);
var td2 = document.createElement('td');
row.appendChild(td2);
var td3 = document.createElement('td');
// td3.innerHTML = data[i].IMO;
row.appendChild(td3);
// create dummy cells for each zone
for(var j = 0; j < groupZones[groupId].length; j++) {
var td = document.createElement('td');
td.id = "cell_" + groupId + "_" + groupZones[groupId][j] + "_" + data[i].MMSI;
row.appendChild(td);
}
// add trailing cells
var td4 = document.createElement('td');
//td4.innerHTML = data[i].NavStatus;
row.appendChild(td4);
var td5 = document.createElement('td');
//td5.innerHTML = data[i].MMSI;
row.appendChild(td5);
var td6 = document.createElement('td');
//td6.innerHTML = data[i].Destination;
row.appendChild(td6);
table.childNodes[1].appendChild(row); // append row to tbody subelement
}
row.setAttribute("isActive", "true");
const colCount = row.childNodes.length;
row.childNodes[0].innerHTML = data[i].Name;
if(data[i].VoyageDirection == 0) row.childNodes[1].innerHTML = '<img src="img/bullet_square_yellow.png" />';
if(data[i].VoyageDirection == 1) row.childNodes[1].innerHTML = '<img src="img/arrow_down_red.png" />';
if(data[i].VoyageDirection == 2) row.childNodes[1].innerHTML = '<img src="img/arrow_up_green.png" />';
if(data[i].VoyageDirection == 3) row.childNodes[1].innerHTML = '<img src="img/clock.png" />';
row.childNodes[2].innerHTML = data[i].IMO;
// find alarm cell and set value
let cellId = "cell_" + groupId + "_" + data[i].MonitorZoneId + "_" + data[i].MMSI;
var cell = document.getElementById(cellId);
if(cell != null) {
const timestamp = Date.parse(data[i].Timestamp_First);
const d = new Date(timestamp);
cell.innerHTML = ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2) + " " + d.getDate() + "." + (d.getMonth() + 1) + "." + ("" + d.getFullYear()).slice(-2);
}
row.childNodes[colCount - 3].innerHTML = data[i].NavStatus;
row.childNodes[colCount - 2].innerHTML = data[i].MMSI;
row.childNodes[colCount - 1].innerHTML = data[i].Destination;
}
// array in-place removal taking place
var i = table.childNodes[1].rows.length;
while (i--) {
row = table.childNodes[1].rows[i];
if(row["isActive"] !== "true") {
table.childNodes[1].rows.remove(row);
}
}
}
function update() {
for(var i = 0; i < groupIds.length; i++)
{
loadAlarms(groupIds[i]);
}
}
function createAreas()
{
fetch('http://localhost:9050/api/zones')
.then(function (response) {
return response.json();
})
.then(function (data) {
createAreasFromData(data);
})
.catch(function (err) {
console.log('error: ' + err);
});
function createAreasFromData(data)
{
var root_div = document.getElementById('root-div');
for (var i = 0; i < data.length; i++) {
groupIds.push(data[i].Id);
groupZones[data[i].Id] = [];
var aDiv = document.createElement('div');
aDiv.className = '';
aDiv.id = 'group-' + data[i].Id;
aDiv.innerHTML = data[i].Name;
root_div.appendChild(aDiv);
// create table header with zones
var aTable = document.createElement('table');
aTable.id = 'table-' + data[i].Id;
aTable.className = "styled-table";
var thead = document.createElement('thead');
aTable.appendChild(thead);
var tr = document.createElement('tr');
thead.appendChild(tr);
var th1 = document.createElement('th');
th1.innerHTML='Name';
tr.appendChild(th1);
var th2 = document.createElement('th');
th2.innerHTML='E/A';
tr.appendChild(th2);
var th3 = document.createElement('th');
th3.innerHTML='IMO';
tr.appendChild(th3);
for(var j = 0; j < data[i].Zones.length; j++) {
groupZones[data[i].Id].push(data[i].Zones[j].Id);
var aTH = document.createElement('th');
aTH.id = 'zone-' + data[i].Zones[j].Id;
aTH.innerHTML = data[i].Zones[j].Name;
tr.appendChild(aTH);
}
var th4 = document.createElement('th');
th4.innerHTML='Nav. status';
tr.appendChild(th4);
var th5 = document.createElement('th');
th5.innerHTML='MMSI';
tr.appendChild(th5);
var th6 = document.createElement('th');
th6.innerHTML='Destination';
tr.appendChild(th6);
tr.setAttribute("isActive", "true"); // set marker so it won't get deleted later
aTable.appendChild(document.createElement('tbody'));
aDiv.appendChild(aTable);
aDiv.appendChild(document.createElement('hr'));
}
setInterval(function () { update(); }, 15000);
}
}