LifePlay-Revival/Source/LifePlayEditorTools/LPCityOsm.h
2026-10-22 21:10:39 +02:00

283 lines
10 KiB
C++

// Lot STM1 (1.3.0) : lecture en flux des fichiers OSM pour les commandlets de ville (LPBuildCityHouses, LPBuildCityStatic,
// LPEnrichCityBuildings), sans interface.
//
// - Projection du plugin StreetMap (StreetMapFactory.cpp : Sanson-Flamsteed, cosinus de la latitude du noeud), relative a
// l'origine de l'import (moyenne des noeuds du .osm importe), en cm.
// - bWays : centre de boite projete et balises de chaque voie d'au moins 3 points (extrait complet : noeuds puis voies) ;
// la voie d'un batiment de l'UStreetMap se retrouve par ce centre, a 50 cm pres.
// - bPoints : points d'interet balises (sortie Overpass « out tags center ») : noeuds, et centre des voies et relations.
// - bLines (1.4.0, STM1-SPEC-17) : points projetes et balises des voies highway=* d'au moins 2 points (lignes ouvertes ou
// fermees, sans retrait du dernier point) : routes pavees, trottoirs.
// Le fichier est lu, jamais execute.
#pragma once
#include "CoreMinimal.h"
#include "FastXml.h"
namespace LPCityOsm
{
// Meme constante que le plugin (StreetMapFactory.cpp:8-9), en centimetres par degre.
constexpr double CmPerDegree = 40075036.0 / 360.0 * 100.0;
inline FVector2D Project(double Lat, double Lon, double OriginLat, double OriginLon)
{
const double CosLat = FMath::Cos(FMath::DegreesToRadians(Lat));
return FVector2D((Lon * CosLat - OriginLon * CosLat) * CmPerDegree, -(Lat - OriginLat) * CmPerDegree);
}
/** Centre de boite des points, en float comme GI.FindCenterOfPolygon (LPDataCompat). */
inline 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);
}
/** Cle de case de 1 m pour retrouver un centre a 50 cm pres. */
inline FIntPoint Cell(const FVector2D& P)
{
return FIntPoint(FMath::FloorToInt32(P.X / 100.0), FMath::FloorToInt32(P.Y / 100.0));
}
inline 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;
}
/** Point d'interet : position projetee, balises, identifiant OSM (type et numero). */
struct FPoint
{
FVector2D Position;
TMap<FString, FString> Tags;
FString Id;
};
class FReader : public IFastXmlCallback
{
public:
double OriginLat = 0.0, OriginLon = 0.0;
bool bWays = true;
bool bPoints = false;
bool bLines = false;
// 1.4.1 (STM1-DIAG-03 E1) : avec bLines, garder aussi les voies fermees leisure, landuse, place ou area:highway.
bool bAreas = false;
// 1.7.6 (STM1-DIAG-16 W) : avec bAreas, garder aussi les plans d'eau fermes (natural=water, amenity=fountain, water=*).
bool bWaterAreas = false;
TMap<int64, FVector2d> Nodes; // X = longitude, Y = latitude
TArray<FVector2D> WayCenters;
TArray<TMap<FString, FString>> WayTags;
TArray<FPoint> Points;
TArray<TArray<FVector2D>> Lines;
TArray<TMap<FString, FString>> LineTags;
virtual ~FReader() = default;
virtual bool ProcessXmlDeclaration(const TCHAR* ElementData, int32 XmlFileLineNumber) override { return true; }
virtual bool ProcessComment(const TCHAR* Comment) override { return true; }
virtual bool ProcessElement(const TCHAR* ElementName, const TCHAR* ElementData, int32 XmlFileLineNumber) override
{
if (!FCString::Strcmp(ElementName, TEXT("node"))) { Begin(EState::Node); }
else if (!FCString::Strcmp(ElementName, TEXT("way"))) { Begin(EState::Way); }
else if (!FCString::Strcmp(ElementName, TEXT("relation"))) { Begin(EState::Relation); }
else if (!FCString::Strcmp(ElementName, TEXT("nd"))) { Child = EChild::Nd; }
else if (!FCString::Strcmp(ElementName, TEXT("tag"))) { Child = EChild::Tag; TagKey.Reset(); }
else if (!FCString::Strcmp(ElementName, TEXT("center"))) { Child = EChild::Center; }
else { Child = EChild::Other; }
return true;
}
virtual bool ProcessAttribute(const TCHAR* AttributeName, const TCHAR* AttributeValue) override
{
switch (Child)
{
case EChild::Tag:
if (!FCString::Strcmp(AttributeName, TEXT("k"))) TagKey = AttributeValue;
else if (!FCString::Strcmp(AttributeName, TEXT("v")) && State != EState::None) Tags.Add(TagKey, AttributeValue);
break;
case EChild::Nd:
if (State == EState::Way && !FCString::Strcmp(AttributeName, TEXT("ref"))) Refs.Add(FCString::Atoi64(AttributeValue));
break;
case EChild::Center:
if (!FCString::Strcmp(AttributeName, TEXT("lat"))) { Lat = FCString::Atod(AttributeValue); bHasPosition = true; }
else if (!FCString::Strcmp(AttributeName, TEXT("lon"))) Lon = FCString::Atod(AttributeValue);
break;
case EChild::None:
if (!FCString::Strcmp(AttributeName, TEXT("id"))) Id = FCString::Atoi64(AttributeValue);
else if (State == EState::Node && !FCString::Strcmp(AttributeName, TEXT("lat"))) { Lat = FCString::Atod(AttributeValue); bHasPosition = true; }
else if (State == EState::Node && !FCString::Strcmp(AttributeName, TEXT("lon"))) Lon = FCString::Atod(AttributeValue);
break;
default:
break;
}
return true;
}
virtual bool ProcessClose(const TCHAR* Element) override
{
const bool bTop = !FCString::Strcmp(Element, TEXT("node")) || !FCString::Strcmp(Element, TEXT("way")) || !FCString::Strcmp(Element, TEXT("relation"));
if (!bTop)
{
Child = EChild::None;
return true;
}
if (State == EState::Node)
{
Nodes.Add(Id, FVector2d(Lon, Lat));
}
else if (State == EState::Way && bWays)
{
FinishWay();
}
if (bPoints && bHasPosition && !Tags.IsEmpty())
{
const TCHAR* Kind = State == EState::Node ? TEXT("node") : State == EState::Way ? TEXT("way") : TEXT("relation");
Points.Add({ Project(Lat, Lon, OriginLat, OriginLon), MoveTemp(Tags), FString::Printf(TEXT("%s/%lld"), Kind, Id) });
}
State = EState::None;
Child = EChild::None;
return true;
}
private:
enum class EState { None, Node, Way, Relation };
enum class EChild { None, Nd, Tag, Center, Other };
EState State = EState::None;
EChild Child = EChild::None;
int64 Id = 0;
double Lat = 0.0, Lon = 0.0;
bool bHasPosition = false;
TArray<int64> Refs;
TMap<FString, FString> Tags;
FString TagKey;
void Begin(EState NewState)
{
State = NewState;
Child = EChild::None;
Id = 0;
Lat = Lon = 0.0;
bHasPosition = false;
Refs.Reset();
Tags.Reset();
}
void FinishWay()
{
TArray<FVector2D> Pts;
for (const int64 Ref : Refs)
{
if (const FVector2d* LL = Nodes.Find(Ref))
{
Pts.Add(Project(LL->Y, LL->X, OriginLat, OriginLon));
}
}
const bool bClosed = Pts.Num() >= 4 && Pts[0].Equals(Pts.Last(), KINDA_SMALL_NUMBER);
const bool bArea = bAreas && bClosed && (Tags.Contains(TEXT("leisure")) || Tags.Contains(TEXT("landuse")) || Tags.Contains(TEXT("place")) || Tags.Contains(TEXT("area:highway"))
|| (bWaterAreas && (Tags.FindRef(TEXT("natural")) == TEXT("water") || Tags.FindRef(TEXT("amenity")) == TEXT("fountain") || Tags.Contains(TEXT("water")))));
if (bLines && Pts.Num() >= 2 && (Tags.Contains(TEXT("highway")) || bArea))
{
Lines.Add(Pts);
LineTags.Add(Tags);
}
if (Pts.Num() > 1 && Pts[0].Equals(Pts.Last(), KINDA_SMALL_NUMBER))
{
Pts.Pop();
}
if (Pts.Num() >= 3)
{
WayCenters.Add(BoxCenter(Pts));
WayTags.Add(Tags);
}
}
};
/** Lit un fichier ; faux et message en cas d'echec. */
inline bool Read(FReader& Reader, const FString& Path, FString& OutError)
{
FText XmlError;
int32 XmlErrorLine = 0;
if (!FFastXml::ParseXmlFile(&Reader, *Path, nullptr, nullptr, false, false, XmlError, XmlErrorLine))
{
OutError = FString::Printf(TEXT("lecture de %s (ligne %d : %s)"), *Path, XmlErrorLine, *XmlError.ToString());
return false;
}
return true;
}
/** Index des voies par centre (case de 1 m) ; Find : indice de la voie dont le centre est a 50 cm pres, sinon -1. */
struct FWayIndex
{
TMultiMap<FIntPoint, int32> Cells;
const TArray<FVector2D>* Centers = nullptr;
void Build(const TArray<FVector2D>& InCenters)
{
Centers = &InCenters;
Cells.Reset();
for (int32 I = 0; I < InCenters.Num(); ++I) Cells.Add(Cell(InCenters[I]), I);
}
int32 Find(const FVector2D& C) const
{
const FIntPoint K = Cell(C);
for (int32 DX = -1; DX <= 1; ++DX)
for (int32 DY = -1; DY <= 1; ++DY)
for (auto It = Cells.CreateConstKeyIterator(K + FIntPoint(DX, DY)); It; ++It)
if (FVector2D::Distance((*Centers)[It.Value()], C) <= 50.0) return It.Value();
return INDEX_NONE;
}
};
/** Batiment retire par les filtres de la 1.2.1 (souterrain, layer < 0, tunnel, toit). L'emprise minimale se teste a part. */
inline bool IsExcludedBuilding(const TMap<FString, FString>* Tags)
{
if (!Tags) return false;
const FString* Layer = Tags->Find(TEXT("layer"));
return Tags->FindRef(TEXT("location")) == TEXT("underground")
|| (Layer && FCString::Atoi(**Layer) < 0)
|| Tags->FindRef(TEXT("tunnel")) == TEXT("yes")
|| Tags->FindRef(TEXT("building")) == TEXT("roof");
}
/** 1.3.2 (COMMUN-27b) : musee ou batiment historique (dans un parc, il est masque : ni immeuble ni volume classique). */
inline bool IsMuseumOrHistoric(const TMap<FString, FString>* Tags)
{
return Tags && (Tags->FindRef(TEXT("tourism")) == TEXT("museum") || Tags->FindRef(TEXT("building")) == TEXT("museum")
|| Tags->Contains(TEXT("historic")));
}
/** Point dans l'un des polygones (boites a l'appui). */
inline bool InAny(const TArray<TArray<FVector2D>>& Polygons, const TArray<FBox2D>& Boxes, const FVector2D& P)
{
for (int32 I = 0; I < Polygons.Num(); ++I)
{
if (Boxes[I].IsInside(P) && InsidePolygon(Polygons[I], P)) return true;
}
return false;
}
/** Etages : building:levels (1 a 15), sinon height / hauteur d'etage ; 0 si inconnus. */
inline int32 KnownLevels(const TMap<FString, FString>* Tags, double LevelHeightCm)
{
if (!Tags) return 0;
const double Levels = FCString::Atod(*Tags->FindRef(TEXT("building:levels")));
if (Levels > 0.0) return FMath::Clamp(FMath::RoundToInt32(Levels), 1, 15);
const double Height = FCString::Atod(*Tags->FindRef(TEXT("height")));
if (Height > 0.0) return FMath::Clamp(FMath::RoundToInt32(Height * 100.0 / LevelHeightCm), 1, 15);
return 0;
}
}