315 lines
14 KiB
C++
315 lines
14 KiB
C++
// Lot STM1 (STM1-SPEC-11, M2). Voir LPBuildCityLandmarksCommandlet.h.
|
|
|
|
#include "LPBuildCityLandmarksCommandlet.h"
|
|
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "Dom/JsonObject.h"
|
|
#include "Engine/StaticMesh.h"
|
|
#include "LPCityLandmarks.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "StreetMap.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/SavePackage.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCityLandmarks, Log, All);
|
|
|
|
namespace LPBuildCityLandmarks
|
|
{
|
|
// Meme constante que le plugin (StreetMapFactory.cpp) et LPBuildCityWater, en centimetres par degre.
|
|
constexpr double CmPerDegree = 40075036.0 / 360.0 * 100.0;
|
|
|
|
/** Projection du plugin (Sanson-Flamsteed), relative a l'origine, en cm. Lat, lon en degres. */
|
|
FVector2D Project(double Lat, double Lon, double OriginLat, double OriginLon)
|
|
{
|
|
return FVector2D((Lon - OriginLon) * FMath::Cos(FMath::DegreesToRadians(Lat)) * CmPerDegree, -(Lat - OriginLat) * CmPerDegree);
|
|
}
|
|
|
|
/** Centre de boite des points, en float comme GI.FindCenterOfPolygon (LPDataCompat). */
|
|
FVector2D BoxCenter(const TArray<FVector2D>& Points)
|
|
{
|
|
float MinX = float(Points[0].X), MaxX = MinX, MinY = float(Points[0].Y), MaxY = MinY;
|
|
for (const FVector2D& P : Points)
|
|
{
|
|
MinX = FMath::Min(MinX, float(P.X)); MaxX = FMath::Max(MaxX, float(P.X));
|
|
MinY = FMath::Min(MinY, float(P.Y)); MaxY = FMath::Max(MaxY, float(P.Y));
|
|
}
|
|
return FVector2D((double(MinX) + double(MaxX)) / 2.0, (double(MinY) + double(MaxY)) / 2.0);
|
|
}
|
|
|
|
bool IsAllowedRoot(const FString& Root)
|
|
{
|
|
if (Root == TEXT("/Game/Heavy/StreetMap"))
|
|
{
|
|
return true;
|
|
}
|
|
TArray<FString> Parts;
|
|
Root.ParseIntoArray(Parts, TEXT("/"));
|
|
return Parts.Num() == 2 && Parts[0].StartsWith(TEXT("lpm_city_")) && Parts[1] == TEXT("StreetMap");
|
|
}
|
|
}
|
|
|
|
ULPBuildCityLandmarksCommandlet::ULPBuildCityLandmarksCommandlet()
|
|
{
|
|
IsClient = false;
|
|
IsServer = false;
|
|
IsEditor = true;
|
|
LogToConsole = true;
|
|
}
|
|
|
|
int32 ULPBuildCityLandmarksCommandlet::Main(const FString& Params)
|
|
{
|
|
using namespace LPBuildCityLandmarks;
|
|
|
|
TArray<FString> Tokens, Switches;
|
|
TMap<FString, FString> ParamsMap;
|
|
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
|
|
const FString City = ParamsMap.FindRef(TEXT("City"));
|
|
const FString MapRoot = ParamsMap.FindRef(TEXT("MapRoot"));
|
|
const FString OriginText = ParamsMap.FindRef(TEXT("Origin"));
|
|
const FString ListPath = ParamsMap.FindRef(TEXT("List"));
|
|
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
|
|
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
|
|
const bool bAllowMissingMesh = Switches.Contains(TEXT("AllowMissingMesh"));
|
|
|
|
TArray<FString> Report;
|
|
auto Line = [&Report](const FString& Text)
|
|
{
|
|
UE_LOG(LogLPBuildCityLandmarks, Display, TEXT("%s"), *Text);
|
|
Report.Add(Text);
|
|
};
|
|
auto Finish = [&](int32 Code)
|
|
{
|
|
Line(FString::Printf(TEXT("CODE %d"), Code));
|
|
if (!ReportPath.IsEmpty())
|
|
{
|
|
FFileHelper::SaveStringArrayToFile(Report, *ReportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
|
|
}
|
|
return Code;
|
|
};
|
|
|
|
TArray<FString> OriginParts;
|
|
OriginText.ParseIntoArray(OriginParts, TEXT(","));
|
|
if (City.IsEmpty() || MapRoot.IsEmpty() || ListPath.IsEmpty() || OriginParts.Num() != 2)
|
|
{
|
|
Line(TEXT("usage : -City=<Ville> -MapRoot=</Game/Heavy/StreetMap | /lpm_city_<v>/StreetMap> -Origin=<lat>,<lon> -List=<f.json> [-DryRun] [-AllowMissingMesh] [-Report=<f.md>]"));
|
|
return Finish(2);
|
|
}
|
|
const double OriginLat = FCString::Atod(*OriginParts[0]);
|
|
const double OriginLon = FCString::Atod(*OriginParts[1]);
|
|
Line(FString::Printf(TEXT("# LPBuildCityLandmarks %s (%s) ; racine %s ; origine lat %.9f lon %.9f"), *City, bDryRun ? TEXT("DryRun") : TEXT("ecriture"), *MapRoot, OriginLat, OriginLon));
|
|
if (!IsAllowedRoot(MapRoot))
|
|
{
|
|
Line(TEXT("ECHEC : -MapRoot doit etre /Game/Heavy/StreetMap ou /lpm_city_<ville>/StreetMap ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
|
|
const UStreetMap* StreetMap = LoadObject<UStreetMap>(nullptr, *FString::Printf(TEXT("%s/%s.%s"), *MapRoot, *City, *City));
|
|
if (!StreetMap)
|
|
{
|
|
Line(TEXT("ECHEC : UStreetMap introuvable"));
|
|
return Finish(1);
|
|
}
|
|
|
|
FString JsonText;
|
|
TSharedPtr<FJsonObject> Root;
|
|
if (!FFileHelper::LoadFileToString(JsonText, *ListPath) || !FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(JsonText), Root) || !Root.IsValid())
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : liste JSON illisible : %s"), *ListPath));
|
|
return Finish(1);
|
|
}
|
|
const TArray<TSharedPtr<FJsonValue>>* Items = nullptr;
|
|
if (!Root->TryGetArrayField(TEXT("landmarks"), Items) || !Items)
|
|
{
|
|
Line(TEXT("ECHEC : champ « landmarks » absent"));
|
|
return Finish(1);
|
|
}
|
|
|
|
const FString PackageName = FString::Printf(TEXT("%s/Landmarks/%s"), *MapRoot, *City);
|
|
if (!bDryRun && FPackageName::DoesPackageExist(PackageName))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *PackageName));
|
|
return Finish(1);
|
|
}
|
|
UPackage* Package = bDryRun ? GetTransientPackage() : CreatePackage(*PackageName);
|
|
ULPCityLandmarks* Asset = NewObject<ULPCityLandmarks>(Package, bDryRun ? NAME_None : FName(*City), bDryRun ? RF_Transient : (RF_Public | RF_Standalone));
|
|
Asset->City = City;
|
|
|
|
const TArray<FStreetMapBuilding>& Buildings = StreetMap->GetBuildings();
|
|
int32 MissingMeshes = 0, ByBoxTotal = 0;
|
|
Line(TEXT(""));
|
|
Line(TEXT("| monument | X (cm) | Y (cm) | maillage | batiments OSM retenus (noms) | centres a masquer |"));
|
|
Line(TEXT("|---|---|---|---|---|---|"));
|
|
for (const TSharedPtr<FJsonValue>& Item : *Items)
|
|
{
|
|
const TSharedPtr<FJsonObject> Obj = Item->AsObject();
|
|
if (!Obj.IsValid()) continue;
|
|
FLPCityLandmark& Landmark = Asset->Landmarks.AddDefaulted_GetRef();
|
|
Landmark.Name = Obj->GetStringField(TEXT("name"));
|
|
const double Lat = Obj->GetNumberField(TEXT("lat"));
|
|
const double Lon = Obj->GetNumberField(TEXT("lon"));
|
|
Landmark.CenterCm = Project(Lat, Lon, OriginLat, OriginLon);
|
|
double Number = 0.0;
|
|
Landmark.YawDeg = Obj->TryGetNumberField(TEXT("yaw"), Number) ? float(Number) : 0.f;
|
|
Landmark.Scale = Obj->TryGetNumberField(TEXT("scale"), Number) ? float(Number) : 1.f;
|
|
Landmark.ZOffsetCm = Obj->TryGetNumberField(TEXT("z"), Number) ? float(Number) : 0.f;
|
|
const double RadiusCm = Obj->TryGetNumberField(TEXT("hide_radius_m"), Number) ? Number * 100.0 : 0.0;
|
|
Obj->TryGetStringField(TEXT("credits"), Landmark.Credits);
|
|
FString MeshPath;
|
|
Obj->TryGetStringField(TEXT("mesh"), MeshPath);
|
|
Landmark.Mesh = TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(MeshPath));
|
|
const bool bMeshFound = !MeshPath.IsEmpty() && FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(MeshPath));
|
|
MissingMeshes += bMeshFound ? 0 : 1;
|
|
const UStaticMesh* LoadedMesh = bMeshFound ? LoadObject<UStaticMesh>(nullptr, *MeshPath) : nullptr;
|
|
const FBox MeshBox = LoadedMesh ? LoadedMesh->GetBoundingBox() : FBox(ForceInit);
|
|
bool bAlign = false, bCenterFromOsm = false;
|
|
Obj->TryGetBoolField(TEXT("align_osm"), bAlign);
|
|
Obj->TryGetBoolField(TEXT("center_from_osm"), bCenterFromOsm);
|
|
const bool bYawGiven = Obj->HasField(TEXT("yaw"));
|
|
double FootprintMarginM = -1.0;
|
|
Obj->TryGetNumberField(TEXT("hide_footprint_margin_m"), FootprintMarginM);
|
|
// 1.7.2 (STM1-DIAG-16 S) : "hide_box_m": [longueur, largeur, axe] (m, m, degres depuis l'est vers le nord), boite centree
|
|
// sur le monument ; y sont masques les batiments de moins de "hide_max_area_m2" (150) : socles, pylones et kiosques aux
|
|
// culees d'un pont.
|
|
double BoxLenCm = 0.0, BoxWidCm = 0.0, BoxMaxAreaCm2 = 150.0 * 1e4;
|
|
FVector2D BoxDir(1.0, 0.0), BoxPerp(0.0, 1.0);
|
|
const TArray<TSharedPtr<FJsonValue>>* BoxArray = nullptr;
|
|
if (Obj->TryGetArrayField(TEXT("hide_box_m"), BoxArray) && BoxArray->Num() == 3)
|
|
{
|
|
BoxLenCm = (*BoxArray)[0]->AsNumber() * 100.0;
|
|
BoxWidCm = (*BoxArray)[1]->AsNumber() * 100.0;
|
|
const double AxisRad = FMath::DegreesToRadians((*BoxArray)[2]->AsNumber());
|
|
BoxDir = FVector2D(FMath::Cos(AxisRad), -FMath::Sin(AxisRad)); // repere de la carte : Y vers le sud
|
|
BoxPerp = FVector2D(FMath::Sin(AxisRad), FMath::Cos(AxisRad));
|
|
}
|
|
if (Obj->TryGetNumberField(TEXT("hide_max_area_m2"), Number)) BoxMaxAreaCm2 = Number * 1e4;
|
|
|
|
TSet<FString> OsmNames;
|
|
const TArray<TSharedPtr<FJsonValue>>* NameValues = nullptr;
|
|
if (Obj->TryGetArrayField(TEXT("osm_names"), NameValues) && NameValues)
|
|
{
|
|
for (const TSharedPtr<FJsonValue>& V : *NameValues) OsmNames.Add(V->AsString().ToLower());
|
|
}
|
|
// Batiment OSM du monument (le plus grand de ceux qui portent un des noms) : centre et arete la plus longue.
|
|
const FStreetMapBuilding* Named = nullptr;
|
|
double NamedArea = -1.0;
|
|
for (const FStreetMapBuilding& Building : Buildings)
|
|
{
|
|
if (Building.BuildingPoints.Num() < 3 || Building.BuildingName.IsEmpty() || !OsmNames.Contains(Building.BuildingName.ToLower())) continue;
|
|
FVector2D Min(TNumericLimits<double>::Max()), Max(TNumericLimits<double>::Lowest());
|
|
for (const FVector2D& P : Building.BuildingPoints) { Min = FVector2D::Min(Min, P); Max = FVector2D::Max(Max, P); }
|
|
const double Area = (Max.X - Min.X) * (Max.Y - Min.Y);
|
|
if (Area > NamedArea) { NamedArea = Area; Named = &Building; }
|
|
}
|
|
FString AlignNote = TEXT("lacet donne");
|
|
if (Named && bAlign && !bYawGiven && LoadedMesh)
|
|
{
|
|
const TArray<FVector2D>& Pts = Named->BuildingPoints;
|
|
double BestLen = -1.0, EdgeDeg = 0.0;
|
|
for (int32 I = 0; I < Pts.Num(); ++I)
|
|
{
|
|
const FVector2D E = Pts[(I + 1) % Pts.Num()] - Pts[I];
|
|
if (E.Size() > BestLen) { BestLen = E.Size(); EdgeDeg = FMath::RadiansToDegrees(FMath::Atan2(E.Y, E.X)); }
|
|
}
|
|
const bool bModelLongX = MeshBox.GetSize().X >= MeshBox.GetSize().Y;
|
|
Landmark.YawDeg = float(FMath::UnwindDegrees(EdgeDeg - (bModelLongX ? 0.0 : 90.0)));
|
|
AlignNote = FString::Printf(TEXT("lacet OSM %.1f (arete %.0f m, axe long du modele %s)"), Landmark.YawDeg, BestLen / 100.0, bModelLongX ? TEXT("X") : TEXT("Y"));
|
|
}
|
|
// Centre : celui du batiment OSM (boite) si demande ; le pied du maillage est decale pour que le centre de sa boite y tombe.
|
|
FVector2D Target = Landmark.CenterCm;
|
|
if (Named && bCenterFromOsm)
|
|
{
|
|
Target = BoxCenter(Named->BuildingPoints);
|
|
}
|
|
if (LoadedMesh)
|
|
{
|
|
const FVector2D BoxMid(MeshBox.GetCenter().X * Landmark.Scale, MeshBox.GetCenter().Y * Landmark.Scale);
|
|
Landmark.CenterCm = Target - BoxMid.GetRotated(Landmark.YawDeg);
|
|
}
|
|
else
|
|
{
|
|
Landmark.CenterCm = Target;
|
|
}
|
|
// 1.2.1 (STM1-DIAG-02 § 5) : bas reel du maillage (min Z de sa boite, a l'echelle) pose sur le sol de la carte, sauf "ground": false.
|
|
const float GivenZ = Landmark.ZOffsetCm;
|
|
bool bGround = true;
|
|
Obj->TryGetBoolField(TEXT("ground"), bGround);
|
|
const double MinZScaled = LoadedMesh ? MeshBox.Min.Z * Landmark.Scale : 0.0;
|
|
if (LoadedMesh && bGround)
|
|
{
|
|
Landmark.ZOffsetCm = float(GivenZ - MinZScaled);
|
|
}
|
|
|
|
TArray<FString> Matched;
|
|
int32 ByFootprint = 0;
|
|
for (const FStreetMapBuilding& Building : Buildings)
|
|
{
|
|
if (Building.BuildingPoints.Num() < 3) continue;
|
|
const FVector2D Center = BoxCenter(Building.BuildingPoints);
|
|
const bool bByName = !Building.BuildingName.IsEmpty() && OsmNames.Contains(Building.BuildingName.ToLower());
|
|
const bool bByRadius = RadiusCm > 0.0 && FVector2D::Distance(Center, Target) <= RadiusCm;
|
|
bool bByFootprint = false;
|
|
if (FootprintMarginM >= 0.0 && LoadedMesh && Landmark.Scale > 0.f)
|
|
{
|
|
// Centre du batiment ramene dans le repere du maillage (sans echelle), compare a sa boite reduite de la marge.
|
|
const FVector2D Local = (Center - Landmark.CenterCm).GetRotated(-Landmark.YawDeg) / double(Landmark.Scale);
|
|
const double Margin = FootprintMarginM * 100.0 / Landmark.Scale;
|
|
bByFootprint = Local.X >= MeshBox.Min.X + Margin && Local.X <= MeshBox.Max.X - Margin
|
|
&& Local.Y >= MeshBox.Min.Y + Margin && Local.Y <= MeshBox.Max.Y - Margin;
|
|
}
|
|
bool bByBox = false;
|
|
if (BoxLenCm > 0.0 && BoxWidCm > 0.0)
|
|
{
|
|
const FVector2D D = Center - Landmark.CenterCm;
|
|
if (FMath::Abs(FVector2D::DotProduct(D, BoxDir)) <= BoxLenCm * 0.5 && FMath::Abs(FVector2D::DotProduct(D, BoxPerp)) <= BoxWidCm * 0.5)
|
|
{
|
|
const TArray<FVector2D>& Pts = Building.BuildingPoints;
|
|
double Twice = 0.0;
|
|
for (int32 I = 0; I < Pts.Num(); ++I) { const FVector2D& A = Pts[I]; const FVector2D& B = Pts[(I + 1) % Pts.Num()]; Twice += A.X * B.Y - B.X * A.Y; }
|
|
bByBox = FMath::Abs(Twice) * 0.5 < BoxMaxAreaCm2;
|
|
}
|
|
}
|
|
if (bByName || bByRadius || bByFootprint || bByBox)
|
|
{
|
|
Landmark.HideCenters.Add(Center);
|
|
ByFootprint += (bByFootprint && !bByName && !bByRadius) ? 1 : 0;
|
|
ByBoxTotal += (bByBox && !bByName && !bByRadius && !bByFootprint) ? 1 : 0;
|
|
if (Matched.Num() < 12) Matched.Add(Building.BuildingName.IsEmpty() ? FString(TEXT("(sans nom)")) : Building.BuildingName);
|
|
}
|
|
}
|
|
Line(FString::Printf(TEXT("%s : %s ; centre %s ; emprise du modele %.0f x %.0f x %.0f m ; masques par l'emprise %d"),
|
|
*Landmark.Name, *AlignNote, (Named && bCenterFromOsm) ? TEXT("du batiment OSM") : TEXT("des coordonnees"),
|
|
MeshBox.GetSize().X * Landmark.Scale / 100.0, MeshBox.GetSize().Y * Landmark.Scale / 100.0, MeshBox.GetSize().Z * Landmark.Scale / 100.0, ByFootprint));
|
|
Line(FString::Printf(TEXT("%s : Z min du maillage %.0f cm (a l'echelle %.0f cm) ; Z de pose %.0f cm (%s) ; bas en jeu %.0f cm ; lacet %.1f ; echelle %.3f"),
|
|
*Landmark.Name, LoadedMesh ? MeshBox.Min.Z : 0.0, MinZScaled, Landmark.ZOffsetCm, bGround ? TEXT("bas au sol") : TEXT("z donne"),
|
|
Landmark.ZOffsetCm + MinZScaled, Landmark.YawDeg, Landmark.Scale));
|
|
Line(FString::Printf(TEXT("| %s | %.0f | %.0f | %s%s | %d (%s) | %d |"), *Landmark.Name, Landmark.CenterCm.X, Landmark.CenterCm.Y,
|
|
MeshPath.IsEmpty() ? TEXT("-") : *MeshPath, bMeshFound ? TEXT("") : TEXT(" (ABSENT)"), Matched.Num(), *FString::Join(Matched, TEXT(", ")), Landmark.HideCenters.Num()));
|
|
}
|
|
Line(TEXT(""));
|
|
Line(FString::Printf(TEXT("monuments %d ; maillages absents %d ; masques par boite (hide_box_m) %d"), Asset->Landmarks.Num(), MissingMeshes, ByBoxTotal));
|
|
|
|
if (bDryRun)
|
|
{
|
|
Line(FString::Printf(TEXT("DryRun : %s non ecrit"), *PackageName));
|
|
return Finish(0);
|
|
}
|
|
if (MissingMeshes > 0 && !bAllowMissingMesh)
|
|
{
|
|
Line(TEXT("ECHEC : maillages absents (importer les modeles d'abord, ou -AllowMissingMesh) ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
FAssetRegistryModule::AssetCreated(Asset);
|
|
FSavePackageArgs SaveArgs;
|
|
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
const FString FileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
|
|
if (!UPackage::SavePackage(Package, Asset, *FileName, SaveArgs))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *FileName));
|
|
return Finish(1);
|
|
}
|
|
Line(FString::Printf(TEXT("asset ecrit : %s (%s)"), *PackageName, *FileName));
|
|
return Finish(0);
|
|
}
|