Create buffers around points

View on GitHub

Generate multiple individual buffers or a single unioned buffer around multiple points.

Image of create buffers around points

Use case

Creating buffers is a core concept in GIS proximity analysis that allows you to visualize and locate geographic features contained within a set distance of a feature. For example, consider an area where wind turbines are proposed. It has been determined that each turbine should be located at least 2 km away from residential premises due to noise pollution regulations, and a proximity analysis is therefore required. The first step would be to generate 2 km buffer polygons around all proposed turbines. As the buffer polygons may overlap for each turbine, unioning the result would produce a single graphic result with a neater visual output. If any premises are located within 2 km of a turbine, that turbine would be in breach of planning regulations.

How to use the sample

Tap on the map to add points. Toggle on "Union" toggle if you want the result to union (combine) the buffers. Tap the "Clear" button to start over. The red dashed envelope shows the area where you can expect reasonable results for planar buffer operations with the North Central Texas State Plane spatial reference.

How it works

  1. Use GeometryEngine.buffer(around, distances, shouldUnion) to create a Polygon. The parameter around are the points to buffer around, distances are the buffer distances for each point (in meters) and shouldUnion is a boolean for whether the results should be unioned.
  2. Add the resulting polygons (if not unioned) or single polygon (if unioned) to the map's GraphicsOverlay as a Graphic.

Relevant API

  • Geometry
  • GeometryEngine
  • SpatialReference

Additional information

The properties of the underlying projection determine the accuracy of buffer polygons in a given area. Planar buffers work well when analyzing distances around features that are concentrated in a relatively small area in a projected coordinate system. Inaccurate buffers could still be created by buffering points inside the spatial reference's envelope with distances that move it outside the envelope. On the other hand, geodesic buffers consider the curved shape of the Earth's surface and provide more accurate buffer offsets for features that are more dispersed (i.e., cover multiple UTM zones, large regions, or even the whole globe). See the "Buffer" sample for an example of a geodesic buffer.

For more information about using buffer analysis, see the topic How Buffer (Analysis) works in the ArcGIS Pro documentation.

Tags

analysis, buffer, geometry, planar

Sample Code

CreateBuffersAroundPointsView.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
// Copyright 2023 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 CreateBuffersAroundPointsView: View {
    /// The view model for the sample.
    @StateObject private var model = Model()

    /// The status of the sample.
    @State private var status = Status.addPoints

    /// The point where the map was tapped.
    @State private var tapPoint: Point?

    /// A Boolean value indicating whether union is on.
    @State private var shouldUnion = false

    /// A Boolean value indicating whether the input box is showing.
    @State private var inputBoxIsPresented = false

    /// The input obtained from the user for the buffer radius of a point.
    @State private var bufferRadius: Double = 100

    var body: some View {
        // Create a map view to display the map.
        MapView(map: model.map, graphicsOverlays: model.graphicsOverlays)
            .onSingleTapGesture { _, mapPoint in
                // Update tapPoint and bring up input box if point is within bounds.
                if model.boundaryContains(mapPoint) {
                    tapPoint = mapPoint
                    inputBoxIsPresented = true
                } else {
                    status = .outOfBoundsTap
                }
            }
            .overlay(alignment: .top) {
                Text(status.label)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding(8)
                    .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal)
            }
            .toolbar {
                ToolbarItemGroup(placement: .bottomBar) {
                    // Union toggle switch.
                    Toggle(shouldUnion ? "Union Enabled" : "Union Disabled", isOn: $shouldUnion)
                        .onChange(of: shouldUnion) { _ in
                            if !model.bufferPoints.isEmpty {
                                model.drawBuffers(unioned: shouldUnion)
                            }
                        }
                    Button("Clear") {
                        model.clearBufferPoints()
                        status = .addPoints
                    }
                    .disabled(model.bufferPoints.isEmpty)
                }
            }
            .alert("Buffer Radius", isPresented: $inputBoxIsPresented, actions: {
                TextField("radius in miles", value: $bufferRadius, format: .number)
                    .keyboardType(.numberPad)
                Button("Done") {
                    guard let tapPoint else {
                        preconditionFailure("Missing tap point")
                    }

                    let newStatus: Status
                    // Check to ensure the tapPoint is within the boundary.
                    if model.boundaryContains(tapPoint) {
                        // Ensure that the input is valid.
                        if bufferRadius > 0 && bufferRadius < 300 {
                            model.addBuffer(point: tapPoint, radius: bufferRadius)
                            model.drawBuffers(unioned: shouldUnion)
                            newStatus = .bufferCreated
                        } else {
                            newStatus = .invalidInput
                        }
                    } else {
                        newStatus = .outOfBoundsTap
                    }
                    status = newStatus

                    // Set the radius to default value.
                    bufferRadius = 100
                }
                Button("Cancel", role: .cancel) { bufferRadius = 100 }
                // Input alert message.
            }, message: {
                Text("Please enter a number between 0 and 300 miles.")
            })
    }
}

private extension CreateBuffersAroundPointsView {
    /// The view model for this sample.
    class Model: ObservableObject {
        /// A map centered on Texas with image layers.
        let map: Map

        /// The graphics overlays used in this sample.
        var graphicsOverlays: [GraphicsOverlay] { [boundaryGraphicsOverlay, bufferGraphicsOverlay, tapPointsGraphicsOverlay] }

        /// An array of the tapped points and their radii.
        private(set) var bufferPoints: [(point: Point, radius: Double)] = []

        /// The graphics overlay for the boundary around the valid area of use.
        private let boundaryGraphicsOverlay: GraphicsOverlay

        /// The graphics overlay for the points' buffers.
        private let bufferGraphicsOverlay: GraphicsOverlay

        /// The graphics overlay for the points of the tapped locations.
        private let tapPointsGraphicsOverlay: GraphicsOverlay

        /// A polygon that represents the valid area of use for the spatial reference.
        private let boundaryPolygon: ArcGIS.Polygon

        init() {
            /// The spatial reference for the sample.
            let statePlaneNorthCentralTexas = SpatialReference(wkid: WKID(32038)!)!

            // Create boundary polygon.
            boundaryPolygon = {
                let boundaryPoints = [
                    Point(latitude: 31.720, longitude: -103.070),
                    Point(latitude: 34.580, longitude: -103.070),
                    Point(latitude: 34.580, longitude: -94.000),
                    Point(latitude: 31.720, longitude: -94.000)
                ]
                let polygon = GeometryEngine.project(Polygon(points: boundaryPoints), into: statePlaneNorthCentralTexas)!
                return polygon
            }()

            // Create map.
            map = Self.makeMap(
                spatialReference: statePlaneNorthCentralTexas,
                viewpointGeometry: boundaryPolygon
            )

            // Create graphics overlays.
            boundaryGraphicsOverlay = Self.makeBoundaryGraphicsOverlay(
                boundaryGeometry: boundaryPolygon
            )
            bufferGraphicsOverlay = Self.makeBufferGraphicsOverlay()
            tapPointsGraphicsOverlay = Self.makeTappedPointsGraphicsOverlay()
        }

        /// Creates a map with image layers from a spatial reference.
        /// - Parameters:
        ///   - spatialReference: The `SpatialReference` the `Map` is derived from.
        ///   - viewpointGeometry: The `Geometry` to center the map's viewpoint on.
        /// - Returns: A new `Map` object with added base layers.
        private static func makeMap(spatialReference: SpatialReference, viewpointGeometry: Geometry) -> Map {
            // Create a map with the spatial reference.
            let map = Map(spatialReference: spatialReference)
            map.initialViewpoint = Viewpoint(boundingGeometry: viewpointGeometry)

            // Add some base layers (counties, cities, and highways).
            let usaLayer = ArcGISMapImageLayer(url: URL(
                string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer"
            )!)
            map.basemap = Basemap(baseLayer: usaLayer)

            return map
        }

        /// Creates a graphics overlay to show the spatial reference's valid area.
        /// - Parameter boundaryGeometry: The `Geometry` to create the boundary graphic from.
        /// - Returns: A new `GraphicsOverlay` object with a boundary graphic added.
        private static func makeBoundaryGraphicsOverlay(boundaryGeometry: Geometry) -> GraphicsOverlay {
            let graphicsOverlay = GraphicsOverlay()
            let lineSymbol = SimpleLineSymbol(style: .dash, color: .red, width: 5)
            let boundaryGraphic = Graphic(geometry: boundaryGeometry, symbol: lineSymbol)
            graphicsOverlay.addGraphic(boundaryGraphic)
            return graphicsOverlay
        }

        /// Creates a graphics overlay for the buffer graphics.
        /// - Returns: A new `GraphicsOverlay` object to be used for the buffers.
        private static func makeBufferGraphicsOverlay() -> GraphicsOverlay {
            let graphicsOverlay = GraphicsOverlay()
            let bufferPolygonOutlineSymbol = SimpleLineSymbol(style: .solid, color: .systemGreen, width: 3)
            let bufferPolygonFillSymbol = SimpleFillSymbol(style: .solid, color: .yellow.withAlphaComponent(0.6), outline: bufferPolygonOutlineSymbol)
            graphicsOverlay.renderer = SimpleRenderer(symbol: bufferPolygonFillSymbol)
            return graphicsOverlay
        }

        /// Creates a graphics overlay for the tapped points graphics.
        /// - Returns: A new `GraphicsOverlay` object to be used for the tapped points.
        private static func makeTappedPointsGraphicsOverlay() -> GraphicsOverlay {
            let graphicsOverlay = GraphicsOverlay()
            let circleSymbol = SimpleMarkerSymbol(style: .circle, color: .red, size: 10)
            graphicsOverlay.renderer = SimpleRenderer(symbol: circleSymbol)
            return graphicsOverlay
        }

        /// Checks if a point is within the valid area of use for this sample.
        /// - Parameter point: A `Point` to validate.
        /// - Returns: A `Bool` indicating whether it is within bounds.
        func boundaryContains(_ point: Point) -> Bool {
            guard GeometryEngine.doesGeometry(boundaryPolygon, contain: point) else {
                return false
            }
            return true
        }

        /// Draws points and their buffers on the map.
        /// - Parameter unioned: A Boolean indicating whether the buffers should be unioned.
        func drawBuffers(unioned: Bool) {
            // Clear existing buffers graphics before drawing.
            bufferGraphicsOverlay.removeAllGraphics()
            tapPointsGraphicsOverlay.removeAllGraphics()

            // Reduce the bufferPoints tuples into points and radii arrays.
            let (points, radii) = bufferPoints.reduce(into: ([Point](), [Double]())) { (result, pointAndRadius) in
                result.0.append(pointAndRadius.point)
                result.1.append(pointAndRadius.radius)
            }

            // Create the buffers.
            // Notice: the radius distances has the same unit of the map's spatial reference's unit.
            // In this case, the statePlaneNorthCentralTexas spatial reference uses US feet.
            let bufferPolygon = GeometryEngine.buffer(around: points, distances: radii, shouldUnion: unioned)

            // Add the tap points to the tapPointsGraphicsOverlay.
            points.forEach { point in
                tapPointsGraphicsOverlay.addGraphic(Graphic(geometry: point))
            }

            // Add the buffers to the bufferGraphicsOverlay.
            bufferPolygon.forEach { buffer in
                bufferGraphicsOverlay.addGraphic(Graphic(geometry: buffer))
            }
        }

        /// Adds a point with its radius to the buffer points array.
        /// - Parameters:
        ///   - point: The center point to create a buffer.
        ///   - radius: The radius of the buffer.
        func addBuffer(point: Point, radius: Double) {
            // Update the buffer radius with the text value.
            let radiusInMiles = Measurement(value: radius, unit: UnitLength.miles)

            // The spatial reference in this sample uses US feet as its unit.
            let radiusInFeet = radiusInMiles.converted(to: .feet).value

            // Add point with radius to bufferPoints Array.
            bufferPoints.append((point: point, radius: radiusInFeet))
        }

        /// Clears the bufferPoints array and related graphics.
        func clearBufferPoints() {
            bufferPoints.removeAll()
            bufferGraphicsOverlay.removeAllGraphics()
            tapPointsGraphicsOverlay.removeAllGraphics()
        }
    }
}

private extension CreateBuffersAroundPointsView {
    /// An enumeration for the sample status.
    enum Status {
        case addPoints, bufferCreated, outOfBoundsTap, invalidInput, noPoints

        /// The text message associated with the current status for the overlay.
        var label: String {
            switch self {
            case .addPoints: return "Tap on the map to add buffers."
            case .bufferCreated: return "Buffer created."
            case .outOfBoundsTap: return "Tap within the boundary to add buffer."
            case .invalidInput: return "Enter a value between 0 and 300 to create a buffer."
            case .noPoints: return "Add a point to draw the buffers."
            }
        }
    }
}

#Preview {
    NavigationView {
        CreateBuffersAroundPointsView()
    }
}

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