Skip to content

Commit 240e862

Browse files
committed
feat(geo): add AWSLocationGeoPlugin (#1453)
* feat(geo): Adds AWSLocationGeoPlugin with support for maps and search capabilities.
1 parent 8b19c11 commit 240e862

File tree

46 files changed

+3692
-464
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3692
-464
lines changed

Amplify.xcodeproj/project.pbxproj

100755100644
Lines changed: 173 additions & 452 deletions
Large diffs are not rendered by default.

Amplify/Categories/Geo/GeoCategory+ClientBehavior.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,14 @@ extension GeoCategory: GeoCategoryBehavior {
5252
// MARK: - Maps
5353

5454
/// Retrieves metadata for available Map resources.
55-
/// - Parameter completionHandler: The completion handler receives a Response
56-
/// object. The success case provides an array of available Map resources.
55+
/// - Returns: Metadata for all available map resources.
5756
public func getAvailableMaps() -> [Geo.MapStyle] {
5857
plugin.getAvailableMaps()
5958
}
6059

6160
/// Retrieves the default Map resource.
62-
/// - Parameter completionHandler: The completion handler receives a Response
63-
/// object. The success case provides an array of available Map resources.
64-
public func getDefaultMap() -> Geo.MapStyle {
61+
/// - Returns: Metadata for the default map resource.
62+
public func getDefaultMap() -> Geo.MapStyle? {
6563
plugin.getDefaultMap()
6664
}
6765
}

Amplify/Categories/Geo/GeoCategoryBehavior.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,10 @@ public protocol GeoCategoryBehavior {
4444
// MARK: - Maps
4545

4646
/// Retrieves metadata for available Map resources.
47-
/// - Parameter completionHandler: The completion handler receives a Response
48-
/// object. The success case provides an array of available Map resources.
47+
/// - Returns: Metadata for all available map resources.
4948
func getAvailableMaps() -> [Geo.MapStyle]
5049

5150
/// Retrieves the default Map resource (first map in amplifyconfiguration.json).
52-
/// - Parameter completionHandler: The completion handler receives a Response
53-
/// object. The success case provides an array of available Map resources.
54-
func getDefaultMap() -> Geo.MapStyle
51+
/// - Returns: Metadata for the default map resource.
52+
func getDefaultMap() -> Geo.MapStyle?
5553
}

Amplify/Categories/Geo/Types/Geo+MapStyle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
public extension Geo {
1111
/// Identifies the name and style for a map resource.
12-
struct MapStyle {
12+
struct MapStyle: Equatable {
1313
/// The name of the map resource.
1414
public let mapName: String
1515
/// The map style selected from an available provider.

AmplifyPlugins.podspec

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ Pod::Spec.new do |s|
5555
ss.dependency 'SQLite.swift', '0.12.2'
5656
end
5757

58+
s.subspec 'AWSLocationGeoPlugin' do |ss|
59+
ss.source_files = 'AmplifyPlugins/Geo/AWSLocationGeoPlugin/**/*.swift'
60+
ss.dependency 'AWSLocation', $OPTIMISTIC_AWS_SDK_VERSION
61+
end
62+
5863
s.subspec 'AWSPinpointAnalyticsPlugin' do |ss|
5964
ss.source_files = 'AmplifyPlugins/Analytics/AWSPinpointAnalyticsPlugin/**/*.swift'
6065
ss.dependency 'AWSPinpoint', $OPTIMISTIC_AWS_SDK_VERSION
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
import Foundation
10+
11+
#if COCOAPODS
12+
import AWSLocation
13+
#else
14+
import AWSLocationXCF
15+
#endif
16+
17+
extension AWSLocationGeoPlugin {
18+
19+
// MARK: - Search
20+
21+
/// Search for places or points of interest.
22+
/// - Parameters:
23+
/// - text: The place name or address to be used in the search. (case insensitive)
24+
/// - area: The area (.near or .boundingBox) for the search. (optional)
25+
/// - countries: Limits the search to the given a list of countries/regions. (optional)
26+
/// - maxResults: The maximum number of results returned per request. (optional)
27+
/// - placeIndexName: The name of the Place Index to query. (optional)
28+
/// - completionHandler: The completion handler receives a Response object. The
29+
/// success case provides a Place array.
30+
public func search(for text: String,
31+
area: Geo.SearchArea? = nil,
32+
countries: [Geo.Country]? = nil,
33+
maxResults: Int? = nil,
34+
placeIndexName: String? = nil,
35+
completionHandler: @escaping Geo.ResultsHandler<[Geo.Place]>) {
36+
37+
assert(pluginConfig.defaultSearchIndex != nil, GeoPluginConfigError.searchConfigMissing)
38+
39+
let request = AWSLocationSearchPlaceIndexForTextRequest()!
40+
request.indexName = placeIndexName ?? pluginConfig.defaultSearchIndex
41+
request.text = text
42+
43+
if let area = area {
44+
switch area {
45+
case .near(let coordinates):
46+
request.biasPosition = [coordinates.longitude as NSNumber,
47+
coordinates.latitude as NSNumber]
48+
case .within(let boundingBox):
49+
request.filterBBox = [boundingBox.southwest.longitude as NSNumber,
50+
boundingBox.southwest.latitude as NSNumber,
51+
boundingBox.northeast.longitude as NSNumber,
52+
boundingBox.northeast.latitude as NSNumber]
53+
}
54+
}
55+
56+
if let countries = countries {
57+
request.filterCountries = countries.map { country in
58+
String(describing: country)
59+
}
60+
}
61+
62+
if let maxResults = maxResults {
63+
request.maxResults = maxResults as NSNumber
64+
}
65+
66+
locationService.searchPlaceIndex(forText: request) { response, error in
67+
completionHandler(AWSLocationGeoPlugin.parsePlaceResponse(response: response, error: error))
68+
}
69+
}
70+
71+
/// Reverse geocodes a given pair of coordinates and returns a list of Places
72+
/// closest to the specified position.
73+
/// - Parameters:
74+
/// - coordinates: Specifies a coordinate for the query.
75+
/// - maxResults: The maximum number of results returned per request. (optional)
76+
/// - placeIndexName: The name of the Place Index to query. (optional)
77+
/// - completionHandler: The completion handler receives a Response object. The
78+
/// success case provides a Place array.
79+
public func search(for coordinates: Geo.Coordinates,
80+
maxResults: Int? = nil,
81+
placeIndexName: String? = nil,
82+
completionHandler: @escaping Geo.ResultsHandler<[Geo.Place]>) {
83+
84+
assert(pluginConfig.defaultSearchIndex != nil, GeoPluginConfigError.searchConfigMissing)
85+
86+
let request = AWSLocationSearchPlaceIndexForPositionRequest()!
87+
request.indexName = placeIndexName ?? pluginConfig.defaultSearchIndex
88+
request.position = [coordinates.longitude as NSNumber,
89+
coordinates.latitude as NSNumber]
90+
91+
if let maxResults = maxResults {
92+
request.maxResults = maxResults as NSNumber
93+
}
94+
95+
locationService.searchPlaceIndex(forPosition: request) { response, error in
96+
completionHandler(AWSLocationGeoPlugin.parsePlaceResponse(response: response, error: error))
97+
}
98+
}
99+
100+
static private func parsePlaceResponse(response: AWSModel?, error: Error?) -> Result<[Geo.Place], Error> {
101+
if let error = error {
102+
return .failure(error)
103+
}
104+
105+
var results = [AWSLocationPlace]()
106+
107+
if let responseResults = (response as? AWSLocationSearchPlaceIndexForTextResponse)?.results {
108+
results = responseResults.compactMap {
109+
$0.place
110+
}
111+
}
112+
113+
if let responseResults = (response as? AWSLocationSearchPlaceIndexForPositionResponse)?.results {
114+
results = responseResults.compactMap {
115+
$0.place
116+
}
117+
}
118+
119+
let places: [Geo.Place] = results.compactMap {
120+
guard let long = $0.geometry?.point?.first as? Double,
121+
let lat = $0.geometry?.point?.last as? Double
122+
else {
123+
return nil
124+
}
125+
126+
return Geo.Place(coordinates: Geo.Coordinates(latitude: lat, longitude: long),
127+
label: $0.label,
128+
addressNumber: $0.addressNumber,
129+
street: $0.street,
130+
municipality: $0.municipality,
131+
region: $0.region,
132+
subRegion: $0.subRegion,
133+
postalCode: $0.postalCode,
134+
country: $0.country)
135+
}
136+
137+
return .success(places)
138+
}
139+
140+
// MARK: - Maps
141+
142+
/// Retrieves metadata for available map resources.
143+
/// - Returns: Metadata for all available map resources.
144+
public func getAvailableMaps() -> [Geo.MapStyle] {
145+
let mapStyles = Array(pluginConfig.maps.values)
146+
assert(!mapStyles.isEmpty, GeoPluginConfigError.mapConfigMissing)
147+
148+
return mapStyles
149+
}
150+
151+
/// Retrieves the default map resource.
152+
/// - Returns: Metadata for the default map resource.
153+
public func getDefaultMap() -> Geo.MapStyle? {
154+
guard let mapName = pluginConfig.defaultMap, let mapStyle = pluginConfig.maps[mapName] else {
155+
assertionFailure(GeoPluginConfigError.mapConfigMissing)
156+
return nil
157+
}
158+
159+
return mapStyle
160+
}
161+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
import AWSPluginsCore
10+
import Foundation
11+
12+
#if COCOAPODS
13+
import AWSLocation
14+
#else
15+
import AWSLocationXCF
16+
#endif
17+
18+
extension AWSLocationGeoPlugin {
19+
/// Configures AWSLocationPlugin with the specified configuration.
20+
///
21+
/// This method will be invoked as part of the Amplify configuration flow.
22+
///
23+
/// - Parameter configuration: The configuration specified for this plugin.
24+
/// - Throws:
25+
/// - PluginError.pluginConfigurationError: If one of the configuration values is invalid or empty.
26+
public func configure(using configuration: Any?) throws {
27+
let pluginConfiguration = try AWSLocationGeoPluginConfiguration(config: configuration)
28+
try configure(using: pluginConfiguration)
29+
}
30+
31+
/// Configure AWSLocationPlugin programatically using AWSLocationPluginConfiguration
32+
public func configure(using configuration: AWSLocationGeoPluginConfiguration) throws {
33+
let authService = AWSAuthService()
34+
let credentialsProvider = authService.getCredentialsProvider()
35+
let region = configuration.region
36+
37+
let serviceConfiguration = AmplifyAWSServiceConfiguration(region: region,
38+
credentialsProvider: credentialsProvider)
39+
AWSLocation.register(with: serviceConfiguration,
40+
forKey: key)
41+
42+
let location = AWSLocation(forKey: key)
43+
let locationService = AWSLocationAdapter(location: location)
44+
45+
configure(locationService: locationService,
46+
authService: authService,
47+
pluginConfig: configuration)
48+
}
49+
50+
// MARK: - Internal
51+
52+
/// Internal configure method to set the properties of the plugin
53+
///
54+
/// Called from the configure method which implements the Plugin protocol. Useful for testing by passing in mocks.
55+
///
56+
/// - Parameters:
57+
/// - locationService: The location service object.
58+
/// - authService: The authentication service object.
59+
/// - pluginConfig: The configuration for the plugin.
60+
func configure(locationService: AWSLocationBehavior,
61+
authService: AWSAuthServiceBehavior,
62+
pluginConfig: AWSLocationGeoPluginConfiguration) {
63+
self.locationService = locationService
64+
self.authService = authService
65+
self.pluginConfig = pluginConfig
66+
}
67+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
10+
extension AWSLocationGeoPlugin: DefaultLogger {}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
import Foundation
10+
11+
extension AWSLocationGeoPlugin {
12+
13+
/// Resets the state of the plugin.
14+
///
15+
/// Sets stored objects to nil to allow deallocation, then calls onComplete closure
16+
/// to signal the reset has completed.
17+
public func reset(onComplete: @escaping (() -> Void)) {
18+
locationService = nil
19+
authService = nil
20+
pluginConfig = nil
21+
22+
onComplete()
23+
}
24+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
import AWSPluginsCore
10+
import Foundation
11+
12+
#if COCOAPODS
13+
import AWSLocation
14+
#else
15+
import AWSLocationXCF
16+
#endif
17+
18+
/// The AWSLocationPlugin implements the Geo APIs for Amazon Location
19+
public final class AWSLocationGeoPlugin: GeoCategoryPlugin {
20+
/// An instance of the AWS Location service
21+
var locationService: AWSLocationBehavior!
22+
23+
/// An instance of the authentication service
24+
public var authService: AWSAuthServiceBehavior!
25+
26+
/// A holder for the plugin configuration. This will be populated during the
27+
/// configuration phase, and is clearable by `reset()`.
28+
public var pluginConfig: AWSLocationGeoPluginConfiguration!
29+
30+
/// The unique key of the plugin within the location category
31+
public let key: PluginKey = "awsLocationGeoPlugin"
32+
33+
/// Instantiates an instance of the AWSLocationPlugin
34+
public init() {}
35+
36+
/// Retrieve the escape hatch to perform actions directly on AWSLocation.
37+
///
38+
/// - Returns: AWSLocation instance
39+
public func getEscapeHatch() -> AWSLocation {
40+
locationService.getEscapeHatch()
41+
}
42+
}
43+
44+
extension AWSLocationGeoPlugin: AmplifyVersionable { }

0 commit comments

Comments
 (0)