Find features in a spatial table related to features in a non-spatial table.
Use case
The non-spatial tables contained by a map service may contain additional information about sublayer features. Such information can be accessed by traversing table relationships defined in the service.
How to use the sample
Tap the toolbar button to prompt a list of comment data from non-spatial features. Tap on one of the comments to query related spatial features and display the first result on the map.
How it works
- Create an
AGSArcGISMapImageLayer
with the URL of a map service. - Load the tables and layers using
loadTablesAndLayers(completion:)
and get its first table. - To query the table, create
AGSQueryParameters
. Set itswhereClause
to filter the request features. - Use
queryFeatures(with:queryFeatureFields:completion:)
to get theAGSFeatureQueryResult
. - Make
AGSFeatureQueryResult
iterable usingfeatureEnumerator()
and loop through to get eachAGSFeature
. - To query for related features, get the table's relationship info with
AGSServiceFeatureTable.layerInfo.relationshipInfos
. This returns an array ofAGSRelationshipInfo
s. - Now create
AGSRelatedQueryParameters
passing in theAGSRelationshipInfo
. To query related features, useAGSServiceFeatureTable.queryRelatedFeatures(for:parameters:completion:)
. - This returns an array of
AGSRelatedFeatureQueryResult
s, each containing a set of related features.
Relevant API
- AGSArcGISFeature
- AGSArcGISMapImageLayer
- AGSFeature
- AGSFeatureQueryResult
- AGSQueryParameters
- AGSRelatedFeatureQueryResult
- AGSRelatedQueryParameters
- AGSRelationshipInfo
- AGSServiceFeatureTable
About the data
This sample uses the Naperville map service, which is used to collect non-emergency requests for service from the general public.
Tags
features, query, related features, search
Sample Code
// Copyright 2022 Esri.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
import ArcGIS
class QueryRelatedFeaturesNonSpatialTableViewController: UIViewController {
@IBOutlet var mapView: AGSMapView! {
didSet {
// Assign the map to the map view.
mapView.map = makeMap()
// Set the viewpoint.
mapView.setViewpoint(AGSViewpoint(latitude: 41.734152, longitude: -88.163718, scale: 2e5))
// Add a graphics overlay to show selected features and add it to the map view.
mapView.graphicsOverlays.add(selectedFeaturesOverlay)
}
}
@IBOutlet var queryBarButtonItem: UIBarButtonItem!
/// The map image layer that uses the service URL.
let serviceRequestsMapImageLayer = AGSArcGISMapImageLayer(
url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/ServiceRequest/MapServer")!
)
/// The (non-spatial) table that contains the service request comments.
var commentsTable: AGSServiceFeatureTable?
/// The array to store the possible comments.
var comments: [AGSFeature] = []
/// The graphics overlay to add graphics to.
let selectedFeaturesOverlay: AGSGraphicsOverlay = {
let overlay = AGSGraphicsOverlay()
overlay.renderer = AGSSimpleRenderer(
symbol: AGSSimpleMarkerSymbol(style: .circle, color: .cyan, size: 14)
)
return overlay
}()
@IBAction func queryFeaturesActions(_ sender: UIBarButtonItem) {
// Create an action sheet to display the various comments to choose from.
let alertController = UIAlertController(title: "Related Service Requests", message: "Select a comment to view related spatial features on the map.", preferredStyle: .actionSheet)
// Create an action for each comment.
comments.forEach { feature in
// Extract the "comments" attribute as a string.
let title = feature.attributes["comments"] as! String
// Create an action with the comments title.
let action = UIAlertAction(title: title, style: .default) { _ in
// Clear the former graphics.
self.selectedFeaturesOverlay.graphics.removeAllObjects()
// Disable the query button while the feature loads.
self.queryBarButtonItem.isEnabled = false
// Cast the selected feature as an AGSArcGISFeature.
guard let selectedFeature = feature as? AGSArcGISFeature else { return }
self.queryCommentsTable(feature: selectedFeature)
}
// Add the action to the controller.
alertController.addAction(action)
}
// Add "cancel" item.
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
alertController.addAction(cancelAction)
// Present the controller.
alertController.popoverPresentationController?.barButtonItem = queryBarButtonItem
present(alertController, animated: true)
}
/// Make a map for the map view.
func makeMap() -> AGSMap {
// Create a map with the ArcGIS streets basemap style.
let map = AGSMap(basemapStyle: .arcGISStreets)
// Add the layer to the map.
map.operationalLayers.add(serviceRequestsMapImageLayer)
// Load the map image layer's tables and layers.
serviceRequestsMapImageLayer.loadTablesAndLayers { [weak self] _ in
self?.queryFeatures()
}
return map
}
/// Query features on the first table in the map image layer.
func queryFeatures() {
// Create query parameters and set its where clause.
let nullCommentsParameters = AGSQueryParameters()
nullCommentsParameters.whereClause = "requestid <> '' AND comments <> ''"
// Set the first table from the map image layer.
commentsTable = serviceRequestsMapImageLayer.tables.first
// Query features on the feature table with the query parameters and all feature fields.
commentsTable?.queryFeatures(with: nullCommentsParameters, queryFeatureFields: .loadAll) { [weak self] result, error in
guard let self = self else { return }
if let comments = result?.featureEnumerator().allObjects {
// Show the records from the service request comments table in the list view control.
self.comments = comments
// Enable the button after the map and features have been loaded.
self.queryBarButtonItem.isEnabled = true
} else if let error = error {
self.presentAlert(error: error)
}
}
}
/// Query related features for the selected feature.
func queryCommentsTable(feature: AGSArcGISFeature) {
// Get the relationship that defines related service request features for features in the comments table (this is the first and only relationship).
guard let relationshipInfo = commentsTable?.layerInfo?.relationshipInfos.first else { return }
// Create query parameters to get the related service request for features in the comments table.
let relatedQueryParameters = AGSRelatedQueryParameters(relationshipInfo: relationshipInfo)
relatedQueryParameters.returnGeometry = true
// Query related features for the selected comment and its related query parameters.
commentsTable?.queryRelatedFeatures(for: feature, parameters: relatedQueryParameters) { [weak self] results, error in
// Get the first related feature.
if let relatedFeature = results?.first?.featureEnumerator().nextObject() as? AGSArcGISFeature {
// Load the feature and get its geometry to show as a graphic on the map.
relatedFeature.load { error in
guard let self = self else { return }
if let error = error {
self.presentAlert(error: error)
} else if let serviceRequestPoint = relatedFeature.geometry as? AGSPoint {
// Create a graphic to add to the graphics overlay.
let graphic = AGSGraphic(geometry: serviceRequestPoint, symbol: nil)
self.selectedFeaturesOverlay.graphics.add(graphic)
// Set the viewpoint to the the related feature.
self.mapView.setViewpointCenter(serviceRequestPoint, scale: 150_000)
// Enable the button after the feature has finished loading.
self.queryBarButtonItem.isEnabled = true
}
}
} else {
// Present an error message is the related feature is not found.
self?.presentAlert(title: "Related feature not found. No Feature", message: nil)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Add the source code button item to the right of navigation bar.
(self.navigationItem.rightBarButtonItem as? SourceCodeBarButtonItem)?.filenames = ["QueryRelatedFeaturesNonSpatialTableViewController"]
}
}