2020-09-02 10:49:09 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using SharpKml.Base;
|
|
|
|
|
using SharpKml.Dom;
|
|
|
|
|
|
|
|
|
|
namespace FlightLoggerLib.Concrete
|
|
|
|
|
{
|
|
|
|
|
public class KMLFlightLogger : IFlightLogger
|
|
|
|
|
{
|
2020-09-09 19:20:42 +00:00
|
|
|
|
private readonly string path;
|
|
|
|
|
private string filename;
|
2020-09-02 10:49:09 +00:00
|
|
|
|
private readonly System.TimeSpan flushInterval;
|
2020-09-09 19:20:42 +00:00
|
|
|
|
private Document output;
|
|
|
|
|
private Folder rootFolder;
|
|
|
|
|
private LineString positionPath;
|
2020-09-02 10:49:09 +00:00
|
|
|
|
|
|
|
|
|
private DateTime lastFlush = DateTime.MinValue;
|
|
|
|
|
private Vector lastPosition;
|
|
|
|
|
private float lastSpeed;
|
|
|
|
|
private DateTime lastPointDate = DateTime.MinValue;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public KMLFlightLogger(string path, System.TimeSpan flushInterval)
|
|
|
|
|
{
|
2020-09-09 19:20:42 +00:00
|
|
|
|
this.path = path;
|
2020-09-02 10:49:09 +00:00
|
|
|
|
this.flushInterval = flushInterval;
|
2020-09-09 19:20:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async Task NewLog()
|
|
|
|
|
{
|
|
|
|
|
if (output == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
CheckEndPoint();
|
|
|
|
|
await Flush();
|
|
|
|
|
output = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
if (output == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
CheckEndPoint();
|
|
|
|
|
await Flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected void CheckEndPoint()
|
|
|
|
|
{
|
|
|
|
|
// TODO replace last "full stop" point if they match
|
|
|
|
|
if (lastPosition != null)
|
|
|
|
|
{
|
|
|
|
|
AddPoint(lastPosition, "End", "end");
|
|
|
|
|
lastPosition = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-09-02 10:49:09 +00:00
|
|
|
|
|
2020-09-09 19:20:42 +00:00
|
|
|
|
protected async Task AutoFlush()
|
|
|
|
|
{
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
var diff = now - lastFlush;
|
|
|
|
|
|
|
|
|
|
if (diff < flushInterval)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await Flush();
|
|
|
|
|
lastFlush = now;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected async Task Flush()
|
|
|
|
|
{
|
|
|
|
|
var serializer = new Serializer();
|
|
|
|
|
serializer.Serialize(output);
|
|
|
|
|
|
|
|
|
|
using (var writer = new StreamWriter(filename))
|
|
|
|
|
{
|
|
|
|
|
await writer.WriteAsync(serializer.Xml);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected void EnsureOutput(DateTime eventTime)
|
|
|
|
|
{
|
|
|
|
|
if (output != null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var dateString = eventTime.ToString("yyyy-MM-dd HH.mm.ss");
|
|
|
|
|
filename = Path.Combine(path, dateString + ".kml");
|
2020-09-02 10:49:09 +00:00
|
|
|
|
|
|
|
|
|
// Create folder
|
|
|
|
|
rootFolder = new Folder
|
|
|
|
|
{
|
|
|
|
|
Name = dateString,
|
|
|
|
|
Open = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create flight path line and placemark
|
|
|
|
|
positionPath = new LineString
|
|
|
|
|
{
|
|
|
|
|
Tessellate = false,
|
|
|
|
|
AltitudeMode = AltitudeMode.Absolute,
|
|
|
|
|
Coordinates = new CoordinateCollection()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var positionPlacemark = new Placemark
|
|
|
|
|
{
|
|
|
|
|
Name = "Flight path",
|
|
|
|
|
StyleUrl = new Uri("#flightpath", UriKind.Relative),
|
|
|
|
|
Geometry = positionPath
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
rootFolder.AddFeature(positionPlacemark);
|
|
|
|
|
|
|
|
|
|
output = new Document();
|
|
|
|
|
|
|
|
|
|
AddFlightPathStyleMap();
|
|
|
|
|
|
|
|
|
|
var paddleHotspot = new Hotspot { X = 31, XUnits = Unit.Pixel, Y = 1, YUnits = Unit.Pixel };
|
|
|
|
|
AddIconStyleMap("start", 1.1, "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png", "http://maps.google.com/mapfiles/kml/paddle/grn-circle-lv.png", paddleHotspot);
|
|
|
|
|
AddIconStyleMap("end", 1.1, "http://maps.google.com/mapfiles/kml/paddle/red-square.png", "http://maps.google.com/mapfiles/kml/paddle/red-square-lv.png", paddleHotspot);
|
|
|
|
|
AddIconStyleMap("fullstop", 1.1, "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png", null, paddleHotspot);
|
|
|
|
|
|
|
|
|
|
output.AddFeature(rootFolder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected void AddFlightPathStyleMap()
|
|
|
|
|
{
|
|
|
|
|
var styleMap = new StyleMapCollection { Id = "flightpath" };
|
|
|
|
|
styleMap.Add(new Pair { State = StyleState.Normal, StyleUrl = new Uri("flightpath-normal", UriKind.Relative) });
|
|
|
|
|
styleMap.Add(new Pair { State = StyleState.Highlight, StyleUrl = new Uri("flightpath-normal", UriKind.Relative) });
|
|
|
|
|
|
|
|
|
|
var styleNormal = new Style
|
|
|
|
|
{
|
|
|
|
|
Id = "flightpath-normal",
|
|
|
|
|
Line = new LineStyle
|
|
|
|
|
{
|
|
|
|
|
Color = new Color32(255, 10, 10, 138),
|
|
|
|
|
Width = 5
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
output.AddStyle(styleMap);
|
|
|
|
|
output.AddStyle(styleNormal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected void AddIconStyleMap(string id, double scale, string iconUrl, string listUrl, Hotspot iconHotspot = null)
|
|
|
|
|
{
|
|
|
|
|
var styleMap = new StyleMapCollection { Id = id };
|
|
|
|
|
styleMap.Add(new Pair { State = StyleState.Normal, StyleUrl = new Uri(id + "-normal", UriKind.Relative) });
|
|
|
|
|
styleMap.Add(new Pair { State = StyleState.Highlight, StyleUrl = new Uri(id + "-normal", UriKind.Relative) });
|
|
|
|
|
|
|
|
|
|
var styleNormal = new Style
|
|
|
|
|
{
|
|
|
|
|
Id = id + "-normal",
|
|
|
|
|
Icon = new IconStyle
|
|
|
|
|
{
|
|
|
|
|
Scale = scale,
|
|
|
|
|
Icon = new IconStyle.IconLink(new Uri(iconUrl, UriKind.Absolute)),
|
|
|
|
|
Hotspot = iconHotspot
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(listUrl))
|
|
|
|
|
{
|
|
|
|
|
styleNormal.List = new ListStyle();
|
|
|
|
|
styleNormal.List.AddItemIcon(new ItemIcon
|
|
|
|
|
{
|
|
|
|
|
Href = new Uri(listUrl, UriKind.Absolute)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
output.AddStyle(styleMap);
|
|
|
|
|
output.AddStyle(styleNormal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected string GetPointDate()
|
|
|
|
|
{
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
|
|
|
|
// If the date hasn't changed since the last point label, just return the time
|
|
|
|
|
if (now.Date == lastPointDate)
|
|
|
|
|
return now.ToString("T");
|
|
|
|
|
|
|
|
|
|
lastPointDate = now.Date;
|
|
|
|
|
return now.ToString("F");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected void AddPoint(Vector coordinate, string label, string styleMapId, bool includeTimestamp = true)
|
|
|
|
|
{
|
|
|
|
|
var point = new Point { Coordinate = coordinate, AltitudeMode = AltitudeMode.Absolute };
|
|
|
|
|
|
|
|
|
|
var placemark = new Placemark
|
|
|
|
|
{
|
|
|
|
|
Name = includeTimestamp ? $"{label} ({GetPointDate()})" : label,
|
|
|
|
|
StyleUrl = new Uri("#" + styleMapId, UriKind.Relative),
|
|
|
|
|
Geometry = point,
|
|
|
|
|
Visibility = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
rootFolder.AddFeature(placemark);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private const float MetersPerFoot = 0.3048f;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async Task LogPosition(DateTime eventTime, FlightPosition position)
|
|
|
|
|
{
|
2020-09-09 19:20:42 +00:00
|
|
|
|
EnsureOutput(eventTime);
|
|
|
|
|
|
2020-09-02 10:49:09 +00:00
|
|
|
|
var altitudeMeters = position.Altitude * MetersPerFoot;
|
|
|
|
|
var coordinate = new Vector(position.Latitude, position.Longitude, altitudeMeters);
|
|
|
|
|
|
|
|
|
|
if (lastPosition == null)
|
|
|
|
|
AddPoint(coordinate, "Start", "start");
|
|
|
|
|
|
|
|
|
|
if (lastSpeed > 0 && position.Airspeed == 0)
|
|
|
|
|
AddPoint(coordinate, "Full stop", "fullstop");
|
|
|
|
|
|
|
|
|
|
lastPosition = coordinate;
|
|
|
|
|
lastSpeed = position.Airspeed;
|
|
|
|
|
|
|
|
|
|
positionPath.Coordinates.Add(coordinate);
|
|
|
|
|
|
|
|
|
|
await AutoFlush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO log events, engine stop etc.
|
|
|
|
|
}
|
|
|
|
|
}
|