From 8e762850cde4ffda3cf28a253b67f5663b7ccfa9 Mon Sep 17 00:00:00 2001 From: Marc Garcia Puig Date: Fri, 30 Nov 2018 21:09:47 +0100 Subject: [PATCH 1/5] Fixed an issue where the OpenDrive was generated twice --- .../Carla/Source/Carla/OpenDriveActor.cpp | 16 +++++++++++++++- .../Plugins/Carla/Source/Carla/OpenDriveActor.h | 9 +++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp index 12f8da8e6..05b647636 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp @@ -39,9 +39,23 @@ void AOpenDriveActor::BeginDestroy() void AOpenDriveActor::OnConstruction(const FTransform &transform) { Super::OnConstruction(transform); - BuildRoutes(); + } +#if WITH_EDITOR +void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& e) { + Super::PostEditChangeProperty(e); + FName PropertyName = (e.Property != NULL) ? e.Property->GetFName() : NAME_None; + + if (PropertyName == GET_MEMBER_NAME_CHECKED(AOpenDriveActor, bGenerateRoutes)) { + if (bGenerateRoutes) { + bGenerateRoutes = false; + BuildRoutes(); + } + } +} +#endif + void AOpenDriveActor::BuildRoutes() { std::string parseError; diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h index 29f60e5e2..b44802fa9 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h @@ -27,6 +27,11 @@ private: TArray RoutePlanners; +#if WITH_EDITOR + UPROPERTY(Category = "Generate", EditAnywhere) + bool bGenerateRoutes = false; +#endif + public: // Sets default values for this actor's properties @@ -40,6 +45,10 @@ public: virtual void OnConstruction(const FTransform &transform) override; +#if WITH_EDITOR + void PostEditChangeProperty(struct FPropertyChangedEvent&); +#endif + ARoutePlanner *GenerateRoutePlanner(const TArray &waypoints); TArray GenerateLaneZeroPoints( From cbd28cc5d1e304a3c16ca9acbb10424cd3fe599c Mon Sep 17 00:00:00 2001 From: marcgpuig Date: Mon, 3 Dec 2018 11:00:49 +0100 Subject: [PATCH 2/5] Fixed package making + formating to UE4's standard --- .../Plugins/Carla/Source/Carla/OpenDriveActor.cpp | 15 +++++++++------ .../Plugins/Carla/Source/Carla/OpenDriveActor.h | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp index 05b647636..14fb581a7 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp @@ -39,16 +39,19 @@ void AOpenDriveActor::BeginDestroy() void AOpenDriveActor::OnConstruction(const FTransform &transform) { Super::OnConstruction(transform); - + } #if WITH_EDITOR -void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& e) { - Super::PostEditChangeProperty(e); - FName PropertyName = (e.Property != NULL) ? e.Property->GetFName() : NAME_None; +void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); - if (PropertyName == GET_MEMBER_NAME_CHECKED(AOpenDriveActor, bGenerateRoutes)) { - if (bGenerateRoutes) { + const FName PropertyName = (e.Property != NULL) ? e.Property->GetFName() : NAME_None; + if (PropertyName == GET_MEMBER_NAME_CHECKED(AOpenDriveActor, bGenerateRoutes)) + { + if (bGenerateRoutes) + { bGenerateRoutes = false; BuildRoutes(); } diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h index b44802fa9..484517707 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h @@ -27,7 +27,7 @@ private: TArray RoutePlanners; -#if WITH_EDITOR +#if WITH_EDITOR_DATA UPROPERTY(Category = "Generate", EditAnywhere) bool bGenerateRoutes = false; #endif @@ -46,7 +46,7 @@ public: virtual void OnConstruction(const FTransform &transform) override; #if WITH_EDITOR - void PostEditChangeProperty(struct FPropertyChangedEvent&); + void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent); #endif ARoutePlanner *GenerateRoutePlanner(const TArray &waypoints); From 4b998bbebce5bc8d4ce6a98a81847c5292161b2c Mon Sep 17 00:00:00 2001 From: marcgpuig Date: Tue, 4 Dec 2018 16:23:20 +0100 Subject: [PATCH 3/5] Major improvment on OpenDriveActor usage --- .../Carla/Source/Carla/OpenDriveActor.cpp | 370 ++++++++++++------ .../Carla/Source/Carla/OpenDriveActor.h | 91 ++++- .../Source/Carla/Traffic/RoutePlanner.cpp | 11 +- .../Carla/Source/Carla/Traffic/RoutePlanner.h | 2 + 4 files changed, 323 insertions(+), 151 deletions(-) diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp index 14fb581a7..7fc8eb6e8 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp @@ -7,7 +7,6 @@ #include "Carla.h" #include "OpenDriveActor.h" -#include "Algo/Reverse.h" #include "Util/OpenDrive.h" @@ -15,78 +14,163 @@ #include #include -AOpenDriveActor::AOpenDriveActor() +#include + +TArray DirectedPointArray2FVectorArray( + const TArray &DirectedPoints) { - PrimaryActorTick.bCanEverTick = true; + TArray Positions; + Positions.Reserve(DirectedPoints.Num()); + for (int i = 0; i < DirectedPoints.Num(); ++i) + { + Positions.Add(DirectedPoints[i].location); + } + return Positions; } -void AOpenDriveActor::BeginPlay() +AOpenDriveActor::AOpenDriveActor(const FObjectInitializer& ObjectInitializer) : + Super(ObjectInitializer) { - Super::BeginPlay(); + PrimaryActorTick.bCanEverTick = false; + + // Structure to hold one-time initialization + struct FConstructorStatics + { + // A helper class object we use to find target UTexture2D object in resource package + ConstructorHelpers::FObjectFinderOptional NoteTextureObject; + FName ID_Notes; // Icon sprite category name + FText NAME_Notes; // Icon sprite display name + FConstructorStatics() + // Use helper class object to find the texture + // "/Engine/EditorResources/S_Note" is resource path + : NoteTextureObject(TEXT("/Engine/EditorResources/S_Note")) + , ID_Notes(TEXT("Notes")) + , NAME_Notes(NSLOCTEXT("SpriteCategory", "Notes", "Notes")) + { + } + }; + static FConstructorStatics ConstructorStatics; + + // We need a scene component to attach Icon sprite + RootComponent = ObjectInitializer.CreateDefaultSubobject(this, TEXT("SceneComponent")); + RootComponent->Mobility = EComponentMobility::Static; + +#if WITH_EDITORONLY_DATA + SpriteComponent = ObjectInitializer.CreateEditorOnlyDefaultSubobject(this, TEXT("Sprite")); + if (SpriteComponent) + { + SpriteComponent->Sprite = ConstructorStatics.NoteTextureObject.Get(); // Get the sprite texture from helper class object + SpriteComponent->SpriteInfo.Category = ConstructorStatics.ID_Notes; // Assign sprite category name + SpriteComponent->SpriteInfo.DisplayName = ConstructorStatics.NAME_Notes; // Assign sprite display name + SpriteComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform); // Attach sprite to scene component + SpriteComponent->Mobility = EComponentMobility::Static; + } +#endif // WITH_EDITORONLY_DATA } void AOpenDriveActor::BeginDestroy() { - for (int i = 0; i < RoutePlanners.Num(); ++i) - { - RoutePlanners[i]->Destroy(); - } - - RoutePlanners.Empty(); + RemoveRoutes(); Super::BeginDestroy(); } -void AOpenDriveActor::OnConstruction(const FTransform &transform) -{ - Super::OnConstruction(transform); - -} - #if WITH_EDITOR -void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& Event) { - Super::PostEditChangeProperty(PropertyChangedEvent); + Super::PostEditChangeProperty(Event); - const FName PropertyName = (e.Property != NULL) ? e.Property->GetFName() : NAME_None; + const FName PropertyName = (Event.Property != NULL ? Event.Property->GetFName() : NAME_None); if (PropertyName == GET_MEMBER_NAME_CHECKED(AOpenDriveActor, bGenerateRoutes)) { if (bGenerateRoutes) { bGenerateRoutes = false; BuildRoutes(); + if (bAddSpawners) + { + AddSpawners(); + } + if (bShowDebug) + { + DebugRoutes(); + } + } + } + if (PropertyName == GET_MEMBER_NAME_CHECKED(AOpenDriveActor, bRemoveRoutes)) + { + if (bRemoveRoutes) + { + bRemoveRoutes = false; + RemoveDebugRoutes(); + RemoveSpawners(); + RemoveRoutes(); + } + } + if (PropertyName == GET_MEMBER_NAME_CHECKED(AOpenDriveActor, bShowDebug)) + { + if (bShowDebug) + { + DebugRoutes(); + } + else + { + RemoveDebugRoutes(); + } + } + if (PropertyName == GET_MEMBER_NAME_CHECKED(AOpenDriveActor, bRemoveCurrentSpawners)) + { + if (bRemoveCurrentSpawners) + { + bRemoveCurrentSpawners = false; + RemoveSpawners(); } } } -#endif +#endif // WITH_EDITOR + +ARoutePlanner *AOpenDriveActor::GenerateRoutePlanner(const TArray &DirectedPoints) +{ + TArray Positions = DirectedPointArray2FVectorArray(DirectedPoints); + ARoutePlanner *RoutePlanner = GetWorld()->SpawnActor(); + + RoutePlanner->SetActorRotation(FRotator(0.0f, CarlaMath::to_degrees(DirectedPoints[0].tangent), 0.0f)); + RoutePlanner->SetActorLocation(DirectedPoints[0].location); + RoutePlanner->SetBoxExtent(FVector(70.0f, 70.0f, 50.0f)); + RoutePlanner->AddRoute(1.0f, Positions); + RoutePlanner->Init(); + RoutePlanners.Add(RoutePlanner); + return RoutePlanner; +} void AOpenDriveActor::BuildRoutes() { - std::string parseError; + // Avoid OpenDrive overlapping + RemoveRoutes(); + + std::string ParseError; // NOTE(Andrei): As the OpenDrive file has the same name as level, // build the path to the xodr file using the lavel name and the // game content directory. - FString mapName = GetWorld()->GetMapName(); - FString xodrContent = FOpenDrive::Load(mapName); + FString MapName = GetWorld()->GetMapName(); + FString XodrContent = FOpenDrive::Load(MapName); - auto map_ptr = carla::opendrive::OpenDrive::Load(TCHAR_TO_UTF8(*xodrContent), + auto map_ptr = carla::opendrive::OpenDrive::Load(TCHAR_TO_UTF8(*XodrContent), XmlInputType::CONTENT, - &parseError); + &ParseError); - if (parseError.size()) + if (ParseError.size()) { - UE_LOG(LogCarla, Error, TEXT("OpenDrive parsing error: '%s'."), *carla::rpc::ToFString(parseError)); + UE_LOG(LogCarla, Error, TEXT("OpenDrive parsing error: '%s'."), *carla::rpc::ToFString(ParseError)); return; } const auto &map = map_ptr->GetData(); - std::vector junctionInfo = map.GetJunctionInformation(); + std::vector JunctionInfo = map.GetJunctionInformation(); - /////////////////////////////////////////////////////////////////////////// // NOTE(Andrei): Build the roads that are not junctions - auto RoadIDsView = map.GetAllIds(); - std::vector roadIDs(RoadIDsView.begin(), RoadIDsView.end()); + std::vector roadIDs(RoadIDsView.begin(), RoadIDsView.end()); std::sort(roadIDs.begin(), roadIDs.end()); for (auto &&id : roadIDs) @@ -94,193 +178,183 @@ void AOpenDriveActor::BuildRoutes() GenerateWaypointsRoad(map.GetRoad(id)); } - /////////////////////////////////////////////////////////////////////////// // NOTE(Andrei): Build the roads that are junctions as one RoutePlanner // can have more than one path that can be taken // junctionId roadID laneID - std::map>> junctions; + std::map>> Junctions; - for (size_t i = 0; i < junctionInfo.size(); ++i) + for (auto && Junction : JunctionInfo) { - TArray> waypoints; + TArray> Waypoints; - int fromRoadID = junctionInfo[i].incomming_road; - int toRoadID = junctionInfo[i].connection_road; - int junctonID = junctionInfo[i].junction_id; + int FromRoadID = Junction.incomming_road; + int ToRoadID = Junction.connection_road; + int JunctonID = Junction.junction_id; - GenerateWaypointsJunction(map.GetRoad(toRoadID), waypoints); + GenerateWaypointsJunction(map.GetRoad(ToRoadID), Waypoints); ARoutePlanner *routePlanner = nullptr; - std::sort(junctionInfo[i].from_lane.begin(), junctionInfo[i].from_lane.end()); - if (junctionInfo[i].from_lane[0] < 0) + std::sort(Junction.from_lane.begin(), Junction.from_lane.end(), std::greater()); + + if (Junction.from_lane[0] < 0) { - std::reverse(junctionInfo[i].from_lane.begin(), junctionInfo[i].from_lane.end()); + std::reverse(Junction.from_lane.begin(), Junction.from_lane.end()); } - for (size_t n = 0; n < junctionInfo[i].from_lane.size(); ++n) + for (size_t n = 0; n < Junction.from_lane.size(); ++n) { - int fromLaneID = junctionInfo[i].from_lane[n]; - routePlanner = junctions[junctonID][fromRoadID][fromLaneID]; + int FromLaneID = Junction.from_lane[n]; + routePlanner = Junctions[JunctonID][FromRoadID][FromLaneID]; if (routePlanner == nullptr) { - routePlanner = GenerateRoutePlanner(waypoints[n]); + routePlanner = GenerateRoutePlanner(Waypoints[n]); routePlanner->SetSplineColor(FColor::MakeRandomColor()); - junctions[junctonID][fromRoadID][fromLaneID] = routePlanner; + Junctions[JunctonID][FromRoadID][FromLaneID] = routePlanner; } else { - routePlanner->AddRoute(1.0, waypoints[n]); + routePlanner->AddRoute(1.0, DirectedPointArray2FVectorArray(Waypoints[n])); } } } +} - /////////////////////////////////////////////////////////////////////////// - - for (int i = 0; i < RoutePlanners.Num(); ++i) +/// Remove all the existing ARoutePlanner and VehicleSpawners previously +/// generated by this class to avoid overlapping +void AOpenDriveActor::RemoveRoutes() +{ + const int rp_num = RoutePlanners.Num(); + for (int i = 0; i < rp_num; i++) { - RoutePlanners[i]->DrawRoutes(); + if (RoutePlanners[i] != nullptr) + { + RoutePlanners[i]->Destroy(); + } } + RoutePlanners.Empty(); } -ARoutePlanner *AOpenDriveActor::GenerateRoutePlanner(const TArray &waypoints) +TArray AOpenDriveActor::GenerateLaneZeroPoints( + const RoadSegment *road) { - ARoutePlanner *junctionRoutePlanner = nullptr; + size_t LanesOffsetIndex = 0; + TArray LaneZeroPoints; - ARoutePlanner *routePlanner = GetWorld()->SpawnActor(); - routePlanner->SetActorLocation(waypoints[0]); + const RoadGeneralInfo *generalInfo = + road->GetInfo(0.0); + std::vector> LanesOffset = generalInfo->GetLanesOffset(); - routePlanner->SetBoxExtent(FVector(70.0f, 70.0f, 50.0f)); - routePlanner->AddRoute(1.0f, waypoints); - routePlanner->Init(); - - RoutePlanners.Add(routePlanner); - return routePlanner; -} - -TArray AOpenDriveActor::GenerateLaneZeroPoints( - const carla::road::element::RoadSegment *road) -{ - size_t lanesOffsetIndex = 0; - TArray laneZeroPoints; - - const carla::road::element::RoadGeneralInfo *generalInfo = - road->GetInfo(0.0); - std::vector> lanesOffset = generalInfo->GetLanesOffset(); - - for (float waypointsOffset = 0.0f; waypointsOffset < road->GetLength() + 2.0; waypointsOffset += 2.0) + for (float WaypointsOffset = 0.0f; WaypointsOffset < road->GetLength() + RoadAccuracy; WaypointsOffset += RoadAccuracy) { // NOTE(Andrei): Calculate the which laneOffset has to be used - if (lanesOffsetIndex < lanesOffset.size() - 1 && - waypointsOffset >= lanesOffset[lanesOffsetIndex + 1].first) + if (LanesOffsetIndex < LanesOffset.size() - 1 && + WaypointsOffset >= LanesOffset[LanesOffsetIndex + 1].first) { - ++lanesOffsetIndex; + ++LanesOffsetIndex; } // NOTE(Andrei): Get waypoin at the offset, and invert the y axis - carla::road::element::DirectedPoint waypoint = road->GetDirectedPointIn(waypointsOffset); - waypoint.location.z = 1; + DirectedPoint Waypoint = road->GetDirectedPointIn(WaypointsOffset); + Waypoint.location.z = 1; // NOTE(Andrei): Applyed the laneOffset of the lane section - waypoint.ApplyLateralOffset(lanesOffset[lanesOffsetIndex].second); + Waypoint.ApplyLateralOffset(LanesOffset[LanesOffsetIndex].second); - laneZeroPoints.Add(waypoint); + LaneZeroPoints.Add(Waypoint); } - return laneZeroPoints; + return LaneZeroPoints; } -TArray> AOpenDriveActor::GenerateRightLaneWaypoints( - const carla::road::element::RoadSegment *road, - const TArray &laneZeroPoints) +TArray> AOpenDriveActor::GenerateRightLaneWaypoints( + const RoadSegment *road, + const TArray &laneZeroPoints) { - const carla::road::element::RoadInfoLane *lanesInfo = - road->GetInfo(0.0); + const RoadInfoLane *lanesInfo = + road->GetInfo(0.0); std::vector rightLanes = - lanesInfo->getLanesIDs(carla::road::element::RoadInfoLane::which_lane_e::Right); + lanesInfo->getLanesIDs(RoadInfoLane::which_lane_e::Right); - TArray> retWaypoints; + TArray> retWaypoints; double currentOffset = 0.0; for (size_t j = 0; j < rightLanes.size(); ++j) { - const carla::road::element::LaneInfo *laneInfo = lanesInfo->getLane(rightLanes[j]); - currentOffset += laneInfo->_width * 0.5; - TArray roadWaypoints; + const LaneInfo *laneInfo = lanesInfo->getLane(rightLanes[j]); + const float HalfWidth = laneInfo->_width * 0.5; + currentOffset += HalfWidth; if (laneInfo->_type == "driving") { + TArray roadWaypoints; for (int i = 0; i < laneZeroPoints.Num(); ++i) { - carla::road::element::DirectedPoint currentPoint = laneZeroPoints[i]; + DirectedPoint currentPoint = laneZeroPoints[i]; currentPoint.ApplyLateralOffset(-currentOffset); - roadWaypoints.Add(currentPoint.location); + roadWaypoints.Add(currentPoint); } - if (roadWaypoints.Num() >= 2) { retWaypoints.Add(roadWaypoints); } } - - currentOffset += laneInfo->_width * 0.5; + currentOffset += HalfWidth; } - return retWaypoints; } -TArray> AOpenDriveActor::GenerateLeftLaneWaypoints( - const carla::road::element::RoadSegment *road, - const TArray &laneZeroPoints) +TArray> AOpenDriveActor::GenerateLeftLaneWaypoints( + const RoadSegment *road, + const TArray &laneZeroPoints) { - const carla::road::element::RoadInfoLane *lanesInfo = - road->GetInfo(0.0); - std::vector leftLanes = lanesInfo->getLanesIDs(carla::road::element::RoadInfoLane::which_lane_e::Left); + const RoadInfoLane *lanesInfo = + road->GetInfo(0.0); + std::vector leftLanes = lanesInfo->getLanesIDs(RoadInfoLane::which_lane_e::Left); - TArray> retWaypoints; + TArray> retWaypoints; double currentOffset = 0.0; for (size_t j = 0; j < leftLanes.size(); ++j) { - const carla::road::element::LaneInfo *laneInfo = lanesInfo->getLane(leftLanes[j]); - currentOffset += laneInfo->_width * 0.5; - TArray roadWaypoints; + const LaneInfo *laneInfo = lanesInfo->getLane(leftLanes[j]); + const float HalfWidth = laneInfo->_width * 0.5; + currentOffset += HalfWidth; if (laneInfo->_type == "driving") { + TArray roadWaypoints; for (int i = 0; i < laneZeroPoints.Num(); ++i) { - carla::road::element::DirectedPoint currentPoint = laneZeroPoints[i]; + DirectedPoint currentPoint = laneZeroPoints[i]; currentPoint.ApplyLateralOffset(currentOffset); - roadWaypoints.Add(currentPoint.location); + roadWaypoints.Add(currentPoint); } - if (roadWaypoints.Num() >= 2) { Algo::Reverse(roadWaypoints); retWaypoints.Add(roadWaypoints); } } - - currentOffset += laneInfo->_width * 0.5; + currentOffset += HalfWidth; } return retWaypoints; } -void AOpenDriveActor::GenerateWaypointsRoad(const carla::road::element::RoadSegment *road) +void AOpenDriveActor::GenerateWaypointsRoad(const RoadSegment *road) { - const carla::road::element::RoadGeneralInfo *generalInfo = - road->GetInfo(0.0); + const RoadGeneralInfo *generalInfo = + road->GetInfo(0.0); if (generalInfo->GetJunctionId() > -1) { return; } - TArray laneZeroPoints = GenerateLaneZeroPoints(road); + TArray laneZeroPoints = GenerateLaneZeroPoints(road); - TArray> rightLaneWaypoints = GenerateRightLaneWaypoints(road, laneZeroPoints); - TArray> leftLaneWaypoints = GenerateLeftLaneWaypoints(road, laneZeroPoints); + TArray> rightLaneWaypoints = GenerateRightLaneWaypoints(road, laneZeroPoints); + TArray> leftLaneWaypoints = GenerateLeftLaneWaypoints(road, laneZeroPoints); for (int i = 0; i < rightLaneWaypoints.Num(); ++i) { @@ -294,17 +368,17 @@ void AOpenDriveActor::GenerateWaypointsRoad(const carla::road::element::RoadSegm } void AOpenDriveActor::GenerateWaypointsJunction( - const carla::road::element::RoadSegment *road, - TArray> &out_waypoints) + const RoadSegment *road, + TArray> &out_waypoints) { - const carla::road::element::RoadGeneralInfo *generalInfo = - road->GetInfo(0.0); + const RoadGeneralInfo *generalInfo = + road->GetInfo(0.0); if (generalInfo->GetJunctionId() == -1) { return; } - TArray laneZeroPoints = GenerateLaneZeroPoints(road); + TArray laneZeroPoints = GenerateLaneZeroPoints(road); out_waypoints = GenerateRightLaneWaypoints(road, laneZeroPoints); if (out_waypoints.Num() == 0) @@ -312,3 +386,47 @@ void AOpenDriveActor::GenerateWaypointsJunction( out_waypoints = GenerateLeftLaneWaypoints(road, laneZeroPoints); } } + +void AOpenDriveActor::DebugRoutes() const +{ + for (int i = 0; i < RoutePlanners.Num(); ++i) + { + if (RoutePlanners[i] != nullptr) + { + RoutePlanners[i]->DrawRoutes(); + } + } +} + +void AOpenDriveActor::RemoveDebugRoutes() const +{ + FlushPersistentDebugLines(GetWorld()); +} + +void AOpenDriveActor::AddSpawners() +{ + for (int i = 0; i < RoutePlanners.Num(); ++i) + { + if (RoutePlanners[i] != nullptr) + { + FTransform Trans = RoutePlanners[i]->GetActorTransform(); + AVehicleSpawnPoint *Spawner = GetWorld()->SpawnActor(); + Spawner->SetActorRotation(Trans.GetRotation()); + Spawner->SetActorLocation(Trans.GetTranslation() + FVector(0.0f, 0.0f, SpawnersHeight)); + VehicleSpawners.Add(Spawner); + } + } +} + +void AOpenDriveActor::RemoveSpawners() +{ + const int vs_num = VehicleSpawners.Num(); + for (int i = 0; i < vs_num; i++) + { + if (VehicleSpawners[i] != nullptr) + { + VehicleSpawners[i]->Destroy(); + } + } + VehicleSpawners.Empty(); +} diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h index 484517707..ed1fd2101 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h @@ -9,8 +9,14 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" +#include "Components/SceneComponent.h" +#include "Components/BillboardComponent.h" + #include "Traffic/RoutePlanner.h" +#include "Vehicle/VehicleSpawnPoint.h" + #include #include #include @@ -23,48 +29,91 @@ class CARLA_API AOpenDriveActor : public AActor { GENERATED_BODY() +protected: + // A UBillboardComponent to hold Icon sprite + UBillboardComponent* SpriteComponent; + // Sprite for the Billboard Component + UTexture2D* SpriteTexture; + private: + UPROPERTY() TArray RoutePlanners; -#if WITH_EDITOR_DATA + UPROPERTY() + TArray VehicleSpawners; + +#if WITH_EDITORONLY_DATA + UPROPERTY(Category = "Generate", EditAnywhere) bool bGenerateRoutes = false; -#endif + + UPROPERTY(Category = "Generate", EditAnywhere, meta = (ClampMin = "0.01", UIMin = "0.01")) + float RoadAccuracy = 2.0f; + + UPROPERTY(Category = "Generate", EditAnywhere) + bool bRemoveRoutes = false; + + UPROPERTY(Category = "Spawners", EditAnywhere) + bool bAddSpawners = true; + + UPROPERTY(Category = "Spawners", EditAnywhere) + float SpawnersHeight = 300.0; + + UPROPERTY(Category = "Spawners", EditAnywhere) + bool bRemoveCurrentSpawners = false; + + UPROPERTY(Category = "Debug", EditAnywhere) + bool bShowDebug = true; +#endif // WITH_EDITORONLY_DATA public: - // Sets default values for this actor's properties - AOpenDriveActor(); + using RoadSegment = carla::road::element::RoadSegment; + using DirectedPoint = carla::road::element::DirectedPoint; + using LaneInfo = carla::road::element::LaneInfo; + using RoadGeneralInfo = carla::road::element::RoadGeneralInfo; + using RoadInfoLane = carla::road::element::RoadInfoLane; + using IdType = carla::road::element::id_type; + using CarlaMath = carla::geom::Math; - void BuildRoutes(); - - virtual void BeginPlay() override; + AOpenDriveActor(const FObjectInitializer& ObjectInitializer); virtual void BeginDestroy() override; - virtual void OnConstruction(const FTransform &transform) override; + void BuildRoutes(); + + void RemoveRoutes(); + + void DebugRoutes() const; + + void RemoveDebugRoutes() const; #if WITH_EDITOR void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent); -#endif +#endif // WITH_EDITOR - ARoutePlanner *GenerateRoutePlanner(const TArray &waypoints); + ARoutePlanner *GenerateRoutePlanner(const TArray &waypoints); - TArray GenerateLaneZeroPoints( - const carla::road::element::RoadSegment *road); + TArray GenerateLaneZeroPoints( + const RoadSegment *road); - TArray> GenerateRightLaneWaypoints( - const carla::road::element::RoadSegment *road, - const TArray &laneZeroPoints); + TArray> GenerateRightLaneWaypoints( + const RoadSegment *road, + const TArray &laneZeroPoints); - TArray> GenerateLeftLaneWaypoints( - const carla::road::element::RoadSegment *road, - const TArray &laneZeroPoints); + TArray> GenerateLeftLaneWaypoints( + const RoadSegment *road, + const TArray &laneZeroPoints); void GenerateWaypointsJunction( - const carla::road::element::RoadSegment *road, - TArray> &waypoints); + const RoadSegment *road, + TArray> &waypoints); + + void GenerateWaypointsRoad(const RoadSegment *road); + + void AddSpawners(); + + void RemoveSpawners(); - void GenerateWaypointsRoad(const carla::road::element::RoadSegment *road); }; diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp index 51c03d4bc..f914a6449 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp @@ -65,6 +65,12 @@ ARoutePlanner::ARoutePlanner(const FObjectInitializer &ObjectInitializer) SplineColor = FColor::Black; } +void ARoutePlanner::BeginDestroy() +{ + CleanRoute(); + Super::BeginDestroy(); +} + #if WITH_EDITOR void ARoutePlanner::PostEditChangeProperty(FPropertyChangedEvent &PropertyChangedEvent) { @@ -93,6 +99,7 @@ void ARoutePlanner::AddRoute(float probability, const TArray &routePoin { USplineComponent *NewSpline = NewObject(this); NewSpline->bHiddenInGame = true; + NewSpline->EditorUnselectedSplineSegmentColor = FLinearColor(0.15f, 0.15f, 0.15f); NewSpline->SetLocationAtSplinePoint(0, routePoints[0], ESplineCoordinateSpace::World, true); NewSpline->SetLocationAtSplinePoint(1, routePoints[1], ESplineCoordinateSpace::World, true); @@ -191,10 +198,6 @@ void ARoutePlanner::DrawRoutes() #if WITH_EDITOR for (int i = 0, lenRoutes = Routes.Num(); i < lenRoutes; ++i) { - FVector boxCenter = Routes[i]->GetLocationAtSplinePoint(0, ESplineCoordinateSpace::World); - boxCenter.Z += i * 101.0f; - DrawDebugBox(GetWorld(), boxCenter, TriggerVolume->GetUnscaledBoxExtent() - FVector(10.0f, 10.0f, 10.0f), SplineColor, true); - for (int j = 0, lenNumPoints = Routes[i]->GetNumberOfSplinePoints() - 1; j < lenNumPoints; ++j) { FVector p0 = Routes[i]->GetLocationAtSplinePoint(j + 0, ESplineCoordinateSpace::World); diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.h b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.h index 4e453a28d..a34bff259 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.h +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.h @@ -26,6 +26,8 @@ public: ARoutePlanner(const FObjectInitializer &ObjectInitializer); + virtual void BeginDestroy() override; + void Init(); void SetBoxExtent(const FVector &Extent) From c0e874c2e6cf682227e5b172a4130746050dd02f Mon Sep 17 00:00:00 2001 From: marcgpuig Date: Wed, 5 Dec 2018 19:39:01 +0100 Subject: [PATCH 4/5] Fixed problem generating the package + new icon --- .../Content/Icons/OpenDriveActorIcon.uasset | Bin 0 -> 35911 bytes .../Carla/Source/Carla/OpenDriveActor.cpp | 61 ++++++++++-------- .../Carla/Source/Carla/OpenDriveActor.h | 10 +-- .../Source/Carla/Traffic/RoutePlanner.cpp | 3 + 4 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 Unreal/CarlaUE4/Plugins/Carla/Content/Icons/OpenDriveActorIcon.uasset diff --git a/Unreal/CarlaUE4/Plugins/Carla/Content/Icons/OpenDriveActorIcon.uasset b/Unreal/CarlaUE4/Plugins/Carla/Content/Icons/OpenDriveActorIcon.uasset new file mode 100644 index 0000000000000000000000000000000000000000..44d6ba8cc42887c1f05b8e12d042df5f22f05999 GIT binary patch literal 35911 zcmc$_WmH^Evnaf07+`RBm*5^W*dT$BV8PurXmFR{PJ|GG2M7?{-4op1HMqOO9iH>P z=bU@K`>l2V-CDD!cUMFHL z0F*rLp(0|VvxS3c0No!my zcCt0*lrwd(cji=bG`E*_vUW3%E8vm87MaiH+WL8 zgtN1`i=3UKgOiK2v5PUVRrpp71CEz;aIjT&a&RnG9j z)|OT-*7lbFZVTpS~mPsRKX&Hkq>iMfg$yeIVkw_BtwU@DrwS&E)v7NcIqp_(ufa~HJ{-pYUW%|#rO=ZhAf6|<)wU@aT(EX96`QI^Z zz&~1P=1Eomp+7)FsI2DcOI2N*TuuK=D_lUz-BXeNrEDH);Nc!Ng85&Ve`SBg=>PE_ zI#n4%Nm&&MK>#U#hvcas8t^9N;KU2;H5X_-we}}?!auf z|CC1lKhmDs#J}O+^#2b3d)mK-2jBtD|KIda&Ens(o^Uz;N_gr`|2Ob&IdGQ$$@Aa& z{wMBfym^AB?~wTC4FFhl;r}G(-&y`m^H2O=5ndPoM8JDA1}f=OW5AGmC9U%B5<#Duo}N>l#nS*K zCoQ4o0owOP^HKj(b@|~4N=w=+MT-?f{XLQvzZsZ#EfFGv0R8aRr!_~V$WP0rYHGDg zQ~dtmWuyj&9nLXYB=i}QXG9)$6;qs~1SCdej5a<%Ij<2+lj2Y$WV!$R_I=vSW|M~B z=Oj^Yi-CUsbnR0E1ZsLH6#OB8n)?4i5Fwi(L0--U9rJ)FSn}!*^Gee_ea#>=<>jHi zmD`VND&E5^Ik|dd=X-+hFQSf@D~Q`*eh7mtfjWg$^_efEU72&Xp7rkwrGLcQzsp1< zYI~Nl1WeKWQY}{fx=RjX${Js{jO^kH8pGAaex(VyF=}%fu6;r8s0&2@L4PppBfx6$~)^DKJF#}e0O0Tf7AtrGFHm(B-AlUAmBwWGR z-VnV4oADgSC@Tbnk8^lY2E`|aa55-pR#R%c+h*dhB^liMLi=q?ikz(k3>z96!0I|R zKm#l7?-ho}Ur9ow0s0l$k8CL5V@VhU))@cQ1rkUv4S&V;!|(Y-jm-(C_>E>QC;R_` zxRoFWET_fc|F!U$S=F-#>{VO{4- z(_yNwgGS2Wx5Z0HEw`g_^7hsmZG)LOIm1q{`Z7-r6GB@AN2vlswJ|c~))*Z1mv)zb z{zzW_Nwt)X>ihoSkIV{DOAYaB1LnZ}xtfj@e@gzlKP%BN!ON6CBJyr-)nNnrbcZL) zU8liPcMR@8LGg^OJ~`DC zclsNy4W-W`k7(yy7|YQ}XBD5Jk;4P%p^M*cL*=0K*qrh&>X>Km(J?NS{jR%{M9-{bv;f77)iH1``SkfUtoE-_}a9ap8@ z+@x!BJ6AIlpJ0^-Udz^hhd;s?CYaeFK+Qwh74bs7dlaJX3JDg(C()8%Va0xZvoKeR zlX@MoH6R%Yrl21)yPV~4vJ$68)v!DZ-g2vg2HY5Z>#nuatwOxk5jY*o zaV8R&c^0hs8V#8LhNC+Cy~c`C^`Tv)8d^CJvwy^1qH17^hK+o{%ns;O+monD6LBw{Iz!~OpC-msr*H}@ag1OE3$Z(AT!#n-cNku6SL9~?{3(;8kXx3Y*SDmdRx4Te#c38A zFeG-<|LoPBE&qcn-CjOEXjzt?Pk;A;kPLM6(0i$hhs{1^O$X%3+h2mSku&a9ROYv< zv0sJtDy8%0Ai^eASn>*rR4pcU)Q{G`a)PYTOOn0r9OKAERpO>``94C)MV;^!?{55qXAm?U1+Tq;l}JQ$Ad9Dc3YftzZY4PVfmlY* z(Q;Kbk7pI%tgfymR7?};i8G#4Wl#dWyB~{CX+RDzTpiAoia%{4cO?wK7fmY?LbFs# z4;j+%MG|)34`!HA$dE!dbZA}a`-q*%w@Sp&1e6STVYu1NAujk2Q@k4-XBsP$lC>JE z$Ee($#;mqqM4gzJz|{NAs*T+(CQe4mXPu2+^!WJjXG!W|{W^XD;vSiic^J8~x_q6R z9#${o>gc)I7pU`|KZbOsrjt#l9?EdZal{d=_Y5w^hrJve`?qMUMc}3ryA93R{GNQ6$>gDBFqmWPB`t@y&KYRm4FJ9C9?k^_tQQM(B}F0Zhn|5+UGSE$Qm_Tl8zC6`z2%U0b8QK7 znZu*l^ktl{a+H7Ky1vCx!J;9Cz=99n-fS6~+oNyI3TNzJSU~-w-Zisq9*3fNG|QuE zizliM@01_eS?BCp%^CHQ?n7GS(4!UfBmG%4*>x%55=I)zT3HDyD?6zxE_&RDPXp{= z%U>I{mk5+1F4$I|wwB)>O275n^yzM4p=w=12p%9vp1{i1ba=#7iL>!$(r8J$vJvz- z<5Kg{Z!1g~@E%l1JHZx)u(d3QQTlYx?{{7DIvd`2F&An#An8=SLoAy8A(6~fV_wYc zpuVJ$Z&?rhdz9JHGrpR9vOOske!>024EF`Zv6mSoAT*}mG{D)ii8c)qK)a8e9o#y> z1mXQ9q;HyrH(dXPmq=MnEjt8BlRxGdjx~ z`Kv}R3dqnZR$tu5`glW>5lTNE5gGb|hVCzfCJ?wp#=2HsFYpP(Z6hd-Kg*wf3nDQ5 z-f;fgb|KW2>-tIlMHY@79(1U?-s&FpZ_ce>(QpYs-krd6PY~T(P?zzWUuE<3M~Qyi_NY=y>G;Gh3Z}rgT&S!lF|$WO zZ*cvVSm&VVzS4kZytiOFM&hrzQ+x)1?-&H^<{|3Hjd+q?@fc(Rt?57SbtSWY*|gs4fW}724__QjQy+WBW9QAk-M&1k`+x$g>us#})e8eW!5JZ=T5+D5;=RUApvn>*Lz0F6a z#-{`qN7MPpSL>z9WIN%w#NM)99S-f8^>!PYa((HPd|~`-jY{>^5(B*;wU$jDA)D89XdB!)@gg#P>lc zfiyK3$8BpB(!jUl@S_MsU~$wYQZB*&EVY(AS?H=>#oZigbVGvZPt9Jch!<2aRf6<_ z<=wN{D2@&gE6cfmb@kovGFwIMdlE}(WE=p;?+jLe5EvZLk0K3qEM1=7csrFItvG)3 z87Gn))ECb0gp@=WGaMO-?p9lZMXPyB+2 zZqb{db+(9o-XA!sC^9aG%laU+bjtb4w<@vK(Y=#OQ4N?44wm zykFIacv_D&=Un&e+x*3fpEasst2p?9)+O>mpnK$R?eW9<6@D>2N|f!g)ON85VTNYz zl9b|p`|wKi5#m zx-oiNcZBer;V}q)pdFO> z@1;S=zKm#9rGZ{x_aAnj0-MH%oX2kO=KmGvTNNBz012UM#`IN zmGOdDgzMJWW6*_02lzi;`dP>PQKJ+#^gUlhy(38xg+mB$5#{=nI{Ysh7IcN^=naGI>#hkij$0H}u3dR|HVHw>;&--XHTG>nph^DhCJcJWMx!XQG}H=a ztysVOXZD6u>x0dMRq(t7Yy;Sd%5so@y{5ubsg$u{7V{m8+I`GIp>3=+3mqk_o*^&l z!D;;EVKSqYOEFG#tuJJ_NG9yjl{l74)V?2ixDobq0A;utWpa0^MakdeuBWSsm3dq( z^LoOxMdg-j=XPe4y15@jzbl(f38=d<(x#p%vQNL^T-%pIKoWY{qt%k>{w?l?L%UkK zsNRf?nwzN>9b^Kql$p^i^O8duAZCKg_tYIoYhMj{U$9@F;&wgL6pgN>lY;Mz%sgJ= zzaXWdb_N@Sr|S6cfS`3z0Z_pGxbl5g1G2mQ9EuASXi2U$Fpu%5XI3d-5TP`4tJ(m`g-F@aAFs*xB@Mzvw){=~$2=c}o;e#C%rt&s(`7N!@d8k;UQZx9DjH?^ zDjGRV7<{{1HPeDGVA(ZYHh~4v_yd>wxMG3GtAS<*qmE6oXv_Dtad3rRa$`i9Qq#ab z2BCoJf#($YhUtF8?l42$5&l9#kP*V4msmIe#|IK-R$Q=4Kz9TfB~aU;|APx5m}1VI zcUkK^QGFDFYdAbc3Ielr#7JgkCeW_2pcqdpRu7$O>Pfq6H=Azstgxm6wh6Pjx%FTF z3b%Ogg6G!TgTs~UiW%S2z^1gVJ>J(+s>X2@ey)iN!nO(SSf;WSXFL+{<_xr_ z=MRPvTW3pR@0Kl%K64XA%~y_9>&=1&&|mAYL1G_v^VntEf?+S12;N#gu(PyepUn9o zaUWxYu<^9AOkYa|sKv_CLR54ruy?Mpq_f2(m@c5lg)E&S3`d zs_G;g|7x*65_r`fS6$*oHOyzG@x!JEY)_`xCKO0GpiBI+G4l{3bWSep$FT}|F)cN? zRb6nwl<*Z)8I>NA63&3+aCaP|Sn^v5+YxRR2%ux@S&@?rIG}a;G2$NMCi3#CJ&H3s z8ha~#PJ&=Rl?X*;T`Q^^I~iKIDI`$;VDdF~J_>MMIjbur8r6u5nw)+O+s%nQ&_&cS07Ge&NcrYkR+VF zzD#{5XmqUMjy)RNW|qz2sV_e>Tj3tJtxahX1?xluRO(*-e$BYqA}U@T-tb#A9-I0? z{JtiAmH}zIA3zD*y)oES!QAQk>09gzcGO%izTr-o#-WQJ0aG_`^+H$2H*A% zXON`5mY}z~GZd*LR-_O@9MIu?6S4GrvZSUxY4-VpP#_)r87)`I6H-N(IzO(t2@z)w zE|k$e+8Ux#UoV)}#<_IbDe|wd@Jqp>5X?S!7403$BiI&w0D>>vh_&KUW1F2TZ-lZx zDL(8sq-;XAlt+G@s2^?5Dk+E`O9;TH*N3Z5J)9(z7wY?OLT$~|D0wshav?Wq->tV< zGD%`rA95B8a<@%l)qcR+0E<5t&2WwTQY}$M$Fo}Sa#qxwe!rr*zx$r>ScS-M!oX;J zVEhCxrfIDF?`Fq$Z%{rHr226FfHh>vgl75jxE?;l7j*d}Mqq;{8@ruNu)768%Brde z&zzf{Kb|gjx}H`*8|@uAjM`2ROU>4CYYVQ5fBPi7Qkgi7J%}t}HM*_df0v?UdcXHG zPZ$^oRUli=UlPfgoJK+5?~ug|Gkm<5g1Mi3j6fSHiM2@YwsKkBLdh#A81wl}Vgun( z1cvt;q_Z3@W6u#lB0Fw}!vV3=KS|a1@&Y0`_dk386*oV@*L`nAhkqDS+JqlhDMP&m zX@&0?jN5)7_6FaSXMg*E@9^`-$pB2!==^3vA|#+WiVI)tMC=@=n+a4}?vrz4lBwhW zP?$fDmcb)U2ch)10)PcpOpI1Qi$XQ zDnm5Sx?!!aWYCjaupb%Iw!(OuGXi z0U>bfMpl+r8QiXP6!uA6OR{U@xHS-=%$QBDLz0>f0>rJz#-WNG6G6Etd9t&xwQ7v@ zm1!IYCZ-@Co-sTSRn}57_=My?2b0hMwdaPVzvsRSOW{l>GGbU041jCM+7tTWVSefW_+hHYeETI*fyt9 zjaLEQXCK?yi&68q#=cX%4W_*hH1U&QqX*%|iu-fBdlJ|S9{JxsGvWmMO-p;O_~6~g zYOy@*+6@+WcpqY*|JnE^=j1uzaru}A;P_@=rmw_5`fZJ;I~4a(jbR4_-{9PZwSb*G zKcED9A2txp6M|DXW@sTUye5bC%O<##M4m5)dTUzdoqLlhcI*6f4r7DM$;^{KOr;bB z(#bZiO@3w9{l1v9ijYtun@(xabf*SrY@pu&QO!x6nLW;3|N{!1R~zGt=f{HCIf z12Idv8SbD`4WsW@$@~)~#d}pVC4tRqWZReJIf>1b#p4T=d!T5xlj_0AtmZr23{MdM zUhdpotS#eeBvr}#GF8g?v$;w)JT>(hP2^s27UW#-n@H3sqt4yuKL)|)BEg>!K=6HJ z36tCyLdIk|zHdB`K^V(&67DUNXZuNqje(R>Z~$w#7o_PoO8q-Iu-787E#f;~>DIza zSVH=ja7y!6iyNxn0 zL2;&ETWkOnWf4i9Uw;BVu1lx;U04$f8fBckIrksO%d4wKI(e~?6xr^6UWUD#IF4rJ zEvMVn@shl=$4w{D5tY%eOty6(X6ljTc5hZSgYf&|p9bIQ{iCxnr2^rN;@%e2ZY{YM zn_?4hL^dWq2TWQ&O+#0{`S+U?ktKX$O^nX_d{ZpKoSaw}hYMJ@EelwihK^$7;+}dr zSiH5gQoDO{@|HuXhzn+(9^TUlz2*tWWH%_1>xi8&Wwo=lGG^yXK zKSZoI;qHMJ{V5>zB{hkWj2|A1`(9qzi4KS2juFEW;)XLRJf?!#j3R3ZB+4$%X6Ejz zLn82Lw^y;uG&+EF4QfcaCWZ`q^WNc0v+;*lRAW1wucoN(18fvMx=I7TBImnKSzoa) zM!>#HEoTNiEbjcIY5#WH+4{pgs;P+5%X18f@;uZsq#X%b*GCBF>AX-s{N7cwK)F`_+RGfcutL z`8cVI(i7c)-IJiWepI;GVZ0P)>*rmEQ_flDDP^Y_=X6+@<0X zzq5Uf?1r=`f}M6hWJ%FcyRTi3ok;rLlA6Rx7-wH@Sd$^8oxX?)bLSV9T1WZiS0Rg! z!5L%9?>dtO86Dpo2?g%#ypv7a({_LuoneGQ&Au+q?V!ge%e&EjK z)X!P|+^`iG3L)qJ%8#M2k$96xQ)E5T96aitf*8VQTP`}ry_F{FO={{kNOkTpo)X|N z_9gc5s^NWlYr3Vga`Hl!J688@eD0n1eFo84bFsj@M@NiI?fkqR^chohYtp7;&VfqT zi~Izr-W}n zz~_+8d)pLM{B>DYMtQP_>cG@ceO@gu5E3!B6a6_kVAsj4tM+p;)Y|tO`o3uHd-LOP zldHp?M?0O4*q-lY=9si8+&t@qFb7AcD;p`g7}MO zTk9$|zx32mBNuP`#kAdy_Hl-2{%rYV!t{r|VOta#K42zzYSB3dM z2Zhg>LzMj(8HLpSW`mS=JI?d(q#P(v&8)_-?I?ff+t<^GFRDo!dr-kDnLfUIpNqi9 z%nHE{&*%Wnvi)r^eA~qqkDc*)sB(8#U-`Js5Qu*a(3J+b#L7zblU^N}_gBfas^&K0 z`Tb55h*Vyua=s)s<@BRB8XgenIQShT?BUVnMKKY^D#ly(qeINcJ>#|E+!oKx!N`&E zb(rJcG`DI>23E6N|Fwf@!6DZ}OZM8cU*6r{zkeyhsRS`~MF z(R_?V%!?3gWqfYeqh?9+}QT9LfA4g1?c6uz^$DWpg}mp_rq{l3Ct z0Q^$U-v91AZz6C}G{$FV>GJ{Y-bg{%aV&WyPE_fd0eEF0i*bjpw%Z*Q2XSL9P8`$M z%UeJjN6hq)3=J)jUCoDfq1*hmNE79JVM8&s6qwp1NSqiec{TITG?{?v`QkgQ>RCTL z8B(od(digXByYU5m5$Ib3M&72%7ra+QXRqZ=eaJiVIH4oY97SM1=47F3E%@8l~B6l zXrv}_%I?uJrkr``690Twa@FqC_daj*GO3^@gb)hjRCl>F@xktuw56saVLHr}=QXy) z$cg?55t>OV)D-Cl2p7Vqq^v$m8SZex#oO$AzPr~+SG=lcjbV zxP@^&O2=byQ)$QxO;I$^ob_w|WV#n9(&y^yz@@!_g-&!r5c!WW(ivF>!&jsStBjNS zi&>lNgz{@-SuEbqN;dPIZImbDS04e7qTk*V#M{V5Ihpuu*dGCE6Pk?N#BwQ+p|Kov z_}u2jKPaQ3)~v&qGy%dGF~$K{OMXqaZKiSJ7X38C*a5)-e9dVZp>+%JN`vLryL2&9j^+tAlzDC<% zT{H{ReX#aV*_dfEtzo8v8v%Gjr9{|J6AgKSZN5z?8xM4V$~^@GECodlIsB&747{{X zr})xVruPM+`GZ^a1AZR4FL-KBUojw1at|Vumx$Lfzua1rmS+OcpCxPY<~be)@k~9*xLP!s+wTxm0zNY;T@cFcuAJ{J;xRn9{dhZ`$9 zOX>aFW6)97gMc3L3S|j7PpUzE|aY+8joS^hU=Ed!r4CVKc zsR&o>6YqO{>YC#FuN~PZ_d$9{vn@)(G(~E4xwtW~wYECANm z?Ue2a2Tk=g1Dzcha|Fz*eXo9t0x$TzcPjk{(yx^JNQ;5i8MC}LahpD+1pmq83bksd z!OPgX355FsORU!AAk2iRk6A99#Gb5_P{|c+d!f=ftjYTmFHUL#ZvZ`fECtqgs{3mS z>uwd)mDbv8&Kk%fHij{^kg=83OjohUVvohg}`1ervneS zlEd0qJ3G#ON>bq{N*+~+`PAXa3f1K*8osy_ujrqX!#4oIm>?@=1;(3|Fx6{qDslFf zZ8e@2`sReCM8TmgXM-&(o+QVH(?;gdHQ{~YzwXTDrk_iFr`)D5*7vGd8 zFp!z3ONbq_U8$1DAP*0B276mLuf72plS{7){bqL@e1Cc(yaSz8xsMJhF}yV+0FU3` zt;qb0*XD5}ctSGX&)=j^cO6wQ>>NGC?G`l& z_dLZ#V{2CICyl#4Z4C6Q4%i^PqQ$joRP(W)ujL+IPyZ~lA;MkDuT_h8hF-<%+X!%Qp<7L}!Li$zwk728b>YLH8 z#ypi@UWL~0D<*J%;&q{EJ;iTiBZ-dV(|-~2Ef!xM8vkPiZptbB_QLKL$|qgq1=J9M z+e2^OrY~XfPWIuRDy99^G@iRzp^g0gMkAA0%UiF+lPSa8b{ty3 zMFe;Kr-bh-sV?z}G@seTxX@T{lMoaVtgcZ?9zhF!^gl6Q(vr{gx+~?^TbmwSd=eCD zw-@+UIUK}%yP}vE_345R&8Zy60J7WS)CQTaOI*!?`93J$_naxqBen!!NZ$I^QK|ulA&cj^!Dqma0k>LqYwCybRq^xP zTyPWS)>5H)MZh7rX?eH%{8G{H=C&xgW+$$*!OvP`W(17N7o}Auto_NtL;U2_ zqJbT<7-5kE78AzfyX6;+D&%RkX5t6nR_lB5aqIxbVZy<4_lfI%|5~QEHR3wOc}>N@(v7rv_~Rf{`M_i2(0lCe$>` zYInKmzOy~LcVKGeA@x&0FUaF^FXU#eQmt>vd2%4PBCjYvWQo`m5%|`NEdh+{=y*Y%&l8g&mc9vn}j0&`_X0$@J@%jTL_x&gH z6y0Ox(fH9b-xzI-3hKvSK3P`k-c5;NY`V4R9QOX)dM+z3I~(o>uXobI85F*^dTyrX z-fdSJ&9w6@QzkQEb1yr&SgDU3`>J?gl8F5N8mGKqEecLuau2fFxcN#`Bi-5ifh%S^ zzY0y7U~a&44z@Bnl74}rws-W^kO-_c8I$f1oaFxv1<(Kah?>hNqr`FdunZmTESV(3 zC}ncQ`%sbW-`La?&4`DF-ND?xzAZ#3fpCl8hxp*uFGTxIG{sk5^_AlEGyD;zSJvpY^urfSjOrek5^>`Y2WzXNn)HNxB3q{+96_ z=3wD#UXNjf>#5lFlkh4ABo_YVX|>@S6lhv=>A_o=r>NL9!?1LLiCqg=fowi!S=0X>dv}4NUXCol~czQdtuPfq2P{48@U<3K2 z)AjFJ$weH8zh;@6_Ze;9zOj3#mqoAw>fM;`Q)`=`Tl=cBCOo)tlcCPzg5-dtqFCB< z=amSiSXr;*4{!Z6AJIKZ9h1&tgJCsk3-Y2M7IBn7i-BZj4EeSrBbrv^iVqsF;yH{8t28c?o$1a0mSa0mx z-$2!r0)k<=ZmIH)M-QmJLm|uohk+rm<0BK-rbg9o@@G-E^ck-G4o$DxZ*Z9Ib_fb+ zN_^Am`NBINdda134f!UQLFt=`g90&}2uPKU8Qjm3U$KanX9L^&|&fr{n5o84K zfL!+9Ene5aeH2wofg6;PgW_5zX_#@Hmz{@-Q=c?_qi(@gieVTpHor2< zzhE&aMr7O4#^#&Omf0)vbjZK@xm(}<_tj3lT!LEm`}~uz9TT03QpZ?*Si%6e z4XnCzdu~p9fe!_=_UpM#Ho6Z34bb#n)4V}Xbz2TR{y_164 z^+tV;ph~j2-CHs+AXlJivre-p2qn(L8=T!90mID3j1EKhA<6YKRqok+#(dHGXNy z9YV*{;?0k7wxZE(Op;w+OR7B>9=fEHIl%aWX$_I^L!`clI8J9s?k|!DH4}EP-JHhm zgI=)vxtFo9rhB?ae(v$}DD54qF*ICzkjQxhlzhnDUA`uDr%)=m)!P>;NW7oPtyx`b z@!2CeS_6f0pyEsSro!&HxBt?r{`p;)Z&^I8b}a{u_vZx%tO}{q+vJq_w=Ik5)lFkk zzBtoe?J#E)j$a<_D4pxQfABukY%JwN(|wP6+KZp;XUIUaj0dnm3ctzR?co90jiJG? z<5H8)*srJuzqvSl+zyM=RvkJE%scr^BXD~fwj8M`0uG#5bQWB|R7?7%$;+QRbn#ivrqKjqQF>GlkUarV|&u{IpD&ndLO+J6z_WRoz zL-M+ds>aq#Rdb4T@J4=3WJe)B22Vuh>IM_>hYe+anU!ANiS)Ik)C=?iEOj!^%C8~m zaDV?OmDN5@k#>a)2BKiv^uwW&*0OoU#tQHt1C`~md90tjEBoPlat=M3^~5spBR!ex zJR+mny-}6-22Phc2LxVqzD(5dOU-s-O7+A~wXo>f`c=~d-yTjHAYOMx6*fFrg?xZ^ z9ltdX?6t~i_HF75o-EhxFyC<=NFs%f<2jPy)s&>V3;RbbTYe55zsbTby*AzWLgjXg zQB=HA`M{D^;=1mMxbf%WE@UI=%nH$${umvjwVASYm}YTNp(o{2fcfTKxk3|g@aGFV zhiXwv6*A1&I+kXB5%+YEKACH56OwoCe;txYcb(GK@y;1Mj`oVW`l-@U(0^Ug16)xc z3=D+A&lYX%1@2;?Rw>Xg-k*Y3Vri1Fk~QcBS?3&0s7jAX6<47d2aBB(O>OAX|ykFbh16eSp*LkkJ!5kUdz3$=P7 z^Cl_W;Z=Yj{J1u4nS0JfgBVKp8^cE1_17FhTq^~QRwd12ffT*SB)M``)V36c9~I$q z%=a0O5e>sep;v!0FOR9yv=UGxEGy@#?a<7ESA$v)xk;X3ELhzicWMqD^e#{~o^fXf zuNU)QLh`BvV)}ePOU}Pm@-g{gxo)TtI>DSFGOQQ6IAI}Xo7fa2ioW+OzUZ!Ws?8I1 z5+OsbUkGi(<4BgYJUY5);%6hyqK3aGQV6Slm5lRC9!RU9H2Sv5-y*-@X>dX0%jdIu zL=g(dS^=dLaz2gTi{avz`*n+`5#G1f{^De&dolXQAL(S^3rhmeBkWjX<#>5U`!&3L`roN))hS4oM*}pV+$?2>mHlbese2y zVctgp>RMp&LfCA4{Lnjk=o@){p>X>PWr6@5zPUmlh`3c`A3?euE=#{Sd9sf2tVaLw z=GRPWC^e8`v0b-XVW;q!Rcz>N?d|CD^;+b9#d+@#IKYGC12edh$)R!f$E8Z4=iu*6 z4`{&cqrz7Z@=0723kwS17dyETX)`^p9t(pyPNmrHAQJnO7U+rmnl>D%HPK-q*Yv5* zC%+*T)%3xI7$ z=G}@rPs*F;d6(c-EgEb3aHJ~%g6e*&3dy0-lj5&I)O{Z=mOiNiwfyKuD8Re>=lVdr zOx%lKW_oqq`)c`gCuqYeD&|Q-zCZ1**>M+BPhTYKIIs42VwtUdRZ<$sz+Uhv-oxZK zmh20aXB+NjXd{t(2nyj-q_}~^(x{Q2;72RfH`+}6O2|EQwa^Dqm0W)lmm25yt*JwE z)Y?woQ=m5Z=3E6JGcq}9jc+dZ%->^R`dl*-U(Ql$HuA_TZD)8G8Vs%0 z20Fz3wQ@2avj$|CJnp=f&JyL8e=5jnjZnCvoOI@BnSFNUAiutN#WtNwKGcD19oef` z(sV)^KU3+6=P~$}0#kuZYzY{96r|V^i*)%BPJs4f{1zDuzfeF;u}|f9XB?!(`nNSg z;M@5I+iP5>!J)VOk8~Ipi_fyHCU~Z{}=DMt$fO7?zMdsh3vh?6MY~-Yoo(>Xp%}^jjx*LdxW^ zPUVo+{6rKV(1p8Aowh>>Xy;=Hnd8EZ#wG*{&e%i&){4<*Ib83?nOfYV5nI2_bqQ+t z3- z#uD}VZJ%Pxd$xxhCI2DA#VvoQnnJ{?MW0`5KI7043UkIT^rk$965qj3M<5P8PMw05 zx>iH@#77a1vg9Q*){oybmsUYl0Uc^XJpjVV8q* z3*M;Vc4v3LDP@DR~C$&9v5o^@6nk2_cjn{T!O3jXOr<-jW`sxVWKER~^ z<*Vp(8$|&UINwCY=H7o1SdthP>?`L`B1`d=E`YjX?DQwQvw=T!M6yGFlYsui;)tk( z0>VBnYnh9K}f+@gWqrSfojdwga@{9yEq%tAAleU@;BdJ z6#kZ6=Tb`)8NXFd%5%6=zN7OsG!&s#P-@TR1E24M&}CfsvxL4*GRdt#0(oCjt9j$* zP3s^1@wV#T_$scfbxZ_-UwwN?O$ve~_#r3c&RsUN{Tk9pij?s_SIBnF>{^ToQzaaD z-h_EE+^Fq5tuB|0aX(&Zt@?;iPkFb5)!r!3v6hsWh-{A5D}O8{dNgwrD1e1dpw^Ur zc0ylk-Ky|VT03$S(Ul%RKvi;d`*NFQOC9JF*Rzi;D$*H_GI%T?+MBiO>?fBhO|{}ucmy+bciXwMGb3j3Im4x!#W1J(OEtmEx$F} z*@y7yHcy+x#x-DACz|AW1TJn?4jz}HAUn*?yt+2S3op4sevD-fz_KWN!0#akC?2kH zV@4lTwbe<~cPbXYAu>TEJ6~35$CA3H(Eg}4fYB_U_40|2*CAJI45IcNF4GVxQNGJp zS68=IYD+CFpz&bxWQ79Kv_ReoYh?9_^PRFAeL}kDpEuC{B;l~n(a98Y>zncs*hcif zF+*X$oq<4>M}gbsKy`oZ5Zpg(sz{51*t3<^Stq5gry8|4vjqqnxt^yRN1Z7GXdHc8c2~yu(q|VDPp&LDH*V=pMUcXp9=fKax-coO-_E$; zST*L^Ocj@2ztJ%(Q@!qW%9{^r;z~i-x^D53jI;5Y27km3pEA#>{{4q;A6q&1z8^vF z5KsSGV2&#<@6XLSmu?Cs^6Sg3#g5Lj10(nPuvAX$Dw%I*tJwk8!#Q*ssBR^c+xs=6 zngoW3f0fL&u0LL{z9p@ZEqL^A=Va9SdP0JdUCP=|oUFP?STX*22HGm9FSU}OPQO&AC_J=B^`y&3K8epw zsJ*9LdOY&&J~WvaYIpcU2-$8YlR#A?n=j)*~Qw*oyj>Bv|yXe|jPMO9Xwl@+|VTG-u4&VJ{uh@xm2 zP7XN-1fvRfz2eN8H=9hQ1XRh2l6Grq3(N|$*DCc}Kcbc&xG%%xHNn~J;0vmkqLHM> zNW&raW@G-8OWl)qw_$ajnn3eDs~{nPuZ9m*mB33SAsCQ;0AY}cA?ar_||Cgije zUe6!lh46v^gM)`s4w`*Zj*s!O{&F94&Hr6`@N?AdWhT!i$ktFq>?CLGf3^_!zS*aC7flOcNB=>T&l5|073xQMv=)C-K~~QZK5K zN1Ov)tECYS#@Nc2%f2$wmxwl_ zfDm1A?>g7r-z<9jbj7B#;(%DpMDR61wp_e(y9l|({*qcJy~o?T9Iso;b#J? z#`im=$uwS3U8>lJJ}15;sqbFo^*e>0s%rTeG56O^>i#s>j}Plh1ySrm(e~m3*FxW} z3L-)^qX-2J>Avhhhnw+Io7=+j>t%iCG1WP!g~v21tftQMc|Je*2r#wS*d{izxJ&b| z$eRCXfSAbDYO_$^2Hqnh`7m;hA_ZD5u*rpA^nAfTzulg)%?j? zJ=Yu9zOhYZsaD^LV{rd-fvS5opv#}!zJ;~qdzs4aGZDD_`SawRG^v5oQjh*-eh`c- zAASzjzK>Jinj&1ouMzj+aE@xL>MBYB9xBPSNT^~YrGJDUSS+i#LT5Hz&X1sTgk~LkS zQL<%ZN$qLhGu9wIIanY9{p8Z{(?hxnV9UOcc%oW8#cj1s-f@g>NNUI5#LA1^h8XG` zh4FhHL6EiD(%(ptw4*Kn$cS(PPWSFQ9-!~RFz`4T{73`^>MAX}p}*g)#g!_&OXn(I z8wLThtpE4FrBg;;lJ^$&C_g(^PXs{HR@YN2Rk4b?f)BU?4EXOT6aNcz3=ICi-~|2` z#s6a+R|p?h=m%=f?)DxJLBNcMz!mU>xTI7d_yVo~GqkK9{tM2;$=bui-t8X{7-Jtl zd*DBTh=8{Q)Xa<}|GD6Q1~+*E7Tya%u^ZXDftVcr_8QJ^_8uTE2Rlj*#V#x?^56ap zoNcerO8zS_7GWRlT>=B>yRqwUrbX zleDw578SR)ww1Dz5|gx-6ce|XwidOvcd)l(M}^%7u24&q?d+~_Rm}eBwz0D19Up72 z|6E}HKNMW?Mps`=#o7AdKj0Liz!ikVzrk5R_T9Gky#hvg_zzQ!RV~%*9jyJ_eE-EO zXE$GapMRBT{I`;;Mu1gn`gr2ya|1F--wf{Jo z@ju@G4F_@s1M^?f6~nIR-xvJ51y{oVUlMQK!W9Y;_c4SGc(M7-^9t7Iimvc)uIRsG zp@5BN_}6#;bs^>ds_?4UV0h0z^na@Ms_<%5ujp!e{bz#wvl3i=b2SIA=t?lJ=t`RY zA(sDm|93yH;ApPs|8f6szh3^Q|AHV2Q^UI+&wuy#N<^>d-=F{C{1rIN75&e(|AWKy zucH5}{}`ozH}hX-UG+Uo`CqU9@w)#v$SKbWm=@HAaRedf)L%5x@YFJeXW8P1sE|kl zw5&5NI(SoEj_H1|CQ?0EK3JCg$uD(kL+TDVHZc*O`1n^uBOUj+TfrK4!{&MTxZQ0B zZclN~UyUnyk=8g-D|%59dN>JQ9Hbrd|Ha4lPMjc@^qim3#al;@p005JaT&*}Hc{Ko zs>`0cjvXsb~E}`9TwCbWq?+^Cs_XD41^<=t42|eOFxNWyKr#dRG^^HLx=?>2k$m7@p3THWgh4 zoYHH$=@FkBfX}4IX}%>wV@*k4=DZ0Adx+_6%^BIPg$%Rn)cA8pRDy20A+4Jh7)Q zW&jU|O4>_4$3vT!gj}>sEdO>kFr|;;T>0$5O zgY%U;4KtS)ha0T~tiFS9Zaw_;EBHgmpB%?29g(Jm^bYo1laH)DPJJI9?DHSSNjJ8B zm)~sk_*gUI<68Rp%{{9~{He?Hnaj@+7rO-;bS+1*xnj0)4QD$&(;O)`eqU^-MATiL zEh*Vwx3~Ldw_t3OYoBGFe%Nw(-u+m9Bm2h1(Y*MZd+nRyhYbsVU#95M-uE3)!>V%q zD&ntW`r_ZXDCDWlt)HJ~;X9z-og~scb~2y(;c)7qzPSCj%!T`#!FSp|nNZ|uNxa8@ zg6?&Fu=~vJ^J11xY0JgQ!s1g)kIB!$c!$42Hk-QZS4(pnH~h*zbTnuk-j!#cx2hd? z(D9#i|Fn?6liy#u>2wL=#BjdN^SQY*M;;+Vu~9^HZuD`$GB)MOc*y?nokP51u-TrP z%(*Sgqb(PHmE#V%T*!3(#*7*^7K+f_+?hP4;J-!PtQTAHp`QZwEYrp)=@s9;CUSI!Wzbs;Zu;s$ME`Bp~NxpmPVV}a9UhN47MbQ^J z%f_V~rAy`Wz<@!fEQwQtF{%+$k}rI-E${wc=uB2d|5yIW3k_uB8z~c zlSO4-!||WokPF&G7B$hMkx$Kj*|59)V@gB6$k4AU^6DBFP3hSo*k)-}fcziHp zU+xCswE-Gd55G%3&2`KVy2%Fg2sf-@e!XgRak&MQFZ_fM^{_CO6U}po7qi045A2Kr z=NV`IsvT}-3)-~D5K`ID$!_iZwzbEUZEXa!Og1gw^4yV3EaVCP9;#-a$=Fn6YJ~?s z63(Xnwx-HmSg-n#5O>ux>dO}wIw`iy2HY1A_{kyzye$3n~^#Fh^v6+uzebCZ8DdQnwyE*P8>EnGeV zL)o>4ANN>lbWVd3a*xL}a-gRM+S3V?C^2sB7F;$`>nm2s30 zyf=A!%r@mS>*Z0WwWPm=D#`2*#@zdtSE5u}_dD^L4aeldT`G*1I{^eMdyh&4UDBCn z26GT|xO9nxAnGd5f1k}aA2w~OyMGGIJt1UwDN^Mv8G&IW>!Sjx@SBR|8>1-cICHI1Xx(8Q;NM!dZFPgg@Z&u@k8pcMz4vUn&VfC8j{9aO>nf>IK_!RfUB2Unl9_E|K|rsD0APqfZ{D`!fO|b+lCj#V=%CaIjKG90QqK2j3rc$m zlN4E@+iA+TBhlz1t-l5_a)`uo7WuQS_H#wS&5P5`07~IraLsKY(x3EJ7elj(G;XzR zjzmVg*kC-N8~V_u`kK)P^80v&DQtUB zEqt`!i^t8X@{S!nE*G*kLO%pFkG9&$<=u2S;|kli;x}KiBkk8T`0Th<;-uK=`@5iC z=XTShkLC$109M^2lIT-B)+0>D7-mD2^~B&!i#gF+Cf?E$JK=&|Mgy({I%C3BPSHVYRJ zepbtYWJ?tJ_$WllF_QPUN3wJdR2(v;lS6?dxn@j^*EBy zz*Uudv`;3kj$Lf-KL!vheG_9T=DxKZ}3y zkepwQSALE8;9?3-zW7kZm=p7=z1gZQhVYk=nJj*2@3<8O8wx&*1J2uVGvJzhZj@p= zkU}{#!0d&CY&4&4?1p`lE_`qEgxFhTDO)R*a9}eVx$AD6IhdpZyeOwY>03GJU@lDx z&h6JzxG@Y64@?Rk6wUKeI7TZ*aC+79GPv)cPwDdP;(-up6izmQcU5Im6+yxf3r2v8 z9KK{dFMiLO(EQbG?T&@FdcgAQrw%ew@N@9St8erR@_+o^^V4VWgFv>p;(Jd=5 zqk+h>Fou6d&LN3k9iY~kUSqpP;%`TYB*9mNX(Q$wDG;$xR+2w$_dGtob|#?aQ=Mo} zo6C#d(vRGG06q~?K8=quSe8A)T|6PLPI=1kjtk8ixRuhzW_8*(8__BMD~%Fz(N6o@ z6YZrv%qkFosX)5wn(0Kh+5)X2a5){}&vx}OFIl<5TTV|#Tk^#SSwdHDe%LOg=VF9( zSyQlMQXp<@;*dqym=bum@cRg9%!V1H6ygqf(}f@>g~vnAE?Q28%jBTGZ6kPs+h#;o z)zO#$wN?b$oSE3Q53S*kwm{R6sKAgc?5L<=61;RYP|k|mr)I8HKBmV|aV$vhFc?Pw zFea5`H;0az>l(nfvkp;sBw{NqwiX`=;cWN9E5=i02Y7JtT1=3s1k!Kqz&U1{uIiFK zNif~V#mR3-?`7kJfMF#Crm{hKSVb$5;cuufprQFb-#zskKFzIGZ`RxuY-cplNUir%)=Gs zO7IyLSkM1z-ZqD@^5!Jg(Y>W|;EoQY$K5(jmiVzHPA9ZqJL$1oa!F5U{RnVcBDYDg zLb79C$#OykiA8A^uX(h)MD_XrBI?!nfedj2&hUX>h9B{M%lWMsfBJgO^;H=*(LbKq zpZ?R)BMsoEh!vUu3EAsGLd^7^5EgRb+%jFqV8wEY4y_Dmm=}+fUSZ2)=uGh`ncUyP z%zm5C0XyA14dBDvZOTW69`bkd;mW#mHQrcK)t}BxW0RP*0?HZa0=^T0sH!O{N$~J@ z@C^;Y_s!we3bfBUStA+`+Y1d$j+y<23?E1`etZ#3Z3nY5JBP`avBa=Z6e|49XG5}4 zSp88k!5_eRqDUO%N&<@SHO(^cu)eye?`@-4&9;5K&5i4_{YnCIuBzQyfelLHVitOz zOjIDvAjY*AaqI;dDWxZ7(~1GZ?fQc@+4RlbYlcIeMAigxl4|sRtmzSgmtT?K)eVCc z=Z>iMjrcdMT|`(Z7>G-UA)-J_VZI@4wm@|U*U3SDAU{+TZX>={J*elAW1qEM_wu`$ z?M7vfu;D}HAqbr^<0AmpML)q7%@ob58`tiql!y8Am}4sQ8xacXL|`DKcr+V5&t11t zUw|7uSuNYNm3P^?gAj~rNfNGi%9}2K1o)myKG_4J8KO}`(QFhLlP`kV46G8?X66{l zwN9lA?-t#_*+_z~U%X!PtVK(!1WTN*iWAqYa_72OA?0s7eqwsJdlJMU6MfTG(&iC!&*p%KX!n(NhEY_aAq^RN z)~6Sourx{lEE>ibizp8_(ZkfX)wl8C2xHVRb^9f*17z71{g=T{#tZ7s)ecO0biATh zMJnh?^C&Ry!(lgJM_3b5t$R5&UU%%vU$PG^0x{4TNNE)leSjYDg7B@HnLoQIA4Suj zXRe`{tVxXvEukEM2^UbEG@iH!7{=1i5|M<{UmIHyPiDfaW9MKYS)_^OOBv2;4_?ChCsKKh;1gr8e#pR>5- z&4cBl+(>mY-k$#EHVCSrH%`l^z0cSyq`ZYoW;B$6S_n}L%U!l zoP$V}#zELxB*Y$OYk~ob~usK=WktK!#b0p)=oAw&W z1JqD3Q@gZ%ysJt*~MI9pbfgg~hHbZiLaZv9)^_^Bb zF_Gl^GX&aHi5%VW%5Xfx7>ouWE+ceFt_JwCCMlnHy`wn=s`2uO6TrSqKpKg~Xxk_~ zs3E`OTak7m7AfprO> zv&$O?$SF5HL+2UEc-WrMpkZBM0e_3)gO_D~sK12u9vmH0U5_bNeT06;!wb$e?AG+_&ki zAGYbU9BT)Wtc(zPnQv7zDljX*G(y~kz+~TjFX$MamEbFJ$d7iya$U2K^_tre&>y1| z$Y7goY2|9xXEP~>s4k*PWVDYM*kp?{e`C2dVN;Jfl6WzDh>mrt zi|ICf`{FVGhp}5@C+)+%il=LO92fl^3=$4UM?^HP8(or}2()@Vb=FgCTV4Gh{Ak#< z&s%N8)64-=Y35|2t{b0b!ab(_Mn7^0_cGcIU+bAZ`5ouz@YZSaNj)k!8`lg;M%Ihk49jiu&u0n9s@14A{2NU zz)dv|5p@9yv?zV$84~iF%!Zl@-hqyd+c?8=Jl-<(MaI?xP$jt1U0~;&uMir$Dh4`# z_K)v=>r}UQF~~tHTJZ`>yjM-PP7Oih$K`bhg1GdJ<4D zNZ@_ns8jeE5wurE4VD0U{@bK+EV> zD;xr6wKerns0-F59&^W{nAs|>GaR^M0|OUt6#jTcC(Ekhc7ncf76J`eUGBm#hrNvI z`^h&#%7ws|R;v$ed0G*oMGZrL?+UkeL9@4a^KdVcgjdL9W&wTJ`#Prn{^c%+0Um#H z^kh}DD-AgF9S)!xlGY^k{Jwf)a4hb2dN>m)04_NaN#+eea98x5Nqp#LQIi2kt|yBa1NqDE_U4@h+E zuhNw{-y3Z5M}JW3Q}q6=CVizYfGeXT%ssfT{NSdW5 z%wMiZ+9lmSdQUP27u7;AmsTmP+e>}HBJ{YEReF2%lNA~fhBT)5D4<%l`tnYYX4jnY z>!z0zk1Kcpvw`noolIBu*=|^9D?XiZb6L)L8sRLOnv51sod>vGf=1=@AaY8#JkXAN zVA2)V4#QGf4>)NSUv@%x_hRWC7&~8YRB!{m2L!C4<@(nX9y}MF?s(Z=S-OJ5#Z#fV zQ!OQ^ls03i^t@7(E&~5Zc4_s>hc9rjcIUC~vHD)#j2XHtkg_^|dMALa-hJM5#*7MJ za>M9HoD7DSiDN#LHBVfYouqt&r4q-e0T9#@{hHRWs+q^<#WQg>-On@+SUV~-WaAJ{ zzm!&DftXD(Y;AG~v#kyQ`i_)8{g7LgF$;$iqsxEX511Zye9a|e&y10hm~SVIX~wH& zu9-VtDwx3$3WCOBcn&7ZioS5U83o)VaU#$_YXI;`4zMQQES^(0L??4VUO~bNin!mu zT}onXx2P(OrqHzA43QHF)suO})u{)p;|ke~Ph&gjfyIQe!bvHQ{YPAoF{^0OMp3t? z_x-yO1J%3RWty@63u&4w@4jtW z+|L07#>Q6kw1N}p-5C2BvF>Uq&>>z_yzzO7?J-BD7?l9)XV@X; z8aC%Hm4`NV_#G6O{@9Bn`xaA~zwJ&Mf{rR5HOAM=cNQnCqJul{&@iPQL6(m6?aVFEl` zbui_7-2Kz^)O5hidh_Y@W^D&=Y2DH2yg}8FkxotHCl@*fEiZ_Al(G$pl~^|Ur$_x% z!*i%CJmf6E0%dCw8{!QCMEBe(Vn0z& zxmA6a3wtGg$Y!D`imfuJtBw24-H&(>6QqZEO+J!VNBtCkPLP!nK&S?OEIIi&bw^ZG z3Wj#{mOKc;$N!j;nm_N;)BG6YEEJ`TMI(?f3oIM7PvOuan2=CG%?fmCFm(B#s?T0- zq{?eC+7rCCwh}}DRWi`h@6t1kJZkUJTcEPKx_oS{1mnTXpu=l5wXopSMITt>>%P)f zs|Wq6Plf482!WhbNr}|jF9(Y@S2g?!4?SDTd#I;B#`*il-tr)`IHYoCh=Sg!_b7-R z=(&Z|`%M^Q8c5P161J@40q*keSDe~3SGn+*B^Ew5!3mE2OIp%+0IeO!^DiOl0#NbU zE9TT-i9q5O#lWl6YhOD#KLDn!+tPIe}MdcThJ1oj3}Y+$~w) z8qWZv>xF>jabm;%$i1-o_3-{9(8(*~hVXh}?)`=4l+UHOPrRp!2}NW6Ks2zOR60-( zbB0HR1nru!)?CugN-6|?#H>l=kX6x>!u(HRLG<*_(88*HN2SwoUiuF4)Re=W9+4EI zU*$x~qSzo&FaR2X!r01UUfVed!YIZ(8rBE;;q%O^vbfUbl zl^r4QLs+eiO+``)fY)FEEcRcgiqju>2Q2aS_BID=+I|=#@(dRP}l1 z<#B~)zv){3et$_=_>;VNHl<$WiJ+fBtuyqb)gz$qu^_}M%c1rc=z(Zx*UD`WX+E+! z1kFU5M)>MQkqZ4BtZh7h)G>~m>zM1QMLM`n);BD_$qi=jPZ6cgjR0gjs`?aj(=>In zMT-ss_eJhx6r^ziM|)#^3fC$D@X?Rk0fPe@M?%&+)%8u1=Ky)hYP$G|X->6$lj?lF;9i?RhtG7|)i zRol3c7YH)xn&vDyEK;qrm5gQgj$+_%$Sj!!tcwJ6ZxRJuo*$l`dcUx~-#GtRUJeUw z-X~!8O`Y^=Cnc~Eygl?adnuLBiX2(m$ydM-491eN+$oxUjA22VUx#%ZtkM?iLy9=# zOo{fMQUG5xj!*9-SN4>-q9kW`h(xOj8#>QEF)Wx{Hi_h=9gWeM+flTL%|iB;1E##= zmA+!(-_g^Nk%-pzZEf3wRrz=1U+%hoe4=s7A(heq*5 zLe;4)%#NIO)%|i6=Cc0H5hT zOAy)?nmKe67DItcVVR?`Q16=f(^JwOF>)bDc-=K+Vv(a{P;>$G{CU3r&>51lWz#7A znMf`y0{azoe-_+4H7|YF$#srJrtgs4wV5rWBYd;OOThrOE zbGH8w?-3jvq0U~!lZB_M4$;uB*==y$kP87lIuf&lT(!S~SM8y}Ha|mtC-9tnr23s8 z7+XCHSm5u#9;u_zR4Zg3X;u?^C?}c!@lyTKAxj3zA1v6qetE+&tkK`8v6a%lP(7+= zJ*{KrKGN1f-A`Uud;=EWn;1j)fkSjGXCN&C_d41<27ccS?3uB~5Sp`g=h> z$f{sx0_PuHsCK@}YHcOSA~1T;T7*(ldv2k5u(aPy61c7UmQT zL3SebOZEd>QW2sUHjE$zw{CW-FqJ{O-m~l7+dHIG{U>T~DJbcG;T7-DV`tv)= zafp$77P%kH_&w4rCSnd__vdRfRKLxuHeLefow&97z44Tnr&_*{wIF?gn~2_4s2m49 z89m^#F5F=P@J_)U0DkIbvD5Sk#u`H!jHG~hp!I3av8t6rM@zmr)sG?%Om)7Wf?P1X zh6tY1Jy#d#CkSF=Q-W@pTBAIi~ei zS8x=O1Nq1(0ZX8jMwl`74O?F0V%EJI15pB2Rx#+k`{GCC8l{Y>w=tyh5O^IWBn!_= zktBR%AJn_q_$Fv&3R89Z1MbKZl~#~o0E`5-udYxA0RHtWDxg2FRM?neL#Z8s&lK;0ETkB9FwJrG29_d3Fh*bC1OFAI-Ei}Y8K8ffvf#i3KV7()Od zh>I~oGas=fC=TOF?}CcrYoHKlEv!}Y#7H3o4;%(AdqC?bi4&)FWJtbm%N0kIhASL9 zkN~V*=XiZ>rEZU8*fDS-*N1RiGwRvbIFJIBQXSL&Uuf?1Dj6ML&Jxt-+b3V!b$WT+ zm;7jiBX7OpEs7N{h<=WVx-Pz^B}$lX&m4FCB7q++o!Z>b0#!Q99rivrmk>BmnNCWAgjt*{|C;VxD9*k6&UHc6g)9d_x9)y0N zEEtKT*F`pjhF)ytzh{x)Whjif*GGJkJwVahcFxNKM5@}PQi~Mgis!!BL-Z)!Ru5*tEuYROD z?vmrze>I-a`!m4{n$Y{lS(g1+FGkDPSOZJ7ZJ^uJk7v&BdtcA&k7$ZLz@s8cMbW`s zS)whyCEOugG!xUc{Nbbt85y%GK6@V0W5k}ub^oL!(1!bWK&m1KlS<`!?Iib*WN*Mn ztLdMwT$E>+IWv{k0~L@N(`pXcrQc}3E3*r}7g2#3B$zF5u{UkJCsB59Kes7P@A|2n zo5d7yww=)HMS4i1@L|c$IYY6Gpv}bBBreP3NmI#q#?>y~GRo8jB^bfZ5|_|LuxE0f zuXsN6R&mjrhsm6sUrC>OPu7slVI$rvaoGN&x3P+)cG_@Mvz!)G*-q~MCD!HL2cIw0 z!%E*Yc?MkXD)!9M@cFWHzavyr9UE73>!&i`{S>Nad?x{M6spknjr&7M<;|Yc*SSAe zUkqs3br+RA-Qd#k4Au{fV18^bo7$BwL~;99FWLd^Sv{c9oy;I{4T}*BIcq`!`81bN znHcP(AG@;7?Wry!ArM-tt;rA9iJ9OXHYPq9taLf3VE4sLrsE4v@RBeIgLiU@RspCZ>LVU1( z51s7havrIwYfhUW;D<+n98c;DU>Tj=xY`L_`oZ}|{%A(ki;X(S;_-Gw z(7uau-*b-evz#A)^rhXzsy;=@ASA2tXiY3kC3b+OmcgdFHtrV`PQ3T(QRik~G>f+6 z4i}^oM5he)yLk$xyeuz1^IygzaPQj*dFA_9RD`2$=)>9;fgOn5^<1~ggr!C^+P@Ef zyJw?B3^1;gkIj7;&YCm1A~X+@)iDfGWgl z{}zMTms^QhG^9`V167Qpa!!gF$snl)cBG?iwaa2VaW}9mO)nj5lKD5-pf67KIk#Nm z5dCFHNJ4(*02z)DYi-Q_!(#b-N}w6mouRLo^FoUxmC_J@ltn4+L^tsrt?#s$*5Q{) z!F4OL)N4JiHPdSPYXuo&-lWk5D3><&IWWOvqKmuxVcr;je@`9Agn1i{oHWhNAUqV~x4Z6;S9ostK0|^fU z*TL=1!zF@qJUR|#VF~v%Ep#)(j-lg^`1N7i3d-K=48t$X25Z9f3d(l!7cpk?!H@?O zJSd7Ri5>_t(jOQnZBwnoa6Fa)=hF*JttR4&*6iDq<_ygb`1k#=*$FNYZ$e;7J2!=- zW9)FCF<-xW~_{74w~uSgh`g2uE5T zELIITMydhkK$~g}zgh+c<7JGsOF8FF*(X=M2SsW>-7+NVh~y~=kjL164Y!!@_Et)> z!`4aY;x3pwS-;bAvVQm!HsjMH)MT51sCp(vE=Bj2&cc?)3(Y?El^3Y9;=|`HvEm~>CU3W*VIMbTl^g~3`($2^{x40TQ~f$PQ^pREI@S^PHk=Q=iEEX znV=(io&zOhg0KVLLy-Vp4yMiA@%dudTsO}c86tuP3Q->tlDc6zYMU2f)-XTOKfI3naf<@v#O+D_>)LfL{C^3>WK9`iP|`ekpzvrFsQo|9i8rhjHY{q}LZ z`TfY8d>Ff(UDTF@wi9naOkziDwz&PxXz3SsXWtCLH}L;$1CNsB=M zgOgTV?U&23WaOYGBgd*`oSeWU_BY#kR+qPG6h&RF4Ks5l?lK}l@|Vz*lXS&PfzG!^ z6LXVh?$v<49oIK(?gA2u9s{$4pRDnJWQZD*#Gd^mXKyuv;_lxoK`@^aREW|G!{uo2 zcH?@w8vA2)2Jd;qGx=a6;D?4ycSc5YeB{nKtedEkvDJAFA-b8W%Maf!+LluWi{9M0 zaakN%F0`8I^(TkFKS|p4-gI9N?%-KQV+HaV)Ub#Xs&-P)~9p}&1EXkie11&|e!qb}tZflr57!a#nH+$ztEN=UHzc`ci zaO{QL9@CrhAY`OtkK^9B+{X3>hZJ6ZY7kA4orfL@6NC%B8$Xm>8cCm16~e5?PZwqxp z@XDWD&mTkW_=E+|WuJNyN8cZttR^8#JR{rb8$MRbqvRd{T?&pUxX~|tR5bD!j|Zh~ zNjZRe_0)@YOg#=`H#a2l*PHmi^ZMXYSZqQYW1S7Ru{y^U}|$3z$T18l63CZPXh3LZq_&74L+ zkzlq6F=HeOSc;8(iFvBr)Q%%WD;DG*EjU6|X`AY9_Nz)#bW7DkY<3Ru$Zu?+@BH~3 zBTm=;wEE^Fo8<5YniwOmjh^`r!&QDw8kyE`c{TKKXk(J@<^~v#wxxzj&c#|GGpykX z6L|QPiHVJ03PvB+dOnIAt!`$znUZmG{Z;mykcxK$u`MT#p0vciyt?3 zbv2dd0;V`+|N z9zPig89&U>L$7r}7umfkDdsb$CuV}>}2tQp12lXdJS6sf$mUX=jFp2_`(gz)i2o#Q$SrodEm|oDSsQ zMt__xKw&WZvg_%~(ZqvSZ?8%5wh}4*eId0R7W|P4`tI=YPZpJ~bYeEkV&+-q?yrJH zZ4zzsFE$a55(`pJq&qO1Ot0a-29i7=MnaZ!)6R?1Ohw|$7Kw!1k{rGrq8;8i2E>?t zMD^{&NA;QZJ^C&Ai#fwBFIJ_kI|ilwAoN>}XpI>Q0ZFiNnT_>$sxyDYX5B@E{at1x zygZ1!h>DG_1jScOO@F4u(i$YXOo_liPn1E(pZ1bI(lP@68l5j_mU^O01* zSUvK(QfQ!XL^2U+pNO0#zG^7-b4nmN4%uAuOlZfC_M9iK^@seXh3~s9rhE+lc04J5 z7ts{a^}Q#*1~y9}QQny(Q^V&Gt~vY@Y05+o@9(>tk+5=adt*@$EG*y8KTEy5?m6bf(#x@+)0Q*d39Z%6CM z2k^C7iX_4(e@LVt2XSDO=+hBp?i`OuY}3SJk3%pr`tGm@jQ{r2P~5M$CX=S48`xUZ zZ9!R5HQ`jg4<-yR!*9WTRA#8|%lM+eA5qzFiokf`Gn_46V)|?!%f1nfSNrb8^L?Ea zUJpaFGhPdH+O1gipi!V>OZ9z4b;p2b{Nb6c7ycOBifn?ZdVKC)$&7oXVfW_*LDn?#zK3Sr1CL}Q2B4r@ain_xnb@8RkEgAg@O9xiDX4Gx%TK8Yq`2ec!^h4>PUMtI`5MWE`J*F$!as9LeT=Uhbp`` zE7_hneB(gyBJ+7zEk!UhO(i@M_OXjA71=1Bj-pL`73l|!6Bz%`>uSxZ?-I{qZJ?`t z-8*mHLu#hy%b!N(>E_8)upML75I5~U&PXw4j!^_orJvl-B2SRv?TyRPQ;$;GYBeN-C*WM!H?exzdqxrYpvN)D0o>Nv3n1Rt+vHlZEJHgj zgM%(@4)P^K)PR8bk${V$ae(yx?E9O!epKHzRfeq2XWT^cwBKC2<@2pu&uge;%1}W> zqPA}(kDOn{)QM1RGz)o8)^J3`>mOqA}k&HlOAw>tk&uHh*(+eX&xv7%Kch^5kBa1^*FMLaioKPvlvr zaPx_K+Rev-R-hHHyZNNX587i&gCxkTcAt$wNTCJJydKdX2O$#JEnpI+4L)DD>q`!) z45=Jrq7Vq_38$B8;)3d@ir`3KLn`%ses)ti2_J zczFI`C_!(N&GJ(bkJ-yFV?2Z0S%Sauvt{q>J)nG4SM;%qXXh~SPx}my6d~#8&f(+v z2rOpoo*lTg0wEOGkkFDweeR_4f)Zd+IrfvtZ9$Ve#Me^2_XadWs0aCe4)wx`zsw>c zL%ohq6ECPd$x)|nf%p63?a5YTS`;UEiZ0V!q96g zjNhIshSj%7b+HmkXF10&C~OTU?OXo|y-;Yo{Jol5TtOk|5GZuRRF7a?J@D7#%fm+r zBH$JDYFzZH z4H=hjQf)~o`i%T)Q0nk1hW>=eltz^ki%*@;D2~9;QfFRO6USq{qU(Mm?UOQdGpk8kHc^ymY-R{g5oTh0%fJ-_Tr@ujCGxd+a~U^=LZ zXA|Mlk9h&3OTAkf=5PH^!qpE+6MOjIH;^rHmPXxq&W^YaMJbqyBi-90`blEg=kgwS zev0>NhtUof$#nYO&{9#o_Bd%J(tgIVAS@k>Vc`5dt9<(&^iNwh?4^$IPM2y18y_x= z0Uxm^-0~%9$`_uB%A8_JAT4b~A zh`^uD4C;s4xLy_4o);mVi%bkT!}JJ`k55VZtd$m1z9Y4oIU($!8a1~aF|?8OUyUrK z8A}wIB7aQWJ!m@om0NB%cP-x*kyPH55kpBRs36>w{$11aFr+TcU+%=gJ;Tc;r*e;c zb;zO5H@G>cMg47G%I`3<+*u5PV^}YoK;iL_Qw+}lK zah_`5BL{|%vm9_#yLEz?UFgmIc084YNX*mWS#D2arQP5@t#q~2*f782#iu+@L?=Q8 zON1Mk39@Jk*i)HBoE+^t%8-xx-=FV_Je>0GR1YF>e+t)4E4MZ58Qxi~bzIiS1Gs>- zgl@oabC?&K!&-{0*{N&5i^wHcao@q`ix8Q|Ba9821KK+CQ5^O|3?Fm}Rr9Yu)Z_0_ zp?r58BgHuqGW3}VD?61-3AwH)_yw`1oI2tG$P&9)cM}g}EC&+?X0pfQv8KA56TGQ7 z=QIg$W@SplBmw-7PxK~@UMsLNi4i9P@50R8)t?~HsGj#P^%iX!A{`4|@cz2IW_?K+QB`UT7W#&X=e9NP+wKB0x2g!g(m z50*=pz$62mn?F*vjzwVYKN;dj^@iGyn#Avz|E7r2`wUEbg1eay#suEYD`=uGttN_d%+aPzOqYBs{89J6|fe_1CS|MZSjM4dI=9r@uI0!D>~@!J2iBy9V+)jK|U zddVZvME+fnd5QC<#~}1|MqY2JVv5CJ?#+5M!bKdu4hE_V7fkAZfN3t}hm`7jJ|RE< zc(~U<_hLT77JJe|r#?~RP3|nT!_IRJ!lR?X`mBkLiI`xviiRqVh3O8gp1C3R`$jF2 za#VpIqWFtPaHz*XJ7@oaP>G}UoS1%8=uR8er{`os(4twzI7Qh7Z7vh3)L|_B?mf>Y zzrizDM^?o47di#&{xghyZ4$Y09arZp9YZ3RBenajq?Fb^3HO7LXZCh?^TC_BLgU1ae|D3S94 zGQ|uv9+F27KW2JZncw?LsBe_bx+%}DXq(CCVghxcqa?fhR@vC(SZ)^fSN;~1{ccp2 zJ2z_)eTuBu$&Q6dtV6haz$e=~1wCsGLUyD<&-SDuQr#8D>%$(fB;eV>7P1JPL#EtijDD zW7^DG2Q?o(b<-vBlF@Pqu9G^T>=VDGA)hx=TO#>wY_&G^lXgb7Q7XO#-(-PZu4?Vh zC}5;bA=tF{(_^#g(7%2|sGjGB=3Dj_UHwn z^N$;IVIit*O~&_xiaP)_jb_(Y|6(l((bjAjQI5d z{-K}|>UMMsJyzU5{zd2J;rI0*Fx?PE2KQQBr`sYA3~7lS+Xd4?akFCmW@=~iVE`5Z%>#0wYK;}!Z@E3ZLftGwGV`c17Dk6-9;gy`9b z%6MxFg|Du zGdXmfLrqgCDY_@C>7K;)MXP=ArerDkTlt((h-6RP|MV*mUBEXQKd>V8o(L`*GyLb+ zpYh%EuD?GCXXmF&Z^_F&` zd97zsW|;K|Ln)=BFJEfrR5$lJz|Eo21H7ioi&J@_A2XUTvxyNGS6c77!NiTa`Ny?s zR+0ifW`7pCGw#)XFvD)(KkyM{j4@Y zj^3#WFtl>Et@>yQMd#Xe4_1e+Jh}z^c^>dfjU=O9gTT7~R9k+8B;LRI_)yYnO3UwD zRT@M;Txp-bDxQlfYS~<>yU-*RA5B1@>WQV3d;NR43uL=Th#^a<4lZwfl|C z4GD3yCN#BJuKw{%FH!RVpE8FVEyu3kFLVs30>GHclU}ZLY|M~`wO)Ic+%ky1l zw(txXBl1gC%rb33dhcQ-kKk>WtqP!-PE{tdyHtKdUWx<>yz=6Xx=*JaHrLB*-AHNV zq67vTxQ{!256mr#;be&kdJ{hVSZ<~G%>6>CTX8S{tQm{v^yH2{!cdiY9qRFQ0pNUt zRz1(-P2|^Y8)WaA$6d7SkPb7^{XDxUVw4Az6lmW)LnxT|=KrBEO2AxW?{JaZU3Ile zDjL1dHBk zBmqlTp2rAJPVQQtnQdV%7y7nZO%frmd3dfO;{#rHfU;eok?WzLwrF05b6=jTWTAxg zkF#jiRK)T%;qO;jgANmMyTvR7`07Vi2}2}iv?M+WKTDYXn-%G?DnO#*kQpzEj}z@Dg}*>z{Aq5+(Rw0c z3d`?Vaep0i90TNkgzUAMQuzzgmNVH%(7TeH35-kzaNuqE(dI1X2mVfYAZ|It`+J1~ zOGSzvs8a7U-e#&eyBfv(XBrmDS=V4MdObT>N+ccxt6&v|MBw40o{*E!Q6`MmZWvBi z2-BoBdXDNNE~?{B4`b~q2)uY9SQ%Ih<>#P#_+ ze#M>6Cv(HSGcMAUzOH5Xi$|&7EgU&wBKH;^jWz5B;Xhf7}# z+o?7)o9VrXe<>J53o-^{1SkSDx*RugFPjAksSHj@TRblxE-=DlT6#=_bpozZp zMmNXE>^k13+nR-K&;`!9mX4*uIdQy49jw3cQa^Lk`sZAVUHHxq1Rjh#hCD7Reo&fr z{fpHrF&sglbBXkt27F8G-Cl~Zu5K6>x7(*Q<0n5$Sw1EZ^}#XL5ODO3{OJhZltFTQy6|?Kw=-B>@DZ?sQq?_Bj|Pyt8O^MwdQvZZEdDV%!qs+ zDlHzie-;KYCIqj3MJ1IQX8TIds@HBN? zUwpr4%=xsjx)|v+Z Dy?{7I literal 0 HcmV?d00001 diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp index 7fc8eb6e8..5ff034d4a 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp @@ -34,46 +34,40 @@ AOpenDriveActor::AOpenDriveActor(const FObjectInitializer& ObjectInitializer) : PrimaryActorTick.bCanEverTick = false; // Structure to hold one-time initialization - struct FConstructorStatics + static struct FConstructorStatics { // A helper class object we use to find target UTexture2D object in resource package - ConstructorHelpers::FObjectFinderOptional NoteTextureObject; - FName ID_Notes; // Icon sprite category name - FText NAME_Notes; // Icon sprite display name + ConstructorHelpers::FObjectFinderOptional TextureObject; + FName Category; + FText Name; FConstructorStatics() - // Use helper class object to find the texture - // "/Engine/EditorResources/S_Note" is resource path - : NoteTextureObject(TEXT("/Engine/EditorResources/S_Note")) - , ID_Notes(TEXT("Notes")) - , NAME_Notes(NSLOCTEXT("SpriteCategory", "Notes", "Notes")) - { - } - }; - static FConstructorStatics ConstructorStatics; + // Use helper class object to find the texture resource path + : TextureObject(TEXT("/Carla/Icons/OpenDriveActorIcon")) + , Category(TEXT("OpenDriveActor")) + , Name(NSLOCTEXT("SpriteCategory", "OpenDriveActor", "OpenDriveActor")) + { + } + } ConstructorStatics; // We need a scene component to attach Icon sprite - RootComponent = ObjectInitializer.CreateDefaultSubobject(this, TEXT("SceneComponent")); + USceneComponent* SceneComponent = ObjectInitializer.CreateDefaultSubobject(this, TEXT("SceneComp")); + RootComponent = SceneComponent; RootComponent->Mobility = EComponentMobility::Static; #if WITH_EDITORONLY_DATA SpriteComponent = ObjectInitializer.CreateEditorOnlyDefaultSubobject(this, TEXT("Sprite")); if (SpriteComponent) { - SpriteComponent->Sprite = ConstructorStatics.NoteTextureObject.Get(); // Get the sprite texture from helper class object - SpriteComponent->SpriteInfo.Category = ConstructorStatics.ID_Notes; // Assign sprite category name - SpriteComponent->SpriteInfo.DisplayName = ConstructorStatics.NAME_Notes; // Assign sprite display name - SpriteComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform); // Attach sprite to scene component + SpriteComponent->Sprite = ConstructorStatics.TextureObject.Get(); // Get the sprite texture from helper class object + SpriteComponent->SpriteInfo.Category = ConstructorStatics.Category; // Assign sprite category name + SpriteComponent->SpriteInfo.DisplayName = ConstructorStatics.Name; // Assign sprite display name + SpriteComponent->SetupAttachment(RootComponent); // Attach sprite to scene component SpriteComponent->Mobility = EComponentMobility::Static; + SpriteComponent->SetEditorScale(1.5f); } #endif // WITH_EDITORONLY_DATA } -void AOpenDriveActor::BeginDestroy() -{ - RemoveRoutes(); - Super::BeginDestroy(); -} - #if WITH_EDITOR void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& Event) { @@ -85,7 +79,11 @@ void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& Event if (bGenerateRoutes) { bGenerateRoutes = false; + + RemoveRoutes(); // Avoid OpenDrive overlapping + RemoveSpawners(); // Restart the spawners in case OpenDrive has changed BuildRoutes(); + if (bAddSpawners) { AddSpawners(); @@ -101,6 +99,7 @@ void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& Event if (bRemoveRoutes) { bRemoveRoutes = false; + RemoveDebugRoutes(); RemoveSpawners(); RemoveRoutes(); @@ -122,6 +121,7 @@ void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& Event if (bRemoveCurrentSpawners) { bRemoveCurrentSpawners = false; + RemoveSpawners(); } } @@ -144,9 +144,6 @@ ARoutePlanner *AOpenDriveActor::GenerateRoutePlanner(const TArray void AOpenDriveActor::BuildRoutes() { - // Avoid OpenDrive overlapping - RemoveRoutes(); - std::string ParseError; // NOTE(Andrei): As the OpenDrive file has the same name as level, @@ -329,6 +326,14 @@ TArray> AOpenDriveActor::GenerateLeftLane { DirectedPoint currentPoint = laneZeroPoints[i]; currentPoint.ApplyLateralOffset(currentOffset); + if (currentPoint.tangent + CarlaMath::pi() < CarlaMath::pi_double()) + { + currentPoint.tangent += CarlaMath::pi(); + } + else + { + currentPoint.tangent -= CarlaMath::pi(); + } roadWaypoints.Add(currentPoint); } if (roadWaypoints.Num() >= 2) @@ -400,7 +405,9 @@ void AOpenDriveActor::DebugRoutes() const void AOpenDriveActor::RemoveDebugRoutes() const { + #if WITH_EDITOR FlushPersistentDebugLines(GetWorld()); + #endif // WITH_EDITOR } void AOpenDriveActor::AddSpawners() diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h index ed1fd2101..765502213 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h @@ -44,15 +44,17 @@ private: TArray VehicleSpawners; #if WITH_EDITORONLY_DATA - UPROPERTY(Category = "Generate", EditAnywhere) bool bGenerateRoutes = false; +#endif // WITH_EDITORONLY_DATA UPROPERTY(Category = "Generate", EditAnywhere, meta = (ClampMin = "0.01", UIMin = "0.01")) float RoadAccuracy = 2.0f; +#if WITH_EDITORONLY_DATA UPROPERTY(Category = "Generate", EditAnywhere) bool bRemoveRoutes = false; +#endif // WITH_EDITORONLY_DATA UPROPERTY(Category = "Spawners", EditAnywhere) bool bAddSpawners = true; @@ -60,9 +62,12 @@ private: UPROPERTY(Category = "Spawners", EditAnywhere) float SpawnersHeight = 300.0; +#if WITH_EDITORONLY_DATA UPROPERTY(Category = "Spawners", EditAnywhere) bool bRemoveCurrentSpawners = false; +#endif // WITH_EDITORONLY_DATA +#if WITH_EDITORONLY_DATA UPROPERTY(Category = "Debug", EditAnywhere) bool bShowDebug = true; #endif // WITH_EDITORONLY_DATA @@ -79,8 +84,6 @@ public: AOpenDriveActor(const FObjectInitializer& ObjectInitializer); - virtual void BeginDestroy() override; - void BuildRoutes(); void RemoveRoutes(); @@ -115,5 +118,4 @@ public: void AddSpawners(); void RemoveSpawners(); - }; diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp index f914a6449..ca52e007f 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/Traffic/RoutePlanner.cpp @@ -99,7 +99,10 @@ void ARoutePlanner::AddRoute(float probability, const TArray &routePoin { USplineComponent *NewSpline = NewObject(this); NewSpline->bHiddenInGame = true; + + #if WITH_EDITOR NewSpline->EditorUnselectedSplineSegmentColor = FLinearColor(0.15f, 0.15f, 0.15f); + #endif // WITH_EDITOR NewSpline->SetLocationAtSplinePoint(0, routePoints[0], ESplineCoordinateSpace::World, true); NewSpline->SetLocationAtSplinePoint(1, routePoints[1], ESplineCoordinateSpace::World, true); From b3e2f2261d88282adffe1a0244346922d1e6967e Mon Sep 17 00:00:00 2001 From: marcgpuig Date: Mon, 10 Dec 2018 15:25:43 +0100 Subject: [PATCH 5/5] Added a description on Unreal's interface --- .../Plugins/Carla/Source/Carla/OpenDriveActor.cpp | 7 +++++++ .../Plugins/Carla/Source/Carla/OpenDriveActor.h | 11 +++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp index 5ff034d4a..6b10931b0 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -130,6 +131,8 @@ void AOpenDriveActor::PostEditChangeProperty(struct FPropertyChangedEvent& Event ARoutePlanner *AOpenDriveActor::GenerateRoutePlanner(const TArray &DirectedPoints) { + using CarlaMath = carla::geom::Math; + TArray Positions = DirectedPointArray2FVectorArray(DirectedPoints); ARoutePlanner *RoutePlanner = GetWorld()->SpawnActor(); @@ -144,6 +147,8 @@ ARoutePlanner *AOpenDriveActor::GenerateRoutePlanner(const TArray void AOpenDriveActor::BuildRoutes() { + using IdType = carla::road::element::id_type; + std::string ParseError; // NOTE(Andrei): As the OpenDrive file has the same name as level, @@ -306,6 +311,8 @@ TArray> AOpenDriveActor::GenerateLeftLane const RoadSegment *road, const TArray &laneZeroPoints) { + using CarlaMath = carla::geom::Math; + const RoadInfoLane *lanesInfo = road->GetInfo(0.0); std::vector leftLanes = lanesInfo->getLanesIDs(RoadInfoLane::which_lane_e::Left); diff --git a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h index 765502213..cf8533f59 100644 --- a/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h +++ b/Unreal/CarlaUE4/Plugins/Carla/Source/Carla/OpenDriveActor.h @@ -9,7 +9,6 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" -#include "Components/ActorComponent.h" #include "Components/SceneComponent.h" #include "Components/BillboardComponent.h" @@ -18,7 +17,6 @@ #include "Vehicle/VehicleSpawnPoint.h" #include -#include #include #include @@ -44,30 +42,37 @@ private: TArray VehicleSpawners; #if WITH_EDITORONLY_DATA + /// Generate the road network using an OpenDrive file (named as the current .umap) UPROPERTY(Category = "Generate", EditAnywhere) bool bGenerateRoutes = false; #endif // WITH_EDITORONLY_DATA + /// Distance between waypoints where the cars will drive UPROPERTY(Category = "Generate", EditAnywhere, meta = (ClampMin = "0.01", UIMin = "0.01")) float RoadAccuracy = 2.0f; #if WITH_EDITORONLY_DATA + /// Remove the previously generated road network. Also, it will remove spawners if necessary UPROPERTY(Category = "Generate", EditAnywhere) bool bRemoveRoutes = false; #endif // WITH_EDITORONLY_DATA + /// If true, spawners will be placed when generating the routes UPROPERTY(Category = "Spawners", EditAnywhere) bool bAddSpawners = true; + /// Determine the height where the spawners will be placed, relative to each RoutePlanner UPROPERTY(Category = "Spawners", EditAnywhere) float SpawnersHeight = 300.0; #if WITH_EDITORONLY_DATA + /// Remove already placed spawners if necessary UPROPERTY(Category = "Spawners", EditAnywhere) bool bRemoveCurrentSpawners = false; #endif // WITH_EDITORONLY_DATA #if WITH_EDITORONLY_DATA + /// Show / Hide additional debug information UPROPERTY(Category = "Debug", EditAnywhere) bool bShowDebug = true; #endif // WITH_EDITORONLY_DATA @@ -79,8 +84,6 @@ public: using LaneInfo = carla::road::element::LaneInfo; using RoadGeneralInfo = carla::road::element::RoadGeneralInfo; using RoadInfoLane = carla::road::element::RoadInfoLane; - using IdType = carla::road::element::id_type; - using CarlaMath = carla::geom::Math; AOpenDriveActor(const FObjectInitializer& ObjectInitializer);