Skip to content

Commit 9ce9593

Browse files
authored
Quality of life: geocoding improvements (#130)
* show ui message when address lookup returned no results * Use api key from project settings if map component doesn't have one * Reuse the player controller variable * formatting fixes
1 parent b1ab50f commit 9ce9593

File tree

4 files changed

+166
-151
lines changed

4 files changed

+166
-151
lines changed

sample_project/Source/sample_project/Geocoding/Geocoder.cpp

Lines changed: 121 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,32 @@ void AGeocoder::BeginPlay()
2424
{
2525
Super::BeginPlay();
2626

27-
if (APlayerController* PlayerController = Cast<APlayerController>(GetWorld()->GetFirstPlayerController()))
27+
if (auto playerController = Cast<APlayerController>(GetWorld()->GetFirstPlayerController()))
2828
{
29-
SetupPlayerInputComponent(PlayerController->InputComponent);
30-
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem
31-
<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
29+
SetupPlayerInputComponent(playerController->InputComponent);
30+
if (auto subsystem = ULocalPlayer::GetSubsystem
31+
<UEnhancedInputLocalPlayerSubsystem>(playerController->GetLocalPlayer()))
3232
{
33-
Subsystem->AddMappingContext(MappingContext, 0);
33+
subsystem->AddMappingContext(MappingContext, 0);
3434
}
35-
}
3635

37-
// Make sure mouse cursor remains visible
38-
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
39-
if (PC)
40-
{
41-
PC->bShowMouseCursor = true;
42-
PC->bEnableClickEvents = true;
36+
// Make sure mouse cursor remains visible
37+
playerController->bShowMouseCursor = true;
38+
playerController->bEnableClickEvents = true;
4339
}
4440

4541
// Create the UI and add it to the viewport
4642
if (UIWidgetClass != nullptr)
4743
{
48-
AActor* self = this;
44+
auto self = this;
4945
UIWidget = CreateWidget<UUserWidget>(GetWorld(), UIWidgetClass);
5046
if (UIWidget)
5147
{
5248
UIWidget->AddToViewport();
53-
UFunction* WidgetFunction = UIWidget->FindFunction(FName("SetGeoCoder"));
49+
auto widgetFunction = UIWidget->FindFunction(FName("SetGeoCoder"));
5450
HideInstructions = UIWidget->FindFunction(FName("HideDirections"));
55-
if (WidgetFunction) {
56-
UIWidget->ProcessEvent(WidgetFunction, &self);
51+
if (widgetFunction) {
52+
UIWidget->ProcessEvent(widgetFunction, &self);
5753
}
5854
WidgetSetInfoFunction = UIWidget->FindFunction(FName("SetInfoString"));
5955
}
@@ -72,11 +68,27 @@ void AGeocoder::Tick(float DeltaTime)
7268
}
7369
}
7470

71+
FString AGeocoder::GetAPIKey()
72+
{
73+
auto mapComponent = UArcGISMapComponent::GetMapComponent(this);
74+
auto apiKey = mapComponent ? mapComponent->GetAPIKey() : "";
75+
76+
if (apiKey.IsEmpty())
77+
{
78+
if (auto settings = GetDefault<UArcGISMapsSDKProjectSettings>())
79+
{
80+
apiKey = settings->APIKey;
81+
}
82+
}
83+
84+
return apiKey;
85+
}
86+
7587
void AGeocoder::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
7688
{
77-
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
89+
if (auto enhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
7890
{
79-
EnhancedInputComponent->BindAction(mousePress, ETriggerEvent::Started, this, &AGeocoder::SelectLocation);
91+
enhancedInputComponent->BindAction(mousePress, ETriggerEvent::Started, this, &AGeocoder::SelectLocation);
8092
}
8193
}
8294

@@ -91,76 +103,78 @@ void AGeocoder::SendAddressQuery(FString Address)
91103
FString temp = "";
92104
UIWidget->ProcessEvent(WidgetSetInfoFunction, &temp);
93105
}
94-
FString Url = "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates";
95-
UArcGISMapComponent* MapComponent = UArcGISMapComponent::GetMapComponent(this);
96-
FString APIToken = MapComponent ? MapComponent->GetAPIKey() : "";
97-
FString Query;
106+
FString url = "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates";
107+
FString apiKey = GetAPIKey();
108+
FString query;
98109

99110
// Set up the query
100-
FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
101-
Request->OnProcessRequestComplete().BindUObject(this, &AGeocoder::ProcessAddressQueryResponse);
102-
Query = FString::Printf(TEXT("%s/?f=json&token=%s&address=%s"), *Url, *APIToken, *Address);
103-
Request->SetURL(Query.Replace(TEXT(" "), TEXT("%20")));
104-
Request->SetVerb("GET");
105-
Request->SetHeader("Content-Type", "x-www-form-urlencoded");
106-
Request->ProcessRequest();
111+
auto request = FHttpModule::Get().CreateRequest();
112+
request->OnProcessRequestComplete().BindUObject(this, &AGeocoder::ProcessAddressQueryResponse);
113+
query = FString::Printf(TEXT("%s/?f=json&token=%s&address=%s"), *url, *apiKey, *Address);
114+
request->SetURL(query.Replace(TEXT(" "), TEXT("%20")));
115+
request->SetVerb("GET");
116+
request->SetHeader("Content-Type", "x-www-form-urlencoded");
117+
request->ProcessRequest();
107118
bWaitingForResponse = true;
108119
}
109120

110121
// Parse the response for a geocoding query
111122
void AGeocoder::ProcessAddressQueryResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSucessfully)
112123
{
113-
FString ResponseAddress = "";
114-
TSharedPtr<FJsonObject> JsonObj;
115-
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
124+
FString responseAddress = "";
125+
TSharedPtr<FJsonObject> jsonObj;
126+
TSharedRef<TJsonReader<>> reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
116127

117128
// Check if the query was successful
118-
if (FJsonSerializer::Deserialize(Reader, JsonObj) &&
129+
if (FJsonSerializer::Deserialize(reader, jsonObj) &&
119130
Response->GetResponseCode() > 199 && Response->GetResponseCode() < 300) {
120-
const TArray<TSharedPtr<FJsonValue>>* Candidates;
121-
TSharedPtr<FJsonValue> Location;
122-
TSharedPtr<FJsonValue> Error;
123-
FString Message;
124-
double PointX, PointY;
131+
const TArray<TSharedPtr<FJsonValue>>* candidates;
132+
TSharedPtr<FJsonValue> location;
133+
TSharedPtr<FJsonValue> error;
134+
FString message;
135+
double pointX, pointY;
125136

126-
if (JsonObj->TryGetArrayField(TEXT("candidates"), Candidates)) {
127-
if (Candidates->Num() > 0) {
128-
TSharedPtr<FJsonValue> candidate = (*Candidates)[0];
137+
if (jsonObj->TryGetArrayField(TEXT("candidates"), candidates)) {
138+
if (candidates->Num() > 0) {
139+
TSharedPtr<FJsonValue> candidate = (*candidates)[0];
129140

130-
JsonObj = candidate->AsObject();
131-
if (!JsonObj->TryGetStringField(TEXT("Address"), ResponseAddress)) {
132-
ResponseAddress = TEXT("Query did not return valid response");
141+
jsonObj = candidate->AsObject();
142+
if (!jsonObj->TryGetStringField(TEXT("Address"), responseAddress)) {
143+
responseAddress = TEXT("Query did not return valid response");
133144
}
134-
if ((Location = JsonObj->TryGetField(TEXT("location")))) {
135-
JsonObj = Location->AsObject();
136-
JsonObj->TryGetNumberField("x", PointX);
137-
JsonObj->TryGetNumberField("y", PointY);
145+
if ((location = jsonObj->TryGetField(TEXT("location")))) {
146+
jsonObj = location->AsObject();
147+
jsonObj->TryGetNumberField("x", pointX);
148+
jsonObj->TryGetNumberField("y", pointY);
138149

139150
// Spawn a QueryLocation actor if not already created
140151
if (QueryLocation == nullptr) {
141-
FActorSpawnParameters SpawnParam = FActorSpawnParameters();
142-
SpawnParam.Owner = this;
143-
QueryLocation = GetWorld()->SpawnActor<AQueryLocation>(AQueryLocation::StaticClass(), FVector3d(0.), FRotator3d(0.), SpawnParam);
152+
auto spawnParam = FActorSpawnParameters();
153+
spawnParam.Owner = this;
154+
QueryLocation = GetWorld()->SpawnActor<AQueryLocation>(AQueryLocation::StaticClass(), FVector3d(0.), FRotator3d(0.), spawnParam);
144155
}
145156
// Update the QueryLocation actor with the query response and place it at high altitude
146157
QueryLocation->SetupAddressQuery(UArcGISPoint::CreateArcGISPointWithXYZSpatialReference(
147-
PointX, PointY, 10000,
148-
UArcGISSpatialReference::CreateArcGISSpatialReference(4326)), ResponseAddress);
149-
150-
// If there are more than 1 candidate, show a notification
151-
Message = FString::Printf(
152-
TEXT("The query returned multiple results. If the shown location is not the intended one, make your input more specific."));
153-
if (WidgetSetInfoFunction && Candidates->Num() > 1) {
154-
UIWidget->ProcessEvent(WidgetSetInfoFunction, &Message);
155-
}
158+
pointX, pointY, 10000,
159+
UArcGISSpatialReference::CreateArcGISSpatialReference(4326)), responseAddress);
156160
}
157161
}
162+
163+
// Show a notification if the query returned no results or more than one candidate
164+
if (candidates->Num() != 1 && WidgetSetInfoFunction)
165+
{
166+
message = candidates->Num() > 1 ?
167+
"The query returned multiple results. If the shown location is not the intended one, make your input more specific." :
168+
"The query didn't return any results. Adjust the input and, if necessary, make it more specific.";
169+
170+
UIWidget->ProcessEvent(WidgetSetInfoFunction, &message);
171+
}
158172
}
159173
// If the server responded with an error, show the error message
160-
else if ((Error = JsonObj->TryGetField(TEXT("error")))) {
161-
JsonObj = Error->AsObject();
162-
if (WidgetSetInfoFunction && JsonObj->TryGetStringField(TEXT("message"), Message)) {
163-
UIWidget->ProcessEvent(WidgetSetInfoFunction, &Message);
174+
else if ((error = jsonObj->TryGetField(TEXT("error")))) {
175+
jsonObj = error->AsObject();
176+
if (WidgetSetInfoFunction && jsonObj->TryGetStringField(TEXT("message"), message)) {
177+
UIWidget->ProcessEvent(WidgetSetInfoFunction, &message);
164178
}
165179
}
166180
}
@@ -178,62 +192,60 @@ void AGeocoder::SendLocationQuery(UArcGISPoint* InPoint)
178192
FString temp = "";
179193
UIWidget->ProcessEvent(WidgetSetInfoFunction, &temp);
180194
}
181-
FString Url = "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode";
182-
UArcGISMapComponent* MapComponent = UArcGISMapComponent::GetMapComponent(this);
183-
FString APIToken = MapComponent ? MapComponent->GetAPIKey() : "";
184-
FString Query;
185-
UArcGISPoint* Point(InPoint);
195+
FString url = "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode";
196+
FString query;
197+
UArcGISPoint* point(InPoint);
186198

187199
// If the geographic coordinates of the point are not in terms of lat & lon, project them
188200
if (InPoint->GetSpatialReference()->GetWKID() != 4326) {
189-
auto ProjectedGeometry = UArcGISGeometryEngine::Project(InPoint,
201+
auto projectedGeometry = UArcGISGeometryEngine::Project(InPoint,
190202
UArcGISSpatialReference::CreateArcGISSpatialReference(4326));
191-
if (ProjectedGeometry != nullptr)
203+
if (projectedGeometry != nullptr)
192204
{
193-
Point = static_cast<UArcGISPoint*>(ProjectedGeometry);
205+
point = static_cast<UArcGISPoint*>(projectedGeometry);
194206
}
195207
}
196208
// Set up the query
197-
FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
198-
Request->OnProcessRequestComplete().BindUObject(this, &AGeocoder::ProcessLocationQueryResponse);
199-
Query = FString::Printf(TEXT("%s/?f=json&langCode=en&location=%f,%f"), *Url, Point->GetX(), Point->GetY());
200-
Request->SetURL(Query.Replace(TEXT(" "), TEXT("%20")));
201-
Request->SetVerb("GET");
202-
Request->SetHeader("Content-Type", "x-www-form-urlencoded");
203-
Request->ProcessRequest();
209+
auto request = FHttpModule::Get().CreateRequest();
210+
request->OnProcessRequestComplete().BindUObject(this, &AGeocoder::ProcessLocationQueryResponse);
211+
query = FString::Printf(TEXT("%s/?f=json&langCode=en&location=%f,%f"), *url, point->GetX(), point->GetY());
212+
request->SetURL(query.Replace(TEXT(" "), TEXT("%20")));
213+
request->SetVerb("GET");
214+
request->SetHeader("Content-Type", "x-www-form-urlencoded");
215+
request->ProcessRequest();
204216
bWaitingForResponse = true;
205217
}
206218

207219
// Parse the response for a reverse geocoding query
208220
void AGeocoder::ProcessLocationQueryResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSucessfully) {
209-
FString ResponseAddress = "";
210-
FString Message;
211-
TSharedPtr<FJsonValue> Error;
212-
TSharedPtr<FJsonObject> JsonObj;
213-
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
221+
FString responseAddress = "";
222+
FString message;
223+
TSharedPtr<FJsonValue> error;
224+
TSharedPtr<FJsonObject> jsonObj;
225+
TSharedRef<TJsonReader<>> reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
214226

215227
// Check if the query was successful
216-
if (FJsonSerializer::Deserialize(Reader, JsonObj) &&
228+
if (FJsonSerializer::Deserialize(reader, jsonObj) &&
217229
Response->GetResponseCode() > 199 && Response->GetResponseCode() < 300) {
218230

219-
TSharedPtr<FJsonValue> AddressField;
220-
if ((AddressField = JsonObj->TryGetField((TEXT("address"))))) {
221-
JsonObj = AddressField->AsObject();
222-
if (!JsonObj->TryGetStringField(TEXT("Match_addr"), ResponseAddress)) {
223-
ResponseAddress = TEXT("Query did not return valid response");
231+
TSharedPtr<FJsonValue> addressField;
232+
if ((addressField = jsonObj->TryGetField((TEXT("address"))))) {
233+
jsonObj = addressField->AsObject();
234+
if (!jsonObj->TryGetStringField(TEXT("Match_addr"), responseAddress)) {
235+
responseAddress = TEXT("Query did not return valid response");
224236
}
225237
}
226238
// If the server responded with an error, show the error message
227-
else if ((Error = JsonObj->TryGetField(TEXT("error")))) {
228-
JsonObj = Error->AsObject();
229-
if (WidgetSetInfoFunction && JsonObj->TryGetStringField(TEXT("message"), Message)) {
230-
UIWidget->ProcessEvent(WidgetSetInfoFunction, &Message);
239+
else if ((error = jsonObj->TryGetField(TEXT("error")))) {
240+
jsonObj = error->AsObject();
241+
if (WidgetSetInfoFunction && jsonObj->TryGetStringField(TEXT("message"), message)) {
242+
UIWidget->ProcessEvent(WidgetSetInfoFunction, &message);
231243
}
232244
}
233245
}
234-
// Show the receivedd address
246+
// Show the received address
235247
if (QueryLocation != nullptr) {
236-
QueryLocation->UpdateAddressCue(ResponseAddress);
248+
QueryLocation->UpdateAddressCue(responseAddress);
237249
}
238250
bWaitingForResponse = false;
239251
}
@@ -247,29 +259,29 @@ void AGeocoder::SelectLocation(const FInputActionValue& value)
247259
return;
248260
}
249261

250-
FHitResult TraceHit;
251-
FVector WorldLocation;
252-
FVector WorldDirection;
253-
float TraceLength = 100000000.f;
254-
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
255-
PlayerController->DeprojectMousePositionToWorld(WorldLocation, WorldDirection);
262+
FHitResult traceHit;
263+
FVector worldLocation;
264+
FVector worldDirection;
265+
float traceLength = 100000000.f;
266+
APlayerController* playerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
267+
playerController->DeprojectMousePositionToWorld(worldLocation, worldDirection);
256268

257-
if (GetWorld()->LineTraceSingleByChannel(TraceHit,
258-
WorldLocation, WorldLocation + TraceLength * WorldDirection, ECC_Visibility, FCollisionQueryParams()))
269+
if (GetWorld()->LineTraceSingleByChannel(traceHit,
270+
worldLocation, worldLocation + traceLength * worldDirection, ECC_Visibility, FCollisionQueryParams()))
259271
{
260-
if (TraceHit.GetActor()->GetClass() == AArcGISMapActor::StaticClass())
272+
if (traceHit.GetActor()->GetClass() == AArcGISMapActor::StaticClass())
261273
{
262274
// Spawn a QueryLocation actor if it doesn't already exist
263275
if (QueryLocation == nullptr)
264276
{
265-
FActorSpawnParameters SpawnParam = FActorSpawnParameters();
266-
SpawnParam.Owner = this;
277+
auto spawnParam = FActorSpawnParameters();
278+
spawnParam.Owner = this;
267279
QueryLocation = GetWorld()->SpawnActor<AQueryLocation>(AQueryLocation::StaticClass(),
268-
FVector3d(0.), FRotator(0.), SpawnParam);
280+
FVector3d(0.), FRotator(0.), spawnParam);
269281
}
270282

271283
// Update the QueryLocation actor with the selected location
272-
QueryLocation->SetupLocationQuery(TraceHit.ImpactPoint);
284+
QueryLocation->SetupLocationQuery(traceHit.ImpactPoint);
273285
AddTickPrerequisiteComponent(QueryLocation->ArcGISLocation);
274286
bShouldSendLocationQuery = true;
275287
}

sample_project/Source/sample_project/Geocoding/Geocoder.h

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515

1616
#pragma once
1717

18-
#include "CoreMinimal.h"
19-
#include "GameFramework/Actor.h"
18+
#include "ArcGISMapsSDK/BlueprintNodes/GameEngine/Geometry/ArcGISGeometryEngine.h"
19+
#include "ArcGISMapsSDK/BlueprintNodes/GameEngine/Geometry/ArcGISSpatialReference.h"
20+
#include "ArcGISMapsSDK/Components/ArcGISMapComponent.h"
21+
#include "ArcGISMapsSDK/Utils/ArcGISMapsSDKProjectSettings.h"
2022
#include "Blueprint/UserWidget.h"
21-
#include "Json.h"
23+
#include "EnhancedInputComponent.h"
24+
#include "EnhancedInputSubsystems.h"
25+
#include "GameFramework/Actor.h"
2226
#include "Http.h"
27+
#include "Json.h"
2328
#include "QueryLocation.h"
24-
#include "EnhancedInputSubsystems.h"
25-
#include "EnhancedInputComponent.h"
26-
#include "ArcGISMapsSDK/Components/ArcGISMapComponent.h"
27-
#include "ArcGISMapsSDK/BlueprintNodes/GameEngine/Geometry/ArcGISGeometryEngine.h"
28-
#include "ArcGISMapsSDK/BlueprintNodes/GameEngine/Geometry/ArcGISSpatialReference.h"
2929
#include "Geocoder.generated.h"
3030

3131
UCLASS()
@@ -47,20 +47,24 @@ class SAMPLE_PROJECT_API AGeocoder : public AActor
4747
virtual void BeginPlay() override;
4848

4949
private:
50-
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent);
50+
FString GetAPIKey();
5151
void ProcessAddressQueryResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSucessfully);
5252
void ProcessLocationQueryResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSucessfully);
5353
void SendLocationQuery(UArcGISPoint* InPoint);
54+
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent);
5455

55-
AQueryLocation* QueryLocation;
56-
bool bWaitingForResponse = false;
5756
bool bShouldSendLocationQuery = false;
58-
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess))
59-
TSubclassOf<class UUserWidget> UIWidgetClass;
57+
bool bWaitingForResponse = false;
58+
59+
AQueryLocation* QueryLocation;
60+
6061
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess))
6162
UUserWidget* UIWidget;
62-
UFunction* WidgetSetInfoFunction;
63+
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess))
64+
TSubclassOf<class UUserWidget> UIWidgetClass;
65+
6366
UFunction* HideInstructions;
67+
UFunction* WidgetSetInfoFunction;
6468

6569
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(AllowPrivateAccess))
6670
UInputMappingContext* MappingContext;

0 commit comments

Comments
 (0)