Skip to content

Show geodesic sector and ellipse

View on GitHub

Create and display geodesic sectors and ellipses.

Image of show geodesic sector and ellipse

Use case

Geodesic sectors and ellipses can be used in a wide range of analyses ranging from projectile landing zones to antenna coverage. For example, given the strength and direction of a cellular tower's signal, you could generate cell coverage geometries to identify areas without sufficient connectivity.

How to use the sample

The geodesic sector and ellipse will display with default parameters at the start. Click anywhere on the map to change the center of the geometries. Adjust any of the controls to see how they affect the sector and ellipse on the fly.

How it works

To create a geodesic sector and ellipse:

  1. CreateGeodesicSectorParameters and GeodesicEllipseParameters values.
  2. Use center, axisDirection, semiAxis1Length, and semiAxis2Length to set the general ellipse position, shape, and orientation.
  3. Use sectorAngle and startDirection to set the sector's shape and orientation.
  4. Use maxPointCount and maxSegmentLength to control the complexity of the geometries and the approximation of the ellipse curve.
  5. Use geometryType to change the result geometry type.
  6. Pass the parameters to the related static methods: GeometryEngine.geodesicEllipse(parameters:) and GeometryEngine.geodesicSector(parameters:). The returned geometry type is that of the parameters.

Relevant API

GeodesicEllipseParameters GeodesicSectorParameters GeometryEngine

Additional information

To create a circle instead of an ellipse, simply set semiAxis2Length to 0 and semiAxis1Length to the desired radius of the circle. This eliminates the need to update both parameters to the same value.

Tags

ellipse, geodesic, geometry, sector

Sample Code

ShowGeodesicSectorAndEllipseView.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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
// Copyright 2025 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 ShowGeodesicSectorAndEllipseView: View {
    /// The data model that helps determine the view.
    @State private var model = Model()

    /// Manages the presentation state of the menu.
    @State private var isPresented = false

    var body: some View {
        MapViewReader { mapView in
            MapView(
                map: model.map,
                graphicsOverlays: model.graphicOverlays
            )
            .onSingleTapGesture { _, tapPoint in
                model.center = tapPoint
            }
            .task(id: model.center) {
                guard let center = model.center else { return }
                await mapView.setViewpoint(
                    Viewpoint(center: center, scale: 1e7)
                )
            }
            .overlay(alignment: .top) {
                if model.center == nil {
                    Text("Tap map to create a geodesic sector.")
                        .frame(maxWidth: .infinity)
                        .padding(.vertical, 6)
                        .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal)
                }
            }
            .toolbar {
                ToolbarItemGroup(placement: .bottomBar) {
                    Button("Settings") {
                        isPresented = true
                    }
                    .disabled(model.center == nil)
                    .sheet(isPresented: $isPresented) {
                        settingsSheet
                    }
                }
            }
        }
    }

    /// The menu which holds the options that change the ellipse and sector.
    private var settingsSheet: some View {
        NavigationStack {
            SectorSettingsView(model: $model)
                .presentationDetents([.medium])
                .navigationTitle("Settings")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .confirmationAction) {
                        Button("Done") {
                            isPresented = false
                        }
                    }
                }
        }
    }
}

private extension ShowGeodesicSectorAndEllipseView {
    /// Custom data type so that Geometry options can be displayed in the menu.
    enum GeometryType: CaseIterable {
        case point, polyline, polygon

        var label: String {
            switch self {
            case .point: "Point"
            case .polyline: "Polyline"
            case .polygon: "Polygon"
            }
        }
    }

    /// A view model that encapsulates logic and state for rendering a geodesic sector and ellipse.
    /// Handles user-configured parameters and updates overlays when those parameters change.
    @Observable
    final class Model {
        /// The map that will be displayed in the map view.
        let map = Map(basemapStyle: .arcGISTopographic)

        /// The map point selected by the user when tapping on the map.
        var center: Point? {
            didSet {
                updateSector()
            }
        }

        var graphicOverlays: [GraphicsOverlay] {
            return [ellipseGraphicOverlay, sectorGraphicOverlay]
        }

        /// The graphics overlay that will be displayed on the map view.
        /// This will hold the graphics that show the ellipse path.
        private let ellipseGraphicOverlay = GraphicsOverlay()

        /// The graphics overlay that will be displayed on the map view.
        /// This will display a highlighted section of the ellipse path.
        private let sectorGraphicOverlay = {
            let overlay = GraphicsOverlay()
            overlay.renderer = SimpleRenderer(symbol: SimpleFillSymbol(style: .solid, color: .green))
            return overlay
        }()

        /// The direction (in degrees) of the ellipse's major axis.
        var axisDirection = Measurement<UnitAngle>(value: 45, unit: .degrees) {
            didSet {
                updateSector()
            }
        }
        /// Controls the complexity of the geometries and the approximation of the ellipse curve.
        var maxSegmentLength = 1.0 {
            didSet {
                updateSector()
            }
        }
        /// Changes the sectors shape.
        var sectorAngle = Measurement<UnitAngle>(value: 90, unit: .degrees) {
            didSet {
                updateSector()
            }
        }
        /// Controls the complexity of the geometries and the approximation of the ellipse curve.
        var maxPointCount = 1_000.0 {
            didSet {
                updateSector()
            }
        }
        /// Changes the length of ellipse shape on one axis.
        var semiAxis1Length = 200.0 {
            didSet {
                updateSector()
            }
        }
        /// Changes the length of ellipse shape on one axis.
        var semiAxis2Length = 100.0 {
            didSet {
                updateSector()
            }
        }
        /// Changes the geometry type which the sector is rendered.
        var geometryType: GeometryType = .polygon {
            didSet {
                updateSector()
            }
        }
        /// Changes the direction of the sector.
        var startDirection = 45.0 {
            didSet {
                updateSector()
            }
        }

        private func updateSector() {
            updateEllipse()
            setupSector()
        }

        private func setupSector() {
            sectorGraphicOverlay.removeAllGraphics()
            switch geometryType {
            case .point:
                // Generate sector as a multipoint (symbols).
                var parameters = GeodesicSectorParameters<Multipoint>()
                fillSectorParameters(&parameters)
                if let geometry = GeometryEngine.geodesicSector(parameters: parameters) {
                    let symbol = SimpleMarkerSymbol(style: .circle, color: .green, size: 2)
                    addSectorGraphic(geometry: geometry, symbol: symbol)
                }
            case .polyline:
                // Generate sector as a polyline (outlined arc).
                var parameters = GeodesicSectorParameters<Polyline>()
                fillSectorParameters(&parameters)
                if let geometry = GeometryEngine.geodesicSector(parameters: parameters) {
                    let symbol = SimpleLineSymbol(style: .solid, color: .green, width: 2)
                    addSectorGraphic(geometry: geometry, symbol: symbol)
                }
            case .polygon:
                // Generate sector as a filled polygon.
                var parameters = GeodesicSectorParameters<ArcGIS.Polygon>()
                fillSectorParameters(&parameters)
                if let geometry = GeometryEngine.geodesicSector(parameters: parameters) {
                    let symbol = SimpleFillSymbol(style: .solid, color: .green)
                    addSectorGraphic(geometry: geometry, symbol: symbol)
                }
            }
        }

        /// Populates a geodesic sector parameters value with current user-defined values.
        /// - Parameter parameters: A reference to the parameter struct that will be filled.
        private func fillSectorParameters<T>(_ parameters: inout GeodesicSectorParameters<T>) {
            parameters.center = center
            parameters.axisDirection = axisDirection.value
            parameters.maxPointCount = Int(maxPointCount.rounded())
            parameters.maxSegmentLength = maxSegmentLength
            parameters.sectorAngle = sectorAngle.value
            parameters.semiAxis1Length = semiAxis1Length
            parameters.semiAxis2Length = semiAxis2Length
            parameters.startDirection = startDirection
            parameters.linearUnit = .miles
        }

        /// Adds a sector graphic to the overlay and applies the appropriate renderer.
        private func addSectorGraphic(geometry: Geometry, symbol: Symbol) {
            let sectorGraphic = Graphic(geometry: geometry, symbol: symbol)
            sectorGraphicOverlay.addGraphic(sectorGraphic)
        }

        /// Generates and adds a geodesic ellipse graphic based on the current settings and center point.
        private func updateEllipse() {
            ellipseGraphicOverlay.removeAllGraphics()
            let parameters = GeodesicEllipseParameters<ArcGIS.Polygon>(
                axisDirection: axisDirection.value,
                center: center,
                linearUnit: .miles,
                maxPointCount: Int(maxPointCount.rounded()),
                maxSegmentLength: maxSegmentLength,
                semiAxis1Length: semiAxis1Length,
                semiAxis2Length: semiAxis2Length
            )
            let geometry = GeometryEngine.geodesicEllipse(parameters: parameters)
            let symbol = SimpleLineSymbol(style: .dash, color: .red, width: 2)
            let graphic = Graphic(geometry: geometry, symbol: symbol)
            ellipseGraphicOverlay.addGraphic(graphic)
        }
    }

    struct SectorSettingsView: View {
        @Binding var model: ShowGeodesicSectorAndEllipseView.Model

        private var numberFormat: FloatingPointFormatStyle<Double> {
            .init().precision(.fractionLength(0))
        }

        private var angleFormat: Measurement<UnitAngle>.FormatStyle {
            .init(width: .narrow, numberFormatStyle: numberFormat)
        }

        var body: some View {
            Form {
                LabeledContent(
                    "Axis Direction",
                    value: model.axisDirection,
                    format: angleFormat
                )

                let axisDirectionRange = 0.0...360.0

                Slider(
                    value: $model.axisDirection.value,
                    in: axisDirectionRange
                ) {
                    Text("Axis Direction")
                } minimumValueLabel: {
                    Text(
                        Measurement<UnitAngle>(
                            value: axisDirectionRange.lowerBound,
                            unit: .degrees
                        ),
                        format: angleFormat
                    )
                } maximumValueLabel: {
                    Text(
                        Measurement<UnitAngle>(
                            value: axisDirectionRange.upperBound,
                            unit: .degrees
                        ),
                        format: angleFormat
                    )
                }
                .listRowSeparator(.hidden, edges: .top)

                LabeledContent(
                    "Max Point Count",
                    value: model.maxPointCount,
                    format: numberFormat
                )

                let maxPointCountRange = 1.0...1_000.0

                Slider(
                    value: $model.maxPointCount,
                    in: maxPointCountRange,
                    step: 1
                ) {
                    Text("Max Point Count")
                } minimumValueLabel: {
                    Text(maxPointCountRange.lowerBound, format: numberFormat)
                } maximumValueLabel: {
                    Text(maxPointCountRange.upperBound, format: numberFormat)
                }
                .listRowSeparator(.hidden, edges: .top)

                LabeledContent(
                    "Max Segment Length",
                    value: model.maxSegmentLength,
                    format: numberFormat
                )

                let maxSegmentLengthRange = 1.0...1_000.0

                Slider(
                    value: $model.maxSegmentLength,
                    in: maxSegmentLengthRange
                ) {
                    Text("Max Segment Length")
                } minimumValueLabel: {
                    Text(maxSegmentLengthRange.lowerBound, format: numberFormat)
                } maximumValueLabel: {
                    Text(maxSegmentLengthRange.upperBound, format: numberFormat)
                }
                .listRowSeparator(.hidden, edges: .top)

                Picker("Geometry Type", selection: $model.geometryType) {
                    ForEach(GeometryType.allCases, id: \.self) { geometryType in
                        Text(geometryType.label)
                    }
                }

                LabeledContent(
                    "Sector Angle",
                    value: model.sectorAngle,
                    format: angleFormat
                )

                let sectorAngleRange = 0.0...360.0

                Slider(
                    value: $model.sectorAngle.value,
                    in: sectorAngleRange
                ) {
                    Text("Sector Angle")
                } minimumValueLabel: {
                    Text(
                        Measurement<UnitAngle>(
                            value: sectorAngleRange.lowerBound,
                            unit: .degrees
                        ),
                        format: angleFormat
                    )
                } maximumValueLabel: {
                    Text(
                        Measurement<UnitAngle>(
                            value: sectorAngleRange.upperBound,
                            unit: .degrees
                        ),
                        format: angleFormat
                    )
                }
                .listRowSeparator(.hidden, edges: .top)

                LabeledContent(
                    "Semi Axis 1 Length",
                    value: model.semiAxis1Length,
                    format: numberFormat
                )

                let semiAxis1LengthRange = 0.0...1_0000.0

                Slider(
                    value: $model.semiAxis1Length,
                    in: semiAxis1LengthRange
                ) {
                    Text("Semi Axis 1 Length")
                } minimumValueLabel: {
                    Text(semiAxis1LengthRange.lowerBound, format: numberFormat)
                } maximumValueLabel: {
                    Text(semiAxis1LengthRange.upperBound, format: numberFormat)
                }
                .listRowSeparator(.hidden, edges: [.top])

                LabeledContent(
                    "Semi Axis 2 Length",
                    value: model.semiAxis2Length,
                    format: numberFormat
                )

                let semiAxis2LengthRange = 0.0...1_000.0

                Slider(
                    value: $model.semiAxis2Length,
                    in: semiAxis2LengthRange
                ) {
                    Text("Semi Axis 2 Length")
                } minimumValueLabel: {
                    Text(semiAxis2LengthRange.lowerBound, format: numberFormat)
                } maximumValueLabel: {
                    Text(semiAxis2LengthRange.upperBound, format: numberFormat)
                }
                .listRowSeparator(.hidden, edges: [.top])
            }
        }
    }
}

#Preview {
    ShowGeodesicSectorAndEllipseView()
}

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