Skip to content
View on GitHubSample viewer app

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

Click/tap on the map to add points. Click the "Create Buffer(s)" button to draw buffer(s) around the points (the size of the buffer is determined by the value entered by the user). Check the check box if you want the result to union (combine) the buffers. Click 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.bufferOrNull(point, radius) to create a Polygon and GeometryEngine.unionOrNull(polygons) to union polygons. The parameter points are the points to buffer around, distances are the buffer distances for each point (in meters).
  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.bufferOrNull
  • GeometryEngine.unionOrNull
  • 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

CreateBuffersAroundPointsViewModel.ktCreateBuffersAroundPointsViewModel.ktMainActivity.ktCreateBuffersAroundPointsScreen.kt
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
/* 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
 *
 *    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.
 *
 */

package com.esri.arcgismaps.sample.createbuffersaroundpoints.components

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.Color
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.Latitude
import com.arcgismaps.geometry.Longitude
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.Polygon
import com.arcgismaps.geometry.PolygonBuilder
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.Basemap
import com.arcgismaps.mapping.layers.ArcGISMapImageLayer
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.symbology.SimpleFillSymbol
import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol
import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle
import com.arcgismaps.mapping.symbology.SimpleRenderer
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

/**
 * ViewModel for the "Create buffers around points" sample.
 */
class CreateBuffersAroundPointsViewModel(app: Application) : AndroidViewModel(app) {

    // Message dialog VM for reporting errors to the UI
    val messageDialogVM = MessageDialogViewModel()

    // Create the spatial reference (State Plane North Central Texas, WKID 32038)
    val statePlaneNorthCentralTexas = SpatialReference(wkid = 32038)

    // Map with projected spatial reference (State Plane North Central Texas).
    val arcGISMap by lazy {
        ArcGISMap(spatialReference = statePlaneNorthCentralTexas).apply {
            // Set initial viewpoint of the map
            initialViewpoint = Viewpoint(boundingGeometry = boundaryPolygon)
            // Set the basemap base layer to display the USA map server.
            setBasemap(
                basemap = Basemap(
                    baseLayer = ArcGISMapImageLayer(
                        url = "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer"
                    )
                )
            )
        }
    }
    // The boundary polygon that defines the valid area of use.
    private val boundaryPolygon: Polygon by lazy {
        // Build the boundary polygon coordinates in lat/long and project into the state plane spatial reference
        val boundaryLatLon = listOf(
            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))
        )

        // Project the polygon into the State Plane spatial reference
        val projectedBoundaryPoints = boundaryLatLon.mapNotNull { point ->
            GeometryEngine.projectOrNull(
                geometry = point,
                spatialReference = statePlaneNorthCentralTexas
            )
        }
        // Build the boundary polygon geometry using the projected points
        PolygonBuilder(spatialReference = statePlaneNorthCentralTexas).apply {
            projectedBoundaryPoints.forEach { addPoint(it) }
        }.toGeometry()
    }

    // Graphics overlays: boundary, buffers, and tapped points
    private val boundaryGraphicsOverlay by lazy {
        GraphicsOverlay().apply {
            // Create a dashed red boundary graphic
            graphics += Graphic(
                geometry = boundaryPolygon,
                symbol = SimpleLineSymbol(
                    style = SimpleLineSymbolStyle.Dash,
                    color = Color.red,
                    width = 5f
                )
            )
        }
    }

    // Configure buffer overlay renderer (yellow fill with green outline)
    private val bufferGraphicsOverlay by lazy {
        val bufferOutline = SimpleLineSymbol(
            style = SimpleLineSymbolStyle.Solid,
            color = Color.green,
            width = 3f
        )
        val bufferFill = SimpleFillSymbol(
            style = SimpleFillSymbolStyle.Solid,
            color = Color.fromRgba(r = 255, g = 255, b = 0, a = 153),
            outline = bufferOutline
        )
        GraphicsOverlay().apply { renderer = SimpleRenderer(symbol = bufferFill) }
    }


    // Configure tapped points overlay renderer (small red circles)
    private val tappedPointsGraphicsOverlay by lazy {
        val tapSymbol = SimpleMarkerSymbol(
            style = SimpleMarkerSymbolStyle.Circle,
            color = Color.red,
            size = 10f
        )
        GraphicsOverlay().apply { renderer = SimpleRenderer(symbol = tapSymbol) }
    }

    val graphicsOverlays: List<GraphicsOverlay>
        get() = listOf(boundaryGraphicsOverlay, bufferGraphicsOverlay, tappedPointsGraphicsOverlay)

    // Keep last tapped point when requesting radius input
    private var lastTappedPoint: Point? = null

    // List of (point, radiusInMapUnits)
    private val bufferPoints: MutableList<Pair<Point, Double>> = mutableListOf()


    private val _statusText = MutableStateFlow("Tap on the map to add buffers.")
    val statusText = _statusText.asStateFlow()

    private val _isInputDialogVisible = MutableStateFlow(false)
    val isInputDialogVisible = _isInputDialogVisible.asStateFlow()

    // Controls whether buffers should be union
    private val _shouldUnion = MutableStateFlow(false)
    val shouldUnion = _shouldUnion.asStateFlow()

    init {
        viewModelScope.launch {
            // Load the map; report any failure
            arcGISMap.load().onFailure { messageDialogVM.showMessageDialog(it) }
        }
    }

    /**
     * Called by the MapView on single tap. If the tap is within the valid boundary,
     * request a buffer radius input from the UI. Otherwise update status to indicate out-of-bounds.
     */
    fun onMapTapped(mapPoint: Point) {
        val contains = GeometryEngine.contains(boundaryPolygon, mapPoint)
        if (!contains) {
            _statusText.value = "Tap within the boundary to add buffer."
            return
        }

        // Store the tapped point and request input dialog
        lastTappedPoint = mapPoint
        _isInputDialogVisible.value = true
        _statusText.value = "Enter buffer radius (miles) for the tapped location"
    }

    /**
     * Submit the radius (in miles) & converts miles to the map's linear units,
     * (the state plane spatial reference uses US feet) then creates the buffer and updates the overlays.
     */
    fun submitRadiusMiles(radiusMiles: Double) {
        val point = lastTappedPoint ?: return

        if (radiusMiles <= 0.0 || radiusMiles >= 300) {
            return messageDialogVM.showMessageDialog("Please enter a value between 0 & 300.")
        }

        // Convert miles to feet (1 mile = 5280 feet). The state plane uses US feet for this sample.
        val radiusFeet = radiusMiles * 5280.0

        // Add to internal list and draw
        bufferPoints.add(point to radiusFeet)

        // Add tap point graphic
        tappedPointsGraphicsOverlay.graphics.add(Graphic(geometry = point))

        // Redraw buffers using current union setting
        drawBuffers(unioned = _shouldUnion.value)

        _isInputDialogVisible.value = false
        _statusText.value = "Buffer created. Tap to add more points or press Clear."
    }

    /**
     * Draws buffers for all stored bufferPoints. If unioned is true, attempt to union buffers
     * into a single geometry before displaying.
     */
    fun drawBuffers(unioned: Boolean) {
        // Clear existing buffer graphics
        bufferGraphicsOverlay.graphics.clear()

        if (bufferPoints.isEmpty()) {
            _statusText.value = "Add a point to draw the buffers."
            return
        }

        // Create buffer geometries for each point
        val polygons = bufferPoints.mapNotNull { (pt, radius) ->
            GeometryEngine.bufferOrNull(pt, radius)
        }

        if (polygons.isEmpty()) {
            messageDialogVM.showMessageDialog("Error creating buffer geometries")
            return
        }

        if (unioned) {
            // Union the polygons into a single geometry (may return a Polygon or Multipart geometry)
            GeometryEngine.unionOrNull(polygons)?.let { unionedGeometry ->
                bufferGraphicsOverlay.graphics.add(Graphic(geometry = unionedGeometry))
            }
        } else {
            // Add each polygon as its own graphic
            polygons.forEach { bufferGraphicsOverlay.graphics.add(Graphic(geometry = it)) }
        }
        _statusText.value = "Buffers drawn (${if (unioned) "unioned" else "individual"})."

    }

    /**
     * Update the union toggle and redraw buffers using the new value.
     */
    fun updateUnion(shouldUnion: Boolean) {
        _shouldUnion.value = shouldUnion
        drawBuffers(unioned = shouldUnion)
    }

    /**
     * Clears all buffer points and corresponding graphics.
     */
    fun clearAll() {
        bufferPoints.clear()
        bufferGraphicsOverlay.graphics.clear()
        tappedPointsGraphicsOverlay.graphics.clear()
        _statusText.value = "Tap on the map to add buffers."
    }

    /**
     * Dismiss the radius input dialog.
     */
    fun dismissInputDialog() {
        _isInputDialogVisible.value = false
        _statusText.value = "Tap on the map to add buffers."
    }
}

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