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 2) der Zeitpunkt des letzten Reports innerhalb der Zone
3) keine Referenz sondern Kopie der Position/Timestamp? 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?) - 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 ## Stand Dez 22

View File

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

View File

@ -21,8 +21,8 @@ namespace bsmd.AIS2Service
#region Fields #region Fields
ConcurrentDictionary<int, AIS_Target> _sitRepDict; private readonly ConcurrentDictionary<int, AIS_Target> _sitRepDict;
AIS_SQLiteStorage _storage; private readonly AIS_SQLiteStorage _storage;
private Thread _thread; private Thread _thread;
private bool _stopFlag = false; private bool _stopFlag = false;
private static readonly ILog _log = LogManager.GetLogger(typeof(AISZoneMonitor)); private static readonly ILog _log = LogManager.GetLogger(typeof(AISZoneMonitor));
@ -45,18 +45,24 @@ namespace bsmd.AIS2Service
{ {
// load zones from storage // load zones from storage
List<MonitorZone> allZones = _storage.LoadMonitorZones(); 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) foreach(MonitorZone zone in allZones)
{ {
// load up Zone list for all assignment mmsi's (to check) // load up Zone list for all assignment mmsi's (to check)
foreach(MonitorAssignment ma in zone.Assignments) foreach(MonitorAssignment ma in zone.Assignments)
{ {
if(!testZones.ContainsKey(ma.MMSI)) testZones[ma.MMSI] = new List<MonitorZone>(); if(!testDict.ContainsKey(ma.MMSI)) testDict[ma.MMSI] = new List<AlarmAssignmentZone>();
testZones[ma.MMSI].Add(zone); ma.Alarms.AddRange(_storage.LoadAlarms(ma));
AlarmAssignmentZone aazone = new AlarmAssignmentZone();
aazone.Zone = zone;
aazone.Assignment = ma;
testDict[ma.MMSI].Add(aazone);
} }
} }
// loop // loop
while(!_stopFlag) while(!_stopFlag)
{ {
@ -64,13 +70,41 @@ namespace bsmd.AIS2Service
foreach(int mmsi in _sitRepDict.Keys) foreach(int mmsi in _sitRepDict.Keys)
{ {
if(testZones.ContainsKey(mmsi)) if(testDict.ContainsKey(mmsi))
{ {
if (!_sitRepDict.ContainsKey(mmsi)) continue;
AIS_Target target = _sitRepDict[mmsi]; AIS_Target target = _sitRepDict[mmsi];
foreach(MonitorZone zone in testZones[mmsi]) { foreach(AlarmAssignmentZone aazone in testDict[mmsi]) {
if(zone.IsPointInPolygon4(target.Position)) 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 #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.Collections.Generic;
using System.Data; using System.Data;
using System.Threading.Tasks;
using System.Threading;
using System.Data.SQLite; using System.Data.SQLite;
using log4net; using System.Threading;
using System.Collections.Concurrent; using System.Threading.Tasks;
namespace bsmd.AIS2Service 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. /// 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. /// Attention: Alarm zones / alarms are also stored here. This might or might not be such a great idea.
/// </summary> /// </summary>
public class AIS_SQLiteStorage : IAISThread public class AIS_SQLiteStorage : IAISThread, IDisposable
{ {
#region Fields #region Fields
private readonly SQLiteConnection _connection; private readonly SQLiteConnection _connection;
private Thread _thread; private Thread _thread;
private bool _stopFlag = false; private bool _stopFlag = false;
private bool disposedValue;
private static readonly ILog _log = LogManager.GetLogger(typeof(AIS_SQLiteStorage)); private static readonly ILog _log = LogManager.GetLogger(typeof(AIS_SQLiteStorage));
private readonly ConcurrentQueue<AIS_Target> _inputQueue; private readonly ConcurrentQueue<AIS_Target> _inputQueue;
private readonly Dictionary<int, AIS_Target> _storageTargets = new Dictionary<int, AIS_Target>(); 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) /// monitor zone loader func for the zone alarm watchdog (doesn't need groups or such)
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public List<MonitorZone> LoadMonitorZones() public List<MonitorZone> LoadMonitorZones(bool loadInnerCollections = true)
{ {
List<MonitorZone> monitorZones = new List<MonitorZone>(); 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 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(); reader.Close();
lzCmd.Dispose(); lzCmd.Dispose();
// load vertices for each zone if (loadInnerCollections)
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)
{ {
lvCmd.Parameters.Clear(); // load vertices for each zone
lvCmd.Parameters.AddWithValue("@ID", mz.Id); string loadVertexString = "SELECT Id, latitude, longitude FROM zone_vertex WHERE monitor_zone_id = @ID";
reader = lvCmd.ExecuteReader(); SQLiteCommand lvCmd = new SQLiteCommand(loadVertexString, _connection);
if(reader.HasRows) 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); while (reader.Read())
GeoPoint gp = new GeoPoint(id); {
gp.Lat = reader.GetDouble(1); int id = reader.GetInt32(0);
gp.Lon = reader.GetDouble(2); GeoPoint gp = new GeoPoint(id);
mz.Vertices.Add(gp); 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 // load mmsi / zone assignments for each zone
string loadAssignmentsString = "SELECT id, mmsi, type FROM zone_assignment WHERE monitor_zone_id = @ID"; string loadAssignmentsString = "SELECT id, mmsi, type FROM zone_assignment WHERE monitor_zone_id = @ID";
SQLiteCommand laCmd = new SQLiteCommand(loadAssignmentsString, _connection); SQLiteCommand laCmd = new SQLiteCommand(loadAssignmentsString, _connection);
foreach (MonitorZone mz in monitorZones) foreach (MonitorZone mz in monitorZones)
{
laCmd.Parameters.Clear();
laCmd.Parameters.AddWithValue("@ID", mz.Id);
reader = laCmd.ExecuteReader();
if (reader.HasRows)
{ {
while (reader.Read()) laCmd.Parameters.Clear();
laCmd.Parameters.AddWithValue("@ID", mz.Id);
reader = laCmd.ExecuteReader();
if (reader.HasRows)
{ {
int id = reader.GetInt32(0); while (reader.Read())
MonitorAssignment ma = new MonitorAssignment(id); {
ma.MMSI = reader.GetInt32(1); int id = reader.GetInt32(0);
ma.MonitorType = (MonitorAssignment.ZoneMonitorType)reader.GetInt32(2); MonitorAssignment ma = new MonitorAssignment(id);
mz.Assignments.Add(ma); 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; return monitorZones;
} }
@ -180,7 +183,8 @@ namespace bsmd.AIS2Service
int id = reader.GetInt32(0); int id = reader.GetInt32(0);
Alarm alarm = new Alarm(id, assignment); Alarm alarm = new Alarm(id, assignment);
alarm.Timestamp_First = reader.GetDateTime(1); 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); alarm.ZoneMonitorType = (MonitorAssignment.ZoneMonitorType)reader.GetInt32(3);
if(!reader.IsDBNull(4)) if(!reader.IsDBNull(4))
alarm.Acknowledged = reader.GetDateTime(4); alarm.Acknowledged = reader.GetDateTime(4);
@ -207,8 +211,10 @@ namespace bsmd.AIS2Service
if (alarm.Id <= 0) if (alarm.Id <= 0)
{ {
// insert // 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); 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(); int insertedRows = cmd.ExecuteNonQuery();
cmd.Dispose(); cmd.Dispose();
alarm.Id = GetLastInsertId(); alarm.Id = GetLastInsertId();
@ -217,8 +223,14 @@ namespace bsmd.AIS2Service
else else
{ {
// update // 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); 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(); int updatedRows = cmd.ExecuteNonQuery();
cmd.Dispose(); cmd.Dispose();
return (updatedRows == 1); return (updatedRows == 1);
@ -247,6 +259,8 @@ namespace bsmd.AIS2Service
groups.Add(mGroup); groups.Add(mGroup);
} }
} }
reader.Close();
laCmd.Dispose();
return groups; return groups;
} }
@ -373,6 +387,42 @@ namespace bsmd.AIS2Service
#endregion #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 #endregion
#region private methods #region private methods
@ -620,5 +670,32 @@ namespace bsmd.AIS2Service
#endregion #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> <value>60</value>
</setting> </setting>
<setting name="SQLiteDBConnectionString" serializeAs="String"> <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>
<setting name="PosReportDBCleanupDays" serializeAs="String"> <setting name="PosReportDBCleanupDays" serializeAs="String">
<value>7</value> <value>7</value>
@ -67,6 +67,12 @@
<setting name="MonitorTargetSaturationSecs" serializeAs="String"> <setting name="MonitorTargetSaturationSecs" serializeAs="String">
<value>120</value> <value>120</value>
</setting> </setting>
<setting name="MinAlarmIntervalMins" serializeAs="String">
<value>60</value>
</setting>
<setting name="AutoAlarmExpiryHours" serializeAs="String">
<value>24</value>
</setting>
</bsmd.AIS2Service.Properties.Settings> </bsmd.AIS2Service.Properties.Settings>
</applicationSettings> </applicationSettings>
<runtime> <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.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [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 { public string SQLiteDBConnectionString {
get { get {
return ((string)(this["SQLiteDBConnectionString"])); return ((string)(this["SQLiteDBConnectionString"]));
@ -121,5 +121,23 @@ namespace bsmd.AIS2Service.Properties {
return ((int)(this["MonitorTargetSaturationSecs"])); 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> <Value Profile="(Default)">60</Value>
</Setting> </Setting>
<Setting Name="SQLiteDBConnectionString" Type="System.String" Scope="Application"> <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>
<Setting Name="PosReportDBCleanupDays" Type="System.Int32" Scope="Application"> <Setting Name="PosReportDBCleanupDays" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">7</Value> <Value Profile="(Default)">7</Value>
@ -35,5 +35,11 @@
<Setting Name="MonitorTargetSaturationSecs" Type="System.Int32" Scope="Application"> <Setting Name="MonitorTargetSaturationSecs" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">120</Value> <Value Profile="(Default)">120</Value>
</Setting> </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> </Settings>
</SettingsFile> </SettingsFile>

View File

@ -94,25 +94,30 @@
<DependentUpon>AIS2_Service.cs</DependentUpon> <DependentUpon>AIS2_Service.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="AISClass.cs" /> <Compile Include="AISClass.cs" />
<Compile Include="AISController.cs" /> <Compile Include="webservice\AISController.cs" />
<Compile Include="AISDecoder.cs" /> <Compile Include="decoding\AISDecoder.cs" />
<Compile Include="AISManager.cs" /> <Compile Include="AISManager.cs" />
<Compile Include="AISZoneMonitor.cs" /> <Compile Include="AISZoneMonitor.cs" />
<Compile Include="AIS_BaseStation.cs" /> <Compile Include="decoding\AIS_BaseStation.cs" />
<Compile Include="AIS_BaseStationReport.cs" /> <Compile Include="decoding\AIS_BaseStationReport.cs" />
<Compile Include="AIS_ClassB.cs" /> <Compile Include="decoding\AIS_ClassB.cs" />
<Compile Include="AIS_ClassBExt.cs" /> <Compile Include="decoding\AIS_ClassBExt.cs" />
<Compile Include="AIS_ClassBStatic.cs" /> <Compile Include="decoding\AIS_ClassBStatic.cs" />
<Compile Include="AIS_PosReport.cs" /> <Compile Include="decoding\AIS_PosReport.cs" />
<Compile Include="AIS_SQLiteStorage.cs" /> <Compile Include="AIS_SQLiteStorage.cs" />
<Compile Include="AIS_StaticData.cs" /> <Compile Include="decoding\AIS_StaticData.cs" />
<Compile Include="AIS_Target.cs" /> <Compile Include="AIS_Target.cs" />
<Compile Include="IAISThread.cs" /> <Compile Include="IAISThread.cs" />
<Compile Include="Lookup.cs" /> <Compile Include="Lookup.cs" />
<Compile Include="MonitorZone.cs" /> <Compile Include="zone_alarm\Alarm.cs" />
<Compile Include="NMEA.cs" /> <Compile Include="zone_alarm\DBEntity.cs" />
<Compile Include="NMEA_AIS_Sentence.cs" /> <Compile Include="zone_alarm\GeoPoint.cs" />
<Compile Include="NMEA_PNMLS_Sentence.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="Program.cs" />
<Compile Include="ProjectInstaller.cs"> <Compile Include="ProjectInstaller.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
@ -127,9 +132,12 @@
<DependentUpon>Settings.settings</DependentUpon> <DependentUpon>Settings.settings</DependentUpon>
</Compile> </Compile>
<Compile Include="SerialTCPReader.cs" /> <Compile Include="SerialTCPReader.cs" />
<Compile Include="webservice\ShipLocationReport.cs" />
<Compile Include="SitRep.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="Util.cs" />
<Compile Include="webservice\ZonesController.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\SQL\ais_initial.db"> <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{ body{
font-family: Helvetica; font-family: Helvetica;
-webkit-font-smoothing: antialiased; -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 */ .styled-table {
.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;
border-collapse: collapse; border-collapse: collapse;
width: 100%; margin: 25px 0;
max-width: 100%; font-size: 0.9em;
white-space: nowrap; font-family: sans-serif;
background-color: white; min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
} }
.fl-table td, .fl-table th { .styled-table thead tr {
text-align: center; background-color: #009879;
padding: 8px;
}
.fl-table td {
border-right: 1px solid #f8f8f8;
font-size: 12px;
}
.fl-table thead th {
color: #ffffff; 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) { #bottomcenter {
color: #ffffff; position: fixed;
background: #324960; left: 50%;
} bottom: 20px;
transform: translate(-50%, -50%);
.fl-table tr:nth-child(even) { margin: 0 auto;
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;
}
}

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}", routeTemplate: "api/{Controller}",
defaults: new { id = RouteParameter.Optional, Controller = "AIS"} 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); config.EnableCors(cors);
appBuilder.UseWebApi(config); 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);
}
}