Get a list of suitable transformations for projecting a geometry between two spatial references with different horizontal datums.
Use case
Transformations (sometimes known as datum or geographic transformations) are used when projecting data from one spatial reference to another when there is a difference in the underlying datum of the spatial references. Transformations can be mathematically defined by specific equations (equation-based transformations), or may rely on external supporting files (grid-based transformations). Choosing the most appropriate transformation for a situation can ensure the best possible accuracy for this operation. Some users familiar with transformations may wish to control which transformation is used in an operation.
How to use the sample
Select a transformation from the list to see the result of projecting the point from EPSG:27700 to EPSG:3857 using that transformation. The result is shown as a red cross; you can visually compare the original blue point with the projected red cross.
Toggle "Order by suitability for map extent" on to sort the transformations in an order that is appropriate for the current extent.
If the selected transformation is not usable (has missing grid files) then an error is displayed in the items details.
How it works
- Pass the input and output spatial references to
class AGSTransformationCatalog.transformationsBySuitability(withInputSpatialReference:outputSpatialReference:)
for transformations based on the map's spatial reference OR additionally provide an extent argument to only return transformations suitable to the extent. This returns a list of ranked transformations. - Use one of the
AGSDatumTransformation
objects returned to project the input geometry to the output spatial reference.
Relevant API
- AGSDatumTransformation
- AGSGeographicTransformation
- AGSGeographicTransformationStep
- AGSGeometryEngine
- AGSTransformationCatalog
- class AGSGeometryEngine.projectGeometry(_:to:datumTransformation:)
About the data
The map starts out zoomed into the grounds of the Royal Observatory, Greenwich. The initial point is in the British National Grid spatial reference, which was created by the United Kingdom Ordnance Survey. The spatial reference after projection is in Web Mercator.
Additional information
Some transformations aren't available until transformation data is provided.
This sample uses AGSGeographicTransformation
, a subclass of AGSDatumTransformation
. As of 100.9, ArcGIS Runtime also includes AGSHorizontalVerticalTransformation
, another subclass of AGSDatumTransformation
. The AGSHorizontalVerticalTransformation
class is used to transform coordinates of z-aware geometries between spatial references that have different geographic and/or vertical coordinate systems.
This sample can be used with or without provisioning projection engine data to your device. If you do not provision data, a limited number of transformations will be available.
To download projection engine data to your device:
- Log in to the ArcGIS for Developers site using your Developer account.
- In the Dashboard page, click 'Download APIs and SDKs' and go to the
Supplemental ArcGIS Runtime Data
tab. - Click the download button next to
Projection Engine Data
to download projection engine data to your computer. - Unzip the downloaded data on your computer.
- Copy the
PEDataRuntime
folder to your application's Documents folder.
Tags
datum, geodesy, projection, spatial reference, transformation
Sample Code
// Copyright 2018 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 ListTransformationsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
@IBOutlet var mapView: AGSMapView!
@IBOutlet var tableView: UITableView!
@IBOutlet var orderByMapExtent: UISwitch!
var datumTransformations = [AGSDatumTransformation]()
var defaultTransformation: AGSDatumTransformation?
let graphicsOverlay = AGSGraphicsOverlay()
var originalGeometry = AGSPoint(x: 538985.355, y: 177329.516, spatialReference: AGSSpatialReference(wkid: 27700))
var projectedGraphic: AGSGraphic? {
if graphicsOverlay.graphics.count > 1 {
return graphicsOverlay.graphics.lastObject as? AGSGraphic
} else {
return nil
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Add the source code button item to the right of navigation bar.
(self.navigationItem.rightBarButtonItem as! SourceCodeBarButtonItem).filenames = ["ListTransformationsViewController"]
// Get MapView from layout and set a map into this view
mapView.map = AGSMap(basemapStyle: .arcGISLightGrayBase)
mapView.graphicsOverlays.add(graphicsOverlay)
// add original graphic to overlay
addGraphic(originalGeometry, color: .red, style: .square)
mapView.map?.load { [weak self] (error) in
if let error = error {
print("map load error = \(error)")
} else {
self?.mapDidLoad()
}
}
}
func mapDidLoad() {
mapView.setViewpoint(AGSViewpoint(center: originalGeometry, scale: 5000), duration: 2.0, completion: nil)
// set the url for our projection engine data;
setPEDataURL()
}
// add a graphic with the given geometry, color and style to the graphics overlay
func addGraphic(_ geometry: AGSGeometry, color: UIColor, style: AGSSimpleMarkerSymbolStyle) {
let sms = AGSSimpleMarkerSymbol(style: style, color: color, size: 15.0)
graphicsOverlay.graphics.add(AGSGraphic(geometry: geometry, symbol: sms, attributes: nil))
}
// set up our datumTransformations array
func setupTransformsList() {
guard let map = mapView.map,
let inputSR = originalGeometry.spatialReference,
let outputSR = map.spatialReference else { return }
// if orderByMapExtent is on, use the map extent when retrieving the transformations
if orderByMapExtent.isOn {
datumTransformations = AGSTransformationCatalog.transformationsBySuitability(withInputSpatialReference: inputSR, outputSpatialReference: outputSR, areaOfInterest: mapView.visibleArea?.extent)
} else {
datumTransformations = AGSTransformationCatalog.transformationsBySuitability(withInputSpatialReference: inputSR, outputSpatialReference: outputSR)
}
defaultTransformation = AGSTransformationCatalog.transformation(forInputSpatialReference: inputSR, outputSpatialReference: outputSR)
// unselect selected row
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
// remove projected graphic from overlay
if let graphic = projectedGraphic {
// we have the projected graphic, remove it (it's always the last one)
graphicsOverlay.graphics.remove(graphic)
}
tableView.reloadData()
}
func setPEDataURL() {
if let projectionEngineDataURL = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first?.appendingPathComponent("PEDataRuntime") {
do {
guard try projectionEngineDataURL.checkResourceIsReachable() else { return }
// Normally, this method would be called immediately upon application startup before any other API method calls.
// So usually it would be called from AppDelegate.application(_:didFinishLaunchingWithOptions:), but for the purposes
// of this sample, we're calling it here.
try AGSTransformationCatalog.setProjectionEngineDirectory(projectionEngineDataURL)
} catch {
print("Could not load projection engine data. See the README file for instructions on adding PE data to your app.")
}
}
setupTransformsList()
}
@IBAction func oderByMapExtentValueChanged(_ sender: Any) {
setupTransformsList()
}
// MARK: - TableView data source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datumTransformations.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DatumTransformCell", for: indexPath)
// get the selected transformation
let transformation = datumTransformations[indexPath.row]
// disable selection if the transformation is missing files
cell.isUserInteractionEnabled = !transformation.isMissingProjectionEngineFiles
cell.textLabel?.text = transformation.name
cell.detailTextLabel?.text = {
if transformation.isMissingProjectionEngineFiles,
// if we're missing the grid files, detail which ones
let geographicTransformation = transformation as? AGSGeographicTransformation {
let files = geographicTransformation.steps.flatMap { (step) -> [String] in
step.isMissingProjectionEngineFiles ? step.projectionEngineFilenames : []
}
return "Missing grid files: \(files.joined(separator: ", "))"
} else {
return ""
}
}()
return cell
}
// MARK: - TableView delegates
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let mapViewSR = mapView.spatialReference else { return }
let selectedTransform = datumTransformations[indexPath.row]
if let projectedGeometry = AGSGeometryEngine.projectGeometry(originalGeometry, to: mapViewSR, datumTransformation: selectedTransform) {
// projectGeometry succeeded
if let graphic = projectedGraphic {
// we've already added the projected graphic
graphic.geometry = projectedGeometry
} else {
// add projected graphic
addGraphic(projectedGeometry, color: .blue, style: .cross)
}
} else {
// If a transformation is missing grid files, then it cannot be
// successfully used to project a geometry, and "projectGeometry" will return nil.
// In that case, remove projected graphic
if graphicsOverlay.graphics.count > 1 {
graphicsOverlay.graphics.removeLastObject()
}
}
}
}