Configure electronic navigational charts

View on GitHub

Display and configure electronic navigational charts per ENC specification.

Screenshot of Configure electronic navigational charts sample Screenshot of Configure electronic navigational charts sample settings

Use case

The S-52 standard defines how Electronic Navigational Chart (ENC) content should be displayed to ensure that data is presented consistently across every charting system. S-52 defines several display options, including variations on symbology to ensure that charts are readable both at night and in direct sunlight.

How to use the sample

When opened, the sample displays an electronic navigational chart. Tap on the map to select ENC features and view the feature's acronyms and descriptions shown in a callout. Tap "Display Settings" and use the options to adjust some of the ENC mariner display settings, such as the colors and symbology.

How it works

  1. To display ENC content:
    1. On ENCEnvironmentSettings.shared, set resourceURL to the local hydrography data directory and sencDataURL to a temporary directory.
    2. Create an ENCExchangeSet using URLs to the local ENC exchange set files and load it.
    3. Make an ENCCell for each of the ENCExchangeSet.datasets and then make an ENCLayer from each cell.
    4. Add the layers to the map using Map.addOperationalLayers(_:) and create a MapView to display the map.
  2. To select ENC features:
    1. Use onSingleTapGesture(perform:) on the map view to get the screen point from the tapped location.
    2. Create a MapViewReader to get the MapViewProxy and use it to identify nearby features to the tapped location with identifyLayers(screenPoint:tolerance:returnPopupsOnly:maximumResultsPerLayer:).
    3. From the resulting IdentifyLayerResult, get the ENCLayer from layerContent and the ENCFeature(s) from geoElements.
    4. Use ENCLayer.select(_:) to select the ENC feature(s).
  3. To set ENC display settings:
    1. Get the ENCDisplaySettings instance from ENCEnvironmentSettings.shared.displaySettings.
    2. Use marinerSettings, textGroupVisibilitySettings, and viewingGroupSettings to access the settings instances and set their properties.
    3. Reset the display settings using resetToDefaults() on the settings instances.

Relevant API

  • ENCCell
  • ENCDataset
  • ENCDisplaySettings
  • ENCEnvironmentSettings
  • ENCExchangeSet
  • ENCLayer
  • ENCMarinerSettings
  • ENCTextGroupVisibilitySettings
  • ENCViewingGroupSettings
  • IdentifyLayerResult

Offline data

This sample downloads the ENC Exchange Set without updates item from ArcGIS Online automatically.

The latest Hydrography Data can be downloaded from the Esri Developer downloads. The S57DataDictionary.xml file is contained there.

Additional information

Read more about displaying and deploying electronic navigational charts on Esri Developer.

Tags

ENC, hydrography, identify, IHO, layers, maritime, nautical chart, S-52, S-57, select, settings, symbology

Sample Code

ConfigureElectronicNavigationalChartsView.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// Copyright 2024 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
//
//   https://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 ArcGIS
import SwiftUI

struct ConfigureElectronicNavigationalChartsView: View {
    /// The view model for the sample.
    @StateObject private var model = Model()

    /// The point on the screen where the user tapped.
    @State private var tapPoint: CGPoint?

    /// The placement of the selected ENC feature callout.
    @State private var calloutPlacement: CalloutPlacement?

    /// A Boolean value indicating whether the display settings view is showing.
    @State private var isShowingDisplaySettings = false

    /// The error shown in the error alert.
    @State private var error: Error?

    var body: some View {
        MapViewReader { mapViewProxy in
            MapView(map: model.map)
                .callout(placement: $calloutPlacement.animation()) { placement in
                    let encFeature = placement.geoElement as! ENCFeature
                    VStack(alignment: .leading) {
                        Text(encFeature.acronym)
                        Text(encFeature.description)
                    }
                    .padding(5)
                }
                .onSingleTapGesture { screenPoint, _ in
                    tapPoint = screenPoint
                }
                .task(id: tapPoint) {
                    // Identifies and selects a tapped feature.
                    guard let tapPoint else {
                        return
                    }

                    do {
                        try await selectENCFeature(screenPoint: tapPoint, proxy: mapViewProxy)
                    } catch {
                        self.error = error
                    }
                }
        }
        .toolbar {
            ToolbarItem(placement: .bottomBar) {
                Button("Display Settings") {
                    isShowingDisplaySettings = true
                }
                .popover(isPresented: $isShowingDisplaySettings) {
                    ENCDisplaySettingsView()
                        .presentationDetents([.fraction(0.5)])
                        .frame(idealWidth: 320, idealHeight: 280)
                }
            }
        }
        .task {
            // Sets up the sample when it opens.
            do {
                try await model.addENCExchangeSet()
                model.configureENCDisplaySettings()
            } catch {
                self.error = error
            }
        }
        .errorAlert(presentingError: $error)
    }

    /// Selects an ENC feature identified at a screen point.
    /// - Parameters:
    ///   - screenPoint: The screen coordinate of the geo view at which to identify.
    ///   - proxy: The map view proxy used to identify the screen point.
    private func selectENCFeature(screenPoint: CGPoint, proxy: MapViewProxy) async throws {
        model.encLayer?.clearSelection()
        calloutPlacement = nil

        // Uses the proxy to identify the layers at the screen point.
        let identifyResults = try await proxy.identifyLayers(
            screenPoint: screenPoint,
            tolerance: 10
        )

        // Gets the ENC layer and feature from the identify results.
        guard let result = identifyResults.first(where: { $0.layerContent is ENCLayer }),
              let encLayer = result.layerContent as? ENCLayer,
              let encFeature = result.geoElements.first as? ENCFeature else {
            return
        }

        // Selects the feature using the layer.
        encLayer.select(encFeature)
        model.encLayer = encLayer

        // Sets the callout to display on the feature.
        let tapLocation = proxy.location(fromScreenPoint: screenPoint)
        calloutPlacement = .geoElement(encFeature, tapLocation: tapLocation)
    }
}

// MARK: Model

private extension ConfigureElectronicNavigationalChartsView {
    /// The view model for the sample.
    @MainActor
    class Model: ObservableObject {
        /// A map with an oceans basemap.
        let map: Map = {
            let map = Map(basemapStyle: .arcGISOceans)
            map.initialViewpoint = Viewpoint(latitude: -32.5, longitude: 60.95, scale: 67_000)
            return map
        }()

        /// The ENC layer for unselecting the selected feature.
        var encLayer: ENCLayer?

        /// A URL to the temporary directory for the generated SENC data files.
        private let sencDataURL = FileManager.createTemporaryDirectory()

        deinit {
            // Resets ENC environment settings when the sample closes.
            let environmentSettings = ENCEnvironmentSettings.shared
            ENCEnvironmentSettings.shared.resourceURL = nil
            ENCEnvironmentSettings.shared.sencDataURL = nil

            try? FileManager.default.removeItem(at: sencDataURL)

            let displaySettings = environmentSettings.displaySettings
            displaySettings.marinerSettings.resetToDefaults()
            displaySettings.textGroupVisibilitySettings.resetToDefaults()
            displaySettings.viewingGroupSettings.resetToDefaults()
        }

        /// Sets up the ENC exchange set and adds it to the map.
        func addENCExchangeSet() async throws {
            // Sets environment settings for loading the dataset.
            let environmentSettings = ENCEnvironmentSettings.shared
            environmentSettings.resourceURL = .hydrographyData
            environmentSettings.sencDataURL = sencDataURL

            // Creates the exchange set from a local file.
            let exchangeSet = ENCExchangeSet(fileURLs: [.exchangeSet])
            try await exchangeSet.load()

            // Creates layers from the exchange set's datasets and adds them to the map.
            let encLayers = exchangeSet.datasets.map { dataset in
                ENCLayer(cell: ENCCell(dataset: dataset))
            }
            map.addOperationalLayers(encLayers)
        }

        /// Disables some ENC environment display settings to make the chart less cluttered.
        func configureENCDisplaySettings() {
            let displaySettings = ENCEnvironmentSettings.shared.displaySettings

            let textGroupVisibilitySettings = displaySettings.textGroupVisibilitySettings
            textGroupVisibilitySettings.includesGeographicNames = false
            textGroupVisibilitySettings.includesNatureOfSeabed = false

            let viewingGroupSettings = displaySettings.viewingGroupSettings
            viewingGroupSettings.includesDepthContours = false
            viewingGroupSettings.includesLights = false
            viewingGroupSettings.includesSpotSoundings = false
        }
    }
}

// MARK: ENC Display Settings View

/// A view for adjusting some ENC mariner display settings.
private struct ENCDisplaySettingsView: View {
    /// The action to dismiss the view.
    @Environment(\.dismiss) private var dismiss

    /// The color scheme selection.
    @State private var colorScheme: ColorScheme = .day

    /// The area symbolization type selection.
    @State private var areaSymbolization: AreaSymbolization = .symbolized

    /// The point symbolization type selection.
    @State private var pointSymbolization: PointSymbolization = .paperChart

    /// The ENC environment mariner display settings for adjusting the app's ENC rendering.
    private let marinerDisplaySettings = ENCEnvironmentSettings.shared.displaySettings.marinerSettings

    // Some ENC mariner settings types.
    private typealias ColorScheme = ENCMarinerSettings.ColorScheme
    private typealias AreaSymbolization = ENCMarinerSettings.AreaSymbolizationType
    private typealias PointSymbolization = ENCMarinerSettings.PointSymbolizationType

    var body: some View {
        NavigationStack {
            Form {
                Picker("Color Scheme", selection: $colorScheme) {
                    Text("Day").tag(ColorScheme.day)
                    Text("Dusk").tag(ColorScheme.dusk)
                    Text("Night").tag(ColorScheme.night)
                }
                .onChange(of: colorScheme) { colorScheme in
                    marinerDisplaySettings.colorScheme = colorScheme
                }

                Picker("Area Symbolization Type", selection: $areaSymbolization) {
                    Text("Plain").tag(AreaSymbolization.plain)
                    Text("Symbolized").tag(AreaSymbolization.symbolized)
                }
                .onChange(of: areaSymbolization) { areaSymbolization in
                    marinerDisplaySettings.areaSymbolizationType = areaSymbolization
                }

                Picker("Point Symbolization Type", selection: $pointSymbolization) {
                    Text("Paper Chart").tag(PointSymbolization.paperChart)
                    Text("Simplified").tag(PointSymbolization.simplified)
                }
                .onChange(of: pointSymbolization) { pointSymbolization  in
                    marinerDisplaySettings.pointSymbolizationType = pointSymbolization
                }
            }
            .navigationTitle("Display Settings")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("Done") {
                        dismiss()
                    }
                }
            }
        }
        .onAppear {
            colorScheme = marinerDisplaySettings.colorScheme
            areaSymbolization = marinerDisplaySettings.areaSymbolizationType
            pointSymbolization = marinerDisplaySettings.pointSymbolizationType
        }
    }
}

// MARK: Helper Extensions

private extension FileManager {
    /// Creates a temporary directory.
    /// - Returns: The URL of the created directory.
    static func createTemporaryDirectory() -> URL {
        // swiftlint:disable:next force_try
        try! FileManager.default.url(
            for: .itemReplacementDirectory,
            in: .userDomainMask,
            appropriateFor: FileManager.default.temporaryDirectory,
            create: true
        )
    }
}

private extension URL {
    /// The URL to the local ENC exchange set file.
    static var exchangeSet: URL {
        Bundle.main.url(
            forResource: "CATALOG",
            withExtension: "031",
            subdirectory: "ExchangeSetwithoutUpdates/ExchangeSetwithoutUpdates/ENC_ROOT"
        )!
    }

    /// The URL to the local hydrography data directory, which contains the ENC resource files.
    static var hydrographyData: URL {
        Bundle.main.url(forResource: "hydrography", withExtension: nil, subdirectory: "hydrography")!
    }
}

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