Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,27 @@ If no target org is found, deploy exits with setup guidance.

## Configure Salesforce

After deploy, open **Setup** and configure the **Vector Map Geocoding** custom setting. This is a **hierarchy** custom setting: use **Manage** to set **organization** defaults so every user gets the same vector tile URL, geocoding provider, and (if needed) NSW Point API key.
After deploy, open **Setup** and configure the **Vector Map Geocoding** custom setting. This is a **hierarchy** custom setting: use **Manage** to set **organization** defaults so every user gets the same vector tile URL and geocoding provider.

**Where to find it:** **Setup** → **Custom Settings** → **Vector Map Geocoding** → **Manage** (set values at the **Organization** level unless you need per-profile overrides).

![Custom Settings list — open Vector Map Geocoding and Manage](docs/images/CustomSettingsOverview.png)

On the manage screen, create or edit the **default organization level** row. Set:

- **Geocoding Provider** — `OpenStreetMap` (Nominatim, default), `NSW_Point` (NSW Point geocoding; requires **NSW Point API Key**), or `None` (no external HTTP geocoding; use Salesforce compound Address lat/lng only).
- **NSW Point API Key** — required only when **Geocoding Provider** is NSW Point; used by Apex as the `x-api-key` header for NSW Point Geocode Address.
- **Geocoding Provider** — `OpenStreetMap` (Nominatim, default), `NSW_Point` (NSW Point geocoding; requires **NSW Point Named Credential** setup below), or `None` (no external HTTP geocoding; use Salesforce compound Address lat/lng only).
- **Vector Tile Service URL** — optional ArcGIS **VectorTileServer** URL (must end with `VectorTileServer`). Leave blank to use the built-in NSW Spatial Services basemap URL (same default as Apex and the LWC).

![Manage Vector Map Geocoding — organization defaults](docs/images/CustomSettingsEdit.png)

### NSW Point API key (Named Credential)

When **Geocoding Provider** is `NSW_Point`, store the API key in the **NSW Point Geocode** Named Credential (not in Custom Settings):

1. **Setup** → **Named Credentials** → **NSW Point Geocode** → **External Credentials** tab → principal **NSW Point API Key**.
2. Set the **ApiKey** authentication parameter to your NSW Point API key (used as the `x-api-key` header on callouts).
3. Assign the **Vector Map NSW Point Geocode** permission set to users who need NSW Point geocoding (grants access to the External Credential principal).

The install script should setup remote sites to allow Salesforce to reach these services, but for reference they should look like this:

![Remote Site Settings](docs/images/RemoteSettings.png)
Expand Down
38 changes: 12 additions & 26 deletions force-app/main/default/classes/VectorMapGeocodeController.cls
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public with sharing class VectorMapGeocodeController {
private static final String SETTING_PROVIDER_NSW = 'NSW_Point';
/** Custom Setting: no HTTP callouts; compound Address lat/lng only. */
private static final String SETTING_PROVIDER_NONE = 'None';
private static final String NSW_POINT_GEOCODE =
'https://point.digital.nsw.gov.au/v3/api/geocodeAddress';
/** Named Credential for NSW Point Geocode Address (x-api-key stored in External Credential). */
private static final String NSW_POINT_NAMED_CREDENTIAL = 'NSW_Point_Geocode';
private static final String NOMINATIM_SEARCH =
'https://nominatim.openstreetmap.org/search';
private static final String NOMINATIM_USER_AGENT =
Expand Down Expand Up @@ -75,13 +75,7 @@ public with sharing class VectorMapGeocodeController {
return out;
}
if (SETTING_PROVIDER_NSW.equals(providerSetting)) {
String nswKey = getNswPointApiKey();
if (String.isBlank(nswKey)) {
out.errorMessage =
'Geocoding Provider is NSW Point but NSW Point API Key is not set in Custom Settings.';
return out;
}
return tryNswPoint(trimmed, nswKey);
return tryNswPoint(trimmed);
}

return tryNominatim(trimmed);
Expand Down Expand Up @@ -112,17 +106,9 @@ public with sharing class VectorMapGeocodeController {
return SETTING_PROVIDER_OSM;
}

private static String getNswPointApiKey() {
Vector_Map_Geocoding__c cfg = Vector_Map_Geocoding__c.getOrgDefaults();
if (cfg == null || String.isBlank(cfg.NSW_Point_API_Key__c)) {
return null;
}
return cfg.NSW_Point_API_Key__c.trim();
}

private static GeocodeResult tryNswPoint(String address, String apiKey) {
private static GeocodeResult tryNswPoint(String address) {
GeocodeResult out = new GeocodeResult();
HttpRequest req = buildNswPointGeocodeRequest(address, apiKey);
HttpRequest req = buildNswPointGeocodeRequest(address);
HttpResponse res = sendHttpRequest(req);
if (res == null) {
out.errorMessage = 'NSW Point request failed.';
Expand Down Expand Up @@ -181,19 +167,19 @@ public with sharing class VectorMapGeocodeController {
}
}

/** NSW Point Geocode Address: GET with x-api-key header. */
private static HttpRequest buildNswPointGeocodeRequest(
String address,
String apiKey
) {
/** NSW Point Geocode Address: GET via Named Credential (x-api-key from External Credential). */
private static HttpRequest buildNswPointGeocodeRequest(String address) {
String encodedAddress = EncodingUtil.urlEncode(address, 'UTF-8');
String endpoint = NSW_POINT_GEOCODE + '?address=' + encodedAddress;
String endpoint =
'callout:' +
NSW_POINT_NAMED_CREDENTIAL +
'/v3/api/geocodeAddress?address=' +
encodedAddress;
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('GET');
req.setHeader('Accept', 'application/json');
req.setHeader('Accept-Crs', '');
req.setHeader('x-api-key', apiKey);
req.setTimeout(HTTP_TIMEOUT_MS);
return req;
}
Expand Down
37 changes: 20 additions & 17 deletions force-app/main/default/classes/VectorMapGeocodeControllerTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ private class VectorMapGeocodeControllerTest {
!req.getEndpoint().contains('subscription-key='),
'NSW Point auth uses x-api-key header, not subscription-key query param'
);
System.assertEquals(
'unit-test-nsw-key',
req.getHeader('x-api-key')
);
res.setStatusCode(200);
res.setBody('{"latitude":-33.8688,"longitude":151.2093}');
return res;
Expand Down Expand Up @@ -58,6 +54,15 @@ private class VectorMapGeocodeControllerTest {
}
}

private class NswHttp401Mock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(401);
res.setBody('{"error":"Unauthorized"}');
return res;
}
}

@IsTest
static void geocodeAddress_blank_returnsError() {
Test.startTest();
Expand All @@ -71,7 +76,7 @@ private class VectorMapGeocodeControllerTest {

@IsTest
static void geocodeAddress_nswPoint_success() {
insertSettings('unit-test-nsw-key', 'NSW_Point');
insertSettings('NSW_Point');
Test.setMock(HttpCalloutMock.class, new NswPointSuccessMock());
Test.startTest();
VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress(
Expand All @@ -86,7 +91,7 @@ private class VectorMapGeocodeControllerTest {

@IsTest
static void geocodeAddress_nswPoint_parsesEsriCandidatesJson() {
insertSettings('key-esri', 'NSW_Point');
insertSettings('NSW_Point');
Test.setMock(HttpCalloutMock.class, new NswPointEsriCandidatesMock());
Test.startTest();
VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress(
Expand Down Expand Up @@ -140,7 +145,7 @@ private class VectorMapGeocodeControllerTest {

@IsTest
static void geocodeAddress_nswPoint_httpError_returnsError() {
insertSettings('key-for-test', 'NSW_Point');
insertSettings('NSW_Point');
Test.setMock(HttpCalloutMock.class, new NswHttp500Mock());
Test.startTest();
VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress(
Expand All @@ -153,28 +158,26 @@ private class VectorMapGeocodeControllerTest {
}

@IsTest
static void geocodeAddress_nswPoint_noApiKey_returnsError() {
insert new Vector_Map_Geocoding__c(
SetupOwnerId = UserInfo.getOrganizationId(),
Geocoding_Provider__c = 'NSW_Point',
NSW_Point_API_Key__c = null
);
static void geocodeAddress_nswPoint_unauthorized_returnsError() {
insertSettings('NSW_Point');
Test.setMock(HttpCalloutMock.class, new NswHttp401Mock());
Test.startTest();
VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress(
'Sydney'
);
Test.stopTest();
System.assertNotEquals(null, r.errorMessage);
System.assert(
r.errorMessage.contains('API Key'),
'Expected message about API key: ' + r.errorMessage
r.errorMessage.contains('401'),
'Expected HTTP 401 in error: ' + r.errorMessage
);
System.assertEquals(null, r.latitude);
System.assertEquals(null, r.longitude);
}

private static void insertSettings(String apiKey, String geocodingProvider) {
private static void insertSettings(String geocodingProvider) {
insert new Vector_Map_Geocoding__c(
SetupOwnerId = UserInfo.getOrganizationId(),
NSW_Point_API_Key__c = apiKey,
Geocoding_Provider__c = geocodingProvider
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<ExternalCredential xmlns="http://soap.sforce.com/2006/04/metadata">
<authenticationProtocol>Custom</authenticationProtocol>
<externalCredentialParameters>
<parameterGroup>NSW_Point_API_Key</parameterGroup>
<parameterName>NSW_Point_API_Key</parameterName>
<parameterType>NamedPrincipal</parameterType>
<sequenceNumber>1</sequenceNumber>
</externalCredentialParameters>
<externalCredentialParameters>
<parameterGroup>NSW_Point_API_Key</parameterGroup>
<parameterName>ApiKey</parameterName>
<parameterType>AuthParameter</parameterType>
<sequenceNumber>2</sequenceNumber>
</externalCredentialParameters>
<externalCredentialParameters>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>x-api-key</parameterName>
<parameterType>AuthHeader</parameterType>
<parameterValue>{!$Credential.NSW_Point_Geocode.ApiKey}</parameterValue>
<sequenceNumber>1</sequenceNumber>
</externalCredentialParameters>
<label>NSW Point Geocode</label>
</ExternalCredential>
2 changes: 1 addition & 1 deletion force-app/main/default/lwc/vectorMap/vectorMap.html
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ <h3 class="details-heading slds-text-heading_small slds-m-bottom_x-small">Addres
<template if:true={enableExternalGeocoding}>
<p class="address-coord-hint slds-m-top_x-small" role="note">
External geocoding is on. Pins are placed using coordinates looked up from the address text
(provider and key in Custom Settings — Vector Map Geocoding). Salesforce compound coordinates
(provider in Custom Settings — Vector Map Geocoding; NSW Point API key in Named Credentials — NSW Point Geocode). Salesforce compound coordinates
are used as fallback when the lookup returns no result.
</p>
</template>
Expand Down
2 changes: 1 addition & 1 deletion force-app/main/default/lwc/vectorMap/vectorMap.js-meta.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
type="Boolean"
label="External geocoding"
default="true"
description="When true (recommended), geocode from the displayed address text via Apex; coordinates override Salesforce compound lat/lng. Provider and NSW Point API key: Setup — Custom SettingsVector Map Geocoding. Turn off to use only Salesforce Address coordinates. Respect Nominatim usage policy when using OpenStreetMap."
description="When true (recommended), geocode from the displayed address text via Apex; coordinates override Salesforce compound lat/lng. Provider: Setup — Custom Settings — Vector Map Geocoding. NSW Point API key: Setup — Named CredentialsNSW Point Geocode. Turn off to use only Salesforce Address coordinates. Respect Nominatim usage policy when using OpenStreetMap."
/>
<property
name="addressFieldNames"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata">
<allowMergeFieldsInBody>false</allowMergeFieldsInBody>
<allowMergeFieldsInHeader>false</allowMergeFieldsInHeader>
<calloutStatus>Enabled</calloutStatus>
<generateAuthorizationHeader>false</generateAuthorizationHeader>
<label>NSW Point Geocode</label>
<namedCredentialParameters>
<parameterName>Url</parameterName>
<parameterType>Url</parameterType>
<parameterValue>https://point.digital.nsw.gov.au</parameterValue>
</namedCredentialParameters>
<namedCredentialParameters>
<externalCredential>NSW_Point_Geocode</externalCredential>
<parameterName>ExternalCredential</parameterName>
<parameterType>Authentication</parameterType>
</namedCredentialParameters>
<namedCredentialType>SecuredEndpoint</namedCredentialType>
</NamedCredential>
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<customSettingsType>Hierarchy</customSettingsType>
<description>Org-level settings for Vector Map: vector tile URL, external geocoding provider, and NSW Point API key. Used by Vector Map and Spatial Services Map.</description>
<description>Org-level settings for Vector Map: vector tile URL and external geocoding provider. NSW Point API key is stored in the NSW Point Geocode Named Credential.</description>
<enableFeeds>false</enableFeeds>
<label>Vector Map Geocoding</label>
<visibility>Public</visibility>
<visibility>Protected</visibility>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Geocoding_Provider__c</fullName>
<description>Address lookup service: OpenStreetMap (Nominatim, default), NSW_Point (requires NSW Point API Key), or None (Salesforce Address latitude/longitude only; no external lookup). Blank defaults to OpenStreetMap.</description>
<description>Address lookup service: OpenStreetMap (Nominatim, default), NSW_Point (requires NSW Point Geocode Named Credential), or None (Salesforce Address latitude/longitude only; no external lookup). Blank defaults to OpenStreetMap.</description>
<externalId>false</externalId>
<label>Geocoding Provider</label>
<length>32</length>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Grants access to the NSW Point Geocode Named Credential used by Vector Map external geocoding.</description>
<externalCredentialPrincipalAccesses>
<enabled>true</enabled>
<externalCredentialPrincipal>NSW_Point_Geocode-NSW_Point_API_Key</externalCredentialPrincipal>
</externalCredentialPrincipalAccesses>
<hasActivationRequired>false</hasActivationRequired>
<label>Vector Map NSW Point Geocode</label>
</PermissionSet>
11 changes: 7 additions & 4 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,22 @@ deploy_phase() {
sf project deploy start "${COMMON_ARGS[@]}" "$@"
}

# 1) Foundation metadata: endpoint allowlists and custom object schema.
deploy_phase "Phase 1/3: Remote Site Settings + Custom Object schema" \
# 1) Foundation metadata: endpoint allowlists, credentials, and custom object schema.
deploy_phase "Phase 1/4: Remote Site Settings + Credentials + Custom Object schema" \
--source-dir force-app/main/default/remoteSiteSettings \
--source-dir force-app/main/default/externalCredentials \
--source-dir force-app/main/default/namedCredentials \
--source-dir force-app/main/default/permissionsets \
--source-dir force-app/main/default/objects

# 2) Server-side logic + VF wrapper pages.
deploy_phase "Phase 2/3: Apex Classes + Visualforce Pages" \
deploy_phase "Phase 2/4: Apex Classes + Visualforce Pages" \
--source-dir force-app/main/default/classes \
--source-dir force-app/main/default/pages \
--test-level "$TEST_LEVEL"

# 3) Front-end assets and LWC.
deploy_phase "Phase 3/3: Static Resources + LWC" \
deploy_phase "Phase 3/4: Static Resources + LWC" \
--source-dir force-app/main/default/staticresources \
--source-dir force-app/main/default/lwc

Expand Down