// 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& 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& 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 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 Nodes; // X = longitude, Y = latitude TArray WayCenters; TArray> WayTags; TArray Points; TArray> Lines; TArray> 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 Refs; TMap 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 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 Cells; const TArray* Centers = nullptr; void Build(const TArray& 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* 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* 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>& Polygons, const TArray& 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* 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; } }