Create a convex hull for a given set of points. The convex hull is a polygon with shortest perimeter that encloses a set of points. As a visual analogy, consider a set of points as nails in a board. The convex hull of the points would be like a rubber band stretched around the outermost nails.
Use case
A convex hull can be useful in collision detection. For example, when charting the position of two yacht fleets (with each vessel represented by a point), if their convex hulls have been precomputed, it is efficient to first check if their convex hulls intersect before computing their proximity point-by-point.
How to use the sample
Tap on the map to add points. Click the "Create Convex Hull" button to generate the convex hull of those points. Click the "Reset" button to start over.
How it works
- Create an input geometry such as a
Multipoint
object. - Use
GeometryEngine.convexHull(inputGeometry)
to create a newGeometry
object representing the convex hull of the input points. The returned geometry will either be aPoint
,Polyline
, orPolygon
based on the number of input points.
Relevant API
- Geometry
- GeometryEngine
Tags
convex hull, geometry, spatial analysis
Sample Code
/* * 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 * * 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.createconvexhullaroundpoints import android.os.Bundle import android.util.Log import com.esri.arcgismaps.sample.sampleslib.EdgeToEdgeCompatActivity import androidx.databinding.DataBindingUtil import androidx.lifecycle.lifecycleScope import com.arcgismaps.ApiKey import com.arcgismaps.ArcGISEnvironment import com.arcgismaps.Color import com.arcgismaps.geometry.Geometry import com.arcgismaps.geometry.GeometryEngine import com.arcgismaps.geometry.Multipoint import com.arcgismaps.geometry.Point import com.arcgismaps.geometry.Polygon import com.arcgismaps.geometry.Polyline import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.Viewpoint 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.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.esri.arcgismaps.sample.createconvexhullaroundpoints.databinding.CreateConvexHullAroundPointsActivityMainBinding import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch class MainActivity : EdgeToEdgeCompatActivity() { // set up data binding for the activity private val activityMainBinding: CreateConvexHullAroundPointsActivityMainBinding by lazy { DataBindingUtil.setContentView(this, R.layout.create_convex_hull_around_points_activity_main) } // setup binding for the MapView private val mapView by lazy { activityMainBinding.mapView } // action button that creates the canvas hull private val createButton by lazy { activityMainBinding.createButton } // action button to reset the map private val resetButton by lazy { activityMainBinding.resetButton } // a red marker symbol for points private val pointSymbol = SimpleMarkerSymbol(SimpleMarkerSymbolStyle.Circle, Color.red, 10f) // a blue line symbol private val lineSymbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.blue, 3f) // a fill symbol with an empty fill for polygons private val fillSymbol = SimpleFillSymbol(SimpleFillSymbolStyle.Null, Color.red, lineSymbol) // set up the point graphic with point symbol private val pointGraphic = Graphic(symbol = pointSymbol) // init the convex hull graphic private val convexHullGraphic = Graphic() // create a graphics overlay to draw all graphics private val graphicsOverlay = GraphicsOverlay() // list to store the selected map points private val inputPoints = mutableListOf<Point>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN) lifecycle.addObserver(mapView) // add point and convex hull graphics to the graphics overlay graphicsOverlay.graphics.addAll(listOf(pointGraphic, convexHullGraphic)) // create and add a map with topographic basemap style val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { // set a default initial point and scale initialViewpoint = Viewpoint(Point(34.77, -10.24), 20e7) } // configure map view assignments mapView.apply { this.map = map // add the graphics overlay to the mapview graphicsOverlays.add(graphicsOverlay) } lifecycleScope.launch { // if the map load fails show the error and return map.load().onFailure { return@launch showError("Error loading map") } // capture and collect when the user taps on the screen mapView.onSingleTapConfirmed.collect { event -> event.mapPoint?.let { point -> addMapPoint(point) } } } // add a click listener to create a convex hull createButton.setOnClickListener { // check if the pointGraphic's geometry is not null pointGraphic.geometry?.let { geometry -> createConvexHull(geometry) } } // add a click listener to reset the map resetButton.setOnClickListener { resetMap() } } /** * Adds the [point] to the map drawn as a Multipoint geometry */ private fun addMapPoint(point: Point) { // add the new point to the points list inputPoints.add(point) // recreate the graphics geometry representing the input points pointGraphic.geometry = Multipoint(inputPoints) // enable all the action buttons, since we have at least one point drawn createButton.isEnabled = true resetButton.isEnabled = true } /** * Creates and draws a convex hull graphic on the map using [pointGeometry] points */ private fun createConvexHull(pointGeometry: Geometry) { // normalize the geometry for panning beyond the meridian // and proceed if the resulting geometry is not null val normalizedPointGeometry = GeometryEngine.normalizeCentralMeridian(pointGeometry) ?: return showError("Error normalizing point geometry") // create a convex hull from the points and proceed if it's not null val convexHullGeometry = GeometryEngine.convexHullOrNull(normalizedPointGeometry) // the convex hull's geometry may be a point or polyline if the number of // points is less than 3, set its symbol accordingly convexHullGraphic.symbol = when (convexHullGeometry) { is Point -> { // set symbol to use the pointSymbol pointSymbol } is Polyline -> { // set symbol to use the lineSymbol lineSymbol } is Polygon -> { // set symbol to use the fillSymbol fillSymbol } else -> { showError("Unknown geometry for convex hull") null } } // update the convex hull graphics geometry convexHullGraphic.geometry = convexHullGeometry // disable the create button until new input points are created createButton.isEnabled = false } /** * Resets the map by clearing any drawn points, graphics and disables all buttons */ private fun resetMap() { // remove all the selected points inputPoints.clear() // remove the geometry for the point graphic and convex hull graphics pointGraphic.geometry = null convexHullGraphic.geometry = null // disable the buttons resetButton.isEnabled = false createButton.isEnabled = false } private fun showError(message: String) { Log.e(localClassName, message) Snackbar.make(mapView, message, Snackbar.LENGTH_SHORT).show() } } /** * Simple extension property that represents a blue color */ private val Color.Companion.blue get() = fromRgba(0, 0, 255)