Sun-based night dimming; separate marker for Heavy aircraft
Night dimming now uses the actual sunrise/sunset at the active location (classic Almanac algorithm) instead of a fixed 22:00-06:00 window, with a fallback to that fixed window while location or time isn't known yet. Applies to both the backlight (main.cpp) and the radar's own color dimming (radar_screen.cpp). Heavy-category aircraft (ADS-B emitter category "A5") now get a distinct marker (larger circle with an outer ring, plus the usual heading line) instead of sharing the orange "notable" ring, on both the device display and the WebUI radar. Note: the orange "notable" ring itself (military/government callsign prefixes) remains unimplemented - Config::NOTABLE_CALLSIGN_PREFIXES has never been defined anywhere in this project. The WebUI radar's "notable" field is hardcoded false until that's actually specified; on the device it was never wired up in the first place. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ static const char* const I18N_DE[] = {
|
|||||||
"WLAN-Netzwerke verwalten",
|
"WLAN-Netzwerke verwalten",
|
||||||
"Standort-Presets",
|
"Standort-Presets",
|
||||||
"Bildschirm-Timeout: ",
|
"Bildschirm-Timeout: ",
|
||||||
"Nachtdimmung (22-6 Uhr): ",
|
"Nachtdimmung (Sonnenzeit): ",
|
||||||
"Statistik",
|
"Statistik",
|
||||||
"Verlauf",
|
"Verlauf",
|
||||||
"Logbuch-Dateien",
|
"Logbuch-Dateien",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ static const char* const I18N_EN[] = {
|
|||||||
"Manage WiFi networks",
|
"Manage WiFi networks",
|
||||||
"Location presets",
|
"Location presets",
|
||||||
"Screen timeout: ",
|
"Screen timeout: ",
|
||||||
"Night dimming (10pm-6am): ",
|
"Night dimming (sun-based): ",
|
||||||
"Statistics",
|
"Statistics",
|
||||||
"History chart",
|
"History chart",
|
||||||
"Logbook files",
|
"Logbook files",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ static const char* const I18N_ES[] = {
|
|||||||
"Gestionar redes WiFi",
|
"Gestionar redes WiFi",
|
||||||
"Ubicaciones guardadas",
|
"Ubicaciones guardadas",
|
||||||
"Apagado de pantalla: ",
|
"Apagado de pantalla: ",
|
||||||
"Atenuación nocturna (22-6h): ",
|
"Atenuación nocturna (solar): ",
|
||||||
"Estadísticas",
|
"Estadísticas",
|
||||||
"Historial",
|
"Historial",
|
||||||
"Archivos del registro",
|
"Archivos del registro",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ static const char* const I18N_FR[] = {
|
|||||||
"Gérer les réseaux WiFi",
|
"Gérer les réseaux WiFi",
|
||||||
"Positions enregistrées",
|
"Positions enregistrées",
|
||||||
"Extinction écran : ",
|
"Extinction écran : ",
|
||||||
"Assombrissement nuit (22h-6h) : ",
|
"Assombrissement nuit (soleil) : ",
|
||||||
"Statistiques",
|
"Statistiques",
|
||||||
"Historique",
|
"Historique",
|
||||||
"Fichiers du journal",
|
"Fichiers du journal",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ static const char* const I18N_IT[] = {
|
|||||||
"Gestisci reti WiFi",
|
"Gestisci reti WiFi",
|
||||||
"Posizioni salvate",
|
"Posizioni salvate",
|
||||||
"Spegnimento schermo: ",
|
"Spegnimento schermo: ",
|
||||||
"Attenuazione notte (22-6): ",
|
"Attenuazione notte (solare): ",
|
||||||
"Statistiche",
|
"Statistiche",
|
||||||
"Cronologia",
|
"Cronologia",
|
||||||
"File del registro",
|
"File del registro",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ static const char* const I18N_TR[] = {
|
|||||||
"WiFi ağlarını yönet",
|
"WiFi ağlarını yönet",
|
||||||
"Konum ön ayarları",
|
"Konum ön ayarları",
|
||||||
"Ekran zaman aşımı: ",
|
"Ekran zaman aşımı: ",
|
||||||
"Gece kısma (22-06): ",
|
"Gece kısma (güneşe göre): ",
|
||||||
"İstatistikler",
|
"İstatistikler",
|
||||||
"Geçmiş",
|
"Geçmiş",
|
||||||
"Kayıt defteri dosyaları",
|
"Kayıt defteri dosyaları",
|
||||||
|
|||||||
35
src/main.cpp
35
src/main.cpp
@@ -16,6 +16,7 @@
|
|||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
#include "location_manager.h"
|
#include "location_manager.h"
|
||||||
#include "location_presets.h"
|
#include "location_presets.h"
|
||||||
|
#include "sun_times.h"
|
||||||
#include "adsb_client.h"
|
#include "adsb_client.h"
|
||||||
#include "touch_input.h"
|
#include "touch_input.h"
|
||||||
#include "calibration_screen.h"
|
#include "calibration_screen.h"
|
||||||
@@ -408,23 +409,43 @@ void updateStatusLine() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prueft, ob die aktuelle Lokalzeit im Nachtdimm-Fenster (22:00-06:00) liegt.
|
// Prueft, ob gerade Nacht ist (fuer die Nachtdimmung). Nutzt den echten
|
||||||
// Ueber Mitternacht hinweg gerechnet. Solange die Uhrzeit noch nicht per NTP
|
// Sonnenauf-/untergang am aktiven Standort (siehe sun_times.h), NICHT mehr
|
||||||
// synchronisiert ist, wird false zurueckgegeben statt zu raten (gleiche
|
// ein festes 22:00-06:00-Fenster - im Sommer war das vorher oft noch hell
|
||||||
// Pruefung wie in updateStatusLine()).
|
// draussen, wenn schon gedimmt wurde, und im Winter blieb es nach 6 Uhr noch
|
||||||
|
// lange dunkel, ohne dass gedimmt wurde. Solange Standort oder Uhrzeit noch
|
||||||
|
// nicht bekannt sind (z.B. kurz nach dem Start, bevor NTP/GPS/IP-Geolocation
|
||||||
|
// fertig sind), faellt die Funktion auf das alte feste Fenster zurueck,
|
||||||
|
// damit die Nachtdimmung nicht komplett ausfaellt.
|
||||||
bool isNightDimHours() {
|
bool isNightDimHours() {
|
||||||
time_t now = time(nullptr);
|
time_t now = time(nullptr);
|
||||||
if (now <= 8 * 3600 * 2) return false;
|
if (now <= 8 * 3600 * 2) return false;
|
||||||
|
|
||||||
struct tm tmNow;
|
struct tm tmNow;
|
||||||
localtime_r(&now, &tmNow);
|
localtime_r(&now, &tmNow);
|
||||||
|
|
||||||
|
double lat = 0, lon = 0;
|
||||||
|
LocationManager::getHomeLocation(lat, lon);
|
||||||
|
if (lat != 0.0 || lon != 0.0) {
|
||||||
|
SunTimes::Result sun = SunTimes::compute(lat, lon, tmNow.tm_year + 1900, tmNow.tm_mon + 1,
|
||||||
|
tmNow.tm_mday, LocationManager::utcOffsetSeconds());
|
||||||
|
if (sun.valid) {
|
||||||
|
if (sun.alwaysDay) return false;
|
||||||
|
if (sun.alwaysNight) return true;
|
||||||
|
float hourNow = tmNow.tm_hour + tmNow.tm_min / 60.0f;
|
||||||
|
return (hourNow < sun.sunriseHour) || (hourNow >= sun.sunsetHour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Standort noch unbekannt.
|
||||||
int hour = tmNow.tm_hour;
|
int hour = tmNow.tm_hour;
|
||||||
return (hour >= 22 || hour < 6);
|
return (hour >= 22 || hour < 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanfte Nachtdimmung des Backlights zwischen 22:00 und 06:00 Uhr, sofern in
|
// Sanfte Nachtdimmung des Backlights zwischen Sonnenuntergang und
|
||||||
// den Einstellungen aktiviert. Der Inaktivitaets-Timeout (screenDimmed) hat
|
// Sonnenaufgang (siehe isNightDimHours()), sofern in den Einstellungen
|
||||||
// Vorrang und wird hier nicht ueberschrieben.
|
// aktiviert. Der Inaktivitaets-Timeout (screenDimmed) hat Vorrang und wird
|
||||||
|
// hier nicht ueberschrieben.
|
||||||
void updateNightDimming() {
|
void updateNightDimming() {
|
||||||
if (screenDimmed) return;
|
if (screenDimmed) return;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "settings_store.h"
|
#include "settings_store.h"
|
||||||
#include "led_alert.h"
|
#include "led_alert.h"
|
||||||
#include "location_manager.h"
|
#include "location_manager.h"
|
||||||
|
#include "sun_times.h"
|
||||||
#include "world_map.h"
|
#include "world_map.h"
|
||||||
#include "airline_filter.h"
|
#include "airline_filter.h"
|
||||||
#include "aircraft_watchlist.h"
|
#include "aircraft_watchlist.h"
|
||||||
@@ -76,6 +77,7 @@ namespace {
|
|||||||
bool isWatched;
|
bool isWatched;
|
||||||
bool isGroundVehicle;
|
bool isGroundVehicle;
|
||||||
bool isRotorcraft;
|
bool isRotorcraft;
|
||||||
|
bool isHeavy;
|
||||||
};
|
};
|
||||||
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
||||||
HitPoint hitPoints[MAX_HIT_POINTS];
|
HitPoint hitPoints[MAX_HIT_POINTS];
|
||||||
@@ -203,15 +205,33 @@ namespace {
|
|||||||
float prevSweepAngleDeg = -1.0f;
|
float prevSweepAngleDeg = -1.0f;
|
||||||
constexpr float SWEEP_DEGREES_PER_SEC = 45.0f;
|
constexpr float SWEEP_DEGREES_PER_SEC = 45.0f;
|
||||||
|
|
||||||
// Gleiches Nachtdimm-Fenster wie main.cpp::isNightDimHours() (22:00-06:00,
|
// Gleiche Logik wie main.cpp::isNightDimHours() - Nacht = zwischen
|
||||||
// ueber Mitternacht hinweg gerechnet). Hier bewusst dupliziert statt
|
// Sonnenuntergang und Sonnenaufgang am aktiven Standort (siehe
|
||||||
// geteilt, siehe CLAUDE.md-Konvention ("jeder Screen unabhaengig
|
// sun_times.h), mit Rueckfall auf ein festes 22:00-06:00-Fenster,
|
||||||
// lauffaehig, kein gemeinsames Modul fuer solche Kleinigkeiten").
|
// solange Standort/Uhrzeit noch nicht bekannt sind. Hier bewusst
|
||||||
|
// dupliziert statt geteilt, siehe CLAUDE.md-Konvention ("jeder Screen
|
||||||
|
// unabhaengig lauffaehig, kein gemeinsames Modul fuer solche
|
||||||
|
// Kleinigkeiten").
|
||||||
bool isNightHours() {
|
bool isNightHours() {
|
||||||
time_t now = time(nullptr);
|
time_t now = time(nullptr);
|
||||||
if (now <= 8 * 3600 * 2) return false; // Uhrzeit noch nicht per NTP synchronisiert
|
if (now <= 8 * 3600 * 2) return false; // Uhrzeit noch nicht per NTP synchronisiert
|
||||||
|
|
||||||
struct tm tmNow;
|
struct tm tmNow;
|
||||||
localtime_r(&now, &tmNow);
|
localtime_r(&now, &tmNow);
|
||||||
|
|
||||||
|
double lat = 0, lon = 0;
|
||||||
|
LocationManager::getHomeLocation(lat, lon);
|
||||||
|
if (lat != 0.0 || lon != 0.0) {
|
||||||
|
SunTimes::Result sun = SunTimes::compute(lat, lon, tmNow.tm_year + 1900, tmNow.tm_mon + 1,
|
||||||
|
tmNow.tm_mday, LocationManager::utcOffsetSeconds());
|
||||||
|
if (sun.valid) {
|
||||||
|
if (sun.alwaysDay) return false;
|
||||||
|
if (sun.alwaysNight) return true;
|
||||||
|
float hourNow = tmNow.tm_hour + tmNow.tm_min / 60.0f;
|
||||||
|
return (hourNow < sun.sunriseHour) || (hourNow >= sun.sunsetHour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int hour = tmNow.tm_hour;
|
int hour = tmNow.tm_hour;
|
||||||
return (hour >= 22 || hour < 6);
|
return (hour >= 22 || hour < 6);
|
||||||
}
|
}
|
||||||
@@ -352,6 +372,37 @@ namespace {
|
|||||||
gfx.drawLine(x, (int16_t)(y - ROTOR_LEN), x, (int16_t)(y + ROTOR_LEN), color);
|
gfx.drawLine(x, (int16_t)(y - ROTOR_LEN), x, (int16_t)(y + ROTOR_LEN), color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eigener Marker fuer "Heavy"-Flugzeuge (ADS-B-Emitter-Kategorie "A5" -
|
||||||
|
// Flugzeuge ueber 136t Starthoechstgewicht, z.B. A380/B747/B777/A330).
|
||||||
|
// Gleicher Aufbau wie drawAircraftMarker() (Kreis + Kurslinie mit
|
||||||
|
// Pfeilkopf), aber mit groesserem Kreis und einem zusaetzlichen duennen
|
||||||
|
// Aussenring - auf den ersten Blick als "groesser/schwerer" erkennbar,
|
||||||
|
// ohne eine komplett neue Formsprache einzufuehren. Die Farbe bleibt wie
|
||||||
|
// gewohnt die Hoehenfarbe (colorForAltitude()) - die Form transportiert
|
||||||
|
// "Heavy", kein Alarm-/Auffaelligkeitszustand.
|
||||||
|
void drawHeavyMarker(TFT_eSPI& gfx, int16_t x, int16_t y, float headingDeg, uint16_t color) {
|
||||||
|
gfx.fillCircle(x, y, 6, color);
|
||||||
|
gfx.drawCircle(x, y, 8, color);
|
||||||
|
|
||||||
|
double rad = headingDeg * PI / 180.0;
|
||||||
|
int16_t dx = (int16_t)(sin(rad) * 10);
|
||||||
|
int16_t dy = (int16_t)(-cos(rad) * 10);
|
||||||
|
int16_t tipX = x + dx;
|
||||||
|
int16_t tipY = y + dy;
|
||||||
|
gfx.drawLine(x, y, tipX, tipY, color);
|
||||||
|
|
||||||
|
constexpr double WING_ANGLE_RAD = 150.0 * PI / 180.0;
|
||||||
|
constexpr int16_t WING_LEN = 4;
|
||||||
|
double wing1 = rad + WING_ANGLE_RAD;
|
||||||
|
double wing2 = rad - WING_ANGLE_RAD;
|
||||||
|
int16_t w1x = tipX + (int16_t)(sin(wing1) * WING_LEN);
|
||||||
|
int16_t w1y = tipY + (int16_t)(-cos(wing1) * WING_LEN);
|
||||||
|
int16_t w2x = tipX + (int16_t)(sin(wing2) * WING_LEN);
|
||||||
|
int16_t w2y = tipY + (int16_t)(-cos(wing2) * WING_LEN);
|
||||||
|
gfx.drawLine(tipX, tipY, w1x, w1y, color);
|
||||||
|
gfx.drawLine(tipX, tipY, w2x, w2y, color);
|
||||||
|
}
|
||||||
|
|
||||||
void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
|
void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
|
||||||
String s = text;
|
String s = text;
|
||||||
if (gfx.textWidth(s) > maxWidth) {
|
if (gfx.textWidth(s) > maxWidth) {
|
||||||
@@ -391,7 +442,7 @@ namespace {
|
|||||||
|
|
||||||
// Dieselbe colorForAltitude()-Funktion wie fuer die Flugzeug-Marker
|
// Dieselbe colorForAltitude()-Funktion wie fuer die Flugzeug-Marker
|
||||||
// selbst verwenden (mit einer typischen Hoehe je Band) - sonst zeigt
|
// selbst verwenden (mit einer typischen Hoehe je Band) - sonst zeigt
|
||||||
// die Legende bei aktiver Nachtdimmung (22-6 Uhr) die HELLEN Farben,
|
// die Legende bei aktiver Nachtdimmung (Sonnenunter- bis -aufgang) die HELLEN Farben,
|
||||||
// waehrend die Marker/Beschriftungen auf dem Radar bereits gedaempft
|
// waehrend die Marker/Beschriftungen auf dem Radar bereits gedaempft
|
||||||
// sind. Das fuehrte dazu, dass z.B. ein rotes Flugzeug nachts eher
|
// sind. Das fuehrte dazu, dass z.B. ein rotes Flugzeug nachts eher
|
||||||
// dunkelorange wirkte, obwohl die Legende noch reines Rot zeigte.
|
// dunkelorange wirkte, obwohl die Legende noch reines Rot zeigte.
|
||||||
@@ -899,6 +950,9 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
// Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle().
|
// Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle().
|
||||||
bool isGroundVehicle = a.category[0] == 'C';
|
bool isGroundVehicle = a.category[0] == 'C';
|
||||||
bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7';
|
bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7';
|
||||||
|
// ADS-B-Emitter-Kategorie "A5" = "Heavy" - eigene Markerform (siehe
|
||||||
|
// drawHeavyMarker()), unabhaengig von jeglicher Ring-Markierung.
|
||||||
|
bool isHeavy = a.category[0] == 'A' && a.category[1] == '5';
|
||||||
uint16_t color = isGroundVehicle ? colorForGroundVehicle(tft) : colorForAltitude(tft, a.altBaroFt);
|
uint16_t color = isGroundVehicle ? colorForGroundVehicle(tft) : colorForAltitude(tft, a.altBaroFt);
|
||||||
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
|
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
|
||||||
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
|
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
|
||||||
@@ -914,6 +968,8 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
drawGroundVehicleMarker(tft, pt.x, pt.y, color);
|
drawGroundVehicleMarker(tft, pt.x, pt.y, color);
|
||||||
} else if (isRotorcraft) {
|
} else if (isRotorcraft) {
|
||||||
drawHelicopterMarker(tft, pt.x, pt.y, color);
|
drawHelicopterMarker(tft, pt.x, pt.y, color);
|
||||||
|
} else if (isHeavy) {
|
||||||
|
drawHeavyMarker(tft, pt.x, pt.y, a.headingDeg, color);
|
||||||
} else {
|
} else {
|
||||||
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color);
|
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color);
|
||||||
}
|
}
|
||||||
@@ -940,6 +996,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
hitPoints[i].isWatched = isWatched;
|
hitPoints[i].isWatched = isWatched;
|
||||||
hitPoints[i].isGroundVehicle = isGroundVehicle;
|
hitPoints[i].isGroundVehicle = isGroundVehicle;
|
||||||
hitPoints[i].isRotorcraft = isRotorcraft;
|
hitPoints[i].isRotorcraft = isRotorcraft;
|
||||||
|
hitPoints[i].isHeavy = isHeavy;
|
||||||
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
||||||
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 1);
|
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 1);
|
||||||
}
|
}
|
||||||
@@ -1051,6 +1108,8 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
|
|||||||
drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK);
|
drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK);
|
||||||
} else if (hp.isRotorcraft) {
|
} else if (hp.isRotorcraft) {
|
||||||
drawHelicopterMarker(tft, hp.x, hp.y, TFT_BLACK);
|
drawHelicopterMarker(tft, hp.x, hp.y, TFT_BLACK);
|
||||||
|
} else if (hp.isHeavy) {
|
||||||
|
drawHeavyMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
|
||||||
} else {
|
} else {
|
||||||
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
|
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
|
||||||
}
|
}
|
||||||
@@ -1069,6 +1128,8 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
|
|||||||
drawGroundVehicleMarker(tft, hp.x, hp.y, hp.color);
|
drawGroundVehicleMarker(tft, hp.x, hp.y, hp.color);
|
||||||
} else if (hp.isRotorcraft) {
|
} else if (hp.isRotorcraft) {
|
||||||
drawHelicopterMarker(tft, hp.x, hp.y, hp.color);
|
drawHelicopterMarker(tft, hp.x, hp.y, hp.color);
|
||||||
|
} else if (hp.isHeavy) {
|
||||||
|
drawHeavyMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
|
||||||
} else {
|
} else {
|
||||||
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
|
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/sun_times.cpp
Normal file
102
src/sun_times.cpp
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#include "sun_times.h"
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
namespace SunTimes {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr double DEG2RAD = 0.017453292519943295;
|
||||||
|
constexpr double RAD2DEG = 57.29577951308232;
|
||||||
|
|
||||||
|
int dayOfYear(int year, int month, int day) {
|
||||||
|
static const int cum[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
|
||||||
|
int n = cum[month - 1] + day;
|
||||||
|
bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
||||||
|
if (leap && month > 2) n += 1;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Klassischer "Sunrise/Sunset Algorithm" (Almanac for Computers, 1990,
|
||||||
|
// weit verbreitete Formel). sunrise=true fuer Sonnenaufgang, false fuer
|
||||||
|
// Sonnenuntergang. Gibt false zurueck, wenn die Sonne an diesem Tag/
|
||||||
|
// Breitengrad ueberhaupt nicht auf-/untergeht (Polartag/-nacht) - dann
|
||||||
|
// ist outHour unveraendert.
|
||||||
|
bool calc(double lat, double lon, int N, int32_t utcOffsetSeconds, bool sunrise, float& outHour) {
|
||||||
|
double lngHour = lon / 15.0;
|
||||||
|
double t = sunrise ? (N + ((6.0 - lngHour) / 24.0)) : (N + ((18.0 - lngHour) / 24.0));
|
||||||
|
|
||||||
|
double M = (0.9856 * t) - 3.289;
|
||||||
|
|
||||||
|
double L = M + (1.916 * sin(M * DEG2RAD)) + (0.020 * sin(2 * M * DEG2RAD)) + 282.634;
|
||||||
|
L = fmod(L, 360.0);
|
||||||
|
if (L < 0) L += 360.0;
|
||||||
|
|
||||||
|
double RA = RAD2DEG * atan(0.91764 * tan(L * DEG2RAD));
|
||||||
|
RA = fmod(RA, 360.0);
|
||||||
|
if (RA < 0) RA += 360.0;
|
||||||
|
|
||||||
|
// RA muss im selben 90-Grad-Quadranten wie L liegen (atan() liefert
|
||||||
|
// nur Werte im Bereich -90..90 Grad, muss also entsprechend
|
||||||
|
// "zurueckgeklappt" werden).
|
||||||
|
double lQuadrant = floor(L / 90.0) * 90.0;
|
||||||
|
double raQuadrant = floor(RA / 90.0) * 90.0;
|
||||||
|
RA = RA + (lQuadrant - raQuadrant);
|
||||||
|
RA = RA / 15.0; // Grad -> Stunden
|
||||||
|
|
||||||
|
double sinDec = 0.39782 * sin(L * DEG2RAD);
|
||||||
|
double cosDec = cos(asin(sinDec));
|
||||||
|
|
||||||
|
constexpr double ZENITH = 90.833; // offizieller Zenit inkl. atm. Refraktion
|
||||||
|
double cosH = (cos(ZENITH * DEG2RAD) - (sinDec * sin(lat * DEG2RAD))) / (cosDec * cos(lat * DEG2RAD));
|
||||||
|
|
||||||
|
if (cosH > 1.0 || cosH < -1.0) return false; // Polartag/-nacht
|
||||||
|
|
||||||
|
double H = sunrise ? (360.0 - RAD2DEG * acos(cosH)) : (RAD2DEG * acos(cosH));
|
||||||
|
H = H / 15.0;
|
||||||
|
|
||||||
|
double T = H + RA - (0.06571 * t) - 6.622;
|
||||||
|
|
||||||
|
double UT = fmod(T - lngHour, 24.0);
|
||||||
|
if (UT < 0) UT += 24.0;
|
||||||
|
|
||||||
|
double local = fmod(UT + (utcOffsetSeconds / 3600.0), 24.0);
|
||||||
|
if (local < 0) local += 24.0;
|
||||||
|
|
||||||
|
outHour = (float)local;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result compute(double lat, double lon, int year, int month, int day, int32_t utcOffsetSeconds) {
|
||||||
|
Result r;
|
||||||
|
if (lat == 0.0 && lon == 0.0) {
|
||||||
|
r.valid = false;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
int N = dayOfYear(year, month, day);
|
||||||
|
|
||||||
|
float sunrise = 0, sunset = 0;
|
||||||
|
bool riseOk = calc(lat, lon, N, utcOffsetSeconds, true, sunrise);
|
||||||
|
bool setOk = calc(lat, lon, N, utcOffsetSeconds, false, sunset);
|
||||||
|
|
||||||
|
if (!riseOk || !setOk) {
|
||||||
|
// Polartag (Sonne geht nicht unter) vs. Polarnacht (Sonne geht nicht
|
||||||
|
// auf): grobe Naeherung ueber "hohe Sonne im Sommerhalbjahr der
|
||||||
|
// jeweiligen Hemisphaere = Polartag, sonst Polarnacht" - reicht fuer
|
||||||
|
// eine Nachtdimmung an den seltenen Orten/Tagen, an denen das
|
||||||
|
// ueberhaupt vorkommt, voellig aus.
|
||||||
|
bool northernSummerHalf = (N > 79 && N < 265); // grob Fruehlings-/Herbst-Tagundnachtgleiche
|
||||||
|
bool polarDay = (lat > 0) == northernSummerHalf;
|
||||||
|
r.valid = true;
|
||||||
|
r.alwaysDay = polarDay;
|
||||||
|
r.alwaysNight = !polarDay;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
r.valid = true;
|
||||||
|
r.sunriseHour = sunrise;
|
||||||
|
r.sunsetHour = sunset;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/sun_times.h
Normal file
24
src/sun_times.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Berechnet Sonnenauf-/untergang (lokale Zeit) fuer einen gegebenen Ort und
|
||||||
|
// Tag - Grundlage fuer die standortbasierte Nachtdimmung (siehe main.cpp::
|
||||||
|
// isNightDimHours()), die das vorherige feste 22:00-06:00-Fenster ersetzt.
|
||||||
|
namespace SunTimes {
|
||||||
|
|
||||||
|
struct Result {
|
||||||
|
bool valid = false; // false = Standort noch unbekannt (lat/lon == 0,0)
|
||||||
|
bool alwaysDay = false; // Polartag: Sonne geht heute an diesem Ort nicht unter
|
||||||
|
bool alwaysNight = false; // Polarnacht: Sonne geht heute an diesem Ort nicht auf
|
||||||
|
float sunriseHour = 0; // Lokale Dezimalstunde (0-24), nur gueltig wenn weder
|
||||||
|
float sunsetHour = 0; // alwaysDay noch alwaysNight gesetzt ist.
|
||||||
|
};
|
||||||
|
|
||||||
|
// year/month/day sind lokale Kalenderdatumswerte (z.B. aus localtime_r()),
|
||||||
|
// utcOffsetSeconds die bekannte Zeitzonenverschiebung (siehe
|
||||||
|
// LocationManager::utcOffsetSeconds()). Nutzt den klassischen "Almanac"-
|
||||||
|
// Sonnenauf-/untergangsalgorithmus (Sonnenzenit 90.833 Grad, beruecksichtigt
|
||||||
|
// atmosphaerische Refraktion) - Genauigkeit liegt typischerweise im Bereich
|
||||||
|
// weniger Minuten, fuer eine sanfte Backlight-Dimmung mehr als ausreichend.
|
||||||
|
Result compute(double lat, double lon, int year, int month, int day, int32_t utcOffsetSeconds);
|
||||||
|
}
|
||||||
@@ -117,6 +117,8 @@ namespace {
|
|||||||
html += "ctx.fillStyle=color;ctx.strokeStyle=color;";
|
html += "ctx.fillStyle=color;ctx.strokeStyle=color;";
|
||||||
html += "if(a.ground_vehicle){ctx.fillRect(x-3,y-3,6,6);}";
|
html += "if(a.ground_vehicle){ctx.fillRect(x-3,y-3,6,6);}";
|
||||||
html += "else if(a.rotorcraft){ctx.beginPath();ctx.moveTo(x,y-5);ctx.lineTo(x+5,y);ctx.lineTo(x,y+5);ctx.lineTo(x-5,y);ctx.closePath();ctx.fill();}";
|
html += "else if(a.rotorcraft){ctx.beginPath();ctx.moveTo(x,y-5);ctx.lineTo(x+5,y);ctx.lineTo(x,y+5);ctx.lineTo(x-5,y);ctx.closePath();ctx.fill();}";
|
||||||
|
html += "else if(a.heavy){ctx.beginPath();ctx.arc(x,y,5,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.arc(x,y,7,0,Math.PI*2);ctx.stroke();";
|
||||||
|
html += "var hr2=a.track_deg*Math.PI/180;ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(x+10*Math.sin(hr2),y-10*Math.cos(hr2));ctx.stroke();}";
|
||||||
html += "else{ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);ctx.fill();";
|
html += "else{ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);ctx.fill();";
|
||||||
html += "var hr=a.track_deg*Math.PI/180;ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(x+10*Math.sin(hr),y-10*Math.cos(hr));ctx.stroke();}";
|
html += "var hr=a.track_deg*Math.PI/180;ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(x+10*Math.sin(hr),y-10*Math.cos(hr));ctx.stroke();}";
|
||||||
html += "if(a.emergency){ctx.strokeStyle='#ff3b3b';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
html += "if(a.emergency){ctx.strokeStyle='#ff3b3b';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
||||||
@@ -366,9 +368,16 @@ namespace {
|
|||||||
if (AirlineFilter::isHidden(a.callsign)) continue;
|
if (AirlineFilter::isHidden(a.callsign)) continue;
|
||||||
|
|
||||||
bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7';
|
bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7';
|
||||||
|
bool isHeavy = isHeavyCategoryWeb(a.category);
|
||||||
bool isEmergency = emergencyOn && isEmergencySquawkWeb(a.squawk);
|
bool isEmergency = emergencyOn && isEmergencySquawkWeb(a.squawk);
|
||||||
bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign);
|
bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign);
|
||||||
bool isNotable = isHeavyCategoryWeb(a.category);
|
// "notable" (oranger Ring) ist fuer auffaellige Rufzeichen
|
||||||
|
// (Militaer/Regierung) reserviert - Heavy-Flugzeuge bekommen
|
||||||
|
// stattdessen die eigene Markerform (siehe "heavy" unten). Es
|
||||||
|
// gibt aktuell aber keine Militaer-/Regierungs-Praefixliste im
|
||||||
|
// Projekt (auch nicht am Geraete-Display, siehe radar_screen.cpp)
|
||||||
|
// - "notable" bleibt daher bis auf Weiteres immer false.
|
||||||
|
bool isNotable = false;
|
||||||
|
|
||||||
JsonObject o = arr.add<JsonObject>();
|
JsonObject o = arr.add<JsonObject>();
|
||||||
o["hex"] = a.hex;
|
o["hex"] = a.hex;
|
||||||
@@ -379,6 +388,7 @@ namespace {
|
|||||||
o["track_deg"] = a.headingDeg;
|
o["track_deg"] = a.headingDeg;
|
||||||
o["ground_vehicle"] = isGroundVehicle;
|
o["ground_vehicle"] = isGroundVehicle;
|
||||||
o["rotorcraft"] = isRotorcraft;
|
o["rotorcraft"] = isRotorcraft;
|
||||||
|
o["heavy"] = isHeavy;
|
||||||
o["emergency"] = isEmergency;
|
o["emergency"] = isEmergency;
|
||||||
o["watched"] = isWatched;
|
o["watched"] = isWatched;
|
||||||
o["notable"] = isNotable;
|
o["notable"] = isNotable;
|
||||||
|
|||||||
Reference in New Issue
Block a user