421 lines
20 KiB
C#
421 lines
20 KiB
C#
using System;
|
|
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.Runtime.Remoting.Messaging;
|
|
|
|
namespace bsmd.AIS2Service
|
|
{
|
|
/// <summary>
|
|
/// This class handles the (short-time) storage of AIS targets with a limited
|
|
/// 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>
|
|
internal class AIS_SQLiteStorage : IAISThread
|
|
{
|
|
#region Fields
|
|
|
|
private readonly SQLiteConnection _connection;
|
|
private Thread _thread;
|
|
private bool _stopFlag = false;
|
|
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>();
|
|
|
|
#endregion
|
|
|
|
#region Construction
|
|
|
|
public AIS_SQLiteStorage (ConcurrentQueue<AIS_Target> inputQueue)
|
|
{
|
|
_inputQueue = inputQueue;
|
|
_connection = new SQLiteConnection(Properties.Settings.Default.SQLiteDBConnectionString);
|
|
_connection.Open();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region public methods
|
|
|
|
/// <summary>
|
|
/// monitor zone loader func for the zone alarm watchdog (doesn't need groups or such)
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public List<MonitorZone> LoadMonitorZones()
|
|
{
|
|
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
|
|
|
|
// load zones
|
|
string loadZoneString = "SELECT id, name, active FROM monitor_zone";
|
|
SQLiteCommand lzCmd = new SQLiteCommand(loadZoneString, _connection);
|
|
SQLiteDataReader reader = lzCmd.ExecuteReader();
|
|
if(reader.HasRows)
|
|
{
|
|
while(reader.Read())
|
|
{
|
|
int id = reader.GetInt32(0);
|
|
string name = reader.GetString(1);
|
|
MonitorZone mz = new MonitorZone(id, name);
|
|
mz.Active = reader.GetInt32(2) == 1;
|
|
monitorZones.Add(mz);
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
lvCmd.Parameters.Clear();
|
|
lvCmd.Parameters.AddWithValue("@ID", mz.Id);
|
|
SQLiteDataReader lvReader = lvCmd.ExecuteReader();
|
|
if(reader.HasRows)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
lvReader.Close();
|
|
}
|
|
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)
|
|
{
|
|
lvCmd.Parameters.Clear();
|
|
lvCmd.Parameters.AddWithValue("@ID", mz.Id);
|
|
SQLiteDataReader lvReader = laCmd.ExecuteReader();
|
|
if (reader.HasRows)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
lvReader.Close();
|
|
}
|
|
laCmd.Dispose();
|
|
|
|
return monitorZones;
|
|
}
|
|
|
|
#region Alarm
|
|
|
|
public List<Alarm> LoadAlarms(MonitorAssignment assignment)
|
|
{
|
|
List<Alarm> result = new List<Alarm>();
|
|
string loadAlarmString = "SELECT id, timestamp, type, acknowledged FROM alarm WHERE zone_assignment_id = @ID";
|
|
SQLiteCommand laCmd = new SQLiteCommand(loadAlarmString, _connection);
|
|
laCmd.Parameters.AddWithValue("@ID", assignment.Id);
|
|
SQLiteDataReader reader = laCmd.ExecuteReader();
|
|
if (reader.HasRows)
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
int id = reader.GetInt32(0);
|
|
Alarm alarm = new Alarm(id, assignment);
|
|
alarm.Timestamp = reader.GetDateTime(1);
|
|
alarm.ZoneMonitorType = (MonitorAssignment.ZoneMonitorType)reader.GetInt32(2);
|
|
if(!reader.IsDBNull(3))
|
|
alarm.Acknowledged = reader.GetDateTime(3);
|
|
result.Add(alarm);
|
|
}
|
|
reader.Close();
|
|
}
|
|
laCmd.Dispose();
|
|
return result;
|
|
}
|
|
|
|
public bool Delete(Alarm alarm)
|
|
{
|
|
if (alarm == null) return false;
|
|
string deleteAlarmString = $"DELETE FROM alarm WHERE id = {alarm.Id}";
|
|
SQLiteCommand cmd = new SQLiteCommand(deleteAlarmString, _connection);
|
|
int affectedRows = cmd.ExecuteNonQuery();
|
|
return (affectedRows == 1);
|
|
}
|
|
|
|
public bool Save(Alarm alarm)
|
|
{
|
|
if (alarm == null) return false;
|
|
if (alarm.Id <= 0)
|
|
{
|
|
// insert
|
|
string saveAlarmString = $"INSERT INTO alarm (zone_assignment_id, timestamp, type, acknowledged) VALUES ({alarm.Assignment.Id}, {alarm.Timestamp}, {alarm.ZoneMonitorType}, {alarm.Acknowledged})";
|
|
SQLiteCommand cmd = new SQLiteCommand(saveAlarmString, _connection);
|
|
int insertedRows = cmd.ExecuteNonQuery();
|
|
return (insertedRows == 1);
|
|
}
|
|
else
|
|
{
|
|
// update
|
|
// TODO
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region private methods
|
|
|
|
private void ReadQueue()
|
|
{
|
|
try
|
|
{
|
|
|
|
// we create "common" commands and just reload the parameters after each shot :P
|
|
string updateStaticString = "UPDATE staticdata SET destination = ?, eta = ?, version = ?, imo = ?, callsign = ?, name = ?, shiptype = ?, typeofdevice = ?, maxpresetstaticdraught = ?, dte = ?, classb = ?, breadth = ?, length = ? WHERE mmsi = ?";
|
|
SQLiteCommand updateStaticCmd = new SQLiteCommand(updateStaticString, _connection);
|
|
updateStaticCmd.Parameters.Add("DESTINATION", DbType.String);
|
|
updateStaticCmd.Parameters.Add("ETA", DbType.DateTime);
|
|
updateStaticCmd.Parameters.Add("VERSION", DbType.Int32);
|
|
updateStaticCmd.Parameters.Add("IMO", DbType.Int32);
|
|
updateStaticCmd.Parameters.Add("CALLSIGN", DbType.String);
|
|
updateStaticCmd.Parameters.Add("NAME", DbType.String);
|
|
updateStaticCmd.Parameters.Add("SHIPTYPE", DbType.Int32);
|
|
updateStaticCmd.Parameters.Add("TYPEOFDEVICE", DbType.Int32);
|
|
updateStaticCmd.Parameters.Add("DRAUGHT", DbType.Int32);
|
|
updateStaticCmd.Parameters.Add("DTE", DbType.Boolean);
|
|
updateStaticCmd.Parameters.Add("CLASSB", DbType.Boolean);
|
|
updateStaticCmd.Parameters.Add("BREADTH", DbType.Int32);
|
|
updateStaticCmd.Parameters.Add("LENGTH", DbType.Int32);
|
|
updateStaticCmd.Parameters.Add("MMSI", DbType.Int32);
|
|
|
|
string insertStaticString = "INSERT INTO staticdata (mmsi, version, imo, callsign, name, shiptype, typeofdevice, maxpresetstaticdraught, destination, dte, classb, breadth, length, eta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
SQLiteCommand insertStaticCmd = new SQLiteCommand(insertStaticString, _connection);
|
|
insertStaticCmd.Parameters.Add("MMSI", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("VERSION", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("IMO", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("CALLSIGN", DbType.String);
|
|
insertStaticCmd.Parameters.Add("NAME", DbType.String);
|
|
insertStaticCmd.Parameters.Add("SHIPTYPE", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("TYPEOFDEVICE", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("DRAUGHT", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("DESTINATION", DbType.String);
|
|
insertStaticCmd.Parameters.Add("DTE", DbType.Boolean);
|
|
insertStaticCmd.Parameters.Add("CLASSB", DbType.Boolean);
|
|
insertStaticCmd.Parameters.Add("BREADTH", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("LENGTH", DbType.Int32);
|
|
insertStaticCmd.Parameters.Add("ETA", DbType.DateTime);
|
|
|
|
string insertPosReportString = "INSERT INTO posreport (mmsi, navstatus, rot, cog, sog, accuracy, longitude, latitude, heading, timestamp, raim, commstate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
SQLiteCommand insertPosReportCmd = new SQLiteCommand(insertPosReportString, _connection);
|
|
insertPosReportCmd.Parameters.Add("MMSI", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("NAVSTATUS", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("ROT", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("COG", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("SOG", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("ACCURACY", DbType.Boolean);
|
|
insertPosReportCmd.Parameters.Add("LONGITUDE", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("LATITUDE", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("HEADING", DbType.Int32);
|
|
insertPosReportCmd.Parameters.Add("TIMESTAMP", DbType.DateTime);
|
|
insertPosReportCmd.Parameters.Add("RAIM", DbType.Boolean);
|
|
insertPosReportCmd.Parameters.Add("COMMSTATE", DbType.Int32);
|
|
|
|
while (!_stopFlag)
|
|
{
|
|
if(_inputQueue.TryDequeue(out AIS_Target target))
|
|
{
|
|
|
|
|
|
if(!_storageTargets.ContainsKey(target.MMSI))
|
|
{
|
|
// insert
|
|
insertStaticCmd.Parameters["MMSI"].Value = target.MMSI;
|
|
insertStaticCmd.Parameters["VERSION"].Value = target.AISVersion ?? (object)DBNull.Value;
|
|
insertStaticCmd.Parameters["IMO"].Value = target.IMO ?? (object) DBNull.Value;
|
|
insertStaticCmd.Parameters["CALLSIGN"].Value = target.Callsign ?? "";
|
|
insertStaticCmd.Parameters["NAME"].Value = target.Name ?? "";
|
|
insertStaticCmd.Parameters["SHIPTYPE"].Value = target.ShipType ?? (object) DBNull.Value;
|
|
insertStaticCmd.Parameters["TYPEOFDEVICE"].Value = target.TypeOfDevice;
|
|
if (target.Draught.HasValue)
|
|
insertStaticCmd.Parameters["DRAUGHT"].Value = (int) (target.Draught * 10);
|
|
else
|
|
insertStaticCmd.Parameters["DRAUGHT"].Value = DBNull.Value;
|
|
insertStaticCmd.Parameters["DESTINATION"].Value = target.Destination ?? "";
|
|
insertStaticCmd.Parameters["DTE"].Value = target.DTE ?? (object)DBNull.Value ;
|
|
insertStaticCmd.Parameters["CLASSB"].Value = target.IsClassB ?? (object)DBNull.Value;
|
|
insertStaticCmd.Parameters["BREADTH"].Value = target.Breadth ?? (object)DBNull.Value;
|
|
insertStaticCmd.Parameters["LENGTH"].Value = target.Length ?? (object)DBNull.Value;
|
|
insertStaticCmd.Parameters["ETA"].Value = target.ETA ?? (object)DBNull.Value;
|
|
|
|
if (_stopFlag) break;
|
|
if (insertStaticCmd.ExecuteNonQuery() != 1)
|
|
{
|
|
_log.WarnFormat("Query didn't affect any rows");
|
|
}
|
|
_storageTargets.Add(target.MMSI, target);
|
|
|
|
}
|
|
else
|
|
{
|
|
// update
|
|
updateStaticCmd.Parameters["DESTINATION"].Value = target.Destination ?? "";
|
|
updateStaticCmd.Parameters["ETA"].Value = target.ETA ?? (object)DBNull.Value;
|
|
updateStaticCmd.Parameters["MMSI"].Value = target.MMSI;
|
|
updateStaticCmd.Parameters["VERSION"].Value = target.AISVersion ?? (object)DBNull.Value;
|
|
updateStaticCmd.Parameters["IMO"].Value = target.IMO ?? (object)DBNull.Value;
|
|
updateStaticCmd.Parameters["CALLSIGN"].Value = target.Callsign ?? "";
|
|
updateStaticCmd.Parameters["NAME"].Value = target.Name ?? "";
|
|
updateStaticCmd.Parameters["SHIPTYPE"].Value = target.ShipType ?? (object)DBNull.Value;
|
|
updateStaticCmd.Parameters["TYPEOFDEVICE"].Value = target.TypeOfDevice ?? (object)DBNull.Value;
|
|
if (target.Draught.HasValue)
|
|
updateStaticCmd.Parameters["DRAUGHT"].Value = (int) (target.Draught * 10);
|
|
else
|
|
updateStaticCmd.Parameters["DRAUGHT"].Value = DBNull.Value;
|
|
updateStaticCmd.Parameters["DTE"].Value = target.DTE ?? (object)DBNull.Value;
|
|
updateStaticCmd.Parameters["CLASSB"].Value = target.IsClassB ?? (object)DBNull.Value;
|
|
updateStaticCmd.Parameters["BREADTH"].Value = target.Breadth ?? (object) DBNull.Value;
|
|
updateStaticCmd.Parameters["LENGTH"].Value = target.Length ?? (object)DBNull.Value;
|
|
if (_stopFlag) break;
|
|
if (updateStaticCmd.ExecuteNonQuery() != 1)
|
|
{
|
|
_log.WarnFormat("Query didn't affect any rows");
|
|
}
|
|
}
|
|
|
|
// now create a position report
|
|
insertPosReportCmd.Parameters["MMSI"].Value = target.MMSI;
|
|
insertPosReportCmd.Parameters["NAVSTATUS"].Value = target.NavStatus;
|
|
insertPosReportCmd.Parameters["ROT"].Value = target.ROT;
|
|
if(target.COG.HasValue)
|
|
insertPosReportCmd.Parameters["COG"].Value = (int) (target.COG * 10);
|
|
else
|
|
insertPosReportCmd.Parameters["COG"].Value = DBNull.Value;
|
|
if (target.SOG.HasValue)
|
|
insertPosReportCmd.Parameters["SOG"].Value = (int) (target.SOG * 10);
|
|
else
|
|
insertPosReportCmd.Parameters["SOG"].Value = DBNull.Value;
|
|
insertPosReportCmd.Parameters["ACCURACY"].Value = target.Accuracy;
|
|
if (target.Longitude.HasValue)
|
|
insertPosReportCmd.Parameters["LONGITUDE"].Value = (int)(target.Longitude.Value * 1000);
|
|
else
|
|
insertPosReportCmd.Parameters["LONGITUDE"].Value = DBNull.Value;
|
|
if (target.Latitude.HasValue)
|
|
insertPosReportCmd.Parameters["LATITUDE"].Value = (int)(target.Latitude.Value * 1000);
|
|
else
|
|
insertPosReportCmd.Parameters["LATITUDE"].Value = DBNull.Value;
|
|
insertPosReportCmd.Parameters["HEADING"].Value = target.Heading;
|
|
insertPosReportCmd.Parameters["TIMESTAMP"].Value = target.LastUpdate;
|
|
insertPosReportCmd.Parameters["RAIM"].Value = target.RAIM;
|
|
insertPosReportCmd.Parameters["COMMSTATE"].Value = target.Radio;
|
|
if (_stopFlag) break;
|
|
if (insertPosReportCmd.ExecuteNonQuery() != 1)
|
|
{
|
|
_log.WarnFormat("Query didn't affect any rows");
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
Thread.Sleep(Properties.Settings.Default.ThreadSleepMS);
|
|
}
|
|
}
|
|
}
|
|
|
|
catch (Exception ex)
|
|
{
|
|
_log.ErrorFormat("Something bad has happened: {0}", ex.Message);
|
|
if(!_stopFlag)
|
|
this.FatalErrorOccurred?.Invoke(this, new EventArgs());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// pre-load all targets already stored in the sqlite database
|
|
/// </summary>
|
|
public async Task<Dictionary<int, AIS_Target>> LoadTargets()
|
|
{
|
|
await Task.Run(() =>
|
|
{
|
|
string staticQuery = "SELECT mmsi, version, imo, callsign, name, shiptype, typeofdevice, maxpresetstaticdraught, destination, dte, classb, breadth, length, eta, changed, updatecount FROM staticdata";
|
|
SQLiteCommand cmd = new SQLiteCommand(staticQuery, _connection);
|
|
SQLiteDataReader reader = cmd.ExecuteReader();
|
|
while(reader.Read())
|
|
{
|
|
AIS_Target target = AIS_Target.CreateFromReader(reader);
|
|
_storageTargets.Add(target.MMSI, target);
|
|
}
|
|
reader.Close();
|
|
cmd.Dispose();
|
|
});
|
|
|
|
return _storageTargets;
|
|
}
|
|
|
|
/// <summary>
|
|
/// cleanup DB by removing past pos reports
|
|
/// </summary>
|
|
public void RemoveOldPosReports()
|
|
{
|
|
try
|
|
{
|
|
string removeQuery = string.Format("DELETE FROM posreport where timestamp <= datetime('now', '-{0} day')",
|
|
Properties.Settings.Default.PosReportDBCleanupDays);
|
|
SQLiteCommand cmd = new SQLiteCommand(removeQuery, _connection);
|
|
int rows = cmd.ExecuteNonQuery();
|
|
if(rows > 0)
|
|
_log.InfoFormat("Removed {0} position reports older than {1} days", rows, Properties.Settings.Default.PosReportDBCleanupDays);
|
|
cmd.Dispose();
|
|
}
|
|
catch(SQLiteException ex)
|
|
{
|
|
_log.ErrorFormat("Failed to delete stale position reports: {0}", ex.Message);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IAISThread implementation
|
|
|
|
public string Name { get { return "SQLite storage"; } }
|
|
|
|
public event EventHandler FatalErrorOccurred;
|
|
|
|
public void Start()
|
|
{
|
|
if (_thread != null) return; // may not run twice
|
|
if (_inputQueue == null) return; // run thread only if we have a queue
|
|
ThreadStart runReader = new ThreadStart(this.ReadQueue);
|
|
_thread = new Thread(runReader);
|
|
_thread.Start();
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (_thread == null) return;
|
|
_stopFlag = true;
|
|
_thread.Join();
|
|
_thread = null;
|
|
_connection.Close();
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|