2020-03-01 15:12:08 +00:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using System.Reflection ;
using System.ComponentModel.Composition ;
using System.ComponentModel ;
using System.Globalization ;
using AirScout.PlaneFeeds.Plugin.MEFContract ;
using System.Diagnostics ;
using System.Net ;
using System.IO ;
using System.Web.Script.Serialization ;
using System.Xml.Linq ;
using System.Xml.Serialization ;
using System.Collections ;
2020-04-10 15:36:18 +00:00
using Newtonsoft.Json ;
2020-03-01 15:12:08 +00:00
//TODO: Rename namespace to a name of your choice
namespace AirScout.PlaneFeeds.Plugin.OpenSky
{
//TODO: Rename settings class to a name of your choice
// Add any persistant setting here
// Use [Browsable(true/false)] to present the setting to the user and allow changes or not
// Use [CategoryAttribute("<description")] to group settings to categories
// Use [DescriptionAttribute("<description")] to add a description to this setting
// Use [DefaultValue(<value>)] to set a default value for this setting
// Use [XmlIgnore] if you don't want this setting to be ex-/imported, but stiil stored in settings file
// Example:
// [CategoryAttribute("Web Feed")]
// [DescriptionAttribute("Timeout for loading the site.")]
// [DefaultValue(30)]
// [XmlIgnore]
// public int Timeout { get; set; }
/// <summary>
/// Keeps all persistant settings of plugin
/// </summary>
public class OpenSkySettings
{
[Browsable(false)]
[DefaultValue("")]
[XmlIgnore]
public string DisclaimerAccepted { get ; set ; }
[Browsable(true)]
[CategoryAttribute("Web Feed")]
[DescriptionAttribute("Save downloaded JSON to file")]
[DefaultValue(false)]
[XmlIgnore]
public bool SaveToFile { get ; set ; }
[Browsable(true)]
[CategoryAttribute("Web Feed")]
[DescriptionAttribute("Base URL for website.")]
2020-04-10 15:36:18 +00:00
[DefaultValue("https://opensky-network.org/api/states/all?lamin=%MINLAT%&lomin=%MINLON%&lamax=%MAXLAT%&lomax=%MAXLON%")]
2020-03-01 15:12:08 +00:00
public string URL { get ; set ; }
2022-08-27 12:13:47 +00:00
[Browsable(true)]
[CategoryAttribute("Web Feed")]
[DescriptionAttribute("Username for Authentication (blank for anonymous)")]
[DefaultValue("")]
public string Username { get ; set ; }
[Browsable(true)]
[CategoryAttribute("Web Feed")]
[DescriptionAttribute("Password for Authentication (blank for anonymous)")]
[DefaultValue("")]
[PasswordPropertyText(true)]
public string Password { get ; set ; }
2020-03-01 15:12:08 +00:00
[Browsable(true)]
[CategoryAttribute("Web Feed")]
[DescriptionAttribute("Timeout for loading the site.")]
2022-08-27 12:13:47 +00:00
[DefaultValue(90)]
2020-03-01 15:12:08 +00:00
[XmlIgnore]
public int Timeout { get ; set ; }
public OpenSkySettings ( )
{
Default ( ) ;
Load ( true ) ;
// TODO:
}
// Methods für Load/Dave/Default, don't change!
#region Load/Save/Default
/// <summary>
/// Sets all properties to their default value according to the [DefaultValue=] attribute
/// </summary>
public void Default ( )
{
// set all properties to their default values according to definition in [DeafultValue=]
foreach ( var p in this . GetType ( ) . GetProperties ( ) )
{
try
{
// initialize all properties with default value if set
if ( Attribute . IsDefined ( p , typeof ( DefaultValueAttribute ) ) )
{
p . SetValue ( this , ( ( DefaultValueAttribute ) Attribute . GetCustomAttribute (
p , typeof ( DefaultValueAttribute ) ) ) ? . Value , null ) ;
}
}
catch ( Exception ex )
{
Console . WriteLine ( "[" + this . GetType ( ) . Name + "]: Cannot set default value of: " + p . Name + ", " + ex . Message ) ;
}
}
}
/// <summary>
/// Loads settings from a XML-formatted configuration file into settings.
/// </summary>
/// <param name="loadall">If true, ignore the [XmlIgnore] attribute, e.g. load all settings available in the file.<br>If false, load only settings without [XmlIgore] attrbute.</br></param>
/// <param name="filename">The filename of the settings file.</param>
public void Load ( bool loadall , string filename = "" )
{
// use standard filename if empty
// be careful because Linux file system is case sensitive
if ( String . IsNullOrEmpty ( filename ) )
filename = new Uri ( Assembly . GetExecutingAssembly ( ) . GetName ( ) . CodeBase . Replace ( ".dll" , ".cfg" ) . Replace ( ".DLL" , ".CFG" ) ) . LocalPath ;
// do nothing if file not exists
if ( ! File . Exists ( filename ) )
return ;
try
{
string xml = "" ;
using ( StreamReader sr = new StreamReader ( File . OpenRead ( filename ) ) )
{
xml = sr . ReadToEnd ( ) ;
}
XDocument xdoc = XDocument . Parse ( xml ) ;
PropertyInfo [ ] properties = this . GetType ( ) . GetProperties ( ) ;
foreach ( PropertyInfo p in properties )
{
if ( ! loadall )
{
// check on XmlIgnore attribute, skip if set
object [ ] attr = p . GetCustomAttributes ( typeof ( XmlIgnoreAttribute ) , false ) ;
if ( attr . Length > 0 )
continue ;
}
try
{
// get matching element
XElement typenode = xdoc . Element ( this . GetType ( ) . Name ) ;
if ( typenode ! = null )
{
XElement element = typenode . Element ( p . Name ) ;
if ( element ! = null )
p . SetValue ( this , Convert . ChangeType ( element . Value , p . PropertyType ) , null ) ;
}
}
catch ( Exception ex )
{
Console . WriteLine ( "[" + this . GetType ( ) . Name + "]: Error while loading property[" + p . Name + " from " + filename + ", " + ex . Message ) ;
}
}
}
catch ( Exception ex )
{
Console . WriteLine ( "[" + this . GetType ( ) . Name + "]: Cannot load settings from " + filename + ", " + ex . Message ) ;
}
}
/// <summary>
/// Saves settings from settings into a XML-formatted configuration file
/// </summary>
/// <param name="saveall">If true, ignore the [XmlIgnore] attribute, e.g. save all settings.<br>If false, save only settings without [XmlIgore] attrbute.</param>
/// <param name="filename">The filename of the settings file.</param>
public void Save ( bool saveall , string filename = "" )
{
// use standard filename if empty
// be careful because Linux file system is case sensitive
if ( String . IsNullOrEmpty ( filename ) )
filename = new Uri ( Assembly . GetExecutingAssembly ( ) . GetName ( ) . CodeBase . Replace ( ".dll" , ".cfg" ) . Replace ( ".DLL" , ".CFG" ) ) . LocalPath ;
XmlAttributeOverrides overrides = new XmlAttributeOverrides ( ) ;
if ( saveall )
{
// ovverride the XmlIgnore attributes to get all serialized
PropertyInfo [ ] properties = this . GetType ( ) . GetProperties ( ) ;
foreach ( PropertyInfo p in properties )
{
XmlAttributes attribs = new XmlAttributes { XmlIgnore = false } ;
overrides . Add ( this . GetType ( ) , p . Name , attribs ) ;
}
}
try
{
using ( StreamWriter sw = new StreamWriter ( File . Create ( filename ) ) )
{
XmlSerializer s = new XmlSerializer ( this . GetType ( ) , overrides ) ;
s . Serialize ( sw , this ) ;
}
}
catch ( Exception ex )
{
throw new InvalidOperationException ( "[" + this . GetType ( ) . Name + "]: Cannot save settings to " + filename + ", " + ex . Message ) ;
}
}
}
# endregion
//TODO: Rename plugin class to a name of yout choice
/// <summary>
/// Holds the plane feed plugin class
/// </summary>
[Export(typeof(IPlaneFeedPlugin))]
[ExportMetadata("Name", "PlaneFeedPlugin")]
public class OpenSkyPlugin : IPlaneFeedPlugin
{
private OpenSkySettings Settings = new OpenSkySettings ( ) ;
// start of interface
//TODO: Change return values so that they represent plugin's functionality
#region Interface
public string Name
{
get
{
return "[WebFeed] OpenSky" ;
}
}
public string Version
{
get
{
return Assembly . GetExecutingAssembly ( ) . GetName ( ) . Version . ToString ( ) ;
}
}
public string Info
{
get
{
return "Web feed from the OpenSky Network.\n" +
"For details see https://opensky-network.org.\n\n" +
"As this is a community network, please consider to run a personal ADSB-receiver and to contribute your data to this network.\n\n" +
2022-08-27 12:13:47 +00:00
"The daily limit for anonymous requests to OpenSky Network is 100. You can extend the limit to 1000 if you are an registerd user.\n" +
"For this, please enter your username & password here." ;
2020-03-01 15:12:08 +00:00
}
}
public bool HasSettings
{
get
{
return true ;
}
}
public bool CanImport
{
get
{
return false ;
}
}
public bool CanExport
{
get
{
return false ;
}
}
public string Disclaimer
{
get
{
return "" ;
}
}
public string DisclaimerAccepted
{
get
{
return Settings . DisclaimerAccepted ;
}
set
{
Settings . DisclaimerAccepted = value ;
}
}
public void ResetSettings ( )
{
Settings . Default ( ) ;
}
public void LoadSettings ( )
{
Settings . Load ( true ) ;
}
public void SaveSettings ( )
{
Settings . Save ( true ) ;
}
public object GetSettings ( )
{
return this . Settings ;
}
public void ImportSettings ( )
{
// nothing to do
}
public void ExportSettings ( )
{
// nothing to do
}
public void Start ( PlaneFeedPluginArgs args )
{
// add code for startup here
}
public PlaneFeedPluginPlaneInfoList GetPlanes ( PlaneFeedPluginArgs args )
{
// intialize variables
VarConverter VC = new VarConverter ( ) ;
VC . AddVar ( "APPDIR" , args . AppDirectory ) ;
VC . AddVar ( "DATADIR" , args . AppDataDirectory ) ;
VC . AddVar ( "LOGDIR" , args . LogDirectory ) ;
VC . AddVar ( "DATABASEDIR" , args . DatabaseDirectory ) ;
VC . AddVar ( "MINLAT" , args . MinLat ) ;
VC . AddVar ( "MAXLAT" , args . MaxLat ) ;
VC . AddVar ( "MINLON" , args . MinLon ) ;
VC . AddVar ( "MAXLON" , args . MaxLon ) ;
VC . AddVar ( "MINALTM" , args . MinAlt ) ;
VC . AddVar ( "MAXALTM" , args . MaxAlt ) ;
VC . AddVar ( "MINALTFT" , ( int ) UnitConverter . m_ft ( ( double ) args . MinAlt ) ) ;
VC . AddVar ( "MAXALTFT" , ( int ) UnitConverter . m_ft ( ( double ) args . MaxAlt ) ) ;
// initialize plane info list
PlaneFeedPluginPlaneInfoList planes = new PlaneFeedPluginPlaneInfoList ( ) ;
string json = "" ;
// calculate url and get json
String url = VC . ReplaceAllVars ( Settings . URL ) ;
Console . WriteLine ( "[" + this . GetType ( ) . Name + "]: Creating web request: " + url ) ;
// this will only run on .NET 4.0 if you have installed .NET 4.5 or later frameworks on your machine!
2020-04-10 15:36:18 +00:00
// ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
// HttpWebRequest webrequest = (HttpWebRequest)HttpWebRequest.Create(url);
// webrequest.Timeout = Settings.Timeout * 1000;
// webrequest.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0";
// webrequest.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
// Console.WriteLine("[" + this.GetType().Name + "]: Getting web response");
// HttpWebResponse webresponse = (HttpWebResponse)webrequest.GetResponse();
// Console.WriteLine("[" + this.GetType().Name + "]: Reading stream");
// using (StreamReader sr = new StreamReader(webresponse.GetResponseStream()))
// {
// json = sr.ReadToEnd();
// }
2022-08-27 12:13:47 +00:00
DateTime downloadstart = DateTime . UtcNow ;
json = OSNTlsClient . DownloadFile ( url , Settings . Timeout * 1000 , Settings . Username , Settings . Password ) ;
Console . WriteLine ( "[" + this . GetType ( ) . Name + "]: Content downloaded, " + ( DateTime . UtcNow - downloadstart ) . TotalMilliseconds . ToString ( "F0" ) + "ms." ) ;
// check for errors
if ( ! json . StartsWith ( "{" ) )
{
// report the first characters of response
throw new Exception ( "Response: " + json . Substring ( 0 , 80 ) ) ;
}
2020-03-01 15:12:08 +00:00
// save raw data to file if enabled
if ( Settings . SaveToFile )
{
using ( StreamWriter sw = new StreamWriter ( args . TmpDirectory + Path . DirectorySeparatorChar + this . GetType ( ) . Name + "_" + DateTime . UtcNow . ToString ( "yyyy-MM-dd HH_mm_ss" ) + ".json" ) )
{
sw . WriteLine ( json ) ;
}
}
Console . WriteLine ( "[" + this . GetType ( ) . Name + "]: Analyzing data" ) ;
2020-04-10 15:36:18 +00:00
// JavaScriptSerializer js = new JavaScriptSerializer();
// dynamic root = js.Deserialize<dynamic>(json);
dynamic root = JsonConvert . DeserializeObject ( json ) ;
2020-03-01 15:12:08 +00:00
try
{
// analyze json string for planes data
// get the planes position list
var aclist = root [ "states" ] ;
2023-01-30 06:00:58 +00:00
if ( aclist ! = null )
2020-03-01 15:12:08 +00:00
{
2023-01-30 06:00:58 +00:00
foreach ( var ac in aclist )
2020-03-01 15:12:08 +00:00
{
2023-01-30 06:00:58 +00:00
try
2020-03-01 15:12:08 +00:00
{
2023-01-30 06:00:58 +00:00
// different handling of reading JSON between Windows (Array) & Linux (ArrayList)
// access to data values itself is the same
int len = 0 ;
len = ac . Count ;
// skip if too few fields in record
if ( len < 17 )
continue ;
PlaneFeedPluginPlaneInfo plane = new PlaneFeedPluginPlaneInfo ( ) ;
// get hex first
plane . Hex = ReadPropertyString ( ac , 0 ) . ToUpper ( ) . Trim ( ) ;
// get callsign
plane . Call = ReadPropertyString ( ac , 1 ) . ToUpperInvariant ( ) . Trim ( ) ;
// get position
plane . Lon = ReadPropertyDouble ( ac , 5 ) ;
plane . Lat = ReadPropertyDouble ( ac , 6 ) ;
// get altitude (provided in m --> convert to ft)
plane . Alt = UnitConverter . m_ft ( ReadPropertyDouble ( ac , 13 ) ) ;
// get track
plane . Track = ReadPropertyDouble ( ac , 10 ) ;
// get speed (provided in m/s --> convert to kts)
plane . Speed = UnitConverter . ms_kts ( ReadPropertyDouble ( ac , 9 ) ) ;
// registration is not provided
plane . Reg = "" ;
// get position timestamp in sec
int l = ReadPropertyInt ( ac , 3 ) ;
if ( l ! = int . MinValue )
{
DateTime timestamp = new System . DateTime ( 1970 , 1 , 1 , 0 , 0 , 0 , 0 ) ;
timestamp = timestamp . AddSeconds ( l ) ;
plane . Time = timestamp ;
}
else
{
// skip plane if no valid timestamp found
continue ;
}
// get type info
plane . Type = ReadPropertyString ( ac , 5 ) ;
// discard planes on ground
bool onground = ReadPropertyBool ( ac , 8 ) ;
if ( onground )
continue ;
// discard planes stopped
if ( plane . Speed < = 0 )
continue ;
planes . Add ( plane ) ;
2020-03-01 15:12:08 +00:00
}
2023-01-30 06:00:58 +00:00
catch ( Exception ex )
2020-03-01 15:12:08 +00:00
{
2023-01-30 06:00:58 +00:00
Console . WriteLine ( "[" + System . Reflection . MethodBase . GetCurrentMethod ( ) . Name + "]" + ex . Message ) ;
2020-03-01 15:12:08 +00:00
}
}
}
}
catch ( Exception ex )
{
2020-04-10 15:36:18 +00:00
Console . WriteLine ( "[" + System . Reflection . MethodBase . GetCurrentMethod ( ) . Name + "]" + ex . Message ) ;
2020-03-01 15:12:08 +00:00
}
Console . WriteLine ( "[" + this . GetType ( ) . Name + "]: Returning " + planes . Count + " planes" ) ;
return planes ;
}
public void Stop ( PlaneFeedPluginArgs args )
{
// add code for stopping here
}
# endregion
// End of interface
// ************************************* Helpers ****************************************************
private string ReadPropertyString ( dynamic o , int propertyindex )
{
string s = null ;
try
{
s = o [ propertyindex ] ;
}
2022-08-27 12:13:47 +00:00
catch ( Exception ex )
2020-03-01 15:12:08 +00:00
{
// do nothing if something went wrong
2022-08-27 12:13:47 +00:00
Console . WriteLine ( "[ReadPropertyString]: " + ex . ToString ( ) ) ;
2020-03-01 15:12:08 +00:00
}
return s ;
}
private int ReadPropertyInt ( dynamic o , int propertyindex )
{
int i = int . MinValue ;
try
{
2022-08-27 12:13:47 +00:00
double d = ReadPropertyDouble ( o , propertyindex ) ;
if ( ( d ! = double . MinValue ) & & ( d > = int . MinValue ) & & ( d < = int . MaxValue ) )
i = ( int ) d ;
2020-03-01 15:12:08 +00:00
}
catch
{
// do nothing if something went wrong
}
2022-08-27 12:13:47 +00:00
return i ;
2020-03-01 15:12:08 +00:00
}
2022-08-27 12:13:47 +00:00
private double ReadPropertyDouble ( dynamic o , int propertyindex )
2020-03-01 15:12:08 +00:00
{
2022-08-27 12:13:47 +00:00
double d = double . MinValue ;
2020-03-01 15:12:08 +00:00
try
{
2022-08-27 12:13:47 +00:00
string s = o [ propertyindex ] . ToString ( CultureInfo . InvariantCulture ) . Trim ( ) ;
// check for special values
if ( String . IsNullOrEmpty ( s ) )
d = double . MinValue ;
else if ( s . ToUpperInvariant ( ) . StartsWith ( "-INF" ) )
d = double . MinValue ;
else if ( s . ToUpperInvariant ( ) . StartsWith ( "+INF" ) )
d = double . MaxValue ;
else if ( s . ToUpperInvariant ( ) . StartsWith ( "NAN" ) )
d = double . NaN ;
else if ( s . ToUpperInvariant ( ) . StartsWith ( "NULL" ) )
d = double . NaN ;
else
d = double . Parse ( s , CultureInfo . InvariantCulture ) ;
2020-03-01 15:12:08 +00:00
}
2022-08-27 12:13:47 +00:00
catch ( Exception ex )
2020-03-01 15:12:08 +00:00
{
// do nothing if something went wrong
2022-08-27 12:13:47 +00:00
Console . WriteLine ( "[ReadPropertyDouble] got invalid value = \"" + o [ propertyindex ] . ToString ( ) + "\": " + ex . ToString ( ) ) ;
2020-03-01 15:12:08 +00:00
}
2022-08-27 12:13:47 +00:00
return d ;
2020-03-01 15:12:08 +00:00
}
private bool ReadPropertyBool ( dynamic o , int propertyindex )
{
bool b = false ;
try
{
string s = o [ propertyindex ] . ToString ( ) ;
b = s . ToLower ( ) = = "true" ;
}
2022-08-27 12:13:47 +00:00
catch ( Exception ex )
2020-03-01 15:12:08 +00:00
{
// do nothing if something went wrong
2022-08-27 12:13:47 +00:00
Console . WriteLine ( "[ReadPropertyBool]: " + ex . ToString ( ) ) ;
2020-03-01 15:12:08 +00:00
}
return b ;
}
}
public static class UnitConverter
{
public static double ft_m ( double feet )
{
return feet / 3.28084 ;
}
public static double m_ft ( double m )
{
return m * 3.28084 ;
}
public static double kts_kmh ( double kts )
{
return kts * 1.852 ;
}
public static double kmh_kts ( double kmh )
{
return kmh / 1.852 ;
}
public static double ms_kts ( double ms )
{
return ms * 3.6 / 1.852 ;
}
public static double km_mi ( double km )
{
return km * 1.609 ;
}
public static double mi_km ( double mi )
{
return mi / 1.609 ;
}
}
public class VarConverter : Dictionary < string , object >
{
public readonly char VarSeparator = '%' ;
public void AddVar ( string var , object value )
{
// adds a new var<>value pair to dictionary
object o ;
if ( this . TryGetValue ( var , out o ) )
{
// item found --> update value
o = value ;
}
else
{
// item not found --> add new
this . Add ( var , value ) ;
}
}
public object GetValue ( string var )
{
// finds a var in dictionary and returns its value
object o ;
if ( this . TryGetValue ( var , out o ) )
{
// item found --> return value
return o ;
}
// item not found --> return null
return null ;
}
public string ReplaceAllVars ( string s )
{
// check for var separotors first
if ( s . Contains ( VarSeparator ) )
{
// OK, string is containing vars --> crack the string first and replace vars
try
{
string [ ] a = s . Split ( VarSeparator ) ;
// as we are always using a pair of separators the length of a[] must be odd
if ( a . Length % 2 = = 0 )
throw new ArgumentException ( "Number of separators is not an even number." ) ;
// create new string and replace all vars (on odd indices)
s = "" ;
for ( int i = 0 ; i < a . Length ; i + + )
{
if ( i % 2 = = 0 )
{
// cannot be not a var on that position
s = s + a [ i ] ;
}
else
{
// var identifier: upper the string and try to convert
a [ i ] = a [ i ] . ToUpper ( ) ;
object o ;
if ( this . TryGetValue ( a [ i ] , out o ) )
{
// convert floating points with invariant culture info
if ( o . GetType ( ) = = typeof ( double ) )
s = s + ( ( double ) o ) . ToString ( CultureInfo . InvariantCulture ) ;
else if ( o . GetType ( ) = = typeof ( float ) )
s = s + ( ( float ) o ) . ToString ( CultureInfo . InvariantCulture ) ;
else
s = s + o . ToString ( ) ;
}
else
{
throw new ArgumentException ( "Var identifier not found: " + a [ i ] ) ;
}
}
}
}
catch ( Exception ex )
{
// throw an excecption
throw new ArgumentException ( "Error while parsing string for variables [" + ex . Message + "]: " + s ) ;
}
}
return s ;
}
}
}