Analyze hotspots

View on GitHubSample viewer app

Use a geoprocessing service and a set of features to identify statistically significant hot spots and cold spots.

Image of analyze hotspots

Use case

This tool identifies statistically significant spatial clusters of high values (hot spots) and low values (cold spots). For example, a hotspot analysis based on the frequency of 911 calls within a set region.

How to use the sample

Tap on Analyze, and select a date from the "FROM" DatePicker and "TO" DatePicker to get a date range from the BottomSheet and tap on Run analysis. The results will be shown on the map upon successful completion of the GeoprocessingJob.

How it works

  1. Create a GeoprocessingTask with the URL set to the endpoint of a geoprocessing service.
  2. Create a query string with the date range as an input of GeoprocessingParameters.
  3. Use the GeoprocessingTask to create a GeoprocessingJob with the GeoprocessingParameters instance.
  4. Start the GeoprocessingJob and wait for it to complete and return a GeoprocessingResult.
  5. Get the resulting ArcGISMapImageLayer using GeoprocessingResult.mapImageLayer.
  6. Add the layer to the map's operational layers.

Relevant API

  • GeoprocessingJob
  • GeoprocessingParameters
  • GeoprocessingResult
  • GeoprocessingTask

Additional information

This sample uses the GeoView-Compose Toolkit module to be able to implement a composable MapView.

Tags

analysis, density, geoprocessing, geoview-compose, hot spots, hotspots, toolkit

Sample Code

MapViewModel.ktMapViewModel.ktMainActivity.ktMainScreen.ktBottomSheetScreen.ktBottomAppContent.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
/* 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.analyzehotspots.components

import android.app.Application
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.tasks.geoprocessing.GeoprocessingJob
import com.arcgismaps.tasks.geoprocessing.GeoprocessingParameters
import com.arcgismaps.tasks.geoprocessing.GeoprocessingResult
import com.arcgismaps.tasks.geoprocessing.GeoprocessingTask
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingString
import com.esri.arcgismaps.sample.analyzehotspots.R
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter

class MapViewModel(
    private val application: Application,
    private val sampleCoroutineScope: CoroutineScope,
) : AndroidViewModel(application) {
    // create a map using the topographic basemap style
    val map: ArcGISMap by mutableStateOf(ArcGISMap(BasemapStyle.ArcGISTopographic))

    // create a ViewModel to handle dialog interactions
    val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel()

    // determinate job progress loading dialog
    val showJobProgressDialog = mutableStateOf(false)

    // determinate job progress percentage
    val geoprocessingJobProgress = mutableIntStateOf(0)

    // job used to run the geoprocessing task on a service
    private var geoprocessingJob: GeoprocessingJob? = null

    init {
       map.apply {
           // Set the map's initialViewpoint
           initialViewpoint = Viewpoint(
               center = Point(-13671170.0, 5693633.0, SpatialReference(wkid = 3857)),
               scale = 1e5
           )
       }
    }
    /**
     * Creates a [geoprocessingJob] with the default [GeoprocessingParameters]
     * and a custom query date range between [fromDate] & [toDate]
     */
    suspend fun createGeoprocessingJob(
        fromDate: String,
        toDate: String,
    ) {
        // a map image layer might be generated, clear previous results
        map.operationalLayers.clear()

        // create and load geoprocessing task
        val geoprocessingTask = GeoprocessingTask(application.getString(R.string.service_url))
        geoprocessingTask.load().getOrElse {
            messageDialogVM.showMessageDialog(it.message.toString(), it.cause.toString())
        }

        // create parameters for geoprocessing job
        val geoprocessingParameters = geoprocessingTask.createDefaultParameters().getOrElse {
            messageDialogVM.showMessageDialog(it.message.toString(), it.cause.toString())
        } as GeoprocessingParameters

        val queryString = StringBuilder("(\"DATE\" > date '")
            .append(fromDate)
            .append(" 00:00:00' AND \"DATE\" < date '")
            .append(toDate)
            .append(" 00:00:00')")

        geoprocessingParameters.inputs["Query"] = GeoprocessingString(queryString.toString())

        // create and start geoprocessing job
        geoprocessingJob = geoprocessingTask.createJob(geoprocessingParameters)

        runGeoprocessingJob()
    }

    /**
     * Starts the [geoprocessingJob], shows the progress dialog and
     * displays the result hotspot map image layer to the MapView
     */
    private suspend fun runGeoprocessingJob() {
        geoprocessingJob?.let { geoprocessingJob ->
            // display the progress dialog
            showJobProgressDialog.value = true
            // start the job
            geoprocessingJob.start()
            // collect the job progress
            sampleCoroutineScope.launch {
                geoprocessingJob.progress.collect { progress ->
                    // updates the job progress dialog
                    geoprocessingJobProgress.intValue = progress
                    Log.i("Progress", "geoprocessingJobProgress: ${geoprocessingJobProgress.intValue}")
                }
            }
            // get the result of the job on completion
            geoprocessingJob.result().onSuccess {
                // dismiss the progress dialog
                showJobProgressDialog.value = false
                // get the job's result
                val geoprocessingResult = geoprocessingJob.result().getOrElse {
                    messageDialogVM.showMessageDialog(it.message.toString(), it.cause.toString())
                } as GeoprocessingResult
                // resulted hotspot map image layer
                val hotspotMapImageLayer = geoprocessingResult.mapImageLayer?.apply {
                    opacity = 0.5f
                } ?: return messageDialogVM.showMessageDialog("Result map image layer is null")

                // add new layer to map
                map.operationalLayers.add(hotspotMapImageLayer)
            }.onFailure { throwable ->
                messageDialogVM.showMessageDialog(
                    title = throwable.message.toString(),
                    description = throwable.cause.toString()
                )
                showJobProgressDialog.value = false
            }
        }
    }

    fun cancelGeoprocessingJob() {
        sampleCoroutineScope.launch {
            geoprocessingJob?.cancel()
        }
    }

    /**
     * Convert epoch time in [millis] to String date format
     */
    fun convertMillisToString(millis: Long): String {
        val instant = Instant.ofEpochMilli(millis)
        val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
        val date = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
        return date.format(formatter)
    }
}

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