678 lines
21 KiB
C++
678 lines
21 KiB
C++
#include "UeSceneWebSocketClient.h"
|
||
|
||
#include "Dom/JsonObject.h"
|
||
#include "Dom/JsonValue.h"
|
||
#include "HttpModule.h"
|
||
#include "JsonObjectConverter.h"
|
||
#include "Serialization/JsonReader.h"
|
||
#include "Serialization/JsonSerializer.h"
|
||
#include "WebSocketsModule.h"
|
||
|
||
namespace
|
||
{
|
||
constexpr double GeoPointMergeTolerance = 1e-4;
|
||
|
||
bool TryParseGeoPoint(const TSharedPtr<FJsonObject>& PointObject, FVector& OutGeoPoint)
|
||
{
|
||
if (!PointObject.IsValid())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
double Latitude = 0.0;
|
||
double Longitude = 0.0;
|
||
double Altitude = 0.0;
|
||
if (!PointObject->TryGetNumberField(TEXT("lat"), Latitude)
|
||
|| !PointObject->TryGetNumberField(TEXT("lng"), Longitude))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
PointObject->TryGetNumberField(TEXT("alt"), Altitude);
|
||
OutGeoPoint = FVector(Longitude, Latitude, Altitude);
|
||
return true;
|
||
}
|
||
|
||
FString ResolveCableName(const TSharedPtr<FJsonObject>& ItemObject)
|
||
{
|
||
FString CableName;
|
||
|
||
const TSharedPtr<FJsonObject>* LabelsObject = nullptr;
|
||
if (ItemObject->TryGetObjectField(TEXT("labels"), LabelsObject)
|
||
&& LabelsObject != nullptr
|
||
&& LabelsObject->IsValid())
|
||
{
|
||
(*LabelsObject)->TryGetStringField(TEXT("title"), CableName);
|
||
}
|
||
|
||
if (CableName.IsEmpty())
|
||
{
|
||
ItemObject->TryGetStringField(TEXT("id"), CableName);
|
||
}
|
||
|
||
return CableName;
|
||
}
|
||
|
||
bool AreGeoPointsConnected(const FVector& A, const FVector& B)
|
||
{
|
||
return FMath::Abs(A.X - B.X) <= GeoPointMergeTolerance
|
||
&& FMath::Abs(A.Y - B.Y) <= GeoPointMergeTolerance
|
||
&& FMath::Abs(A.Z - B.Z) <= GeoPointMergeTolerance;
|
||
}
|
||
|
||
void AppendMergedSegment(
|
||
TArray<TArray<FVector>>& MergedSegments,
|
||
const TArray<FVector>& IncomingSegment
|
||
)
|
||
{
|
||
if (IncomingSegment.Num() < 2)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (MergedSegments.Num() == 0)
|
||
{
|
||
MergedSegments.Add(IncomingSegment);
|
||
return;
|
||
}
|
||
|
||
TArray<FVector>& LastMergedSegment = MergedSegments.Last();
|
||
if (LastMergedSegment.Num() == 0)
|
||
{
|
||
LastMergedSegment = IncomingSegment;
|
||
return;
|
||
}
|
||
|
||
if (AreGeoPointsConnected(LastMergedSegment.Last(), IncomingSegment[0]))
|
||
{
|
||
for (int32 Index = 1; Index < IncomingSegment.Num(); ++Index)
|
||
{
|
||
LastMergedSegment.Add(IncomingSegment[Index]);
|
||
}
|
||
return;
|
||
}
|
||
|
||
MergedSegments.Add(IncomingSegment);
|
||
}
|
||
}
|
||
|
||
UUeSceneWebSocketClient::UUeSceneWebSocketClient()
|
||
{
|
||
PrimaryComponentTick.bCanEverTick = false;
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::BeginPlay()
|
||
{
|
||
Super::BeginPlay();
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||
{
|
||
DisconnectFromBackend();
|
||
Super::EndPlay(EndPlayReason);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::ConnectToBackend()
|
||
{
|
||
if (Username.IsEmpty() || Password.IsEmpty())
|
||
{
|
||
EmitError(TEXT("Username / Password 不能为空。"));
|
||
return;
|
||
}
|
||
|
||
RequestAccessToken();
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::DisconnectFromBackend()
|
||
{
|
||
if (Socket.IsValid())
|
||
{
|
||
Socket->Close();
|
||
Socket.Reset();
|
||
}
|
||
|
||
PendingFullScene.Reset();
|
||
OnConnectedChanged.Broadcast(false);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::SendSubscribe()
|
||
{
|
||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||
Root->SetStringField(TEXT("type"), TEXT("subscribe"));
|
||
|
||
TSharedRef<FJsonObject> Data = MakeShared<FJsonObject>();
|
||
TArray<TSharedPtr<FJsonValue>> Channels;
|
||
for (const FString& Channel : DefaultChannels)
|
||
{
|
||
Channels.Add(MakeShared<FJsonValueString>(Channel));
|
||
}
|
||
Data->SetArrayField(TEXT("channels"), Channels);
|
||
Root->SetObjectField(TEXT("data"), Data);
|
||
|
||
FString Payload;
|
||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Payload);
|
||
FJsonSerializer::Serialize(Root, Writer);
|
||
SendJsonString(Payload);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::SendInitialSyncRequest()
|
||
{
|
||
SendSyncRequest(TEXT("initial_connect"), INDEX_NONE);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::SendSyncRequest(const FString& Reason, int32 InLastSequence)
|
||
{
|
||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||
Root->SetStringField(TEXT("type"), TEXT("sync_request"));
|
||
|
||
TSharedRef<FJsonObject> Data = MakeShared<FJsonObject>();
|
||
Data->SetStringField(TEXT("channel"), TEXT("ue_scene"));
|
||
Data->SetStringField(TEXT("reason"), Reason);
|
||
if (InLastSequence >= 0)
|
||
{
|
||
Data->SetNumberField(TEXT("last_sequence"), InLastSequence);
|
||
}
|
||
Root->SetObjectField(TEXT("data"), Data);
|
||
|
||
FString Payload;
|
||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Payload);
|
||
FJsonSerializer::Serialize(Root, Writer);
|
||
SendJsonString(Payload);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::RequestAccessToken()
|
||
{
|
||
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
|
||
Request->SetURL(BuildHttpUrl(TEXT("/api/v1/auth/login")));
|
||
Request->SetVerb(TEXT("POST"));
|
||
Request->SetHeader(TEXT("Content-Type"), TEXT("application/x-www-form-urlencoded"));
|
||
Request->SetContentAsString(
|
||
FString::Printf(TEXT("username=%s&password=%s"), *Username, *Password)
|
||
);
|
||
Request->OnProcessRequestComplete().BindUObject(
|
||
this, &UUeSceneWebSocketClient::HandleLoginResponse
|
||
);
|
||
Request->ProcessRequest();
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::HandleLoginResponse(
|
||
FHttpRequestPtr Request,
|
||
FHttpResponsePtr Response,
|
||
bool bSucceeded
|
||
)
|
||
{
|
||
if (!bSucceeded || !Response.IsValid())
|
||
{
|
||
EmitError(TEXT("登录请求失败。"));
|
||
return;
|
||
}
|
||
|
||
if (Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
|
||
{
|
||
EmitError(FString::Printf(TEXT("登录失败,HTTP %d"), Response->GetResponseCode()));
|
||
return;
|
||
}
|
||
|
||
TSharedPtr<FJsonObject> Root;
|
||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
|
||
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
|
||
{
|
||
EmitError(TEXT("登录响应 JSON 解析失败。"));
|
||
return;
|
||
}
|
||
|
||
AccessToken = Root->GetStringField(TEXT("access_token"));
|
||
if (AccessToken.IsEmpty())
|
||
{
|
||
EmitError(TEXT("登录响应里没有 access_token。"));
|
||
return;
|
||
}
|
||
|
||
OpenWebSocket();
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::OpenWebSocket()
|
||
{
|
||
if (!FModuleManager::Get().IsModuleLoaded(TEXT("WebSockets")))
|
||
{
|
||
FModuleManager::Get().LoadModule(TEXT("WebSockets"));
|
||
}
|
||
|
||
Socket = FWebSocketsModule::Get().CreateWebSocket(BuildWsUrl());
|
||
BindSocketEvents();
|
||
Socket->Connect();
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::BindSocketEvents()
|
||
{
|
||
if (!Socket.IsValid())
|
||
{
|
||
return;
|
||
}
|
||
|
||
Socket->OnConnected().AddLambda([this]()
|
||
{
|
||
OnConnectedChanged.Broadcast(true);
|
||
});
|
||
|
||
Socket->OnConnectionError().AddLambda([this](const FString& Error)
|
||
{
|
||
EmitError(FString::Printf(TEXT("WebSocket 连接失败: %s"), *Error));
|
||
});
|
||
|
||
Socket->OnClosed().AddLambda([this](int32 StatusCode, const FString& Reason, bool bWasClean)
|
||
{
|
||
OnConnectedChanged.Broadcast(false);
|
||
});
|
||
|
||
Socket->OnMessage().AddLambda([this](const FString& Message)
|
||
{
|
||
OnRawMessage.Broadcast(Message);
|
||
TryBroadcastSceneFrame(Message);
|
||
});
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::EmitError(const FString& ErrorMessage)
|
||
{
|
||
OnError.Broadcast(ErrorMessage);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::TryBroadcastSceneFrame(const FString& Message)
|
||
{
|
||
TSharedPtr<FJsonObject> Root;
|
||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
|
||
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
|
||
{
|
||
return;
|
||
}
|
||
|
||
FString Type;
|
||
if (!Root->TryGetStringField(TEXT("type"), Type) || Type != TEXT("data_frame"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
FString Channel;
|
||
if (!Root->TryGetStringField(TEXT("channel"), Channel) || Channel != TEXT("ue_scene"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
const TSharedPtr<FJsonObject>* DataObject = nullptr;
|
||
if (!Root->TryGetObjectField(TEXT("data"), DataObject) || DataObject == nullptr || !DataObject->IsValid())
|
||
{
|
||
return;
|
||
}
|
||
TSharedPtr<FJsonObject> EffectiveDataObject = *DataObject;
|
||
|
||
FString UpdateType = TEXT("unknown");
|
||
EffectiveDataObject->TryGetStringField(TEXT("update_type"), UpdateType);
|
||
|
||
int32 Sequence = 0;
|
||
EffectiveDataObject->TryGetNumberField(TEXT("sequence"), Sequence);
|
||
|
||
int32 TotalRecords = 0;
|
||
const TSharedPtr<FJsonObject>* PayloadObject = nullptr;
|
||
if (EffectiveDataObject->TryGetObjectField(TEXT("payload"), PayloadObject) && PayloadObject != nullptr && PayloadObject->IsValid())
|
||
{
|
||
const TSharedPtr<FJsonObject>* MetaObject = nullptr;
|
||
if ((*PayloadObject)->TryGetObjectField(TEXT("meta"), MetaObject) && MetaObject != nullptr && MetaObject->IsValid())
|
||
{
|
||
(*MetaObject)->TryGetNumberField(TEXT("total_records"), TotalRecords);
|
||
}
|
||
}
|
||
|
||
if (UpdateType == TEXT("full"))
|
||
{
|
||
TSharedPtr<FJsonObject> AssembledDataObject;
|
||
if (!TryAssembleFullScene(EffectiveDataObject, Sequence, TotalRecords, AssembledDataObject))
|
||
{
|
||
return;
|
||
}
|
||
if (AssembledDataObject.IsValid())
|
||
{
|
||
EffectiveDataObject = AssembledDataObject;
|
||
}
|
||
}
|
||
|
||
LastSequence = Sequence;
|
||
OnSceneFrame.Broadcast(UpdateType, Sequence, TotalRecords);
|
||
BroadcastScenePayload(UpdateType, Sequence, EffectiveDataObject);
|
||
}
|
||
|
||
bool UUeSceneWebSocketClient::TryAssembleFullScene(
|
||
const TSharedPtr<FJsonObject>& DataObject,
|
||
int32 Sequence,
|
||
int32 TotalRecords,
|
||
TSharedPtr<FJsonObject>& OutAssembledDataObject
|
||
)
|
||
{
|
||
OutAssembledDataObject = nullptr;
|
||
|
||
const TSharedPtr<FJsonObject>* PayloadObject = nullptr;
|
||
if (!DataObject->TryGetObjectField(TEXT("payload"), PayloadObject) || PayloadObject == nullptr || !PayloadObject->IsValid())
|
||
{
|
||
EmitError(TEXT("Full scene payload is missing."));
|
||
return false;
|
||
}
|
||
|
||
const TSharedPtr<FJsonObject>* MetaObject = nullptr;
|
||
if (!(*PayloadObject)->TryGetObjectField(TEXT("meta"), MetaObject) || MetaObject == nullptr || !MetaObject->IsValid())
|
||
{
|
||
EmitError(TEXT("Full scene meta is missing."));
|
||
return false;
|
||
}
|
||
|
||
bool bChunked = false;
|
||
(*MetaObject)->TryGetBoolField(TEXT("chunked"), bChunked);
|
||
if (!bChunked)
|
||
{
|
||
OutAssembledDataObject = DataObject;
|
||
PendingFullScene.Reset();
|
||
return true;
|
||
}
|
||
|
||
int32 TransportChunkIndex = 0;
|
||
int32 TransportChunkCount = 0;
|
||
int32 LayerChunkIndex = 0;
|
||
int32 LayerChunkCount = 0;
|
||
FString LayerName;
|
||
(*MetaObject)->TryGetNumberField(TEXT("transport_chunk_index"), TransportChunkIndex);
|
||
(*MetaObject)->TryGetNumberField(TEXT("transport_chunk_count"), TransportChunkCount);
|
||
(*MetaObject)->TryGetNumberField(TEXT("layer_chunk_index"), LayerChunkIndex);
|
||
(*MetaObject)->TryGetNumberField(TEXT("layer_chunk_count"), LayerChunkCount);
|
||
(*MetaObject)->TryGetStringField(TEXT("layer_name"), LayerName);
|
||
|
||
if (TransportChunkIndex <= 0 || TransportChunkCount <= 0 || LayerChunkIndex <= 0 || LayerChunkCount <= 0 || LayerName.IsEmpty())
|
||
{
|
||
EmitError(TEXT("Chunked full scene metadata is incomplete."));
|
||
PendingFullScene.Reset();
|
||
return false;
|
||
}
|
||
|
||
if (PendingFullScene.Sequence != Sequence)
|
||
{
|
||
PendingFullScene.Reset();
|
||
PendingFullScene.Sequence = Sequence;
|
||
PendingFullScene.ExpectedTransportChunkCount = TransportChunkCount;
|
||
PendingFullScene.TotalRecords = TotalRecords;
|
||
PendingFullScene.DisplayProfile = DataObject->GetObjectField(TEXT("display_profile"));
|
||
PendingFullScene.CameraState = DataObject->GetObjectField(TEXT("camera_state"));
|
||
PendingFullScene.Meta = MakeShared<FJsonObject>(*MetaObject->Get());
|
||
DataObject->TryGetStringField(TEXT("cluster_time"), PendingFullScene.ClusterTime);
|
||
}
|
||
|
||
if (PendingFullScene.Sequence != Sequence || PendingFullScene.ExpectedTransportChunkCount != TransportChunkCount)
|
||
{
|
||
EmitError(TEXT("Chunked full scene sequence mismatch."));
|
||
PendingFullScene.Reset();
|
||
return false;
|
||
}
|
||
|
||
if (PendingFullScene.ReceivedTransportChunkIndices.Contains(TransportChunkIndex))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
PendingFullScene.ReceivedTransportChunkIndices.Add(TransportChunkIndex);
|
||
|
||
const TSharedPtr<FJsonObject>* LayersObject = nullptr;
|
||
if (!(*PayloadObject)->TryGetObjectField(TEXT("layers"), LayersObject) || LayersObject == nullptr || !LayersObject->IsValid())
|
||
{
|
||
EmitError(TEXT("Chunked full scene layers are missing."));
|
||
PendingFullScene.Reset();
|
||
return false;
|
||
}
|
||
|
||
const TSharedPtr<FJsonObject>* LayerDataObject = nullptr;
|
||
if (!(*LayersObject)->TryGetObjectField(LayerName, LayerDataObject) || LayerDataObject == nullptr || !LayerDataObject->IsValid())
|
||
{
|
||
EmitError(TEXT("Chunked full scene layer payload is missing."));
|
||
PendingFullScene.Reset();
|
||
return false;
|
||
}
|
||
|
||
FLayerChunkAccumulator& LayerAccumulator = PendingFullScene.Layers.FindOrAdd(LayerName);
|
||
if (LayerAccumulator.ExpectedChunkCount == 0)
|
||
{
|
||
LayerAccumulator.ExpectedChunkCount = LayerChunkCount;
|
||
LayerDataObject->Get()->TryGetNumberField(TEXT("revision"), LayerAccumulator.Revision);
|
||
}
|
||
else if (LayerAccumulator.ExpectedChunkCount != LayerChunkCount)
|
||
{
|
||
EmitError(TEXT("Layer chunk count changed while assembling full scene."));
|
||
PendingFullScene.Reset();
|
||
return false;
|
||
}
|
||
|
||
if (LayerAccumulator.ReceivedChunkIndices.Contains(LayerChunkIndex))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const TArray<TSharedPtr<FJsonValue>>* LayerItems = nullptr;
|
||
if (!LayerDataObject->Get()->TryGetArrayField(TEXT("items"), LayerItems) || LayerItems == nullptr)
|
||
{
|
||
EmitError(TEXT("Chunked full scene items are missing."));
|
||
PendingFullScene.Reset();
|
||
return false;
|
||
}
|
||
|
||
LayerAccumulator.ReceivedChunkIndices.Add(LayerChunkIndex);
|
||
LayerAccumulator.Items.Append(*LayerItems);
|
||
|
||
if (PendingFullScene.ReceivedTransportChunkIndices.Num() != PendingFullScene.ExpectedTransportChunkCount)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
for (const TPair<FString, FLayerChunkAccumulator>& LayerPair : PendingFullScene.Layers)
|
||
{
|
||
if (LayerPair.Value.ReceivedChunkIndices.Num() != LayerPair.Value.ExpectedChunkCount)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
TSharedRef<FJsonObject> AssembledDataObject = MakeShared<FJsonObject>();
|
||
AssembledDataObject->SetStringField(TEXT("update_type"), TEXT("full"));
|
||
AssembledDataObject->SetNumberField(TEXT("sequence"), Sequence);
|
||
AssembledDataObject->SetStringField(TEXT("cluster_time"), PendingFullScene.ClusterTime);
|
||
if (PendingFullScene.DisplayProfile.IsValid())
|
||
{
|
||
AssembledDataObject->SetObjectField(TEXT("display_profile"), PendingFullScene.DisplayProfile);
|
||
}
|
||
if (PendingFullScene.CameraState.IsValid())
|
||
{
|
||
AssembledDataObject->SetObjectField(TEXT("camera_state"), PendingFullScene.CameraState);
|
||
}
|
||
|
||
TSharedRef<FJsonObject> AssembledPayloadObject = MakeShared<FJsonObject>();
|
||
TSharedRef<FJsonObject> AssembledMetaObject = PendingFullScene.Meta.IsValid()
|
||
? MakeShared<FJsonObject>(*PendingFullScene.Meta.Get())
|
||
: MakeShared<FJsonObject>();
|
||
AssembledMetaObject->SetBoolField(TEXT("chunked"), false);
|
||
AssembledMetaObject->RemoveField(TEXT("layer_name"));
|
||
AssembledMetaObject->RemoveField(TEXT("layer_chunk_index"));
|
||
AssembledMetaObject->RemoveField(TEXT("layer_chunk_count"));
|
||
AssembledMetaObject->RemoveField(TEXT("transport_chunk_index"));
|
||
AssembledMetaObject->RemoveField(TEXT("transport_chunk_count"));
|
||
AssembledPayloadObject->SetObjectField(TEXT("meta"), AssembledMetaObject);
|
||
|
||
TSharedRef<FJsonObject> AssembledLayersObject = MakeShared<FJsonObject>();
|
||
for (TPair<FString, FLayerChunkAccumulator>& LayerPair : PendingFullScene.Layers)
|
||
{
|
||
TSharedRef<FJsonObject> LayerObject = MakeShared<FJsonObject>();
|
||
LayerObject->SetNumberField(TEXT("revision"), LayerPair.Value.Revision);
|
||
LayerObject->SetArrayField(TEXT("items"), LayerPair.Value.Items);
|
||
AssembledLayersObject->SetObjectField(LayerPair.Key, LayerObject);
|
||
}
|
||
AssembledPayloadObject->SetObjectField(TEXT("layers"), AssembledLayersObject);
|
||
AssembledDataObject->SetObjectField(TEXT("payload"), AssembledPayloadObject);
|
||
|
||
OutAssembledDataObject = AssembledDataObject;
|
||
PendingFullScene.Reset();
|
||
return true;
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::BroadcastScenePayload(
|
||
const FString& UpdateType,
|
||
int32 Sequence,
|
||
const TSharedPtr<FJsonObject>& DataObject
|
||
)
|
||
{
|
||
FString PayloadJson;
|
||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&PayloadJson);
|
||
if (FJsonSerializer::Serialize(DataObject.ToSharedRef(), Writer))
|
||
{
|
||
OnScenePayload.Broadcast(UpdateType, Sequence, PayloadJson);
|
||
}
|
||
|
||
BroadcastLegacySubmarineCableEvents(DataObject);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::BroadcastLegacySubmarineCableEvents(
|
||
const TSharedPtr<FJsonObject>& DataObject
|
||
)
|
||
{
|
||
if (!DataObject.IsValid())
|
||
{
|
||
return;
|
||
}
|
||
|
||
const TSharedPtr<FJsonObject>* PayloadObject = nullptr;
|
||
if (!DataObject->TryGetObjectField(TEXT("payload"), PayloadObject)
|
||
|| PayloadObject == nullptr
|
||
|| !PayloadObject->IsValid())
|
||
{
|
||
return;
|
||
}
|
||
|
||
const TSharedPtr<FJsonObject>* LayersObject = nullptr;
|
||
if (!(*PayloadObject)->TryGetObjectField(TEXT("layers"), LayersObject)
|
||
|| LayersObject == nullptr
|
||
|| !LayersObject->IsValid())
|
||
{
|
||
return;
|
||
}
|
||
|
||
const TSharedPtr<FJsonObject>* CableLayerObject = nullptr;
|
||
if (!(*LayersObject)->TryGetObjectField(TEXT("submarine_cables"), CableLayerObject)
|
||
|| CableLayerObject == nullptr
|
||
|| !CableLayerObject->IsValid())
|
||
{
|
||
return;
|
||
}
|
||
|
||
const TArray<TSharedPtr<FJsonValue>>* CableItems = nullptr;
|
||
if (!(*CableLayerObject)->TryGetArrayField(TEXT("items"), CableItems) || CableItems == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (const TSharedPtr<FJsonValue>& CableItemValue : *CableItems)
|
||
{
|
||
const TSharedPtr<FJsonObject> CableItemObject = CableItemValue.IsValid()
|
||
? CableItemValue->AsObject()
|
||
: nullptr;
|
||
if (!CableItemObject.IsValid())
|
||
{
|
||
continue;
|
||
}
|
||
|
||
const FString CableName = ResolveCableName(CableItemObject);
|
||
|
||
const TSharedPtr<FJsonObject>* GeoObject = nullptr;
|
||
if (!CableItemObject->TryGetObjectField(TEXT("geo"), GeoObject)
|
||
|| GeoObject == nullptr
|
||
|| !GeoObject->IsValid())
|
||
{
|
||
continue;
|
||
}
|
||
|
||
const TArray<TSharedPtr<FJsonValue>>* SegmentArray = nullptr;
|
||
if ((*GeoObject)->TryGetArrayField(TEXT("segments"), SegmentArray) && SegmentArray != nullptr)
|
||
{
|
||
TArray<TArray<FVector>> MergedSegments;
|
||
for (const TSharedPtr<FJsonValue>& SegmentValue : *SegmentArray)
|
||
{
|
||
const TArray<TSharedPtr<FJsonValue>>* SegmentPoints = nullptr;
|
||
if (!SegmentValue.IsValid() || !SegmentValue->TryGetArray(SegmentPoints) || SegmentPoints == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
TArray<FVector> GeoPoints;
|
||
GeoPoints.Reserve(SegmentPoints->Num());
|
||
|
||
for (const TSharedPtr<FJsonValue>& PointValue : *SegmentPoints)
|
||
{
|
||
FVector GeoPoint = FVector::ZeroVector;
|
||
if (PointValue.IsValid() && TryParseGeoPoint(PointValue->AsObject(), GeoPoint))
|
||
{
|
||
GeoPoints.Add(GeoPoint);
|
||
}
|
||
}
|
||
|
||
AppendMergedSegment(MergedSegments, GeoPoints);
|
||
}
|
||
|
||
for (const TArray<FVector>& MergedSegment : MergedSegments)
|
||
{
|
||
if (MergedSegment.Num() >= 2)
|
||
{
|
||
OnSubmarineCableReceived.Broadcast(CableName, MergedSegment);
|
||
}
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
const TArray<TSharedPtr<FJsonValue>>* PathArray = nullptr;
|
||
if (!(*GeoObject)->TryGetArrayField(TEXT("path"), PathArray) || PathArray == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
TArray<FVector> GeoPoints;
|
||
GeoPoints.Reserve(PathArray->Num());
|
||
for (const TSharedPtr<FJsonValue>& PointValue : *PathArray)
|
||
{
|
||
FVector GeoPoint = FVector::ZeroVector;
|
||
if (PointValue.IsValid() && TryParseGeoPoint(PointValue->AsObject(), GeoPoint))
|
||
{
|
||
GeoPoints.Add(GeoPoint);
|
||
}
|
||
}
|
||
|
||
if (GeoPoints.Num() >= 2)
|
||
{
|
||
OnSubmarineCableReceived.Broadcast(CableName, GeoPoints);
|
||
}
|
||
}
|
||
}
|
||
|
||
FString UUeSceneWebSocketClient::BuildWsUrl() const
|
||
{
|
||
FString WsBase = BackendBaseUrl;
|
||
WsBase = WsBase.Replace(TEXT("http://"), TEXT("ws://"));
|
||
WsBase = WsBase.Replace(TEXT("https://"), TEXT("wss://"));
|
||
return FString::Printf(TEXT("%s/ws?token=%s"), *WsBase, *AccessToken);
|
||
}
|
||
|
||
FString UUeSceneWebSocketClient::BuildHttpUrl(const FString& Path) const
|
||
{
|
||
return FString::Printf(TEXT("%s%s"), *BackendBaseUrl, *Path);
|
||
}
|
||
|
||
void UUeSceneWebSocketClient::SendJsonString(const FString& JsonPayload)
|
||
{
|
||
if (!Socket.IsValid() || !Socket->IsConnected())
|
||
{
|
||
EmitError(TEXT("WebSocket 尚未连接。"));
|
||
return;
|
||
}
|
||
|
||
Socket->Send(JsonPayload);
|
||
}
|