Export vector tiles

View inC++QMLView on GitHubSample viewer app

Export tiles from an online vector tile service.

screenshot

Use case

Field workers with limited network connectivity can use exported vector tiles as a basemap for use while offline.

How to use the sample

When the vector tiled layer loads, zoom in to the extent you want to export. The red box shows the extent that will be exported. Click the "Export area" button to start the job. When finished, a dialog will show the exported result as a new basemap.

How it works

  1. Create an ArcGISVectorTiledLayer from the map's base layers.
  2. Create an ExportVectorTilesTask using the vector tiled layer's URL.
  3. Create default ExportVectorTilesParameters from the task, specifying extent and maximum scale.
  4. Create a ExportVectorTilesJob from the task using the parameters, and specifying a vector tile cache path and an item resource path. The resource path is required if you want to export the tiles with the style.
  5. Start the job, and once it completes successfully, get the resulting ExportVectorTilesResult.
  6. Get the VectorTileCache and ItemResourceCache from the result to create an ArcGISVectorTiledLayer that can be displayed to the map view.

Relevant API

  • ArcGISVectorTiledLayer
  • ExportVectorTilesJob
  • ExportVectorTilesParameters
  • ExportVectorTilesResult
  • ExportVectorTilesTask
  • ItemResourceCache
  • VectorTileCache

Additional information

NOTE: Downloading tiles for offline use requires authentication with the web map's server. To use this sample, you will require an ArcGIS Online account.

Vector tiles have high drawing performance and smaller file size compared to regular tiled layers due to consisting solely of points, lines, and polygons. Visit Layer types on the ArcGIS Online Developer's portal to learn more.

Tags

cache, download, offline, vector, vector tiled layer

Sample Code

ExportVectorTiles.qml
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
// [WriteFile Name=ExportVectorTiles, Category=Layers]
// [Legal]
// Copyright 2022 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.
// [Legal]

import QtQuick
import Esri.ArcGISRuntime
import QtQuick.Controls
import Esri.ArcGISExtras

Rectangle {
    id: rootRectangle
    clip: true
    width: 800
    height: 600

    property int exportProgress: 0
    property bool isUsingOfflineBasemap: false

    enum ExportJobStatus {
      NotStarted,
      Started,
      Paused,
      Succeeded,
      Failed,
      Cancelling
    }

    property int exportJobStatus: ExportVectorTiles.ExportJobStatus.NotStarted

    // add a mapView component
    MapView {
        id: mapView
        anchors.fill: parent

        Map {
            id: map
            initBasemapStyle: Enums.BasemapStyleArcGISStreetsNight
            initialViewpoint: ViewpointCenter {
                id: viewpoint
                Point {
                    x: -117.181
                    y: 34.049
                    spatialReference: SpatialReference { wkid: 4326 }
                }
                targetScale: 1e4
            }
        }

        GraphicsOverlay {
            id: graphicsOverlay

            Graphic {
                id: exportAreaGraphic
                symbol: SimpleLineSymbol {
                    style: Enums.SimpleLineSymbolStyleSolid
                    color: "green"
                    width: 3
                }
            }
        }

        Component.onCompleted: {
            // Set and keep the focus on MapView to enable keyboard navigation
            forceActiveFocus();
        }
    }

    Rectangle {
        id: exportProgressWindow
        anchors.centerIn: parent

        color: "white"
        visible: false

        border {
            color: "black"
            width: 2
        }

        Column {
            id: exportProgressColumn
            anchors.centerIn: parent
            spacing: 10

            BusyIndicator {
                id: busyIndicator
                anchors.horizontalCenter: parent.horizontalCenter
                running: visible
            }

            Text {
                id: statusText
                anchors.horizontalCenter: parent.horizontalCenter
                text: "Export job status: " + ["Not started", "Started", "Paused", "Succeeded", "Failed", "Cancelling"][exportJobStatus]
                font.pixelSize: 16
            }

            Text {
                id: statusTextCanceled
                anchors.horizontalCenter: parent.horizontalCenter
                text: "Job cancelled"
                visible: !statusText.visible
                font.pixelSize: 16
            }

            Text {
                id: progressText
                anchors.horizontalCenter: parent.horizontalCenter
                text: exportProgress + "% Completed"
                font.pixelSize: 16
            }

            onWidthChanged: {
                exportProgressWindow.width = exportProgressColumn.width + 20
            }

            onHeightChanged: {
                exportProgressWindow.height = exportProgressColumn.height + 20
            }
        }
    }

    // Create an extent rectangle for selecting the offline area
    Rectangle {
        id: extentRectangle
        anchors.centerIn: parent

        width: parent.width - parent.width * 0.1
        height: parent.height - parent.height * 0.25
        color: "transparent"
        border {
            color: "red"
            width: 3
        }
    }

    // Button to start the download
    Button {
        id: button
        anchors {
            bottom: parent.bottom
            bottomMargin: rootRectangle.height * .05
            horizontalCenter: parent.horizontalCenter
        }
        width: 150

        text: "Export area"

        onClicked: {
            switch(exportJobStatus) {
            case ExportVectorTiles.ExportJobStatus.NotStarted:
                startExport(extentRectangle.x, (extentRectangle.y + extentRectangle.height), (extentRectangle.x + extentRectangle.width), extentRectangle.y);
                break;
            case ExportVectorTiles.ExportJobStatus.Started:
                cancel();
                break;
            case ExportVectorTiles.ExportJobStatus.Paused:
                break;
            case ExportVectorTiles.ExportJobStatus.Succeeded:
                reset();
                break;
            case ExportVectorTiles.ExportJobStatus.Failed:
                startExport(extentRectangle.x, (extentRectangle.y + extentRectangle.height), (extentRectangle.x + extentRectangle.width), extentRectangle.y);
                break;
            case ExportVectorTiles.ExportJobStatus.Cancelling:
                break;
            default:
                break;
            }
        }
    }

    function startExport(xSW, ySW, xNE, yNE) {
        if (map.basemap === undefined || map.basemap.loadStatus !== Enums.LoadStatusLoaded)
            return;

        const vectorTiledLayer = map.basemap.baseLayers.get(0);
        if (vectorTiledLayer === undefined || vectorTiledLayer.layerType !== Enums.LayerTypeArcGISVectorTiledLayer)
            return;

        // Create an envelope from the QML rectangle corners
        const pointSW = mapView.screenToLocation(xSW, ySW);
        const pointNE = mapView.screenToLocation(xNE, yNE);
        const extent = ArcGISRuntimeEnvironment.createObject("Envelope", {
            xMin: pointSW.x,
            xMax: pointNE.x,
            yMin: pointNE.y,
            yMax: pointSW.y
        });
        const exportArea = GeometryEngine.normalizeCentralMeridian(GeometryEngine.project(extent, vectorTiledLayer.spatialReference));
        extentRectangle.visible = false;
        exportProgressWindow.visible = true;
        exportAreaGraphic.geometry = exportArea;

        // Ensure exportVectorTilesTask is not loaded because we can't set the url after it is loaded
        if (exportVectorTilesTask.loadStatus !== Enums.LoadStatusLoaded) {
            exportVectorTilesTask.url = vectorTiledLayer.url;
        }

        // Instantiate export parameters to create the export job with
        exportVectorTilesTask.createDefaultExportVectorTilesParameters(exportArea, mapView.mapScale * 0.1);
    }

    ExportVectorTilesTask {
        id: exportVectorTilesTask

        property ExportVectorTilesJob exportVectorTilesJob;

        onCreateDefaultExportVectorTilesParametersStatusChanged: {
            if (createDefaultExportVectorTilesParametersStatus !== Enums.TaskStatusCompleted)
                return;

            // Using the reduced fonts service will reduce the download size of a vtpk by around 80 Mb
            // It is useful for taking the basemap offline but not recommended if you plan to later upload the vtpk
            defaultExportVectorTilesParameters.esriVectorTilesDownloadOption = Enums.EsriVectorTilesDownloadOptionUseReducedFontsService;

            // Create a path to store the vector tile package, the file must not already exist
            const vectorTileCachePath = System.temporaryFolder.url + "/vectorTiles_%1.vtpk".arg(new Date().getTime().toString());
            // Create a path to an empty directory store the styling resources (in this case, the night mode version of the layer)
            const itemResourcePath = System.temporaryFolder.url + "/itemResources_%1".arg(new Date().getTime().toString());

            exportVectorTilesJob = exportVectorTilesWithStyleResources(defaultExportVectorTilesParameters, vectorTileCachePath, itemResourcePath);
            exportVectorTilesJobConnections.target = exportVectorTilesJob;

            exportVectorTilesJob.resultChanged.connect(() => {
                if (exportVectorTilesJob.result) {
                    // Create a vector tiled layer when the download is completed
                    const exportedVectorTiledLayer = ArcGISRuntimeEnvironment.createObject("ArcGISVectorTiledLayer", {
                        vectorTileCache: exportVectorTilesJob.result.vectorTileCache,
                        itemResourceCache: exportVectorTilesJob.result.itemResourceCache
                    });
                    const basemap = ArcGISRuntimeEnvironment.createObject("Basemap");
                    basemap.baseLayers.append(exportedVectorTiledLayer);
                    map.basemap = basemap;
                    isUsingOfflineBasemap = true;
                    exportProgressWindow.visible = false;
                }
            });

            // Display the download progress to the user
            exportVectorTilesJob.progressChanged.connect(() => {
               exportProgress = exportVectorTilesJob.progress;
            });

            exportVectorTilesJob.statusChanged.connect((status) => {
                exportJobStatus = status;
                switch(exportJobStatus) {
                case ExportVectorTiles.ExportJobStatus.NotStarted:
                    button.text = "Export area"
                    exportProgressWindow.visible = false;
                    break;
                case ExportVectorTiles.ExportJobStatus.Started:
                    button.text = "Cancel export"
                    break;
                case ExportVectorTiles.ExportJobStatus.Paused:
                    break;
                case ExportVectorTiles.ExportJobStatus.Succeeded:
                    button.text = "Reset"
                    exportProgressWindow.visible = false;
                    break;
                case ExportVectorTiles.ExportJobStatus.Failed:
                    button.text = "Export area"
                    exportProgressWindow.visible = false;
                    break;
                case ExportVectorTiles.ExportJobStatus.Cancelling:
                    break;
                default:
                    break;
                }
            });

            // Start the export job once export parameters have been created
            exportVectorTilesJob.start();
        }

        readonly property Timer timer: Timer {
            id: jobCancelDoneTimer
            interval: 2000
            onTriggered: { exportProgressWindow.visible = false; statusText.visible = true }
        }

        Connections {
            id: exportVectorTilesJobConnections
            ignoreUnknownSignals: true
            function onCancelAsyncTaskStatusChanged() {
                if(exportVectorTilesTask.exportVectorTilesJob.cancelAsyncTaskStatus === Enums.TaskStatusCompleted)
                    statusTextCanceled.text = "Job canceled successfully";
                else if(exportVectorTilesTask.exportVectorTilesJob.cancelAsyncTaskStatus === Enums.TaskStatusCompleted)
                    statusTextCanceled.text = "Job failed to cancel successfully";
                else
                    return;
                exportProgressWindow.visible = true;
                statusText.visible = false;
                jobCancelDoneTimer.start();
            }
        }
    }

    function cancel() {
        exportProgressWindow.visible = false;
        exportVectorTilesTask.exportVectorTilesJob.cancelAsync();
        reset();
    }

    function reset() {
        if (isUsingOfflineBasemap) {
            map.basemap = ArcGISRuntimeEnvironment.createObject("Basemap", {initStyle: Enums.BasemapStyleArcGISStreetsNight});
            isUsingOfflineBasemap = false;
        }

        exportAreaGraphic.geometry = ArcGISRuntimeEnvironment.createObject("Point", {});
        extentRectangle.visible = true;
        button.text = "Export area"
        exportJobStatus = 0;
    }
}

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