Endpunkte für Gruppen / Alarmabrufe hinzugefügt
This commit is contained in:
parent
8e0b4c56c1
commit
80b4a39a58
@ -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,12 +39,12 @@ 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]);
|
||||||
@ -58,7 +59,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 +88,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
|
||||||
|
|
||||||
|
|||||||
@ -15,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>();
|
||||||
@ -53,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
|
||||||
@ -78,6 +79,8 @@ namespace bsmd.AIS2Service
|
|||||||
reader.Close();
|
reader.Close();
|
||||||
lzCmd.Dispose();
|
lzCmd.Dispose();
|
||||||
|
|
||||||
|
if (loadInnerCollections)
|
||||||
|
{
|
||||||
// load vertices for each zone
|
// load vertices for each zone
|
||||||
string loadVertexString = "SELECT Id, latitude, longitude FROM zone_vertex WHERE monitor_zone_id = @ID";
|
string loadVertexString = "SELECT Id, latitude, longitude FROM zone_vertex WHERE monitor_zone_id = @ID";
|
||||||
SQLiteCommand lvCmd = new SQLiteCommand(loadVertexString, _connection);
|
SQLiteCommand lvCmd = new SQLiteCommand(loadVertexString, _connection);
|
||||||
@ -123,6 +126,7 @@ namespace bsmd.AIS2Service
|
|||||||
reader.Close();
|
reader.Close();
|
||||||
}
|
}
|
||||||
laCmd.Dispose();
|
laCmd.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
return monitorZones;
|
return monitorZones;
|
||||||
}
|
}
|
||||||
@ -255,6 +259,8 @@ namespace bsmd.AIS2Service
|
|||||||
groups.Add(mGroup);
|
groups.Add(mGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
reader.Close();
|
||||||
|
laCmd.Dispose();
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,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}";
|
||||||
|
|
||||||
|
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
|
||||||
@ -628,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
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
AIS/bsmd.AIS2Service/SLRController.cs
Normal file
27
AIS/bsmd.AIS2Service/SLRController.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace bsmd.AIS2Service
|
||||||
|
{
|
||||||
|
public class SLRController : ApiController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public IEnumerable<ShipLocationReport> Get([FromUri] int? id)
|
||||||
|
{
|
||||||
|
if (!id.HasValue) return null;
|
||||||
|
List<ShipLocationReport> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
AIS/bsmd.AIS2Service/ShipLocationReport.cs
Normal file
31
AIS/bsmd.AIS2Service/ShipLocationReport.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
41
AIS/bsmd.AIS2Service/ZonesController.cs
Normal file
41
AIS/bsmd.AIS2Service/ZonesController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -127,9 +127,12 @@
|
|||||||
<DependentUpon>Settings.settings</DependentUpon>
|
<DependentUpon>Settings.settings</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="SerialTCPReader.cs" />
|
<Compile Include="SerialTCPReader.cs" />
|
||||||
|
<Compile Include="ShipLocationReport.cs" />
|
||||||
<Compile Include="SitRep.cs" />
|
<Compile Include="SitRep.cs" />
|
||||||
|
<Compile Include="SLRController.cs" />
|
||||||
<Compile Include="StartupWebAPI.cs" />
|
<Compile Include="StartupWebAPI.cs" />
|
||||||
<Compile Include="Util.cs" />
|
<Compile Include="Util.cs" />
|
||||||
|
<Compile Include="ZonesController.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\SQL\ais_initial.db">
|
<None Include="..\SQL\ais_initial.db">
|
||||||
|
|||||||
25
AIS/bsmd.AIS2Service/lauf.html
Normal file
25
AIS/bsmd.AIS2Service/lauf.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="DataTables/datatables.css">
|
||||||
|
<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>
|
||||||
|
<script type="text/javascript" src="DataTables/jQuery-3.6.0/jquery-3.6.0.js"></script>
|
||||||
|
<script type="text/javascript" src="DataTables/DataTables-1.12.1/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#aisTable').DataTable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="startup()">
|
||||||
|
<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>
|
||||||
@ -126,3 +126,11 @@ h2{
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bottomcenter {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 20px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
74
AIS/bsmd.AIS2Service/zonen.js
Normal file
74
AIS/bsmd.AIS2Service/zonen.js
Normal file
@ -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 = `<td>` + data[i].MMSI +
|
||||||
|
`</td><td id="name_` + data[i].MMSI + `"/>` +
|
||||||
|
`<td id="timestamp_` + data[i].MMSI + `"/>` +
|
||||||
|
`<td id="lat_` + data[i].MMSI + `"/>` +
|
||||||
|
`<td id="lon_` + data[i].MMSI + `"/>` +
|
||||||
|
`<td id="imo_` + data[i].MMSI + `"/>` +
|
||||||
|
`<td>` + data[i].IsClassB + `</td>`;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user