Java tutorial
/* 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. * */ package com.esri.arcgisruntime.generateofflinemapoverrides; import java.io.File; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; import android.Manifest; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import com.esri.arcgisruntime.concurrent.Job; import com.esri.arcgisruntime.concurrent.ListenableFuture; import com.esri.arcgisruntime.data.ServiceFeatureTable; import com.esri.arcgisruntime.geometry.Envelope; import com.esri.arcgisruntime.geometry.GeometryEngine; import com.esri.arcgisruntime.geometry.Point; import com.esri.arcgisruntime.layers.FeatureLayer; import com.esri.arcgisruntime.layers.Layer; import com.esri.arcgisruntime.loadable.LoadStatus; import com.esri.arcgisruntime.mapping.ArcGISMap; import com.esri.arcgisruntime.mapping.LayerList; import com.esri.arcgisruntime.mapping.view.Graphic; import com.esri.arcgisruntime.mapping.view.GraphicsOverlay; import com.esri.arcgisruntime.mapping.view.MapView; import com.esri.arcgisruntime.portal.Portal; import com.esri.arcgisruntime.portal.PortalItem; import com.esri.arcgisruntime.security.AuthenticationManager; import com.esri.arcgisruntime.security.DefaultAuthenticationChallengeHandler; import com.esri.arcgisruntime.symbology.SimpleLineSymbol; import com.esri.arcgisruntime.tasks.geodatabase.GenerateGeodatabaseParameters; import com.esri.arcgisruntime.tasks.geodatabase.GenerateLayerOption; import com.esri.arcgisruntime.tasks.offlinemap.GenerateOfflineMapJob; import com.esri.arcgisruntime.tasks.offlinemap.GenerateOfflineMapParameterOverrides; import com.esri.arcgisruntime.tasks.offlinemap.GenerateOfflineMapParameters; import com.esri.arcgisruntime.tasks.offlinemap.GenerateOfflineMapResult; import com.esri.arcgisruntime.tasks.offlinemap.OfflineMapParametersKey; import com.esri.arcgisruntime.tasks.offlinemap.OfflineMapTask; import com.esri.arcgisruntime.tasks.tilecache.ExportTileCacheParameters; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private Button mGenerateOfflineMapOverridesButton; private MapView mMapView; private GraphicsOverlay mGraphicsOverlay; private Graphic mDownloadArea; private GenerateOfflineMapParameterOverrides mParameterOverrides; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // access MapView from layout mMapView = findViewById(R.id.mapView); // access button to take the map offline and disable it until map is loaded mGenerateOfflineMapOverridesButton = findViewById(R.id.generateOfflineMapOverridesButton); mGenerateOfflineMapOverridesButton.setEnabled(false); // handle authentication with the portal AuthenticationManager.setAuthenticationChallengeHandler(new DefaultAuthenticationChallengeHandler(this)); // create a portal item with the itemId of the web map Portal portal = new Portal(getString(R.string.portal_url), false); PortalItem portalItem = new PortalItem(portal, getString(R.string.item_id)); // create a map with the portal item ArcGISMap map = new ArcGISMap(portalItem); // request write permission String[] reqPermission = { Manifest.permission.WRITE_EXTERNAL_STORAGE }; int requestCode = 2; // for API level 23+ request permission at runtime if (ContextCompat.checkSelfPermission(this, reqPermission[0]) == PackageManager.PERMISSION_GRANTED) { map.addDoneLoadingListener(() -> { if (map.getLoadStatus() == LoadStatus.LOADED) { // enable offline map button only after permission is granted and map is loaded mGenerateOfflineMapOverridesButton.setEnabled(true); } }); } else { // request permission ActivityCompat.requestPermissions(this, reqPermission, requestCode); } // set the map to the map view mMapView.setMap(map); // create a graphics overlay for the map view mGraphicsOverlay = new GraphicsOverlay(); mMapView.getGraphicsOverlays().add(mGraphicsOverlay); // define the download area graphic mDownloadArea = new Graphic(); mGraphicsOverlay.getGraphics().add(mDownloadArea); SimpleLineSymbol simpleLineSymbol = new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, Color.RED, 2); mDownloadArea.setSymbol(simpleLineSymbol); // update the download area box whenever the viewpoint changes mMapView.addViewpointChangedListener(viewpointChangedEvent -> { if (map.getLoadStatus() == LoadStatus.LOADED) { mDownloadArea.setGeometry(createDownloadAreaGeometry()); } }); // when the button is clicked, start the offline map task job mGenerateOfflineMapOverridesButton.setOnClickListener(v -> showParametersDialog()); } /** * Create an envelope representing the download area, used to define an area of interest * * @return download area Envelope */ private Envelope createDownloadAreaGeometry() { // upper left corner of the area to take offline android.graphics.Point minScreenPoint = new android.graphics.Point(200, 200); // lower right corner of the downloaded area android.graphics.Point maxScreenPoint = new android.graphics.Point(mMapView.getWidth() - 200, mMapView.getHeight() - 200); // convert screen points to map points Point minPoint = mMapView.screenToLocation(minScreenPoint); Point maxPoint = mMapView.screenToLocation(maxScreenPoint); // use the points to define and return an envelope if (minPoint != null && maxPoint != null) { return new Envelope(minPoint, maxPoint); } return null; } /** * Creates parameters dialog and handles processing of input to generateOfflineMap(...) when Start Job button is clicked. */ private void showParametersDialog() { View overrideParametersView = getLayoutInflater().inflate(R.layout.override_parameters_dialog, null); // min and max seek bars TextView currMinScaleTextView = overrideParametersView.findViewById(R.id.currMinScaleTextView); TextView currMaxScaleTextView = overrideParametersView.findViewById(R.id.currMaxScaleTextview); SeekBar minScaleSeekBar = buildSeekBar(overrideParametersView.findViewById(R.id.minScaleSeekBar), currMinScaleTextView, 22, 15); SeekBar maxScaleSeekBar = buildSeekBar(overrideParametersView.findViewById(R.id.maxScaleSeekBar), currMaxScaleTextView, 23, 20); minScaleSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { currMinScaleTextView.setText(String.valueOf(progress)); if (progress >= maxScaleSeekBar.getProgress()) { // set max to 1 more than min value (since max must always be greater than min) currMaxScaleTextView.setText(String.valueOf(progress + 1)); maxScaleSeekBar.setProgress(progress + 1); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); maxScaleSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { currMaxScaleTextView.setText(String.valueOf(progress)); if (progress <= minScaleSeekBar.getProgress()) { // set min to 1 less than max value (since min must always be less than max) currMinScaleTextView.setText(String.valueOf(progress - 1)); minScaleSeekBar.setProgress(progress - 1); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); // extent buffer seek bar SeekBar extentBufferDistanceSeekBar = buildSeekBar( overrideParametersView.findViewById(R.id.extentBufferDistanceSeekBar), overrideParametersView.findViewById(R.id.currExtentBufferDistanceTextView), 500, 300); // include layers checkboxes CheckBox systemValves = overrideParametersView.findViewById(R.id.systemValvesCheckBox); CheckBox serviceConnections = overrideParametersView.findViewById(R.id.serviceConnectionsCheckBox); // min hydrant flow rate seek bar SeekBar minHydrantFlowRateSeekBar = buildSeekBar( overrideParametersView.findViewById(R.id.minHydrantFlowRateSeekBar), overrideParametersView.findViewById(R.id.currMinHydrantFlowRateTextView), 2000, 500); // crop layer to extent checkbox CheckBox waterPipes = overrideParametersView.findViewById(R.id.waterPipesCheckBox); // setup dialog AlertDialog.Builder overrideParametersDialogBuilder = new AlertDialog.Builder(this); AlertDialog overrideParametersDialog = overrideParametersDialogBuilder.create(); overrideParametersDialogBuilder.setView(overrideParametersView).setTitle("Override Parameters") .setCancelable(true) .setNegativeButton("Cancel", (dialog, which) -> overrideParametersDialog.dismiss()) .setPositiveButton("Start Job", (dialog, which) -> { // re-create download area geometry in case user hasn't changed the Viewpoint mDownloadArea.setGeometry(createDownloadAreaGeometry()); defineParameters(minScaleSeekBar.getProgress(), maxScaleSeekBar.getProgress(), extentBufferDistanceSeekBar.getProgress(), systemValves.isChecked(), serviceConnections.isChecked(), minHydrantFlowRateSeekBar.getProgress(), waterPipes.isChecked()); }).show(); } /** * Use parameters from the override parameters dialog to define parameter overrides. * * @param minScale levelId * @param maxScale levelId * @param bufferDistance around the given area of interest * @param includeSystemValves whether to include System Valves layer * @param includeServiceConnections whether to include the Service Connections layer * @param flowRate to limit hydrants in a where clause * @param cropWaterPipes whether to crop the pipes layer */ private void defineParameters(int minScale, int maxScale, int bufferDistance, boolean includeSystemValves, boolean includeServiceConnections, int flowRate, boolean cropWaterPipes) { // create an offline map offlineMapTask with the map OfflineMapTask offlineMapTask = new OfflineMapTask(mMapView.getMap()); // create default generate offline map parameters from the offline map task ListenableFuture<GenerateOfflineMapParameters> generateOfflineMapParametersFuture = offlineMapTask .createDefaultGenerateOfflineMapParametersAsync(mDownloadArea.getGeometry()); generateOfflineMapParametersFuture.addDoneListener(() -> { try { final GenerateOfflineMapParameters generateOfflineMapParameters = generateOfflineMapParametersFuture .get(); // create parameter overrides for greater control ListenableFuture<GenerateOfflineMapParameterOverrides> parameterOverridesFuture = offlineMapTask .createGenerateOfflineMapParameterOverridesAsync(generateOfflineMapParameters); parameterOverridesFuture.addDoneListener(() -> { try { // get the parameter overrides mParameterOverrides = parameterOverridesFuture.get(); // set basemap scale and area of interest setBasemapScaleAndAreaOfInterest(minScale, maxScale, bufferDistance); // exclude system valve layer if (!includeSystemValves) { excludeLayerFromDownload("System Valve"); } // exclude service connection layer if (!includeServiceConnections) { excludeLayerFromDownload("Service Connection"); } // crop pipes layer if (cropWaterPipes) { for (GenerateLayerOption generateLayerOption : getGenerateGeodatabaseParametersLayerOptions( "Main")) { generateLayerOption.setUseGeometry(true); } } // set flow rate where clause on the hydrant layer for (GenerateLayerOption generateLayerOption : getGenerateGeodatabaseParametersLayerOptions( "Hydrant")) { if (generateLayerOption.getLayerId() == getServiceLayerId( Objects.requireNonNull(getFeatureLayerByName("Hydrant")))) { generateLayerOption.setWhereClause("FLOW >= " + flowRate); generateLayerOption.setQueryOption(GenerateLayerOption.QueryOption.USE_FILTER); } } // start a an offline map job from the task and parameters generateOfflineMap(offlineMapTask, generateOfflineMapParameters); } catch (InterruptedException | ExecutionException e) { String error = "Error creating parameter overrides: " + e.getCause().getMessage(); Toast.makeText(this, error, Toast.LENGTH_LONG).show(); Log.e(TAG, error); } }); } catch (InterruptedException | ExecutionException e) { String error = "Error generating default generate offline map parameters: " + e.getCause().getMessage(); Toast.makeText(this, error, Toast.LENGTH_LONG).show(); Log.e(TAG, error); } }); } /** * Use the generate offline map job to generate an offline map. */ private void generateOfflineMap(OfflineMapTask offlineMapTask, GenerateOfflineMapParameters generateOfflineMapParameters) { // delete any offline map already in the cache String tempDirectoryPath = getCacheDir() + File.separator + "offlineMap"; deleteDirectory(new File(tempDirectoryPath)); // create an offline map job with the download directory path and parameters and start the job GenerateOfflineMapJob job = offlineMapTask.generateOfflineMap(generateOfflineMapParameters, tempDirectoryPath, mParameterOverrides); // show the job's progress in a progress dialog showProgressDialog(job); // replace the current map with the result offline map when the job finishes job.addJobDoneListener(() -> { if (job.getStatus() == Job.Status.SUCCEEDED) { GenerateOfflineMapResult result = job.getResult(); mMapView.setMap(result.getOfflineMap()); mGraphicsOverlay.getGraphics().clear(); mGenerateOfflineMapOverridesButton.setEnabled(false); Toast.makeText(this, "Now displaying offline map.", Toast.LENGTH_LONG).show(); } else { String error = "Error in generate offline map job: " + job.getError().getAdditionalMessage(); Toast.makeText(this, error, Toast.LENGTH_LONG).show(); Log.e(TAG, error); } }); // start the job job.start(); } /** * Set basemap scale and area of interest using the given values * * @param minScale levelId * @param maxScale levelId * @param bufferDistance around the given area of interest */ private void setBasemapScaleAndAreaOfInterest(int minScale, int maxScale, int bufferDistance) { // get the export tile cache parameters ExportTileCacheParameters exportTileCacheParameters = getExportTileCacheParameters( mMapView.getMap().getBasemap().getBaseLayers().get(0)); // create a new sublist of LODs in the range requested by the user exportTileCacheParameters.getLevelIDs().clear(); for (int i = minScale; i < maxScale; i++) { exportTileCacheParameters.getLevelIDs().add(i); } // set the area of interest to the original download area plus a buffer exportTileCacheParameters .setAreaOfInterest(GeometryEngine.buffer(mDownloadArea.getGeometry(), bufferDistance)); } /** * Remove the layer named from the generate layer options list in the generate geodatabase parameters. * * @param layerName as a string */ private void excludeLayerFromDownload(String layerName) { // get the named feature layer FeatureLayer targetLayer = getFeatureLayerByName(layerName); // get the layer's id long targetLayerId = getServiceLayerId(targetLayer); // get the layer's layer options List<GenerateLayerOption> layerOptions = getGenerateGeodatabaseParametersLayerOptions(layerName); // remove the target layer for (GenerateLayerOption layerOption : layerOptions) { if (layerOption.getLayerId() == targetLayerId) { layerOptions.remove(layerOption); break; } } } /** * Helper method to get export tile cache parameters for the given layer. * * @param layer to get parameters for * @return ExportTileCacheParameters for the given layer */ private ExportTileCacheParameters getExportTileCacheParameters(Layer layer) { OfflineMapParametersKey key = new OfflineMapParametersKey(layer); return mParameterOverrides.getExportTileCacheParameters().get(key); } /** * Helper method to get generate geodatabase parameters for the given layer. * * @param layer to get parameters for * @return GenerateGeodatabaseParameters for the given layer */ private GenerateGeodatabaseParameters getGenerateGeodatabaseParameters(Layer layer) { OfflineMapParametersKey key = new OfflineMapParametersKey(layer); return mParameterOverrides.getGenerateGeodatabaseParameters().get(key); } /** * Helper method to get the generate geodatabase parameters layer options for the given layer. * * @param layerName to get layer options for * @return list of GenerateLayerOptions */ private List<GenerateLayerOption> getGenerateGeodatabaseParametersLayerOptions(String layerName) { // get the named feature layer FeatureLayer targetFeatureLayer = getFeatureLayerByName(layerName); // get the generate geodatabase parameters for the layer GenerateGeodatabaseParameters generateGeodatabaseParameters = getGenerateGeodatabaseParameters( targetFeatureLayer); // return the layer options return generateGeodatabaseParameters.getLayerOptions(); } /** * Helper method to get the service layer id for the given feature layer * * @param featureLayer to get service id for * @return service layer id as a long */ private long getServiceLayerId(FeatureLayer featureLayer) { ServiceFeatureTable serviceFeatureTable = (ServiceFeatureTable) featureLayer.getFeatureTable(); return serviceFeatureTable.getLayerInfo().getServiceLayerId(); } /** * Helper method to get the named feature layer from the map's operational layers. * * @param layerName as a String * @return the named feature layer, or null, if not found or if named layer is not a feature layer */ private FeatureLayer getFeatureLayerByName(String layerName) { LayerList operationalLayers = mMapView.getMap().getOperationalLayers(); for (Layer layer : operationalLayers) { if (layer instanceof FeatureLayer && layer.getName().equals(layerName)) { return (FeatureLayer) layer; } } return null; } /** * Shows a progress dialog for the given job. * * @param job to track progress from */ private void showProgressDialog(Job job) { // create a progress dialog to show download progress ProgressDialog progressDialog = new ProgressDialog(this); progressDialog.setTitle("Generate Offline Map Job"); progressDialog.setMessage("Taking map offline..."); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setIndeterminate(false); progressDialog.setProgress(0); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", (dialog, which) -> job.cancel()); progressDialog.show(); // show the job's progress with the progress dialog job.addProgressChangedListener(() -> progressDialog.setProgress(job.getProgress())); // dismiss dialog when job is done job.addJobDoneListener(progressDialog::dismiss); } /** * Handle the permissions request response. */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mMapView.getMap().addDoneLoadingListener(() -> { if (mMapView.getMap().getLoadStatus() == LoadStatus.LOADED) { // enable offline map button only after permission is granted and map is loaded mGenerateOfflineMapOverridesButton.setEnabled(true); } }); Log.d(TAG, "permission granted"); } else { // report to user that permission was denied Toast.makeText(this, getString(R.string.offline_map_write_permission_denied), Toast.LENGTH_SHORT) .show(); } } @Override protected void onPause() { mMapView.pause(); super.onPause(); } @Override protected void onResume() { super.onResume(); mMapView.resume(); } @Override protected void onDestroy() { mMapView.dispose(); super.onDestroy(); } /** * Builds a seek bar and handles updating of the associated current seek bar text view. * * @param seekBar view to build * @param currSeekBarTextView to be updated when the seek bar progress changes * @param max max value for the seek bar * @param progress initial progress position of the seek bar * @return the built seek bar */ private static SeekBar buildSeekBar(SeekBar seekBar, TextView currSeekBarTextView, int max, int progress) { seekBar.setMax(max); seekBar.setProgress(progress); currSeekBarTextView.setText(String.valueOf(seekBar.getProgress())); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { currSeekBarTextView.setText(String.valueOf(progress)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); return seekBar; } /** * Recursively deletes all files in the given directory. * * @param file to delete */ private static void deleteDirectory(File file) { if (file.isDirectory()) for (File subFile : file.listFiles()) { deleteDirectory(subFile); } if (!file.delete()) { Log.e(TAG, "Failed to delete file: " + file.getPath()); } } }