1012 lines
46 KiB
C++
1012 lines
46 KiB
C++
// Lot STM1 (STM1-SPEC-03, pilote O-B ; STM1-IMPL-02). Voir LPBuildCityWaterCommandlet.h.
|
|
|
|
#include "LPBuildCityWaterCommandlet.h"
|
|
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "CompGeom/PolygonTriangulation.h"
|
|
#include "ConstrainedDelaunay2.h"
|
|
#include "Curve/GeneralPolygon2.h"
|
|
#include "LPCityOsm.h"
|
|
#include "LPCityWaterMask.h"
|
|
#include "Engine/StaticMesh.h"
|
|
#include "Materials/MaterialInterface.h"
|
|
#include "MeshDescription.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "Polygon2.h"
|
|
#include "StaticMeshAttributes.h"
|
|
#include "StreetMap.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/SavePackage.h"
|
|
#include "XmlFile.h"
|
|
#include "MaterialEditingLibrary.h"
|
|
#include "Materials/Material.h"
|
|
#include "Materials/MaterialExpressionConstant.h"
|
|
#include "Materials/MaterialExpressionConstant3Vector.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCityWater, Log, All);
|
|
|
|
using namespace UE::Geometry;
|
|
|
|
namespace LPCityWater
|
|
{
|
|
// Meme constante que le plugin (StreetMapFactory.cpp:8-9), en centimetres par degre.
|
|
constexpr double CmPerDegree = 40075036.0 / 360.0 * 100.0;
|
|
|
|
/** Point OSM : X = longitude, Y = latitude. */
|
|
using FLonLat = FVector2d;
|
|
|
|
struct FOsmWay
|
|
{
|
|
int64 Id = 0;
|
|
TArray<FLonLat> Points;
|
|
TMap<FString, FString> Tags;
|
|
// STM1-B2 (R1) : centre de <bounds>. Une sortie Overpass "out tags geom" ne donne que la boite, pas les noeuds.
|
|
FLonLat BoundsCenter = FLonLat::ZeroVector;
|
|
bool bHasBounds = false;
|
|
};
|
|
|
|
struct FOsmMember
|
|
{
|
|
FString Role;
|
|
TArray<FLonLat> Points;
|
|
};
|
|
|
|
struct FOsmRelation
|
|
{
|
|
int64 Id = 0;
|
|
TArray<FOsmMember> Members;
|
|
TMap<FString, FString> Tags;
|
|
};
|
|
|
|
/** Points <nd lat=.. lon=..> d'un noeud XML (voie ou membre). */
|
|
void ReadPoints(const FXmlNode* Node, TArray<FLonLat>& Out)
|
|
{
|
|
for (const FXmlNode* Child : Node->GetChildrenNodes())
|
|
{
|
|
if (Child->GetTag() == TEXT("nd"))
|
|
{
|
|
const FString Lat = Child->GetAttribute(TEXT("lat"));
|
|
const FString Lon = Child->GetAttribute(TEXT("lon"));
|
|
if (!Lat.IsEmpty() && !Lon.IsEmpty())
|
|
{
|
|
Out.Add(FLonLat(FCString::Atod(*Lon), FCString::Atod(*Lat)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ReadTags(const FXmlNode* Node, TMap<FString, FString>& Out)
|
|
{
|
|
for (const FXmlNode* Child : Node->GetChildrenNodes())
|
|
{
|
|
if (Child->GetTag() == TEXT("tag"))
|
|
{
|
|
Out.Add(Child->GetAttribute(TEXT("k")), Child->GetAttribute(TEXT("v")));
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Lit une reponse Overpass "out geom" (voies et relations avec geometrie en ligne). Echec : false et message. */
|
|
bool ReadOverpass(const FString& Path, TArray<FOsmWay>& Ways, TArray<FOsmRelation>& Relations, FString& Error)
|
|
{
|
|
FXmlFile File(Path);
|
|
if (!File.IsValid())
|
|
{
|
|
Error = FString::Printf(TEXT("XML illisible : %s (%s)"), *Path, *File.GetLastError());
|
|
return false;
|
|
}
|
|
const FXmlNode* Root = File.GetRootNode();
|
|
for (const FXmlNode* Node : Root->GetChildrenNodes())
|
|
{
|
|
if (Node->GetTag() == TEXT("remark"))
|
|
{
|
|
Error = FString::Printf(TEXT("reponse Overpass en erreur : %s"), *Node->GetContent());
|
|
return false;
|
|
}
|
|
if (Node->GetTag() == TEXT("way"))
|
|
{
|
|
FOsmWay& Way = Ways.AddDefaulted_GetRef();
|
|
Way.Id = FCString::Atoi64(*Node->GetAttribute(TEXT("id")));
|
|
ReadPoints(Node, Way.Points);
|
|
ReadTags(Node, Way.Tags);
|
|
if (const FXmlNode* Bounds = Node->FindChildNode(TEXT("bounds")))
|
|
{
|
|
const double MinLat = FCString::Atod(*Bounds->GetAttribute(TEXT("minlat")));
|
|
const double MinLon = FCString::Atod(*Bounds->GetAttribute(TEXT("minlon")));
|
|
const double MaxLat = FCString::Atod(*Bounds->GetAttribute(TEXT("maxlat")));
|
|
const double MaxLon = FCString::Atod(*Bounds->GetAttribute(TEXT("maxlon")));
|
|
Way.BoundsCenter = FLonLat(0.5 * (MinLon + MaxLon), 0.5 * (MinLat + MaxLat));
|
|
Way.bHasBounds = true;
|
|
}
|
|
}
|
|
else if (Node->GetTag() == TEXT("relation"))
|
|
{
|
|
FOsmRelation& Rel = Relations.AddDefaulted_GetRef();
|
|
Rel.Id = FCString::Atoi64(*Node->GetAttribute(TEXT("id")));
|
|
ReadTags(Node, Rel.Tags);
|
|
for (const FXmlNode* Child : Node->GetChildrenNodes())
|
|
{
|
|
if (Child->GetTag() == TEXT("member") && Child->GetAttribute(TEXT("type")) == TEXT("way"))
|
|
{
|
|
FOsmMember& Member = Rel.Members.AddDefaulted_GetRef();
|
|
Member.Role = Child->GetAttribute(TEXT("role"));
|
|
ReadPoints(Child, Member.Points);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Centroide par moyenne des sommets distincts (le point de fermeture repete n'est pas compte), comme l'importeur. */
|
|
template<typename VecType>
|
|
VecType Centroid(const TArray<VecType>& Points)
|
|
{
|
|
int32 Count = Points.Num();
|
|
if (Count > 1 && Points[0].Equals(Points[Count - 1], 1e-9))
|
|
{
|
|
--Count;
|
|
}
|
|
VecType Sum(0, 0);
|
|
for (int32 Index = 0; Index < Count; ++Index)
|
|
{
|
|
Sum += Points[Index];
|
|
}
|
|
return Count > 0 ? Sum / double(Count) : Sum;
|
|
}
|
|
|
|
/** Projection du plugin (Sanson-Flamsteed), relative a l'origine, en cm. */
|
|
FVector2d Project(const FLonLat& P, const FLonLat& Origin)
|
|
{
|
|
return FVector2d(
|
|
(P.X - Origin.X) * FMath::Cos(FMath::DegreesToRadians(P.Y)) * CmPerDegree,
|
|
-(P.Y - Origin.Y) * CmPerDegree);
|
|
}
|
|
|
|
double Median(TArray<double> Values)
|
|
{
|
|
if (Values.IsEmpty())
|
|
{
|
|
return 0.0;
|
|
}
|
|
Values.Sort();
|
|
const int32 Mid = Values.Num() / 2;
|
|
return (Values.Num() % 2) ? Values[Mid] : 0.5 * (Values[Mid - 1] + Values[Mid]);
|
|
}
|
|
|
|
/** Chaine des segments ouverts en anneaux fermes (extremites communes). Anneaux non fermables : comptes dans Broken. */
|
|
void ChainRings(TArray<TArray<FLonLat>> Segments, TArray<TArray<FLonLat>>& Rings, int32& Broken)
|
|
{
|
|
constexpr double Tol = 1e-7;
|
|
while (Segments.Num() > 0)
|
|
{
|
|
TArray<FLonLat> Ring = Segments.Pop();
|
|
bool bGrew = true;
|
|
while (!Ring[0].Equals(Ring.Last(), Tol) && bGrew)
|
|
{
|
|
bGrew = false;
|
|
for (int32 Index = 0; Index < Segments.Num(); ++Index)
|
|
{
|
|
TArray<FLonLat>& Seg = Segments[Index];
|
|
if (Seg[0].Equals(Ring.Last(), Tol))
|
|
{
|
|
Ring.Append(&Seg[1], Seg.Num() - 1);
|
|
}
|
|
else if (Seg.Last().Equals(Ring.Last(), Tol))
|
|
{
|
|
for (int32 P = Seg.Num() - 2; P >= 0; --P)
|
|
{
|
|
Ring.Add(Seg[P]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
Segments.RemoveAtSwap(Index);
|
|
bGrew = true;
|
|
break;
|
|
}
|
|
}
|
|
if (Ring.Num() >= 4 && Ring[0].Equals(Ring.Last(), Tol))
|
|
{
|
|
Rings.Add(MoveTemp(Ring));
|
|
}
|
|
else
|
|
{
|
|
++Broken;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Sutherland-Hodgman contre un rectangle (repere de la carte, cm). */
|
|
TArray<FVector2d> ClipToBox(const TArray<FVector2d>& In, const FVector2d& Min, const FVector2d& Max)
|
|
{
|
|
TArray<FVector2d> Out = In;
|
|
auto ClipEdge = [&Out](TFunctionRef<bool(const FVector2d&)> Inside, TFunctionRef<FVector2d(const FVector2d&, const FVector2d&)> Cut)
|
|
{
|
|
TArray<FVector2d> Src = MoveTemp(Out);
|
|
Out.Reset();
|
|
for (int32 Index = 0; Index < Src.Num(); ++Index)
|
|
{
|
|
const FVector2d& A = Src[Index];
|
|
const FVector2d& B = Src[(Index + 1) % Src.Num()];
|
|
const bool bA = Inside(A);
|
|
const bool bB = Inside(B);
|
|
if (bA) { Out.Add(A); }
|
|
if (bA != bB) { Out.Add(Cut(A, B)); }
|
|
}
|
|
};
|
|
auto LerpX = [](const FVector2d& A, const FVector2d& B, double X) { const double T = (X - A.X) / (B.X - A.X); return FVector2d(X, A.Y + T * (B.Y - A.Y)); };
|
|
auto LerpY = [](const FVector2d& A, const FVector2d& B, double Y) { const double T = (Y - A.Y) / (B.Y - A.Y); return FVector2d(A.X + T * (B.X - A.X), Y); };
|
|
ClipEdge([&](const FVector2d& P) { return P.X >= Min.X; }, [&](const FVector2d& A, const FVector2d& B) { return LerpX(A, B, Min.X); });
|
|
if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.X <= Max.X; }, [&](const FVector2d& A, const FVector2d& B) { return LerpX(A, B, Max.X); });
|
|
if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.Y >= Min.Y; }, [&](const FVector2d& A, const FVector2d& B) { return LerpY(A, B, Min.Y); });
|
|
if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.Y <= Max.Y; }, [&](const FVector2d& A, const FVector2d& B) { return LerpY(A, B, Max.Y); });
|
|
return Out.Num() >= 3 ? Out : TArray<FVector2d>();
|
|
}
|
|
}
|
|
|
|
ULPBuildCityWaterCommandlet::ULPBuildCityWaterCommandlet()
|
|
{
|
|
IsClient = false;
|
|
IsServer = false;
|
|
IsEditor = true;
|
|
LogToConsole = true;
|
|
}
|
|
|
|
int32 ULPBuildCityWaterCommandlet::Main(const FString& Params)
|
|
{
|
|
using namespace LPCityWater;
|
|
|
|
TArray<FString> Tokens;
|
|
TArray<FString> Switches;
|
|
TMap<FString, FString> ParamsMap;
|
|
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
|
|
|
|
const FString City = ParamsMap.FindRef(TEXT("City"));
|
|
const FString WaterPath = ParamsMap.FindRef(TEXT("Water"));
|
|
const FString BuildingsPath = ParamsMap.FindRef(TEXT("Buildings"));
|
|
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
|
|
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
|
|
const int32 MinPairs = ParamsMap.Contains(TEXT("MinPairs")) ? FCString::Atoi(*ParamsMap[TEXT("MinPairs")]) : 500;
|
|
const double MaxMedianCm = ParamsMap.Contains(TEXT("MaxMedianCm")) ? FCString::Atod(*ParamsMap[TEXT("MaxMedianCm")]) : 200.0;
|
|
// STM1-SPEC-09 (B6) : carte d'un pack (racine /lpm_city_<v>/StreetMap) et origine connue a l'import (moyenne des noeuds
|
|
// du .osm, rapport de LPClipCityOSM) : pas de recalage par les batiments nommes. L'eau est ecrite sous <MapRoot>/Water.
|
|
const FString MapRoot = ParamsMap.Contains(TEXT("MapRoot")) ? ParamsMap[TEXT("MapRoot")] : FString(TEXT("/Game/Heavy/StreetMap"));
|
|
const FString OriginText = ParamsMap.FindRef(TEXT("Origin"));
|
|
const bool bFixedOrigin = !OriginText.IsEmpty();
|
|
// STM1-SPEC-10 (W1) / SPEC-11 (M1) : eau coupee a la boite du pack (lon/lat), sans marge.
|
|
const FString ClipBoxText = ParamsMap.FindRef(TEXT("ClipBox"));
|
|
|
|
TArray<FString> Report;
|
|
auto Line = [&Report](const FString& Text)
|
|
{
|
|
UE_LOG(LogLPBuildCityWater, 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;
|
|
};
|
|
|
|
if (City.IsEmpty() || WaterPath.IsEmpty() || (BuildingsPath.IsEmpty() && !bFixedOrigin))
|
|
{
|
|
Line(TEXT("usage : -City=<Ville> -Water=<q1.osm.xml> (-Buildings=<q2.osm.xml> | -Origin=<lat>,<lon>) [-MapRoot=</lpm_city_<v>/StreetMap>] [-ClipBox=<sud>,<ouest>,<nord>,<est>] [-DryRun] [-Report=<f.md>] [-ListBuildingsInWater=<f.md>] [-ThresholdM2=<m2>] [-MaskOnly [-TreeBlockOsm=<f.osm.xml>] [-HideAllTrees]] [-SkipParkWaterMaxM2=<m2>] [-PackMaterial=<Nom> [-WaterRoughness=0.7] [-WaterSpecular=0.5]] [-WaterZOffsetCm=<cm>]"));
|
|
return Finish(2);
|
|
}
|
|
Line(FString::Printf(TEXT("# LPBuildCityWater %s (%s) ; racine %s"), *City, bDryRun ? TEXT("DryRun") : TEXT("ecriture"), *MapRoot));
|
|
{
|
|
TArray<FString> RootParts;
|
|
MapRoot.ParseIntoArray(RootParts, TEXT("/"));
|
|
const bool bBaseRoot = MapRoot == TEXT("/Game/Heavy/StreetMap");
|
|
const bool bPackRoot = RootParts.Num() == 2 && RootParts[0].StartsWith(TEXT("lpm_city_")) && RootParts[1] == TEXT("StreetMap");
|
|
if (!bBaseRoot && !bPackRoot)
|
|
{
|
|
Line(TEXT("ECHEC : -MapRoot doit etre /Game/Heavy/StreetMap ou /lpm_city_<ville>/StreetMap ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
if (!bBaseRoot && Switches.Contains(TEXT("CreateMaterial")))
|
|
{
|
|
Line(TEXT("ECHEC : -CreateMaterial seulement pour le jeu de base (le pack reference M_LP_Water du jeu) ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
}
|
|
|
|
// 1. Carte et maillage de reference.
|
|
const UStreetMap* StreetMap = LoadObject<UStreetMap>(nullptr, *FString::Printf(TEXT("%s/%s.%s"), *MapRoot, *City, *City));
|
|
const UStaticMesh* CityMesh = LoadObject<UStaticMesh>(nullptr, *FString::Printf(TEXT("%s/Static/%s.%s"), *MapRoot, *City, *City));
|
|
if (!StreetMap || !CityMesh)
|
|
{
|
|
Line(TEXT("ECHEC : UStreetMap ou maillage Static introuvable"));
|
|
return Finish(1);
|
|
}
|
|
const FVector2d MapMin = StreetMap->GetBoundsMin();
|
|
const FVector2d MapMax = StreetMap->GetBoundsMax();
|
|
const FBox MeshBox = CityMesh->GetBoundingBox();
|
|
Line(FString::Printf(TEXT("bornes UStreetMap (cm) : (%.0f, %.0f) a (%.0f, %.0f)"), MapMin.X, MapMin.Y, MapMax.X, MapMax.Y));
|
|
Line(FString::Printf(TEXT("bornes maillage (cm) : (%.0f, %.0f) a (%.0f, %.0f)"), MeshBox.Min.X, MeshBox.Min.Y, MeshBox.Max.X, MeshBox.Max.Y));
|
|
const double BoundsGap = FMath::Max(
|
|
FMath::Max(FMath::Abs(MeshBox.Min.X - MapMin.X), FMath::Abs(MeshBox.Min.Y - MapMin.Y)),
|
|
FMath::Max(FMath::Abs(MeshBox.Max.X - MapMax.X), FMath::Abs(MeshBox.Max.Y - MapMax.Y)));
|
|
Line(FString::Printf(TEXT("ecart des bornes maillage / UStreetMap : %.0f cm (critere <= 100 ; indicatif : les bornes UStreetMap ne couvrent que les objets importes)"), BoundsGap));
|
|
|
|
// 2. Extraits OSM.
|
|
TArray<FOsmWay> WaterWays, BuildingWays;
|
|
TArray<FOsmRelation> WaterRelations, UnusedRelations;
|
|
FString Error;
|
|
if (!ReadOverpass(WaterPath, WaterWays, WaterRelations, Error) || (!bFixedOrigin && !ReadOverpass(BuildingsPath, BuildingWays, UnusedRelations, Error)))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : %s"), *Error));
|
|
return Finish(1);
|
|
}
|
|
Line(FString::Printf(TEXT("extraits : eau %d voies, %d relations ; batiments %d voies"), WaterWays.Num(), WaterRelations.Num(), BuildingWays.Num()));
|
|
|
|
FLonLat Origin(0.0, 0.0);
|
|
if (bFixedOrigin)
|
|
{
|
|
TArray<FString> OriginParts;
|
|
OriginText.ParseIntoArray(OriginParts, TEXT(","));
|
|
if (OriginParts.Num() != 2)
|
|
{
|
|
Line(TEXT("ECHEC : -Origin=<lat>,<lon> attendu ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
Origin = FLonLat(FCString::Atod(*OriginParts[1]), FCString::Atod(*OriginParts[0]));
|
|
Line(FString::Printf(TEXT("origine donnee (moyenne des noeuds du .osm importe) : lat %.9f lon %.9f ; pas de recalage"), Origin.Y, Origin.X));
|
|
}
|
|
else
|
|
{
|
|
// 3. Recalage : batiments nommes, noms uniques des deux cotes.
|
|
// STM1-B2 (R1) : l'extrait Overpass des batiments ne donne que <bounds> (pas de noeuds). On compare donc des centres de
|
|
// boite des deux cotes (meme definition) : moins precis qu'un centroide, biais possible sur les grands batiments
|
|
// irreguliers ; la mediane et la MAD des residus le mesurent.
|
|
auto BoxCenter = [](const TArray<FVector2d>& Points)
|
|
{
|
|
FVector2d Min(TNumericLimits<double>::Max(), TNumericLimits<double>::Max());
|
|
FVector2d Max(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Lowest());
|
|
for (const FVector2d& P : Points)
|
|
{
|
|
Min = FVector2d(FMath::Min(Min.X, P.X), FMath::Min(Min.Y, P.Y));
|
|
Max = FVector2d(FMath::Max(Max.X, P.X), FMath::Max(Max.Y, P.Y));
|
|
}
|
|
return 0.5 * (Min + Max);
|
|
};
|
|
TMap<FString, int32> AssetCount, OsmCount;
|
|
TMap<FString, FVector2d> AssetCentroid;
|
|
TMap<FString, FLonLat> OsmCentroid;
|
|
for (const FStreetMapBuilding& Building : StreetMap->GetBuildings())
|
|
{
|
|
if (Building.BuildingName.IsEmpty() || Building.BuildingPoints.Num() < 3) continue;
|
|
const FString Key = Building.BuildingName.ToLower();
|
|
AssetCount.FindOrAdd(Key)++;
|
|
AssetCentroid.Add(Key, BoxCenter(Building.BuildingPoints));
|
|
}
|
|
int32 OsmFromBounds = 0, OsmFromNodes = 0;
|
|
for (const FOsmWay& Way : BuildingWays)
|
|
{
|
|
const FString* Name = Way.Tags.Find(TEXT("name"));
|
|
if (!Name || (!Way.bHasBounds && Way.Points.Num() < 3)) continue;
|
|
const FString Key = Name->ToLower();
|
|
OsmCount.FindOrAdd(Key)++;
|
|
if (Way.Points.Num() >= 3)
|
|
{
|
|
OsmCentroid.Add(Key, BoxCenter(Way.Points));
|
|
++OsmFromNodes;
|
|
}
|
|
else
|
|
{
|
|
OsmCentroid.Add(Key, Way.BoundsCenter);
|
|
++OsmFromBounds;
|
|
}
|
|
}
|
|
Line(FString::Printf(TEXT("batiments nommes : asset %d noms, OSM %d noms (centre de <bounds> : %d, centre de boite des noeuds : %d) ; methode : centres de boite des deux cotes"),
|
|
AssetCount.Num(), OsmCount.Num(), OsmFromBounds, OsmFromNodes));
|
|
TArray<TPair<FVector2d, FLonLat>> Pairs;
|
|
for (const TPair<FString, int32>& Entry : AssetCount)
|
|
{
|
|
if (Entry.Value == 1 && OsmCount.FindRef(Entry.Key) == 1)
|
|
{
|
|
Pairs.Add({ AssetCentroid[Entry.Key], OsmCentroid[Entry.Key] });
|
|
}
|
|
}
|
|
// STM1-B2 (S1, decision de Big Boss) : deux passes. Passe 1 : origine = mediane sur toutes les paires. Passe 2 : rejet des
|
|
// paires au-dela de mediane + 3 x MAD, origine recalculee sur les paires retenues. Criteres d'acceptation non circulaires :
|
|
// au moins MinPairs paires retenues, residu median (paires retenues) <= MaxMedianCm, taux de paires retenues >= 70 %.
|
|
// Pour information : part sous 5 m sur TOUTES les paires avec l'origine corrigee, et deplacement de l'origine (cm).
|
|
auto EstimateOrigin = [](const TArray<TPair<FVector2d, FLonLat>>& InPairs)
|
|
{
|
|
TArray<double> RelLats, RelLons;
|
|
for (const TPair<FVector2d, FLonLat>& Pair : InPairs)
|
|
{
|
|
const FVector2d& XY = Pair.Key;
|
|
const FLonLat& LL = Pair.Value;
|
|
RelLats.Add(LL.Y + XY.Y / CmPerDegree);
|
|
RelLons.Add(LL.X - XY.X / (CmPerDegree * FMath::Cos(FMath::DegreesToRadians(LL.Y))));
|
|
}
|
|
return FLonLat(Median(RelLons), Median(RelLats));
|
|
};
|
|
auto ResidualsOf = [](const TArray<TPair<FVector2d, FLonLat>>& InPairs, const FLonLat& InOrigin)
|
|
{
|
|
TArray<double> Out;
|
|
for (const TPair<FVector2d, FLonLat>& Pair : InPairs)
|
|
{
|
|
Out.Add(FVector2d::Distance(Project(Pair.Value, InOrigin), Pair.Key));
|
|
}
|
|
return Out;
|
|
};
|
|
auto MadOf = [](const TArray<double>& Values, double Center)
|
|
{
|
|
TArray<double> Deviations;
|
|
for (const double V : Values)
|
|
{
|
|
Deviations.Add(FMath::Abs(V - Center));
|
|
}
|
|
return Median(Deviations);
|
|
};
|
|
auto ShareUnder = [](const TArray<double>& Values, double Limit)
|
|
{
|
|
int32 Count = 0;
|
|
for (const double V : Values) { Count += V <= Limit ? 1 : 0; }
|
|
return Values.Num() ? double(Count) / Values.Num() : 0.0;
|
|
};
|
|
|
|
// Passe 1.
|
|
const FLonLat Origin1 = EstimateOrigin(Pairs);
|
|
const TArray<double> Residuals1 = ResidualsOf(Pairs, Origin1);
|
|
const double Median1 = Median(Residuals1);
|
|
const double Mad1 = MadOf(Residuals1, Median1);
|
|
Line(FString::Printf(TEXT("passe 1 : %d paires ; origine lat %.7f lon %.7f ; residu median %.0f cm ; MAD %.0f cm ; %.1f %% sous 5 m"),
|
|
Pairs.Num(), Origin1.Y, Origin1.X, Median1, Mad1, 100.0 * ShareUnder(Residuals1, 500.0)));
|
|
|
|
// Passe 2 : rejet a mediane + 3 x MAD.
|
|
const double RejectCm = Median1 + 3.0 * Mad1;
|
|
TArray<TPair<FVector2d, FLonLat>> Kept;
|
|
for (int32 Index = 0; Index < Pairs.Num(); ++Index)
|
|
{
|
|
if (Residuals1[Index] <= RejectCm)
|
|
{
|
|
Kept.Add(Pairs[Index]);
|
|
}
|
|
}
|
|
Origin = Kept.Num() ? EstimateOrigin(Kept) : Origin1;
|
|
const TArray<double> ResidualsKept = ResidualsOf(Kept, Origin);
|
|
const double MedianKept = Median(ResidualsKept);
|
|
const double KeptRate = Pairs.Num() ? double(Kept.Num()) / Pairs.Num() : 0.0;
|
|
const double ShareAllUnder5m = ShareUnder(ResidualsOf(Pairs, Origin), 500.0);
|
|
const double OriginShiftCm = Project(Origin1, Origin).Length();
|
|
// Preuve que la passe 2 recalcule l'origine (demande de Big Boss) : valeurs en pleine precision et decalage en mm.
|
|
Line(FString::Printf(TEXT("origines pleine precision : passe 1 lat %.12f lon %.12f ; passe 2 lat %.12f lon %.12f ; ecart lat %.3e deg, lon %.3e deg ; decalage %.3f mm ; passe 2 calculee sur %d paires (passe 1 : %d)"),
|
|
Origin1.Y, Origin1.X, Origin.Y, Origin.X, Origin.Y - Origin1.Y, Origin.X - Origin1.X, OriginShiftCm * 10.0, Kept.Num(), Pairs.Num()));
|
|
Line(FString::Printf(TEXT("passe 2 : seuil de rejet %.0f cm (mediane + 3 x MAD) ; %d paires retenues, %d ecartees (taux retenu %.1f %%) ; origine lat %.7f lon %.7f ; residu median (retenues) %.0f cm"),
|
|
RejectCm, Kept.Num(), Pairs.Num() - Kept.Num(), 100.0 * KeptRate, Origin.Y, Origin.X, MedianKept));
|
|
Line(FString::Printf(TEXT("information : %.1f %% sous 5 m sur toutes les paires avec l'origine corrigee ; deplacement de l'origine passe 1 -> passe 2 : %.0f cm"),
|
|
100.0 * ShareAllUnder5m, OriginShiftCm));
|
|
Line(TEXT("note : centres de boite moins precis qu'un centroide (biais possible sur les grands batiments) ; le juge final est l'essai visuel (eau sur les quais)"));
|
|
if (Kept.Num() < MinPairs || MedianKept > MaxMedianCm || KeptRate < 0.70)
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : criteres de recalage non atteints (paires retenues %d >= %d ; residu median %.0f <= %.0f cm ; taux retenu %.1f >= 70 %%) ; aucun asset ecrit"),
|
|
Kept.Num(), MinPairs, MedianKept, MaxMedianCm, 100.0 * KeptRate));
|
|
return Finish(1);
|
|
}
|
|
Line(TEXT("recalage accepte (criteres : paires retenues, residu median, taux retenu)"));
|
|
}
|
|
|
|
// 4. Polygones d'eau (anneaux en lon/lat), puis projection et decoupe a la boite de la carte + 50 m.
|
|
TArray<TArray<FLonLat>> OuterLL, InnerLL;
|
|
int32 Broken = 0;
|
|
for (const FOsmWay& Way : WaterWays)
|
|
{
|
|
if (Way.Points.Num() >= 4 && Way.Points[0].Equals(Way.Points.Last(), 1e-7))
|
|
{
|
|
OuterLL.Add(Way.Points);
|
|
}
|
|
}
|
|
for (const FOsmRelation& Rel : WaterRelations)
|
|
{
|
|
TArray<TArray<FLonLat>> OuterSegs, InnerSegs;
|
|
for (const FOsmMember& Member : Rel.Members)
|
|
{
|
|
if (Member.Points.Num() < 2) continue;
|
|
(Member.Role == TEXT("inner") ? InnerSegs : OuterSegs).Add(Member.Points);
|
|
}
|
|
ChainRings(MoveTemp(OuterSegs), OuterLL, Broken);
|
|
ChainRings(MoveTemp(InnerSegs), InnerLL, Broken);
|
|
}
|
|
const FVector2d ClipMin = MapMin - FVector2d(5000.0, 5000.0);
|
|
const FVector2d ClipMax = MapMax + FVector2d(5000.0, 5000.0);
|
|
FLonLat BoxMinLL(0.0, 0.0), BoxMaxLL(0.0, 0.0);
|
|
const bool bClipBox = !ClipBoxText.IsEmpty();
|
|
if (bClipBox)
|
|
{
|
|
TArray<FString> BoxParts;
|
|
ClipBoxText.ParseIntoArray(BoxParts, TEXT(","));
|
|
if (BoxParts.Num() != 4)
|
|
{
|
|
Line(TEXT("ECHEC : -ClipBox=<sud>,<ouest>,<nord>,<est> attendu ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
BoxMinLL = FLonLat(FCString::Atod(*BoxParts[1]), FCString::Atod(*BoxParts[0]));
|
|
BoxMaxLL = FLonLat(FCString::Atod(*BoxParts[3]), FCString::Atod(*BoxParts[2]));
|
|
Line(FString::Printf(TEXT("eau coupee a la boite : sud %.5f ouest %.5f nord %.5f est %.5f (sans marge)"), BoxMinLL.Y, BoxMinLL.X, BoxMaxLL.Y, BoxMaxLL.X));
|
|
}
|
|
auto ToMap = [&](const TArray<FLonLat>& Ring)
|
|
{
|
|
TArray<FVector2d> LL;
|
|
for (int32 Index = 0; Index + 1 < Ring.Num(); ++Index) // sans le point de fermeture
|
|
{
|
|
LL.Add(Ring[Index]);
|
|
}
|
|
if (bClipBox)
|
|
{
|
|
LL = ClipToBox(LL, BoxMinLL, BoxMaxLL);
|
|
}
|
|
TArray<FVector2d> P;
|
|
for (const FVector2d& Point : LL)
|
|
{
|
|
P.Add(Project(Point, Origin));
|
|
}
|
|
return ClipToBox(P, ClipMin, ClipMax);
|
|
};
|
|
TArray<TPolygon2<double>> Outers, Inners;
|
|
for (const TArray<FLonLat>& Ring : OuterLL) { TArray<FVector2d> P = ToMap(Ring); if (P.Num() >= 3) Outers.Emplace(P); }
|
|
for (const TArray<FLonLat>& Ring : InnerLL) { TArray<FVector2d> P = ToMap(Ring); if (P.Num() >= 3) Inners.Emplace(P); }
|
|
Line(FString::Printf(TEXT("polygones dans la carte : %d exterieurs, %d trous ; anneaux non fermes ignores : %d"), Outers.Num(), Inners.Num(), Broken));
|
|
// 1.3.0 (COMMUN-26, bassins noirs des Tuileries) : -SkipParkWaterMaxM2=<m2> retire du maillage d'eau les plans d'eau plus
|
|
// petits que ce seuil dont le centre tombe dans un parc de l'UStreetMap (bassins, fontaines de jardin). La Seine reste.
|
|
const double SkipParkWaterMaxCm2 = ParamsMap.Contains(TEXT("SkipParkWaterMaxM2")) ? FCString::Atod(*ParamsMap[TEXT("SkipParkWaterMaxM2")]) * 1e4 : 0.0;
|
|
if (SkipParkWaterMaxCm2 > 0.0)
|
|
{
|
|
TArray<TPolygon2<double>> Parks;
|
|
for (const FStreetMapPark& Park : StreetMap->GetParks())
|
|
{
|
|
if (Park.ParkPoints.Num() < 3) continue;
|
|
TArray<FVector2d> P;
|
|
for (const FVector2D& V : Park.ParkPoints) P.Add(FVector2d(V.X, V.Y));
|
|
Parks.Emplace(P);
|
|
}
|
|
int32 Skipped = 0;
|
|
double SkippedCm2 = 0.0;
|
|
for (int32 Index = Outers.Num() - 1; Index >= 0; --Index)
|
|
{
|
|
const double Area = FMath::Abs(Outers[Index].SignedArea());
|
|
if (Area >= SkipParkWaterMaxCm2) continue;
|
|
FVector2d Mean(0.0, 0.0);
|
|
for (const FVector2d& V : Outers[Index].GetVertices()) Mean += V;
|
|
Mean /= double(FMath::Max(1, Outers[Index].VertexCount()));
|
|
const bool bInPark = Parks.ContainsByPredicate([&Mean](const TPolygon2<double>& Park) { return Park.Contains(Mean); });
|
|
if (!bInPark) continue;
|
|
Outers.RemoveAt(Index);
|
|
++Skipped;
|
|
SkippedCm2 += Area;
|
|
}
|
|
Line(FString::Printf(TEXT("plans d'eau dans un parc, sous %.0f m2 : %d retires (%.0f m2) ; parcs %d ; restent %d exterieurs"),
|
|
SkipParkWaterMaxCm2 / 1e4, Skipped, SkippedCm2 / 1e4, Parks.Num(), Outers.Num()));
|
|
}
|
|
|
|
// 5. Triangulation (exterieur anti-horaire, trous horaires, trou rattache a l'exterieur qui contient son premier sommet).
|
|
TArray<FVector3f> Vertices;
|
|
TArray<FIntVector3> Triangles;
|
|
double AreaCm2 = 0.0;
|
|
TArray<TGeneralPolygon2<double>> WaterPolygons;
|
|
for (TPolygon2<double>& Outer : Outers)
|
|
{
|
|
if (Outer.IsClockwise()) Outer.Reverse();
|
|
TGeneralPolygon2<double> General(Outer);
|
|
for (TPolygon2<double> Hole : Inners)
|
|
{
|
|
if (Outer.Contains(Hole.GetVertices()[0]))
|
|
{
|
|
if (!Hole.IsClockwise()) Hole.Reverse();
|
|
General.AddHole(Hole, false, false);
|
|
}
|
|
}
|
|
const TArray<FIndex3i> Tris = ConstrainedDelaunayTriangulate<double>(General);
|
|
const int32 Base = Vertices.Num();
|
|
for (const FVector2d& V : General.GetOuter().GetVertices()) Vertices.Emplace(float(V.X), float(V.Y), 0.f);
|
|
for (const TPolygon2<double>& H : General.GetHoles()) for (const FVector2d& V : H.GetVertices()) Vertices.Emplace(float(V.X), float(V.Y), 0.f);
|
|
for (const FIndex3i& T : Tris) Triangles.Emplace(Base + T.A, Base + T.B, Base + T.C);
|
|
AreaCm2 += FMath::Abs(General.SignedArea());
|
|
WaterPolygons.Add(MoveTemp(General));
|
|
}
|
|
Line(FString::Printf(TEXT("maillage : %d sommets, %d triangles, aire %.3f km2"), Vertices.Num(), Triangles.Num(), AreaCm2 / 1e10));
|
|
if (Triangles.IsEmpty())
|
|
{
|
|
Line(TEXT("ECHEC : aucun triangle"));
|
|
return Finish(1);
|
|
}
|
|
// STM1-SPEC-04 (O3) : batiments de la carte dont le centre tombe dans l'eau. Centre et aire comme MapMarkers
|
|
// (GI.FindCenterOfPolygon / calculateArea : boite englobante en float, aire = dX x dY / 1000) ; aire du polygone en m2 en plus.
|
|
const FString ListPath = ParamsMap.FindRef(TEXT("ListBuildingsInWater"));
|
|
const bool bMaskOnly = Switches.Contains(TEXT("MaskOnly"));
|
|
if (!ListPath.IsEmpty() || bMaskOnly)
|
|
{
|
|
const double ThresholdM2 = ParamsMap.Contains(TEXT("ThresholdM2")) ? FCString::Atod(*ParamsMap[TEXT("ThresholdM2")]) : 1500.0;
|
|
struct FInWater { FString Name; FString Amenity; double PolyM2; double BoxM2; double GameArea; FVector2d Center; int32 Index; };
|
|
TArray<FInWater> InWater;
|
|
const TArray<FStreetMapBuilding>& Buildings = StreetMap->GetBuildings();
|
|
for (int32 Index = 0; Index < Buildings.Num(); ++Index)
|
|
{
|
|
const FStreetMapBuilding& Building = Buildings[Index];
|
|
if (Building.BuildingPoints.Num() < 3) continue;
|
|
float MinX = float(Building.BuildingPoints[0].X), MaxX = MinX, MinY = float(Building.BuildingPoints[0].Y), MaxY = MinY;
|
|
double Shoelace = 0.0;
|
|
for (int32 P = 0; P < Building.BuildingPoints.Num(); ++P)
|
|
{
|
|
const FVector2D& A = Building.BuildingPoints[P];
|
|
const FVector2D& B = Building.BuildingPoints[(P + 1) % Building.BuildingPoints.Num()];
|
|
MinX = FMath::Min(MinX, float(A.X)); MaxX = FMath::Max(MaxX, float(A.X));
|
|
MinY = FMath::Min(MinY, float(A.Y)); MaxY = FMath::Max(MaxY, float(A.Y));
|
|
Shoelace += A.X * B.Y - B.X * A.Y;
|
|
}
|
|
const FVector2d Center((double(MinX) + double(MaxX)) / 2.0, (double(MinY) + double(MaxY)) / 2.0);
|
|
bool bInside = false;
|
|
for (const TGeneralPolygon2<double>& Water : WaterPolygons)
|
|
{
|
|
if (Water.Bounds().Contains(Center) && Water.Contains(Center)) { bInside = true; break; }
|
|
}
|
|
if (!bInside) continue;
|
|
const double BoxCm2 = (double(MaxX) - double(MinX)) * (double(MaxY) - double(MinY));
|
|
InWater.Add({ Building.BuildingName, Building.Amenity, FMath::Abs(Shoelace) / 2.0 / 1e4, BoxCm2 / 1e4, BoxCm2 / 1000.0, Center, Index });
|
|
}
|
|
InWater.Sort([](const FInWater& A, const FInWater& B) { return A.PolyM2 > B.PolyM2; });
|
|
int32 Above = 0, Named = 0;
|
|
for (const FInWater& B : InWater) { Above += B.PolyM2 >= ThresholdM2 ? 1 : 0; Named += B.Name.IsEmpty() ? 0 : 1; }
|
|
TArray<FString> List;
|
|
List.Add(FString::Printf(TEXT("# Batiments de %s dont le centre tombe dans l'eau (seuil %.0f m2)"), *City, ThresholdM2));
|
|
List.Add(TEXT(""));
|
|
List.Add(FString::Printf(TEXT("total %d sur %d batiments ; nommes %d ; au-dessus du seuil (aire du polygone) %d ; en dessous %d"),
|
|
InWater.Num(), Buildings.Num(), Named, Above, InWater.Num() - Above));
|
|
List.Add(TEXT(""));
|
|
List.Add(TEXT("| # | indice | nom | amenite | aire polygone (m2) | aire boite (m2) | aire du jeu (dX*dY/1000) | echelle XY du jeu | centre X (cm) | centre Y (cm) | seuil |"));
|
|
List.Add(TEXT("|---|---|---|---|---|---|---|---|---|---|---|"));
|
|
for (int32 Row = 0; Row < InWater.Num(); ++Row)
|
|
{
|
|
const FInWater& B = InWater[Row];
|
|
const double Scale = FMath::GetMappedRangeValueClamped(FVector2d(0.0, 100000.0), FVector2d(2.0, 20.0), B.GameArea);
|
|
List.Add(FString::Printf(TEXT("| %d | %d | %s | %s | %.0f | %.0f | %.0f | %.2f | %.0f | %.0f | %s |"), Row + 1, B.Index,
|
|
B.Name.IsEmpty() ? TEXT("-") : *B.Name.Replace(TEXT("|"), TEXT("/")), B.Amenity.IsEmpty() ? TEXT("-") : *B.Amenity,
|
|
B.PolyM2, B.BoxM2, B.GameArea, Scale, B.Center.X, B.Center.Y, B.PolyM2 >= ThresholdM2 ? TEXT("epargne") : TEXT("masque")));
|
|
}
|
|
if (!ListPath.IsEmpty())
|
|
{
|
|
FFileHelper::SaveStringArrayToFile(List, *ListPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
|
|
}
|
|
Line(FString::Printf(TEXT("O3 : %d batiments dans l'eau (au-dessus de %.0f m2 : %d) ; liste %s"), InWater.Num(), ThresholdM2, Above,
|
|
ListPath.IsEmpty() ? TEXT("(non ecrite)") : *ListPath));
|
|
|
|
// STM1-SPEC-08 (O2) : masque /Game/Heavy/StreetMap/Water/<Ville>_Mask, sans relire ni reecrire le maillage d'eau.
|
|
if (bMaskOnly)
|
|
{
|
|
const FString MaskPackageName = FString::Printf(TEXT("%s/Water/%s_Mask"), *MapRoot, *City);
|
|
if (bDryRun)
|
|
{
|
|
Line(FString::Printf(TEXT("DryRun : masque %s non ecrit (%d centres)"), *MaskPackageName, InWater.Num()));
|
|
return Finish(0);
|
|
}
|
|
if (FPackageName::DoesPackageExist(MaskPackageName))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *MaskPackageName));
|
|
return Finish(1);
|
|
}
|
|
UPackage* MaskPackage = CreatePackage(*MaskPackageName);
|
|
ULPCityWaterMask* Mask = NewObject<ULPCityWaterMask>(MaskPackage, *FPackageName::GetLongPackageAssetName(MaskPackageName), RF_Public | RF_Standalone);
|
|
Mask->City = City;
|
|
Mask->SourceBuildingCount = Buildings.Num();
|
|
for (const FInWater& B : InWater)
|
|
{
|
|
Mask->Centers.Add(B.Center);
|
|
Mask->AreasM2.Add(float(B.PolyM2));
|
|
}
|
|
// STM1-SPEC-10 (G3) : polygones d'eau, pour masquer en jeu les arbres de MapMarkers poses dans l'eau.
|
|
for (const TGeneralPolygon2<double>& Water : WaterPolygons)
|
|
{
|
|
const int32 OuterRing = Mask->WaterRingStarts.Num();
|
|
Mask->WaterRingStarts.Add(Mask->WaterPoints.Num());
|
|
Mask->WaterRingOuter.Add(INDEX_NONE);
|
|
for (const FVector2d& V : Water.GetOuter().GetVertices()) Mask->WaterPoints.Add(V);
|
|
for (const TPolygon2<double>& Hole : Water.GetHoles())
|
|
{
|
|
Mask->WaterRingStarts.Add(Mask->WaterPoints.Num());
|
|
Mask->WaterRingOuter.Add(OuterRing);
|
|
for (const FVector2d& V : Hole.GetVertices()) Mask->WaterPoints.Add(V);
|
|
}
|
|
}
|
|
// 1.4.2 (STM1-SPEC-19 B1) : arbres de MapMarkers qui tombent sur une chaussee, un trottoir ou la pierre du pack.
|
|
// Regle du BP (MapMarkers CustomEvent_3) : routes de l'UStreetMap au nom non vide, sans « Bridge », de plus de
|
|
// 100 m d'un bout a l'autre ; un arbre a chaque point sauf le dernier, a 10 m de l'axe d'un cote tire au hasard.
|
|
// Les deux positions possibles qui genent recoivent un carre de 1 m dans les anneaux, que le jeu traite comme
|
|
// l'eau (IsInWater, arbres seulement).
|
|
const FString TreeBlockOsm = ParamsMap.FindRef(TEXT("TreeBlockOsm"));
|
|
FString TreeBlockText;
|
|
if (!TreeBlockOsm.IsEmpty())
|
|
{
|
|
if (!bFixedOrigin)
|
|
{
|
|
Line(TEXT("ECHEC : -TreeBlockOsm demande -Origin ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
struct FStrip { FVector2d A, B; double Half; bool bExtend; int32 Kind; };
|
|
TArray<FStrip> Strips;
|
|
TArray<TArray<FVector2D>> StonePolys;
|
|
for (const FStreetMapRoad& Road : StreetMap->GetRoads())
|
|
{
|
|
// Largeurs du plugin (FStreetMapMeshBuildSettings) : rues 8 m, grandes routes 10 m, voies rapides 14 m.
|
|
const double Half = (Road.RoadType == EStreetMapRoadType::Highway ? 1400.0 : Road.RoadType == EStreetMapRoadType::MajorRoad ? 1000.0 : 800.0) * 0.5;
|
|
for (int32 P = 0; P + 1 < Road.RoadPoints.Num(); ++P)
|
|
{
|
|
Strips.Add({ FVector2d(Road.RoadPoints[P]), FVector2d(Road.RoadPoints[P + 1]), Half, false, 0 });
|
|
}
|
|
}
|
|
LPCityOsm::FReader Osm;
|
|
Osm.OriginLat = Origin.Y;
|
|
Osm.OriginLon = Origin.X;
|
|
Osm.bLines = true;
|
|
Osm.bAreas = true;
|
|
FString OsmError;
|
|
if (!LPCityOsm::Read(Osm, TreeBlockOsm, OsmError))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : %s ; aucun asset ecrit"), *OsmError));
|
|
return Finish(1);
|
|
}
|
|
Osm.Nodes.Empty();
|
|
// Memes couches que LPBuildCitySidewalks : trottoirs 2,5 m, rues pietonnes 6 m (prolongees), surfaces de pierre.
|
|
for (int32 L = 0; L < Osm.Lines.Num(); ++L)
|
|
{
|
|
const TMap<FString, FString>& Tags = Osm.LineTags[L];
|
|
const TArray<FVector2D>& Pts = Osm.Lines[L];
|
|
const FString Highway = Tags.FindRef(TEXT("highway"));
|
|
const bool bAreaYes = Tags.FindRef(TEXT("area")) == TEXT("yes");
|
|
const bool bClosed = Pts.Num() >= 4 && Pts[0].Equals(Pts.Last(), KINDA_SMALL_NUMBER);
|
|
const bool bStone = ((Highway == TEXT("pedestrian") || Highway == TEXT("footway")) && bAreaYes)
|
|
|| Tags.FindRef(TEXT("place")) == TEXT("square") || Tags.Contains(TEXT("area:highway"));
|
|
if (bStone)
|
|
{
|
|
if (bClosed) StonePolys.Add(TArray<FVector2D>(Pts.GetData(), Pts.Num() - 1));
|
|
continue;
|
|
}
|
|
const bool bSidewalk = Highway == TEXT("footway") && Tags.FindRef(TEXT("footway")) == TEXT("sidewalk");
|
|
const bool bPedestrian = Highway == TEXT("pedestrian") && !bAreaYes;
|
|
if (!bSidewalk && !bPedestrian) continue;
|
|
for (int32 P = 0; P + 1 < Pts.Num(); ++P)
|
|
{
|
|
Strips.Add({ FVector2d(Pts[P]), FVector2d(Pts[P + 1]), bSidewalk ? 125.0 : 300.0, true, bSidewalk ? 1 : 2 });
|
|
}
|
|
}
|
|
// Grille de 20 m : bandes et polygones par case de leur boite.
|
|
constexpr double Cell = 2000.0;
|
|
auto CellOf = [](double X, double Y) { return FIntPoint(FMath::FloorToInt32(X / Cell), FMath::FloorToInt32(Y / Cell)); };
|
|
TMultiMap<FIntPoint, int32> StripCells, PolyCells;
|
|
for (int32 I = 0; I < Strips.Num(); ++I)
|
|
{
|
|
const FStrip& S = Strips[I];
|
|
const double M = S.Half * (S.bExtend ? 2.0 : 1.0);
|
|
const FIntPoint Min = CellOf(FMath::Min(S.A.X, S.B.X) - M, FMath::Min(S.A.Y, S.B.Y) - M);
|
|
const FIntPoint Max = CellOf(FMath::Max(S.A.X, S.B.X) + M, FMath::Max(S.A.Y, S.B.Y) + M);
|
|
for (int32 X = Min.X; X <= Max.X; ++X) for (int32 Y = Min.Y; Y <= Max.Y; ++Y) StripCells.Add(FIntPoint(X, Y), I);
|
|
}
|
|
TArray<FBox2D> PolyBoxes;
|
|
for (int32 I = 0; I < StonePolys.Num(); ++I)
|
|
{
|
|
const FBox2D Box(StonePolys[I]);
|
|
PolyBoxes.Add(Box);
|
|
const FIntPoint Min = CellOf(Box.Min.X, Box.Min.Y), Max = CellOf(Box.Max.X, Box.Max.Y);
|
|
for (int32 X = Min.X; X <= Max.X; ++X) for (int32 Y = Min.Y; Y <= Max.Y; ++Y) PolyCells.Add(FIntPoint(X, Y), I);
|
|
}
|
|
// Couche genante au point : 0 chaussee, 1 trottoir, 2 pierre ; -1 aucune.
|
|
auto Blocking = [&](const FVector2d& P) -> int32
|
|
{
|
|
int32 Best = -1;
|
|
for (auto It = StripCells.CreateConstKeyIterator(CellOf(P.X, P.Y)); It; ++It)
|
|
{
|
|
const FStrip& S = Strips[It.Value()];
|
|
const FVector2d D = S.B - S.A;
|
|
const double Len = D.Length();
|
|
if (Len < 1.0) continue;
|
|
const FVector2d U = D / Len;
|
|
const double Along = (P - S.A).Dot(U), Across = FMath::Abs((P - S.A).Dot(FVector2d(-U.Y, U.X)));
|
|
const double Lo = S.bExtend ? -S.Half : 0.0, Hi = S.bExtend ? Len + S.Half : Len;
|
|
if (Along >= Lo && Along <= Hi && Across <= S.Half && (Best < 0 || S.Kind < Best)) Best = S.Kind;
|
|
if (Best == 0) return 0;
|
|
}
|
|
if (Best < 0)
|
|
{
|
|
for (auto It = PolyCells.CreateConstKeyIterator(CellOf(P.X, P.Y)); It; ++It)
|
|
{
|
|
if (PolyBoxes[It.Value()].IsInside(FVector2D(P)) && LPCityOsm::InsidePolygon(StonePolys[It.Value()], FVector2D(P))) return 2;
|
|
}
|
|
}
|
|
return Best;
|
|
};
|
|
int32 Roads = 0, Candidates = 0, Blocked[3] = { 0, 0, 0 };
|
|
for (const FStreetMapRoad& Road : StreetMap->GetRoads())
|
|
{
|
|
const TArray<FVector2D>& Pts = Road.RoadPoints;
|
|
if (Road.RoadName.IsEmpty() || Road.RoadName.Contains(TEXT("Bridge"), ESearchCase::IgnoreCase) || Pts.Num() < 2
|
|
|| FVector2D::Distance(Pts[0], Pts.Last()) <= 10000.0)
|
|
{
|
|
continue;
|
|
}
|
|
++Roads;
|
|
for (int32 P = 0; P + 1 < Pts.Num(); ++P)
|
|
{
|
|
// FindLookAtRotation(point, point suivant) puis (0, +-1000, 0) tourne de ce lacet.
|
|
const double Yaw = FMath::Atan2(Pts[P + 1].Y - Pts[P].Y, Pts[P + 1].X - Pts[P].X);
|
|
const FVector2d Side(-FMath::Sin(Yaw) * 1000.0, FMath::Cos(Yaw) * 1000.0);
|
|
for (const double Sign : { 1.0, -1.0 })
|
|
{
|
|
const FVector2d Tree = FVector2d(Pts[P]) + Side * Sign;
|
|
++Candidates;
|
|
const int32 Kind = Blocking(Tree);
|
|
if (Kind < 0) continue;
|
|
++Blocked[Kind];
|
|
Mask->WaterRingStarts.Add(Mask->WaterPoints.Num());
|
|
Mask->WaterRingOuter.Add(INDEX_NONE);
|
|
for (const FVector2d& Corner : { FVector2d(-50, -50), FVector2d(50, -50), FVector2d(50, 50), FVector2d(-50, 50) })
|
|
{
|
|
Mask->WaterPoints.Add(Tree + Corner);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
TreeBlockText = FString::Printf(TEXT(" ; B1 : %d routes, %d positions d'arbre (2 par point), genantes %d (chaussee %d, trottoir %d, pierre %d) -> %d carres de 1 m ; %d bandes, %d surfaces de pierre"),
|
|
Roads, Candidates, Blocked[0] + Blocked[1] + Blocked[2], Blocked[0], Blocked[1], Blocked[2], Blocked[0] + Blocked[1] + Blocked[2], Strips.Num(), StonePolys.Num());
|
|
}
|
|
// 1.4.3 (STM1-SPEC-19 A1) : -HideAllTrees, un anneau couvrant toute la carte (bornes de l'UStreetMap + 100 m) ; tous les
|
|
// arbres de MapMarkers sont masques, remplaces par les arbres du pack (variantes de la table Houses).
|
|
if (Switches.Contains(TEXT("HideAllTrees")))
|
|
{
|
|
const FVector2d Min = FVector2d(StreetMap->GetBoundsMin()) - FVector2d(10000.0), Max = FVector2d(StreetMap->GetBoundsMax()) + FVector2d(10000.0);
|
|
Mask->WaterRingStarts.Add(Mask->WaterPoints.Num());
|
|
Mask->WaterRingOuter.Add(INDEX_NONE);
|
|
for (const FVector2d& Corner : { Min, FVector2d(Max.X, Min.Y), Max, FVector2d(Min.X, Max.Y) }) Mask->WaterPoints.Add(Corner);
|
|
TreeBlockText += FString::Printf(TEXT(" ; tous les arbres masques : anneau (%.0f, %.0f) a (%.0f, %.0f) cm"), Min.X, Min.Y, Max.X, Max.Y);
|
|
}
|
|
FAssetRegistryModule::AssetCreated(Mask);
|
|
FSavePackageArgs MaskSaveArgs;
|
|
MaskSaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
const FString MaskFile = FPackageName::LongPackageNameToFilename(MaskPackageName, FPackageName::GetAssetPackageExtension());
|
|
if (!UPackage::SavePackage(MaskPackage, Mask, *MaskFile, MaskSaveArgs))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *MaskFile));
|
|
return Finish(1);
|
|
}
|
|
Line(FString::Printf(TEXT("asset ecrit : %s (%s), %d centres sur %d batiments ; polygones d'eau : %d anneaux, %d points ; maillage d'eau non touche%s"),
|
|
*MaskPackageName, *MaskFile, InWater.Num(), Buildings.Num(), Mask->WaterRingStarts.Num(), Mask->WaterPoints.Num(), *TreeBlockText));
|
|
return Finish(0);
|
|
}
|
|
}
|
|
|
|
if (bDryRun)
|
|
{
|
|
Line(TEXT("DryRun : aucun asset ecrit"));
|
|
return Finish(0);
|
|
}
|
|
|
|
// 6. Asset /Game/Heavy/StreetMap/Water/<Ville> (plat, Z = 0, une section, M_LP_Water, sans collision).
|
|
// Garde : ne jamais ecraser un maillage d'eau existant.
|
|
const FString PackageName = FString::Printf(TEXT("%s/Water/%s"), *MapRoot, *City);
|
|
if (FPackageName::DoesPackageExist(PackageName))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *PackageName));
|
|
return Finish(1);
|
|
}
|
|
|
|
// 1.7.3b (STM1-DIAG-18) : -PackMaterial=<Nom> [-WaterRoughness=0.7] [-WaterSpecular=0.5] : materiau d'eau propre au pack,
|
|
// cree dans <MapRoot>/Water/<Nom> (jamais ecrase), meme couleur que M_LP_Water ; une rugosite au-dessus de
|
|
// r.SSR.MaxRoughness (0,6 par defaut) sort l'eau des reflexions SSR, qui scintillaient sous TAA.
|
|
const FString PackMaterialName = ParamsMap.FindRef(TEXT("PackMaterial"));
|
|
const FString PackMaterialPackage = PackMaterialName.IsEmpty() ? FString() : FString::Printf(TEXT("%s/Water/%s"), *MapRoot, *PackMaterialName);
|
|
if (!PackMaterialName.IsEmpty() && (MapRoot == TEXT("/Game/Heavy/StreetMap") || FPackageName::DoesPackageExist(PackMaterialPackage)))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : -PackMaterial seulement pour un pack, et %s ne doit pas exister ; aucun asset ecrit"), *PackMaterialPackage));
|
|
return Finish(1);
|
|
}
|
|
const float WaterRoughness = ParamsMap.Contains(TEXT("WaterRoughness")) ? FCString::Atof(*ParamsMap[TEXT("WaterRoughness")]) : 0.7f;
|
|
const float WaterSpecular = ParamsMap.Contains(TEXT("WaterSpecular")) ? FCString::Atof(*ParamsMap[TEXT("WaterSpecular")]) : 0.5f;
|
|
const bool bPackMaterial = !PackMaterialName.IsEmpty();
|
|
const FString MaterialPackageString = bPackMaterial ? PackMaterialPackage : FString(TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water"));
|
|
const TCHAR* MaterialPackageName = *MaterialPackageString;
|
|
UMaterialInterface* WaterMaterial = bPackMaterial ? nullptr : LoadObject<UMaterialInterface>(nullptr, TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water.M_LP_Water"));
|
|
if (!WaterMaterial && (bPackMaterial || Switches.Contains(TEXT("CreateMaterial"))))
|
|
{
|
|
// STM1-SPEC-03 § 2.5 : opaque, deux faces, DefaultLit, couleur (0,03 ; 0,18 ; 0,32), rugosite 0,15, speculaire 0,5.
|
|
UPackage* MaterialPackage = CreatePackage(MaterialPackageName);
|
|
UMaterial* NewMaterial = NewObject<UMaterial>(MaterialPackage, bPackMaterial ? FName(*PackMaterialName) : FName(TEXT("M_LP_Water")), RF_Public | RF_Standalone);
|
|
NewMaterial->TwoSided = true;
|
|
UMaterialExpressionConstant3Vector* Color = Cast<UMaterialExpressionConstant3Vector>(
|
|
UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant3Vector::StaticClass(), -300, 0));
|
|
UMaterialExpressionConstant* Roughness = Cast<UMaterialExpressionConstant>(
|
|
UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant::StaticClass(), -300, 150));
|
|
UMaterialExpressionConstant* Specular = Cast<UMaterialExpressionConstant>(
|
|
UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant::StaticClass(), -300, 250));
|
|
if (!Color || !Roughness || !Specular)
|
|
{
|
|
Line(TEXT("ECHEC : creation des expressions du materiau ; aucun asset ecrit"));
|
|
return Finish(1);
|
|
}
|
|
Color->Constant = FLinearColor(0.03f, 0.18f, 0.32f);
|
|
Roughness->R = bPackMaterial ? WaterRoughness : 0.15f;
|
|
Specular->R = bPackMaterial ? WaterSpecular : 0.5f;
|
|
const bool bLinked = UMaterialEditingLibrary::ConnectMaterialProperty(Color, TEXT(""), MP_BaseColor)
|
|
&& UMaterialEditingLibrary::ConnectMaterialProperty(Roughness, TEXT(""), MP_Roughness)
|
|
&& UMaterialEditingLibrary::ConnectMaterialProperty(Specular, TEXT(""), MP_Specular);
|
|
UMaterialEditingLibrary::RecompileMaterial(NewMaterial);
|
|
FAssetRegistryModule::AssetCreated(NewMaterial);
|
|
FSavePackageArgs MaterialSaveArgs;
|
|
MaterialSaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
const FString MaterialFile = FPackageName::LongPackageNameToFilename(MaterialPackageName, FPackageName::GetAssetPackageExtension());
|
|
if (!bLinked || !UPackage::SavePackage(MaterialPackage, NewMaterial, *MaterialFile, MaterialSaveArgs))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : materiau (liaisons %d) ou sauvegarde de %s"), bLinked ? 1 : 0, *MaterialFile));
|
|
return Finish(1);
|
|
}
|
|
Line(FString::Printf(TEXT("materiau cree : %s (%s), deux faces ; rugosite %.2f ; speculaire %.2f"), MaterialPackageName, *MaterialFile, Roughness->R, Specular->R));
|
|
WaterMaterial = NewMaterial;
|
|
}
|
|
if (!WaterMaterial)
|
|
{
|
|
Line(TEXT("ECHEC : materiau /Game/Heavy/StreetMap/Water/M_LP_Water absent (option -CreateMaterial, STM1-SPEC-03 § 2.5)"));
|
|
return Finish(1);
|
|
}
|
|
FMeshDescription Mesh;
|
|
FStaticMeshAttributes Attributes(Mesh);
|
|
Attributes.Register();
|
|
const FPolygonGroupID Group = Mesh.CreatePolygonGroup();
|
|
Attributes.GetPolygonGroupMaterialSlotNames()[Group] = TEXT("Water");
|
|
// 1.7.5 (STM1-DIAG-20) : -WaterZOffsetCm=<cm> releve tous les sommets de l'eau. Le jeu la pose au plan de pave de MapMarkers
|
|
// + 2 cm ; ce plan (BasicShapes/Plane a l'echelle 1e6, sommets a ~5e7 cm) n'a qu'une resolution de quelques cm en flottant :
|
|
// a 2 cm, l'eau et le pave se disputaient les pixels (scintillement de toute la surface).
|
|
const float WaterZOffsetCm = ParamsMap.Contains(TEXT("WaterZOffsetCm")) ? FCString::Atof(*ParamsMap[TEXT("WaterZOffsetCm")]) : 0.f;
|
|
if (WaterZOffsetCm != 0.f)
|
|
{
|
|
Line(FString::Printf(TEXT("decalage Z de l'eau : %.1f cm (%d sommets)"), WaterZOffsetCm, Vertices.Num()));
|
|
}
|
|
TArray<FVertexID> VertexIds;
|
|
VertexIds.Reserve(Vertices.Num());
|
|
for (const FVector3f& V : Vertices)
|
|
{
|
|
const FVertexID Id = Mesh.CreateVertex();
|
|
Attributes.GetVertexPositions()[Id] = V + FVector3f(0.f, 0.f, WaterZOffsetCm);
|
|
VertexIds.Add(Id);
|
|
}
|
|
for (const FIntVector3& T : Triangles)
|
|
{
|
|
TArray<FVertexInstanceID> Corners;
|
|
for (const int32 Index : { T.X, T.Y, T.Z })
|
|
{
|
|
const FVertexInstanceID Instance = Mesh.CreateVertexInstance(VertexIds[Index]);
|
|
Attributes.GetVertexInstanceNormals()[Instance] = FVector3f::UpVector;
|
|
Attributes.GetVertexInstanceUVs().Set(Instance, 0, FVector2f(Vertices[Index].X / 100.f, Vertices[Index].Y / 100.f));
|
|
Corners.Add(Instance);
|
|
}
|
|
Mesh.CreatePolygon(Group, Corners);
|
|
}
|
|
|
|
UPackage* Package = CreatePackage(*PackageName);
|
|
UStaticMesh* WaterMesh = NewObject<UStaticMesh>(Package, *City, RF_Public | RF_Standalone);
|
|
WaterMesh->GetStaticMaterials().Add(FStaticMaterial(WaterMaterial, TEXT("Water")));
|
|
UStaticMesh::FBuildMeshDescriptionsParams BuildParams;
|
|
BuildParams.bBuildSimpleCollision = false;
|
|
BuildParams.bCommitMeshDescription = true;
|
|
BuildParams.bMarkPackageDirty = true;
|
|
WaterMesh->BuildFromMeshDescriptions({ &Mesh }, BuildParams);
|
|
FAssetRegistryModule::AssetCreated(WaterMesh);
|
|
|
|
FSavePackageArgs SaveArgs;
|
|
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
const FString FileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
|
|
if (!UPackage::SavePackage(Package, WaterMesh, *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);
|
|
}
|