// 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& 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 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 Tokens, Switches; TMap 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 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 OriginParts; OriginText.ParseIntoArray(OriginParts, TEXT(",")); if (City.IsEmpty() || MapRoot.IsEmpty() || ListPath.IsEmpty() || OriginParts.Num() != 2) { Line(TEXT("usage : -City= -MapRoot=/StreetMap> -Origin=, -List= [-DryRun] [-AllowMissingMesh] [-Report=]")); 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_/StreetMap ; aucun asset ecrit")); return Finish(1); } const UStreetMap* StreetMap = LoadObject(nullptr, *FString::Printf(TEXT("%s/%s.%s"), *MapRoot, *City, *City)); if (!StreetMap) { Line(TEXT("ECHEC : UStreetMap introuvable")); return Finish(1); } FString JsonText; TSharedPtr 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>* 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(Package, bDryRun ? NAME_None : FName(*City), bDryRun ? RF_Transient : (RF_Public | RF_Standalone)); Asset->City = City; const TArray& 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& Item : *Items) { const TSharedPtr 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(FSoftObjectPath(MeshPath)); const bool bMeshFound = !MeshPath.IsEmpty() && FPackageName::DoesPackageExist(FPackageName::ObjectPathToPackageName(MeshPath)); MissingMeshes += bMeshFound ? 0 : 1; const UStaticMesh* LoadedMesh = bMeshFound ? LoadObject(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>* 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 OsmNames; const TArray>* NameValues = nullptr; if (Obj->TryGetArrayField(TEXT("osm_names"), NameValues) && NameValues) { for (const TSharedPtr& 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::Max()), Max(TNumericLimits::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& 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 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& 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); }