List transformations

View on GitHubSample viewer app

Get a list of suitable transformations for projecting a geometry between two spatial references with different horizontal datums.

Image of list transformations by suitability

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

  1. 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.
  2. 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:

  1. Log in to the ArcGIS for Developers site using your Developer account.
  2. In the Dashboard page, click 'Download APIs and SDKs' and go to the Supplemental ArcGIS Runtime Data tab.
  3. Click the download button next to Projection Engine Data to download projection engine data to your computer.
  4. Unzip the downloaded data on your computer.
  5. Copy the PEDataRuntime folder to your application's Documents folder.

Tags

datum, geodesy, projection, spatial reference, transformation

Sample Code

ListTransformationsViewController.swift
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// 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()
            }
        }
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.