/*
*openPilotLog - A FOSS Pilot Logbook Application
*Copyright (C) 2020-2021 Felix Turowsky
*
*This program is free software: you can redistribute it and/or modify
*it under the terms of the GNU General Public License as published by
*the Free Software Foundation, either version 3 of the License, or
*(at your option) any later version.
*
*This program is distributed in the hope that it will be useful,
*but WITHOUT ANY WARRANTY; without even the implied warranty of
*MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
*GNU General Public License for more details.
*
*You should have received a copy of the GNU General Public License
*along with this program. If not, see .
*/
#include "acalc.h"
#include "src/functions/alog.h"
#include "src/database/adatabase.h"
#include "src/classes/asettings.h"
#include "src/opl.h"
#include "src/functions/atime.h"
using namespace ACalc;
/*!
* \brief ACalc::formatTimeInput verifies user input and formats to hh:mm
* if the output is not a valid time, an empty string is returned. Accepts
* input as hh:mm, h:mm, hhmm or hmm.
* \param userinput from a QLineEdit
* \return formatted QString "hh:mm" or Empty String
*/
QT_DEPRECATED
QString ACalc::formatTimeInput(QString user_input)
{
QString output; //
QTime temp_time; //empty time object is invalid by default
bool contains_seperator = user_input.contains(':');
if (user_input.length() == 4 && !contains_seperator) {
temp_time = QTime::fromString(user_input, QStringLiteral("hhmm"));
} else if (user_input.length() == 3 && !contains_seperator) {
if (user_input.toInt() < 240) { //Qtime is invalid if time is between 000 and 240 for this case
QString tempstring = user_input.prepend(QStringLiteral("0"));
temp_time = QTime::fromString(tempstring, QStringLiteral("hhmm"));
} else {
temp_time = QTime::fromString(user_input, QStringLiteral("Hmm"));
}
} else if (user_input.length() == 4 && contains_seperator) {
temp_time = QTime::fromString(user_input, QStringLiteral("h:mm"));
} else if (user_input.length() == 5 && contains_seperator) {
temp_time = QTime::fromString(user_input, QStringLiteral("hh:mm"));
}
output = temp_time.toString(QStringLiteral("hh:mm"));
if (output.isEmpty()) {
DEB << "Time input is invalid.";
}
return output;
}
/*!
* The purpose of the following functions is to provide functionality enabling the Calculation of
* night flying time. EASA defines night as follows:
*
* ‘Night’ means the period between the end of evening civil twilight and the beginning of
* morning civil twilight or such other period between sunset and sunrise as may be prescribed
* by the appropriate authority, as defined by the Member State.
*
*
*
* This is the proccess of Calculating night time in this program:
*
* 1) A flight from A to B follows the Great Circle Track along these two points
* at an average cruising height of 11km. (~FL 360)
*
* 2) Any time the Elevation of the Sun at the current position is less
* than -6 degrees, night conditions are present.
* 3) The Calculation is performed for every minute of flight time.
*
* In general, input and output for most functions is decimal degrees, like coordinates
* are stowed in the airports table. Calculations are normally done using
* Radians.
*/
double ACalc::greatCircleDistance(double lat1, double lon1, double lat2, double lon2)
{
// Converting Latitude and Longitude to Radians
lat1 = degToRad(lat1);
lon1 = degToRad(lon1);
lat2 = degToRad(lat2);
lon2 = degToRad(lon2);
// Haversine Formula
double delta_lon = lon2 - lon1;
double delta_lat = lat2 - lat1;
double result = pow(sin(delta_lat / 2), 2) +
cos(lat1) * cos(lat2) * pow(sin(delta_lon / 2), 2);
result = 2 * asin(sqrt(result));
return result;
}
double ACalc::greatCircleDistanceBetweenAirports(const QString &dept, const QString &dest)
{
QString statement = QLatin1String("SELECT lat, long FROM airports WHERE icao = '")
+ dept
+ QLatin1String("' OR icao = '")
+ dest
+ QLatin1String("'");
auto lat_lon = aDB->customQuery(statement, 2);
if (lat_lon.length() != 4) {
DEB << "Invalid input. Aborting.";
return 0;
}
double dept_lat = degToRad(lat_lon[0].toDouble());
double dept_lon = degToRad(lat_lon[1].toDouble());
double dest_lat = degToRad(lat_lon[2].toDouble());
double dest_lon = degToRad(lat_lon[3].toDouble());
// Haversine Formula
double delta_lon = dest_lon - dept_lon;
double delta_lat = dest_lat - dept_lat;
double result = pow(sin(delta_lat / 2), 2) +
cos(dept_lat) * cos(dest_lat) * pow(sin(delta_lon / 2), 2);
result = 2 * asin(sqrt(result));
return radToNauticalMiles(result);
}
QVector> ACalc::intermediatePointsOnGreatCircle(double lat1, double lon1,
double lat2, double lon2, int tblk)
{
double d = greatCircleDistance(lat1, lon1, lat2, lon2); //Calculate distance (Haversine)
// where d is the great circle distance in radians, to get KM take * 6371 (radius of earth).
//DEB << "Distance (nm): " << (d*6371.0088) * 0.5399568; // km->nm = *0.54
// Converting Latitude and Longitude to Radians
lat1 = degToRad(lat1);
lon1 = degToRad(lon1);
lat2 = degToRad(lat2);
lon2 = degToRad(lon2);
//loop for creating one minute steps along the Great Circle
// 0 is departure point, 1 is end point
QVector> coordinates;
double fraction = 1.0 / tblk;
for (int i = 0; i <= tblk; i++) {
// Calculating intermediate point for fraction of distance
double A = sin((1 - fraction * i) * d) / sin(d);
double B = sin(fraction * i * d) / sin(d);
double x = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2);
double y = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2);
double z = A * sin(lat1) + B * sin(lat2);
double lat = atan2(z, sqrt( pow(x, 2) + pow(y, 2) ));
double lon = atan2(y, x);
QVector coordinate = {radToDeg(lat), radToDeg(lon)};
coordinates.prepend(coordinate);
}
return coordinates;
}
double ACalc::solarElevation(QDateTime utc_time_point, double lat, double lon)
{
double Alt = 11; // Assuming 11 kilometers as an average cruising height for a commercial passenger airplane.
// convert current DateTime Object to a J2000 value used in the Calculation
double d = utc_time_point.date().toJulianDay() - 2451544 + utc_time_point.time().hour() / 24.0 +
utc_time_point.time().minute() / 1440.0;
// Orbital Elements (in degress)
double w = 282.9404 + 4.70935e-5 * d; // (longitude of perihelion)
double e = 0.016709 - 1.151e-9 * d; // (eccentricity)
double M = fmod(356.0470 + 0.9856002585 * d,
360.0); // (mean anomaly, needs to be between 0 and 360 degrees)
double oblecl = 23.4393 - 3.563e-7 * d; // (Sun's obliquity of the ecliptic)
double L = w + M; // (Sun's mean longitude)
// auxiliary angle
double E = M + (180 / M_PI) * e * sin(M * (M_PI / 180)) * (1 + e * cos(M * (M_PI / 180)));
// The Sun's rectangular coordinates in the plane of the ecliptic
double x = cos(E * (M_PI / 180)) - e;
double y = sin(E * (M_PI / 180)) * sqrt(1 - pow(e, 2));
// find the distance and true anomaly
double r = sqrt(pow(x, 2) + pow(y, 2));
double v = atan2(y, x) * (180 / M_PI);
// find the longitude of the sun
double solar_longitude = v + w;
// compute the ecliptic rectangular coordinates
double x_eclip = r * cos(solar_longitude * (M_PI / 180));
double y_eclip = r * sin(solar_longitude * (M_PI / 180));
double z_eclip = 0.0;
//rotate these coordinates to equitorial rectangular coordinates
double x_equat = x_eclip;
double y_equat = y_eclip * cos(oblecl * (M_PI / 180)) + z_eclip * sin(oblecl * (M_PI / 180));
double z_equat = y_eclip * sin(23.4406 * (M_PI / 180)) + z_eclip * cos(oblecl * (M_PI / 180));
// convert equatorial rectangular coordinates to RA and Decl:
r = sqrt(pow(x_equat, 2) + pow(y_equat, 2) + pow(z_equat,
2)) - (Alt / 149598000); //roll up the altitude correction
double RA = atan2(y_equat, x_equat) * (180 / M_PI);
double delta = asin(z_equat / r) * (180 / M_PI);
// GET UT in hours time
double uth = utc_time_point.time().hour() + utc_time_point.time().minute() / 60.0 +
utc_time_point.time().second() / 3600.0;
// Calculate local siderial time
double gmst0 = fmod(L + 180, 360.0) / 15;
double sid_time = gmst0 + uth + lon / 15;
// Replace RA with hour angle HA
double HA = (sid_time * 15 - RA);
// convert to rectangular coordinate system
x = cos(HA * (M_PI / 180)) * cos(delta * (M_PI / 180));
//y = sin(HA * (M_PI / 180)) * cos(delta * (M_PI / 180)); //value of y not needed
double z = sin(delta * (M_PI / 180));
// rotate this along an axis going east - west.
double zhor = x * sin((90 - lat) * (M_PI / 180)) + z * cos((90 - lat) * (M_PI / 180));
// Find the Elevation
double elevation = asin(zhor) * (180 / M_PI);
return elevation;
}
int ACalc::calculateNightTime(const QString &dept, const QString &dest, QDateTime departureTime, int tblk, int night_angle)
{
const QString statement = QLatin1String("SELECT lat, long FROM airports WHERE icao = '")
+ dept
+ QLatin1String("' OR icao = '") + dest
+ QLatin1String("'");
auto lat_lon = aDB->customQuery(statement, 2);
double dept_lat;
double dept_lon;
double dest_lat;
double dest_lon;
int night_time = 0;
if (lat_lon.length() == 4) { // normal flight from A to B
dept_lat = lat_lon[0].toDouble();
dept_lon = lat_lon[1].toDouble();
dest_lat = lat_lon[2].toDouble();
dest_lon = lat_lon[3].toDouble();
} else if (lat_lon.length() == 2 ) { // Dept == Dest, i.e. local flight
for (int i = 0; i < tblk; i++) {
dept_lat = lat_lon[0].toDouble();
dept_lon = lat_lon[1].toDouble();
if (solarElevation(departureTime.addSecs(60 * i), dept_lat, dept_lon) < night_angle)
night_time++;
}
return night_time;
} else {
DEB << "Invalid input. Aborting.";
return 0;
}
QVector> route = intermediatePointsOnGreatCircle(dept_lat, dept_lon,
dest_lat, dest_lon,
tblk);
for (int i = 0; i < tblk ; i++) {
if (solarElevation(departureTime.addSecs(60 * i), route[i][0],
route[i][1]) < night_angle) {
night_time ++;
}
}
return night_time;
}
bool ACalc::isNight(const QString &icao, QDateTime event_time, int night_angle)
{
const QString statement = QLatin1String("SELECT lat, long FROM airports WHERE icao = '")
+ icao
+ QLatin1String("'");
auto lat_lon = aDB->customQuery(statement, 2);
if (lat_lon.length() != 2) {
DEB << "Invalid input. Aborting.";
return 0;
}
double lat = lat_lon[0].toDouble();
double lon = lat_lon[1].toDouble();
if(solarElevation(event_time, lat, lon) < night_angle){
return true;
} else {
return false;
}
}
/*!
* \brief ACalc::updateAutoTimes When the details of an aircraft are changed,
* this function recalculates deductable times for this aircraft and updates
* the database accordingly.
* \param acft An aircraft object.
* \return
*/
void ACalc::updateAutoTimes(int acft_id)
{
//find all flights for aircraft
const QString statement = QStringLiteral("SELECT flight_id FROM flights WHERE acft = ") + QString::number(acft_id);
auto flight_list = aDB->customQuery(statement, 1);
if (flight_list.isEmpty()) {
DEB << "No flights for this tail found.";
return;
}
DEB << "Updating " << flight_list.length() << " flights with this aircraft.";
auto acft = aDB->getTailEntry(acft_id);
auto acft_data = acft.getData();
for (const auto& item : flight_list) {
auto flight = aDB->getFlightEntry(item.toInt());
auto flight_data = flight.getData();
if(acft_data.value(OPL::Db::TAILS_MULTIPILOT).toInt() == 0
&& acft_data.value(OPL::Db::TAILS_MULTIENGINE) == 0) {
DEB << "SPSE";
flight_data.insert(OPL::Db::FLIGHTS_TSPSE, flight_data.value(OPL::Db::FLIGHTS_TBLK));
flight_data.insert(OPL::Db::FLIGHTS_TSPME, QString());
flight_data.insert(OPL::Db::FLIGHTS_TMP, QString());
} else if ((acft_data.value(OPL::Db::TAILS_MULTIPILOT) == 0
&& acft.getData().value(OPL::Db::TAILS_MULTIENGINE) == 1)) {
DEB << "SPME";
flight_data.insert(OPL::Db::FLIGHTS_TSPME, flight_data.value(OPL::Db::FLIGHTS_TBLK));
flight_data.insert(OPL::Db::FLIGHTS_TSPSE, QString());
flight_data.insert(OPL::Db::FLIGHTS_TMP, QString());
} else if ((acft_data.value(OPL::Db::TAILS_MULTIPILOT) == 1)) {
DEB << "MPME";
flight_data.insert(OPL::Db::FLIGHTS_TMP, flight_data.value(OPL::Db::FLIGHTS_TBLK));
flight_data.insert(OPL::Db::FLIGHTS_TSPSE, QString());
flight_data.insert(OPL::Db::FLIGHTS_TSPME, QString());
}
flight.setData(flight_data);
aDB->commit(flight);
}
}
/*!
* \brief ACalc::updateNightTimes updates the night times in the database,
* used when changing night angle setting for example
*/
void ACalc::updateNightTimes()
{
const int night_angle = ASettings::read(ASettings::FlightLogging::NightAngle).toInt();
//find all flights for aircraft
auto statement = QStringLiteral("SELECT ROWID FROM flights");
auto flight_list = aDB->customQuery(statement, 1);
if (flight_list.isEmpty()) {
DEB << "No flights found.";
return;
}
DEB << "Updating " << flight_list.length() << " flights in the database.";
for (const auto& item : flight_list) {
auto flt = aDB->getFlightEntry(item.toInt());
auto data = flt.getData();
auto dateTime = QDateTime(QDate::fromString(data.value(OPL::Db::FLIGHTS_DOFT).toString(), Qt::ISODate),
QTime().addSecs(data.value(OPL::Db::FLIGHTS_TOFB).toInt() * 60),
Qt::UTC);
data.insert(OPL::Db::FLIGHTS_TNIGHT,
calculateNightTime(data.value(OPL::Db::FLIGHTS_DEPT).toString(),
data.value(OPL::Db::FLIGHTS_DEST).toString(),
dateTime,
data.value(OPL::Db::FLIGHTS_TBLK).toInt(),
night_angle));
flt.setData(data);
aDB->commit(flt);
}
}