LifePlay-Revival/Source/LifePlayEditorTools/LPClipCityOSMCommandlet.cpp

360 lines
12 KiB
C++
Raw Normal View History

// Lot STM1 (STM1-SPEC-09, B1). Voir LPClipCityOSMCommandlet.h.
#include "LPClipCityOSMCommandlet.h"
#include "FastXml.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformMemory.h"
#include "HAL/PlatformTime.h"
#include "Misc/FileHelper.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPClipCityOSM, Log, All);
namespace LPClipCityOSM
{
struct FWay
{
int64 Id = 0;
TArray<int64> Refs;
/** Cle et valeur telles que lues (deja echappees en XML : recopiees sans transformation). */
TArray<TPair<FString, FString>> Tags;
bool bHighway = false;
bool bBuilding = false;
};
/** Lecteur en flux : meme automate que FOSMFile du plugin (noeuds, voies, nd, tag). */
class FReader : public IFastXmlCallback
{
public:
TMap<int64, FVector2d> Nodes; // X = lon, Y = lat
TArray<FWay> Ways;
virtual ~FReader() = default;
virtual bool ProcessXmlDeclaration(const TCHAR*, int32) override { return true; }
virtual bool ProcessComment(const TCHAR*) override { return true; }
virtual bool ProcessElement(const TCHAR* Name, const TCHAR*, int32) override
{
if (State == EState::Root)
{
if (!FCString::Stricmp(Name, TEXT("node"))) { State = EState::Node; NodeId = 0; NodePos = FVector2d::ZeroVector; }
else if (!FCString::Stricmp(Name, TEXT("way"))) { State = EState::Way; Way = FWay(); }
}
else if (State == EState::Node)
{
State = EState::NodeChild;
}
else if (State == EState::Way)
{
if (!FCString::Stricmp(Name, TEXT("nd"))) State = EState::WayNd;
else if (!FCString::Stricmp(Name, TEXT("tag"))) { State = EState::WayTag; TagKey.Reset(); }
else State = EState::WayOther;
}
return true;
}
virtual bool ProcessAttribute(const TCHAR* Name, const TCHAR* Value) override
{
switch (State)
{
case EState::Node:
if (!FCString::Stricmp(Name, TEXT("id"))) NodeId = FCString::Atoi64(Value);
else if (!FCString::Stricmp(Name, TEXT("lat"))) NodePos.Y = FCString::Atod(Value);
else if (!FCString::Stricmp(Name, TEXT("lon"))) NodePos.X = FCString::Atod(Value);
break;
case EState::Way:
if (!FCString::Stricmp(Name, TEXT("id"))) Way.Id = FCString::Atoi64(Value);
break;
case EState::WayNd:
if (!FCString::Stricmp(Name, TEXT("ref"))) Way.Refs.Add(FCString::Atoi64(Value));
break;
case EState::WayTag:
if (!FCString::Stricmp(Name, TEXT("k"))) TagKey = Value;
else if (!FCString::Stricmp(Name, TEXT("v")))
{
Way.bHighway |= TagKey == TEXT("highway");
Way.bBuilding |= TagKey == TEXT("building");
Way.Tags.Emplace(TagKey, FString(Value));
}
break;
default:
break;
}
return true;
}
virtual bool ProcessClose(const TCHAR*) override
{
switch (State)
{
case EState::Node: Nodes.Add(NodeId, NodePos); State = EState::Root; break;
case EState::NodeChild: State = EState::Node; break;
case EState::Way: Ways.Add(MoveTemp(Way)); State = EState::Root; break;
case EState::WayNd:
case EState::WayTag:
case EState::WayOther: State = EState::Way; break;
default: break;
}
return true;
}
private:
enum class EState { Root, Node, NodeChild, Way, WayNd, WayTag, WayOther };
EState State = EState::Root;
int64 NodeId = 0;
FVector2d NodePos = FVector2d::ZeroVector;
FWay Way;
FString TagKey;
};
/** Liang-Barsky : portion [T0, T1] du segment A-B dans la boite ; false si le segment est dehors. */
bool ClipSegment(const FVector2d& A, const FVector2d& B, const FVector2d& Min, const FVector2d& Max, double& T0, double& T1)
{
T0 = 0.0;
T1 = 1.0;
const FVector2d D = B - A;
const double P[4] = { -D.X, D.X, -D.Y, D.Y };
const double Q[4] = { A.X - Min.X, Max.X - A.X, A.Y - Min.Y, Max.Y - A.Y };
for (int32 I = 0; I < 4; ++I)
{
if (FMath::IsNearlyZero(P[I]))
{
if (Q[I] < 0.0) return false;
continue;
}
const double R = Q[I] / P[I];
if (P[I] < 0.0) T0 = FMath::Max(T0, R);
else T1 = FMath::Min(T1, R);
if (T0 > T1) return false;
}
return true;
}
FString Coord(double Value)
{
return FString::Printf(TEXT("%.7f"), Value);
}
}
ULPClipCityOSMCommandlet::ULPClipCityOSMCommandlet()
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
}
int32 ULPClipCityOSMCommandlet::Main(const FString& Params)
{
using namespace LPClipCityOSM;
TArray<FString> Tokens, Switches;
TMap<FString, FString> ParamsMap;
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
const FString OsmPath = ParamsMap.FindRef(TEXT("Osm"));
const FString OutPath = ParamsMap.FindRef(TEXT("Out"));
const FString BoxText = ParamsMap.FindRef(TEXT("Box"));
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
TArray<FString> Report;
auto Line = [&Report](const FString& Text)
{
UE_LOG(LogLPClipCityOSM, 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> BoxParts;
BoxText.ParseIntoArray(BoxParts, TEXT(","));
if (OsmPath.IsEmpty() || OutPath.IsEmpty() || BoxParts.Num() != 4)
{
Line(TEXT("usage : -Osm=<entree.osm> -Out=<sortie.osm> -Box=<sud>,<ouest>,<nord>,<est> [-Report=<f.md>]"));
return Finish(2);
}
const FVector2d Min(FCString::Atod(*BoxParts[1]), FCString::Atod(*BoxParts[0])); // lon, lat
const FVector2d Max(FCString::Atod(*BoxParts[3]), FCString::Atod(*BoxParts[2]));
Line(FString::Printf(TEXT("# LPClipCityOSM %s -> %s"), *OsmPath, *OutPath));
Line(FString::Printf(TEXT("boite : sud %.5f ouest %.5f nord %.5f est %.5f"), Min.Y, Min.X, Max.Y, Max.X));
if (Min.X >= Max.X || Min.Y >= Max.Y)
{
Line(TEXT("ECHEC : boite invalide"));
return Finish(1);
}
if (IFileManager::Get().FileExists(*OutPath))
{
Line(TEXT("ECHEC : le fichier de sortie existe deja ; rien ecrit"));
return Finish(1);
}
// 1. Lecture.
const double Start = FPlatformTime::Seconds();
FReader Reader;
FText Error;
int32 ErrorLine = 0;
if (!FFastXml::ParseXmlFile(&Reader, *OsmPath, nullptr, nullptr, false, false, Error, ErrorLine))
{
Line(FString::Printf(TEXT("ECHEC : lecture XML (%s, ligne %d)"), *Error.ToString(), ErrorLine));
return Finish(1);
}
Line(FString::Printf(TEXT("lu : %d noeuds, %d voies en %.1f s"), Reader.Nodes.Num(), Reader.Ways.Num(), FPlatformTime::Seconds() - Start));
// 2. Decoupe.
auto Inside = [&](const FVector2d& P) { return P.X >= Min.X && P.X <= Max.X && P.Y >= Min.Y && P.Y <= Max.Y; };
TMap<int64, FVector2d> OutNodes;
TArray<FWay> OutWays;
int64 NextNodeId = -1, NextWayId = -1;
int32 BoundaryNodes = 0;
int32 RoadsIn = 0, RoadsKept = 0, RoadsCut = 0, RoadPieces = 0, AreasIn = 0, AreasKept = 0, MissingRefs = 0;
for (const FWay& Way : Reader.Ways)
{
TArray<FVector2d> Points;
TArray<int64> Refs;
for (const int64 Ref : Way.Refs)
{
if (const FVector2d* P = Reader.Nodes.Find(Ref)) { Points.Add(*P); Refs.Add(Ref); }
else ++MissingRefs;
}
if (Points.Num() < 2) continue;
if (Way.bHighway && !Way.bBuilding)
{
++RoadsIn;
TArray<TArray<int64>> Pieces;
TArray<int64> Current;
bool bCut = false;
for (int32 I = 0; I + 1 < Points.Num(); ++I)
{
double T0, T1;
if (!ClipSegment(Points[I], Points[I + 1], Min, Max, T0, T1))
{
if (Current.Num() >= 2) Pieces.Add(Current);
Current.Reset();
bCut = true;
continue;
}
auto PointId = [&](double T) -> int64
{
if (T <= 0.0) return Refs[I];
if (T >= 1.0) return Refs[I + 1];
const int64 Id = NextNodeId--;
++BoundaryNodes;
OutNodes.Add(Id, Points[I] + T * (Points[I + 1] - Points[I]));
bCut = true;
return Id;
};
const int64 A = PointId(T0);
if (Current.IsEmpty() || Current.Last() != A)
{
if (Current.Num() >= 2) Pieces.Add(Current);
Current.Reset();
Current.Add(A);
}
Current.Add(PointId(T1));
if (T1 < 1.0)
{
Pieces.Add(Current);
Current.Reset();
}
}
if (Current.Num() >= 2) Pieces.Add(Current);
if (Pieces.IsEmpty()) continue;
++RoadsKept;
RoadsCut += bCut ? 1 : 0;
for (int32 Index = 0; Index < Pieces.Num(); ++Index)
{
FWay& Piece = OutWays.AddDefaulted_GetRef();
Piece.Id = Index == 0 ? Way.Id : NextWayId--;
Piece.Refs = Pieces[Index];
Piece.Tags = Way.Tags;
for (const int64 Ref : Piece.Refs)
{
if (Ref > 0) OutNodes.Add(Ref, Reader.Nodes[Ref]);
}
++RoadPieces;
}
}
else
{
++AreasIn;
FVector2d BoxMin = Points[0], BoxMax = Points[0];
for (const FVector2d& P : Points)
{
BoxMin = FVector2d(FMath::Min(BoxMin.X, P.X), FMath::Min(BoxMin.Y, P.Y));
BoxMax = FVector2d(FMath::Max(BoxMax.X, P.X), FMath::Max(BoxMax.Y, P.Y));
}
if (!Inside(0.5 * (BoxMin + BoxMax))) continue;
++AreasKept;
FWay& Kept = OutWays.AddDefaulted_GetRef();
Kept.Id = Way.Id;
Kept.Refs = Refs;
Kept.Tags = Way.Tags;
for (const int64 Ref : Refs) OutNodes.Add(Ref, Reader.Nodes[Ref]);
}
}
Line(FString::Printf(TEXT("routes : %d lues, %d gardees (dont %d coupees au bord), %d morceaux ; autres voies : %d lues, %d gardees (centre de boite dans la boite) ; references absentes %d"),
RoadsIn, RoadsKept, RoadsCut, RoadPieces, AreasIn, AreasKept, MissingRefs));
// 3. Ecriture (noeuds puis voies) et origine attendue (moyenne des coordonnees ecrites, comme FOSMFile).
TUniquePtr<FArchive> Writer(IFileManager::Get().CreateFileWriter(*OutPath));
if (!Writer)
{
Line(TEXT("ECHEC : creation du fichier de sortie"));
return Finish(1);
}
FString Buffer;
auto Flush = [&](bool bForce)
{
if (bForce || Buffer.Len() > (1 << 20))
{
const FTCHARToUTF8 Utf8(*Buffer);
Writer->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length());
Buffer.Reset();
}
};
Buffer += TEXT("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"LifePlay LPClipCityOSM (STM1-SPEC-09)\">\n");
Buffer += FString::Printf(TEXT(" <bounds minlat=\"%s\" minlon=\"%s\" maxlat=\"%s\" maxlon=\"%s\"/>\n"), *Coord(Min.Y), *Coord(Min.X), *Coord(Max.Y), *Coord(Max.X));
TArray<int64> NodeIds;
OutNodes.GetKeys(NodeIds);
NodeIds.Sort();
double SumLat = 0.0, SumLon = 0.0;
for (const int64 Id : NodeIds)
{
const FVector2d& P = OutNodes[Id];
const FString Lat = Coord(P.Y), Lon = Coord(P.X);
SumLat += FCString::Atod(*Lat);
SumLon += FCString::Atod(*Lon);
Buffer += FString::Printf(TEXT(" <node id=\"%lld\" lat=\"%s\" lon=\"%s\"/>\n"), Id, *Lat, *Lon);
Flush(false);
}
for (const FWay& Way : OutWays)
{
Buffer += FString::Printf(TEXT(" <way id=\"%lld\">\n"), Way.Id);
for (const int64 Ref : Way.Refs) Buffer += FString::Printf(TEXT(" <nd ref=\"%lld\"/>\n"), Ref);
for (const TPair<FString, FString>& Tag : Way.Tags) Buffer += FString::Printf(TEXT(" <tag k=\"%s\" v=\"%s\"/>\n"), *Tag.Key, *Tag.Value);
Buffer += TEXT(" </way>\n");
Flush(false);
}
Buffer += TEXT("</osm>\n");
Flush(true);
Writer->Close();
const int64 OutBytes = IFileManager::Get().FileSize(*OutPath);
const double OriginLat = NodeIds.Num() ? SumLat / NodeIds.Num() : 0.0;
const double OriginLon = NodeIds.Num() ? SumLon / NodeIds.Num() : 0.0;
Line(FString::Printf(TEXT("ecrit : %s, %lld octets ; %d noeuds (dont %d crees au bord), %d voies"),
*OutPath, OutBytes, NodeIds.Num(), BoundaryNodes, OutWays.Num()));
Line(FString::Printf(TEXT("origine attendue de l'import (moyenne des noeuds ecrits) : lat %.9f lon %.9f"), OriginLat, OriginLon));
const FPlatformMemoryStats Mem = FPlatformMemory::GetStats();
Line(FString::Printf(TEXT("duree %.1f s ; pic physique %.2f Go"), FPlatformTime::Seconds() - Start, double(Mem.PeakUsedPhysical) / (1024.0 * 1024.0 * 1024.0)));
return Finish(0);
}