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; namespace bsmd.AIS2Service { /// /// 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. /// 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 _inputQueue; private readonly Dictionary _storageTargets = new Dictionary(); #endregion #region Construction public AIS_SQLiteStorage (ConcurrentQueue inputQueue) { _inputQueue = inputQueue; _connection = new SQLiteConnection(Properties.Settings.Default.SQLiteDBConnectionString); _connection.Open(); } #endregion #region public methods #region MonitorZone /// /// monitor zone loader func for the zone alarm watchdog (doesn't need groups or such) /// /// public List LoadMonitorZones() { List monitorZones = new List(); 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, monitor_group_id, sequence 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; mz.MonitorGroupId = reader.GetInt32(3); mz.Sequence = reader.GetInt32(4); 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; } bool Save(MonitorZone mZone) { if (mZone == null) return false; if(mZone.Id <= 0) { // insert string insertZoneString = $"INSERT INTO monitor_zone (name, active, monitor_group_id, sequence) VALUES ('{mZone.Name}', {mZone.Active}, {mZone.MonitorGroupId}, {mZone.Sequence}"; SQLiteCommand cmd = new SQLiteCommand(insertZoneString, _connection); int insertedRows = cmd.ExecuteNonQuery(); cmd.Dispose(); mZone.Id = GetLastInsertId(); return insertedRows == 1; } else { // update string updateZoneString = $"UPDATE monitor_zone SET name = '{mZone.Name}', active = '{mZone.Active}', sequence = '{mZone.Sequence}' WHERE id = {mZone.Id}'"; SQLiteCommand cmd = new SQLiteCommand(updateZoneString, _connection); int updatedRows = cmd.ExecuteNonQuery(); cmd.Dispose(); return updatedRows == 1; } } bool Delete(MonitorZone mZone) { if (mZone == null) return false; string deleteAlarmString = $"DELETE FROM monitor_zone WHERE id = {mZone.Id}"; SQLiteCommand cmd = new SQLiteCommand(deleteAlarmString, _connection); int affectedRows = cmd.ExecuteNonQuery(); return (affectedRows == 1); } #endregion #region Alarm public List LoadAlarms(MonitorAssignment assignment) { List result = new List(); 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(); cmd.Dispose(); alarm.Id = GetLastInsertId(); return (insertedRows == 1); } else { // update string updateAlarmString = $"UPDATE alarm SET acknowledged = {alarm.Acknowledged} WHERE id = {alarm.Id}"; SQLiteCommand cmd = new SQLiteCommand(updateAlarmString, _connection); int updatedRows = cmd.ExecuteNonQuery(); cmd.Dispose(); return (updatedRows == 1); } } #endregion #region MonitorGroup public List LoadGroups() { List groups = new List(); string loadGroupsString = "SELECT id, name from monitor_group ORDER BY name"; SQLiteCommand laCmd = new SQLiteCommand(loadGroupsString, _connection); SQLiteDataReader reader = laCmd.ExecuteReader(); if (reader.HasRows) { while (reader.Read()) { int id = reader.GetInt32(0); string name = ""; if(!reader.IsDBNull(1)) name = reader.GetString(1); MonitorGroup mGroup = new MonitorGroup(id, name); groups.Add(mGroup); } } return groups; } public bool Delete(MonitorGroup mGroup) { if (mGroup == null) return false; string deleteGroupString = $"DELETE FROM monitor_group WHERE id = {mGroup.Id}"; SQLiteCommand cmd = new SQLiteCommand(deleteGroupString, _connection); int deletedGroups = cmd.ExecuteNonQuery(); cmd.Dispose(); return deletedGroups == 1; } public bool Save(MonitorGroup mGroup) { if(mGroup == null) return false; if(mGroup.Id <= 0) { // insert string saveGroupString = $"INSERT INTO monitor_group (name) VALUES ('{mGroup.Name}')"; SQLiteCommand cmd = new SQLiteCommand(saveGroupString, _connection); int insertedRows = cmd.ExecuteNonQuery(); cmd.Dispose(); mGroup.Id = GetLastInsertId(); return (insertedRows == 1); } else { // update string saveGroupString = $"UPDATE monitor_group SET name = '{mGroup.Name}' WHERE id = {mGroup.Id}"; SQLiteCommand cmd = new SQLiteCommand(saveGroupString, _connection); int updatedRows = cmd.ExecuteNonQuery(); cmd.Dispose(); return (updatedRows == 1); } } #endregion #region ZoneVertex public bool Save(GeoPoint geoPoint) { if (geoPoint == null) return false; string saveGeoPointString = $"INSERT INTO zone_vertex (monitor_zone_id, latitude, longitude) VALUES ({geoPoint.MonitorZoneId}, {geoPoint.Lat}, {geoPoint.Lon})"; SQLiteCommand cmd = new SQLiteCommand(saveGeoPointString, _connection); int insertedRows = cmd.ExecuteNonQuery(); cmd.Dispose(); return insertedRows == 1; } #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()); } } /// /// pre-load all targets already stored in the sqlite database /// public async Task> 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; } /// /// cleanup DB by removing past pos reports /// 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); } } public int GetLastInsertId() { string sql = "SELECT last_insert_rowid()"; SQLiteCommand cmd = new SQLiteCommand(sql, _connection); int lastID = (Int32)cmd.ExecuteScalar(); return lastID; } #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 } }