diff --git a/Diagrams/aircraft-lookup.png b/Diagrams/aircraft-lookup.png
new file mode 100644
index 0000000..bfbf238
Binary files /dev/null and b/Diagrams/aircraft-lookup.png differ
diff --git a/README.md b/README.md
index 2c69e11..24177cd 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,6 @@
## The Console Application
-### Overview
-
- The repository includes a console application that uses the [Spectre.Console package](https://github.com/spectreconsole/spectre.console) to render a live view of the aircraft currently being tracked:
![Console Application](Diagrams/screenshot.png)
@@ -35,7 +33,63 @@
- As it moves through the tracking states (see below), it will be highlighted in yellow, when it reaches the "Recent" state, and red, when it reaches the "Stale" state
- When it is removed from the tracker's tracking list, it is also removed from the live table
-### Configuration File
+## GUI
+
+- The repository includes a UI built using [Avalonia UI](https://www.avaloniaui.net/):
+
+![UI](Diagrams/ui-screenshot.png)
+
+### Tracking Menu
+
+- The tracking menu is only available when the "Live View" or "Map View" tabs are selected
+- To start live tracking, select the "Start live tracking" option on this menu
+- To stop live tracking, select the "Stop live tracking" option on this menu
+
+#### Tracking Filters
+
+- To filter the live tracking view, make sure the application is currently tracking
+- Open the "Tracking Filters" dialog (Tracking > Filters)
+
+
+
+- Enter the required filtering criteria and click "OK"
+- To clear the live tracking filters, make sure the application is currently tracking
+- Select "Clear filters"on the Tracking Menu
+
+#### Aircraft Details Lookup
+
+- To look-up aircraft details from the Live View or Database Search, click on the row containing the aircraft of interest
+- The aircraft lookup dialog will be displayed, pre-populated with the aircraft address, and the lookup will be completed automatically:
+
+
+
+- Alternatively, select the "Aircraft Lookup" option from the Tracking menu
+- The same dialog is opened, but with no details pre-populated
+- Enter the aircraft's ICAO address and click on the "Lookup" button to perform the lookup
+
+#### Tracking Options
+
+- To view/edit the tracking options, make sure the application is not currently tracking
+- Open the "Tracking Options" dialog (Tracking > Options)
+- The resulting dialog allows you to specify many of the tracking parameters described under "Configuration File", above
+
+
+
+### Database Menu
+
+- The database menu is only available when the "Database Search" tab is selected
+- To search the database, select the "Search" option from the "Database" menu
+
+
+
+- Enter the search criteria then click OK
+- The database grid will be populated with any records matching the specified criteria
+- To export the records currently shown in the database search results, select "Export" from the "Database" menu
+- A file selection dialog will be displayed allowing you to export the results in either XLSX or CSV format
+
+## Application Configuration File
+
+- The console and GUI applications use a common configuration file format, described in this section
### General Settings and Database Connection String
@@ -61,11 +115,13 @@
| ApplicationSettings | ReceiverLatitude | --latitude | -la | Receiver latitude, used in aircraft distance calculations |
| ApplicationSettings | ReceiverLongitude | --longitude | -lo | Receiver longitude, used in aircraft distance calculations |
| ApplicationSettings | Columns | - | - | Set of column definitions for columns to be included in the output |
+| ApplicationSettings | ApiEndpoints | - | - | Set of endpoint definitions for external APIs (GUI only) |
+| ApplicationSettings | ApiServiceKeys | - | - | Set of API key definitions for external APIs (GUI only) |
| ConnectionStrings | BaseStationReaderDB | - | - | SQLite connection string for the database |
- Values may also be passed using the indicated command line arguments, in which case the values are first read from the configuration file and then any values specified on the command line are then applied
-#### Column Definitions
+### Column Definitions
- The Columns property in the ApplicationSettings section of the file contains a list of column definitions:
@@ -94,36 +150,21 @@
- Each column definition contains the following items:
-| Item | Comments |
-| -------- | ------------------------------------------------------------------------------------------ |
-| Property | Case-sensitive name of the property on the Aircraft entity to be rendered in this column |
-| Label | Column title |
-| Format | The C# format string used to render the property (for Decimal and DateTime types) or blank |
-| Context | Specifies the named context in which the column definition is used (GUI only, see below) |
+| Item | Comments |
+| -------- | ---------------------------------------------------------------------------------------- |
+| Property | Case-sensitive name of the property on the Aircraft entity to be rendered in this column |
+| Label | Column title |
+| Format | The C# format string used to render the property or blank for default formatting |
+| Context | Specifies the named context in which the column definition is used (GUI only) |
- The application will show only the columns listed in this section of the configuration file, showing them in the order in which they appear here and formatted according to the format specifier
-
-#### Row Limits and Column Control
-
-- The maximum row limit and custom column control are intended to support running the application on small screens
-- The following shows the console application running on a Raspberry Pi with 3.5" LCD screen:
-
-![Raspberry Pi](Diagrams/RaspberryPi.jpg)
-
-## The GUI
-
-- The repository includes a UI built using [Avalonia UI](https://www.avaloniaui.net/):
-
-![UI](Diagrams/ui-screenshot.png)
-
-### Configuration File
-
-- The application configuration file is very similar to the Console Application configuration file (see above)
+- For the Console application, the format string is passed to the ".ToString()" method to format the column
- For the GUI, the format specifier given in the column definitions is a "String.Format" string adhering to the guidelines in the following article:
[String.Format Method](https://learn.microsoft.com/en-us/dotnet/api/system.string.format?view=net-7.0)
-- The Context that forms part of the column definition (see above) is also respected and may be one of the following values:
+- For the console application, the "Context" property is ignored
+- For the GUI, it should be one of the following values:
| Value | Applies To |
| ------------------- | ----------------------------------------- |
@@ -131,42 +172,59 @@
| TrackedAircraftGrid | Live view only |
| DatabaseGrid | Database search results only |
-### Tracking Menu
+### Row Limits and Column Control
-- The tracking menu is only available when the "Live View" or "Map View" tabs are selected
-- To start live tracking, select the "Start live tracking" option on this menu
-- To stop live tracking, select the "Stop live tracking" option on this menu
+- The maximum row limit and custom column control are intended to support running the application on small screens
+- The following shows the console application running on a Raspberry Pi with 3.5" LCD screen:
-#### Tracking Filters
+![Raspberry Pi](Diagrams/RaspberryPi.jpg)
-- To filter the live tracking view, make sure the application is currently tracking
-- Open the "Tracking Filters" dialog (Tracking > Filters)
+### External API Configuration
-
+- The UI application includes integration with the AirLabs public APIs for aircraft details lookup:
-- Enter the required filtering criteria and click "OK"
-- To clear the live tracking filters, make sure the application is currently tracking
-- Select "Clear filters"on the Tracking Menu
+[AirLabs API Documentation](https://airlabs.co/docs/)
-#### Tracking Options
+- To use the integration, an AirLabs subscription is needed, as this includes an API key needed to acces the AirLabs APIs
+- The integration will work with the free subscription, though with restricted monthly usage
+- The integration is configured via the following keys in the configuration file:
-- To view/edit the tracking options, make sure the application is not currently tracking
-- Open the "Tracking Options" dialog (Tracking > Options)
-- The resulting dialog allows you to specify many of the tracking parameters described under "Configuration File", above
+| Section | Sub-Section | Purpose |
+| ------------------- | -------------- | ------------------------------------------------------------------------------------------- |
+| ApplicationSettings | ApiEndpoints | A list of endpoint definitions, each containing the endpoint type, service and endpoint URL |
+| ApplicationSettings | ApiServiceKeys | A list of entries mapping each service to the API key needed to access that service |
-
+#### ApiEndpoint Definitions
-### Database Menu
+- An example API endpoint definition is shown below:
-- The database menu is only available when the "Database Search" tab is selected
-- To search the database, select the "Search" option from the "Database" menu
+```json
+{
+ "EndpointType": "Airlines",
+ "Service": "AirLabs",
+ "Url": "https://airlabs.co/api/v9/airlines"
+}
+```
-
+- Possible values for the endpoint type are:
-- Enter the search criteria then click OK
-- The database grid will be populated with any records matching the specified criteria
-- To export the records currently shown in the database search results, select "Export" from the "Database" menu
-- A file selection dialog will be displayed allowing you to export the results in either XLSX or CSV format
+| Type | Description |
+| -------- | ------------------------------------------------------------------------- |
+| Airlines | Endpoint used to retrieve airline details given an airline IATA/ICAO code |
+| Aircraft | Endpoint used to retrieve aircraft details given a 24-bit ICAO address |
+
+- Currently, only the AirLabs APIs are supported
+
+#### ApiServiceKey Definitions
+
+- An example key definition for a service is shown below:
+
+```json
+{
+ "Service": "AirLabs",
+ "Key": "put-your-api-key-here"
+}
+```
## Aircraft Tracking
@@ -204,6 +262,15 @@
- As messages are received, the tracker selects the appropriate parser based on the message type
- Currently, the only parser that has been implemented is for the MSG message type
+## External Service Integration
+
+- The section describing the configuration file, above, describes the configuration needed to enable external API integration
+- When configured, the APIs are used to look up the following details based on the ICAO 24-bit address:
+ - Airline
+ - Model IATA and ICAO codes
+- The model IATA and ICAO codes are used to look up model and manufacturer details from the local SQLite database
+- Airline details and a record mapping the aircraft's ICAO address to the airline, manufacturer and model are stored in the local SQLite database, to improve lookup performance for that address
+
## SQLite Database
### Database Schema
@@ -238,6 +305,16 @@ dotnet ef database update -s ../BaseStationReader.Terminal/BaseStationReader.Ter
- If the database doesn't exist, it will create it
- It will then bring the database up to date by applying all pending migrations
+### Model Lookup Data
+
+- If the intention is to use the API integration to provide aircraft details lookup, the aircraft model data should be created
+- To do this, run the following SQL scripts from the "sql\lookup" folder against the database, in the order shown:
+
+```
+PopulateManufacturers.sql
+PopulateAircraftModels.sql
+```
+
### Record Locking
- As stated above, the [ICAO 24-bit address](https://en.wikipedia.org/wiki/Aviation_transponder_interrogation_modes) is used as the unique identifier for an aircraft when writing updates to the database
diff --git a/sql/lookup/PopulateAircraftModels.sql b/sql/lookup/PopulateAircraftModels.sql
new file mode 100644
index 0000000..ddce3cf
--- /dev/null
+++ b/sql/lookup/PopulateAircraftModels.sql
@@ -0,0 +1,5 @@
+PRAGMA journal_mode = WAL;
+
+SELECT m.IATA, m.ICAO, m.Name, ma.Name AS "Manufacturer"
+FROM MODEL m
+INNER JOIN MANUFACTURER ma ON ma.Id = m.ManufacturerId;
diff --git a/sql/lookup/PopulateManufacturers.sql b/sql/lookup/PopulateManufacturers.sql
new file mode 100644
index 0000000..2296c14
--- /dev/null
+++ b/sql/lookup/PopulateManufacturers.sql
@@ -0,0 +1,67 @@
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '1', 'Aerospatiale (Nord)');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '2', 'Aerospatiale');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '3', 'Aerospatiale/Alenia');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '4', 'Airbus');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '5', 'Antonov');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '6', 'Avro');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '7', 'BAe');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '8', 'Beechraft');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '9', 'Bell');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '10', 'Beriev');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '11', 'Boeing');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '12', 'Bombardier');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '13', 'Bombardier Global Express');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '14', 'British Aerospace');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '15', 'Canadair');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '16', 'CASA/IPTN');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '17', 'Cessna');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '18', 'Cirrus');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '19', 'COMARC');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '20', 'Convair');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '21', 'Curtiss');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '22', 'Daher');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '23', 'Dassault');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '24', 'De Havilland');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '25', 'Diamond');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '26', 'Dornier');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '27', 'Douglas');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '28', 'Eclipse');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '29', 'Embraer');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '30', 'Eurocopter (Aerospatiale)');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '31', 'Eurocopter');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '32', 'Fairchild Dornier');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '33', 'Fairchild Swearingen');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '34', 'Fokker');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '35', 'Government Aircraft Factories');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '36', 'Grumman');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '37', 'Gulfstream Aerospace');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '38', 'Gulfstream');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '39', 'Gulfstream/Rockwell ');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '40', 'Harbin');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '41', 'Hawker');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '42', 'Hawker Siddeley');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '43', 'Honda');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '44', 'Ilyushin');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '45', 'Israel Aircraft Industries');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '46', 'Junkers');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '47', 'Learjet');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '48', 'LET');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '49', 'Lockheed');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '50', 'Lockheed Martin');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '51', 'McDonnell Douglas');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '52', 'MD Helicopters');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '53', 'MIL');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '54', 'Mitsubishi');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '55', 'NAMC');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '56', 'Patrenavia');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '57', 'Piaggio');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '58', 'Pilatus');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '59', 'Piper');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '60', 'Reims-Cessna');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '61', 'Saab');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '62', 'Shorts');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '63', 'Sikorsky');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '64', 'Sukhoi');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '65', 'Tecnam');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '66', 'Tupolev');
+INSERT INTO MANUFACTURER ( Id, Name ) VALUES ( '67', 'Yakovlev');
diff --git a/sql/Cleardown.sql b/sql/queries/Cleardown.sql
similarity index 100%
rename from sql/Cleardown.sql
rename to sql/queries/Cleardown.sql
diff --git a/sql/queries/ListAircraftModels.sql b/sql/queries/ListAircraftModels.sql
new file mode 100644
index 0000000..ddce3cf
--- /dev/null
+++ b/sql/queries/ListAircraftModels.sql
@@ -0,0 +1,5 @@
+PRAGMA journal_mode = WAL;
+
+SELECT m.IATA, m.ICAO, m.Name, ma.Name AS "Manufacturer"
+FROM MODEL m
+INNER JOIN MANUFACTURER ma ON ma.Id = m.ManufacturerId;
diff --git a/sql/ReportPositions.sql b/sql/queries/ReportPositions.sql
similarity index 100%
rename from sql/ReportPositions.sql
rename to sql/queries/ReportPositions.sql
diff --git a/sql/Summarise.sql b/sql/queries/Summarise.sql
similarity index 100%
rename from sql/Summarise.sql
rename to sql/queries/Summarise.sql
diff --git a/sql/SummariseByAddress.sql b/sql/queries/SummariseByAddress.sql
similarity index 100%
rename from sql/SummariseByAddress.sql
rename to sql/queries/SummariseByAddress.sql
diff --git a/src/BaseStationReader.Data/BaseStationReader.Data.csproj b/src/BaseStationReader.Data/BaseStationReader.Data.csproj
index 7e08a2f..b021b1a 100644
--- a/src/BaseStationReader.Data/BaseStationReader.Data.csproj
+++ b/src/BaseStationReader.Data/BaseStationReader.Data.csproj
@@ -5,7 +5,7 @@
enable
enable
BaseStationReader.Data
- 1.27.0.0
+ 1.28.0.0
Dave Walker
Copyright (c) Dave Walker 2023
Dave Walker
@@ -17,7 +17,7 @@
https://github.com/davewalker5/ADS-B-BaseStationReader
MIT
false
- 1.27.0.0
+ 1.28.0.0
diff --git a/src/BaseStationReader.Data/BaseStationReaderDbContext.cs b/src/BaseStationReader.Data/BaseStationReaderDbContext.cs
index b0a93cb..b61e02a 100644
--- a/src/BaseStationReader.Data/BaseStationReaderDbContext.cs
+++ b/src/BaseStationReader.Data/BaseStationReaderDbContext.cs
@@ -1,4 +1,5 @@
-using BaseStationReader.Entities.Tracking;
+using BaseStationReader.Entities.Lookup;
+using BaseStationReader.Entities.Tracking;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics.CodeAnalysis;
@@ -9,6 +10,10 @@ public partial class BaseStationReaderDbContext : DbContext
{
public virtual DbSet Aircraft { get; set; }
public virtual DbSet AircraftPositions { get; set; }
+ public virtual DbSet Airlines { get; set; }
+ public virtual DbSet Manufacturers { get; set; }
+ public virtual DbSet Models { get; set; }
+ public virtual DbSet AircraftDetails { get; set; }
public BaseStationReaderDbContext(DbContextOptions options) : base(options)
{
@@ -24,11 +29,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
{
entity.ToTable("AIRCRAFT");
- entity.Property(e => e.Id)
- .HasColumnName("Id")
- .ValueGeneratedOnAdd();
-
- entity.Property(e => e.Address).HasColumnName("Address");
+ entity.Property(e => e.Id).HasColumnName("Id").ValueGeneratedOnAdd();
+ entity.Property(e => e.Address).IsRequired().HasColumnName("Address");
entity.Property(e => e.Callsign).HasColumnName("Callsign");
entity.Property(e => e.Altitude).HasColumnName("Altitude");
entity.Property(e => e.GroundSpeed).HasColumnName("GroundSpeed");
@@ -50,6 +52,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnName("LastSeen")
.HasColumnType("DATETIME");
+
+ entity.HasMany(e => e.Positions)
+ .WithOne(e => e.Aircraft)
+ .HasForeignKey(e => e.AircraftId);
+
});
modelBuilder.Entity(entity =>
@@ -58,28 +65,55 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.ToTable("AIRCRAFT_POSITION");
- entity.Property(e => e.Id)
- .HasColumnName("Id")
- .ValueGeneratedOnAdd();
+ entity.Property(e => e.Id).HasColumnName("Id").ValueGeneratedOnAdd();
+ entity.Property(e => e.AircraftId).IsRequired().HasColumnName("AircraftId");
+ entity.Property(e => e.Latitude).IsRequired().HasColumnName("Latitude");
+ entity.Property(e => e.Longitude).IsRequired().HasColumnName("Longitude");
+ entity.Property(e => e.Distance).HasColumnName("Distance");
+ entity.Property(e => e.Timestamp).IsRequired().HasColumnName("Timestamp").HasColumnType("DATETIME");
+ });
- entity.Property(e => e.AircraftId)
- .IsRequired()
- .HasColumnName("AircraftId");
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("MANUFACTURER");
- entity.Property(e => e.Latitude)
- .IsRequired()
- .HasColumnName("Latitude");
+ entity.Property(e => e.Id).HasColumnName("Id").ValueGeneratedOnAdd();
+ entity.Property(e => e.Name).IsRequired().HasColumnName("Name");
- entity.Property(e => e.Longitude)
- .IsRequired()
- .HasColumnName("Longitude");
+ entity.HasMany(e => e.Models)
+ .WithOne(e => e.Manufacturer)
+ .HasForeignKey(e => e.ManufacturerId);
- entity.Property(e => e.Timestamp)
- .IsRequired()
- .HasColumnName("Timestamp")
- .HasColumnType("DATETIME");
+ });
- entity.Property(e => e.Distance).HasColumnName("Distance");
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("AIRLINE");
+
+ entity.Property(e => e.Id).HasColumnName("Id").ValueGeneratedOnAdd();
+ entity.Property(e => e.IATA).IsRequired().HasColumnName("IATA");
+ entity.Property(e => e.ICAO).IsRequired().HasColumnName("ICAO");
+ entity.Property(e => e.Name).IsRequired().HasColumnName("Name");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("MODEL");
+
+ entity.Property(e => e.Id).HasColumnName("Id").ValueGeneratedOnAdd();
+ entity.Property(e => e.IATA).HasColumnName("IATA");
+ entity.Property(e => e.ICAO).HasColumnName("ICAO");
+ entity.Property(e => e.Name).HasColumnName("Name");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("AIRCRAFT_DETAILS");
+
+ entity.Property(e => e.Id).HasColumnName("Id").ValueGeneratedOnAdd();
+ entity.Property(e => e.Address).IsRequired().HasColumnName("Address");
+ entity.Property(e => e.ModelId).HasColumnName("ModelId");
+ entity.Property(e => e.AirlineId).HasColumnName("AirlineId");
});
}
}
diff --git a/src/BaseStationReader.Data/Migrations/20230926123432_AircraftModelLookup.Designer.cs b/src/BaseStationReader.Data/Migrations/20230926123432_AircraftModelLookup.Designer.cs
new file mode 100644
index 0000000..34d6cb5
--- /dev/null
+++ b/src/BaseStationReader.Data/Migrations/20230926123432_AircraftModelLookup.Designer.cs
@@ -0,0 +1,239 @@
+//
+using System;
+using BaseStationReader.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace BaseStationReader.Data.Migrations
+{
+ [DbContext(typeof(BaseStationReaderDbContext))]
+ [Migration("20230926123432_AircraftModelLookup")]
+ partial class AircraftModelLookup
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.10");
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Airline", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("IATA")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("IATA");
+
+ b.Property("ICAO")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ICAO");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.ToTable("AIRLINE", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Manufacturer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.ToTable("MANUFACTURER", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Model", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("IATA")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("IATA");
+
+ b.Property("ICAO")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ICAO");
+
+ b.Property("ManufacturerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ManufacturerId");
+
+ b.ToTable("MODEL", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.Aircraft", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Address");
+
+ b.Property("Altitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Altitude");
+
+ b.Property("Callsign")
+ .HasColumnType("TEXT")
+ .HasColumnName("Callsign");
+
+ b.Property("Distance")
+ .HasColumnType("REAL")
+ .HasColumnName("Distance");
+
+ b.Property("FirstSeen")
+ .HasColumnType("DATETIME")
+ .HasColumnName("FirstSeen");
+
+ b.Property("GroundSpeed")
+ .HasColumnType("TEXT")
+ .HasColumnName("GroundSpeed");
+
+ b.Property("LastSeen")
+ .HasColumnType("DATETIME")
+ .HasColumnName("LastSeen");
+
+ b.Property("Latitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Latitude");
+
+ b.Property("Longitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Longitude");
+
+ b.Property("Messages")
+ .HasColumnType("INTEGER")
+ .HasColumnName("Messages");
+
+ b.Property("Squawk")
+ .HasColumnType("TEXT")
+ .HasColumnName("Squawk");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER")
+ .HasColumnName("Status");
+
+ b.Property("Track")
+ .HasColumnType("TEXT")
+ .HasColumnName("Track");
+
+ b.Property("VerticalRate")
+ .HasColumnType("TEXT")
+ .HasColumnName("VerticalRate");
+
+ b.HasKey("Id");
+
+ b.ToTable("AIRCRAFT", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.AircraftPosition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("AircraftId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("AircraftId");
+
+ b.Property("Altitude")
+ .HasColumnType("TEXT");
+
+ b.Property("Distance")
+ .HasColumnType("REAL")
+ .HasColumnName("Distance");
+
+ b.Property("Latitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Latitude");
+
+ b.Property("Longitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Longitude");
+
+ b.Property("Timestamp")
+ .HasColumnType("DATETIME")
+ .HasColumnName("Timestamp");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AircraftId");
+
+ b.ToTable("AIRCRAFT_POSITION", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Model", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Lookup.Manufacturer", "Manufacturer")
+ .WithMany("Models")
+ .HasForeignKey("ManufacturerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manufacturer");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.AircraftPosition", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Tracking.Aircraft", "Aircraft")
+ .WithMany("Positions")
+ .HasForeignKey("AircraftId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Aircraft");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Manufacturer", b =>
+ {
+ b.Navigation("Models");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.Aircraft", b =>
+ {
+ b.Navigation("Positions");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/BaseStationReader.Data/Migrations/20230926123432_AircraftModelLookup.cs b/src/BaseStationReader.Data/Migrations/20230926123432_AircraftModelLookup.cs
new file mode 100644
index 0000000..e38f543
--- /dev/null
+++ b/src/BaseStationReader.Data/Migrations/20230926123432_AircraftModelLookup.cs
@@ -0,0 +1,105 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable disable
+
+namespace BaseStationReader.Data.Migrations
+{
+ ///
+ [ExcludeFromCodeCoverage]
+ public partial class AircraftModelLookup : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AIRLINE",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ IATA = table.Column(type: "TEXT", nullable: false),
+ ICAO = table.Column(type: "TEXT", nullable: false),
+ Name = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AIRLINE", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MANUFACTURER",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MANUFACTURER", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MODEL",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ ManufacturerId = table.Column(type: "INTEGER", nullable: false),
+ IATA = table.Column(type: "TEXT", nullable: false),
+ ICAO = table.Column(type: "TEXT", nullable: false),
+ Name = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MODEL", x => x.Id);
+ table.ForeignKey(
+ name: "FK_MODEL_MANUFACTURER_ManufacturerId",
+ column: x => x.ManufacturerId,
+ principalTable: "MANUFACTURER",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AIRCRAFT_POSITION_AircraftId",
+ table: "AIRCRAFT_POSITION",
+ column: "AircraftId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MODEL_ManufacturerId",
+ table: "MODEL",
+ column: "ManufacturerId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_AIRCRAFT_POSITION_AIRCRAFT_AircraftId",
+ table: "AIRCRAFT_POSITION",
+ column: "AircraftId",
+ principalTable: "AIRCRAFT",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_AIRCRAFT_POSITION_AIRCRAFT_AircraftId",
+ table: "AIRCRAFT_POSITION");
+
+ migrationBuilder.DropTable(
+ name: "AIRLINE");
+
+ migrationBuilder.DropTable(
+ name: "MODEL");
+
+ migrationBuilder.DropTable(
+ name: "MANUFACTURER");
+
+ migrationBuilder.DropIndex(
+ name: "IX_AIRCRAFT_POSITION_AircraftId",
+ table: "AIRCRAFT_POSITION");
+ }
+ }
+}
diff --git a/src/BaseStationReader.Data/Migrations/20230926181237_AircraftDetails.Designer.cs b/src/BaseStationReader.Data/Migrations/20230926181237_AircraftDetails.Designer.cs
new file mode 100644
index 0000000..a2f44b4
--- /dev/null
+++ b/src/BaseStationReader.Data/Migrations/20230926181237_AircraftDetails.Designer.cs
@@ -0,0 +1,283 @@
+//
+using System;
+using BaseStationReader.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace BaseStationReader.Data.Migrations
+{
+ [DbContext(typeof(BaseStationReaderDbContext))]
+ [Migration("20230926181237_AircraftDetails")]
+ partial class AircraftDetails
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.10");
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.AircraftDetails", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Address");
+
+ b.Property("AirlineId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("AirlineId");
+
+ b.Property("ModelId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("ModelId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AirlineId");
+
+ b.HasIndex("ModelId");
+
+ b.ToTable("AIRCRAFT_DETAILS", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Airline", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("IATA")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("IATA");
+
+ b.Property("ICAO")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ICAO");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.ToTable("AIRLINE", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Manufacturer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.ToTable("MANUFACTURER", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Model", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("IATA")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("IATA");
+
+ b.Property("ICAO")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ICAO");
+
+ b.Property("ManufacturerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ManufacturerId");
+
+ b.ToTable("MODEL", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.Aircraft", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Address");
+
+ b.Property("Altitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Altitude");
+
+ b.Property("Callsign")
+ .HasColumnType("TEXT")
+ .HasColumnName("Callsign");
+
+ b.Property("Distance")
+ .HasColumnType("REAL")
+ .HasColumnName("Distance");
+
+ b.Property("FirstSeen")
+ .HasColumnType("DATETIME")
+ .HasColumnName("FirstSeen");
+
+ b.Property("GroundSpeed")
+ .HasColumnType("TEXT")
+ .HasColumnName("GroundSpeed");
+
+ b.Property("LastSeen")
+ .HasColumnType("DATETIME")
+ .HasColumnName("LastSeen");
+
+ b.Property("Latitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Latitude");
+
+ b.Property("Longitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Longitude");
+
+ b.Property("Messages")
+ .HasColumnType("INTEGER")
+ .HasColumnName("Messages");
+
+ b.Property("Squawk")
+ .HasColumnType("TEXT")
+ .HasColumnName("Squawk");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER")
+ .HasColumnName("Status");
+
+ b.Property("Track")
+ .HasColumnType("TEXT")
+ .HasColumnName("Track");
+
+ b.Property("VerticalRate")
+ .HasColumnType("TEXT")
+ .HasColumnName("VerticalRate");
+
+ b.HasKey("Id");
+
+ b.ToTable("AIRCRAFT", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.AircraftPosition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("AircraftId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("AircraftId");
+
+ b.Property("Altitude")
+ .HasColumnType("TEXT");
+
+ b.Property("Distance")
+ .HasColumnType("REAL")
+ .HasColumnName("Distance");
+
+ b.Property("Latitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Latitude");
+
+ b.Property("Longitude")
+ .HasColumnType("TEXT")
+ .HasColumnName("Longitude");
+
+ b.Property("Timestamp")
+ .HasColumnType("DATETIME")
+ .HasColumnName("Timestamp");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AircraftId");
+
+ b.ToTable("AIRCRAFT_POSITION", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.AircraftDetails", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Lookup.Airline", "Airline")
+ .WithMany()
+ .HasForeignKey("AirlineId");
+
+ b.HasOne("BaseStationReader.Entities.Lookup.Model", "Model")
+ .WithMany()
+ .HasForeignKey("ModelId");
+
+ b.Navigation("Airline");
+
+ b.Navigation("Model");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Model", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Lookup.Manufacturer", "Manufacturer")
+ .WithMany("Models")
+ .HasForeignKey("ManufacturerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manufacturer");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.AircraftPosition", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Tracking.Aircraft", "Aircraft")
+ .WithMany("Positions")
+ .HasForeignKey("AircraftId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Aircraft");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Manufacturer", b =>
+ {
+ b.Navigation("Models");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.Aircraft", b =>
+ {
+ b.Navigation("Positions");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/BaseStationReader.Data/Migrations/20230926181237_AircraftDetails.cs b/src/BaseStationReader.Data/Migrations/20230926181237_AircraftDetails.cs
new file mode 100644
index 0000000..f989f69
--- /dev/null
+++ b/src/BaseStationReader.Data/Migrations/20230926181237_AircraftDetails.cs
@@ -0,0 +1,58 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable disable
+
+namespace BaseStationReader.Data.Migrations
+{
+ ///
+ [ExcludeFromCodeCoverage]
+ public partial class AircraftDetails : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AIRCRAFT_DETAILS",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Address = table.Column(type: "TEXT", nullable: false),
+ ModelId = table.Column(type: "INTEGER", nullable: true),
+ AirlineId = table.Column(type: "INTEGER", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AIRCRAFT_DETAILS", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AIRCRAFT_DETAILS_AIRLINE_AirlineId",
+ column: x => x.AirlineId,
+ principalTable: "AIRLINE",
+ principalColumn: "Id");
+ table.ForeignKey(
+ name: "FK_AIRCRAFT_DETAILS_MODEL_ModelId",
+ column: x => x.ModelId,
+ principalTable: "MODEL",
+ principalColumn: "Id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AIRCRAFT_DETAILS_AirlineId",
+ table: "AIRCRAFT_DETAILS",
+ column: "AirlineId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AIRCRAFT_DETAILS_ModelId",
+ table: "AIRCRAFT_DETAILS",
+ column: "ModelId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AIRCRAFT_DETAILS");
+ }
+ }
+}
diff --git a/src/BaseStationReader.Data/Migrations/BaseStationReaderDbContextModelSnapshot.cs b/src/BaseStationReader.Data/Migrations/BaseStationReaderDbContextModelSnapshot.cs
index 96ebf69..68ea383 100644
--- a/src/BaseStationReader.Data/Migrations/BaseStationReaderDbContextModelSnapshot.cs
+++ b/src/BaseStationReader.Data/Migrations/BaseStationReaderDbContextModelSnapshot.cs
@@ -1,5 +1,6 @@
//
using System;
+using System.Diagnostics.CodeAnalysis;
using BaseStationReader.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -10,6 +11,7 @@
namespace BaseStationReader.Data.Migrations
{
[DbContext(typeof(BaseStationReaderDbContext))]
+ [ExcludeFromCodeCoverage]
partial class BaseStationReaderDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
@@ -17,6 +19,111 @@ protected override void BuildModel(ModelBuilder modelBuilder)
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.10");
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.AircraftDetails", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Address");
+
+ b.Property("AirlineId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("AirlineId");
+
+ b.Property("ModelId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("ModelId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AirlineId");
+
+ b.HasIndex("ModelId");
+
+ b.ToTable("AIRCRAFT_DETAILS", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Airline", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("IATA")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("IATA");
+
+ b.Property("ICAO")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ICAO");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.ToTable("AIRLINE", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Manufacturer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.ToTable("MANUFACTURER", (string)null);
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Model", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("Id");
+
+ b.Property("IATA")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("IATA");
+
+ b.Property("ICAO")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ICAO");
+
+ b.Property("ManufacturerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("Name");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ManufacturerId");
+
+ b.ToTable("MODEL", (string)null);
+ });
+
modelBuilder.Entity("BaseStationReader.Entities.Tracking.Aircraft", b =>
{
b.Property("Id")
@@ -118,8 +225,57 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasKey("Id");
+ b.HasIndex("AircraftId");
+
b.ToTable("AIRCRAFT_POSITION", (string)null);
});
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.AircraftDetails", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Lookup.Airline", "Airline")
+ .WithMany()
+ .HasForeignKey("AirlineId");
+
+ b.HasOne("BaseStationReader.Entities.Lookup.Model", "Model")
+ .WithMany()
+ .HasForeignKey("ModelId");
+
+ b.Navigation("Airline");
+
+ b.Navigation("Model");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Model", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Lookup.Manufacturer", "Manufacturer")
+ .WithMany("Models")
+ .HasForeignKey("ManufacturerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Manufacturer");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.AircraftPosition", b =>
+ {
+ b.HasOne("BaseStationReader.Entities.Tracking.Aircraft", "Aircraft")
+ .WithMany("Positions")
+ .HasForeignKey("AircraftId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Aircraft");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Lookup.Manufacturer", b =>
+ {
+ b.Navigation("Models");
+ });
+
+ modelBuilder.Entity("BaseStationReader.Entities.Tracking.Aircraft", b =>
+ {
+ b.Navigation("Positions");
+ });
#pragma warning restore 612, 618
}
}
diff --git a/src/BaseStationReader.Entities/BaseStationReader.Entities.csproj b/src/BaseStationReader.Entities/BaseStationReader.Entities.csproj
index 4b8ada0..1632f3d 100644
--- a/src/BaseStationReader.Entities/BaseStationReader.Entities.csproj
+++ b/src/BaseStationReader.Entities/BaseStationReader.Entities.csproj
@@ -5,7 +5,7 @@
enable
enable
BaseStationReader.Entities
- 1.27.0.0
+ 1.28.0.0
Dave Walker
Copyright (c) Dave Walker 2023
Dave Walker
@@ -17,7 +17,7 @@
https://github.com/davewalker5/ADS-B-BaseStationReader
MIT
false
- 1.27.0.0
+ 1.28.0.0
diff --git a/src/BaseStationReader.Entities/Config/ApiEndpoint.cs b/src/BaseStationReader.Entities/Config/ApiEndpoint.cs
new file mode 100644
index 0000000..cca843f
--- /dev/null
+++ b/src/BaseStationReader.Entities/Config/ApiEndpoint.cs
@@ -0,0 +1,12 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Entities.Config
+{
+ [ExcludeFromCodeCoverage]
+ public class ApiEndpoint
+ {
+ public ApiEndpointType EndpointType { get; set; }
+ public ApiServiceType Service { get; set; }
+ public string Url { get; set; } = "";
+ }
+}
diff --git a/src/BaseStationReader.Entities/Config/ApiEndpointType.cs b/src/BaseStationReader.Entities/Config/ApiEndpointType.cs
new file mode 100644
index 0000000..e6f18f3
--- /dev/null
+++ b/src/BaseStationReader.Entities/Config/ApiEndpointType.cs
@@ -0,0 +1,8 @@
+namespace BaseStationReader.Entities.Config
+{
+ public enum ApiEndpointType
+ {
+ Airlines,
+ Aircraft
+ }
+}
diff --git a/src/BaseStationReader.Entities/Config/ApiServiceKey.cs b/src/BaseStationReader.Entities/Config/ApiServiceKey.cs
new file mode 100644
index 0000000..10a395c
--- /dev/null
+++ b/src/BaseStationReader.Entities/Config/ApiServiceKey.cs
@@ -0,0 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Entities.Config
+{
+ [ExcludeFromCodeCoverage]
+ public class ApiServiceKey
+ {
+ public ApiServiceType Service { get; set; }
+ public string Key { get; set; } = "";
+ }
+}
diff --git a/src/BaseStationReader.Entities/Config/ApiServiceType.cs b/src/BaseStationReader.Entities/Config/ApiServiceType.cs
new file mode 100644
index 0000000..17fce2c
--- /dev/null
+++ b/src/BaseStationReader.Entities/Config/ApiServiceType.cs
@@ -0,0 +1,7 @@
+namespace BaseStationReader.Entities.Config
+{
+ public enum ApiServiceType
+ {
+ AirLabs
+ }
+}
diff --git a/src/BaseStationReader.Entities/Config/TrackerApplicationSettings.cs b/src/BaseStationReader.Entities/Config/TrackerApplicationSettings.cs
index 42686db..515f0c1 100644
--- a/src/BaseStationReader.Entities/Config/TrackerApplicationSettings.cs
+++ b/src/BaseStationReader.Entities/Config/TrackerApplicationSettings.cs
@@ -23,6 +23,8 @@ public class TrackerApplicationSettings
public int MaximumRows { get; set; }
public double? ReceiverLatitude { get; set; }
public double? ReceiverLongitude { get; set; }
+ public List ApiEndpoints { get; set; } = new List();
+ public List ApiServiceKeys { get; set; } = new List();
public List Columns { get; set; } = new List();
}
}
\ No newline at end of file
diff --git a/src/BaseStationReader.Entities/Interfaces/IAircraftApi.cs b/src/BaseStationReader.Entities/Interfaces/IAircraftApi.cs
new file mode 100644
index 0000000..7fae336
--- /dev/null
+++ b/src/BaseStationReader.Entities/Interfaces/IAircraftApi.cs
@@ -0,0 +1,10 @@
+using BaseStationReader.Entities.Lookup;
+using BaseStationReader.Entities.Tracking;
+
+namespace BaseStationReader.Entities.Interfaces
+{
+ public interface IAircraftApi
+ {
+ Task?> LookupAircraft(string address);
+ }
+}
diff --git a/src/BaseStationReader.Entities/Interfaces/IAircraftDetailsManager.cs b/src/BaseStationReader.Entities/Interfaces/IAircraftDetailsManager.cs
new file mode 100644
index 0000000..ad9080e
--- /dev/null
+++ b/src/BaseStationReader.Entities/Interfaces/IAircraftDetailsManager.cs
@@ -0,0 +1,12 @@
+using BaseStationReader.Entities.Lookup;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Entities.Interfaces
+{
+ public interface IAircraftDetailsManager
+ {
+ Task AddAsync(string address, int? airlineId, int? modelId);
+ Task GetAsync(Expression> predicate);
+ Task> ListAsync(Expression> predicate);
+ }
+}
\ No newline at end of file
diff --git a/src/BaseStationReader.Entities/Interfaces/IAirlineManager.cs b/src/BaseStationReader.Entities/Interfaces/IAirlineManager.cs
new file mode 100644
index 0000000..a254f77
--- /dev/null
+++ b/src/BaseStationReader.Entities/Interfaces/IAirlineManager.cs
@@ -0,0 +1,12 @@
+using BaseStationReader.Entities.Lookup;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Entities.Interfaces
+{
+ public interface IAirlineManager
+ {
+ Task AddAsync(string iata, string icao, string name);
+ Task GetAsync(Expression> predicate);
+ Task> ListAsync(Expression> predicate);
+ }
+}
\ No newline at end of file
diff --git a/src/BaseStationReader.Entities/Interfaces/IAirlinesApi.cs b/src/BaseStationReader.Entities/Interfaces/IAirlinesApi.cs
new file mode 100644
index 0000000..ed605a2
--- /dev/null
+++ b/src/BaseStationReader.Entities/Interfaces/IAirlinesApi.cs
@@ -0,0 +1,11 @@
+using BaseStationReader.Entities.Lookup;
+using BaseStationReader.Entities.Tracking;
+
+namespace BaseStationReader.Entities.Interfaces
+{
+ public interface IAirlinesApi
+ {
+ Task?> LookupAirlineByIATACode(string iata);
+ Task?> LookupAirlineByICAOCode(string icao);
+ }
+}
\ No newline at end of file
diff --git a/src/BaseStationReader.Entities/Interfaces/IManufacturerManager.cs b/src/BaseStationReader.Entities/Interfaces/IManufacturerManager.cs
new file mode 100644
index 0000000..50cfbd8
--- /dev/null
+++ b/src/BaseStationReader.Entities/Interfaces/IManufacturerManager.cs
@@ -0,0 +1,12 @@
+using BaseStationReader.Entities.Lookup;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Entities.Interfaces
+{
+ public interface IManufacturerManager
+ {
+ Task AddAsync(string name);
+ Task GetAsync(Expression> predicate);
+ Task> ListAsync(Expression> predicate);
+ }
+}
\ No newline at end of file
diff --git a/src/BaseStationReader.Entities/Interfaces/IModelManager.cs b/src/BaseStationReader.Entities/Interfaces/IModelManager.cs
new file mode 100644
index 0000000..0388a20
--- /dev/null
+++ b/src/BaseStationReader.Entities/Interfaces/IModelManager.cs
@@ -0,0 +1,12 @@
+using BaseStationReader.Entities.Lookup;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Entities.Interfaces
+{
+ public interface IModelManager
+ {
+ Task GetAsync(Expression> predicate);
+ Task> ListAsync(Expression> predicate);
+ Task AddAsync(string iata, string icao, string name, int manufacturerId);
+ }
+}
\ No newline at end of file
diff --git a/src/BaseStationReader.Entities/Lookup/AircraftDetails.cs b/src/BaseStationReader.Entities/Lookup/AircraftDetails.cs
new file mode 100644
index 0000000..7e385fa
--- /dev/null
+++ b/src/BaseStationReader.Entities/Lookup/AircraftDetails.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Entities.Lookup
+{
+ [ExcludeFromCodeCoverage]
+ public class AircraftDetails
+ {
+ [Key]
+ public int Id { get; set; }
+
+ [Required]
+ public string Address { get; set; } = "";
+
+ public int? ModelId { get; set; }
+ public int? AirlineId { get; set; }
+
+ public Model? Model { get; set; }
+ public Airline? Airline { get; set; }
+ }
+}
diff --git a/src/BaseStationReader.Entities/Lookup/Airline.cs b/src/BaseStationReader.Entities/Lookup/Airline.cs
new file mode 100644
index 0000000..a2de59f
--- /dev/null
+++ b/src/BaseStationReader.Entities/Lookup/Airline.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Entities.Lookup
+{
+ [ExcludeFromCodeCoverage]
+ public class Airline
+ {
+ [Key]
+ public int Id { get; set; }
+
+ [Required]
+ public string IATA { get; set; } = "";
+
+ [Required]
+ public string ICAO { get; set; } = "";
+
+ [Required]
+ public string Name { get; set; } = "";
+ }
+}
diff --git a/src/BaseStationReader.Entities/Lookup/Manufacturer.cs b/src/BaseStationReader.Entities/Lookup/Manufacturer.cs
new file mode 100644
index 0000000..ad65c4d
--- /dev/null
+++ b/src/BaseStationReader.Entities/Lookup/Manufacturer.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Entities.Lookup
+{
+ [ExcludeFromCodeCoverage]
+ public class Manufacturer
+ {
+ [Key]
+ public int Id { get; set; }
+
+ [Required]
+ public string Name { get; set; } = "";
+
+#pragma warning disable CS8618
+ public ICollection Models { get; set; }
+#pragma warning restore CS8618
+ }
+}
diff --git a/src/BaseStationReader.Entities/Lookup/Model.cs b/src/BaseStationReader.Entities/Lookup/Model.cs
new file mode 100644
index 0000000..c7af2ce
--- /dev/null
+++ b/src/BaseStationReader.Entities/Lookup/Model.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Entities.Lookup
+{
+ [ExcludeFromCodeCoverage]
+ public class Model
+ {
+ [Key]
+ public int Id { get; set; }
+
+ [Required]
+ public int ManufacturerId { get; set; }
+
+ [Required]
+ public string IATA { get; set; } = "";
+
+ [Required]
+ public string ICAO { get; set; } = "";
+
+ [Required]
+ public string Name { get; set; } = "";
+
+#pragma warning disable CS8618
+ public Manufacturer Manufacturer { get; set; }
+#pragma warning restore CS8618
+ }
+}
diff --git a/src/BaseStationReader.Entities/Tracking/Aircraft.cs b/src/BaseStationReader.Entities/Tracking/Aircraft.cs
index 26a74af..55f101a 100644
--- a/src/BaseStationReader.Entities/Tracking/Aircraft.cs
+++ b/src/BaseStationReader.Entities/Tracking/Aircraft.cs
@@ -55,6 +55,10 @@ public class Aircraft : ICloneable
[Required]
public TrackingStatus Status { get; set; }
+#pragma warning disable CS8618
+ public ICollection Positions { get; set; }
+#pragma warning restore CS8618
+
public object Clone()
{
return MemberwiseClone();
diff --git a/src/BaseStationReader.Entities/Tracking/AircraftPosition.cs b/src/BaseStationReader.Entities/Tracking/AircraftPosition.cs
index 371064d..c1017ea 100644
--- a/src/BaseStationReader.Entities/Tracking/AircraftPosition.cs
+++ b/src/BaseStationReader.Entities/Tracking/AircraftPosition.cs
@@ -30,5 +30,9 @@ public class AircraftPosition
[Export("Timestamp", 6)]
public DateTime Timestamp { get; set; }
+
+#pragma warning disable CS8618
+ public Aircraft Aircraft { get; set; }
+#pragma warning restore CS8618
}
}
diff --git a/src/BaseStationReader.Entities/Tracking/ApiProperty.cs b/src/BaseStationReader.Entities/Tracking/ApiProperty.cs
new file mode 100644
index 0000000..eb86be5
--- /dev/null
+++ b/src/BaseStationReader.Entities/Tracking/ApiProperty.cs
@@ -0,0 +1,12 @@
+namespace BaseStationReader.Entities.Tracking
+{
+ public enum ApiProperty
+ {
+ AirlineIATA,
+ AirlineICAO,
+ AirlineName,
+ ManufacturerName,
+ ModelIATA,
+ ModelICAO
+ }
+}
diff --git a/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAircraftApi.cs b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAircraftApi.cs
new file mode 100644
index 0000000..7fe25c7
--- /dev/null
+++ b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAircraftApi.cs
@@ -0,0 +1,66 @@
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Entities.Tracking;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Logic.Api.AirLabs
+{
+ [ExcludeFromCodeCoverage]
+ public class AirLabsAircraftApi : ExternalApiBase, IAircraftApi
+ {
+ private readonly string _baseAddress;
+
+ public AirLabsAircraftApi(string url, string key)
+ {
+ _baseAddress = $"{url}?api_key={key}";
+ }
+
+ ///
+ /// Lookup an aircraft's details using its ICAO 24-bit address
+ ///
+ ///
+ ///
+ public async Task?> LookupAircraft(string address)
+ {
+ return await MakeApiRequest($"&hex={address}");
+ }
+
+ ///
+ /// Make a request to the specified URL
+ ///
+ ///
+ ///
+ private async Task?> MakeApiRequest(string parameters)
+ {
+ Dictionary? properties = null;
+
+ // Make a request for the data from the API
+ var url = $"{_baseAddress}{parameters}";
+ var node = await SendRequest(url);
+
+ if (node != null)
+ {
+ try
+ {
+ // Extract the response element from the JSON DOM
+ var apiResponse = node!["response"]![0];
+
+ // Extract the values into a dictionary
+ properties = new()
+ {
+ { ApiProperty.AirlineIATA, apiResponse!["airline_iata"]?.GetValue() ?? "" },
+ { ApiProperty.AirlineICAO, apiResponse!["airline_icao"]?.GetValue() ?? "" },
+ { ApiProperty.ManufacturerName, apiResponse!["manufacturer"]?.GetValue() ?? "" },
+ { ApiProperty.ModelIATA, apiResponse!["iata"]?.GetValue() ?? "" },
+ { ApiProperty.ModelICAO, apiResponse!["icao"]?.GetValue() ?? "" }
+ };
+ }
+ catch
+ {
+ properties = null;
+ }
+ }
+
+ return properties;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAirlinesApi.cs b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAirlinesApi.cs
new file mode 100644
index 0000000..10467f5
--- /dev/null
+++ b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAirlinesApi.cs
@@ -0,0 +1,74 @@
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Entities.Tracking;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Logic.Api.AirLabs
+{
+ [ExcludeFromCodeCoverage]
+ public class AirLabsAirlinesApi : ExternalApiBase, IAirlinesApi
+ {
+ private readonly string _baseAddress;
+
+ public AirLabsAirlinesApi(string url, string key)
+ {
+ _baseAddress = $"{url}?api_key={key}";
+ }
+
+ ///
+ /// Lookup an airline using its IATA code
+ ///
+ ///
+ ///
+ public async Task?> LookupAirlineByIATACode(string iata)
+ {
+ return await MakeApiRequest($"&iata_code={iata}");
+ }
+
+ ///
+ /// Lookup an airline using it's ICAO code
+ ///
+ ///
+ ///
+ public async Task?> LookupAirlineByICAOCode(string icao)
+ {
+ return await MakeApiRequest($"&icao_code={icao}");
+ }
+
+ ///
+ /// Make a request to the specified URL and return the response properties as a dictionary
+ ///
+ ///
+ ///
+ private async Task?> MakeApiRequest(string parameters)
+ {
+ Dictionary? properties = null;
+
+ // Make a request for the data from the API
+ var url = $"{_baseAddress}{parameters}";
+ var node = await SendRequest(url);
+
+ if (node != null)
+ {
+ try
+ {
+ // Extract the response element from the JSON DOM
+ var apiResponse = node!["response"]![0];
+
+ // Extract the values into a dictionary
+ properties = new()
+ {
+ { ApiProperty.AirlineIATA, apiResponse!["iata_code"]!.GetValue() },
+ { ApiProperty.AirlineICAO, apiResponse!["icao_code"]!.GetValue() },
+ { ApiProperty.AirlineName, apiResponse!["name"]!.GetValue() },
+ };
+ }
+ catch
+ {
+ properties = null;
+ }
+ }
+
+ return properties;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Logic/Api/ExternalApiBase.cs b/src/BaseStationReader.Logic/Api/ExternalApiBase.cs
new file mode 100644
index 0000000..92dd315
--- /dev/null
+++ b/src/BaseStationReader.Logic/Api/ExternalApiBase.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Nodes;
+
+namespace BaseStationReader.Logic.Api
+{
+ [ExcludeFromCodeCoverage]
+ public abstract class ExternalApiBase
+ {
+ protected readonly HttpClient _client = new();
+
+ ///
+ /// Make a request to the specified URL and return the response properties as a dictionary
+ ///
+ ///
+ ///
+ protected async Task SendRequest(string endpoint)
+ {
+ JsonNode? node = null;
+
+ // Make a request for the data from the API
+ using (var response = await _client.GetAsync(endpoint))
+ {
+ // Check the request was successful
+ if (response.IsSuccessStatusCode)
+ {
+ try
+ {
+ // Read the response, parse to a JSON DOM
+ var json = await response.Content.ReadAsStringAsync();
+ node = JsonNode.Parse(json);
+ }
+ catch
+ {
+ node = null;
+ }
+ }
+ }
+
+ return node;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Logic/BaseStationReader.Logic.csproj b/src/BaseStationReader.Logic/BaseStationReader.Logic.csproj
index 0abfbed..5f895e1 100644
--- a/src/BaseStationReader.Logic/BaseStationReader.Logic.csproj
+++ b/src/BaseStationReader.Logic/BaseStationReader.Logic.csproj
@@ -5,7 +5,7 @@
enable
enable
BaseStationReader.Logic
- 1.27.0.0
+ 1.28.0.0
Dave Walker
Copyright (c) Dave Walker 2023
Dave Walker
@@ -17,7 +17,7 @@
https://github.com/davewalker5/ADS-B-BaseStationReader
MIT
false
- 1.27.0.0
+ 1.28.0.0
diff --git a/src/BaseStationReader.Logic/BaseStationReader.Logic.sln b/src/BaseStationReader.Logic/BaseStationReader.Logic.sln
new file mode 100644
index 0000000..579edd7
--- /dev/null
+++ b/src/BaseStationReader.Logic/BaseStationReader.Logic.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BaseStationReader.Logic", "BaseStationReader.Logic.csproj", "{ACB44E4B-16E9-4ADB-BD4B-9C0B7E3460D5}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {ACB44E4B-16E9-4ADB-BD4B-9C0B7E3460D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ACB44E4B-16E9-4ADB-BD4B-9C0B7E3460D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ACB44E4B-16E9-4ADB-BD4B-9C0B7E3460D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ACB44E4B-16E9-4ADB-BD4B-9C0B7E3460D5}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {125239F8-1241-4183-85F0-1AEDE0DD248B}
+ EndGlobalSection
+EndGlobal
diff --git a/src/BaseStationReader.Logic/Database/AircraftDetailsManager.cs b/src/BaseStationReader.Logic/Database/AircraftDetailsManager.cs
new file mode 100644
index 0000000..f41e6f0
--- /dev/null
+++ b/src/BaseStationReader.Logic/Database/AircraftDetailsManager.cs
@@ -0,0 +1,68 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Entities.Lookup;
+using Microsoft.EntityFrameworkCore;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Logic.Database
+{
+ public class AircraftDetailsManager : IAircraftDetailsManager
+ {
+ private readonly BaseStationReaderDbContext _context;
+
+ public AircraftDetailsManager(BaseStationReaderDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Return the first set of details matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task GetAsync(Expression> predicate)
+ {
+ List details = await ListAsync(predicate);
+
+#pragma warning disable CS8603
+ return details.FirstOrDefault();
+#pragma warning restore CS8603
+ }
+
+ ///
+ /// Return all details matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task> ListAsync(Expression> predicate)
+#pragma warning disable CS8602
+ => await _context.AircraftDetails
+ .Where(predicate)
+ .Include(a => a.Airline)
+ .Include(a => a.Model)
+ .ThenInclude(m => m.Manufacturer)
+ .ToListAsync();
+#pragma warning restore CS8602
+
+ ///
+ /// Add a set of details, if the associated ICAO address doesn't already exist
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task AddAsync(string address, int? airlineId, int? modelId)
+ {
+ var details = await GetAsync(a => a.Address == address);
+
+ if (details == null)
+ {
+ details = new AircraftDetails { Address = address, AirlineId = airlineId, ModelId = modelId };
+ await _context.AircraftDetails.AddAsync(details);
+ await _context.SaveChangesAsync();
+ }
+
+ return details;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Logic/Database/AircraftWriter.cs b/src/BaseStationReader.Logic/Database/AircraftWriter.cs
index cdd0672..e5c5272 100644
--- a/src/BaseStationReader.Logic/Database/AircraftWriter.cs
+++ b/src/BaseStationReader.Logic/Database/AircraftWriter.cs
@@ -27,7 +27,7 @@ public AircraftWriter(BaseStationReaderDbContext context)
///
public async Task GetAsync(Expression> predicate)
{
- List aircraft = await _context.Aircraft.Where(predicate).ToListAsync();
+ List aircraft = await ListAsync(predicate);
#pragma warning disable CS8603
return aircraft.FirstOrDefault();
#pragma warning restore CS8603
diff --git a/src/BaseStationReader.Logic/Database/AirlineManager.cs b/src/BaseStationReader.Logic/Database/AirlineManager.cs
new file mode 100644
index 0000000..e2d22fe
--- /dev/null
+++ b/src/BaseStationReader.Logic/Database/AirlineManager.cs
@@ -0,0 +1,61 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Entities.Lookup;
+using Microsoft.EntityFrameworkCore;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Logic.Database
+{
+ public class AirlineManager : IAirlineManager
+ {
+ private readonly BaseStationReaderDbContext _context;
+
+ public AirlineManager(BaseStationReaderDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Return the first airline matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task GetAsync(Expression> predicate)
+ {
+ List airlines = await ListAsync(predicate);
+
+#pragma warning disable CS8603
+ return airlines.FirstOrDefault();
+#pragma warning restore CS8603
+ }
+
+ ///
+ /// Return all airlines matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task> ListAsync(Expression> predicate)
+ => await _context.Airlines.Where(predicate).ToListAsync();
+
+ ///
+ /// Add an airline, if it doesn't already exist
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task AddAsync(string iata, string icao, string name)
+ {
+ var airline = await GetAsync(a => (a.IATA == iata) || (a.ICAO == icao) || (a.Name == name));
+
+ if (airline == null)
+ {
+ airline = new Airline { IATA = iata, ICAO = icao, Name = name };
+ await _context.Airlines.AddAsync(airline);
+ await _context.SaveChangesAsync();
+ }
+
+ return airline;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Logic/Database/ManufacturerManager.cs b/src/BaseStationReader.Logic/Database/ManufacturerManager.cs
new file mode 100644
index 0000000..ba79918
--- /dev/null
+++ b/src/BaseStationReader.Logic/Database/ManufacturerManager.cs
@@ -0,0 +1,59 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Entities.Lookup;
+using Microsoft.EntityFrameworkCore;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Logic.Database
+{
+ public class ManufacturerManager : IManufacturerManager
+ {
+ private readonly BaseStationReaderDbContext _context;
+
+ public ManufacturerManager(BaseStationReaderDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Return the first manufacturer matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task GetAsync(Expression> predicate)
+ {
+ List manufacturers = await ListAsync(predicate);
+
+#pragma warning disable CS8603
+ return manufacturers.FirstOrDefault();
+#pragma warning restore CS8603
+ }
+
+ ///
+ /// Return all manufacturers matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task> ListAsync(Expression> predicate)
+ => await _context.Manufacturers.Where(predicate).ToListAsync();
+
+ ///
+ /// Add a manufacturer, if it doesn't already exist
+ ///
+ ///
+ ///
+ public async Task AddAsync(string name)
+ {
+ var manufacturer = await GetAsync(a => a.Name == name);
+
+ if (manufacturer == null)
+ {
+ manufacturer = new Manufacturer { Name = name };
+ await _context.Manufacturers.AddAsync(manufacturer);
+ await _context.SaveChangesAsync();
+ }
+
+ return manufacturer;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Logic/Database/ModelManager.cs b/src/BaseStationReader.Logic/Database/ModelManager.cs
new file mode 100644
index 0000000..bcce7a7
--- /dev/null
+++ b/src/BaseStationReader.Logic/Database/ModelManager.cs
@@ -0,0 +1,68 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Entities.Lookup;
+using Microsoft.EntityFrameworkCore;
+using System.Linq.Expressions;
+
+namespace BaseStationReader.Logic.Database
+{
+ public class ModelManager : IModelManager
+ {
+ private readonly BaseStationReaderDbContext _context;
+
+ public ModelManager(BaseStationReaderDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Get the first aircraft model matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task GetAsync(Expression> predicate)
+ {
+ List models = await ListAsync(predicate);
+
+#pragma warning disable CS8603
+ return models.FirstOrDefault();
+#pragma warning restore CS8603
+ }
+
+ ///
+ /// Return all aircraft models matching the specified criteria
+ ///
+ ///
+ ///
+ public async Task> ListAsync(Expression> predicate)
+ => await _context
+ .Models
+ .Where(predicate)
+ .Include(x => x.Manufacturer)
+ .ToListAsync();
+
+ ///
+ /// Add a new model to the database
+ ///
+ ///
+ ///
+ public async Task AddAsync(string iata, string icao, string name, int manufacturerId)
+ {
+ var model = await GetAsync(x => (x.IATA == iata) || (x.ICAO == icao) || (x.Name == name));
+ if (model == null)
+ {
+ model = new Model
+ {
+ IATA = iata,
+ ICAO = icao,
+ Name = name,
+ ManufacturerId = manufacturerId
+ };
+ await _context.Models.AddAsync(model);
+ await _context.SaveChangesAsync();
+ }
+
+ return model;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Logic/Database/PositionWriter.cs b/src/BaseStationReader.Logic/Database/PositionWriter.cs
index 2de0c4b..6d61e2c 100644
--- a/src/BaseStationReader.Logic/Database/PositionWriter.cs
+++ b/src/BaseStationReader.Logic/Database/PositionWriter.cs
@@ -27,7 +27,7 @@ public PositionWriter(BaseStationReaderDbContext context)
///
public async Task GetAsync(Expression> predicate)
{
- List aircraft = await _context.AircraftPositions.Where(predicate).ToListAsync();
+ List aircraft = await ListAsync(predicate);
#pragma warning disable CS8603
return aircraft.FirstOrDefault();
#pragma warning restore CS8603
diff --git a/src/BaseStationReader.Logic/Tracking/AircraftLookupManager.cs b/src/BaseStationReader.Logic/Tracking/AircraftLookupManager.cs
new file mode 100644
index 0000000..9bf9826
--- /dev/null
+++ b/src/BaseStationReader.Logic/Tracking/AircraftLookupManager.cs
@@ -0,0 +1,132 @@
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Entities.Lookup;
+using BaseStationReader.Entities.Tracking;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BaseStationReader.Logic.Tracking
+{
+ [ExcludeFromCodeCoverage]
+ public class AircraftLookupManager
+ {
+ private readonly IAirlineManager _airlineManager;
+ private readonly IAircraftDetailsManager _detailsManager;
+ private readonly IModelManager _modelManager;
+ private readonly IAirlinesApi _airlinesApi;
+ private readonly IAircraftApi _aircraftApi;
+
+ public AircraftLookupManager(
+ IAirlineManager airlineManager,
+ IAircraftDetailsManager detailsManager,
+ IModelManager modelManager,
+ IAirlinesApi airlinesApi,
+ IAircraftApi aircraftApi)
+ {
+ _airlineManager = airlineManager;
+ _detailsManager = detailsManager;
+ _modelManager = modelManager;
+ _airlinesApi = airlinesApi;
+ _aircraftApi = aircraftApi;
+ }
+
+ ///
+ /// Lookup an aircraft's details given its ICAO 24-bit address
+ ///
+ ///
+ ///
+ public async Task LookupAircraft(string address)
+ {
+ // See if the details are locally cached, first
+ var details = await _detailsManager!.GetAsync(x => x.Address == address);
+ if (details == null)
+ {
+ // Not locally cached, so request a set of properties via the aircraft API
+ var properties = await _aircraftApi!.LookupAircraft(address);
+ if (properties != null)
+ {
+ // Retrieve the model
+ var model = await GetModel(properties[ApiProperty.ModelIATA], properties[ApiProperty.ModelICAO]);
+
+ // If we don't have model details, there's no point caching the aircraft details
+ // locally, so check we have a model
+ if (model != null)
+ {
+ // Get the airline details
+ var airline = await GetAirlineFromResponse(properties[ApiProperty.AirlineIATA], properties[ApiProperty.AirlineICAO]);
+
+ // Add a new aircraft details record to the local database
+ details = await _detailsManager.AddAsync(address, airline?.Id, model.Id);
+ }
+ }
+
+ }
+
+ return details;
+ }
+
+ ///
+ /// Retrieve the model given the IATA and ICAO codes
+ ///
+ ///
+ ///
+ ///
+ private async Task GetModel(string iata, string icao)
+ {
+ // Look for a match for both the IATA and ICAO codes
+ Model? model = await _modelManager!.GetAsync(x => (x.IATA == iata) && (x.ICAO == icao));
+
+ // See if there's a match? If not, use the IATA code alone. This provides more granularity
+ // than the ICAO code alone. For example, there are multiple aircraft models with ICAO
+ // designation B738, but each has a different IATA code
+ if ((model == null) && !string.IsNullOrEmpty(iata))
+ {
+ model = await _modelManager!.GetAsync(x => x.IATA == iata);
+ }
+
+ // See if there's a match? If not, fallback to using the ICAO code alone
+ if ((model == null) && !string.IsNullOrEmpty(icao))
+ {
+ model = await _modelManager!.GetAsync(x => x.ICAO == icao);
+ }
+
+ return model;
+ }
+
+ ///
+ /// Get an airline instance with the properties returned by the API
+ ///
+ ///
+ ///
+ ///
+ private async Task GetAirlineFromResponse(string iata, string icao)
+ {
+ // See if the airline has been cached locally
+ Airline? airline = await _airlineManager!.GetAsync(x => (x.IATA == iata) || (x.ICAO == icao));
+ if (airline == null)
+ {
+ // Not cached locally, so look the airline up using the API, either using the ICAO code or IATA
+ // code, whichever is valid
+ Dictionary? properties = null;
+ if (!string.IsNullOrEmpty(icao))
+ {
+ properties = await _airlinesApi!.LookupAirlineByICAOCode(icao);
+ }
+ else if (!string.IsNullOrEmpty(iata))
+ {
+ properties = await _airlinesApi!.LookupAirlineByIATACode(iata);
+ }
+
+ // Check we have some airline properties
+ if (properties != null)
+ {
+ // Lookup has worked, so cache the airline in the local database
+ airline = await _airlineManager.AddAsync(
+ properties[ApiProperty.AirlineIATA],
+ properties[ApiProperty.AirlineICAO],
+ properties[ApiProperty.AirlineName]);
+ }
+ }
+
+ return airline;
+ }
+ }
+}
diff --git a/src/BaseStationReader.Simulator/BaseStationReader.Simulator.csproj b/src/BaseStationReader.Simulator/BaseStationReader.Simulator.csproj
index 8981cd0..3a45a7b 100644
--- a/src/BaseStationReader.Simulator/BaseStationReader.Simulator.csproj
+++ b/src/BaseStationReader.Simulator/BaseStationReader.Simulator.csproj
@@ -3,9 +3,9 @@
Exe
net7.0
- 1.27.0.0
- 1.27.0.0
- 1.27.0
+ 1.28.0.0
+ 1.28.0.0
+ 1.28.0
enable
enable
diff --git a/src/BaseStationReader.Terminal/BaseStationReader.Terminal.csproj b/src/BaseStationReader.Terminal/BaseStationReader.Terminal.csproj
index 1db8dbc..a486b84 100644
--- a/src/BaseStationReader.Terminal/BaseStationReader.Terminal.csproj
+++ b/src/BaseStationReader.Terminal/BaseStationReader.Terminal.csproj
@@ -3,9 +3,9 @@
Exe
net7.0
- 1.27.0.0
- 1.27.0.0
- 1.27.0
+ 1.28.0.0
+ 1.28.0.0
+ 1.28.0
enable
enable
diff --git a/src/BaseStationReader.Tests/AircraftDetailsTest.cs b/src/BaseStationReader.Tests/AircraftDetailsTest.cs
new file mode 100644
index 0000000..e3395ce
--- /dev/null
+++ b/src/BaseStationReader.Tests/AircraftDetailsTest.cs
@@ -0,0 +1,69 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Logic.Database;
+using DocumentFormat.OpenXml.ExtendedProperties;
+
+namespace BaseStationReader.Tests
+{
+ [TestClass]
+ public class AircraftDetailsTest
+ {
+ private readonly string Address = new Random().Next(0, 16777215).ToString("X6");
+ private const string Manufacturer = "Airbus";
+ private const string AirlineIATA = "BA";
+ private const string AirlineICAO = "BAW";
+ private const string Airline = "British Airways";
+ private const string ModelIATA = "332";
+ private const string ModelICAO = "A332";
+ private const string ModelName = "A330-200";
+
+ private IAircraftDetailsManager? _manager = null;
+
+ [TestInitialize]
+ public void Initialise()
+ {
+ BaseStationReaderDbContext context = BaseStationReaderDbContextFactory.CreateInMemoryDbContext();
+
+ // Set up a manufacturer
+ var manufacturerManager = new ManufacturerManager(context);
+ var manufacturerId = Task.Run(() => manufacturerManager.AddAsync(Manufacturer)).Result.Id;
+
+ // Set up an airline
+ var airlineManager = new AirlineManager(context);
+ var airlineId = Task.Run(() => airlineManager.AddAsync(AirlineIATA, AirlineICAO, Airline)).Result.Id;
+
+ // Add an aircraft model
+ var modelManager = new ModelManager(context);
+ var modelId = Task.Run(() => modelManager.AddAsync(ModelIATA, ModelICAO, ModelName, manufacturerId)).Result.Id;
+
+ // Set up a details record
+ _manager = new AircraftDetailsManager(context);
+ Task.Run(() => _manager.AddAsync(Address, airlineId, modelId)).Wait();
+ }
+
+ [TestMethod]
+ public async Task AddDuplicateTest()
+ {
+ await _manager!.AddAsync(Address, null, null);
+ var details = await _manager.ListAsync(x => true);
+ Assert.AreEqual(1, details.Count);
+ }
+
+ [TestMethod]
+ public async Task AddAndGetTest()
+ {
+ var details = await _manager!.GetAsync(a => a.Address == Address);
+ Assert.IsNotNull(details);
+ Assert.IsTrue(details.Id > 0);
+ Assert.AreEqual(Address, details.Address);
+ Assert.AreEqual(Airline, details.Airline!.Name);
+ Assert.AreEqual(AirlineIATA, details.Airline!.IATA);
+ Assert.AreEqual(AirlineICAO, details.Airline!.ICAO);
+ Assert.AreEqual(ModelIATA, details.Model!.IATA);
+ Assert.AreEqual(ModelICAO, details.Model!.ICAO);
+ Assert.AreEqual(ModelName, details.Model!.Name);
+ Assert.AreEqual(Manufacturer, details.Model!.Manufacturer.Name);
+ }
+ }
+}
+
diff --git a/src/BaseStationReader.Tests/AirlineManagerTest.cs b/src/BaseStationReader.Tests/AirlineManagerTest.cs
new file mode 100644
index 0000000..478759e
--- /dev/null
+++ b/src/BaseStationReader.Tests/AirlineManagerTest.cs
@@ -0,0 +1,105 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Logic.Database;
+
+namespace BaseStationReader.Tests
+{
+ [TestClass]
+ public class AirlineManagerTest
+ {
+ private const string IATA = "BA";
+ private const string ICAO = "BAW";
+ private const string Name = "British Airways";
+
+ private IAirlineManager? _manager = null;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ BaseStationReaderDbContext context = BaseStationReaderDbContextFactory.CreateInMemoryDbContext();
+ _manager = new AirlineManager(context);
+ Task.Run(() => _manager.AddAsync(IATA, ICAO, Name)).Wait();
+ }
+
+ [TestMethod]
+ public async Task AddDuplicateByIATATest()
+ {
+ await _manager!.AddAsync(IATA, "XX", "Some Other Name");
+ var airlines = await _manager.ListAsync(x => true);
+ Assert.AreEqual(1, airlines.Count);
+ }
+
+ [TestMethod]
+ public async Task AddDuplicateByICAOTest()
+ {
+ await _manager!.AddAsync("XX", ICAO, "Some Other Name");
+ var airlines = await _manager.ListAsync(x => true);
+ Assert.AreEqual(1, airlines.Count);
+ }
+
+ [TestMethod]
+ public async Task AddDuplicateByNameTest()
+ {
+ await _manager!.AddAsync("XX", "XX", Name);
+ var airlines = await _manager.ListAsync(x => true);
+ Assert.AreEqual(1, airlines.Count);
+ }
+
+ [TestMethod]
+ public async Task AddAndGetByIATATest()
+ {
+ var airline = await _manager!.GetAsync(a => a.IATA == IATA);
+ Assert.IsNotNull(airline);
+ Assert.IsTrue(airline.Id > 0);
+ Assert.AreEqual(IATA, airline.IATA);
+ Assert.AreEqual(ICAO, airline.ICAO);
+ Assert.AreEqual(Name, airline.Name);
+ }
+
+ [TestMethod]
+ public async Task AddAndGetByICAOTest()
+ {
+ var airline = await _manager!.GetAsync(a => a.ICAO == ICAO);
+ Assert.IsNotNull(airline);
+ Assert.IsTrue(airline.Id > 0);
+ Assert.AreEqual(IATA, airline.IATA);
+ Assert.AreEqual(ICAO, airline.ICAO);
+ Assert.AreEqual(Name, airline.Name);
+ }
+
+ [TestMethod]
+ public async Task AddAndGetByNameTest()
+ {
+ var airline = await _manager!.GetAsync(a => a.Name == Name);
+ Assert.IsNotNull(airline);
+ Assert.IsTrue(airline.Id > 0);
+ Assert.AreEqual(IATA, airline.IATA);
+ Assert.AreEqual(ICAO, airline.ICAO);
+ Assert.AreEqual(Name, airline.Name);
+ }
+
+ [TestMethod]
+ public async Task GetMissingTest()
+ {
+ var airline = await _manager!.GetAsync(a => a.Name == "Missing");
+ Assert.IsNull(airline);
+ }
+
+ [TestMethod]
+ public async Task ListAllTest()
+ {
+ var airlines = await _manager!.ListAsync(x => true);
+ Assert.AreEqual(1, airlines!.Count);
+ Assert.AreEqual(IATA, airlines.First().IATA);
+ Assert.AreEqual(ICAO, airlines.First().ICAO);
+ Assert.AreEqual(Name, airlines.First().Name);
+ }
+
+ [TestMethod]
+ public async Task ListMissingTest()
+ {
+ var airlines = await _manager!.ListAsync(e => e.Name == "Missing");
+ Assert.AreEqual(0, airlines!.Count);
+ }
+ }
+}
diff --git a/src/BaseStationReader.Tests/ConfigReaderTest.cs b/src/BaseStationReader.Tests/ConfigReaderTest.cs
index b2c38f2..695cead 100644
--- a/src/BaseStationReader.Tests/ConfigReaderTest.cs
+++ b/src/BaseStationReader.Tests/ConfigReaderTest.cs
@@ -1,4 +1,5 @@
-using BaseStationReader.Entities.Logging;
+using BaseStationReader.Entities.Config;
+using BaseStationReader.Entities.Logging;
using BaseStationReader.Logic.Configuration;
namespace BaseStationReader.Tests
@@ -35,6 +36,17 @@ public void ReadAppSettingsTest()
Assert.AreEqual("Lat", settings?.Columns.First().Label);
Assert.AreEqual("N5", settings?.Columns.First().Format);
Assert.AreEqual("Decimal", settings?.Columns.First().TypeName);
+
+ Assert.IsNotNull(settings?.ApiEndpoints);
+ Assert.AreEqual(1, settings?.ApiEndpoints.Count);
+ Assert.AreEqual(ApiEndpointType.Airlines, settings?.ApiEndpoints.First().EndpointType);
+ Assert.AreEqual(ApiServiceType.AirLabs, settings?.ApiEndpoints.First().Service);
+ Assert.AreEqual("https://airlabs.co/api/v9/airlines", settings?.ApiEndpoints.First().Url);
+
+ Assert.IsNotNull(settings?.ApiServiceKeys);
+ Assert.AreEqual(1, settings?.ApiServiceKeys.Count);
+ Assert.AreEqual(ApiServiceType.AirLabs, settings?.ApiServiceKeys.First().Service);
+ Assert.AreEqual("my-key", settings?.ApiServiceKeys.First().Key);
}
}
}
diff --git a/src/BaseStationReader.Tests/ManufacturerManagerTest.cs b/src/BaseStationReader.Tests/ManufacturerManagerTest.cs
new file mode 100644
index 0000000..6a38c33
--- /dev/null
+++ b/src/BaseStationReader.Tests/ManufacturerManagerTest.cs
@@ -0,0 +1,61 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Logic.Database;
+
+namespace BaseStationReader.Tests
+{
+ [TestClass]
+ public class ManufacturerManagerTest
+ {
+ private const string Name = "Airbus";
+
+ private IManufacturerManager? _manager = null;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ BaseStationReaderDbContext context = BaseStationReaderDbContextFactory.CreateInMemoryDbContext();
+ _manager = new ManufacturerManager(context);
+ Task.Run(() => _manager.AddAsync(Name)).Wait();
+ }
+
+ [TestMethod]
+ public async Task AddDuplicateTest()
+ {
+ await _manager!.AddAsync(Name);
+ var manufacturers = await _manager.ListAsync(x => true);
+ Assert.AreEqual(1, manufacturers.Count);
+ }
+
+ [TestMethod]
+ public async Task AddAndGetTest()
+ {
+ var manufacturer = await _manager!.GetAsync(a => a.Name == Name);
+ Assert.IsNotNull(manufacturer);
+ Assert.IsTrue(manufacturer.Id > 0);
+ Assert.AreEqual(Name, manufacturer.Name);
+ }
+
+ [TestMethod]
+ public async Task GetMissingTest()
+ {
+ var manufacturer = await _manager!.GetAsync(a => a.Name == "Missing");
+ Assert.IsNull(manufacturer);
+ }
+
+ [TestMethod]
+ public async Task ListAllTest()
+ {
+ var manufacturers = await _manager!.ListAsync(x => true);
+ Assert.AreEqual(1, manufacturers!.Count);
+ Assert.AreEqual(Name, manufacturers.First().Name);
+ }
+
+ [TestMethod]
+ public async Task ListMissingTest()
+ {
+ var manufacturers = await _manager!.ListAsync(e => e.Name == "Missing");
+ Assert.AreEqual(0, manufacturers!.Count);
+ }
+ }
+}
diff --git a/src/BaseStationReader.Tests/ModelManagerTest.cs b/src/BaseStationReader.Tests/ModelManagerTest.cs
new file mode 100644
index 0000000..b4fc685
--- /dev/null
+++ b/src/BaseStationReader.Tests/ModelManagerTest.cs
@@ -0,0 +1,66 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Interfaces;
+using BaseStationReader.Logic.Database;
+
+namespace BaseStationReader.Tests
+{
+ [TestClass]
+ public class ModelManagerTest
+ {
+ private IModelManager? _manager = null;
+
+ [TestInitialize]
+ public void Initialise()
+ {
+ BaseStationReaderDbContext context = BaseStationReaderDbContextFactory.CreateInMemoryDbContext();
+
+ // Set up a manufacturer
+ var manufacturerManager = new ManufacturerManager(context);
+ var manufacturerId = Task.Run(() => manufacturerManager.AddAsync("Airbus")).Result.Id;
+
+ // Add two aircraft models
+ _manager = new ModelManager(context);
+ Task.Run(() => _manager.AddAsync("332", "A332", "A330-200", manufacturerId)).Wait();
+ Task.Run(() => _manager.AddAsync("345", "A345", "A340-500", manufacturerId)).Wait();
+ }
+
+ [TestMethod]
+ public void GetAircraftByIATATest()
+ {
+ var aircraft = Task.Run(() => _manager!.GetAsync(x => x.IATA == "332")).Result;
+ Assert.AreEqual("332", aircraft.IATA);
+ Assert.AreEqual("A332", aircraft.ICAO);
+ Assert.AreEqual("A330-200", aircraft.Name);
+ Assert.AreEqual("Airbus", aircraft.Manufacturer.Name);
+ }
+
+ [TestMethod]
+ public void GetAircraftByICAOTest()
+ {
+ var aircraft = Task.Run(() => _manager!.GetAsync(x => x.ICAO == "A345")).Result;
+ Assert.AreEqual("345", aircraft.IATA);
+ Assert.AreEqual("A345", aircraft.ICAO);
+ Assert.AreEqual("A340-500", aircraft.Name);
+ Assert.AreEqual("Airbus", aircraft.Manufacturer.Name);
+ }
+
+ [TestMethod]
+ public void GetAircraftByNameTest()
+ {
+ var aircraft = Task.Run(() => _manager!.GetAsync(x => x.Name == "A330-200")).Result;
+ Assert.AreEqual("332", aircraft.IATA);
+ Assert.AreEqual("A332", aircraft.ICAO);
+ Assert.AreEqual("A330-200", aircraft.Name);
+ Assert.AreEqual("Airbus", aircraft.Manufacturer.Name);
+ }
+
+ [TestMethod]
+ public void ListAircraftByManufacturerTest()
+ {
+ var aircraft = Task.Run(() => _manager!.ListAsync(x => x.Manufacturer.Name == "Airbus")).Result;
+ Assert.AreEqual(2, aircraft.Count);
+ Assert.IsNotNull(aircraft.Find(x => x.IATA == "332"));
+ Assert.IsNotNull(aircraft.Find(x => x.IATA == "345"));
+ }
+ }
+}
diff --git a/src/BaseStationReader.Tests/trackersettings.json b/src/BaseStationReader.Tests/trackersettings.json
index 253175c..39dcc5b 100644
--- a/src/BaseStationReader.Tests/trackersettings.json
+++ b/src/BaseStationReader.Tests/trackersettings.json
@@ -17,6 +17,19 @@
"MaximumRows": 20,
"ReceiverLatitude": 51.47138888,
"ReceiverLongitude": -0.45277777,
+ "ApiEndpoints": [
+ {
+ "EndpointType": "Airlines",
+ "Service": "AirLabs",
+ "Url": "https://airlabs.co/api/v9/airlines"
+ }
+ ],
+ "ApiServiceKeys": [
+ {
+ "Service": "AirLabs",
+ "Key": "my-key"
+ }
+ ],
"Columns": [
{
"Property": "Latitude",
diff --git a/src/BaseStationReader.UI/BaseStationReader.UI.csproj b/src/BaseStationReader.UI/BaseStationReader.UI.csproj
index f9e438e..c843924 100644
--- a/src/BaseStationReader.UI/BaseStationReader.UI.csproj
+++ b/src/BaseStationReader.UI/BaseStationReader.UI.csproj
@@ -2,9 +2,9 @@
WinExe
net7.0
- 1.27.0.0
- 1.27.0.0
- 1.27.0
+ 1.28.0.0
+ 1.28.0.0
+ 1.28.0
enable
true
app.manifest
diff --git a/src/BaseStationReader.UI/Models/AircraftLookupCriteria.cs b/src/BaseStationReader.UI/Models/AircraftLookupCriteria.cs
new file mode 100644
index 0000000..40b52bd
--- /dev/null
+++ b/src/BaseStationReader.UI/Models/AircraftLookupCriteria.cs
@@ -0,0 +1,9 @@
+using BaseStationReader.UI.ViewModels;
+
+namespace BaseStationReader.UI.Models
+{
+ public class AircraftLookupCriteria : ViewModelBase
+ {
+ public string? Address { get; set; }
+ }
+}
diff --git a/src/BaseStationReader.UI/Models/AircraftLookupModel.cs b/src/BaseStationReader.UI/Models/AircraftLookupModel.cs
new file mode 100644
index 0000000..d72d7d2
--- /dev/null
+++ b/src/BaseStationReader.UI/Models/AircraftLookupModel.cs
@@ -0,0 +1,55 @@
+using BaseStationReader.Data;
+using BaseStationReader.Entities.Config;
+using BaseStationReader.Entities.Lookup;
+using BaseStationReader.Logic.Api.AirLabs;
+using BaseStationReader.Logic.Database;
+using BaseStationReader.Logic.Tracking;
+using System;
+using System.Threading.Tasks;
+
+namespace BaseStationReader.UI.Models
+{
+ public class AircraftLookupModel
+ {
+ private readonly AircraftLookupManager _lookupManager;
+
+ public AircraftLookupModel(TrackerApplicationSettings settings)
+ {
+ // Create a database context
+ var context = new BaseStationReaderDbContextFactory().CreateDbContext(Array.Empty());
+
+ // Create the database management instances
+ var airlinesManager = new AirlineManager(context);
+ var detailsManager = new AircraftDetailsManager(context);
+ var modelsManager = new ModelManager(context);
+
+ // Get the service endpoint details
+ var key = settings.ApiServiceKeys.Find(x => x.Service == ApiServiceType.AirLabs)!.Key;
+ var airlinesUrl = settings.ApiEndpoints.Find(x => x.EndpointType == ApiEndpointType.Airlines)!.Url;
+ var aircraftUrl = settings.ApiEndpoints.Find(x => x.EndpointType == ApiEndpointType.Aircraft)!.Url;
+
+ // Create the API wrappers
+ var airlinesApi = new AirLabsAirlinesApi(airlinesUrl, key);
+ var aircraftApi = new AirLabsAircraftApi(aircraftUrl, key);
+
+ // Finally, create a lookup manager
+ _lookupManager = new AircraftLookupManager(airlinesManager, detailsManager, modelsManager, airlinesApi, aircraftApi);
+ }
+
+ ///
+ /// Look up the details of the specified aircraft
+ ///
+ ///
+ public AircraftDetails? Search(string? address)
+ {
+ AircraftDetails? details = null;
+
+ if (!string.IsNullOrEmpty(address))
+ {
+ details = Task.Run(() => _lookupManager.LookupAircraft(address)).Result;
+ }
+
+ return details;
+ }
+ }
+}
diff --git a/src/BaseStationReader.UI/ViewModels/AircraftLookupWindowViewModel.cs b/src/BaseStationReader.UI/ViewModels/AircraftLookupWindowViewModel.cs
new file mode 100644
index 0000000..15a861a
--- /dev/null
+++ b/src/BaseStationReader.UI/ViewModels/AircraftLookupWindowViewModel.cs
@@ -0,0 +1,35 @@
+using BaseStationReader.Entities.Config;
+using BaseStationReader.Entities.Lookup;
+using BaseStationReader.UI.Models;
+using ReactiveUI;
+using System.Reactive;
+
+namespace BaseStationReader.UI.ViewModels
+{
+ public class AircraftLookupWindowViewModel : AircraftLookupCriteria
+ {
+ private readonly AircraftLookupModel _aircraftLookup;
+
+ public ReactiveCommand CloseCommand { get; private set; }
+
+ public AircraftLookupWindowViewModel(TrackerApplicationSettings settings, AircraftLookupCriteria? initialValues)
+ {
+ // Set up the aircraft lookup model
+ _aircraftLookup = new AircraftLookupModel(settings);
+
+ // Populate from the initial values, if supplied
+ Address = initialValues?.Address;
+
+ // Create a command that can be bound to the Cancel button on the dialog
+ CloseCommand = ReactiveCommand.Create(() => { return (AircraftLookupCriteria?)this; });
+ }
+
+ ///
+ /// Search for the specified aircraft address
+ ///
+ ///
+ ///
+ public AircraftDetails? Search(string? address)
+ => _aircraftLookup.Search(address);
+ }
+}
diff --git a/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs b/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs
index 918fcf6..7664f9b 100644
--- a/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs
+++ b/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs
@@ -11,35 +11,99 @@ namespace BaseStationReader.UI.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
+ ///
+ /// Model underlying the live view
+ ///
private readonly LiveViewModel _liveView = new LiveViewModel();
+
+ ///
+ /// Model underlying the database search view
+ ///
private readonly DatabaseSearchModel _databaseSearch = new DatabaseSearchModel();
+ ///
+ /// Application settings
+ ///
+ public TrackerApplicationSettings? Settings { get; set; }
+
+ ///
+ /// True if the tracker is actively tracking
+ ///
public bool IsTracking { get { return _liveView.IsTracking; } }
+
+ ///
+ /// Collection of currently tracked aircraft
+ ///
public ObservableCollection TrackedAircraft { get { return _liveView.TrackedAircraft; } }
+
+ ///
+ /// Filtering criteria for the live view
+ ///
public BaseFilters? LiveViewFilters
{
get { return _liveView.Filters; }
set { _liveView.Filters = value; }
}
+ ///
+ /// Collection of database search results
+ ///
public ObservableCollection SearchResults { get { return _databaseSearch.SearchResults; } }
+
+ ///
+ /// Database search criteria
+ ///
public DatabaseSearchCriteria? DatabaseSearchCriteria
{
get { return _databaseSearch.SearchCriteria; }
set { _databaseSearch.SearchCriteria = value; }
}
- public TrackerApplicationSettings? Settings { get; set; }
+ ///
+ /// Aircraft lookup criteria
+ ///
+ public AircraftLookupCriteria? AircraftLookupCriteria { get; set; }
+ ///
+ /// Command to show the live view filtering dialog
+ ///
public ICommand ShowTrackingFiltersCommand { get; private set; }
+
+ ///
+ /// Interaction to show the live view filtering dialog
+ ///
public Interaction ShowFiltersDialog { get; private set; }
- public ICommand ShowDatabaseSearchCommand { get; private set; }
- public Interaction ShowDatabaseSearchDialog { get; private set; }
+ ///
+ /// Command to show the aircraft lookup dialog
+ ///
+ public ICommand ShowAircraftLookupCommand { get; private set; }
+ ///
+ /// Interaction to show the aircraft lookup dialog
+ ///
+ public Interaction ShowAircraftLookupDialog { get; private set; }
+
+ ///
+ /// Command to show the tracking options dialog
+ ///
public ICommand ShowTrackingOptionsCommand { get; private set; }
+
+ ///
+ /// Interaction to show the tracking options dialog
+ ///
public Interaction ShowTrackingOptionsDialog { get; private set; }
+ ///
+ /// Command to show the database search dialog
+ ///
+ public ICommand ShowDatabaseSearchCommand { get; private set; }
+
+ ///
+ /// Interaction to show the database search dialog
+ ///
+ public Interaction ShowDatabaseSearchDialog { get; private set; }
+
public MainWindowViewModel()
{
// Wire up the tracking filters dialog
@@ -51,6 +115,15 @@ public MainWindowViewModel()
return result;
});
+ // Wire up the aircraft lookup dialog
+ ShowAircraftLookupDialog = new Interaction();
+ ShowAircraftLookupCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ var dialogViewModel = new AircraftLookupWindowViewModel(Settings!, AircraftLookupCriteria);
+ var result = await ShowAircraftLookupDialog.Handle(dialogViewModel);
+ return result;
+ });
+
// Wire up the tracking options dialog
ShowTrackingOptionsDialog = new Interaction();
ShowTrackingOptionsCommand = ReactiveCommand.CreateFromTask(async () =>
diff --git a/src/BaseStationReader.UI/Views/AircraftLookupWindow.axaml b/src/BaseStationReader.UI/Views/AircraftLookupWindow.axaml
new file mode 100644
index 0000000..8ace6e4
--- /dev/null
+++ b/src/BaseStationReader.UI/Views/AircraftLookupWindow.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BaseStationReader.UI/Views/AircraftLookupWindow.axaml.cs b/src/BaseStationReader.UI/Views/AircraftLookupWindow.axaml.cs
new file mode 100644
index 0000000..de2919b
--- /dev/null
+++ b/src/BaseStationReader.UI/Views/AircraftLookupWindow.axaml.cs
@@ -0,0 +1,77 @@
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.ReactiveUI;
+using BaseStationReader.UI.ViewModels;
+using ReactiveUI;
+using System;
+
+namespace BaseStationReader.UI;
+
+public partial class AircraftLookupWindow : ReactiveWindow
+{
+ private const string DetailsNotAvailableText = "Not available";
+
+ public AircraftLookupWindow()
+ {
+ InitializeComponent();
+
+ // Register the dialog button handlers
+ this.WhenActivated(a => a(ViewModel!.CloseCommand.Subscribe(Close)));
+ }
+
+ ///
+ /// Handler called to initialise the window once it's fully loaded
+ ///
+ ///
+ ///
+ private void OnLoaded(object? source, RoutedEventArgs e)
+ {
+ Address.Text = ViewModel?.Address ?? "";
+ if (!string.IsNullOrEmpty(Address.Text))
+ {
+ LookupAircraftDetails();
+ }
+ }
+
+ ///
+ /// Handler called to initiate a search
+ ///
+ ///
+ ///
+ private void OnLookup(object source, RoutedEventArgs e)
+ {
+ LookupAircraftDetails();
+ }
+
+ ///
+ /// Look up the aircraft details and populate the dialog with the results
+ ///
+ private void LookupAircraftDetails()
+ {
+ // Search for the current ICAO address
+ var originalCursor = Cursor;
+ Cursor = new Cursor(StandardCursorType.Wait);
+ var details = ViewModel!.Search(Address.Text);
+ Cursor = originalCursor;
+
+ // Check we have some valid details
+ if (details != null)
+ {
+ // Result is valid, so populate the text blocks with the aircraft details
+ AirlineName.Text = details.Airline?.Name ?? DetailsNotAvailableText;
+ ManufacturerName.Text = details.Model?.Manufacturer.Name ?? DetailsNotAvailableText;
+ ModelName.Text = details.Model?.Name ?? DetailsNotAvailableText;
+ ModelIATA.Text = details.Model?.IATA ?? DetailsNotAvailableText;
+ ModelICAO.Text = details.Model?.ICAO ?? DetailsNotAvailableText;
+ }
+ else
+ {
+ // No details availables, so set the default "not available" text
+ AirlineName.Text = DetailsNotAvailableText;
+ ManufacturerName.Text = DetailsNotAvailableText;
+ ModelName.Text = DetailsNotAvailableText;
+ ModelIATA.Text = DetailsNotAvailableText;
+ ModelICAO.Text = DetailsNotAvailableText;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BaseStationReader.UI/Views/MainWindow.axaml b/src/BaseStationReader.UI/Views/MainWindow.axaml
index 1e59b69..a684f3e 100644
--- a/src/BaseStationReader.UI/Views/MainWindow.axaml
+++ b/src/BaseStationReader.UI/Views/MainWindow.axaml
@@ -33,6 +33,8 @@
+
+