Skip to content

Commit 382c8c9

Browse files
authored
Merge pull request #297 from Esri/Caleb/New-FindClosestFacilityFromPoint
[New] Find closest facility from point
2 parents 5aaa293 + 2716eb2 commit 382c8c9

File tree

5 files changed

+325
-0
lines changed

5 files changed

+325
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@
231231
D76EE6082AF9AFEC00DA0325 /* FindRouteAroundBarriersView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D76EE6062AF9AFE100DA0325 /* FindRouteAroundBarriersView.Model.swift */; };
232232
D7705D582AFC244E00CC0335 /* FindClosestFacilityToMultiplePointsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7705D552AFC244E00CC0335 /* FindClosestFacilityToMultiplePointsView.swift */; };
233233
D7705D5B2AFC246A00CC0335 /* FindClosestFacilityToMultiplePointsView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D7705D552AFC244E00CC0335 /* FindClosestFacilityToMultiplePointsView.swift */; };
234+
D7705D642AFC570700CC0335 /* FindClosestFacilityFromPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7705D612AFC570700CC0335 /* FindClosestFacilityFromPointView.swift */; };
235+
D7705D662AFC575000CC0335 /* FindClosestFacilityFromPointView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D7705D612AFC570700CC0335 /* FindClosestFacilityFromPointView.swift */; };
234236
D7749AD62AF08BF50086632F /* FindRouteInTransportNetworkView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7749AD52AF08BF50086632F /* FindRouteInTransportNetworkView.Model.swift */; };
235237
D77570C02A2942F800F490CD /* AnimateImagesWithImageOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77570BF2A2942F800F490CD /* AnimateImagesWithImageOverlayView.swift */; };
236238
D77570C12A2943D900F490CD /* AnimateImagesWithImageOverlayView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D77570BF2A2942F800F490CD /* AnimateImagesWithImageOverlayView.swift */; };
@@ -365,6 +367,7 @@
365367
dstPath = "";
366368
dstSubfolderSpec = 7;
367369
files = (
370+
D7705D662AFC575000CC0335 /* FindClosestFacilityFromPointView.swift in Copy Source Code Files */,
368371
D73FD0002B02C9610006360D /* FindRouteAroundBarriersView.Views.swift in Copy Source Code Files */,
369372
D76EE6082AF9AFEC00DA0325 /* FindRouteAroundBarriersView.Model.swift in Copy Source Code Files */,
370373
D7DDF8562AF47C86004352D9 /* FindRouteAroundBarriersView.swift in Copy Source Code Files */,
@@ -625,6 +628,7 @@
625628
D769C2112A29019B00030F61 /* SetUpLocationDrivenGeotriggersView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetUpLocationDrivenGeotriggersView.swift; sourceTree = "<group>"; };
626629
D76EE6062AF9AFE100DA0325 /* FindRouteAroundBarriersView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindRouteAroundBarriersView.Model.swift; sourceTree = "<group>"; };
627630
D7705D552AFC244E00CC0335 /* FindClosestFacilityToMultiplePointsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindClosestFacilityToMultiplePointsView.swift; sourceTree = "<group>"; };
631+
D7705D612AFC570700CC0335 /* FindClosestFacilityFromPointView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindClosestFacilityFromPointView.swift; sourceTree = "<group>"; };
628632
D7749AD52AF08BF50086632F /* FindRouteInTransportNetworkView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindRouteInTransportNetworkView.Model.swift; sourceTree = "<group>"; };
629633
D77570BF2A2942F800F490CD /* AnimateImagesWithImageOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimateImagesWithImageOverlayView.swift; sourceTree = "<group>"; };
630634
D77572AD2A295DDD00F490CD /* PacificSouthWest2 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = PacificSouthWest2; sourceTree = "<group>"; };
@@ -810,6 +814,7 @@
810814
E070A0A1286F3B3400F2B606 /* Download preplanned map area */,
811815
E004A6EE284E4B7A002A1FE6 /* Download vector tiles to local cache */,
812816
1C26ED122A859525009B7721 /* Filter features in scene */,
817+
D7705D5F2AFC570700CC0335 /* Find closest facility from point */,
813818
D7705D542AFC244E00CC0335 /* Find closest facility to multiple points */,
814819
D78666A92A21616D00C60110 /* Find nearest vertex */,
815820
E066DD33285CF3A0004D3D5B /* Find route */,
@@ -1520,6 +1525,14 @@
15201525
path = "Find closest facility to multiple points";
15211526
sourceTree = "<group>";
15221527
};
1528+
D7705D5F2AFC570700CC0335 /* Find closest facility from point */ = {
1529+
isa = PBXGroup;
1530+
children = (
1531+
D7705D612AFC570700CC0335 /* FindClosestFacilityFromPointView.swift */,
1532+
);
1533+
path = "Find closest facility from point";
1534+
sourceTree = "<group>";
1535+
};
15231536
D77570BC2A29427200F490CD /* Animate images with image overlay */ = {
15241537
isa = PBXGroup;
15251538
children = (
@@ -2198,6 +2211,7 @@
21982211
D7058FB12ACB423C00A40F14 /* Animate3DGraphicView.Model.swift in Sources */,
21992212
0044CDDF2995C39E004618CE /* ShowDeviceLocationHistoryView.swift in Sources */,
22002213
E041ABC0287CA9F00056009B /* WebView.swift in Sources */,
2214+
D7705D642AFC570700CC0335 /* FindClosestFacilityFromPointView.swift in Sources */,
22012215
E088E1572862579D00413100 /* SetSurfacePlacementModeView.swift in Sources */,
22022216
1CAF831F2A20305F000E1E60 /* ShowUtilityAssociationsView.swift in Sources */,
22032217
00C7993B2A845AAF00AFE342 /* Sidebar.swift in Sources */,
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright 2023 Esri
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import ArcGIS
16+
import SwiftUI
17+
18+
struct FindClosestFacilityFromPointView: View {
19+
/// The view model for the sample.
20+
@StateObject private var model = Model()
21+
22+
/// A Boolean value indicating whether a routing operation is in progress.
23+
@State private var isRouting = false
24+
25+
/// A Boolean value indicating whether routing is currently disabled.
26+
@State private var routingIsDisabled = true
27+
28+
/// A Boolean value indicating whether the error alert is showing.
29+
@State private var errorAlertIsShowing = false
30+
31+
/// The error shown in the error alert.
32+
@State private var error: Error? {
33+
didSet { errorAlertIsShowing = error != nil }
34+
}
35+
36+
var body: some View {
37+
MapViewReader { mapViewProxy in
38+
MapView(map: model.map, graphicsOverlays: [model.graphicsOverlay])
39+
.overlay(alignment: .center) {
40+
if isRouting {
41+
ProgressView("Routing...")
42+
.padding()
43+
.background(.ultraThickMaterial)
44+
.cornerRadius(10)
45+
.shadow(radius: 50)
46+
}
47+
}
48+
.toolbar {
49+
ToolbarItemGroup(placement: .bottomBar) {
50+
Button("Solve Routes") {
51+
Task {
52+
do {
53+
isRouting = true
54+
defer { isRouting = false }
55+
56+
try await model.solveRoutes()
57+
routingIsDisabled = true
58+
} catch {
59+
self.error = error
60+
}
61+
}
62+
}
63+
.disabled(routingIsDisabled)
64+
65+
Spacer()
66+
67+
Button("Reset") {
68+
model.graphicsOverlay.removeAllGraphics()
69+
routingIsDisabled = false
70+
}
71+
.disabled(model.graphicsOverlay.graphics.isEmpty)
72+
}
73+
}
74+
.task {
75+
// Get the extents of the layers on the map.
76+
await model.map.operationalLayers.load()
77+
let layerExtents = model.map.operationalLayers.compactMap(\.fullExtent)
78+
79+
// Zoom to the extents to view the layers' features.
80+
guard let extent = GeometryEngine.combineExtents(of: layerExtents) else { return }
81+
await mapViewProxy.setViewpointGeometry(extent, padding: 30)
82+
}
83+
}
84+
.task {
85+
// Set up the closest facility parameters when the sample loads.
86+
do {
87+
try await model.configureClosestFacilityParameters()
88+
routingIsDisabled = false
89+
} catch {
90+
self.error = error
91+
}
92+
}
93+
.alert(isPresented: $errorAlertIsShowing, presentingError: error)
94+
}
95+
}
96+
97+
private extension FindClosestFacilityFromPointView {
98+
/// The view model for the sample.
99+
class Model: ObservableObject {
100+
/// A map with a streets relief basemap.
101+
let map = Map(basemapStyle: .arcGISStreetsRelief)
102+
103+
/// The graphics overlay for the route graphics.
104+
let graphicsOverlay = GraphicsOverlay()
105+
106+
/// The blue line symbol for the route graphics.
107+
private let routeSymbol = SimpleLineSymbol(
108+
style: .solid,
109+
color: UIColor(red: 0, green: 0, blue: 1, alpha: 77 / 255),
110+
width: 5
111+
)
112+
113+
/// The task for finding the closest facility.
114+
private let closestFacilityTask = ClosestFacilityTask(url: .sanDiegoNetworkAnalysis)
115+
116+
/// The parameters to be passed to the closest facility task.
117+
private var closestFacilityParameters: ClosestFacilityParameters?
118+
119+
init() {
120+
// Create the feature layers and add them to the map.
121+
addFeatureLayer(tableURL: .facilitiesLayer, imageURL: .fireStationImage)
122+
addFeatureLayer(tableURL: .incidentsLayer, imageURL: .fireImage)
123+
}
124+
125+
/// Creates the closest facility parameters and adds the facilities and incidents from the feature layers.
126+
func configureClosestFacilityParameters() async throws {
127+
// Create the default parameters from the closest facility task.
128+
async let parameters = try closestFacilityTask.makeDefaultParameters()
129+
130+
// Get the feature layers on the map.
131+
await map.operationalLayers.load()
132+
let facilitiesLayer = map.operationalLayers.first(
133+
where: { $0.name == "sandiegofacilities" }
134+
) as? FeatureLayer
135+
let incidentsLayer = map.operationalLayers.first(
136+
where: { $0.name == "sandiegoincidents" }
137+
) as? FeatureLayer
138+
139+
// Get the feature tables from the feature layers.
140+
guard let facilitiesTable = facilitiesLayer?.featureTable as? ArcGISFeatureTable,
141+
let incidentsTable = incidentsLayer?.featureTable as? ArcGISFeatureTable
142+
else { return }
143+
144+
// Create query parameters that will return all the features.
145+
let queryParameters = QueryParameters()
146+
queryParameters.whereClause = "1=1"
147+
148+
// Set the parameters' facilities and incidents using the tables.
149+
try await parameters.setFacilities(
150+
fromFeaturesIn: facilitiesTable,
151+
queryParameters: queryParameters
152+
)
153+
try await parameters.setIncidents(
154+
fromFeaturesIn: incidentsTable,
155+
queryParameters: queryParameters
156+
)
157+
closestFacilityParameters = try await parameters
158+
}
159+
160+
/// Finds the closest facility routes for the incidents.
161+
func solveRoutes() async throws {
162+
guard let closestFacilityParameters else { return }
163+
164+
// Get the closest facility result from the task using the parameters.
165+
let closestFacilityResult = try await closestFacilityTask.solveClosestFacility(
166+
using: closestFacilityParameters
167+
)
168+
169+
// Create a route graphic for each incident in the result.
170+
let incidentsIndices = closestFacilityResult.incidents.indices
171+
let routeGraphics = incidentsIndices.compactMap { incidentIndex -> Graphic? in
172+
// Get the index for the facility closest to the given incident and facility route.
173+
guard let closestFacilityIndex = closestFacilityResult.rankedIndexesOfFacilities(
174+
forIncidentAtIndex: incidentIndex
175+
).first,
176+
let closestFacilityRoute = closestFacilityResult.route(
177+
toFacilityAtIndex: closestFacilityIndex,
178+
fromIncidentAtIndex: incidentIndex
179+
) else {
180+
return nil
181+
}
182+
183+
// Create a graphic using the route's geometry.
184+
return Graphic(geometry: closestFacilityRoute.routeGeometry, symbol: routeSymbol)
185+
}
186+
187+
graphicsOverlay.addGraphics(routeGraphics)
188+
}
189+
190+
/// Creates and adds a feature layer to the map.
191+
/// - Parameters:
192+
/// - tableURL: The URL to the feature table to create the feature layer from.
193+
/// - imageURL: The URL to the image to use as the layer's renderer.
194+
private func addFeatureLayer(tableURL: URL, imageURL: URL) {
195+
// Create a layer from the feature table URL.
196+
let featureTable = ServiceFeatureTable(url: tableURL)
197+
let featureLayer = FeatureLayer(featureTable: featureTable)
198+
199+
// Create a simple renderer from the image URL and add it to the layer.
200+
let markerSymbol = PictureMarkerSymbol(url: imageURL)
201+
markerSymbol.width = 30
202+
markerSymbol.height = 30
203+
featureLayer.renderer = SimpleRenderer(symbol: markerSymbol)
204+
205+
map.addOperationalLayer(featureLayer)
206+
}
207+
}
208+
}
209+
210+
private extension URL {
211+
/// The URL to a network analysis server for San Diego, CA, USA on ArcGIS Online.
212+
static var sanDiegoNetworkAnalysis: URL {
213+
URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/NetworkAnalysis/SanDiego/NAServer/ClosestFacility")!
214+
}
215+
216+
/// The URL to a San Diego facilities feature layer on ArcGIS Online.
217+
static var facilitiesLayer: URL {
218+
URL(string: "https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/ArcGIS/rest/services/San_Diego_Facilities/FeatureServer/0")!
219+
}
220+
221+
/// The URL to a San Diego facilities feature layer on ArcGIS Online.
222+
static var incidentsLayer: URL {
223+
URL(string: "https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/ArcGIS/rest/services/San_Diego_Incidents/FeatureServer/0")!
224+
}
225+
226+
/// The URL to an image of a fire station symbol on ArcGIS Online.
227+
static var fireStationImage: URL {
228+
URL(string: "https://static.arcgis.com/images/Symbols/SafetyHealth/FireStation.png")!
229+
}
230+
231+
/// The URL to an image of a fire symbol on ArcGIS Online.
232+
static var fireImage: URL {
233+
URL(string: "https://static.arcgis.com/images/Symbols/SafetyHealth/esriCrimeMarker_56_Gradient.png")!
234+
}
235+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Find closest facility from point
2+
3+
Find routes from several locations to the respective closest facility.
4+
5+
![Image of find closest facility from point](find-closest-facility-from-point.png)
6+
7+
## Use case
8+
9+
Quickly and accurately determining the most efficient route between a location and a facility is a frequently encountered task. For example, a city's fire department may need to know which fire stations in the vicinity offer the quickest routes to multiple fires. Solving for the closest fire station to the fire's location using an impedance of "travel time" would provide this information.
10+
11+
## How to use the sample
12+
13+
Tap the "Solve Routes" button to solve and display the route from each incident (fire) to the nearest facility (fire station).
14+
15+
## How it works
16+
17+
1. Create a `ClosestFacilityTask` using a URL from an online service.
18+
2. Get the default set of `ClosestFacilityParameters` from the task: `ClosestFacilityTask.makeDefaultParameters()`.
19+
3. Create a `FeatureTable` using `ServiceFeatureTable.init(url:)`.
20+
4. Add a list of all facilities to the task parameters: `ClosestFacilityParameters.setFacilities(fromFeaturesIn:queryParameters:)`.
21+
5. Add a list of all incidents to the task parameters: `ClosestFacilityParameters.setIncidents(fromFeaturesIn:queryParameters:)`.
22+
6. Get `ClosestFacilityResult` by solving the task with the provided parameters: `ClosestFacilityTask.solveClosestFacility(using:)`.
23+
7. Find the closest facility for each incident by iterating over the list of `Incident`s.
24+
8. Display the route as a `Graphic` using the `ClosestFacilityRoute.routeGeometry`.
25+
26+
## Relevant API
27+
28+
* ClosestFacilityParameters
29+
* ClosestFacilityResult
30+
* ClosestFacilityRoute
31+
* ClosestFacilityTask
32+
* Facility
33+
* Graphic
34+
* GraphicsOverlay
35+
* Incident
36+
37+
## Tags
38+
39+
incident, network analysis, route, search
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"category": "Routing and Logistics",
3+
"description": "Find routes from several locations to the respective closest facility.",
4+
"ignore": false,
5+
"images": [
6+
"find-closest-facility-from-point.png"
7+
],
8+
"keywords": [
9+
"incident",
10+
"network analysis",
11+
"route",
12+
"search",
13+
"ClosestFacilityParameters",
14+
"ClosestFacilityResult",
15+
"ClosestFacilityRoute",
16+
"ClosestFacilityTask",
17+
"Facility",
18+
"Graphic",
19+
"GraphicsOverlay",
20+
"Incident"
21+
],
22+
"redirect_from": [],
23+
"relevant_apis": [
24+
"ClosestFacilityParameters",
25+
"ClosestFacilityResult",
26+
"ClosestFacilityRoute",
27+
"ClosestFacilityTask",
28+
"Facility",
29+
"Graphic",
30+
"GraphicsOverlay",
31+
"Incident"
32+
],
33+
"snippets": [
34+
"FindClosestFacilityFromPointView.swift"
35+
],
36+
"title": "Find closest facility from point"
37+
}
238 KB
Loading

0 commit comments

Comments
 (0)