1119 lines
58 KiB
C++
1119 lines
58 KiB
C++
|
|
// Lot STM1 (STM1-SPEC-12, G4-b ; 1.2.1). Voir LPBuildCityHousesCommandlet.h.
|
||
|
|
|
||
|
|
#include "LPBuildCityHousesCommandlet.h"
|
||
|
|
|
||
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||
|
|
#include "LPCityOsm.h"
|
||
|
|
#include "LPCityHouses.h"
|
||
|
|
#include "LPCityLandmarks.h"
|
||
|
|
#include "LPCityWaterMask.h"
|
||
|
|
#include "Misc/FileHelper.h"
|
||
|
|
#include "Misc/PackageName.h"
|
||
|
|
#include "StreetMap.h"
|
||
|
|
#include "UObject/Package.h"
|
||
|
|
#include "UObject/SavePackage.h"
|
||
|
|
|
||
|
|
DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCityHouses, Log, All);
|
||
|
|
|
||
|
|
namespace LPBuildCityHouses
|
||
|
|
{
|
||
|
|
/** 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");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Cle de case de 1 m pour retrouver un centre a 50 cm pres. */
|
||
|
|
FIntPoint Cell(const FVector2D& P)
|
||
|
|
{
|
||
|
|
return FIntPoint(FMath::FloorToInt32(P.X / 100.0), FMath::FloorToInt32(P.Y / 100.0));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Aire du polygone (cm2). */
|
||
|
|
double PolygonArea(const TArray<FVector2D>& Pts)
|
||
|
|
{
|
||
|
|
double A = 0.0;
|
||
|
|
for (int32 I = 0; I < Pts.Num(); ++I)
|
||
|
|
{
|
||
|
|
const FVector2D& P = Pts[I];
|
||
|
|
const FVector2D& Q = Pts[(I + 1) % Pts.Num()];
|
||
|
|
A += P.X * Q.Y - Q.X * P.Y;
|
||
|
|
}
|
||
|
|
return FMath::Abs(A) * 0.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool InsidePolygon(const TArray<FVector2D>& Pts, const FVector2D& P)
|
||
|
|
{
|
||
|
|
bool bInside = false;
|
||
|
|
for (int32 I = 0, J = Pts.Num() - 1; I < Pts.Num(); J = I++)
|
||
|
|
{
|
||
|
|
if (((Pts[I].Y > P.Y) != (Pts[J].Y > P.Y))
|
||
|
|
&& (P.X < (Pts[J].X - Pts[I].X) * (P.Y - Pts[I].Y) / (Pts[J].Y - Pts[I].Y) + Pts[I].X))
|
||
|
|
{
|
||
|
|
bInside = !bInside;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return bInside;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Lecteur OSM commun (LPCityOsm.h, 1.3.0). */
|
||
|
|
using FOsmReader = LPCityOsm::FReader;
|
||
|
|
|
||
|
|
/** Axe local du maillage (+X, -X, +Y, -Y) ; faux si le texte est autre. */
|
||
|
|
bool ParseAxis(const FString& Text, FVector2D& Out)
|
||
|
|
{
|
||
|
|
if (Text == TEXT("+X")) { Out = FVector2D(1, 0); return true; }
|
||
|
|
if (Text == TEXT("-X")) { Out = FVector2D(-1, 0); return true; }
|
||
|
|
if (Text == TEXT("+Y")) { Out = FVector2D(0, 1); return true; }
|
||
|
|
if (Text == TEXT("-Y")) { Out = FVector2D(0, -1); return true; }
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
double AngleDeg(const FVector2D& V)
|
||
|
|
{
|
||
|
|
return FMath::RadiansToDegrees(FMath::Atan2(V.Y, V.X));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Amenite de commerce ou de restauration (valeurs OSM ou cles d'AmenityImageDict ; 1.3.0). */
|
||
|
|
bool IsShopAmenity(const FString& Amenity)
|
||
|
|
{
|
||
|
|
if (Amenity.IsEmpty()) return false;
|
||
|
|
static const TSet<FString> NotShops = { TEXT("home"), TEXT("work"), TEXT("university"), TEXT("university:school"), TEXT("university:work"),
|
||
|
|
TEXT("hotel"), TEXT("art"), TEXT("theatre"), TEXT("cinema"), TEXT("hospital"), TEXT("hospital:work"), TEXT("place_of_worship"),
|
||
|
|
TEXT("police"), TEXT("post_office"), TEXT("library"), TEXT("community_centre"), TEXT("toilets"), TEXT("sports_centre"),
|
||
|
|
TEXT("prison"), TEXT("fraternity"), TEXT("dentist"), TEXT("doctor"), TEXT("veterinary"), TEXT("parking"), TEXT("school") };
|
||
|
|
return !NotShops.Contains(Amenity);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Index des variantes du mode Haussmann. */
|
||
|
|
enum EVariant : uint8 { Etroit = 0, Large = 1, AngleCoupe = 2, AngleCoupole = 3, Commerces = 4, PorteCochere = 5, Petit = 6 };
|
||
|
|
}
|
||
|
|
|
||
|
|
ULPBuildCityHousesCommandlet::ULPBuildCityHousesCommandlet()
|
||
|
|
{
|
||
|
|
IsClient = false;
|
||
|
|
IsServer = false;
|
||
|
|
IsEditor = true;
|
||
|
|
LogToConsole = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
int32 ULPBuildCityHousesCommandlet::Main(const FString& Params)
|
||
|
|
{
|
||
|
|
using namespace LPBuildCityHouses;
|
||
|
|
|
||
|
|
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 VariantsText = ParamsMap.FindRef(TEXT("Variants"));
|
||
|
|
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
|
||
|
|
const FString OsmPath = ParamsMap.FindRef(TEXT("Osm"));
|
||
|
|
const FString OriginText = ParamsMap.FindRef(TEXT("Origin"));
|
||
|
|
const bool bHaussmann = ParamsMap.FindRef(TEXT("Layout")) == TEXT("Haussmann");
|
||
|
|
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
|
||
|
|
auto Num = [&ParamsMap](const TCHAR* Key, double Default) { return ParamsMap.Contains(Key) ? FCString::Atod(*ParamsMap[Key]) : Default; };
|
||
|
|
const int32 DefaultLevels = int32(Num(TEXT("DefaultLevels"), 6));
|
||
|
|
const double LevelHeightCm = Num(TEXT("LevelHeightCm"), 300.0);
|
||
|
|
const double MinSideCm = Num(TEXT("MinSideCm"), 300.0);
|
||
|
|
const double MinAreaCm2 = Num(TEXT("MinAreaM2"), 20.0) * 1e4;
|
||
|
|
const double SmallAreaCm2 = Num(TEXT("SmallAreaM2"), 30.0) * 1e4;
|
||
|
|
const double ModuleCm = FMath::Max(300.0, Num(TEXT("ModuleCm"), 1300.0));
|
||
|
|
const double DepthModuleCm = FMath::Max(300.0, Num(TEXT("DepthModuleCm"), 1500.0));
|
||
|
|
const double StreetCm = Num(TEXT("StreetCm"), 2500.0);
|
||
|
|
const double CornerMaxCm = Num(TEXT("CornerMaxCm"), 2500.0);
|
||
|
|
const double CornerStreetCm = Num(TEXT("CornerStreetCm"), 1200.0);
|
||
|
|
// 1.2.2 : variante Petit reservee aux petites emprises (et aux pavillons de parc sans etages OSM).
|
||
|
|
const double PetitMaxAreaCm2 = Num(TEXT("PetitMaxAreaM2"), 100.0) * 1e4;
|
||
|
|
const double ParkPetitMaxAreaCm2 = Num(TEXT("ParkPetitMaxAreaM2"), 1000.0) * 1e4;
|
||
|
|
const double TinyMaskCm2 = Num(TEXT("TinyMaskM2"), 30.0) * 1e4;
|
||
|
|
// 1.3.0 : -RealShops : la variante Commerces va aux batiments dont l'amenite (UStreetMap enrichi, LPEnrichCityBuildings)
|
||
|
|
// est un commerce ou de la restauration, et a eux seuls.
|
||
|
|
const bool bRealShops = Switches.Contains(TEXT("RealShops"));
|
||
|
|
|
||
|
|
TArray<FString> Report;
|
||
|
|
auto Line = [&Report](const FString& Text)
|
||
|
|
{
|
||
|
|
UE_LOG(LogLPBuildCityHouses, 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> VariantPaths;
|
||
|
|
VariantsText.ParseIntoArray(VariantPaths, TEXT(";"));
|
||
|
|
TArray<FString> TreePaths;
|
||
|
|
ParamsMap.FindRef(TEXT("Trees")).ParseIntoArray(TreePaths, TEXT(";"));
|
||
|
|
if (City.IsEmpty() || MapRoot.IsEmpty() || VariantPaths.IsEmpty() || VariantPaths.Num() > 255 || (!OsmPath.IsEmpty() && OriginText.IsEmpty()))
|
||
|
|
{
|
||
|
|
Line(TEXT("usage : -City=<Ville> -MapRoot=</Game/Heavy/StreetMap | /lpm_city_<v>/StreetMap> -Variants=<objet>;<objet>;... [-KeepSkyscrapers] [-DefaultLevels=6] [-LevelHeightCm=300] [-MinSideCm=300] [-Osm=<f.osm.xml> -Origin=<lat>,<lon> [-MinAreaM2=20] [-SmallAreaM2=30]] [-Layout=Haussmann -MeshFront=-Y -MeshCorner=+X [-VariantLevels=5,6,6,6,5,5] [-ModuleCm=1300] [-DepthModuleCm=1500] [-StreetCm=2500] [-CornerStreetCm=1200] [-CornerMaxCm=2500] [-PetitMaxAreaM2=100] [-ParkPetitMaxAreaM2=1000]] [-RealShops] [-Trees=<objet>;... [-TreePitchM=10] [-TreeHeightM=9,13] [-TreeNearRoadM=15]] [-Churches=<allongee>;...;<trapue> [-ChurchRatio=1.8] [-ChurchHeightM=10,60] [-ChurchStretch=1.3] [-ChurchMaxRatio=4]] [-TallBar=<objet>@<n>;...] [-TallTower=...] [-TallOffice=...] [-TallFromLevels=10] [-TallTowerLevels=15] [-TallMaxLevels=40] [-ModernLow=<objet>@<n>;... -ModernBox=<s>,<o>,<n>,<e> [-ModernNearTallM=40] [-ModernBeforeYear=1950] [-ModernArea=<nom_OSM> [-ModernAreaMarginM=30] [-ModernAreaBaseM=8]]] [-TallSkipMonuments] [-TallMaxBoxRatio=<r>] [-Furniture=<objet>@<poses.csv>@<hauteur m>@<+X|-X|+Y|-Y>;...] [-DryRun] [-Report=<f.md>]"));
|
||
|
|
return Finish(2);
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("# LPBuildCityHouses %s (%s) ; racine %s ; %d variante(s) ; etages par defaut %d x %.0f cm ; disposition %s"),
|
||
|
|
*City, bDryRun ? TEXT("DryRun") : TEXT("ecriture"), *MapRoot, VariantPaths.Num(), DefaultLevels, LevelHeightCm, bHaussmann ? TEXT("Haussmann") : TEXT("une maison par batiment")));
|
||
|
|
if (!IsAllowedRoot(MapRoot))
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : -MapRoot doit etre /Game/Heavy/StreetMap ou /lpm_city_<ville>/StreetMap ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
TArray<FBox> VariantBoxes;
|
||
|
|
for (const FString& Path : VariantPaths)
|
||
|
|
{
|
||
|
|
const UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, *Path);
|
||
|
|
const FBox Box = Mesh ? Mesh->GetBoundingBox() : FBox(ForceInit);
|
||
|
|
VariantBoxes.Add(Box);
|
||
|
|
Line(FString::Printf(TEXT("variante %s : %s"), *Path, Mesh ? *FString::Printf(TEXT("boite %.0f x %.0f x %.0f cm, pied Z %.0f"), Box.GetSize().X, Box.GetSize().Y, Box.GetSize().Z, Box.Min.Z) : TEXT("ABSENTE")));
|
||
|
|
if (!Mesh)
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : variante introuvable ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mode Haussmann : axes du maillage et etages des variantes.
|
||
|
|
FVector2D MeshFront(0, -1), MeshCorner(1, 0);
|
||
|
|
TArray<int32> VariantLevels = { 5, 6, 6, 6, 5, 5, 1 };
|
||
|
|
if (bHaussmann)
|
||
|
|
{
|
||
|
|
if (VariantPaths.Num() < 6 || VariantPaths.Num() > 7
|
||
|
|
|| !ParseAxis(ParamsMap.FindRef(TEXT("MeshFront")), MeshFront) || !ParseAxis(ParamsMap.FindRef(TEXT("MeshCorner")), MeshCorner)
|
||
|
|
|| FMath::Abs(FVector2D::DotProduct(MeshFront, MeshCorner)) > 0.5)
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : -Layout=Haussmann attend 6 ou 7 variantes (Etroit;Large;Angle_Coupe;Angle_Coupole;Commerces;Porte_Cochere[;Petit]), -MeshFront et -MeshCorner perpendiculaires ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
if (ParamsMap.Contains(TEXT("VariantLevels")))
|
||
|
|
{
|
||
|
|
TArray<FString> LevelText;
|
||
|
|
ParamsMap[TEXT("VariantLevels")].ParseIntoArray(LevelText, TEXT(","));
|
||
|
|
for (int32 I = 0; I < LevelText.Num() && I < VariantLevels.Num(); ++I) VariantLevels[I] = FMath::Max(1, FCString::Atoi(*LevelText[I]));
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("Haussmann : facade du maillage %s, coin coupe cote %s ; etages des variantes %d,%d,%d,%d,%d,%d ; module %.0f cm, profondeur %.0f cm ; rue a %.0f cm ; angle d'au plus %.0f cm, rues a %.0f cm ; variante Petit %s"),
|
||
|
|
*ParamsMap.FindRef(TEXT("MeshFront")), *ParamsMap.FindRef(TEXT("MeshCorner")), VariantLevels[0], VariantLevels[1], VariantLevels[2], VariantLevels[3], VariantLevels[4], VariantLevels[5],
|
||
|
|
ModuleCm, DepthModuleCm, StreetCm, CornerMaxCm, CornerStreetCm, VariantPaths.Num() == 7 ? TEXT("oui") : TEXT("non")));
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Exclusions : centres du masque d'eau et des monuments (meme definition de centre).
|
||
|
|
TMultiMap<FIntPoint, FVector2D> Excluded;
|
||
|
|
int32 WaterExcluded = 0, LandmarkExcluded = 0;
|
||
|
|
if (const ULPCityWaterMask* Mask = LoadObject<ULPCityWaterMask>(nullptr, *FString::Printf(TEXT("%s/Water/%s_Mask.%s_Mask"), *MapRoot, *City, *City)))
|
||
|
|
{
|
||
|
|
for (const FVector2D& C : Mask->Centers) Excluded.Add(Cell(C), C);
|
||
|
|
WaterExcluded = Mask->Centers.Num();
|
||
|
|
}
|
||
|
|
if (const ULPCityLandmarks* Landmarks = LoadObject<ULPCityLandmarks>(nullptr, *FString::Printf(TEXT("%s/Landmarks/%s.%s"), *MapRoot, *City, *City)))
|
||
|
|
{
|
||
|
|
for (const FLPCityLandmark& L : Landmarks->Landmarks)
|
||
|
|
{
|
||
|
|
for (const FVector2D& C : L.HideCenters) { Excluded.Add(Cell(C), C); ++LandmarkExcluded; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("exclusions : masque d'eau %d centres ; monuments %d centres"), WaterExcluded, LandmarkExcluded));
|
||
|
|
auto FindNear = [](const TMultiMap<FIntPoint, FVector2D>& Map, const FVector2D& C, FVector2D* OutFound) -> bool
|
||
|
|
{
|
||
|
|
const FIntPoint K = Cell(C);
|
||
|
|
for (int32 DX = -1; DX <= 1; ++DX)
|
||
|
|
for (int32 DY = -1; DY <= 1; ++DY)
|
||
|
|
for (auto It = Map.CreateConstKeyIterator(K + FIntPoint(DX, DY)); It; ++It)
|
||
|
|
if (FVector2D::Distance(It.Value(), C) <= 50.0) { if (OutFound) *OutFound = It.Value(); return true; }
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Balises OSM (1.2.1).
|
||
|
|
FOsmReader Osm;
|
||
|
|
TMultiMap<FIntPoint, FVector2D> OsmCells;
|
||
|
|
TMap<FVector2D, int32> OsmIndex;
|
||
|
|
if (!OsmPath.IsEmpty())
|
||
|
|
{
|
||
|
|
TArray<FString> OriginParts;
|
||
|
|
OriginText.ParseIntoArray(OriginParts, TEXT(","));
|
||
|
|
if (OriginParts.Num() != 2)
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : -Origin=<lat>,<lon> attendu ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
Osm.OriginLat = FCString::Atod(*OriginParts[0]);
|
||
|
|
Osm.OriginLon = FCString::Atod(*OriginParts[1]);
|
||
|
|
FText XmlError;
|
||
|
|
int32 XmlErrorLine = 0;
|
||
|
|
if (!FFastXml::ParseXmlFile(&Osm, *OsmPath, nullptr, nullptr, false, false, XmlError, XmlErrorLine))
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("ECHEC : lecture de %s (ligne %d : %s) ; aucun asset ecrit"), *OsmPath, XmlErrorLine, *XmlError.ToString()));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
for (int32 I = 0; I < Osm.WayCenters.Num(); ++I)
|
||
|
|
{
|
||
|
|
OsmCells.Add(Cell(Osm.WayCenters[I]), Osm.WayCenters[I]);
|
||
|
|
OsmIndex.Add(Osm.WayCenters[I], I);
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("OSM %s : %d noeuds, %d voies d'au moins 3 points ; origine lat %.9f lon %.9f"), *OsmPath, Osm.Nodes.Num(), Osm.WayCenters.Num(), Osm.OriginLat, Osm.OriginLon));
|
||
|
|
Osm.Nodes.Empty();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Routes de l'UStreetMap : segments ranges par cases de 50 m (facades sur rue, mode Haussmann).
|
||
|
|
constexpr double RoadCellCm = 5000.0;
|
||
|
|
TArray<TPair<FVector2D, FVector2D>> Segments;
|
||
|
|
TMultiMap<FIntPoint, int32> SegmentCells;
|
||
|
|
auto RoadCell = [](const FVector2D& P) { return FIntPoint(FMath::FloorToInt32(P.X / RoadCellCm), FMath::FloorToInt32(P.Y / RoadCellCm)); };
|
||
|
|
if (bHaussmann)
|
||
|
|
{
|
||
|
|
for (const FStreetMapRoad& Road : StreetMap->GetRoads())
|
||
|
|
{
|
||
|
|
for (int32 I = 0; I + 1 < Road.RoadPoints.Num(); ++I)
|
||
|
|
{
|
||
|
|
const FVector2D A = Road.RoadPoints[I], B = Road.RoadPoints[I + 1];
|
||
|
|
const int32 Index = Segments.Add(TPair<FVector2D, FVector2D>(A, B));
|
||
|
|
const FIntPoint CA = RoadCell(A), CB = RoadCell(B);
|
||
|
|
for (int32 X = FMath::Min(CA.X, CB.X); X <= FMath::Max(CA.X, CB.X); ++X)
|
||
|
|
for (int32 Y = FMath::Min(CA.Y, CB.Y); Y <= FMath::Max(CA.Y, CB.Y); ++Y)
|
||
|
|
SegmentCells.Add(FIntPoint(X, Y), Index);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("routes : %d segments"), Segments.Num()));
|
||
|
|
}
|
||
|
|
// Parcs de l'UStreetMap (pavillons sans etages : variante Petit).
|
||
|
|
TArray<const FStreetMapPark*> Parks;
|
||
|
|
TArray<FBox2D> ParkBoxes;
|
||
|
|
for (const FStreetMapPark& Park : StreetMap->GetParks())
|
||
|
|
{
|
||
|
|
if (Park.ParkPoints.Num() < 3) continue;
|
||
|
|
Parks.Add(&Park);
|
||
|
|
ParkBoxes.Add(FBox2D(Park.ParkPoints));
|
||
|
|
}
|
||
|
|
auto InAnyPark = [&](const FVector2D& P)
|
||
|
|
{
|
||
|
|
for (int32 I = 0; I < Parks.Num(); ++I)
|
||
|
|
{
|
||
|
|
if (ParkBoxes[I].IsInside(P) && InsidePolygon(Parks[I]->ParkPoints, P)) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
auto DistanceToRoad = [&](const FVector2D& P) -> double
|
||
|
|
{
|
||
|
|
double Best = TNumericLimits<double>::Max();
|
||
|
|
const FIntPoint K = RoadCell(P);
|
||
|
|
const int32 R = FMath::CeilToInt32(StreetCm / RoadCellCm);
|
||
|
|
TSet<int32> Seen;
|
||
|
|
for (int32 DX = -R; DX <= R; ++DX)
|
||
|
|
for (int32 DY = -R; DY <= R; ++DY)
|
||
|
|
for (auto It = SegmentCells.CreateConstKeyIterator(K + FIntPoint(DX, DY)); It; ++It)
|
||
|
|
{
|
||
|
|
bool bSeen = false;
|
||
|
|
Seen.Add(It.Value(), &bSeen);
|
||
|
|
if (bSeen) continue;
|
||
|
|
const FVector2D& A = Segments[It.Value()].Key;
|
||
|
|
const FVector2D& B = Segments[It.Value()].Value;
|
||
|
|
const FVector2D AB = B - A;
|
||
|
|
const double T = AB.SizeSquared() > 0.0 ? FMath::Clamp(FVector2D::DotProduct(P - A, AB) / AB.SizeSquared(), 0.0, 1.0) : 0.0;
|
||
|
|
Best = FMath::Min(Best, FVector2D::Distance(P, A + AB * T));
|
||
|
|
}
|
||
|
|
return Best;
|
||
|
|
};
|
||
|
|
|
||
|
|
const FString PackageName = FString::Printf(TEXT("%s/Houses/%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);
|
||
|
|
ULPCityHouses* Asset = NewObject<ULPCityHouses>(Package, bDryRun ? NAME_None : FName(*City), bDryRun ? RF_Transient : (RF_Public | RF_Standalone));
|
||
|
|
Asset->City = City;
|
||
|
|
Asset->bReplaceSkyscrapers = !Switches.Contains(TEXT("KeepSkyscrapers"));
|
||
|
|
for (const FString& Path : VariantPaths) Asset->Variants.Add(TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(Path)));
|
||
|
|
|
||
|
|
static const TSet<FString> SmallTypes = { TEXT("kiosk"), TEXT("shed"), TEXT("toilets"), TEXT("hut"), TEXT("cabin"), TEXT("garage"), TEXT("garages"), TEXT("carport") };
|
||
|
|
int32 Skipped = 0, Excl = 0, FromLevels = 0, FromHeight = 0, FromDefault = 0, SmallOne = 0, Matched = 0;
|
||
|
|
int32 OutUnderground = 0, OutLayer = 0, OutTunnel = 0, OutRoof = 0, OutArea = 0;
|
||
|
|
int32 Buildings = 0, Corners = 0, Tiled = 0, NoStreet = 0, SmallBuildings = 0, CellsOutside = 0, ParkPavilions = 0, NarrowTall = 0, ShopBuildings = 0, ParkMonuments = 0, TinyMasked = 0;
|
||
|
|
TArray<int32> PerVariant;
|
||
|
|
PerVariant.SetNumZeroed(VariantPaths.Num());
|
||
|
|
|
||
|
|
// 1.6.0 (STM1-SPEC-21 § 3 bis, E1) : -Churches=<allongee>;<trapue> : eglises et chapelles (building=church, chapel, cathedral,
|
||
|
|
// basilica, ou amenity=place_of_worship + religion=christian) en une seule entree d'un modele generique, au lieu des modules
|
||
|
|
// Haussmann : nef (axe X du maillage) sur le grand cote de la boite orientee, plan etire sur l'emprise, hauteur = longueur x
|
||
|
|
// (hauteur / longueur du modele), bornee par -ChurchHeightM (10,60) ; modele allonge si longueur / largeur >= -ChurchRatio
|
||
|
|
// (1,8), sinon trapu. Variantes ajoutees apres celles des maisons, avant les arbres.
|
||
|
|
// 1.6.1 (STM1-DIAG-07 b) : -Churches=<allongee>;...;<trapue> (le dernier est le trapu ; les allongees alternent par la position) ;
|
||
|
|
// longueur posee au plus largeur x rapport du modele x -ChurchStretch (1,3), centree ; emprise de rapport > -ChurchMaxRatio (4) :
|
||
|
|
// pas d'eglise (modules Haussmann).
|
||
|
|
TArray<FString> ChurchPaths;
|
||
|
|
ParamsMap.FindRef(TEXT("Churches")).ParseIntoArray(ChurchPaths, TEXT(";"));
|
||
|
|
const int32 ChurchBase = Asset->Variants.Num();
|
||
|
|
TArray<FBox> ChurchBoxes;
|
||
|
|
double ChurchHMin = 1000.0, ChurchHMax = 6000.0;
|
||
|
|
const double ChurchRatio = ParamsMap.Contains(TEXT("ChurchRatio")) ? FCString::Atod(*ParamsMap[TEXT("ChurchRatio")]) : 1.8;
|
||
|
|
const double ChurchStretch = ParamsMap.Contains(TEXT("ChurchStretch")) ? FCString::Atod(*ParamsMap[TEXT("ChurchStretch")]) : 0.0;
|
||
|
|
const double ChurchMaxRatio = ParamsMap.Contains(TEXT("ChurchMaxRatio")) ? FCString::Atod(*ParamsMap[TEXT("ChurchMaxRatio")]) : 0.0;
|
||
|
|
int32 Churches[2] = { 0, 0 }, ChurchesShortened = 0, ChurchesTooLong = 0;
|
||
|
|
if (!ChurchPaths.IsEmpty())
|
||
|
|
{
|
||
|
|
if (ChurchPaths.Num() < 2 || OsmPath.IsEmpty())
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : -Churches attend au moins deux maillages (allonge;...;trapu) et -Osm ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
TArray<FString> HText;
|
||
|
|
ParamsMap.FindRef(TEXT("ChurchHeightM")).ParseIntoArray(HText, TEXT(","));
|
||
|
|
if (HText.Num() == 2) { ChurchHMin = FCString::Atod(*HText[0]) * 100.0; ChurchHMax = FCString::Atod(*HText[1]) * 100.0; }
|
||
|
|
for (const FString& Path : ChurchPaths)
|
||
|
|
{
|
||
|
|
const UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, *Path);
|
||
|
|
if (!Mesh)
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("ECHEC : eglise %s introuvable ; aucun asset ecrit"), *Path));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
ChurchBoxes.Add(Mesh->GetBoundingBox());
|
||
|
|
Asset->Variants.Add(TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(Path)));
|
||
|
|
PerVariant.Add(0);
|
||
|
|
Line(FString::Printf(TEXT("eglise %s : boite %.0f x %.0f x %.0f cm"), *Path, ChurchBoxes.Last().GetSize().X, ChurchBoxes.Last().GetSize().Y, ChurchBoxes.Last().GetSize().Z));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 1.7.0 (STM1-SPEC-21 § 3) : immeubles hauts. -TallBar, -TallTower, -TallOffice = <objet>@<niveaux>;... : un batiment de
|
||
|
|
// -TallFromLevels (10) niveaux reels ou plus (building:levels, sinon height / 3 m, sans la borne de 15) prend une seule entree
|
||
|
|
// d'un modele de sa famille : Bureau si building=office|commercial, office=* ou building:use=office ; sinon Tour a partir de
|
||
|
|
// -TallTowerLevels (15), Barre en dessous. Dans la famille, le modele aux niveaux les plus proches (egalite : position).
|
||
|
|
// Plan etire sur la boite orientee (grand cote sur X du maillage), hauteur reelle (niveaux x -LevelHeightCm, au plus
|
||
|
|
// -TallMaxLevels). Variantes ajoutees apres les eglises, avant les arbres.
|
||
|
|
struct FTallModel { int32 Variant; int32 Levels; };
|
||
|
|
TArray<FTallModel> TallFamilies[3]; // 0 Barre, 1 Tour, 2 Bureau
|
||
|
|
const TCHAR* const TallKeys[3] = { TEXT("TallBar"), TEXT("TallTower"), TEXT("TallOffice") };
|
||
|
|
const TCHAR* const TallNames[3] = { TEXT("barres"), TEXT("tours"), TEXT("bureaux") };
|
||
|
|
const int32 TallFrom = ParamsMap.Contains(TEXT("TallFromLevels")) ? FCString::Atoi(*ParamsMap[TEXT("TallFromLevels")]) : 10;
|
||
|
|
const int32 TallTower = ParamsMap.Contains(TEXT("TallTowerLevels")) ? FCString::Atoi(*ParamsMap[TEXT("TallTowerLevels")]) : 15;
|
||
|
|
const int32 TallMax = ParamsMap.Contains(TEXT("TallMaxLevels")) ? FCString::Atoi(*ParamsMap[TEXT("TallMaxLevels")]) : 40;
|
||
|
|
int32 TallCount[3] = { 0, 0, 0 }, TallFallback = 0;
|
||
|
|
for (int32 F = 0; F < 3; ++F)
|
||
|
|
{
|
||
|
|
TArray<FString> Items;
|
||
|
|
ParamsMap.FindRef(TallKeys[F]).ParseIntoArray(Items, TEXT(";"));
|
||
|
|
for (const FString& Item : Items)
|
||
|
|
{
|
||
|
|
FString Path, LevelsText;
|
||
|
|
if (!Item.Split(TEXT("@"), &Path, &LevelsText) || OsmPath.IsEmpty() || !LoadObject<UStaticMesh>(nullptr, *Path))
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("ECHEC : -%s : %s (attendu <objet>@<niveaux>, maillage lisible, et -Osm) ; aucun asset ecrit"), TallKeys[F], *Item));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
TallFamilies[F].Add({ Asset->Variants.Num(), FMath::Max(1, FCString::Atoi(*LevelsText)) });
|
||
|
|
Asset->Variants.Add(TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(Path)));
|
||
|
|
PerVariant.Add(0);
|
||
|
|
const FBox TallBox = LoadObject<UStaticMesh>(nullptr, *Path)->GetBoundingBox();
|
||
|
|
Line(FString::Printf(TEXT("%s : %s, %s niveaux ; boite %.0f x %.0f x %.0f cm"), TallNames[F], *Path, *LevelsText, TallBox.GetSize().X, TallBox.GetSize().Y, TallBox.GetSize().Z));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const bool bTall = !TallFamilies[0].IsEmpty() || !TallFamilies[1].IsEmpty() || !TallFamilies[2].IsEmpty();
|
||
|
|
// 1.7.1 (STM1-DIAG-10 B) : secteur moderne. -ModernLow=<objet>@<niveaux>;... -ModernBox=<sud>,<ouest>,<nord>,<est> (degres) :
|
||
|
|
// un batiment de moins de -TallFromLevels niveaux (3 au moins, niveaux OSM ou defaut), dont le centre est dans la boite et
|
||
|
|
// a moins de -ModernNearTallM (40) d'un immeuble haut de la boite, et sans start_date anterieure a -ModernBeforeYear (1950),
|
||
|
|
// prend le modele bas aux niveaux les plus proches (une entree, plan etire, hauteur niveaux x -LevelHeightCm) au lieu des
|
||
|
|
// modules haussmanniens. Variantes ajoutees apres les immeubles hauts.
|
||
|
|
TArray<FTallModel> ModernModels;
|
||
|
|
{
|
||
|
|
TArray<FString> Items;
|
||
|
|
ParamsMap.FindRef(TEXT("ModernLow")).ParseIntoArray(Items, TEXT(";"));
|
||
|
|
for (const FString& Item : Items)
|
||
|
|
{
|
||
|
|
FString Path, LevelsText;
|
||
|
|
if (!Item.Split(TEXT("@"), &Path, &LevelsText) || OsmPath.IsEmpty() || !LoadObject<UStaticMesh>(nullptr, *Path))
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("ECHEC : -ModernLow : %s (attendu <objet>@<niveaux>, maillage lisible, et -Osm) ; aucun asset ecrit"), *Item));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
ModernModels.Add({ Asset->Variants.Num(), FMath::Max(1, FCString::Atoi(*LevelsText)) });
|
||
|
|
Asset->Variants.Add(TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(Path)));
|
||
|
|
PerVariant.Add(0);
|
||
|
|
const FBox ModernMeshBox = LoadObject<UStaticMesh>(nullptr, *Path)->GetBoundingBox();
|
||
|
|
Line(FString::Printf(TEXT("moderne bas : %s, %s niveaux ; boite %.0f x %.0f x %.0f cm"), *Path, *LevelsText, ModernMeshBox.GetSize().X, ModernMeshBox.GetSize().Y, ModernMeshBox.GetSize().Z));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const bool bModern = !ModernModels.IsEmpty();
|
||
|
|
const double ModernNearCm = Num(TEXT("ModernNearTallM"), 40.0) * 100.0;
|
||
|
|
const int32 ModernBeforeYear = int32(Num(TEXT("ModernBeforeYear"), 1950.0));
|
||
|
|
FBox2D ModernBox(ForceInit);
|
||
|
|
TArray<FVector2D> ModernTallCenters;
|
||
|
|
int32 ModernCount = 0, ModernOld = 0;
|
||
|
|
if (bModern)
|
||
|
|
{
|
||
|
|
TArray<FString> BoxParts;
|
||
|
|
ParamsMap.FindRef(TEXT("ModernBox")).ParseIntoArray(BoxParts, TEXT(","));
|
||
|
|
if (BoxParts.Num() != 4 || !bTall)
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : -ModernLow demande -ModernBox=<sud>,<ouest>,<nord>,<est> et des immeubles hauts (-TallBar, -TallTower ou -TallOffice) ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
const double S = FCString::Atod(*BoxParts[0]), W = FCString::Atod(*BoxParts[1]), N = FCString::Atod(*BoxParts[2]), E = FCString::Atod(*BoxParts[3]);
|
||
|
|
for (const double La : { S, N })
|
||
|
|
{
|
||
|
|
for (const double Lo : { W, E }) { ModernBox += LPCityOsm::Project(La, Lo, Osm.OriginLat, Osm.OriginLon); }
|
||
|
|
}
|
||
|
|
// Immeubles hauts de la boite (meme lecture des niveaux que plus bas).
|
||
|
|
for (const FStreetMapBuilding& Building : StreetMap->GetBuildings())
|
||
|
|
{
|
||
|
|
if (Building.BuildingPoints.Num() < 3) continue;
|
||
|
|
const FVector2D C = BoxCenter(Building.BuildingPoints);
|
||
|
|
FVector2D F;
|
||
|
|
if (!ModernBox.IsInside(C) || !FindNear(OsmCells, C, &F)) continue;
|
||
|
|
const TMap<FString, FString>& T = Osm.WayTags[OsmIndex[F]];
|
||
|
|
const FString LevelsTag = T.FindRef(TEXT("building:levels")), HeightTag = T.FindRef(TEXT("height"));
|
||
|
|
const double RealLevels = !LevelsTag.IsEmpty() && FCString::Atod(*LevelsTag) > 0.0 ? FCString::Atod(*LevelsTag)
|
||
|
|
: (!HeightTag.IsEmpty() && FCString::Atod(*HeightTag) > 0.0 ? FCString::Atod(*HeightTag) * 100.0 / LevelHeightCm : 0.0);
|
||
|
|
if (RealLevels >= double(TallFrom)) ModernTallCenters.Add(C);
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("secteur moderne : boite %.0f..%.0f x %.0f..%.0f cm ; %d immeubles hauts ; rayon %.0f cm ; anterieurs a %d gardes"),
|
||
|
|
ModernBox.Min.X, ModernBox.Max.X, ModernBox.Min.Y, ModernBox.Max.Y, ModernTallCenters.Num(), ModernNearCm, ModernBeforeYear));
|
||
|
|
}
|
||
|
|
// 1.7.6 (STM1-DIAG-15 § 2, R) : -TallSkipMonuments : un monument (historic, heritage ; building palace, public, civic,
|
||
|
|
// government, university, museum ; tourism=museum) n'est jamais un immeuble haut ; -TallMaxBoxRatio=<r> : une emprise
|
||
|
|
// dont la boite orientee depasse r fois l'aire (L, U, courbe) n'est pas etiree. Les deux gardent leurs modules.
|
||
|
|
const bool bTallSkipMonuments = Switches.Contains(TEXT("TallSkipMonuments"));
|
||
|
|
const double TallMaxBoxRatio = Num(TEXT("TallMaxBoxRatio"), 0.0);
|
||
|
|
static const TSet<FString> MonumentKinds = { TEXT("palace"), TEXT("public"), TEXT("civic"), TEXT("government"), TEXT("university"), TEXT("museum") };
|
||
|
|
int32 TallMonuments = 0, TallIrregular = 0;
|
||
|
|
// 1.7.6 (STM1-DIAG-14, F) : -ModernArea=<nom OSM> [-ModernAreaMarginM=30] [-ModernAreaBaseM=8] : les emprises des batiments
|
||
|
|
// de ce nom (la dalle) forment un secteur moderne. Batiment (ni haut, ni eglise) dont le centre est dans une emprise ou a
|
||
|
|
// moins de la marge de son bord : layer >= 1 et 2 niveaux connus au plus -> masque (socle, sous la dalle) ; anterieur a
|
||
|
|
// -ModernBeforeYear -> Haussmann ; sinon modele bas moderne, rehausse de la base si layer >= 1.
|
||
|
|
// Les espaces du nom s'ecrivent '_' (les recettes PROD1 refusent les arguments avec espace) : Dalle_de_Beaugrenelle.
|
||
|
|
const FString ModernAreaName = ParamsMap.FindRef(TEXT("ModernArea")).Replace(TEXT("_"), TEXT(" "));
|
||
|
|
const double ModernAreaMarginCm = Num(TEXT("ModernAreaMarginM"), 30.0) * 100.0;
|
||
|
|
const double ModernAreaBaseCm = Num(TEXT("ModernAreaBaseM"), 8.0) * 100.0;
|
||
|
|
TArray<TArray<FVector2D>> ModernAreas;
|
||
|
|
int32 AreaModern = 0, AreaHidden = 0, AreaOld = 0;
|
||
|
|
if (!ModernAreaName.IsEmpty())
|
||
|
|
{
|
||
|
|
if (!bModern)
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : -ModernArea demande -ModernLow ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
double AreaCm2 = 0.0;
|
||
|
|
for (const FStreetMapBuilding& Building : StreetMap->GetBuildings())
|
||
|
|
{
|
||
|
|
if (Building.BuildingPoints.Num() < 3) continue;
|
||
|
|
FVector2D F;
|
||
|
|
if (!FindNear(OsmCells, BoxCenter(Building.BuildingPoints), &F)) continue;
|
||
|
|
if (Osm.WayTags[OsmIndex[F]].FindRef(TEXT("name")) != ModernAreaName) continue;
|
||
|
|
ModernAreas.Add(Building.BuildingPoints);
|
||
|
|
AreaCm2 += PolygonArea(Building.BuildingPoints);
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("dalle (-ModernArea %s) : %d emprises, %.0f m2 ; marge %.0f cm ; base %.0f cm"), *ModernAreaName, ModernAreas.Num(), AreaCm2 / 1e4, ModernAreaMarginCm, ModernAreaBaseCm));
|
||
|
|
if (ModernAreas.IsEmpty())
|
||
|
|
{
|
||
|
|
Line(TEXT("ECHEC : -ModernArea : aucune emprise de ce nom ; aucun asset ecrit"));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
auto InModernArea = [&](const FVector2D& C)
|
||
|
|
{
|
||
|
|
for (const TArray<FVector2D>& A : ModernAreas)
|
||
|
|
{
|
||
|
|
if (InsidePolygon(A, C)) return true;
|
||
|
|
for (int32 I = 0; I < A.Num(); ++I)
|
||
|
|
{
|
||
|
|
const FVector2D P = A[I], D = A[(I + 1) % A.Num()] - A[I];
|
||
|
|
const double T = FMath::Clamp(FVector2D::DotProduct(C - P, D) / FMath::Max(D.SizeSquared(), 1e-6), 0.0, 1.0);
|
||
|
|
if (FVector2D::DistSquared(C, P + D * T) <= ModernAreaMarginCm * ModernAreaMarginCm) return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
auto PickModern = [&](int32 Levels, const FVector2D& C)
|
||
|
|
{
|
||
|
|
const uint32 ModernHash = HashCombine(GetTypeHash(FMath::RoundToInt32(C.X)), GetTypeHash(FMath::RoundToInt32(C.Y)));
|
||
|
|
int32 Best = 0;
|
||
|
|
double BestGap = TNumericLimits<double>::Max();
|
||
|
|
for (int32 M = 0; M < ModernModels.Num(); ++M)
|
||
|
|
{
|
||
|
|
const double Gap = FMath::Abs(ModernModels[M].Levels - Levels) + 0.001 * double((ModernHash >> (M % 16)) & 7u);
|
||
|
|
if (Gap < BestGap) { BestGap = Gap; Best = M; }
|
||
|
|
}
|
||
|
|
return Best;
|
||
|
|
};
|
||
|
|
auto NearModernTall = [&](const FVector2D& C)
|
||
|
|
{
|
||
|
|
for (const FVector2D& T : ModernTallCenters) { if (FVector2D::DistSquared(C, T) < ModernNearCm * ModernNearCm) return true; }
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
auto IsChurch = [](const TMap<FString, FString>* T)
|
||
|
|
{
|
||
|
|
if (!T) return false;
|
||
|
|
const FString Kind = T->FindRef(TEXT("building"));
|
||
|
|
return Kind == TEXT("church") || Kind == TEXT("chapel") || Kind == TEXT("cathedral") || Kind == TEXT("basilica")
|
||
|
|
|| (T->FindRef(TEXT("amenity")) == TEXT("place_of_worship") && T->FindRef(TEXT("religion")) == TEXT("christian"));
|
||
|
|
};
|
||
|
|
auto AddEntry = [&](const FVector2D& Center, double Yaw, const FVector& Size, uint8 V)
|
||
|
|
{
|
||
|
|
Asset->Centers.Add(Center);
|
||
|
|
Asset->YawDeg.Add(float(FMath::UnwindDegrees(Yaw)));
|
||
|
|
Asset->SizeCm.Add(Size);
|
||
|
|
Asset->Variant.Add(V);
|
||
|
|
++PerVariant[V];
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const FStreetMapBuilding& Building : StreetMap->GetBuildings())
|
||
|
|
{
|
||
|
|
const TArray<FVector2D>& Pts = Building.BuildingPoints;
|
||
|
|
if (Pts.Num() < 3) { ++Skipped; continue; }
|
||
|
|
const FVector2D Center = BoxCenter(Pts);
|
||
|
|
if (FindNear(Excluded, Center, nullptr)) { ++Excl; continue; }
|
||
|
|
|
||
|
|
// Balises OSM du batiment (voie de meme centre de boite).
|
||
|
|
const TMap<FString, FString>* Tags = nullptr;
|
||
|
|
FVector2D Found;
|
||
|
|
if (!OsmPath.IsEmpty() && FindNear(OsmCells, Center, &Found))
|
||
|
|
{
|
||
|
|
Tags = &Osm.WayTags[OsmIndex[Found]];
|
||
|
|
++Matched;
|
||
|
|
}
|
||
|
|
auto Tag = [Tags](const TCHAR* Key) { return Tags ? Tags->FindRef(Key) : FString(); };
|
||
|
|
const double Area = PolygonArea(Pts);
|
||
|
|
if (!OsmPath.IsEmpty())
|
||
|
|
{
|
||
|
|
if (Tag(TEXT("location")) == TEXT("underground")) { ++OutUnderground; continue; }
|
||
|
|
if (!Tag(TEXT("layer")).IsEmpty() && FCString::Atoi(*Tag(TEXT("layer"))) < 0) { ++OutLayer; continue; }
|
||
|
|
if (Tag(TEXT("tunnel")) == TEXT("yes")) { ++OutTunnel; continue; }
|
||
|
|
if (Tag(TEXT("building")) == TEXT("roof")) { ++OutRoof; continue; }
|
||
|
|
if (Area < MinAreaCm2) { ++OutArea; continue; }
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1.3.2 (COMMUN-27b) : musee ou batiment historique dans un parc (Orangerie, Jeu de Paume) : masque, pas d'immeuble.
|
||
|
|
if (bHaussmann && LPCityOsm::IsMuseumOrHistoric(Tags) && InAnyPark(Center)) { ++ParkMonuments; continue; }
|
||
|
|
|
||
|
|
// Etages : OSM reel, sinon height, sinon defaut ; petits types et petites emprises sans etages : 1.
|
||
|
|
int32 Levels = DefaultLevels;
|
||
|
|
bool bLevelsKnown = false;
|
||
|
|
if (OsmPath.IsEmpty())
|
||
|
|
{
|
||
|
|
if (Building.BuildingLevels > 0) { Levels = Building.BuildingLevels; ++FromLevels; bLevelsKnown = true; }
|
||
|
|
else if (Building.Height > 0.0) { Levels = FMath::Max(1, FMath::RoundToInt32(Building.Height / LevelHeightCm)); ++FromHeight; bLevelsKnown = true; }
|
||
|
|
else { ++FromDefault; }
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const FString LevelsTag = Tag(TEXT("building:levels")), HeightTag = Tag(TEXT("height"));
|
||
|
|
if (!LevelsTag.IsEmpty() && FCString::Atod(*LevelsTag) > 0.0) { Levels = FMath::Clamp(FMath::RoundToInt32(FCString::Atod(*LevelsTag)), 1, 15); ++FromLevels; bLevelsKnown = true; }
|
||
|
|
else if (!HeightTag.IsEmpty() && FCString::Atod(*HeightTag) > 0.0) { Levels = FMath::Clamp(FMath::RoundToInt32(FCString::Atod(*HeightTag) * 100.0 / LevelHeightCm), 1, 15); ++FromHeight; bLevelsKnown = true; }
|
||
|
|
else if (SmallTypes.Contains(Tag(TEXT("building"))) || Area < SmallAreaCm2) { Levels = 1; ++SmallOne; }
|
||
|
|
else { ++FromDefault; }
|
||
|
|
}
|
||
|
|
++Buildings;
|
||
|
|
|
||
|
|
// Boite orientee sur l'arete la plus longue.
|
||
|
|
double BestLen = -1.0;
|
||
|
|
FVector2D Dir(1.0, 0.0);
|
||
|
|
for (int32 I = 0; I < Pts.Num(); ++I)
|
||
|
|
{
|
||
|
|
const FVector2D E = Pts[(I + 1) % Pts.Num()] - Pts[I];
|
||
|
|
const double Len = E.Size();
|
||
|
|
if (Len > BestLen) { BestLen = Len; Dir = E / FMath::Max(Len, 1e-6); }
|
||
|
|
}
|
||
|
|
const FVector2D Perp(-Dir.Y, Dir.X);
|
||
|
|
double MinA = TNumericLimits<double>::Max(), MaxA = TNumericLimits<double>::Lowest(), MinB = MinA, MaxB = MaxA;
|
||
|
|
for (const FVector2D& P : Pts)
|
||
|
|
{
|
||
|
|
const double A = FVector2D::DotProduct(P - Center, Dir), B = FVector2D::DotProduct(P - Center, Perp);
|
||
|
|
MinA = FMath::Min(MinA, A); MaxA = FMath::Max(MaxA, A); MinB = FMath::Min(MinB, B); MaxB = FMath::Max(MaxB, B);
|
||
|
|
}
|
||
|
|
const FVector2D OrientedCenter = Center + Dir * (0.5 * (MinA + MaxA)) + Perp * (0.5 * (MinB + MaxB));
|
||
|
|
const double LenA = FMath::Max(MaxA - MinA, MinSideCm), LenB = FMath::Max(MaxB - MinB, MinSideCm);
|
||
|
|
|
||
|
|
// 1.6.0 (E1) : eglise generique.
|
||
|
|
// 1.7.0 : immeuble haut (avant les eglises : une eglise n'est jamais un immeuble haut, voir IsChurch plus bas).
|
||
|
|
if (bTall && !IsChurch(Tags))
|
||
|
|
{
|
||
|
|
const FString LevelsTag = Tag(TEXT("building:levels")), HeightTag = Tag(TEXT("height"));
|
||
|
|
const double RealLevels = !LevelsTag.IsEmpty() && FCString::Atod(*LevelsTag) > 0.0 ? FCString::Atod(*LevelsTag)
|
||
|
|
: (!HeightTag.IsEmpty() && FCString::Atod(*HeightTag) > 0.0 ? FCString::Atod(*HeightTag) * 100.0 / LevelHeightCm : 0.0);
|
||
|
|
const FString Kind0 = Tag(TEXT("building"));
|
||
|
|
const bool bTallMonument = RealLevels >= double(TallFrom) && bTallSkipMonuments
|
||
|
|
&& (!Tag(TEXT("historic")).IsEmpty() || !Tag(TEXT("heritage")).IsEmpty() || MonumentKinds.Contains(Kind0) || Tag(TEXT("tourism")) == TEXT("museum"));
|
||
|
|
// Le secteur moderne (-ModernBox) garde ses tours, meme d'emprise irreguliere (Front de Seine : tour Totem, etc.).
|
||
|
|
const bool bTallIrregular = RealLevels >= double(TallFrom) && !bTallMonument && TallMaxBoxRatio > 0.0 && LenA * LenB > TallMaxBoxRatio * Area
|
||
|
|
&& !(bModern && ModernBox.IsInside(Center));
|
||
|
|
TallMonuments += bTallMonument ? 1 : 0;
|
||
|
|
TallIrregular += bTallIrregular ? 1 : 0;
|
||
|
|
if (RealLevels >= double(TallFrom) && !bTallMonument && !bTallIrregular)
|
||
|
|
{
|
||
|
|
const FString Kind = Tag(TEXT("building"));
|
||
|
|
const bool bOffice = Kind == TEXT("office") || Kind == TEXT("commercial") || !Tag(TEXT("office")).IsEmpty() || Tag(TEXT("building:use")) == TEXT("office");
|
||
|
|
int32 F = bOffice ? 2 : (RealLevels >= double(TallTower) ? 1 : 0);
|
||
|
|
if (TallFamilies[F].IsEmpty()) { F = !TallFamilies[1].IsEmpty() ? 1 : (!TallFamilies[0].IsEmpty() ? 0 : 2); ++TallFallback; }
|
||
|
|
const uint32 TallHash = HashCombine(GetTypeHash(FMath::RoundToInt32(Center.X)), GetTypeHash(FMath::RoundToInt32(Center.Y)));
|
||
|
|
int32 Best = -1;
|
||
|
|
double BestGap = TNumericLimits<double>::Max();
|
||
|
|
for (int32 M = 0; M < TallFamilies[F].Num(); ++M)
|
||
|
|
{
|
||
|
|
// Ecart de niveaux, plus un tie-break deterministe par la position.
|
||
|
|
const double Gap = FMath::Abs(TallFamilies[F][M].Levels - RealLevels) + 0.001 * double((TallHash >> (M % 16)) & 7u);
|
||
|
|
if (Gap < BestGap) { BestGap = Gap; Best = M; }
|
||
|
|
}
|
||
|
|
double L = LenA, W = LenB, Yaw = AngleDeg(Dir);
|
||
|
|
if (W > L) { Swap(L, W); Yaw += 90.0; }
|
||
|
|
const double H = FMath::Min(RealLevels, double(TallMax)) * LevelHeightCm;
|
||
|
|
AddEntry(OrientedCenter, Yaw, FVector(L, W, H), uint8(TallFamilies[F][Best].Variant));
|
||
|
|
++TallCount[F];
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 1.7.6 : dalle (voir -ModernArea plus haut).
|
||
|
|
if (!ModernAreas.IsEmpty() && !IsChurch(Tags) && InModernArea(Center))
|
||
|
|
{
|
||
|
|
const int32 Layer = Tag(TEXT("layer")).IsEmpty() ? 0 : FCString::Atoi(*Tag(TEXT("layer")));
|
||
|
|
const FString Date = Tag(TEXT("start_date"));
|
||
|
|
const int32 Year = Date.Len() >= 4 ? FCString::Atoi(*Date.Left(4)) : 0;
|
||
|
|
if (Layer >= 1 && bLevelsKnown && Levels <= 2) { ++AreaHidden; continue; }
|
||
|
|
if (Year > 0 && Year < ModernBeforeYear) { ++AreaOld; }
|
||
|
|
else
|
||
|
|
{
|
||
|
|
double L = LenA, W = LenB, Yaw = AngleDeg(Dir);
|
||
|
|
if (W > L) { Swap(L, W); Yaw += 90.0; }
|
||
|
|
const double H = double(Levels) * LevelHeightCm + (Layer >= 1 ? ModernAreaBaseCm : 0.0);
|
||
|
|
AddEntry(OrientedCenter, Yaw, FVector(L, W, H), uint8(ModernModels[PickModern(Levels, Center)].Variant));
|
||
|
|
++AreaModern;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 1.7.1 : secteur moderne (voir -ModernLow plus haut).
|
||
|
|
if (bModern && Levels >= 3 && !IsChurch(Tags) && ModernBox.IsInside(Center) && NearModernTall(Center))
|
||
|
|
{
|
||
|
|
const FString Date = Tag(TEXT("start_date"));
|
||
|
|
const int32 Year = Date.Len() >= 4 ? FCString::Atoi(*Date.Left(4)) : 0;
|
||
|
|
if (Year > 0 && Year < ModernBeforeYear) { ++ModernOld; }
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const uint32 ModernHash = HashCombine(GetTypeHash(FMath::RoundToInt32(Center.X)), GetTypeHash(FMath::RoundToInt32(Center.Y)));
|
||
|
|
int32 Best = 0;
|
||
|
|
double BestGap = TNumericLimits<double>::Max();
|
||
|
|
for (int32 M = 0; M < ModernModels.Num(); ++M)
|
||
|
|
{
|
||
|
|
const double Gap = FMath::Abs(ModernModels[M].Levels - Levels) + 0.001 * double((ModernHash >> (M % 16)) & 7u);
|
||
|
|
if (Gap < BestGap) { BestGap = Gap; Best = M; }
|
||
|
|
}
|
||
|
|
double L = LenA, W = LenB, Yaw = AngleDeg(Dir);
|
||
|
|
if (W > L) { Swap(L, W); Yaw += 90.0; }
|
||
|
|
AddEntry(OrientedCenter, Yaw, FVector(L, W, double(Levels) * LevelHeightCm), uint8(ModernModels[Best].Variant));
|
||
|
|
++ModernCount;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const bool bChurch = !ChurchBoxes.IsEmpty() && IsChurch(Tags);
|
||
|
|
const bool bChurchTooLong = bChurch && ChurchMaxRatio > 0.0 && FMath::Max(LenA, LenB) / FMath::Max(FMath::Min(LenA, LenB), 1.0) > ChurchMaxRatio;
|
||
|
|
ChurchesTooLong += bChurchTooLong ? 1 : 0;
|
||
|
|
if (bChurch && !bChurchTooLong)
|
||
|
|
{
|
||
|
|
double L = LenA, W = LenB, Yaw = AngleDeg(Dir);
|
||
|
|
if (W > L) { Swap(L, W); Yaw += 90.0; }
|
||
|
|
const int32 Compact = ChurchBoxes.Num() - 1;
|
||
|
|
const uint32 ChurchHash = HashCombine(GetTypeHash(FMath::RoundToInt32(Center.X)), GetTypeHash(FMath::RoundToInt32(Center.Y)));
|
||
|
|
const int32 C = L / FMath::Max(W, 1.0) >= ChurchRatio ? int32(ChurchHash % uint32(Compact)) : Compact;
|
||
|
|
const FVector Box = ChurchBoxes[C].GetSize().ComponentMax(FVector(1.0));
|
||
|
|
if (ChurchStretch > 0.0 && L > W * (Box.X / Box.Y) * ChurchStretch)
|
||
|
|
{
|
||
|
|
L = W * (Box.X / Box.Y) * ChurchStretch;
|
||
|
|
++ChurchesShortened;
|
||
|
|
}
|
||
|
|
AddEntry(OrientedCenter, Yaw, FVector(L, W, FMath::Clamp(L * Box.Z / Box.X, ChurchHMin, ChurchHMax)), uint8(ChurchBase + C));
|
||
|
|
++Churches[C == Compact ? 1 : 0];
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1.3.4 (COMMUN-29, dalles rouges ; regle approuvee par l'utilisateur) : Petit (maison du jeu de base) seulement pour les
|
||
|
|
// petits types (kiosk, shed, hut, toilets...) ; hors de ces types, emprise sous -TinyMaskM2 (30) : masquee ; emprise sous
|
||
|
|
// -PetitMaxAreaM2 (100) : un seul module Etroit a ses etages (sous 3 etages ou inconnus : 2 ; pavillon de parc : 1).
|
||
|
|
const bool bInPark = bHaussmann && !bLevelsKnown && Area < ParkPetitMaxAreaCm2 && InAnyPark(Center);
|
||
|
|
const bool bSmallType = SmallTypes.Contains(Tag(TEXT("building")));
|
||
|
|
if (bHaussmann && !bSmallType && Area < TinyMaskCm2) { ++TinyMasked; continue; }
|
||
|
|
const bool bNarrowTall = bHaussmann && !bSmallType && Area < PetitMaxAreaCm2;
|
||
|
|
const bool bSmall = bSmallType;
|
||
|
|
if (bInPark) { Levels = 1; ++ParkPavilions; }
|
||
|
|
if (bNarrowTall)
|
||
|
|
{
|
||
|
|
if (!bLevelsKnown && !bInPark) Levels = 2;
|
||
|
|
++NarrowTall;
|
||
|
|
}
|
||
|
|
if (!bHaussmann || (bSmall && VariantPaths.Num() == 7))
|
||
|
|
{
|
||
|
|
// Une maison par batiment (G4-b), ou variante Petit du mode Haussmann.
|
||
|
|
const uint32 Hash = HashCombine(GetTypeHash(FMath::RoundToInt32(Center.X)), GetTypeHash(FMath::RoundToInt32(Center.Y)));
|
||
|
|
const uint8 V = bHaussmann ? uint8(Petit) : uint8(Hash % uint32(VariantPaths.Num()));
|
||
|
|
// 1.3.0 : la variante Petit (maison du jeu de base) est plafonnee a 1 etage.
|
||
|
|
AddEntry(OrientedCenter, AngleDeg(Dir), FVector(LenA, LenB, (bHaussmann ? 1 : Levels) * LevelHeightCm), V);
|
||
|
|
SmallBuildings += bHaussmann ? 1 : 0;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mode Haussmann : cotes de la boite orientee (normale exterieure, milieu, longueur).
|
||
|
|
struct FSide { FVector2D Normal; FVector2D Mid; double Length; double Street; };
|
||
|
|
FSide Sides[4] = {
|
||
|
|
{ -Perp, OrientedCenter - Perp * (0.5 * LenB), LenA, 0.0 },
|
||
|
|
{ Dir, OrientedCenter + Dir * (0.5 * LenA), LenB, 0.0 },
|
||
|
|
{ Perp, OrientedCenter + Perp * (0.5 * LenB), LenA, 0.0 },
|
||
|
|
{ -Dir, OrientedCenter - Dir * (0.5 * LenA), LenB, 0.0 } };
|
||
|
|
int32 Front = 0;
|
||
|
|
for (int32 S = 0; S < 4; ++S)
|
||
|
|
{
|
||
|
|
Sides[S].Street = DistanceToRoad(Sides[S].Mid);
|
||
|
|
if (Sides[S].Street < Sides[Front].Street || (Sides[S].Street == Sides[Front].Street && Sides[S].Length > Sides[Front].Length)) Front = S;
|
||
|
|
}
|
||
|
|
if (Sides[Front].Street > StreetCm)
|
||
|
|
{
|
||
|
|
Front = LenA >= LenB ? 0 : 1;
|
||
|
|
++NoStreet;
|
||
|
|
}
|
||
|
|
auto LevelsHeight = [&](uint8 V) { return VariantBoxes[V].GetSize().Z * double(Levels) / double(VariantLevels[V]); };
|
||
|
|
auto YawFor = [&](const FVector2D& WorldNormal) { return AngleDeg(WorldNormal) - AngleDeg(MeshFront); };
|
||
|
|
const uint32 Hash = HashCombine(GetTypeHash(FMath::RoundToInt32(Center.X)), GetTypeHash(FMath::RoundToInt32(Center.Y)));
|
||
|
|
|
||
|
|
// Angle : la facade et un cote voisin a moins de CornerStreetCm d'une voie ; les deux cotes d'au plus CornerMaxCm.
|
||
|
|
const int32 Left = (Front + 3) % 4, Right = (Front + 1) % 4;
|
||
|
|
const int32 Other = Sides[Left].Street <= Sides[Right].Street ? Left : Right;
|
||
|
|
if (!bNarrowTall && Sides[Other].Street <= CornerStreetCm && Sides[Front].Street <= CornerStreetCm && Sides[Front].Length <= CornerMaxCm && Sides[Other].Length <= CornerMaxCm)
|
||
|
|
{
|
||
|
|
const uint8 V = (Levels >= 7 || (Hash % 4u) == 0u) ? uint8(AngleCoupole) : uint8(AngleCoupe);
|
||
|
|
// Facade du maillage sur l'un des deux cotes, le cote du coin coupe sur l'autre ; un des deux sens convient toujours.
|
||
|
|
int32 F = Front, G = Other;
|
||
|
|
double Yaw = YawFor(Sides[F].Normal);
|
||
|
|
if (FVector2D::DotProduct(MeshCorner.GetRotated(Yaw), Sides[G].Normal) < 0.5)
|
||
|
|
{
|
||
|
|
Swap(F, G);
|
||
|
|
Yaw = YawFor(Sides[F].Normal);
|
||
|
|
}
|
||
|
|
// Largeur du maillage (le long de la facade) = longueur du cote F ; profondeur = longueur du cote G.
|
||
|
|
const bool bFrontAlongX = FMath::Abs(MeshFront.Y) > 0.5;
|
||
|
|
const FVector Size = bFrontAlongX ? FVector(Sides[F].Length, Sides[G].Length, LevelsHeight(V)) : FVector(Sides[G].Length, Sides[F].Length, LevelsHeight(V));
|
||
|
|
AddEntry(OrientedCenter, Yaw, Size, V);
|
||
|
|
++Corners;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decoupe en modules : facade (cote Front), profondeur vers l'interieur.
|
||
|
|
const FVector2D U = FVector2D(-Sides[Front].Normal.Y, Sides[Front].Normal.X);
|
||
|
|
const double F = Sides[Front].Length;
|
||
|
|
const double D = (Front % 2 == 0) ? LenB : LenA;
|
||
|
|
const int32 NU = bNarrowTall ? 1 : FMath::Max(1, FMath::RoundToInt32(F / ModuleCm));
|
||
|
|
const int32 NV = bNarrowTall ? 1 : FMath::Max(1, FMath::RoundToInt32(D / DepthModuleCm));
|
||
|
|
const bool bShop = bRealShops && IsShopAmenity(Building.Amenity);
|
||
|
|
ShopBuildings += bShop ? 1 : 0;
|
||
|
|
const double W = F / NU, DD = D / NV;
|
||
|
|
const double Yaw = YawFor(Sides[Front].Normal);
|
||
|
|
const bool bFrontAlongX = FMath::Abs(MeshFront.Y) > 0.5;
|
||
|
|
int32 Added = 0;
|
||
|
|
for (int32 J = 0; J < NV; ++J)
|
||
|
|
{
|
||
|
|
for (int32 I = 0; I < NU; ++I)
|
||
|
|
{
|
||
|
|
const FVector2D C = OrientedCenter + U * (-0.5 * F + (I + 0.5) * W) + Sides[Front].Normal * (0.5 * D - (J + 0.5) * DD);
|
||
|
|
if (NU * NV > 1 && !InsidePolygon(Pts, C)) { ++CellsOutside; continue; }
|
||
|
|
const uint32 CellHash = HashCombine(Hash, GetTypeHash(I * 131 + J));
|
||
|
|
uint8 V = uint8(Large);
|
||
|
|
if (bNarrowTall || W < 1000.0) V = uint8(Etroit);
|
||
|
|
else if (bRealShops)
|
||
|
|
{
|
||
|
|
// Vrais commerces : tout le rang sur rue d'un batiment commercant ; ailleurs, plus de Commerces tires au hasard.
|
||
|
|
V = (bShop && J == 0) ? uint8(Commerces) : (CellHash % 3u) == 0u ? uint8(PorteCochere) : uint8(Large);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const uint32 Pick = CellHash % 4u;
|
||
|
|
V = Pick == 0u ? uint8(PorteCochere) : (Pick == 1u && J == 0) ? uint8(Commerces) : uint8(Large);
|
||
|
|
}
|
||
|
|
AddEntry(C, Yaw, bFrontAlongX ? FVector(W, DD, LevelsHeight(V)) : FVector(DD, W, LevelsHeight(V)), V);
|
||
|
|
++Added;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (Added == 0)
|
||
|
|
{
|
||
|
|
AddEntry(OrientedCenter, Yaw, bFrontAlongX ? FVector(F, D, LevelsHeight(Large)) : FVector(D, F, LevelsHeight(Large)), uint8(Large));
|
||
|
|
}
|
||
|
|
++Tiled;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1.4.3 (STM1-SPEC-19 A1) : arbres d'alignement, variantes ajoutees apres celles des maisons (-Trees). Sur l'axe des
|
||
|
|
// trottoirs OSM (footway=sidewalk, hors area=yes) a moins de -TreeNearRoadM d'une voie de l'UStreetMap nommee Boulevard,
|
||
|
|
// Avenue, Quai, Cours ou Allee, tous les -TreePitchM ; ecartes sur une chaussee (largeurs du plugin), dans un batiment de
|
||
|
|
// l'UStreetMap ou sur la pierre (memes regles que LPBuildCitySidewalks). Variante, lacet et hauteur (-TreeHeightM=min,max)
|
||
|
|
// tires de facon deterministe par la position ; proportions du maillage gardees.
|
||
|
|
int32 Trees = 0, TreeOnRoad = 0, TreeInBuilding = 0, TreeOnStone = 0, TreeFar = 0;
|
||
|
|
if (!TreePaths.IsEmpty())
|
||
|
|
{
|
||
|
|
const int32 TreeBase = Asset->Variants.Num();
|
||
|
|
TArray<FBox> TreeBoxes;
|
||
|
|
for (const FString& Path : TreePaths)
|
||
|
|
{
|
||
|
|
const UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, *Path);
|
||
|
|
if (!Mesh || TreeBase + TreeBoxes.Num() > 255)
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("ECHEC : arbre %s introuvable (ou plus de 256 variantes) ; aucun asset ecrit"), *Path));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
TreeBoxes.Add(Mesh->GetBoundingBox());
|
||
|
|
Asset->Variants.Add(TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(Path)));
|
||
|
|
PerVariant.Add(0);
|
||
|
|
Line(FString::Printf(TEXT("arbre %s : boite %.0f x %.0f x %.0f cm"), *Path, TreeBoxes.Last().GetSize().X, TreeBoxes.Last().GetSize().Y, TreeBoxes.Last().GetSize().Z));
|
||
|
|
}
|
||
|
|
LPCityOsm::FReader Lines;
|
||
|
|
Lines.OriginLat = Osm.OriginLat;
|
||
|
|
Lines.OriginLon = Osm.OriginLon;
|
||
|
|
Lines.bLines = true;
|
||
|
|
Lines.bAreas = true;
|
||
|
|
FString LinesError;
|
||
|
|
if (OsmPath.IsEmpty() || !LPCityOsm::Read(Lines, OsmPath, LinesError))
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("ECHEC : -Trees demande -Osm lisible (%s) ; aucun asset ecrit"), *LinesError));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
Lines.Nodes.Empty();
|
||
|
|
constexpr double TreeCellCm = 2000.0;
|
||
|
|
auto TCell = [](double X, double Y) { return FIntPoint(FMath::FloorToInt32(X / TreeCellCm), FMath::FloorToInt32(Y / TreeCellCm)); };
|
||
|
|
struct FStrip { FVector2D A, B; double Half; bool bExtend; };
|
||
|
|
auto AddStripTo = [&](TArray<FStrip>& Strips, TMultiMap<FIntPoint, int32>& Cells, const FStrip& S)
|
||
|
|
{
|
||
|
|
const int32 I = Strips.Add(S);
|
||
|
|
const double M = S.Half * 2.0;
|
||
|
|
const FIntPoint Mn = TCell(FMath::Min(S.A.X, S.B.X) - M, FMath::Min(S.A.Y, S.B.Y) - M), Mx = TCell(FMath::Max(S.A.X, S.B.X) + M, FMath::Max(S.A.Y, S.B.Y) + M);
|
||
|
|
for (int32 X = Mn.X; X <= Mx.X; ++X) for (int32 Y = Mn.Y; Y <= Mx.Y; ++Y) Cells.Add(FIntPoint(X, Y), I);
|
||
|
|
};
|
||
|
|
auto InStrip = [](const FStrip& S, const FVector2D& P)
|
||
|
|
{
|
||
|
|
const FVector2D D = S.B - S.A;
|
||
|
|
const double Len = D.Size();
|
||
|
|
if (Len < 1.0) return false;
|
||
|
|
const FVector2D U = D / Len;
|
||
|
|
const double Along = FVector2D::DotProduct(P - S.A, U), Across = FMath::Abs(FVector2D::DotProduct(P - S.A, FVector2D(-U.Y, U.X)));
|
||
|
|
return Along >= (S.bExtend ? -S.Half : 0.0) && Along <= (S.bExtend ? Len + S.Half : Len) && Across <= S.Half;
|
||
|
|
};
|
||
|
|
auto AnyStrip = [&](const TArray<FStrip>& Strips, const TMultiMap<FIntPoint, int32>& Cells, const FVector2D& P)
|
||
|
|
{
|
||
|
|
for (auto It = Cells.CreateConstKeyIterator(TCell(P.X, P.Y)); It; ++It) if (InStrip(Strips[It.Value()], P)) return true;
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
TArray<FStrip> RoadStrips, NamedStrips, StoneStrips;
|
||
|
|
TMultiMap<FIntPoint, int32> RoadCells, NamedCells, StoneCells;
|
||
|
|
const double NearCm = Num(TEXT("TreeNearRoadM"), 15.0) * 100.0;
|
||
|
|
for (const FStreetMapRoad& Road : StreetMap->GetRoads())
|
||
|
|
{
|
||
|
|
const double Half = (Road.RoadType == EStreetMapRoadType::Highway ? 1400.0 : Road.RoadType == EStreetMapRoadType::MajorRoad ? 1000.0 : 800.0) * 0.5;
|
||
|
|
const FString& Name = Road.RoadName;
|
||
|
|
const bool bNamed = Name.StartsWith(TEXT("Boulevard "), ESearchCase::IgnoreCase) || Name.StartsWith(TEXT("Avenue "), ESearchCase::IgnoreCase)
|
||
|
|
|| Name.StartsWith(TEXT("Quai "), ESearchCase::IgnoreCase) || Name.StartsWith(TEXT("Cours "), ESearchCase::IgnoreCase)
|
||
|
|
|| Name.StartsWith(TEXT("All\u00e9e "), ESearchCase::IgnoreCase) || Name.StartsWith(TEXT("Allee "), ESearchCase::IgnoreCase);
|
||
|
|
for (int32 I = 0; I + 1 < Road.RoadPoints.Num(); ++I)
|
||
|
|
{
|
||
|
|
AddStripTo(RoadStrips, RoadCells, { Road.RoadPoints[I], Road.RoadPoints[I + 1], Half, false });
|
||
|
|
if (bNamed) AddStripTo(NamedStrips, NamedCells, { Road.RoadPoints[I], Road.RoadPoints[I + 1], NearCm, true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
auto AddPoly = [&](TArray<const TArray<FVector2D>*>& Polys, TArray<FBox2D>& Boxes, TMultiMap<FIntPoint, int32>& Cells, const TArray<FVector2D>& Pts)
|
||
|
|
{
|
||
|
|
const int32 I = Polys.Add(&Pts);
|
||
|
|
Boxes.Add(FBox2D(Pts));
|
||
|
|
const FIntPoint Mn = TCell(Boxes[I].Min.X, Boxes[I].Min.Y), Mx = TCell(Boxes[I].Max.X, Boxes[I].Max.Y);
|
||
|
|
for (int32 X = Mn.X; X <= Mx.X; ++X) for (int32 Y = Mn.Y; Y <= Mx.Y; ++Y) Cells.Add(FIntPoint(X, Y), I);
|
||
|
|
};
|
||
|
|
auto AnyPoly = [&](const TArray<const TArray<FVector2D>*>& Polys, const TArray<FBox2D>& Boxes, const TMultiMap<FIntPoint, int32>& Cells, const FVector2D& P)
|
||
|
|
{
|
||
|
|
for (auto It = Cells.CreateConstKeyIterator(TCell(P.X, P.Y)); It; ++It) if (Boxes[It.Value()].IsInside(P) && InsidePolygon(*Polys[It.Value()], P)) return true;
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
TArray<const TArray<FVector2D>*> BuildingPolys;
|
||
|
|
TArray<FBox2D> BuildingBoxes;
|
||
|
|
TMultiMap<FIntPoint, int32> BuildingCells;
|
||
|
|
for (const FStreetMapBuilding& Building : StreetMap->GetBuildings())
|
||
|
|
{
|
||
|
|
if (Building.BuildingPoints.Num() >= 3) AddPoly(BuildingPolys, BuildingBoxes, BuildingCells, Building.BuildingPoints);
|
||
|
|
}
|
||
|
|
TArray<TArray<FVector2D>> StoneAreas;
|
||
|
|
TArray<int32> Sidewalks;
|
||
|
|
for (int32 L = 0; L < Lines.Lines.Num(); ++L)
|
||
|
|
{
|
||
|
|
const TMap<FString, FString>& Tags = Lines.LineTags[L];
|
||
|
|
const TArray<FVector2D>& Pts = Lines.Lines[L];
|
||
|
|
const FString Highway = Tags.FindRef(TEXT("highway"));
|
||
|
|
const bool bAreaYes = Tags.FindRef(TEXT("area")) == TEXT("yes");
|
||
|
|
if (((Highway == TEXT("pedestrian") || Highway == TEXT("footway")) && bAreaYes) || Tags.FindRef(TEXT("place")) == TEXT("square") || Tags.Contains(TEXT("area:highway")))
|
||
|
|
{
|
||
|
|
if (Pts.Num() >= 4 && Pts[0].Equals(Pts.Last(), KINDA_SMALL_NUMBER)) StoneAreas.Add(TArray<FVector2D>(Pts.GetData(), Pts.Num() - 1));
|
||
|
|
}
|
||
|
|
else if (Highway == TEXT("pedestrian"))
|
||
|
|
{
|
||
|
|
for (int32 P = 0; P + 1 < Pts.Num(); ++P) AddStripTo(StoneStrips, StoneCells, { Pts[P], Pts[P + 1], 300.0, true });
|
||
|
|
}
|
||
|
|
else if (Highway == TEXT("footway") && Tags.FindRef(TEXT("footway")) == TEXT("sidewalk"))
|
||
|
|
{
|
||
|
|
Sidewalks.Add(L);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
TArray<const TArray<FVector2D>*> StonePolys;
|
||
|
|
TArray<FBox2D> StoneBoxes;
|
||
|
|
TMultiMap<FIntPoint, int32> StonePolyCells;
|
||
|
|
for (const TArray<FVector2D>& A : StoneAreas) AddPoly(StonePolys, StoneBoxes, StonePolyCells, A);
|
||
|
|
|
||
|
|
const double PitchCm = FMath::Max(200.0, Num(TEXT("TreePitchM"), 10.0) * 100.0);
|
||
|
|
double HMin = 900.0, HMax = 1300.0;
|
||
|
|
{
|
||
|
|
TArray<FString> HText;
|
||
|
|
ParamsMap.FindRef(TEXT("TreeHeightM")).ParseIntoArray(HText, TEXT(","));
|
||
|
|
if (HText.Num() == 2) { HMin = FCString::Atod(*HText[0]) * 100.0; HMax = FCString::Atod(*HText[1]) * 100.0; }
|
||
|
|
}
|
||
|
|
for (const int32 L : Sidewalks)
|
||
|
|
{
|
||
|
|
const TArray<FVector2D>& Pts = Lines.Lines[L];
|
||
|
|
double Next = PitchCm * 0.5, Walked = 0.0;
|
||
|
|
for (int32 P = 0; P + 1 < Pts.Num(); ++P)
|
||
|
|
{
|
||
|
|
const double Len = FVector2D::Distance(Pts[P], Pts[P + 1]);
|
||
|
|
while (Len > 0.0 && Next <= Walked + Len)
|
||
|
|
{
|
||
|
|
const FVector2D C = Pts[P] + (Pts[P + 1] - Pts[P]) * ((Next - Walked) / Len);
|
||
|
|
Next += PitchCm;
|
||
|
|
if (!AnyStrip(NamedStrips, NamedCells, C)) { ++TreeFar; continue; }
|
||
|
|
if (AnyStrip(RoadStrips, RoadCells, C)) { ++TreeOnRoad; continue; }
|
||
|
|
if (AnyPoly(BuildingPolys, BuildingBoxes, BuildingCells, C)) { ++TreeInBuilding; continue; }
|
||
|
|
if (AnyStrip(StoneStrips, StoneCells, C) || AnyPoly(StonePolys, StoneBoxes, StonePolyCells, C)) { ++TreeOnStone; continue; }
|
||
|
|
const uint32 Hash = HashCombine(GetTypeHash(FMath::RoundToInt32(C.X)), GetTypeHash(FMath::RoundToInt32(C.Y)));
|
||
|
|
const int32 T = int32(Hash % uint32(TreeBoxes.Num()));
|
||
|
|
const double H = HMin + (HMax - HMin) * double((Hash >> 8) % 1000u) / 999.0;
|
||
|
|
const FVector BoxSize = TreeBoxes[T].GetSize().ComponentMax(FVector(1.0));
|
||
|
|
AddEntry(C, double((Hash >> 4) % 360u), BoxSize * (H / BoxSize.Z), uint8(TreeBase + T));
|
||
|
|
++Trees;
|
||
|
|
}
|
||
|
|
Walked += Len;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("arbres (A1) : %d poses sur %d trottoirs, pas %.0f cm, hauteur %.0f a %.0f cm ; ecartes : loin d'une grande voie nommee %d, chaussee %d, batiment %d, pierre %d"),
|
||
|
|
Trees, Sidewalks.Num(), PitchCm, HMin, HMax, TreeFar, TreeOnRoad, TreeInBuilding, TreeOnStone));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1.7.7 (STM1-SPEC-MOB1) : -Furniture=<objet>@<poses.csv>@<hauteur m>@<face>;... (avec -Osm et -Origin) : mobilier urbain.
|
||
|
|
// Chaque ligne du CSV (lat;lon;cap_deg;..., en-tete ignore ; cap = direction du regard, degres depuis l'est vers le nord)
|
||
|
|
// ajoute une entree : centre projete, maillage mis a la hauteur donnee (echelle uniforme), face du modele (+X, -X, +Y, -Y)
|
||
|
|
// tournee vers le cap. Le jeu reconnait le mobilier au nom du maillage (Mobilier_*) : effacement et ombres.
|
||
|
|
{
|
||
|
|
TArray<FString> Items;
|
||
|
|
ParamsMap.FindRef(TEXT("Furniture")).ParseIntoArray(Items, TEXT(";"));
|
||
|
|
for (const FString& Item : Items)
|
||
|
|
{
|
||
|
|
TArray<FString> Parts;
|
||
|
|
Item.ParseIntoArray(Parts, TEXT("@"));
|
||
|
|
const UStaticMesh* Mesh = Parts.Num() == 4 ? LoadObject<UStaticMesh>(nullptr, *Parts[0]) : nullptr;
|
||
|
|
TArray<FString> Rows;
|
||
|
|
const bool bCsv = Parts.Num() == 4 && FFileHelper::LoadFileToStringArray(Rows, *Parts[1]);
|
||
|
|
const double HeightCm = Parts.Num() == 4 ? FCString::Atod(*Parts[2]) * 100.0 : 0.0;
|
||
|
|
const FString Face = Parts.Num() == 4 ? Parts[3] : FString();
|
||
|
|
const double FaceDeg = Face == TEXT("+X") ? 0.0 : Face == TEXT("-X") ? 180.0 : Face == TEXT("+Y") ? 90.0 : Face == TEXT("-Y") ? -90.0 : 1e9;
|
||
|
|
if (!Mesh || !bCsv || HeightCm <= 0.0 || FaceDeg > 1e8 || OsmPath.IsEmpty() || Asset->Variants.Num() > 255)
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("ECHEC : -Furniture : %s (attendu <objet>@<poses.csv>@<hauteur m>@<+X|-X|+Y|-Y>, maillage et CSV lisibles, -Osm et -Origin) ; aucun asset ecrit"), *Item));
|
||
|
|
return Finish(1);
|
||
|
|
}
|
||
|
|
const FBox MeshBox = Mesh->GetBoundingBox();
|
||
|
|
const FVector Size = MeshBox.GetSize().ComponentMax(FVector(1.0)) * (HeightCm / FMath::Max(MeshBox.GetSize().Z, 1.0));
|
||
|
|
const uint8 V = uint8(Asset->Variants.Num());
|
||
|
|
Asset->Variants.Add(TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(Parts[0])));
|
||
|
|
PerVariant.Add(0);
|
||
|
|
int32 Posed = 0, Bad = 0;
|
||
|
|
for (int32 R = 1; R < Rows.Num(); ++R)
|
||
|
|
{
|
||
|
|
TArray<FString> Cols;
|
||
|
|
Rows[R].ParseIntoArray(Cols, TEXT(";"), false);
|
||
|
|
if (Cols.Num() < 3) { Bad += Rows[R].TrimStartAndEnd().IsEmpty() ? 0 : 1; continue; }
|
||
|
|
const FVector2D C = LPCityOsm::Project(FCString::Atod(*Cols[0]), FCString::Atod(*Cols[1]), Osm.OriginLat, Osm.OriginLon);
|
||
|
|
// Repere de la carte : Y vers le sud ; le cap (vers le nord positif) devient un lacet de -cap, moins l'angle de la face.
|
||
|
|
AddEntry(C, -FCString::Atod(*Cols[2]) - FaceDeg, Size, V);
|
||
|
|
++Posed;
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("mobilier %s : %d poses ; lignes illisibles %d ; hauteur %.0f cm ; taille %.0f x %.0f x %.0f cm ; face %s"),
|
||
|
|
*FPackageName::GetShortName(Parts[0]), Posed, Bad, HeightCm, Size.X, Size.Y, Size.Z, *Face));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
TArray<FString> PerText;
|
||
|
|
for (int32 I = 0; I < PerVariant.Num(); ++I) PerText.Add(FString::Printf(TEXT("%d"), PerVariant[I]));
|
||
|
|
Line(FString::Printf(TEXT("batiments %d ; gardes %d ; exclus %d (eau et monuments) ; ignores %d (moins de 3 points)"),
|
||
|
|
StreetMap->GetBuildings().Num(), Buildings, Excl, Skipped));
|
||
|
|
if (!OsmPath.IsEmpty())
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("OSM : associes %d ; retires : souterrains %d, layer < 0 %d, tunnel %d, toits %d, emprise sous %.0f m2 %d"),
|
||
|
|
Matched, OutUnderground, OutLayer, OutTunnel, OutRoof, MinAreaCm2 / 1e4, OutArea));
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("etages : building:levels %d, height %d, petits a 1 etage %d, defaut (%d) %d"), FromLevels, FromHeight, SmallOne, DefaultLevels, FromDefault));
|
||
|
|
if (bHaussmann)
|
||
|
|
{
|
||
|
|
if (bTall)
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("immeubles hauts (a partir de %d niveaux ; tours a partir de %d ; hauteur au plus %d niveaux) : barres %d, tours %d, bureaux %d ; famille vide, reportes %d"),
|
||
|
|
TallFrom, TallTower, TallMax, TallCount[0], TallCount[1], TallCount[2], TallFallback));
|
||
|
|
}
|
||
|
|
if (bModern)
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("secteur moderne : %d batiments en modeles bas ; %d gardes en Haussmann (start_date anterieure a %d)"), ModernCount, ModernOld, ModernBeforeYear));
|
||
|
|
}
|
||
|
|
if (bTall && (bTallSkipMonuments || TallMaxBoxRatio > 0.0))
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("immeubles hauts ecartes : monuments %d, emprises irregulieres %d (boite > %.2f x l'aire)"), TallMonuments, TallIrregular, TallMaxBoxRatio));
|
||
|
|
}
|
||
|
|
if (!ModernAreas.IsEmpty())
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("dalle : %d batiments en modeles modernes ; %d socles masques ; %d gardes en Haussmann (start_date anterieure a %d)"), AreaModern, AreaHidden, AreaOld, ModernBeforeYear));
|
||
|
|
}
|
||
|
|
if (!ChurchBoxes.IsEmpty())
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("eglises (E1) : %d allongees (%d modeles alternes), %d trapues (rapport %.2f ; hauteur %.0f a %.0f cm) ; raccourcies %d (etirement au plus %.2f) ; trop longues, laissees en immeubles %d (rapport > %.1f)"),
|
||
|
|
Churches[0], ChurchBoxes.Num() - 1, Churches[1], ChurchRatio, ChurchHMin, ChurchHMax, ChurchesShortened, ChurchStretch, ChurchesTooLong, ChurchMaxRatio));
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("Haussmann : angles %d ; decoupes %d (sans rue a %.0f cm : %d ; modules hors polygone ecartes %d) ; petits %d (dont pavillons de parc %d ; parcs %d) ; etroits hauts %d ; batiments commercants %d ; musees et batiments historiques de parc masques %d ; petites emprises masquees %d%s"),
|
||
|
|
Corners, Tiled, StreetCm, NoStreet, CellsOutside, SmallBuildings, ParkPavilions, Parks.Num(), NarrowTall, ShopBuildings, ParkMonuments, TinyMasked, bRealShops ? TEXT("") : TEXT(" (-RealShops absent : Commerces tires au hasard)")));
|
||
|
|
}
|
||
|
|
Line(FString::Printf(TEXT("entrees %d ; par variante %s ; tours remplacees %d"), Asset->Centers.Num(), *FString::Join(PerText, TEXT("/")), Asset->bReplaceSkyscrapers ? 1 : 0));
|
||
|
|
|
||
|
|
if (bDryRun)
|
||
|
|
{
|
||
|
|
Line(FString::Printf(TEXT("DryRun : %s non ecrit"), *PackageName));
|
||
|
|
return Finish(0);
|
||
|
|
}
|
||
|
|
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);
|
||
|
|
}
|