Skip To Content ArcGIS for Developers Sign In Dashboard

ArcGIS Runtime SDK for Qt

Generate offline map (overrides)

Sample Viewer View Sample on GitHub

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

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

  1. When the sample starts, you may be prompted to sign into arcgis.com.
  2. Click on "Generate Offline Map (Overrides)".
  3. Use the range slider to adjust the min/max levelIds to be taken offline for the Streets basemap.
  4. Use the spin-box to set the buffer radius for the streets basemap.
  5. Click the buttons to skip the System Valves and Service Connections layers.
  6. Use the combo-box to select the maximum flow rate for the features from the Hydrant layer.
  7. Use the check-box to skip the geometry filter for the water pipes features.
  8. Click "Start Job"
  9. Wait for the progress bar to indicate that the task has completed.
  10. You should see that the basemap does not display when you zoom out past a certain range and is padded around the original area of interest. The network dataset should extend beyond the target area. The System Valves and Service Connections should be omitted from the offline map and the Hydrants layer should contain a subset of the original features.

How it works

The sample creates a PortalItem object using a web map’s ID. This portal item is also used to initialize an OfflineMapTask object. When the button is clicked, the sample requests the default parameters for the task, with the selected extent, by calling OfflineMapTask::createDefaultGenerateOfflineMapParameters. Once the parameters are retrieved, they are used to create a set of GenerateOfflineMapParameterOverrides by calling OfflineMapTask::createGenerateOfflineMapParameterOverrides. The overrides are then adjusted so that specific layers will be taken offline using custom settings.

Streets basemap (adjust scale range)

In order to minimize the download size for offline map, this sample reduces the scale range for the "World Streets Basemap" layer by adjusting the relevant ExportTileCacheParameters in the GenerateOfflineMapParameterOverrides. The basemap layer is used to contsruct an OfflineMapParametersKeyobject. The key is then used to retrieve the specific ExportTileCacheParameters for the basemap and the levelIds are updated to skip unwanted levels of detail (based on the values selected in the UI). Note that the original "Streets" basemap is swapped for the "for export" version of the service - see https://www.arcgis.com/home/item.html?id=e384f5aa4eb1433c92afff09500b073d.

Streets Basemap (buffer extent)

To provide context beyond the study area, the extent for streets basemap is padded. Again, the key for the basemap layer is used to obtain the key and the default extent Geometry is retrieved. This extent is then padded (by the distance specified in the UI) using the GeoemetryEngine::bufferGeodesic function and applied to the ExportTileCacheParameters.

System Valves and Service Connections (skip layers)

In this example, the survey is primarily concerned with the Hydrants layer, so other information is not taken offline: this keeps the download smaller and reduces clutter in the offline map. The two layers "System Valves" and "Service Connections" are retrieved from the operational layers list of the map. They are then used to construct an OfflineMapParametersKey. This key is used to obtain the relevant GenerateGeodatabaseParameters from the GenerateOfflineMapParameterOverrides::generateGeodatabaseParameters property. The GenerateLayerOption for each of the layers is removed from the geodatabse parameters layerOptions by checking for the FeatureLayer::serviceLayerId. Note, that you could also choose to download only the schema for these layers by setting the GenerateLayerOption::queryOption to GenerateLayerQueryOption::None.

Hydrant Layer (filter features)

Next, the hydrant layer is filtered to exclude certain features. This approach could be taken if the offline map is intended for use with only certain data - for example, where a re-survey is required. To achieve this, a whereClause (for example, "Flow Rate (GPM) < 500") needs to be applied to the hydrant's GenerateLayerOption in the GenerateGeodatabaseParameters. The minimum flow rate value is obtained from the UI setting. The sample constructs a key object from the hydrant layer as in the previous step, and iterates over the available GenerateGeodatabaseParameters until the correct one is found and the GenerateLayerOption can be updated.

Water Pipes Dataset (skip geometry filter)

Lastly, the water network dataset is adjusted so that the features are downloaded for the entire dataset - rather than clipped to the area of interest. Again, the key for the layer is constructed using the layer and the relevant GenerateGeodatabaseParameters are obtained from the overrides dictionary. The layer options are then adjusted to set useGeometry to false.

Having adjusted the GenerateOfflineMapParameterOverrides to reflect the custom requirements for the offline map, the original parameters and the custom overrides, along with the download path for the offline map, are then used to create a GenerateOfflineMapJob object from the offline map task. This job is then started and on successful completion the offline map is added to the map view. To provide feedback to the user, the progress property of GenerateOfflineMapJob is displayed in a window.

As the web map that is being taken offline contains an Esri basemap, this sample requires that you sign in with an ArcGIS Online organizational account.

Relevant API

  • ExportTileCacheParameters
  • GenerateGeodatabaseParameters
  • GenerateLayerOption
  • GenerateOfflineMapJob
  • GenerateOfflineMapParameterOverrides
  • GenerateOfflineMapParameters
  • GenerateOfflineMapResult
  • 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

import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import Esri.ArcGISRuntime 100.9
import Esri.ArcGISExtras 1.1
import Esri.ArcGISRuntime.Toolkit.Dialogs 100.9

Rectangle {
    id: rootRectangle
    clip: true
    width: 800
    height: 600
    
    readonly property url outputMapPackage: System.temporaryFolder.url + "/OfflineMap_Overrides_%1.mmpk".arg(new Date().getTime().toString())
    readonly property string webMapId: "acc027394bc84c2fb04d1ed317aac674"
    property var generateJob: null
    property alias overrides: offlineMapTask.createGenerateOfflineMapParameterOverridesResult
    property alias parameters: offlineMapTask.createDefaultGenerateOfflineMapParametersResult

    MapView {
        id: mapView
        anchors.fill: parent

        // Create a Map from a Portal Item
        Map {
            id: map

            PortalItem {
                id: mapPortalItem

                itemId: webMapId
                Portal {
                    loginRequired: true
                }
            }
        }

        // Create a button and anchor it to the attribution top
        DownloadButton {
            id: downloadButton
            anchors {
                horizontalCenter: parent.horizontalCenter
                bottom: mapView.attributionTop
                margins: 5
            }
            visible: map.loadStatus === Enums.LoadStatusLoaded

            onButtonClicked: createDefaultParametersFromRectangle();
        }
    }

    // Create an extent rectangle for selecting the offline area
    Rectangle {
        id: extentRectangle
        anchors.centerIn: parent
        width: parent.width - 50
        height: parent.height - 125
        color: "transparent"
        visible: map.loadStatus === Enums.LoadStatusLoaded
        border {
            color: "red"
            width: 3
        }
    }

    function createDefaultParametersFromRectangle() {
        const corner1 = mapView.screenToLocation(extentRectangle.x, extentRectangle.y);
        const corner2 = mapView.screenToLocation((extentRectangle.x + extentRectangle.width), (extentRectangle.y + extentRectangle.height));
        const envBuilder = ArcGISRuntimeEnvironment.createObject("EnvelopeBuilder");
        envBuilder.setCorners(corner1, corner2);
        const mapExtent = GeometryEngine.project(envBuilder.geometry, Factory.SpatialReference.createWebMercator());
        offlineMapTask.createDefaultGenerateOfflineMapParameters(mapExtent);
    }

    // Create Offline Map Task
    OfflineMapTask {
        id: offlineMapTask
        portalItem: mapPortalItem

        onErrorChanged: console.log("error:", error.message, error.additionalMessage);

        onCreateDefaultGenerateOfflineMapParametersStatusChanged: {
            if (createDefaultGenerateOfflineMapParametersStatus !== Enums.TaskStatusCompleted)
                return;

            // Now that we have created our parameters, we can now create out overrides based upon
            // these.
            createGenerateOfflineMapParameterOverrides(createDefaultGenerateOfflineMapParametersResult);
        }

        onCreateGenerateOfflineMapParameterOverridesStatusChanged: {
            console.log("onCreateGenerateOfflineMapParameterOverridesStatusChanged", createGenerateOfflineMapParameterOverridesStatus);
        }


        Component.onDestruction: {
            if (generateJob) {
                generateJob.jobStatusChanged.disconnect(updateJobStatus);
                generateJob.progressChanged.disconnect(updateProgress);
            }
        }
    }

    function overridesReady() {
        return offlineMapTask.createGenerateOfflineMapParameterOverridesStatus === Enums.TaskStatusCompleted;
    }

    function setBasemapLOD(min, max) {
        if (!overridesReady())
            return;

        const layers = map.basemap.baseLayers;
        if (layers && layers.count < 1)
            return;

        // Obtain a key for the given basemap-layer.
        const keyForTiledLayer = ArcGISRuntimeEnvironment.createObject("OfflineMapParametersKey", {initLayer: layers.get(0)});

        if (keyForTiledLayer.empty || keyForTiledLayer.type !== Enums.OfflineMapParametersTypeExportTileCache)
            return;

        // Obtain the dictionary of parameters for taking the basemap offline.
        const dictionary = overrides.exportTileCacheParameters;
        if (!dictionary.contains(keyForTiledLayer))
            return;

        // Create a new sublist of LODs in the range requested by the user.
        const newLODs = [];
        for (let i = min; i < max; ++i )
            newLODs.push(i);

        // Apply the sublist as the LOD level in tilecache parameters for the given
        // service.
        const exportTileCacheParam = dictionary.value(keyForTiledLayer);
        exportTileCacheParam.levelIds = newLODs;
    }

    function setBasemapBuffer(bufferMeters) {
        if (!overridesReady())
            return;

        const layers = map.basemap.baseLayers;
        if (layers && layers.count < 1)
            return;

        // Obtain a key for the given basemap-layer.
        const keyForTiledLayer = ArcGISRuntimeEnvironment.createObject("OfflineMapParametersKey", {initLayer: layers.get(0)});

        if (keyForTiledLayer.empty || keyForTiledLayer.type !== Enums.OfflineMapParametersTypeExportTileCache)
            return;

        // Obtain the dictionary of parameters for taking the basemap offline.
        const dictionary = overrides.exportTileCacheParameters;
        if (!dictionary.contains(keyForTiledLayer))
            return;

        // Create a new geometry around the origional area of interest.
        const bufferGeom = GeometryEngine.buffer(parameters.areaOfInterest, bufferMeters);

        // Apply the geometry to the ExportTileCacheParameters.
        const exportTileCacheParam = dictionary.value(keyForTiledLayer);

        // Set the parameters back into the dictionary.
        exportTileCacheParam.areaOfInterest = bufferGeom;
    }

    function removeSystemValves() {
        removeFeatureLayer("System Valve");
    }


    function getFeatureLayerByName(layerName)
    {
        // Find the feature layer with the given name
        const opLayers = map.operationalLayers;
        for (let i = 0; i < opLayers.count; ++i)
        {
            const candidateLayer = opLayers.get(i);

            if (candidateLayer.layerType === Enums.LayerTypeFeatureLayer && candidateLayer.name.includes(layerName)) {
                return candidateLayer;
            }
        }

        return null;
    }

    function removeFeatureLayer(layerName) {
        if (!overridesReady())
            return;

        const targetLayer = getFeatureLayerByName(layerName);
        if (!targetLayer)
            return;

        // Obtain a key for the given basemap-layer.
        const keyForTargetLayer = ArcGISRuntimeEnvironment.createObject("OfflineMapParametersKey", {initLayer: targetLayer});

        if (keyForTargetLayer.empty || keyForTargetLayer.type !== Enums.OfflineMapParametersTypeGenerateGeodatabase)
            return;

        // Get the dictionary of GenerateGeoDatabaseParameters.
        const dictionary = overrides.generateGeodatabaseParameters;

        if (!dictionary.contains(keyForTargetLayer))
            return;

        // Grab the GenerateGeoDatabaseParameters associated with the given key.
        const generateGdbParam = dictionary.value(keyForTargetLayer);

        const table = targetLayer.featureTable;

        // Get the service layer id for the given layer.
        const targetLayerId = table.layerInfo.serviceLayerIdAsInt;

        // Remove the layer option from the list.
        const layerOptions = generateGdbParam.layerOptions;
        const newLayerOptions = [];
        for (let i = 0; i < layerOptions.length; i++) {
            if (layerOptions[i].layerIdAsInt !== targetLayerId) {
                newLayerOptions.push(layerOptions[i]);
            }
        }

        //// Add layer options back to parameters and re-add to the dictionary.
        generateGdbParam.layerOptions =  newLayerOptions;
    }

    function removeServiceConnection() {
        removeFeatureLayer("Service Connection");
    }

    function setHydrantWhereClause(whereClause) {

        if (!overridesReady())
            return;

        const targetLayer = getFeatureLayerByName("Hydrant");
        if (!targetLayer)
            return;

        // Obtain a key for the given basemap-layer.
        const keyForTargetLayer = ArcGISRuntimeEnvironment.createObject("OfflineMapParametersKey", {initLayer: targetLayer});

        if (keyForTargetLayer.empty || keyForTargetLayer.type !== Enums.OfflineMapParametersTypeGenerateGeodatabase)
            return;

        // Get the dictionary of GenerateGeoDatabaseParameters.
        const dictionary = overrides.generateGeodatabaseParameters;

        if (!dictionary.contains(keyForTargetLayer))
            return;

        // Grab the GenerateGeoDatabaseParameters associated with the given key.
        const generateGdbParam = dictionary.value(keyForTargetLayer);

        const table = targetLayer.featureTable;

        // Get the service layer id for the given layer.
        const targetLayerId = table.layerInfo.serviceLayerIdAsInt;

        // Update the where-clause on the layer.
        const layerOptions = generateGdbParam.layerOptions;
        for (let i = 0; i < layerOptions.length; i++) {
            const layerOption = layerOptions[i];
            if (layerOption.layerIdAsInt === targetLayerId) {
                layerOption.whereClause = whereClause;
                layerOption.queryOption = Enums.GenerateLayerQueryOptionUseFilter;
                break;
            }
        }
    }

    function setClipWaterPipesAOI(clip) {

        if (!overridesReady())
            return;

        const targetLayer = getFeatureLayerByName("Main");
        if (!targetLayer)
            return;

        // Obtain a key for the given basemap-layer.
        const keyForTargetLayer = ArcGISRuntimeEnvironment.createObject("OfflineMapParametersKey", {initLayer: targetLayer});

        if (keyForTargetLayer.empty || keyForTargetLayer.type !== Enums.OfflineMapParametersTypeGenerateGeodatabase)
            return;

        // Get the dictionary of GenerateGeoDatabaseParameters.
        const dictionary = overrides.generateGeodatabaseParameters;

        if (!dictionary.contains(keyForTargetLayer))
            return;

        // Grab the GenerateGeoDatabaseParameters associated with the given key.
        const generateGdbParam = dictionary.value(keyForTargetLayer);

        const table = targetLayer.featureTable;

        // Get the service layer id for the given layer.
        const targetLayerId = table.layerInfo.serviceLayerIdAsInt;

        // Set the use geometry flag on the layer.
        const layerOptions = generateGdbParam.layerOptions;
        for (let i = 0; i < layerOptions.length; i++) {
            const layerOption = layerOptions[i];
            if (layerOption.layerIdAsInt === targetLayerId) {
                layerOption.useGeometry = clip;
                break;
            }
        }
    }

    function takeMapOffline() {
        // create the job
        generateJob = offlineMapTask.generateOfflineMapWithOverrides(parameters, outputMapPackage, overrides);

        // check if job is valid
        if (generateJob) {
            // show the export window
            generateWindow.visible = true;

            // connect to the job's status changed signal to know once it is done
            generateJob.jobStatusChanged.connect(updateJobStatus);
            // connect to the job's progress changed signal
            generateJob.progressChanged.connect(updateProgress);

            generateJob.start();
        } else {
            generateWindow.visible = true;
            generateWindow.statusText = "Task failed";
            generateWindow.hideWindow(5000);
        }
    }

    function updateJobStatus() {
        switch(generateJob.jobStatus) {
        case Enums.JobStatusFailed:
            generateWindow.statusText = "Task failed";
            generateWindow.hideWindow(5000);
            break;
        case Enums.JobStatusNotStarted:
            generateWindow.statusText = "Job not started";
            break;
        case Enums.JobStatusPaused:
            generateWindow.statusText = "Job paused";
            break;
        case Enums.JobStatusStarted:
            generateWindow.statusText = "In progress";
            break;
        case Enums.JobStatusSucceeded:
            // show any layer errors
            if (generateJob.result.hasErrors) {
                const layerErrors = generateJob.result.layerErrors;
                let errorText = "";
                for (let i = 0; i < layerErrors.length; i++) {
                    const errorPair = layerErrors[i];
                    errorText += errorPair.layer.name + ": " + errorPair.error.message + "\n";
                }
                msgDialog.detailedText = errorText;
                msgDialog.open();
            }

            // show the map
            generateWindow.statusText = "Complete";
            generateWindow.hideWindow(1500);
            displayOfflineMap(generateJob.result);
            break;
        default:
            console.log("default");
            break;
        }
    }

    function updateProgress() {
        generateWindow.progressText = generateJob.progress;
    }

    function displayOfflineMap(result) {
        // Set the offline map to the MapView
        mapView.map = result.offlineMap;
        downloadButton.visible = false;
        extentRectangle.visible = false;
    }

    OverridesWindow {
        id: overridesWindow
        anchors.fill: parent
        visible: offlineMapTask.createGenerateOfflineMapParameterOverridesStatus === Enums.TaskStatusCompleted

        onBasemapLODSelected: setBasemapLOD(min, max);
        onBasemapBufferChanged: setBasemapBuffer(buffer);
        onRemoveSystemValvesChanged: removeSystemValves();
        onRemoveServiceConnectionChanged: removeServiceConnection();
        onHydrantWhereClauseChanged: setHydrantWhereClause(whereClause);
        onClipWaterPipesAOIChanged: setClipWaterPipesAOI(clip);

        onOverridesAccepted:  takeMapOffline();
    }

    GenerateWindow {
        id: generateWindow
        anchors.fill: parent
    }

    Dialog {
        id: msgDialog
        modal: true
        x: Math.round(parent.width - width) / 2
        y: Math.round(parent.height - height) / 2
        standardButtons: Dialog.Ok
        title: "Layer Errors"
        property alias text : textLabel.text
        property alias detailedText : detailsLabel.text
        ColumnLayout {
            Text {
                id: textLabel
                text: "Some layers could not be taken offline."
            }
            Text {
                id: detailsLabel
            }
        }
    }

    BusyIndicator {
        anchors.centerIn: parent
        running: offlineMapTask.createGenerateOfflineMapParameterOverridesStatus === Enums.TaskStatusInProgress ||
                 offlineMapTask.createDefaultGenerateOfflineMapParameterStatus === Enums.TaskStatusInProgress
    }

    /* Uncomment this section when running as standalone application
    AuthenticationView {
        anchors.fill: parent
        authenticationManager: AuthenticationManager
    }
    */
}
// Copyright 2018 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.

import QtQuick 2.6

// Create the download button to export the tile cache
Rectangle {
    property bool pressed: false
    signal buttonClicked()

    width: 265
    height: 35
    color: pressed ? "#959595" : "#D6D6D6"
    radius: 5
    border {
        color: "#585858"
        width: 1
    }

    Row {
        anchors.fill: parent
        spacing: 5
        Image {
            width: 38
            height: width
            source: "qrc:/Samples/Maps/GenerateOfflineMap_Overrides/download.png"
        }
        Text {
            anchors.verticalCenter: parent.verticalCenter
            text: "Generate Offline Map (Overrides)"
            font.pixelSize: 14
            color: "#474747"
        }
    }

    MouseArea {
        anchors.fill: parent
        onPressed: downloadButton.pressed = true
        onReleased: downloadButton.pressed = false
        onClicked: {
            buttonClicked();
        }
    }
}
// Copyright 2018 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.

import QtQuick 2.6
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0

Rectangle {
    id: exportWindow
    color: "transparent"
    visible: false
    clip: true

    property string statusText: ""
    property string progressText: ""

    RadialGradient {
        anchors.fill: parent
        opacity: 0.7
        gradient: Gradient {
            GradientStop { position: 0.0; color: "lightgrey" }
            GradientStop { position: 0.7; color: "black" }
        }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: mouse.accepted = true
        onWheel: wheel.accepted = true
    }

    Rectangle {
        anchors.centerIn: parent
        width: 135
        height: 100
        color: "lightgrey"
        opacity: 0.8
        radius: 5
        border {
            color: "#4D4D4D"
            width: 1
        }

        Column {
            anchors {
                fill: parent
                margins: 10
            }
            spacing: 10

            BusyIndicator {
                anchors.horizontalCenter: parent.horizontalCenter
            }

            Text {
                anchors.horizontalCenter: parent.horizontalCenter
                text: "%1: %2%".arg(statusText).arg(progressText)
                font.pixelSize: 16
            }
        }
    }

    Timer {
        id: hideWindowTimer

        onTriggered: parent.visible = false;
    }

    function hideWindow(time) {
        hideWindowTimer.interval = time;
        hideWindowTimer.restart();
    }
}
// Copyright 2018 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.

import QtQuick 2.6
import QtQuick.Controls 2.2

Rectangle {
    id: overridesPanel
    visible: overridesReady
    signal basemapLODSelected(real min, real max)
    signal basemapBufferChanged(real buffer)
    signal removeSystemValvesChanged()
    signal removeServiceConnectionChanged()
    signal hydrantWhereClauseChanged(string whereClause)
    signal clipWaterPipesAOIChanged(bool clip)
    signal overridesAccepted()

    color: "#D6D6D6"

    Text {
        id: title
        anchors {
            top: parent.top
            horizontalCenter: parent.horizontalCenter
            margins: 16
        }
        text: "Layer specific overrides"
        font {
            bold: true
            underline: true
            pixelSize: 18
        }
        color: "#474747"
    }

    ScrollView {
        id: scrollView
        anchors {
            top: title.bottom
            left: overridesPanel.left
            right: overridesPanel.right
            bottom: takeOfflineButton.top
            margins: 16
        }
        clip: true
        ScrollBar.vertical.interactive: true
        ScrollBar.vertical.policy: ScrollBar.AsNeeded

        Rectangle {
            implicitHeight: childrenRect.height
            implicitWidth: overridesPanel.width
            color: "transparent"

            Text {
                id: lodTitle
                text: "Basemap Levels of Detail:"
                anchors {
                    topMargin: 16
                    horizontalCenter: parent.horizontalCenter
                }
                font {
                    pixelSize: 14
                }
                color: "#474747"
            }

            Row {
                id: lodRange
                anchors {
                    top: lodTitle.bottom
                    topMargin: 8
                    horizontalCenter: parent.horizontalCenter
                }
                Text {
                    text: "(Least detail)"
                    font {
                        pixelSize: 12
                    }
                    color: "#474747"
                }

                RangeSlider {
                    id: lodsSlider
                    from: 0
                    to: 24
                    width: overridesPanel.width * 0.5
                    first.value: 0
                    second.value: 23
                    first.onPressedChanged: {
                        if (first.pressed)
                            return;
                        basemapLODSelected(first.value, second.value);
                    }

                    second.onPressedChanged: {
                        if (second.pressed)
                            return;
                        basemapLODSelected(first.value, second.value);
                    }
                }

                Text {
                    text: "(Most detail)"
                    font {
                        pixelSize: 12
                    }
                    color: "#474747"
                }
            }

            Text {
                id: basemapBufferLabel
                text: "Basemap Buffer (m):"
                anchors {
                    top: lodRange.bottom
                    topMargin: 32
                    horizontalCenter: parent.horizontalCenter
                }
                font {
                    pixelSize: 14
                }
                color: "#474747"
            }

            SpinBox {
                id: basemapBufferSB
                anchors {
                    top: basemapBufferLabel.bottom
                    topMargin: 8
                    horizontalCenter: parent.horizontalCenter
                }
                from: 0
                to: 500
                stepSize: 50

                font.pixelSize: 12
                onValueChanged: basemapBufferChanged(value);
            }

            Button {
                id: systemVavlesCB
                text: "Remove System Valves"
                anchors {
                    top: basemapBufferSB.bottom
                    topMargin: 32
                    horizontalCenter: parent.horizontalCenter
                }
                font {
                    pixelSize: 14
                }


                onClicked: {
                    removeSystemValvesChanged();
                    enabled = false;
                }
            }

            Button {
                id: serviceConnCB
                text: "Remove Service Connection"
                anchors {
                    top: systemVavlesCB.bottom
                    topMargin: 32
                    horizontalCenter: parent.horizontalCenter
                }
                font {
                    pixelSize: 14
                }

                onClicked: {
                    removeServiceConnectionChanged();
                    enabled = false;
                }
            }

            Text {
                id: filterLabel
                text: "Filter Hydrants:"
                anchors {
                    top: serviceConnCB.bottom
                    topMargin: 32
                    horizontalCenter: parent.horizontalCenter
                }
                font {
                    pixelSize: 14
                }
                color: "#474747"
            }

            Rectangle {
                anchors.fill: filterComboBox
                color: "white"
            }

            ComboBox {
                id: filterComboBox
                anchors {
                    top: filterLabel.bottom
                    topMargin: 8
                    horizontalCenter: parent.horizontalCenter
                }
                property int modelWidth: 0
                width: modelWidth + leftPadding + rightPadding + indicator.width
                model: [ "No filter", "FLOW < 500", "FLOW < 300", "FLOW < 100" ]

                onCurrentTextChanged: {
                    // 1=1 equivelent to select all in a WHERE clause.
                    hydrantWhereClauseChanged(currentText === "No filter" ? "1=1"
                                                                          : currentText)
                }

                Component.onCompleted : {
                    for (let i = 0; i < model.length; ++i) {
                        metrics.text = model[i];
                        modelWidth = Math.max(modelWidth, metrics.width);
                    }
                }
                TextMetrics {
                    id: metrics
                    font: filterComboBox.font
                }
            }

            CheckBox {
                id: clipCB
                text: "Clip Water Pipes to AOI"
                anchors {
                    top: filterComboBox.bottom
                    topMargin: 32
                    horizontalCenter: parent.horizontalCenter
                }
                font {
                    pixelSize: 14
                }

                checked: true

                onCheckedChanged: clipWaterPipesAOIChanged(checked)
            }
        }
    }

    Button {
        id: takeOfflineButton
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.bottom: parent.bottom
        anchors.margins: 16
        text: "Start Job"
        font {
            bold: true
            pixelSize: 18
        }

        onClicked: {
            overridesAccepted();
            parent.visible = false;
        }
    }
}