mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 20:19:20 +02:00
969 lines
34 KiB
C++
969 lines
34 KiB
C++
/*
|
|
This Source Code Form is subject to the
|
|
terms of the Mozilla Public License, v.
|
|
2.0. If a copy of the MPL was not
|
|
distributed with this file, You can
|
|
obtain one at
|
|
http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "environment/weather.h"
|
|
|
|
#include "utilities/Globals.h"
|
|
#include "utilities/Logs.h"
|
|
#include "utilities/parser.h"
|
|
#include "simulation/simulationenvironment.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
// name, abbreviation, base, thickness, extinction, cell, spread, bottom, top, top variation, anvil, erosion, stretch, opacity, flat.
|
|
// the extinction of the volumetric genera is about what makes the visibility inside a cloud a few tens of metres (cumulus) to
|
|
// a few hundred (altostratus); of the flat ones it is the optical depth of the sheet
|
|
cloud_genus const GENERA[static_cast<int>(cloud_type::count)]{
|
|
{"cumulus", "Cu", 1100.f, 1200.f, 0.040f, 1400.f, 5000.f, 0.06f, 0.45f, 0.65f, 0.0f, 0.35f, 1.0f, 0.80f, false},
|
|
{"towering_cumulus", "TCu", 900.f, 4000.f, 0.050f, 2500.f, 9000.f, 0.05f, 0.35f, 0.60f, 0.0f, 0.30f, 1.0f, 0.90f, false},
|
|
{"cumulonimbus", "Cb", 800.f, 9000.f, 0.060f, 6000.f, 25000.f, 0.04f, 0.15f, 0.30f, 1.0f, 0.20f, 1.0f, 1.00f, false},
|
|
{"stratocumulus", "Sc", 900.f, 600.f, 0.030f, 900.f, 3500.f, 0.15f, 0.50f, 0.30f, 0.0f, 0.30f, 1.0f, 0.85f, false},
|
|
{"stratus", "St", 300.f, 450.f, 0.020f, 3000.f, 9000.f, 0.25f, 0.30f, 0.10f, 0.0f, 0.10f, 1.5f, 1.00f, false},
|
|
{"nimbostratus", "Ns", 500.f, 3000.f, 0.030f, 4000.f, 15000.f, 0.10f, 0.30f, 0.15f, 0.0f, 0.10f, 1.0f, 1.00f, false},
|
|
{"altocumulus", "Ac", 3500.f, 400.f, 0.020f, 450.f, 3000.f, 0.20f, 0.50f, 0.30f, 0.0f, 0.45f, 1.0f, 0.50f, false},
|
|
{"altostratus", "As", 4000.f, 1500.f, 0.006f, 5000.f, 15000.f, 0.20f, 0.40f, 0.10f, 0.0f, 0.10f, 1.5f, 0.60f, false},
|
|
{"cirrus", "Ci", 8000.f, 800.f, 0.50f, 3000.f, 12000.f, 0.0f, 0.0f, 0.0f, 0.0f, 0.50f, 5.0f, 0.15f, true},
|
|
{"cirrostratus", "Cs", 8500.f, 700.f, 0.30f, 8000.f, 20000.f, 0.0f, 0.0f, 0.0f, 0.0f, 0.20f, 2.0f, 0.25f, true},
|
|
{"cirrocumulus", "Cc", 7500.f, 300.f, 0.25f, 300.f, 3000.f, 0.0f, 0.0f, 0.0f, 0.0f, 0.60f, 1.0f, 0.20f, true},
|
|
};
|
|
|
|
// the old fog (exp2 of the squared distance over the fog range) thinned out a dark object to 5% at 1.7 ranges and to a half at
|
|
// 0.83. the visibility is put where an exponential fog crosses the old one around a third
|
|
float const LEGACY_VISIBILITY_SCALE{2.5f};
|
|
float const FEET{0.3048f};
|
|
float const KNOTS{0.514444f};
|
|
|
|
std::string upper(std::string_view Text)
|
|
{
|
|
std::string result{Text};
|
|
for (auto &c : result)
|
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
return result;
|
|
}
|
|
|
|
std::string lower(std::string_view Text)
|
|
{
|
|
std::string result{Text};
|
|
for (auto &c : result)
|
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
return result;
|
|
}
|
|
|
|
bool all_digits(std::string_view Text)
|
|
{
|
|
return !Text.empty() && std::all_of(Text.begin(), Text.end(), [](char c) { return std::isdigit(static_cast<unsigned char>(c)) != 0; });
|
|
}
|
|
|
|
// a number in the C locale, the whole token
|
|
std::optional<float> number(std::string_view Text)
|
|
{
|
|
if (Text.empty())
|
|
return std::nullopt;
|
|
std::istringstream stream{std::string{Text}};
|
|
stream.imbue(std::locale::classic());
|
|
float value;
|
|
stream >> value;
|
|
if (stream.fail())
|
|
return std::nullopt;
|
|
char rest;
|
|
if (stream >> rest)
|
|
return std::nullopt;
|
|
return value;
|
|
}
|
|
|
|
std::vector<std::string> split(std::string_view Text)
|
|
{
|
|
std::vector<std::string> tokens;
|
|
std::string token;
|
|
for (auto const c : Text)
|
|
{
|
|
// commas and semicolons separate as the spaces do: the text of a memory cell set by an event can't hold spaces
|
|
if (std::isspace(static_cast<unsigned char>(c)) || c == ';' || c == '=' || c == ',')
|
|
{
|
|
if (!token.empty())
|
|
tokens.push_back(token);
|
|
token.clear();
|
|
}
|
|
else
|
|
token += c;
|
|
}
|
|
if (!token.empty())
|
|
tokens.push_back(token);
|
|
return tokens;
|
|
}
|
|
|
|
std::string strip_quotes(std::string Text)
|
|
{
|
|
if (!Text.empty() && Text.front() == '\"')
|
|
Text.erase(0, 1);
|
|
if (!Text.empty() && Text.back() == '\"')
|
|
Text.pop_back();
|
|
return Text;
|
|
}
|
|
|
|
// the genus of a layer of a report, which names only the convective ones: by the height, the amount and the precipitation
|
|
cloud_type report_genus(float const Base, float const Coverage, bool const Precipitation, bool const Showers)
|
|
{
|
|
if (Base < 2000.f)
|
|
{
|
|
if (Coverage >= 0.99f)
|
|
return Precipitation ? cloud_type::nimbostratus : (Base < 600.f ? cloud_type::stratus : cloud_type::stratocumulus);
|
|
if (Coverage > 0.5f)
|
|
return (Showers ? cloud_type::cumulus : (Base < 400.f ? cloud_type::stratus : cloud_type::stratocumulus));
|
|
return cloud_type::cumulus;
|
|
}
|
|
if (Base < 6000.f)
|
|
return (Coverage > 0.5f ? cloud_type::altostratus : cloud_type::altocumulus);
|
|
return (Coverage > 0.5f ? cloud_type::cirrostratus : cloud_type::cirrus);
|
|
}
|
|
|
|
float coverage_from_amount(std::string_view Amount)
|
|
{
|
|
if (Amount == "FEW")
|
|
return 1.5f / 8.f;
|
|
if (Amount == "SCT")
|
|
return 3.5f / 8.f;
|
|
if (Amount == "BKN")
|
|
return 6.f / 8.f;
|
|
if (Amount == "OVC")
|
|
return 1.f;
|
|
return -1.f;
|
|
}
|
|
|
|
// one layer of a typical depth of its genus, below the next height it may not reach
|
|
cloud_layer typical_layer(cloud_type const Type, float const Coverage, float const Base)
|
|
{
|
|
auto const &genus{cloud_genus_info(Type)};
|
|
cloud_layer layer;
|
|
layer.type = Type;
|
|
layer.coverage = Coverage;
|
|
layer.base = Base;
|
|
layer.top = Base + genus.thickness;
|
|
return layer;
|
|
}
|
|
|
|
// reads the body of a weather section (or the text the scripting interface gives) into Conditions
|
|
void parse_section(std::vector<std::string> const &Tokens, weather_conditions &Conditions, std::optional<float> *Elevation, float *Transition)
|
|
{
|
|
std::size_t i{0};
|
|
auto const next_number = [&](float &Value) {
|
|
if (i < Tokens.size())
|
|
{
|
|
if (auto const value = number(Tokens[i]))
|
|
{
|
|
Value = *value;
|
|
++i;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
while (i < Tokens.size())
|
|
{
|
|
auto const keyword{lower(Tokens[i++])};
|
|
if (keyword == "metar")
|
|
{
|
|
// the report is a single quoted token, or the rest of the tokens up to the next keyword of the section
|
|
if (i < Tokens.size())
|
|
parse_metar(strip_quotes(Tokens[i++]), Conditions);
|
|
}
|
|
else if (keyword == "visibility")
|
|
next_number(Conditions.visibility);
|
|
else if (keyword == "haze")
|
|
next_number(Conditions.haze_height);
|
|
else if (keyword == "groundfog")
|
|
{
|
|
next_number(Conditions.groundfog_thickness);
|
|
next_number(Conditions.groundfog_visibility);
|
|
}
|
|
else if (keyword == "fognoise")
|
|
next_number(Conditions.fog_noise);
|
|
else if (keyword == "noclouds" || keyword == "clear")
|
|
Conditions.clouds.clear();
|
|
else if (keyword == "clouds")
|
|
{
|
|
if (i >= Tokens.size())
|
|
break;
|
|
auto const type{cloud_type_from_name(Tokens[i++])};
|
|
if (!type)
|
|
{
|
|
WriteLog("Weather: unknown cloud genus \"" + Tokens[i - 1] + "\"", logtype::file);
|
|
continue;
|
|
}
|
|
auto const &genus{cloud_genus_info(*type)};
|
|
float coverage{0.f};
|
|
if (i < Tokens.size())
|
|
{
|
|
auto const amount{coverage_from_amount(upper(Tokens[i]))};
|
|
if (amount >= 0.f)
|
|
{
|
|
coverage = amount;
|
|
++i;
|
|
}
|
|
else if (next_number(coverage))
|
|
coverage /= 8.f; // oktas
|
|
}
|
|
auto layer{typical_layer(*type, coverage, genus.base)};
|
|
if (next_number(layer.base))
|
|
{
|
|
layer.top = layer.base + genus.thickness;
|
|
next_number(layer.top);
|
|
next_number(layer.density);
|
|
}
|
|
Conditions.clouds.push_back(layer);
|
|
}
|
|
else if (keyword == "precipitation")
|
|
{
|
|
if (i < Tokens.size())
|
|
{
|
|
auto const kind{lower(Tokens[i])};
|
|
auto const known{kind == "none" || kind == "rain" || kind == "snow" || kind == "auto" || kind == "automatic"};
|
|
if (known)
|
|
{
|
|
++i;
|
|
Conditions.precipitation = (kind == "none") ? precipitation_type::none : (kind == "rain") ? precipitation_type::rain : (kind == "snow") ? precipitation_type::snow : precipitation_type::automatic;
|
|
}
|
|
Conditions.precipitation_intensity = (Conditions.precipitation == precipitation_type::none) ? 0.f : 0.5f;
|
|
next_number(Conditions.precipitation_intensity);
|
|
}
|
|
}
|
|
else if (keyword == "cloudwind")
|
|
{
|
|
next_number(Conditions.cloud_wind_direction);
|
|
next_number(Conditions.cloud_wind_speed);
|
|
}
|
|
else if (keyword == "temperature")
|
|
{
|
|
float temperature{0.f};
|
|
if (next_number(temperature))
|
|
Conditions.temperature = temperature;
|
|
}
|
|
else if (keyword == "elevation")
|
|
{
|
|
float elevation{0.f};
|
|
if (next_number(elevation) && (Elevation != nullptr))
|
|
*Elevation = elevation;
|
|
}
|
|
else if (keyword == "seed")
|
|
{
|
|
float seed{0.f};
|
|
if (next_number(seed))
|
|
Conditions.seed = static_cast<std::uint32_t>(std::max(seed, 0.f));
|
|
}
|
|
else if (keyword == "transition")
|
|
{
|
|
float transition{0.f};
|
|
if (next_number(transition) && (Transition != nullptr))
|
|
*Transition = std::max(transition, 0.f);
|
|
}
|
|
else
|
|
WriteLog("Weather: unknown entry \"" + Tokens[i - 1] + "\"", logtype::file);
|
|
}
|
|
Conditions.normalize();
|
|
}
|
|
|
|
bool is_section_keyword(std::string const &Token)
|
|
{
|
|
static std::array<char const *, 14> const keywords{"metar", "visibility", "haze", "groundfog", "fognoise", "noclouds", "clear", "clouds", "precipitation", "cloudwind", "temperature", "elevation", "seed", "transition"};
|
|
auto const token{lower(Token)};
|
|
return std::any_of(keywords.begin(), keywords.end(), [&](char const *Keyword) { return token == Keyword; });
|
|
}
|
|
|
|
} // namespace
|
|
|
|
cloud_genus const &cloud_genus_info(cloud_type const Type)
|
|
{
|
|
auto const index{std::clamp(static_cast<int>(Type), 0, static_cast<int>(cloud_type::count) - 1)};
|
|
return GENERA[index];
|
|
}
|
|
|
|
std::optional<cloud_type> cloud_type_from_name(std::string_view const Name)
|
|
{
|
|
auto const name{lower(Name)};
|
|
for (int i = 0; i < static_cast<int>(cloud_type::count); ++i)
|
|
{
|
|
if (name == GENERA[i].name || name == lower(GENERA[i].abbreviation))
|
|
return static_cast<cloud_type>(i);
|
|
}
|
|
if (name == "toweringcumulus")
|
|
return cloud_type::towering_cumulus;
|
|
return std::nullopt;
|
|
}
|
|
|
|
float weather_conditions::overcast() const
|
|
{
|
|
float clear{1.f};
|
|
for (auto const &layer : clouds)
|
|
{
|
|
auto const &genus{cloud_genus_info(layer.type)};
|
|
clear *= 1.f - std::clamp(layer.coverage * genus.opacity * std::min(layer.density, 1.5f), 0.f, 1.f);
|
|
}
|
|
if ((precipitation != precipitation_type::none) && (precipitation_intensity > 0.f))
|
|
return 1.f + std::clamp(precipitation_intensity, 0.f, 1.f);
|
|
return std::clamp(1.f - clear, 0.f, 1.f);
|
|
}
|
|
|
|
void weather_conditions::normalize()
|
|
{
|
|
for (auto &layer : clouds)
|
|
{
|
|
layer.coverage = std::clamp(layer.coverage, 0.f, 1.f);
|
|
layer.base = std::clamp(layer.base, 0.f, 15000.f);
|
|
layer.top = std::clamp(layer.top, layer.base + 50.f, 20000.f);
|
|
layer.density = std::clamp(layer.density, 0.f, 4.f);
|
|
}
|
|
clouds.erase(std::remove_if(clouds.begin(), clouds.end(), [](cloud_layer const &Layer) { return Layer.coverage <= 0.f; }), clouds.end());
|
|
if (clouds.size() > max_cloud_layers)
|
|
{
|
|
// the ones which cover the most are kept
|
|
std::stable_sort(clouds.begin(), clouds.end(), [](cloud_layer const &Left, cloud_layer const &Right) { return Left.coverage > Right.coverage; });
|
|
clouds.resize(max_cloud_layers);
|
|
}
|
|
std::stable_sort(clouds.begin(), clouds.end(), [](cloud_layer const &Left, cloud_layer const &Right) { return Left.base < Right.base; });
|
|
visibility = std::clamp(visibility, 10.f, 200000.f);
|
|
haze_height = std::clamp(haze_height, 10.f, 10000.f);
|
|
groundfog_thickness = std::clamp(groundfog_thickness, 0.f, 2000.f);
|
|
groundfog_visibility = std::clamp(groundfog_visibility, 10.f, 100000.f);
|
|
fog_noise = std::clamp(fog_noise, 0.f, 1.f);
|
|
precipitation_intensity = std::clamp(precipitation_intensity, 0.f, 1.f);
|
|
cloud_wind_direction = std::fmod(std::fmod(cloud_wind_direction, 360.f) + 360.f, 360.f);
|
|
cloud_wind_speed = std::clamp(cloud_wind_speed, 0.f, 100.f);
|
|
}
|
|
|
|
bool parse_metar(std::string_view const Report, weather_conditions &Conditions)
|
|
{
|
|
// underscores may stand for the spaces too, as in the text of a memory cell
|
|
auto report{upper(Report)};
|
|
std::replace(report.begin(), report.end(), '_', ' ');
|
|
auto const tokens{split(report)};
|
|
bool understood{false};
|
|
bool cloudsgiven{false};
|
|
std::vector<cloud_layer> clouds;
|
|
struct report_layer
|
|
{
|
|
float coverage;
|
|
float base;
|
|
std::string kind;
|
|
};
|
|
std::vector<report_layer> layers;
|
|
std::optional<float> visibility;
|
|
bool fog{false}, shallowfog{false}, patchyfog{false}, mist{false}, haze{false}, showers{false}, thunder{false};
|
|
std::optional<float> verticalvisibility;
|
|
precipitation_type precipitation{precipitation_type::none};
|
|
float intensity{0.f};
|
|
|
|
for (std::size_t i = 0; i < tokens.size(); ++i)
|
|
{
|
|
auto token{tokens[i]};
|
|
if (token == "TEMPO" || token == "BECMG" || token == "RMK" || token == "NOSIG" || token == "TREND")
|
|
break; // forecasts and remarks
|
|
if (token == "METAR" || token == "SPECI" || token == "AUTO" || token == "COR" || token == "NIL")
|
|
continue;
|
|
// wind: dddssKT, dddssGggKT, VRBssKT, MPS or KMH
|
|
{
|
|
auto const unit{token.ends_with("KT") ? 2 : (token.ends_with("MPS") || token.ends_with("KMH")) ? 3 : 0};
|
|
if (unit > 0 && token.size() >= 5 + static_cast<std::size_t>(unit))
|
|
{
|
|
auto const body{token.substr(0, token.size() - unit)};
|
|
auto const direction{body.substr(0, 3)};
|
|
auto speedtext{body.substr(3)};
|
|
if (auto const gust = speedtext.find('G'); gust != std::string::npos)
|
|
speedtext = speedtext.substr(0, gust);
|
|
if ((all_digits(direction) || direction == "VRB") && all_digits(speedtext))
|
|
{
|
|
auto speed{static_cast<float>(std::stoi(speedtext))};
|
|
speed *= token.ends_with("KT") ? KNOTS : token.ends_with("KMH") ? (1.f / 3.6f) : 1.f;
|
|
if (direction != "VRB")
|
|
Conditions.cloud_wind_direction = static_cast<float>(std::stoi(direction));
|
|
// the wind at the clouds is stronger than at the ground
|
|
Conditions.cloud_wind_speed = std::max(speed * 1.5f, 2.f);
|
|
understood = true;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
// variable wind direction range, runway visual range, pressure
|
|
if ((token.size() == 7 && token[3] == 'V' && all_digits(token.substr(0, 3)) && all_digits(token.substr(4))) || (token.size() > 1 && token[0] == 'R' && token.find('/') != std::string::npos) ||
|
|
(token.size() == 5 && (token[0] == 'Q' || token[0] == 'A') && all_digits(token.substr(1))))
|
|
continue;
|
|
// station and time
|
|
if ((token.size() == 4 && std::all_of(token.begin(), token.end(), [](char c) { return std::isalpha(static_cast<unsigned char>(c)) != 0; }) && i == 0) ||
|
|
(token.size() == 7 && token.back() == 'Z' && all_digits(token.substr(0, 6))))
|
|
continue;
|
|
if (token == "CAVOK")
|
|
{
|
|
visibility = 15000.f;
|
|
cloudsgiven = true;
|
|
understood = true;
|
|
continue;
|
|
}
|
|
// visibility in metres, 9999 being 10 km or more
|
|
if (token.size() == 4 && all_digits(token))
|
|
{
|
|
auto const value{static_cast<float>(std::stoi(token))};
|
|
visibility = (value >= 9999.f) ? 15000.f : std::max(value, 50.f);
|
|
understood = true;
|
|
continue;
|
|
}
|
|
if (token.size() > 4 && all_digits(token.substr(0, 4)) && !all_digits(token.substr(4)) && token.substr(4).find_first_not_of("NESW") == std::string::npos)
|
|
continue; // lowest visibility in a direction
|
|
// visibility in statute miles: P6SM, 10SM, 1/2SM, 1 1/2SM
|
|
if (token.ends_with("SM"))
|
|
{
|
|
auto body{token.substr(0, token.size() - 2)};
|
|
float miles{-1.f};
|
|
if (!body.empty() && (body[0] == 'P' || body[0] == 'M'))
|
|
body.erase(0, 1);
|
|
if (auto const slash = body.find('/'); slash != std::string::npos)
|
|
{
|
|
auto const numerator{body.substr(0, slash)}, denominator{body.substr(slash + 1)};
|
|
if (all_digits(numerator) && all_digits(denominator) && std::stoi(denominator) > 0)
|
|
miles = static_cast<float>(std::stoi(numerator)) / static_cast<float>(std::stoi(denominator));
|
|
// a whole number before the fraction
|
|
if (i > 0 && all_digits(tokens[i - 1]) && tokens[i - 1].size() <= 2 && visibility)
|
|
miles += *visibility / 1609.34f;
|
|
}
|
|
else if (all_digits(body))
|
|
miles = static_cast<float>(std::stoi(body));
|
|
if (miles >= 0.f)
|
|
{
|
|
visibility = std::max(miles * 1609.34f, 50.f);
|
|
understood = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (all_digits(token) && token.size() <= 2 && i + 1 < tokens.size() && tokens[i + 1].ends_with("SM"))
|
|
{
|
|
// whole miles before a fraction, added by the fraction
|
|
visibility = static_cast<float>(std::stoi(token)) * 1609.34f;
|
|
continue;
|
|
}
|
|
// clouds
|
|
if (token == "SKC" || token == "CLR" || token == "NSC" || token == "NCD")
|
|
{
|
|
cloudsgiven = true;
|
|
understood = true;
|
|
continue;
|
|
}
|
|
if (token.size() >= 6 && coverage_from_amount(token.substr(0, 3)) >= 0.f)
|
|
{
|
|
auto const height{token.substr(3, 3)};
|
|
auto const kind{token.substr(6)};
|
|
cloudsgiven = true;
|
|
understood = true;
|
|
if (all_digits(height))
|
|
layers.push_back({coverage_from_amount(token.substr(0, 3)), static_cast<float>(std::stoi(height)) * 100.f * FEET, kind});
|
|
continue;
|
|
}
|
|
if (token.size() == 5 && token.starts_with("VV"))
|
|
{
|
|
if (all_digits(token.substr(2)))
|
|
verticalvisibility = static_cast<float>(std::stoi(token.substr(2))) * 100.f * FEET;
|
|
understood = true;
|
|
continue;
|
|
}
|
|
// temperature and dew point
|
|
if (auto const slash = token.find('/'); slash != std::string::npos && slash > 0)
|
|
{
|
|
auto temperature{token.substr(0, slash)};
|
|
auto const negative{temperature[0] == 'M'};
|
|
if (negative)
|
|
temperature.erase(0, 1);
|
|
if (all_digits(temperature) && temperature.size() <= 2)
|
|
{
|
|
Conditions.temperature = static_cast<float>(std::stoi(temperature)) * (negative ? -1.f : 1.f);
|
|
understood = true;
|
|
continue;
|
|
}
|
|
}
|
|
// present weather: intensity, descriptor, phenomena
|
|
{
|
|
std::string_view body{token};
|
|
float strength{0.6f};
|
|
if (body.starts_with('-'))
|
|
{
|
|
strength = 0.3f;
|
|
body.remove_prefix(1);
|
|
}
|
|
else if (body.starts_with('+'))
|
|
{
|
|
strength = 1.f;
|
|
body.remove_prefix(1);
|
|
}
|
|
else if (body.starts_with("VC"))
|
|
{
|
|
// in the vicinity only
|
|
continue;
|
|
}
|
|
bool recognized{false};
|
|
bool shallow{false}, patches{false}, freezing{false};
|
|
while (body.size() >= 2)
|
|
{
|
|
auto const code{body.substr(0, 2)};
|
|
body.remove_prefix(2);
|
|
if (code == "MI") { shallow = true; recognized = true; }
|
|
else if (code == "BC" || code == "PR") { patches = true; recognized = true; }
|
|
else if (code == "SH") { showers = true; recognized = true; }
|
|
else if (code == "TS") { thunder = true; showers = true; recognized = true; }
|
|
else if (code == "FZ") { freezing = true; recognized = true; }
|
|
else if (code == "DR" || code == "BL") { recognized = true; }
|
|
else if (code == "RA" || code == "DZ" || code == "UP")
|
|
{
|
|
if (precipitation != precipitation_type::snow)
|
|
precipitation = precipitation_type::rain;
|
|
intensity = std::max(intensity, strength * (code == "DZ" ? 0.4f : 1.f));
|
|
recognized = true;
|
|
}
|
|
else if (code == "SN" || code == "SG" || code == "PL" || code == "IC" || code == "GS" || code == "GR")
|
|
{
|
|
precipitation = precipitation_type::snow;
|
|
intensity = std::max(intensity, strength);
|
|
recognized = true;
|
|
}
|
|
else if (code == "FG") { fog = true; shallowfog = shallowfog || shallow; patchyfog = patchyfog || patches; recognized = true; }
|
|
else if (code == "BR") { mist = true; recognized = true; }
|
|
else if (code == "HZ" || code == "FU" || code == "DU" || code == "SA" || code == "VA") { haze = true; recognized = true; }
|
|
else
|
|
{
|
|
recognized = false;
|
|
break;
|
|
}
|
|
}
|
|
(void)freezing;
|
|
if (recognized)
|
|
{
|
|
understood = true;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
if (!understood)
|
|
return false;
|
|
|
|
if (visibility)
|
|
Conditions.visibility = *visibility;
|
|
Conditions.groundfog_thickness = 0.f;
|
|
if (fog || verticalvisibility)
|
|
{
|
|
// the reported visibility is the one in the fog; above it the air is clearer
|
|
auto const infog{visibility.value_or(500.f)};
|
|
Conditions.groundfog_visibility = infog;
|
|
Conditions.groundfog_thickness = verticalvisibility ? std::max(*verticalvisibility, 30.f) : shallowfog ? 8.f : 120.f;
|
|
Conditions.visibility = std::max(infog * 6.f, 4000.f);
|
|
Conditions.fog_noise = patchyfog ? 0.8f : shallowfog ? 0.5f : 0.3f;
|
|
}
|
|
else if (mist || haze)
|
|
{
|
|
Conditions.haze_height = mist ? 300.f : 1200.f;
|
|
Conditions.fog_noise = mist ? 0.4f : 0.2f;
|
|
}
|
|
else
|
|
Conditions.haze_height = 800.f;
|
|
if (precipitation != precipitation_type::none)
|
|
{
|
|
Conditions.precipitation = precipitation;
|
|
Conditions.precipitation_intensity = std::clamp(intensity, 0.1f, 1.f);
|
|
}
|
|
else
|
|
{
|
|
Conditions.precipitation = precipitation_type::none;
|
|
Conditions.precipitation_intensity = 0.f;
|
|
}
|
|
if (cloudsgiven || verticalvisibility)
|
|
{
|
|
auto const precipitating{precipitation != precipitation_type::none};
|
|
for (std::size_t l = 0; l < layers.size(); ++l)
|
|
{
|
|
auto const &layer{layers[l]};
|
|
cloud_type type;
|
|
if (layer.kind == "CB")
|
|
type = cloud_type::cumulonimbus;
|
|
else if (layer.kind == "TCU")
|
|
type = cloud_type::towering_cumulus;
|
|
else
|
|
type = report_genus(layer.base, layer.coverage, precipitating && l == 0, showers);
|
|
auto cloud{typical_layer(type, layer.coverage, layer.base)};
|
|
// a layer doesn't reach into the next one reported above it, apart from the towers
|
|
if ((l + 1 < layers.size()) && (type != cloud_type::cumulonimbus) && (type != cloud_type::towering_cumulus))
|
|
cloud.top = std::max(cloud.base + 100.f, std::min(cloud.top, layers[l + 1].base - 100.f));
|
|
clouds.push_back(cloud);
|
|
}
|
|
if (thunder && std::none_of(clouds.begin(), clouds.end(), [](cloud_layer const &Layer) { return Layer.type == cloud_type::cumulonimbus; }))
|
|
clouds.push_back(typical_layer(cloud_type::cumulonimbus, 0.3f, 900.f));
|
|
Conditions.clouds = clouds;
|
|
}
|
|
Conditions.normalize();
|
|
return true;
|
|
}
|
|
|
|
std::string to_metar(weather_conditions const &Conditions)
|
|
{
|
|
std::ostringstream report;
|
|
report.imbue(std::locale::classic());
|
|
auto const knots{static_cast<int>(std::round(Conditions.cloud_wind_speed / 1.5f / KNOTS))};
|
|
report << std::setfill('0') << std::setw(3) << static_cast<int>(std::round(Conditions.cloud_wind_direction / 10.f) * 10.f) % 360 << std::setw(2) << std::min(knots, 99) << "KT ";
|
|
auto const fogged{Conditions.groundfog_thickness > 0.f};
|
|
auto const visibility{fogged ? Conditions.groundfog_visibility : Conditions.visibility};
|
|
if (visibility >= 10000.f)
|
|
report << "9999";
|
|
else
|
|
report << std::setw(4) << static_cast<int>(std::round(visibility / 50.f) * 50.f);
|
|
if (Conditions.precipitation_intensity > 0.f && Conditions.precipitation != precipitation_type::none)
|
|
{
|
|
report << ' ' << (Conditions.precipitation_intensity < 0.4f ? "-" : Conditions.precipitation_intensity > 0.8f ? "+" : "");
|
|
auto const snow{(Conditions.precipitation == precipitation_type::snow) || ((Conditions.precipitation == precipitation_type::automatic) && (Global.AirTemperature <= 1.f))};
|
|
report << (snow ? "SN" : "RA");
|
|
}
|
|
if (fogged)
|
|
report << ' ' << (Conditions.groundfog_thickness < 30.f ? "MIFG" : Conditions.groundfog_visibility < 1000.f ? "FG" : "BR");
|
|
else if (Conditions.visibility < 5000.f)
|
|
report << " BR";
|
|
bool any{false};
|
|
for (auto const &layer : Conditions.clouds)
|
|
{
|
|
if (layer.coverage < 0.03f)
|
|
continue;
|
|
auto const amount{layer.coverage <= 0.25f ? "FEW" : layer.coverage <= 0.5f ? "SCT" : layer.coverage < 0.95f ? "BKN" : "OVC"};
|
|
report << ' ' << amount << std::setw(3) << std::clamp(static_cast<int>(std::round(layer.base / FEET / 100.f)), 0, 999);
|
|
if (layer.type == cloud_type::cumulonimbus)
|
|
report << "CB";
|
|
else if (layer.type == cloud_type::towering_cumulus)
|
|
report << "TCU";
|
|
any = true;
|
|
}
|
|
if (!any)
|
|
report << " NSC";
|
|
if (Conditions.temperature)
|
|
{
|
|
auto const temperature{static_cast<int>(std::round(*Conditions.temperature))};
|
|
report << ' ' << (temperature < 0 ? "M" : "") << std::setw(2) << std::abs(temperature) << '/';
|
|
}
|
|
return report.str();
|
|
}
|
|
|
|
std::vector<cloud_layer> clouds_from_overcast(float const Overcast)
|
|
{
|
|
auto const overcast{std::clamp(Overcast, 0.f, 2.f)};
|
|
auto const sky{std::min(overcast, 1.f)};
|
|
std::vector<cloud_layer> clouds;
|
|
auto const add = [&](cloud_type const Type, float const Coverage, float const Base, float const Top, float const Density = 1.f) {
|
|
clouds.push_back(cloud_layer{Type, Coverage, Base, Top, Density});
|
|
};
|
|
// the high veil is there on most days, hidden once the low clouds close up
|
|
if (sky < 0.95f)
|
|
add(cloud_type::cirrus, 0.15f + 0.3f * sky, 8200.f, 9000.f);
|
|
if (sky <= 0.5f)
|
|
// fair weather cumulus, growing taller with the cover
|
|
add(cloud_type::cumulus, std::max(0.05f, sky * 0.9f), 1200.f, 2000.f + 1600.f * sky);
|
|
else if (sky < 0.9f)
|
|
{
|
|
add(cloud_type::stratocumulus, 0.45f + (sky - 0.5f) * 1.2f, 1000.f, 1700.f);
|
|
add(cloud_type::altocumulus, 0.3f, 3600.f, 4000.f);
|
|
}
|
|
else if (overcast <= 1.f)
|
|
{
|
|
add(cloud_type::stratocumulus, 0.95f + (sky - 0.9f) * 0.5f, 700.f, 1500.f);
|
|
add(cloud_type::altostratus, 0.6f, 3500.f, 5000.f);
|
|
}
|
|
else
|
|
{
|
|
// rain or snow: a thick, low sheet, lower and denser the heavier it falls, with storms in the heaviest
|
|
auto const precipitation{overcast - 1.f};
|
|
add(cloud_type::nimbostratus, 1.f, 600.f - 300.f * precipitation, 3500.f, 1.f + 0.5f * precipitation);
|
|
if (precipitation > 0.6f)
|
|
add(cloud_type::cumulonimbus, 0.3f * (precipitation - 0.6f) / 0.4f, 600.f, 9000.f);
|
|
}
|
|
return clouds;
|
|
}
|
|
|
|
float visibility_from_fog_range(float const Fogend, float const Overcast)
|
|
{
|
|
auto const range{Fogend / std::max(1.f, Overcast * 2.f)};
|
|
return std::clamp(range * LEGACY_VISIBILITY_SCALE, 10.f, 200000.f);
|
|
}
|
|
|
|
float fog_range_from_visibility(float const Visibility, float const Overcast)
|
|
{
|
|
return Visibility / LEGACY_VISIBILITY_SCALE * std::max(1.f, Overcast * 2.f);
|
|
}
|
|
|
|
void world_weather::reset()
|
|
{
|
|
m_scenery.reset();
|
|
m_elevation.reset();
|
|
m_described_clouds = false;
|
|
m_described_fog = false;
|
|
m_known_overcast = -1.f;
|
|
m_known_fogend = -1.f;
|
|
m_transition = 0.0;
|
|
m_transition_time = 0.0;
|
|
m_ground_set = false;
|
|
m_ground = 0.f;
|
|
m_cloud_offset = glm::dvec2{0.0};
|
|
m_time = 0.0;
|
|
}
|
|
|
|
void world_weather::deserialize(cParser &Input)
|
|
{
|
|
std::vector<std::string> tokens;
|
|
auto token{Input.getToken<std::string>(false)};
|
|
while (!token.empty() && lower(token) != "endweather")
|
|
{
|
|
tokens.push_back(token);
|
|
token = Input.getToken<std::string>(false);
|
|
}
|
|
// a later section adds to an earlier one
|
|
auto conditions{m_scenery.value_or(weather_conditions{})};
|
|
parse_section(tokens, conditions, &m_elevation, nullptr);
|
|
m_scenery = conditions;
|
|
}
|
|
|
|
void world_weather::init()
|
|
{
|
|
m_transition = 0.0;
|
|
m_transition_time = 0.0;
|
|
if (m_scenery)
|
|
{
|
|
// the description wins over the atmo section, wherever it was in the scenery
|
|
m_target = *m_scenery;
|
|
if (m_target.temperature)
|
|
Global.AirTemperature = *m_target.temperature;
|
|
m_described_clouds = true;
|
|
m_described_fog = true;
|
|
m_current = m_target;
|
|
write_globals();
|
|
}
|
|
else
|
|
{
|
|
m_target = weather_conditions{};
|
|
follow_overcast();
|
|
follow_fog_range();
|
|
m_current = m_target;
|
|
}
|
|
m_from = m_current;
|
|
if (m_elevation)
|
|
{
|
|
m_ground = *m_elevation;
|
|
m_ground_set = true;
|
|
}
|
|
WriteLog("Weather: " + to_metar(m_current) + (m_scenery ? " (described)" : " (from the overcast and the fog range)"));
|
|
}
|
|
|
|
void world_weather::follow_overcast()
|
|
{
|
|
m_target.clouds = clouds_from_overcast(Global.Overcast);
|
|
m_target.precipitation = precipitation_type::automatic;
|
|
m_target.precipitation_intensity = std::clamp(Global.Overcast - 1.f, 0.f, 1.f);
|
|
m_target.normalize();
|
|
m_known_overcast = Global.Overcast;
|
|
m_described_clouds = false;
|
|
}
|
|
|
|
void world_weather::follow_fog_range()
|
|
{
|
|
m_target.visibility = visibility_from_fog_range(Global.fFogEnd, Global.Overcast);
|
|
m_target.groundfog_thickness = 0.f;
|
|
m_target.haze_height = 800.f;
|
|
m_target.normalize();
|
|
m_known_fogend = Global.fFogEnd;
|
|
m_described_fog = false;
|
|
}
|
|
|
|
void world_weather::write_globals()
|
|
{
|
|
auto changed{false};
|
|
if (m_described_clouds)
|
|
{
|
|
auto const overcast{m_current.overcast()};
|
|
if (overcast != Global.Overcast)
|
|
{
|
|
Global.Overcast = overcast;
|
|
changed = true;
|
|
}
|
|
m_known_overcast = Global.Overcast;
|
|
}
|
|
if (m_described_fog)
|
|
{
|
|
// the fog the old renderers draw, and the scripting interface reads
|
|
auto const fogend{std::clamp(fog_range_from_visibility(std::min(m_current.visibility, m_current.groundfog_thickness > 0.f ? m_current.groundfog_visibility : m_current.visibility), Global.Overcast), 10.f, 50000.f)};
|
|
Global.fFogEnd = fogend;
|
|
m_known_fogend = Global.fFogEnd;
|
|
}
|
|
if (changed)
|
|
{
|
|
// the weather name, the turbidity and the precipitation follow the overcast. the name drives the precipitation renderer,
|
|
// which picks it up by the change of the overcast
|
|
simulation::Environment.compute_weather();
|
|
}
|
|
}
|
|
|
|
void world_weather::update(double const Deltatime, glm::dvec3 const &Camera)
|
|
{
|
|
if (!m_ground_set)
|
|
{
|
|
// the ground below the place the simulation starts at, for want of anything better: the camera is in the cab
|
|
m_ground = static_cast<float>(Camera.y) - 3.f;
|
|
m_ground_set = true;
|
|
}
|
|
// something set the old overcast or fog range: the scenery atmo, the sliders, a script, the network. tiny changes are the way
|
|
// the precipitation is made to update after a change of the season, which keeps the description
|
|
auto const overcastchanged{std::abs(Global.Overcast - m_known_overcast) > 1e-3f};
|
|
if (overcastchanged)
|
|
{
|
|
follow_overcast();
|
|
m_current.clouds = m_target.clouds;
|
|
m_current.precipitation = m_target.precipitation;
|
|
m_current.precipitation_intensity = m_target.precipitation_intensity;
|
|
m_from = m_current;
|
|
}
|
|
else
|
|
m_known_overcast = Global.Overcast;
|
|
// the old fog thickened with the overcast, so it is followed with every change of either
|
|
if ((std::abs(Global.fFogEnd - m_known_fogend) > 0.5f) || (false == m_described_fog && overcastchanged))
|
|
{
|
|
follow_fog_range();
|
|
m_current.visibility = m_target.visibility;
|
|
m_current.groundfog_thickness = m_target.groundfog_thickness;
|
|
m_current.haze_height = m_target.haze_height;
|
|
m_from = m_current;
|
|
}
|
|
|
|
// a change in progress
|
|
if (m_transition > 0.0)
|
|
{
|
|
m_transition_time += Deltatime;
|
|
auto const t{static_cast<float>(std::clamp(m_transition_time / m_transition, 0.0, 1.0))};
|
|
auto mixed{m_target};
|
|
auto const mix = [t](float From, float To) { return From + (To - From) * t; };
|
|
mixed.visibility = std::exp(mix(std::log(m_from.visibility), std::log(m_target.visibility)));
|
|
mixed.haze_height = mix(m_from.haze_height, m_target.haze_height);
|
|
mixed.groundfog_thickness = mix(m_from.groundfog_thickness, m_target.groundfog_thickness);
|
|
mixed.groundfog_visibility = std::exp(mix(std::log(m_from.groundfog_visibility), std::log(m_target.groundfog_visibility)));
|
|
mixed.fog_noise = mix(m_from.fog_noise, m_target.fog_noise);
|
|
auto const falling = [](weather_conditions const &Conditions) { return Conditions.precipitation == precipitation_type::none ? 0.f : Conditions.precipitation_intensity; };
|
|
mixed.precipitation_intensity = mix(falling(m_from), falling(m_target));
|
|
if (mixed.precipitation == precipitation_type::none && mixed.precipitation_intensity > 0.f)
|
|
mixed.precipitation = m_from.precipitation;
|
|
mixed.cloud_wind_speed = mix(m_from.cloud_wind_speed, m_target.cloud_wind_speed);
|
|
auto turn{m_target.cloud_wind_direction - m_from.cloud_wind_direction};
|
|
turn -= 360.f * std::round(turn / 360.f);
|
|
mixed.cloud_wind_direction = m_from.cloud_wind_direction + turn * t;
|
|
// the same layers change their sizes; different ones fade out and in
|
|
auto const same{(m_from.clouds.size() == m_target.clouds.size()) &&
|
|
std::equal(m_from.clouds.begin(), m_from.clouds.end(), m_target.clouds.begin(), [](cloud_layer const &Left, cloud_layer const &Right) { return Left.type == Right.type; })};
|
|
if (same)
|
|
{
|
|
for (std::size_t i = 0; i < mixed.clouds.size(); ++i)
|
|
{
|
|
auto &layer{mixed.clouds[i]};
|
|
auto const &from{m_from.clouds[i]};
|
|
layer.coverage = mix(from.coverage, layer.coverage);
|
|
layer.base = mix(from.base, layer.base);
|
|
layer.top = mix(from.top, layer.top);
|
|
layer.density = mix(from.density, layer.density);
|
|
}
|
|
}
|
|
else if (t < 0.5f)
|
|
{
|
|
mixed.clouds = m_from.clouds;
|
|
for (auto &layer : mixed.clouds)
|
|
layer.coverage *= 1.f - 2.f * t;
|
|
}
|
|
else
|
|
{
|
|
for (auto &layer : mixed.clouds)
|
|
layer.coverage *= 2.f * t - 1.f;
|
|
}
|
|
mixed.clouds.erase(std::remove_if(mixed.clouds.begin(), mixed.clouds.end(), [](cloud_layer const &Layer) { return Layer.coverage <= 0.f; }), mixed.clouds.end());
|
|
m_current = mixed;
|
|
if (t >= 1.f)
|
|
{
|
|
m_transition = 0.0;
|
|
m_current = m_target;
|
|
}
|
|
}
|
|
else
|
|
m_current = m_target;
|
|
|
|
write_globals();
|
|
|
|
// the clouds go with the wind
|
|
m_time += Deltatime;
|
|
m_cloud_offset += glm::dvec2{cloud_velocity()} * Deltatime;
|
|
// kept small, the shaders take it in single precision; the noise of the clouds repeats long before this
|
|
auto const period{1024.0 * 1024.0};
|
|
m_cloud_offset = glm::mod(m_cloud_offset, glm::dvec2{period});
|
|
}
|
|
|
|
glm::vec2 world_weather::cloud_velocity() const
|
|
{
|
|
// where the wind goes to, from the north clockwise. the world has the north at +z and the west at +x
|
|
auto const to{glm::radians(m_current.cloud_wind_direction + 180.f)};
|
|
return glm::vec2{-std::sin(to), std::cos(to)} * m_current.cloud_wind_speed;
|
|
}
|
|
|
|
std::optional<precipitation_type> world_weather::precipitation_override() const
|
|
{
|
|
if (!m_described_clouds)
|
|
return std::nullopt;
|
|
if (m_current.precipitation == precipitation_type::rain || m_current.precipitation == precipitation_type::snow)
|
|
return m_current.precipitation;
|
|
return std::nullopt;
|
|
}
|
|
|
|
void world_weather::apply(weather_conditions const &Conditions, float const Transition, bool const Quiet)
|
|
{
|
|
m_from = m_current;
|
|
m_target = Conditions;
|
|
m_target.normalize();
|
|
if (m_target.temperature)
|
|
Global.AirTemperature = *m_target.temperature;
|
|
m_described_clouds = true;
|
|
m_described_fog = true;
|
|
m_transition = std::max(Transition, 0.f);
|
|
m_transition_time = 0.0;
|
|
if (m_transition <= 0.0)
|
|
m_current = m_target;
|
|
write_globals();
|
|
if (false == Quiet)
|
|
WriteLog("Weather: changed to " + to_metar(m_target) + (m_transition > 0.0 ? " over " + std::to_string(static_cast<int>(m_transition)) + " s" : ""));
|
|
}
|
|
|
|
bool world_weather::apply_text(std::string_view const Text, float const Transition)
|
|
{
|
|
auto tokens{split(Text)};
|
|
if (tokens.empty())
|
|
return false;
|
|
auto conditions{m_target};
|
|
auto transition{Transition};
|
|
if (is_section_keyword(tokens.front()))
|
|
{
|
|
// the quoted report of a section given as a text is split here, glue it back up to the next keyword
|
|
std::vector<std::string> glued;
|
|
for (std::size_t i = 0; i < tokens.size(); ++i)
|
|
{
|
|
glued.push_back(tokens[i]);
|
|
if (lower(tokens[i]) == "metar")
|
|
{
|
|
std::string report;
|
|
while (i + 1 < tokens.size() && !is_section_keyword(tokens[i + 1]))
|
|
report += (report.empty() ? "" : " ") + strip_quotes(tokens[++i]);
|
|
glued.push_back(report);
|
|
}
|
|
}
|
|
parse_section(glued, conditions, nullptr, &transition);
|
|
}
|
|
else if (!parse_metar(Text, conditions))
|
|
return false;
|
|
apply(conditions, transition);
|
|
return true;
|
|
}
|