diff --git a/AIS/bsmd.AIS2Service/AISManager.cs b/AIS/bsmd.AIS2Service/AISManager.cs index ce040ef8..d61a9d18 100644 --- a/AIS/bsmd.AIS2Service/AISManager.cs +++ b/AIS/bsmd.AIS2Service/AISManager.cs @@ -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,12 +39,12 @@ 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 targets = await sqliteStorage.LoadTargets(); + Dictionary targets = await _sqliteStorage.LoadTargets(); foreach(int key in targets.Keys) { _sitRepList.TryAdd(key, targets[key]); @@ -58,7 +59,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 +88,9 @@ namespace bsmd.AIS2Service #region Properties - public static ConcurrentDictionary SitRep - { - get { return _sitRepList; } - } + public static ConcurrentDictionary SitRep { get { return _sitRepList; } } + + public static AIS_SQLiteStorage SQLiteStorage { get { return _sqliteStorage; } } #endregion diff --git a/AIS/bsmd.AIS2Service/AIS_SQLiteStorage.cs b/AIS/bsmd.AIS2Service/AIS_SQLiteStorage.cs index 2c9fad79..4065abbe 100644 --- a/AIS/bsmd.AIS2Service/AIS_SQLiteStorage.cs +++ b/AIS/bsmd.AIS2Service/AIS_SQLiteStorage.cs @@ -15,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. /// - 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 _inputQueue; private readonly Dictionary _storageTargets = new Dictionary(); @@ -53,7 +54,7 @@ namespace bsmd.AIS2Service /// monitor zone loader func for the zone alarm watchdog (doesn't need groups or such) /// /// - public List LoadMonitorZones() + public List LoadMonitorZones(bool loadInnerCollections = true) { List monitorZones = new List(); if ((_connection == null) || (_connection.State != ConnectionState.Open)) return monitorZones; // can't load but return nothing in a friendly way @@ -78,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; } @@ -255,6 +259,8 @@ namespace bsmd.AIS2Service groups.Add(mGroup); } } + reader.Close(); + laCmd.Dispose(); return groups; } @@ -381,6 +387,42 @@ namespace bsmd.AIS2Service #endregion + #region ShipLocationReport + + /// + /// 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) + /// + public List GetShipLocationReports(long groupId) + { + List slrs = new List(); + + 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}"; + + 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 @@ -628,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 + } } diff --git a/AIS/bsmd.AIS2Service/SLRController.cs b/AIS/bsmd.AIS2Service/SLRController.cs new file mode 100644 index 00000000..7c26b1ab --- /dev/null +++ b/AIS/bsmd.AIS2Service/SLRController.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Web.Http; + +namespace bsmd.AIS2Service +{ + public class SLRController : ApiController + { + [HttpGet] + public IEnumerable Get([FromUri] int? id) + { + if (!id.HasValue) return null; + List result = AISManager.SQLiteStorage.GetShipLocationReports(id.Value); + + foreach(ShipLocationReport report in result) { + 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; + } + } + + return result; + } + } +} diff --git a/AIS/bsmd.AIS2Service/ShipLocationReport.cs b/AIS/bsmd.AIS2Service/ShipLocationReport.cs new file mode 100644 index 00000000..5e947577 --- /dev/null +++ b/AIS/bsmd.AIS2Service/ShipLocationReport.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace bsmd.AIS2Service +{ + /// + /// Helper class to report a ship position / entry into a zone (aka alarm connected to zone) + /// + 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; } + + } +} diff --git a/AIS/bsmd.AIS2Service/StartupWebAPI.cs b/AIS/bsmd.AIS2Service/StartupWebAPI.cs index b2d1ac0d..216e0f16 100644 --- a/AIS/bsmd.AIS2Service/StartupWebAPI.cs +++ b/AIS/bsmd.AIS2Service/StartupWebAPI.cs @@ -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); diff --git a/AIS/bsmd.AIS2Service/ZonesController.cs b/AIS/bsmd.AIS2Service/ZonesController.cs new file mode 100644 index 00000000..5aa84476 --- /dev/null +++ b/AIS/bsmd.AIS2Service/ZonesController.cs @@ -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 Get() // Get([FromUri] int? id) + { + List allZones = AISManager.SQLiteStorage.LoadMonitorZones(false); + List 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 groups = AISManager.SQLiteStorage.LoadGroups(); + return groups; + } + else + { + return null; + } + */ + } + } +} diff --git a/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj b/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj index ee70ec22..2da0d300 100644 --- a/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj +++ b/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj @@ -127,9 +127,12 @@ Settings.settings + + + diff --git a/AIS/bsmd.AIS2Service/lauf.html b/AIS/bsmd.AIS2Service/lauf.html new file mode 100644 index 00000000..242145c8 --- /dev/null +++ b/AIS/bsmd.AIS2Service/lauf.html @@ -0,0 +1,25 @@ + + + + + + + + + Zonen Übersicht + + + + + + +
+ + + \ No newline at end of file diff --git a/AIS/bsmd.AIS2Service/style.css b/AIS/bsmd.AIS2Service/style.css index 72fe0f90..6aa16f65 100644 --- a/AIS/bsmd.AIS2Service/style.css +++ b/AIS/bsmd.AIS2Service/style.css @@ -125,4 +125,12 @@ h2{ display: block; text-align: center; } -} \ No newline at end of file +} + +#bottomcenter { + position: fixed; + left: 50%; + bottom: 20px; + transform: translate(-50%, -50%); + margin: 0 auto; + } \ No newline at end of file diff --git a/AIS/bsmd.AIS2Service/zonen.js b/AIS/bsmd.AIS2Service/zonen.js new file mode 100644 index 00000000..2320feb3 --- /dev/null +++ b/AIS/bsmd.AIS2Service/zonen.js @@ -0,0 +1,74 @@ + + + +/* startup, load groups from database */ + +document.addEventListener('DOMContentLoaded', function () { + let table = new DataTable('#aisTable', { + "ajax" : { + "url" : "http://192.168.2.25:9050/api/ais", + "dataSrc" : "", + "type" : "GET" + }, + columns: [ + { data: 'MMSI' }, + { data: 'Name' }, + { data: 'LastUpdate' }, + { data: 'Latitude' }, + { data: 'Longitude' }, + { data: 'IMO' }, + { data: 'IsClassB' } + ] + } ); + + setInterval( function () { + table.ajax.reload( null, false ); // user paging is not reset on reload + }, 30000 ); + +} ); + + +function update() { + fetch('http://192.168.2.25:9050/api/ais') + .then(function (response) { + return response.json(); + }) + .then(function (data) { + updateData(data); + }) + .catch(function (err) { + console.log('error: ' + err); + }); + function updateData(data) { + var table = document.getElementById('aisTable'); + for (var i = 0; i < data.length; i++) { + let row_id = "row_" + 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); + row.innerHTML = `` + data[i].MMSI + + `` + + `` + + `` + + `` + + `` + + `` + data[i].IsClassB + ``; + table.appendChild(row); + } + + // update existing row + var td = document.getElementById("name_" + data[i].MMSI); + td.innerHTML = data[i].Name; + td = document.getElementById("timestamp_" + data[i].MMSI); + td.innerHTML = data[i].LastUpdate; + td = document.getElementById("lat_" + data[i].MMSI); + td.innerHTML = data[i].Latitude; + td = document.getElementById("lon_" + data[i].MMSI); + td.innerHTML = data[i].Longitude; + td = document.getElementById("imo_" + data[i].MMSI); + td.innerHTML = data[i].IMO; + } + } +} \ No newline at end of file