2026-10-20 15:35:10 +00:00
// 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"
2026-10-22 19:10:39 +00:00
# include "LPCityOsm.h"
2026-10-20 15:35:10 +00:00
# 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 ;
2026-10-22 19:10:39 +00:00
// 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 " ) ) ;
2026-10-20 15:35:10 +00:00
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 ;
} ;
2026-10-22 19:10:39 +00:00
if ( City . IsEmpty ( ) | | WaterPath . IsEmpty ( ) | | ( BuildingsPath . IsEmpty ( ) & & ! bFixedOrigin ) )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
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>] " ) ) ;
2026-10-20 15:35:10 +00:00
return Finish ( 2 ) ;
}
2026-10-22 19:10:39 +00:00
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 ) ;
}
}
2026-10-20 15:35:10 +00:00
// 1. Carte et maillage de reference.
2026-10-22 19:10:39 +00:00
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 ) ) ;
2026-10-20 15:35:10 +00:00
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 ;
2026-10-22 19:10:39 +00:00
if ( ! ReadOverpass ( WaterPath , WaterWays , WaterRelations , Error ) | | ( ! bFixedOrigin & & ! ReadOverpass ( BuildingsPath , BuildingWays , UnusedRelations , Error ) ) )
2026-10-20 15:35:10 +00:00
{
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 ( ) ) ) ;
2026-10-22 19:10:39 +00:00
FLonLat Origin ( 0.0 , 0.0 ) ;
if ( bFixedOrigin )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
TArray < FString > OriginParts ;
OriginText . ParseIntoArray ( OriginParts , TEXT ( " , " ) ) ;
if ( OriginParts . Num ( ) ! = 2 )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
Line ( TEXT ( " ECHEC : -Origin=<lat>,<lon> attendu ; aucun asset ecrit " ) ) ;
return Finish ( 1 ) ;
2026-10-20 15:35:10 +00:00
}
2026-10-22 19:10:39 +00:00
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 ) ) ;
2026-10-20 15:35:10 +00:00
}
2026-10-22 19:10:39 +00:00
else
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
// 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 )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
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 ( ) )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
if ( Building . BuildingName . IsEmpty ( ) | | Building . BuildingPoints . Num ( ) < 3 ) continue ;
const FString Key = Building . BuildingName . ToLower ( ) ;
AssetCount . FindOrAdd ( Key ) + + ;
AssetCentroid . Add ( Key , BoxCenter ( Building . BuildingPoints ) ) ;
2026-10-20 15:35:10 +00:00
}
2026-10-22 19:10:39 +00:00
int32 OsmFromBounds = 0 , OsmFromNodes = 0 ;
for ( const FOsmWay & Way : BuildingWays )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
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 ;
}
2026-10-20 15:35:10 +00:00
}
2026-10-22 19:10:39 +00:00
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 )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
if ( Entry . Value = = 1 & & OsmCount . FindRef ( Entry . Key ) = = 1 )
{
Pairs . Add ( { AssetCentroid [ Entry . Key ] , OsmCentroid [ Entry . Key ] } ) ;
}
2026-10-20 15:35:10 +00:00
}
2026-10-22 19:10:39 +00:00
// 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 )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
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 )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
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 ;
} ;
2026-10-20 15:35:10 +00:00
2026-10-22 19:10:39 +00:00
// 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 )
2026-10-20 15:35:10 +00:00
{
2026-10-22 19:10:39 +00:00
if ( Residuals1 [ Index ] < = RejectCm )
{
Kept . Add ( Pairs [ Index ] ) ;
}
2026-10-20 15:35:10 +00:00
}
2026-10-22 19:10:39 +00:00
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) " ) ) ;
2026-10-20 15:35:10 +00:00
}
// 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 ) ;
2026-10-22 19:10:39 +00:00
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 ) ) ;
}
2026-10-20 15:35:10 +00:00
auto ToMap = [ & ] ( const TArray < FLonLat > & Ring )
{
2026-10-22 19:10:39 +00:00
TArray < FVector2d > LL ;
2026-10-20 15:35:10 +00:00
for ( int32 Index = 0 ; Index + 1 < Ring . Num ( ) ; + + Index ) // sans le point de fermeture
{
2026-10-22 19:10:39 +00:00
LL . Add ( Ring [ Index ] ) ;
}
if ( bClipBox )
{
LL = ClipToBox ( LL , BoxMinLL , BoxMaxLL ) ;
}
TArray < FVector2d > P ;
for ( const FVector2d & Point : LL )
{
P . Add ( Project ( Point , Origin ) ) ;
2026-10-20 15:35:10 +00:00
}
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 ) ) ;
2026-10-22 19:10:39 +00:00
// 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 ( ) ) ) ;
}
2026-10-20 15:35:10 +00:00
// 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 )
{
2026-10-22 19:10:39 +00:00
const FString MaskPackageName = FString : : Printf ( TEXT ( " %s/Water/%s_Mask " ) , * MapRoot , * City ) ;
2026-10-20 15:35:10 +00:00
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 ) ) ;
}
2026-10-22 19:10:39 +00:00
// 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 ) ;
}
2026-10-20 15:35:10 +00:00
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 ) ;
}
2026-10-22 19:10:39 +00:00
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 ) ) ;
2026-10-20 15:35:10 +00:00
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.
2026-10-22 19:10:39 +00:00
const FString PackageName = FString : : Printf ( TEXT ( " %s/Water/%s " ) , * MapRoot , * City ) ;
2026-10-20 15:35:10 +00:00
if ( FPackageName : : DoesPackageExist ( PackageName ) )
{
Line ( FString : : Printf ( TEXT ( " ECHEC : %s existe deja ; aucun asset ecrit " ) , * PackageName ) ) ;
return Finish ( 1 ) ;
}
2026-10-22 19:10:39 +00:00
// 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 " ) ) ) )
2026-10-20 15:35:10 +00:00
{
// 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 ) ;
2026-10-22 19:10:39 +00:00
UMaterial * NewMaterial = NewObject < UMaterial > ( MaterialPackage , bPackMaterial ? FName ( * PackMaterialName ) : FName ( TEXT ( " M_LP_Water " ) ) , RF_Public | RF_Standalone ) ;
2026-10-20 15:35:10 +00:00
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 ) ;
2026-10-22 19:10:39 +00:00
Roughness - > R = bPackMaterial ? WaterRoughness : 0.15f ;
Specular - > R = bPackMaterial ? WaterSpecular : 0.5f ;
2026-10-20 15:35:10 +00:00
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 ) ;
}
2026-10-22 19:10:39 +00:00
Line ( FString : : Printf ( TEXT ( " materiau cree : %s (%s), deux faces ; rugosite %.2f ; speculaire %.2f " ) , MaterialPackageName , * MaterialFile , Roughness - > R , Specular - > R ) ) ;
2026-10-20 15:35:10 +00:00
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 " ) ;
2026-10-22 19:10:39 +00:00
// 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 ( ) ) ) ;
}
2026-10-20 15:35:10 +00:00
TArray < FVertexID > VertexIds ;
VertexIds . Reserve ( Vertices . Num ( ) ) ;
for ( const FVector3f & V : Vertices )
{
const FVertexID Id = Mesh . CreateVertex ( ) ;
2026-10-22 19:10:39 +00:00
Attributes . GetVertexPositions ( ) [ Id ] = V + FVector3f ( 0.f , 0.f , WaterZOffsetCm ) ;
2026-10-20 15:35:10 +00:00
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 ) ;
}