Generate offline map with custom parameters

View on GitHubSample viewer app

Take a web map offline with additional options for each layer.

Image of generate offline map with custom parameters

Use case

When taking a web map offline, you may adjust the data (such as layers or tiles) that is downloaded by using custom parameter overrides. This can be used to reduce the extent of the map or the download size of the offline map. It can also be used to highlight specific data by removing irrelevant data. Additionally, this workflow allows you to take features offline that don't have a geometry - for example, features whose attributes have been populated in the office, but still need a site survey for their geometry.

How to use the sample

To modify the overrides parameters:

  • Use the min/max scale input fields to adjust the level IDs to be taken offline for the streets basemap.
  • Use the extent buffer distance input field to set the buffer radius for the streets basemap.
  • Check the checkboxes for the feature operational layers you want to include in the offline map.
  • Use the min hydrant flow rate input field to only download features with a flow rate higher than this value.
  • Select the "Water Pipes" checkbox if you want to crop the water pipe features to the extent of the map.

After you have set up the overrides to your liking, tap the "Generate offline map" button to start the download. A progress bar will display. Tap the "Cancel" button if you want to stop the download. When the download is complete, the view will display the offline map. Pan around to see that it is cropped to the download area's extent.

How it works

  1. Load a web map from a PortalItem.
  2. Create an OfflineMapTask with the map.
  3. Generate default task parameters using the extent area you want to download with offlineMapTask.createDefaultGenerateOfflineMapParameters(areaOfInterest).
  4. Generate additional "override" parameters using the default parameters with offlineMapTask.createGenerateOfflineMapParameterOverrides(generateOfflineMapParameters).
  5. For the basemap:
    • Get the parameters OfflineMapParametersKey for the basemap layer.
    • Get the ExportTileCacheParameters for the basemap layer with parameterOverrides.exportTileCacheParameters[basemapParamKey].
    • Set the level IDs you want to download with exportTileCacheParameters.levelIds.add(...).
    • To buffer the extent, use exportTileCacheParameters.areaOfInterest where bufferedGeometry can be calculated with the GeometryEngine.
  6. To remove operational layers from the download:
    • Create an OfflineParametersKey with the operational layer.
    • Get the generate geodatabase layer options using the key with parameterOverrides.generateGeodatabaseParameters[key].layerOptions
    • Use the GenerateLayerOption list to remove the layer if the layer option's ID matches the layer's ID.
  7. To filter the features downloaded in an operational layer:
    • Get the layer options for the operational layer using the directions in step 6.
    • For the desired layer, set the filter clause with generateLayerOption.whereClause(...) and set the query option with generateLayerOption.queryOption = GenerateLayerQueryOption.UseFilter.
  8. To not crop a layer's features to the extent of the offline map (default is true):
    • Set layerOption.generateLayerOption = true.
  9. Create a GenerateOfflineMapJob with offlineMapTask.createGenerateOfflineMapJob(parameters, offlineMapPath, parameterOverrides. Start the GenerateOfflineMapJob with offlineMapJob.start(). You can collect on the job's progress to update a progress bar in your UI.
  10. When the job is done, check offlineMapJob.result().onSuccess { ... } to get a reference to the offlineMap.

Relevant API

  • ExportTileCacheParameters
  • GenerateGeodatabaseParameters
  • GenerateLayerOption
  • GenerateOfflineMapJob
  • GenerateOfflineMapParameterOverrides
  • GenerateOfflineMapParameters
  • OfflineMapParametersKey
  • OfflineMapTask

Additional information

For applications where you just need to take all layers offline, use the standard workflow (using only GenerateOfflineMapParameters). For a simple example of how you take a map offline, please consult the Generate offline map sample.

Tags

adjust, download, extent, filter, LOD, offline, override, parameters, reduce, scale range, setting

Sample Code

GenerateOfflineMapWithCustomParametersViewModel.ktGenerateOfflineMapWithCustomParametersViewModel.ktMainActivity.ktGenerateOfflineMapWithCustomParametersScreen.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
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
/* 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.generateofflinemapwithcustomparameters.components

import android.app.Application
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.Color
import com.arcgismaps.LoadStatus
import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.geometry.Envelope
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.PortalItem
import com.arcgismaps.mapping.layers.FeatureLayer
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.ScreenCoordinate
import com.arcgismaps.portal.Portal
import com.arcgismaps.tasks.geodatabase.GenerateGeodatabaseParameters
import com.arcgismaps.tasks.geodatabase.GenerateLayerQueryOption
import com.arcgismaps.tasks.offlinemaptask.GenerateOfflineMapJob
import com.arcgismaps.tasks.offlinemaptask.GenerateOfflineMapParameterOverrides
import com.arcgismaps.tasks.offlinemaptask.GenerateOfflineMapParameters
import com.arcgismaps.tasks.offlinemaptask.OfflineMapParametersKey
import com.arcgismaps.tasks.offlinemaptask.OfflineMapTask
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.esri.arcgismaps.sample.generateofflinemapwithcustomparameters.R
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File

class GenerateOfflineMapWithCustomParametersViewModel(private val application: Application) :
    AndroidViewModel(application) {

    private val provisionPath: String by lazy {
        application.getExternalFilesDir(null)?.path.toString() + File.separator + application.getString(R.string.generate_offline_map_with_custom_parameters_app_name)
    }

    val mapViewProxy = MapViewProxy()

    // View model to handle popup dialogs
    val messageDialogVM = MessageDialogViewModel()

    // Define map that returns an ArcGISMap
    var arcGISMap = ArcGISMap()
        private set

    // Define the download area graphic
    private val downloadAreaGraphic = Graphic().apply {
        symbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.red, 2f)
    }

    // Create a graphics overlay for the map view
    val graphicsOverlay = GraphicsOverlay().apply {
        graphics.add(downloadAreaGraphic)
    }

    // Defined to send messages related to offlineMapJob
    val snackbarHostState = SnackbarHostState()

    // Determinate job progress loading dialog visibility state
    var showJobProgressDialog by mutableStateOf(false)
        private set

    // Determinate job progress percentage
    var offlineMapJobProgress by mutableIntStateOf(0)
        private set

    var showResetButton by mutableStateOf(false)
        private set

    private var generateOfflineMapJob: GenerateOfflineMapJob? = null

    // Create an IntSize to retrieve dimensions of the map
    private var mapViewSize by mutableStateOf(IntSize(0, 0))

    fun updateMapViewSize(size: IntSize) {
        mapViewSize = size
    }

    init {
        setUpMap()
    }

    /**
     * Use map view's size to determine dimensions of the map to get the download offline area
     * and use [MapViewProxy] to assist in converting screen points to map points
     */
    fun calculateDownloadOfflineArea() {
        // Ensure the map is loaded before calculating the download area
        if (arcGISMap.loadStatus.value == LoadStatus.Loaded) {
            // Upper left corner of the area to take offline
            val minScreenPoint = ScreenCoordinate(200.0, 200.0)
            // Lower right corner of the downloaded area
            val maxScreenPoint = ScreenCoordinate(
                x = mapViewSize.width - 200.0, y = mapViewSize.height - 200.0
            )
            // Convert screen points to map points
            val minPoint = mapViewProxy.screenToLocationOrNull(minScreenPoint)
            val maxPoint = mapViewProxy.screenToLocationOrNull(maxScreenPoint)
            // Create an envelope to set the download area's geometry using the defined bounds
            if (minPoint != null && maxPoint != null) {
                val envelope = Envelope(minPoint, maxPoint)
                downloadAreaGraphic.geometry = envelope
            }
        }
    }

    /**
     * Sets up a portal item and displays map area to take offline
     */
    private fun setUpMap() {
        // Create a portal item with the itemId of the web map
        val portal = Portal("https://www.arcgis.com")
        val portalItem = PortalItem(portal, "acc027394bc84c2fb04d1ed317aac674")

        // Clear, then add the download graphic to the graphics overlay
        graphicsOverlay.graphics.clear()
        graphicsOverlay.graphics.add(downloadAreaGraphic)

        arcGISMap = ArcGISMap(portalItem)
        viewModelScope.launch(Dispatchers.Main) {
            arcGISMap.load().onFailure {
                messageDialogVM.showMessageDialog(
                    title = it.message.toString()
                )
            }
        }
        showResetButton = false
    }

    /**
     * Define the [GenerateOfflineMapParameters] for the offline map job and add the custom
     * [GenerateOfflineMapParameterOverrides] using the given override values.
     */
    fun defineGenerateOfflineMapParameters(
        minScale: Int,
        maxScale: Int,
        bufferDistance: Int,
        isIncludeSystemValvesEnabled: Boolean,
        isIncludeServiceConnectionsEnabled: Boolean,
        minHydrantFlowRate: Int,
        isCropWaterPipesEnabled: Boolean
    ) {
        // Create an offline map offlineMapTask with the map
        val offlineMapTask = OfflineMapTask(arcGISMap)
        // The current area of interest displayed on the map
        val downloadArea = downloadAreaGraphic.geometry ?: return
        viewModelScope.launch {
            // Create default generate offline map parameters from the offline map task
            val generateOfflineMapParameters =
                offlineMapTask.createDefaultGenerateOfflineMapParameters(areaOfInterest = downloadArea).getOrElse {
                    messageDialogVM.showMessageDialog(
                        title = "Error", description = "Failed to create default generate offline map parameters"
                    )
                    return@launch
                }.apply {
                    // Return a job failure if generate offline map encounters an error
                    continueOnErrors = false
                }
            // Create parameter overrides for greater control
            offlineMapTask.createGenerateOfflineMapParameterOverrides(generateOfflineMapParameters)
                .onSuccess { parameterOverrides ->
                    // Set basemap scale and area of interest
                    setBasemapScaleAndAreaOfInterest(parameterOverrides, minScale, maxScale, bufferDistance)
                    // Exclude system valve layer
                    if (!isIncludeSystemValvesEnabled) {
                        excludeLayerFromDownload(parameterOverrides, getFeatureLayer("System Valve"))
                    }
                    // Exclude service connection layer
                    if (!isIncludeServiceConnectionsEnabled) {
                        excludeLayerFromDownload(parameterOverrides, getFeatureLayer("Service Connection"))
                    }
                    // Crop pipes layer
                    if (isCropWaterPipesEnabled) {
                        getGenerateGeodatabaseParameters(
                            parameterOverrides, getFeatureLayer("Main")
                        )?.layerOptions?.forEach {
                            it.useGeometry = true
                        }
                    }
                    // Get a reference to the hydrant layer
                    getFeatureLayer("Hydrant")?.let { hydrantLayer ->
                        // Get it's service layer id
                        val serviceLayerId = getServiceLayerId(hydrantLayer)
                        getGenerateGeodatabaseParameters(
                            parameterOverrides, getFeatureLayer(hydrantLayer.name)
                        )?.layerOptions?.filter { it.layerId == serviceLayerId }?.forEach {
                            it.whereClause = "FLOW >= $minHydrantFlowRate"
                            it.queryOption = GenerateLayerQueryOption.UseFilter
                        }
                    }
                    // Start a an offline map job from the task and parameters
                    createOfflineMapJob(offlineMapTask, generateOfflineMapParameters, parameterOverrides)
                }
        }
    }

    /**
     * Generate an offline map job with the given [OfflineMapTask], [GenerateOfflineMapParameters] and
     * [GenerateOfflineMapParameterOverrides].
     */
    private fun createOfflineMapJob(
        offlineMapTask: OfflineMapTask,
        generateOfflineMapParameters: GenerateOfflineMapParameters,
        parameterOverrides: GenerateOfflineMapParameterOverrides
    ) {
        // Store the offline map in the app's scoped storage directory
        val offlineMapPath = provisionPath + File.separator + "OfflineMap"
        val offlineMapFile = File(offlineMapPath)

        // Delete any offline map already present
        offlineMapFile.deleteRecursively()
        // Make the relevant directories for the offline map
        offlineMapFile.mkdirs()

        // Report any errors that occur during the offline map job
        viewModelScope.launch(Dispatchers.Main) {
            offlineMapTask.load().onFailure { error ->
                messageDialogVM.showMessageDialog(
                    title = error.message.toString(), description = error.cause.toString()
                )
            }
        }

        // Create an offline map job with the download directory path and parameters and start the job
        generateOfflineMapJob = offlineMapTask.createGenerateOfflineMapJob(
            parameters = generateOfflineMapParameters,
            downloadDirectoryPath = offlineMapPath,
            overrides = parameterOverrides
        )

        runOfflineMapJob()
    }

    /**
     * Starts the [GenerateOfflineMapJob], shows the progress dialog and displays the result offline map to the MapView.
     */
    private fun runOfflineMapJob() {
        // Show the Job Progress Dialog
        showJobProgressDialog = true
        with(viewModelScope) {
            // Create a flow-collection for the job's progress
            launch(Dispatchers.Main) {
                generateOfflineMapJob?.progress?.collect { progress ->
                    // Display the current job's progress value
                    offlineMapJobProgress = progress
                }
            }
            launch(Dispatchers.IO) {
                // Start the job and wait for Job result
                generateOfflineMapJob?.start()
                generateOfflineMapJob?.result()?.onSuccess {
                    // Set the offline map result as the displayed map and clear the red bounding box graphic
                    arcGISMap = it.offlineMap
                    showResetButton = true
                    graphicsOverlay.graphics.clear()
                    // Dismiss the progress dialog
                    showJobProgressDialog = false
                    // Show user where map was locally saved
                    snackbarHostState.showSnackbar(message = "Map saved at: " + generateOfflineMapJob?.downloadDirectoryPath)
                }?.onFailure { throwable ->
                    messageDialogVM.showMessageDialog(
                        title = throwable.message.toString(), description = throwable.cause.toString()
                    )
                    showJobProgressDialog = false
                }
            }
        }
    }

    /**
     * Cancel the offline map job.
     */
    fun cancelOfflineMapJob() {
        with(viewModelScope) {
            launch(Dispatchers.IO) {
                generateOfflineMapJob?.cancel()
            }
            launch(Dispatchers.Main) {
                snackbarHostState.showSnackbar(message = "User canceled.")
            }
        }
    }

    /**
     * Set basemap scale and area of interest using the given values
     */
    private fun setBasemapScaleAndAreaOfInterest(
        parameterOverrides: GenerateOfflineMapParameterOverrides, minScale: Int, maxScale: Int, bufferDistance: Int
    ) {
        // Get the first basemap layer
        arcGISMap.basemap.value?.baseLayers?.first()?.let { basemapLayer ->
            // Use the basemap layer to make an offline map parameters key
            val key = OfflineMapParametersKey(basemapLayer)
            // Create export tile cache parameters
            val exportTileCacheParameters = parameterOverrides.exportTileCacheParameters[key]?.apply {
                // Create a new list of levels in the scale range requested by the user
                levelIds.clear()
                levelIds.addAll((minScale until maxScale).toList())
            }
            downloadAreaGraphic.geometry?.let { downloadArea ->
                // Set the area of interest to the original download area plus a buffer
                exportTileCacheParameters?.areaOfInterest =
                    GeometryEngine.bufferOrNull(downloadArea, bufferDistance.toDouble())
            }
        }
    }

    /**
     * Remove the layer named from the generate layer options list in the generate geodatabase parameters.
     */
    private fun excludeLayerFromDownload(
        parameterOverrides: GenerateOfflineMapParameterOverrides, targetFeatureLayer: FeatureLayer?
    ) {
        // Get the layer's id
        val targetLayerId = getServiceLayerId(featureLayer = targetFeatureLayer)
        // Get the layer's layer options
        getGenerateGeodatabaseParameters(parameterOverrides, targetFeatureLayer)?.apply {
            // Remove the target layer
            layerOptions.remove(layerOptions.find { it.layerId == targetLayerId })
        }
    }

    /**
     * Helper function to add the [parameterOverrides] to the generate geodatabase parameters
     * using the given [targetFeatureLayer] to create the key.
     */
    private fun getGenerateGeodatabaseParameters(
        parameterOverrides: GenerateOfflineMapParameterOverrides, targetFeatureLayer: FeatureLayer?
    ): GenerateGeodatabaseParameters? {
        // Get the named feature layer
        targetFeatureLayer?.let {
            val key = OfflineMapParametersKey(it)
            // Return the layer's geodatabase parameters options
            return parameterOverrides.generateGeodatabaseParameters[key]
        }
        return null
    }

    /**
     * Helper function to get a feature layer by it's name.
     */
    private fun getFeatureLayer(layerName: String): FeatureLayer? {
        return arcGISMap.operationalLayers.find { it.name == layerName } as? FeatureLayer
    }

    /**
     * Helper function to get the service layer id for the given feature layer.
     */
    private fun getServiceLayerId(featureLayer: FeatureLayer?): Long? {
        return (featureLayer?.featureTable as? ServiceFeatureTable)?.layerInfo?.serviceLayerId
    }

    /**
     * Clear the preview map and set up mapView again
     */
    fun reset() {
        // Add the download graphic to the graphics overlay
        graphicsOverlay.graphics.clear()
        // Set up the portal item to take offline
        setUpMap()
    }
}

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