496 lines
21 KiB
C++
496 lines
21 KiB
C++
// Copyright Pacificao. All Rights Reserved.
|
|
|
|
#include "AgrarianWeatherProviderSubsystem.h"
|
|
#include "AgrarianGameState.h"
|
|
#include "Dom/JsonObject.h"
|
|
#include "Engine/World.h"
|
|
#include "HttpModule.h"
|
|
#include "Interfaces/IHttpResponse.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::RequestWeatherForActiveGameState()
|
|
{
|
|
UWorld* World = GetWorld();
|
|
AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
|
|
if (!GameState || !GameState->HasAuthority())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return RequestWeatherForTile(GameState->ActiveSolarTileId, GameState->ActiveTileLatitude, GameState->ActiveTileLongitude);
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::RequestWeatherForTile(FName TileId, float Latitude, float Longitude)
|
|
{
|
|
if (TileId == NAME_None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UWorld* World = GetWorld();
|
|
if (!World || !World->GetAuthGameMode())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AAgrarianGameState* GameState = World->GetGameState<AAgrarianGameState>();
|
|
if (TryApplyCachedSnapshot(TileId, TEXT("open-meteo"), GameState))
|
|
{
|
|
return true;
|
|
}
|
|
if (!bEnableLiveWeatherRequests)
|
|
{
|
|
return ApplyDeterministicFallbackWeather(TileId, Latitude, Longitude, GameState);
|
|
}
|
|
|
|
const FString Url = BuildOpenMeteoForecastUrl(TileId, Latitude, Longitude);
|
|
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
|
|
Request->SetURL(Url);
|
|
Request->SetVerb(TEXT("GET"));
|
|
Request->SetHeader(TEXT("Accept"), TEXT("application/json"));
|
|
Request->SetHeader(TEXT("User-Agent"), TEXT("AgrarianGameMVP/0.1"));
|
|
Request->OnProcessRequestComplete().BindUObject(this, &UAgrarianWeatherProviderSubsystem::OnOpenMeteoResponse, TileId, Latitude, Longitude);
|
|
return Request->ProcessRequest();
|
|
}
|
|
|
|
FString UAgrarianWeatherProviderSubsystem::BuildOpenMeteoForecastUrl(FName TileId, float Latitude, float Longitude) const
|
|
{
|
|
const float ClampedLatitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
|
|
const float ClampedLongitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
|
|
return FString::Printf(
|
|
TEXT("%s?latitude=%.6f&longitude=%.6f¤t=temperature_2m,relative_humidity_2m,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_sum,wind_speed_10m_max&forecast_days=1&timezone=auto"),
|
|
*OpenMeteoForecastEndpoint,
|
|
ClampedLatitude,
|
|
ClampedLongitude);
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::RequestNoaaNwsFallbackForTile(FName TileId, float Latitude, float Longitude)
|
|
{
|
|
if (!bEnableNoaaNwsFallbackForUSTiles || TileId == NAME_None || !IsNoaaNwsEligibleCoordinate(Latitude, Longitude))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UWorld* World = GetWorld();
|
|
if (!World || !World->GetAuthGameMode())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AAgrarianGameState* GameState = World->GetGameState<AAgrarianGameState>();
|
|
if (TryApplyCachedSnapshot(TileId, TEXT("noaa-nws"), GameState))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
|
|
Request->SetURL(BuildNoaaNwsPointsUrl(Latitude, Longitude));
|
|
Request->SetVerb(TEXT("GET"));
|
|
Request->SetHeader(TEXT("Accept"), TEXT("application/geo+json"));
|
|
Request->SetHeader(TEXT("User-Agent"), NoaaNwsUserAgent);
|
|
Request->OnProcessRequestComplete().BindUObject(this, &UAgrarianWeatherProviderSubsystem::OnNoaaNwsPointsResponse, TileId, Latitude, Longitude);
|
|
return Request->ProcessRequest();
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::IsNoaaNwsEligibleCoordinate(float Latitude, float Longitude) const
|
|
{
|
|
return Latitude >= 18.0f && Latitude <= 72.0f && Longitude >= -180.0f && Longitude <= -64.0f;
|
|
}
|
|
|
|
FString UAgrarianWeatherProviderSubsystem::BuildNoaaNwsPointsUrl(float Latitude, float Longitude) const
|
|
{
|
|
return FString::Printf(
|
|
TEXT("%s/%.4f,%.4f"),
|
|
*NoaaNwsPointsEndpoint,
|
|
FMath::Clamp(Latitude, -90.0f, 90.0f),
|
|
FMath::Clamp(Longitude, -180.0f, 180.0f));
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::ApplySnapshotToGameState(const FAgrarianWeatherProviderSnapshot& Snapshot, AAgrarianGameState* GameState) const
|
|
{
|
|
if (!Snapshot.bIsValid || !GameState || !GameState->HasAuthority())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
GameState->ApplyMappedWeatherInputs(MapSnapshotToAgrarianWeatherInputs(Snapshot));
|
|
return true;
|
|
}
|
|
|
|
FAgrarianMappedWeatherInputs UAgrarianWeatherProviderSubsystem::MapSnapshotToAgrarianWeatherInputs(const FAgrarianWeatherProviderSnapshot& Snapshot) const
|
|
{
|
|
FAgrarianMappedWeatherInputs MappedInputs;
|
|
MappedInputs.TemperatureC = Snapshot.CurrentTemperatureC;
|
|
MappedInputs.DailyLowTemperatureC = Snapshot.DailyLowTemperatureC;
|
|
MappedInputs.DailyHighTemperatureC = Snapshot.DailyHighTemperatureC;
|
|
MappedInputs.PrecipitationMm = Snapshot.PrecipitationMm;
|
|
MappedInputs.WindSpeedKmh = Snapshot.WindSpeedKmh;
|
|
MappedInputs.CloudCoverPercent = Snapshot.CloudCoverPercent;
|
|
MappedInputs.RelativeHumidityPercent = Snapshot.RelativeHumidityPercent;
|
|
MappedInputs.PressureMslHpa = Snapshot.PressureMslHpa > 0.0f ? Snapshot.PressureMslHpa : 1013.25f;
|
|
MappedInputs.VisibilityMeters = Snapshot.VisibilityMeters;
|
|
MappedInputs.ProviderWeatherCode = Snapshot.WeatherCode;
|
|
MappedInputs.MappedWeather = Snapshot.MappedWeather;
|
|
MappedInputs.Provider = Snapshot.Provider;
|
|
MappedInputs.ProviderTimestamp = Snapshot.ProviderTimestamp;
|
|
MappedInputs.bHasProviderData = Snapshot.bIsValid;
|
|
return MappedInputs;
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::TryApplyCachedSnapshot(FName TileId, const FString& Provider, AAgrarianGameState* GameState)
|
|
{
|
|
if (!GameState || !GameState->HasAuthority())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FAgrarianCachedWeatherSnapshot* CachedSnapshot = ServerWeatherSnapshotCache.Find(MakeCacheKey(TileId, Provider));
|
|
if (!CachedSnapshot || !CachedSnapshot->IsFresh(FDateTime::UtcNow()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LastSnapshot = CachedSnapshot->Snapshot;
|
|
return ApplySnapshotToGameState(CachedSnapshot->Snapshot, GameState);
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::HasFreshCachedSnapshot(FName TileId, const FString& Provider) const
|
|
{
|
|
const FAgrarianCachedWeatherSnapshot* CachedSnapshot = ServerWeatherSnapshotCache.Find(MakeCacheKey(TileId, Provider));
|
|
return CachedSnapshot && CachedSnapshot->IsFresh(FDateTime::UtcNow());
|
|
}
|
|
|
|
void UAgrarianWeatherProviderSubsystem::ClearWeatherSnapshotCache()
|
|
{
|
|
ServerWeatherSnapshotCache.Empty();
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::ApplyDeterministicFallbackWeather(FName TileId, float Latitude, float Longitude, AAgrarianGameState* GameState)
|
|
{
|
|
if (!bEnableDeterministicFallbackWeather || !GameState || !GameState->HasAuthority() || TileId == NAME_None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FAgrarianWeatherProviderSnapshot Snapshot = BuildDeterministicFallbackSnapshot(
|
|
TileId,
|
|
Latitude,
|
|
Longitude,
|
|
GameState->ActiveDayOfYear,
|
|
GameState->WorldHours);
|
|
LastSnapshot = Snapshot;
|
|
CacheSnapshot(Snapshot, WeatherSnapshotCacheTtlSeconds);
|
|
return ApplySnapshotToGameState(Snapshot, GameState);
|
|
}
|
|
|
|
FAgrarianWeatherProviderSnapshot UAgrarianWeatherProviderSubsystem::BuildDeterministicFallbackSnapshot(FName TileId, float Latitude, float Longitude, int32 DayOfYear, float HourOfDay) const
|
|
{
|
|
const int32 SafeDay = FMath::Clamp(DayOfYear, 1, 366);
|
|
const float SeasonalRadians = ((static_cast<float>(SafeDay) - 172.0f) / 366.0f) * 2.0f * PI;
|
|
const float LatitudeSeasonScale = FMath::Clamp(FMath::Abs(Latitude) / 60.0f, 0.0f, 1.0f);
|
|
const float SeasonalTemperatureC = 12.0f + (FMath::Cos(SeasonalRadians) * 10.0f * LatitudeSeasonScale);
|
|
const float DailySwingC = FMath::Lerp(4.0f, 10.0f, LatitudeSeasonScale);
|
|
const float Noise = GetDeterministicWeatherNoise(TileId, SafeDay, 11);
|
|
const float StormNoise = GetDeterministicWeatherNoise(TileId, SafeDay, 23);
|
|
const float CloudNoise = GetDeterministicWeatherNoise(TileId, SafeDay, 37);
|
|
const float WindNoise = GetDeterministicWeatherNoise(TileId, SafeDay, 53);
|
|
|
|
FAgrarianWeatherProviderSnapshot Snapshot;
|
|
Snapshot.TileId = TileId;
|
|
Snapshot.Latitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
|
|
Snapshot.Longitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
|
|
Snapshot.Provider = TEXT("deterministic-fallback");
|
|
Snapshot.ProviderTimestamp = FString::Printf(TEXT("day-%03d-hour-%02d"), SafeDay, FMath::FloorToInt(FMath::Clamp(HourOfDay, 0.0f, 24.0f)));
|
|
Snapshot.DailyLowTemperatureC = SeasonalTemperatureC - DailySwingC + FMath::Lerp(-2.0f, 2.0f, Noise);
|
|
Snapshot.DailyHighTemperatureC = SeasonalTemperatureC + DailySwingC + FMath::Lerp(-2.0f, 2.0f, Noise);
|
|
Snapshot.CurrentTemperatureC = (Snapshot.DailyLowTemperatureC + Snapshot.DailyHighTemperatureC) * 0.5f;
|
|
Snapshot.CloudCoverPercent = FMath::Clamp(CloudNoise * 100.0f, 0.0f, 100.0f);
|
|
Snapshot.RelativeHumidityPercent = FMath::Clamp(45.0f + (Snapshot.CloudCoverPercent * 0.35f) + FMath::Lerp(-10.0f, 10.0f, Noise), 10.0f, 100.0f);
|
|
Snapshot.WindSpeedKmh = FMath::Clamp(5.0f + (WindNoise * 35.0f), 0.0f, 75.0f);
|
|
Snapshot.PressureMslHpa = FMath::Clamp(1018.0f - (Snapshot.CloudCoverPercent * 0.12f) - (Snapshot.WindSpeedKmh * 0.08f), 960.0f, 1040.0f);
|
|
Snapshot.PrecipitationMm = StormNoise > 0.72f ? FMath::Lerp(0.1f, 8.0f, StormNoise) : 0.0f;
|
|
Snapshot.VisibilityMeters = FMath::Clamp(10000.0f - (Snapshot.CloudCoverPercent * 30.0f) - (Snapshot.PrecipitationMm * 300.0f), 250.0f, 10000.0f);
|
|
Snapshot.WeatherCode = Snapshot.PrecipitationMm > 0.0f ? 61 : 0;
|
|
Snapshot.MappedWeather = MapOpenMeteoWeatherCode(Snapshot.WeatherCode, Snapshot.PrecipitationMm, Snapshot.WindSpeedKmh);
|
|
Snapshot.bIsValid = true;
|
|
return Snapshot;
|
|
}
|
|
|
|
EAgrarianWeatherType UAgrarianWeatherProviderSubsystem::MapOpenMeteoWeatherCode(int32 WeatherCode, float PrecipitationMm, float WindSpeedKmh)
|
|
{
|
|
if (WeatherCode >= 95 || WindSpeedKmh >= 55.0f)
|
|
{
|
|
return EAgrarianWeatherType::Storm;
|
|
}
|
|
if (WeatherCode == 71 || WeatherCode == 73 || WeatherCode == 75 || WeatherCode == 77 || WeatherCode == 85 || WeatherCode == 86)
|
|
{
|
|
return EAgrarianWeatherType::ColdWind;
|
|
}
|
|
if ((WeatherCode >= 51 && WeatherCode <= 67) || (WeatherCode >= 80 && WeatherCode <= 82) || PrecipitationMm > 0.05f)
|
|
{
|
|
return EAgrarianWeatherType::Rain;
|
|
}
|
|
if (WindSpeedKmh >= 35.0f)
|
|
{
|
|
return EAgrarianWeatherType::ColdWind;
|
|
}
|
|
return EAgrarianWeatherType::Clear;
|
|
}
|
|
|
|
float UAgrarianWeatherProviderSubsystem::GetDeterministicWeatherNoise(FName TileId, int32 DayOfYear, int32 Salt) const
|
|
{
|
|
const FString SeedString = FString::Printf(TEXT("%s:%d:%d"), *TileId.ToString(), DayOfYear, Salt);
|
|
return static_cast<float>(GetTypeHash(SeedString) % 10000) / 9999.0f;
|
|
}
|
|
|
|
FString UAgrarianWeatherProviderSubsystem::MakeCacheKey(FName TileId, const FString& Provider) const
|
|
{
|
|
return FString::Printf(TEXT("%s:%s"), *Provider.ToLower(), *TileId.ToString());
|
|
}
|
|
|
|
void UAgrarianWeatherProviderSubsystem::CacheSnapshot(const FAgrarianWeatherProviderSnapshot& Snapshot, float TimeToLiveSeconds)
|
|
{
|
|
if (!Snapshot.bIsValid || Snapshot.TileId == NAME_None || Snapshot.Provider.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FAgrarianCachedWeatherSnapshot CachedSnapshot;
|
|
CachedSnapshot.Snapshot = Snapshot;
|
|
CachedSnapshot.CachedAtUtc = FDateTime::UtcNow();
|
|
CachedSnapshot.TimeToLiveSeconds = FMath::Max(60.0f, TimeToLiveSeconds);
|
|
ServerWeatherSnapshotCache.Add(MakeCacheKey(Snapshot.TileId, Snapshot.Provider), CachedSnapshot);
|
|
}
|
|
|
|
void UAgrarianWeatherProviderSubsystem::OnOpenMeteoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FName TileId, float Latitude, float Longitude)
|
|
{
|
|
if (!bWasSuccessful || !Response.IsValid() || Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
ApplyDeterministicFallbackWeather(TileId, Latitude, Longitude, World ? World->GetGameState<AAgrarianGameState>() : nullptr);
|
|
return;
|
|
}
|
|
|
|
FAgrarianWeatherProviderSnapshot Snapshot;
|
|
if (!ParseOpenMeteoForecast(Response->GetContentAsString(), TileId, Latitude, Longitude, Snapshot))
|
|
{
|
|
UWorld* World = GetWorld();
|
|
ApplyDeterministicFallbackWeather(TileId, Latitude, Longitude, World ? World->GetGameState<AAgrarianGameState>() : nullptr);
|
|
return;
|
|
}
|
|
|
|
LastSnapshot = Snapshot;
|
|
CacheSnapshot(Snapshot, WeatherSnapshotCacheTtlSeconds);
|
|
|
|
UWorld* World = GetWorld();
|
|
AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
|
|
ApplySnapshotToGameState(Snapshot, GameState);
|
|
}
|
|
|
|
void UAgrarianWeatherProviderSubsystem::OnNoaaNwsPointsResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FName TileId, float Latitude, float Longitude)
|
|
{
|
|
if (!bWasSuccessful || !Response.IsValid() || Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TSharedPtr<FJsonObject> RootObject;
|
|
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
|
|
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const TSharedPtr<FJsonObject>* PropertiesObject = nullptr;
|
|
if (!RootObject->TryGetObjectField(TEXT("properties"), PropertiesObject) || !PropertiesObject || !PropertiesObject->IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FString ForecastGridDataUrl;
|
|
if (!(*PropertiesObject)->TryGetStringField(TEXT("forecastGridData"), ForecastGridDataUrl) || ForecastGridDataUrl.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
LastNoaaNwsForecastGridDataUrl = ForecastGridDataUrl;
|
|
|
|
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> GridRequest = FHttpModule::Get().CreateRequest();
|
|
GridRequest->SetURL(ForecastGridDataUrl);
|
|
GridRequest->SetVerb(TEXT("GET"));
|
|
GridRequest->SetHeader(TEXT("Accept"), TEXT("application/geo+json"));
|
|
GridRequest->SetHeader(TEXT("User-Agent"), NoaaNwsUserAgent);
|
|
GridRequest->OnProcessRequestComplete().BindUObject(this, &UAgrarianWeatherProviderSubsystem::OnNoaaNwsGridDataResponse, TileId, Latitude, Longitude);
|
|
GridRequest->ProcessRequest();
|
|
}
|
|
|
|
void UAgrarianWeatherProviderSubsystem::OnNoaaNwsGridDataResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FName TileId, float Latitude, float Longitude)
|
|
{
|
|
if (!bWasSuccessful || !Response.IsValid() || Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
ApplyDeterministicFallbackWeather(TileId, Latitude, Longitude, World ? World->GetGameState<AAgrarianGameState>() : nullptr);
|
|
return;
|
|
}
|
|
|
|
FAgrarianWeatherProviderSnapshot Snapshot;
|
|
if (!ParseNoaaNwsGridData(Response->GetContentAsString(), TileId, Latitude, Longitude, Snapshot))
|
|
{
|
|
UWorld* World = GetWorld();
|
|
ApplyDeterministicFallbackWeather(TileId, Latitude, Longitude, World ? World->GetGameState<AAgrarianGameState>() : nullptr);
|
|
return;
|
|
}
|
|
|
|
LastSnapshot = Snapshot;
|
|
CacheSnapshot(Snapshot, WeatherSnapshotCacheTtlSeconds);
|
|
|
|
UWorld* World = GetWorld();
|
|
AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
|
|
ApplySnapshotToGameState(Snapshot, GameState);
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::ParseOpenMeteoForecast(const FString& ResponseContent, FName TileId, float Latitude, float Longitude, FAgrarianWeatherProviderSnapshot& OutSnapshot) const
|
|
{
|
|
TSharedPtr<FJsonObject> RootObject;
|
|
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseContent);
|
|
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TSharedPtr<FJsonObject>* CurrentObject = nullptr;
|
|
if (!RootObject->TryGetObjectField(TEXT("current"), CurrentObject) || !CurrentObject || !CurrentObject->IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TSharedPtr<FJsonObject>* DailyObject = nullptr;
|
|
if (!RootObject->TryGetObjectField(TEXT("daily"), DailyObject) || !DailyObject || !DailyObject->IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TArray<TSharedPtr<FJsonValue>>* DailyLowValues = nullptr;
|
|
const TArray<TSharedPtr<FJsonValue>>* DailyHighValues = nullptr;
|
|
if (!(*DailyObject)->TryGetArrayField(TEXT("temperature_2m_min"), DailyLowValues)
|
|
|| !(*DailyObject)->TryGetArrayField(TEXT("temperature_2m_max"), DailyHighValues)
|
|
|| !DailyLowValues || !DailyHighValues || DailyLowValues->Num() == 0 || DailyHighValues->Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
double CurrentTemperature = 0.0;
|
|
double RelativeHumidity = 0.0;
|
|
double Precipitation = 0.0;
|
|
double Rain = 0.0;
|
|
double Showers = 0.0;
|
|
double Snowfall = 0.0;
|
|
double WeatherCode = 0.0;
|
|
double CloudCover = 0.0;
|
|
double Pressure = 0.0;
|
|
double WindSpeed = 0.0;
|
|
(*CurrentObject)->TryGetNumberField(TEXT("temperature_2m"), CurrentTemperature);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("relative_humidity_2m"), RelativeHumidity);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("precipitation"), Precipitation);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("rain"), Rain);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("showers"), Showers);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("snowfall"), Snowfall);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("weather_code"), WeatherCode);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("cloud_cover"), CloudCover);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("pressure_msl"), Pressure);
|
|
(*CurrentObject)->TryGetNumberField(TEXT("wind_speed_10m"), WindSpeed);
|
|
|
|
OutSnapshot.TileId = TileId;
|
|
OutSnapshot.Latitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
|
|
OutSnapshot.Longitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
|
|
OutSnapshot.Provider = TEXT("open-meteo");
|
|
(*CurrentObject)->TryGetStringField(TEXT("time"), OutSnapshot.ProviderTimestamp);
|
|
OutSnapshot.CurrentTemperatureC = static_cast<float>(CurrentTemperature);
|
|
OutSnapshot.DailyLowTemperatureC = static_cast<float>((*DailyLowValues)[0]->AsNumber());
|
|
OutSnapshot.DailyHighTemperatureC = static_cast<float>((*DailyHighValues)[0]->AsNumber());
|
|
OutSnapshot.PrecipitationMm = static_cast<float>(FMath::Max(Precipitation, FMath::Max(Rain + Showers, Snowfall)));
|
|
OutSnapshot.WindSpeedKmh = static_cast<float>(WindSpeed);
|
|
OutSnapshot.CloudCoverPercent = static_cast<float>(CloudCover);
|
|
OutSnapshot.RelativeHumidityPercent = static_cast<float>(RelativeHumidity);
|
|
OutSnapshot.PressureMslHpa = static_cast<float>(Pressure);
|
|
OutSnapshot.VisibilityMeters = FMath::Clamp(10000.0f - (OutSnapshot.CloudCoverPercent * 35.0f) - (OutSnapshot.PrecipitationMm * 250.0f), 250.0f, 10000.0f);
|
|
OutSnapshot.WeatherCode = FMath::RoundToInt(WeatherCode);
|
|
OutSnapshot.MappedWeather = MapOpenMeteoWeatherCode(OutSnapshot.WeatherCode, OutSnapshot.PrecipitationMm, OutSnapshot.WindSpeedKmh);
|
|
OutSnapshot.bIsValid = true;
|
|
return true;
|
|
}
|
|
|
|
bool UAgrarianWeatherProviderSubsystem::ParseNoaaNwsGridData(const FString& ResponseContent, FName TileId, float Latitude, float Longitude, FAgrarianWeatherProviderSnapshot& OutSnapshot) const
|
|
{
|
|
TSharedPtr<FJsonObject> RootObject;
|
|
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseContent);
|
|
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TSharedPtr<FJsonObject>* PropertiesObject = nullptr;
|
|
if (!RootObject->TryGetObjectField(TEXT("properties"), PropertiesObject) || !PropertiesObject || !PropertiesObject->IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto ReadFirstGridValue = [](const TSharedPtr<FJsonObject>& Properties, const TCHAR* FieldName, double& OutValue) -> bool
|
|
{
|
|
const TSharedPtr<FJsonObject>* FieldObject = nullptr;
|
|
if (!Properties->TryGetObjectField(FieldName, FieldObject) || !FieldObject || !FieldObject->IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
|
|
if (!(*FieldObject)->TryGetArrayField(TEXT("values"), Values) || !Values || Values->Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TSharedPtr<FJsonObject> FirstValue = (*Values)[0]->AsObject();
|
|
return FirstValue.IsValid() && FirstValue->TryGetNumberField(TEXT("value"), OutValue);
|
|
};
|
|
|
|
double TemperatureC = 0.0;
|
|
if (!ReadFirstGridValue(*PropertiesObject, TEXT("temperature"), TemperatureC))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
double PrecipitationPercent = 0.0;
|
|
double WindSpeedKmh = 0.0;
|
|
double RelativeHumidity = 0.0;
|
|
double SkyCover = 0.0;
|
|
double Pressure = 0.0;
|
|
double Visibility = 0.0;
|
|
ReadFirstGridValue(*PropertiesObject, TEXT("probabilityOfPrecipitation"), PrecipitationPercent);
|
|
ReadFirstGridValue(*PropertiesObject, TEXT("windSpeed"), WindSpeedKmh);
|
|
ReadFirstGridValue(*PropertiesObject, TEXT("relativeHumidity"), RelativeHumidity);
|
|
ReadFirstGridValue(*PropertiesObject, TEXT("skyCover"), SkyCover);
|
|
ReadFirstGridValue(*PropertiesObject, TEXT("pressure"), Pressure);
|
|
ReadFirstGridValue(*PropertiesObject, TEXT("visibility"), Visibility);
|
|
|
|
OutSnapshot.TileId = TileId;
|
|
OutSnapshot.Latitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
|
|
OutSnapshot.Longitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
|
|
OutSnapshot.Provider = TEXT("noaa-nws");
|
|
OutSnapshot.ProviderTimestamp = FDateTime::UtcNow().ToIso8601();
|
|
OutSnapshot.CurrentTemperatureC = static_cast<float>(TemperatureC);
|
|
OutSnapshot.DailyLowTemperatureC = static_cast<float>(TemperatureC - 4.0);
|
|
OutSnapshot.DailyHighTemperatureC = static_cast<float>(TemperatureC + 4.0);
|
|
OutSnapshot.PrecipitationMm = PrecipitationPercent > 30.0 ? 0.1f : 0.0f;
|
|
OutSnapshot.WindSpeedKmh = static_cast<float>(WindSpeedKmh);
|
|
OutSnapshot.CloudCoverPercent = static_cast<float>(SkyCover);
|
|
OutSnapshot.RelativeHumidityPercent = static_cast<float>(RelativeHumidity);
|
|
OutSnapshot.PressureMslHpa = Pressure > 0.0 ? static_cast<float>(Pressure / 100.0) : 1013.25f;
|
|
OutSnapshot.VisibilityMeters = Visibility > 0.0 ? static_cast<float>(Visibility) : FMath::Clamp(10000.0f - (OutSnapshot.CloudCoverPercent * 35.0f), 250.0f, 10000.0f);
|
|
OutSnapshot.WeatherCode = PrecipitationPercent > 30.0 ? 61 : 0;
|
|
OutSnapshot.MappedWeather = MapOpenMeteoWeatherCode(OutSnapshot.WeatherCode, OutSnapshot.PrecipitationMm, OutSnapshot.WindSpeedKmh);
|
|
OutSnapshot.bIsValid = true;
|
|
return true;
|
|
}
|