diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8815ffd6..aeeccefc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: with: java-version: 11 - name: mobsfscan - uses: MobSF/mobsfscan@0.0.8 + uses: MobSF/mobsfscan@0.1.2 with: args: '. --sarif --output results.sarif || true' - name: Upload mobsfscan report diff --git a/MapCacheAndroid.sketch b/MapCacheAndroid.sketch index 07c7e3c3..563717a5 100644 Binary files a/MapCacheAndroid.sketch and b/MapCacheAndroid.sketch differ diff --git a/docs/exampleTileUrls.json b/docs/exampleTileUrls.json new file mode 100644 index 00000000..61475327 --- /dev/null +++ b/docs/exampleTileUrls.json @@ -0,0 +1,3 @@ +{ + "osm.mil":"https://osm.gs.mil/tiles/default/{z}/{x}/{y}.png" +} diff --git a/docs/hostedGeopackages.json b/docs/hostedGeopackages.json new file mode 100644 index 00000000..1e0745b5 --- /dev/null +++ b/docs/hostedGeopackages.json @@ -0,0 +1,4 @@ +{ +"Hurricane Ian":"https://github.com/ngageoint/geopackage-mapcache-android/raw/master/docs/ianFlooding.gpkg", +"Florida Imagery":"https://github.com/ngageoint/geopackage-mapcache-android/raw/master/docs/ianFlooding.gpkg" +} diff --git a/gradle.properties b/gradle.properties index 15b2d281..6ad3c903 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ android.enableJetifier=true android.useAndroidX=true -android.databinding.incremental=true \ No newline at end of file +android.databinding.incremental=true diff --git a/mapcache/build.gradle b/mapcache/build.gradle index c6d7bc74..9be5d7fd 100644 --- a/mapcache/build.gradle +++ b/mapcache/build.gradle @@ -13,10 +13,10 @@ android { defaultConfig { applicationId "mil.nga.mapcache" resValue "string", "applicationId", applicationId - minSdkVersion 21 + minSdkVersion 28 targetSdkVersion 31 - versionCode 42 - versionName '2.1.4' + versionCode 44 + versionName '2.1.5' multiDexEnabled true } buildTypes { @@ -53,11 +53,12 @@ dependencies { api 'com.google.android.material:material:1.0.0' api 'androidx.preference:preference:1.1.1' api 'androidx.lifecycle:lifecycle-extensions:2.2.0' - api 'mil.nga.geopackage.map:geopackage-android-map:6.4.0' // comment out to build locally + api 'mil.nga.geopackage.map:geopackage-android-map:6.7.0' // comment out to build locally //api project(':geopackage-map') // uncomment me to build locally - api 'mil.nga.mgrs:mgrs-android:2.1.0' - api 'mil.nga.gars:gars-android:1.1.0' + api 'mil.nga.mgrs:mgrs-android:2.2.0' + api 'mil.nga.gars:gars-android:1.2.0' api 'androidx.multidex:multidex:2.0.1' + implementation 'com.google.code.gson:gson:2.8.7' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'com.google.android.gms:play-services-location:19.0.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' diff --git a/mapcache/src/main/java/mil/nga/mapcache/FeatureRowProcessor.java b/mapcache/src/main/java/mil/nga/mapcache/FeatureRowProcessor.java new file mode 100644 index 00000000..d589d81b --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/FeatureRowProcessor.java @@ -0,0 +1,244 @@ +package mil.nga.mapcache; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import mil.nga.geopackage.BoundingBox; +import mil.nga.geopackage.features.user.FeatureDao; +import mil.nga.geopackage.features.user.FeatureRow; +import mil.nga.geopackage.geom.GeoPackageGeometryData; +import mil.nga.geopackage.map.features.StyleCache; +import mil.nga.geopackage.map.geom.GoogleMapShape; +import mil.nga.geopackage.map.geom.GoogleMapShapeConverter; +import mil.nga.geopackage.tiles.TileBoundingBoxUtils; +import mil.nga.sf.Geometry; +import mil.nga.sf.GeometryEnvelope; +import mil.nga.sf.GeometryType; +import mil.nga.sf.util.GeometryEnvelopeBuilder; + +/** + * Single feature row processor + * + * @author osbornb + */ +public class FeatureRowProcessor implements Runnable { + + /** + * Map update task + */ + private final MapFeaturesUpdateTask task; + + /** + * Database + */ + private final String database; + + /** + * Feature DAO + */ + private final FeatureDao featureDao; + + /** + * Feature row + */ + private final FeatureRow row; + + /** + * Total feature count + */ + private final AtomicInteger count; + + /** + * Total max features + */ + private final int maxFeatures; + + /** + * Editable shape flag + */ + private final boolean editable; + + /** + * Shape converter + */ + private final GoogleMapShapeConverter converter; + + /** + * Style Cache + */ + private final StyleCache styleCache; + + /** + * Filter bounding box + */ + private final BoundingBox filterBoundingBox; + + /** + * Max projection longitude + */ + private final double maxLongitude; + + /** + * Filter flag + */ + private final boolean filter; + + /** + * Contains various states for the map. + */ + private final MapModel model; + + /** + * Lock for concurrently updating the features bounding box + */ + private final Lock featuresBoundingBoxLock = new ReentrantLock(); + + /** + * The application context. + */ + private final Context context; + + /** + * Constructor + * + * @param task The update task. + * @param database The name of the geopackage the features belong too. + * @param featureDao The feature data access object. + * @param row The row to process. + * @param count The current total count of features. + * @param maxFeatures The maximum features to display on the map. + * @param editable True if the feature should look editable on the map. + * @param converter Converts the feature's shape to one to use on the map. + * @param styleCache The style cache. + * @param filterBoundingBox The bounding box to use for filtering. + * @param maxLongitude The maximum longitude. + * @param filter True if we should filter using the passed in bounding box. + * @param model Contains various states for the map. + * @param context The application context. + */ + public FeatureRowProcessor(MapFeaturesUpdateTask task, String database, FeatureDao featureDao, + FeatureRow row, AtomicInteger count, int maxFeatures, + boolean editable, GoogleMapShapeConverter converter, StyleCache styleCache, + BoundingBox filterBoundingBox, double maxLongitude, boolean filter, + MapModel model, Context context) { + this.task = task; + this.database = database; + this.featureDao = featureDao; + this.row = row; + this.count = count; + this.maxFeatures = maxFeatures; + this.editable = editable; + this.converter = converter; + this.styleCache = styleCache; + this.filterBoundingBox = filterBoundingBox; + this.maxLongitude = maxLongitude; + this.filter = filter; + this.model = model; + this.context = context; + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + processFeatureRow(task, database, featureDao, converter, styleCache, row, count, maxFeatures, + editable, filterBoundingBox, maxLongitude, filter); + } + + /** + * Process the feature row + * + * @param task The map update task. + * @param database The geopackage name the feature row belongs too. + * @param featureDao The feature data access object. + * @param converter Converts the feature shape to one that can be used on a google map. + * @param styleCache The style cache. + * @param row The row to process. + * @param count The current feature count displayed on map. + * @param maxFeatures The maximum features to display on the map. + * @param editable True if the feature should look editable on the map. + * @param boundingBox The bounding box to use to filter features. + * @param maxLongitude The maximum longitude. + * @param filter True if we should filer using the bounding box. + */ + private void processFeatureRow(MapFeaturesUpdateTask task, String database, FeatureDao featureDao, + GoogleMapShapeConverter converter, StyleCache styleCache, FeatureRow row, AtomicInteger count, + int maxFeatures, boolean editable, BoundingBox boundingBox, double maxLongitude, + boolean filter) { + + boolean exists; + synchronized (model.getFeatureShapes()) { + exists = model.getFeatureShapes().exists(row.getId(), database, featureDao.getTableName()); + } + + if (!exists && task.NotCancelled()) { + + try { + GeoPackageGeometryData geometryData = row.getGeometry(); + if (geometryData != null && !geometryData.isEmpty()) { + + final Geometry geometry = geometryData.getGeometry(); + + if (geometry != null) { + + boolean passesFilter = true; + + if (filter && boundingBox != null) { + GeometryEnvelope envelope = geometryData.getEnvelope(); + if (envelope == null) { + envelope = GeometryEnvelopeBuilder.buildEnvelope(geometry); + } + if (envelope != null) { + if (geometry.getGeometryType() == GeometryType.POINT) { + mil.nga.sf.Point point = (mil.nga.sf.Point) geometry; + passesFilter = TileBoundingBoxUtils.isPointInBoundingBox(point, boundingBox, maxLongitude); + } else { + BoundingBox geometryBoundingBox = new BoundingBox(envelope); + passesFilter = TileBoundingBoxUtils.overlap(boundingBox, geometryBoundingBox, maxLongitude) != null; + } + } + } + + if (passesFilter && count.getAndIncrement() < maxFeatures) { + final long featureId = row.getId(); + final GoogleMapShape shape = converter.toShape(geometry); + updateFeaturesBoundingBox(shape); + ShapeHelper.getInstance().prepareShapeOptions(shape, styleCache, row, editable, true, context); + task.addToMap(featureId, database, featureDao.getTableName(), shape); + } + } + } + } catch (Exception e) { + new Handler(Looper.getMainLooper()).post(() -> { + Toast toast = Toast.makeText(context, "Error loading geometry", Toast.LENGTH_SHORT); + toast.show(); + }); + } + } + } + + /** + * Update the features bounding box with the shape + * + * @param shape The shape to use to expand the features bounding box. + */ + private void updateFeaturesBoundingBox(GoogleMapShape shape) { + try { + featuresBoundingBoxLock.lock(); + if (model.getFeaturesBoundingBox() != null) { + shape.expandBoundingBox(model.getFeaturesBoundingBox()); + } else { + model.setFeaturesBoundingBox(shape.boundingBox()); + } + } finally { + featuresBoundingBoxLock.unlock(); + } + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java b/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java old mode 100755 new mode 100644 index 780ba5d7..26c759d1 --- a/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java +++ b/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java @@ -8,16 +8,12 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; -import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Point; import android.hardware.SensorEvent; import android.location.Location; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.os.Looper; import android.os.Vibrator; import android.preference.PreferenceManager; @@ -81,7 +77,6 @@ import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.MapStyleOptions; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; @@ -89,8 +84,6 @@ import com.google.android.gms.maps.model.PolygonOptions; import com.google.android.gms.maps.model.Polyline; import com.google.android.gms.maps.model.PolylineOptions; -import com.google.android.gms.maps.model.TileOverlayOptions; -import com.google.android.gms.maps.model.TileProvider; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.textfield.TextInputEditText; @@ -103,15 +96,12 @@ import java.sql.SQLException; import java.text.DecimalFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -121,14 +111,9 @@ import mil.nga.geopackage.contents.Contents; import mil.nga.geopackage.contents.ContentsDao; import mil.nga.geopackage.db.GeoPackageDataType; -import mil.nga.geopackage.extension.nga.link.FeatureTileTableLinker; -import mil.nga.geopackage.extension.nga.scale.TileScaling; -import mil.nga.geopackage.extension.nga.scale.TileTableScaling; -import mil.nga.geopackage.extension.nga.style.FeatureStyle; import mil.nga.geopackage.extension.schema.SchemaExtension; import mil.nga.geopackage.extension.schema.columns.DataColumns; import mil.nga.geopackage.extension.schema.columns.DataColumnsDao; -import mil.nga.geopackage.features.columns.GeometryColumns; import mil.nga.geopackage.features.index.FeatureIndexListResults; import mil.nga.geopackage.features.index.FeatureIndexManager; import mil.nga.geopackage.features.index.FeatureIndexResults; @@ -142,40 +127,19 @@ import mil.nga.geopackage.map.MapUtils; import mil.nga.geopackage.map.features.FeatureInfoBuilder; import mil.nga.geopackage.map.features.StyleCache; -import mil.nga.geopackage.map.geom.FeatureShapes; import mil.nga.geopackage.map.geom.GoogleMapShape; import mil.nga.geopackage.map.geom.GoogleMapShapeConverter; import mil.nga.geopackage.map.geom.GoogleMapShapeMarkers; import mil.nga.geopackage.map.geom.GoogleMapShapeType; -import mil.nga.geopackage.map.geom.MultiLatLng; -import mil.nga.geopackage.map.geom.MultiMarker; -import mil.nga.geopackage.map.geom.MultiPolygon; -import mil.nga.geopackage.map.geom.MultiPolygonOptions; -import mil.nga.geopackage.map.geom.MultiPolyline; -import mil.nga.geopackage.map.geom.MultiPolylineOptions; import mil.nga.geopackage.map.geom.PolygonHoleMarkers; import mil.nga.geopackage.map.geom.ShapeMarkers; import mil.nga.geopackage.map.geom.ShapeWithChildrenMarkers; -import mil.nga.geopackage.map.tiles.TileBoundingBoxMapUtils; -import mil.nga.geopackage.map.tiles.overlay.BoundedOverlay; -import mil.nga.geopackage.map.tiles.overlay.FeatureOverlay; import mil.nga.geopackage.map.tiles.overlay.FeatureOverlayQuery; -import mil.nga.geopackage.map.tiles.overlay.GeoPackageOverlayFactory; -import mil.nga.geopackage.srs.SpatialReferenceSystem; import mil.nga.geopackage.tiles.TileBoundingBoxUtils; -import mil.nga.geopackage.tiles.features.DefaultFeatureTiles; -import mil.nga.geopackage.tiles.features.FeatureTiles; -import mil.nga.geopackage.tiles.features.custom.NumberFeaturesTile; -import mil.nga.geopackage.tiles.matrixset.TileMatrixSet; -import mil.nga.geopackage.tiles.matrixset.TileMatrixSetDao; -import mil.nga.geopackage.tiles.user.TileDao; import mil.nga.mapcache.data.GeoPackageDatabase; import mil.nga.mapcache.data.GeoPackageDatabases; -import mil.nga.mapcache.data.GeoPackageFeatureOverlayTable; -import mil.nga.mapcache.data.GeoPackageFeatureTable; import mil.nga.mapcache.data.GeoPackageTable; import mil.nga.mapcache.data.GeoPackageTableType; -import mil.nga.mapcache.data.GeoPackageTileTable; import mil.nga.mapcache.data.MarkerFeature; import mil.nga.mapcache.indexer.IIndexerTask; import mil.nga.mapcache.listeners.DetailActionListener; @@ -194,8 +158,9 @@ import mil.nga.mapcache.preferences.PreferencesActivity; import mil.nga.mapcache.repository.GeoPackageModifier; import mil.nga.mapcache.sensors.SensorHandler; +import mil.nga.mapcache.utils.ProjUtils; +import mil.nga.mapcache.utils.SampleDownloader; import mil.nga.mapcache.utils.SwipeController; -import mil.nga.mapcache.utils.ThreadUtils; import mil.nga.mapcache.utils.ViewAnimation; import mil.nga.mapcache.view.GeoPackageAdapter; import mil.nga.mapcache.view.detail.DetailActionUtil; @@ -256,12 +221,6 @@ public class GeoPackageMapFragment extends Fragment implements */ private static final String MAX_FEATURES_MESSAGE_KEY = "max_features_warning"; - - /** - * Active GeoPackages - */ - private GeoPackageDatabases active; - /** * Google map */ @@ -275,17 +234,17 @@ public class GeoPackageMapFragment extends Fragment implements /** * View */ - private static View view; + private View view; /** * Edit features view */ - private static View editFeaturesView; + private View editFeaturesView; /** * Edit features polygon hole view */ - private static View editFeaturesPolygonHoleView; + private View editFeaturesPolygonHoleView; /** * True when the map is visible @@ -338,11 +297,6 @@ public class GeoPackageMapFragment extends Fragment implements */ private final Lock updateLock = new ReentrantLock(); - /** - * Mapping of open GeoPackage feature DAOs - */ - private final Map> featureDaos = new HashMap<>(); - /** * Vibrator */ @@ -358,11 +312,6 @@ public class GeoPackageMapFragment extends Fragment implements */ private boolean boundingBoxMode = false; - /** - * Edit features mode - */ - private boolean editFeaturesMode = false; - /** * Bounding box starting corner */ @@ -393,46 +342,11 @@ public class GeoPackageMapFragment extends Fragment implements */ private MenuItem editFeaturesMenuItem; - /** - * Edit features database - */ - private String editFeaturesDatabase; - - /** - * Edit features table - */ - private String editFeaturesTable; - - /** - * Feature shapes - */ - private final FeatureShapes featureShapes = new FeatureShapes(); - /** * Current zoom level */ private int currentZoom = -1; - /** - * Flag indicating if the initial zoom is still needed - */ - private boolean needsInitialZoom = true; - - /** - * Mapping between marker ids and the feature ids - */ - private final Map editFeatureIds = new HashMap<>(); - - /** - * Mapping between marker ids and the features - */ - private final Map markerIds = new HashMap<>(); - - /** - * Mapping between marker ids and feature objects - */ - private final Map editFeatureObjects = new HashMap<>(); - /** * Edit points type */ @@ -537,31 +451,6 @@ private enum EditType { */ private ImageButton editClearPolygonHolesButton; - /** - * Bounding box around the features on the map - */ - private BoundingBox featuresBoundingBox; - - /** - * Lock for concurrently updating the features bounding box - */ - private final Lock featuresBoundingBoxLock = new ReentrantLock(); - - /** - * Bounding box around the tiles on the map - */ - private BoundingBox tilesBoundingBox; - - /** - * True when a tile layer is drawn from features - */ - private boolean featureOverlayTiles = false; - - /** - * List of Feature Overlay Queries for querying tile overlay clicks - */ - private final List featureOverlayQueries = new ArrayList<>(); - /** * Intent activity request code when choosing a file */ @@ -643,7 +532,7 @@ private enum EditType { private View coordTextCard; /** - * Floating Action Button for creating geopackages + * Floating Action Button for creating geoPackages */ private FloatingActionButton fab; @@ -653,7 +542,7 @@ private enum EditType { private FloatingActionButton layerFab; /** - * Task for importing a geopackage + * Task for importing a geoPackage */ private ImportTask importTask; @@ -684,10 +573,20 @@ private enum EditType { private ShareTask shareTask; /** - * Controls user selected basemaps. + * Controls user selected base maps. */ private BasemapApplier basemapApplier; + /** + * Used to zoom the maps position to various spots. + */ + private Zoomer zoomer; + + /** + * Model that contains various states involving the map. + */ + private final MapModel model = new MapModel(); + /** * Activity launchers */ @@ -699,7 +598,7 @@ private enum EditType { private final GoogleMap.OnCameraMoveListener moveListener = new GoogleMap.OnCameraMoveListener() { @Override public void onCameraMove() { - if(zoomLevelText.getVisibility() == View.VISIBLE && map != null) { + if (zoomLevelText.getVisibility() == View.VISIBLE && map != null) { zoomLevelText.setText(getResources().getString( R.string.zoom_level, map.getCameraPosition().zoom)); @@ -729,21 +628,22 @@ public void onCreate(Bundle savedInstanceState) { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if(getActivity() != null) { + if (getActivity() != null) { geoPackageViewModel = new ViewModelProvider(getActivity()).get(GeoPackageViewModel.class); geoPackageViewModel.init(); + model.setActive(new GeoPackageDatabases( + getActivity().getApplicationContext(), + "active")); + vibrator = (Vibrator) getActivity().getSystemService( + Context.VIBRATOR_SERVICE); } - active = new GeoPackageDatabases(getActivity().getApplicationContext(), "active"); - - if(geoPackageViewModel != null && geoPackageViewModel.getGeos() != null) { + if (geoPackageViewModel != null && geoPackageViewModel.getGeos() != null) { GeoPackageSynchronizer.getInstance().synchronizeTables( geoPackageViewModel.getGeos().getValue(), - active); + model.getActive()); } - vibrator = (Vibrator) getActivity().getSystemService( - Context.VIBRATOR_SERVICE); view = inflater.inflate(R.layout.fragment_map, container, false); getMapFragment().getMapAsync(this); @@ -754,8 +654,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, // Set listeners for icons on map setIconListeners(); - // Set up loaciton provider - if(getContext() != null) { + // Set up location provider + if (getContext() != null) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(getContext()); } @@ -782,7 +682,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, // NOTE: This view is invisible by default transBox = getLayoutInflater().inflate(R.layout.transparent_box_view, null); - // Create a sharetask to handle sharing to other apps or saving to disk + // Create a ShareTask to handle sharing to other apps or saving to disk shareTask = new ShareTask(getActivity()); // Set up activity launchers registered for results @@ -804,20 +704,20 @@ public void launchPreferences() { * Set up activity launchers for results * (replaces startActivityForResult) */ - private void setupLaunchers(){ - // Import a geopackage from file + private void setupLaunchers() { + // Import a geoPackage from file importGeoPackageActivityResultLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - (ActivityResult result) -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent data = result.getData(); - if(data != null) { - // Import geopackage from file - ImportTask task = new ImportTask(getActivity(), data); - task.importFile(); + new ActivityResultContracts.StartActivityForResult(), + (ActivityResult result) -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + if (data != null) { + // Import geoPackage from file + ImportTask task = new ImportTask(getActivity(), data); + task.importFile(); + } } - } - }); + }); } /** @@ -876,7 +776,7 @@ private void populateRecyclerWithDetail() { * Sets the main RecyclerView to show the details for a selected layer from the GeoPackage * detail page * - * @param layerAdapter - A prepopulated adapter to populate with a layer's detail + * @param layerAdapter - A pre-populated adapter to populate with a layer's detail */ private void populateRecyclerWithLayerDetail(LayerPageAdapter layerAdapter) { layerFab.hide(); @@ -901,11 +801,11 @@ private void createGeoPackageRecycler() { populateRecyclerWithGeoPackages(); - // Listener for swiping a geopackage to the right to enable/disable all layers + // Listener for swiping a geoPackage to the right to enable/disable all layers EnableAllLayersListener gpSwipeListener = (boolean active, GeoPackageDatabase db) -> geoPackageViewModel.setAllLayersActive(active, db); - if(getContext() != null) { + if (getContext() != null) { SwipeController controller = new SwipeController(getContext(), gpSwipeListener); controller.getTouchHelper().attachToRecyclerView(geoPackageRecycler); } @@ -920,7 +820,7 @@ private void createGeoPackageRecycler() { private void subscribeGeoPackageRecycler() { // Observe list of GeoPackages geoPackageViewModel.getGeos().observe(getViewLifecycleOwner(), newGeos -> { - // Set the visibility of the 'no geopackages found' message + // Set the visibility of the 'no geoPackages found' message setListVisibility(newGeos.getDatabases().isEmpty()); // If not empty, repopulate the list geoPackageRecyclerAdapter.clear(); @@ -933,20 +833,20 @@ private void subscribeGeoPackageRecycler() { // Make sure the detail page is repopulated in case a new layer is added if (detailPageAdapter != null) { - detailPageAdapter.updateAllTables(newGeos, active); + detailPageAdapter.updateAllTables(newGeos, model.getActive()); } }); // Observe Active Tables - used to determine which layers are enabled. Update main list // of geoPackages when a change is made in order to change the active state geoPackageViewModel.getActive().observe(getViewLifecycleOwner(), newTables -> { - GeoPackageSynchronizer.getInstance().synchronizeTables(active, newTables); - active = newTables; + GeoPackageSynchronizer.getInstance().synchronizeTables(model.getActive(), newTables); + model.setActive(newTables); geoPackageRecyclerAdapter.updateActiveTables(newTables.getDatabases()); geoPackageRecyclerAdapter.notifyDataSetChanged(); // Get the total number of active features and the max features setting - int totalFeatures = active.getAllFeaturesCount(); + int totalFeatures = model.getActive().getAllFeaturesCount(); int maxFeatureSetting = getMaxFeatures(); if (totalFeatures > maxFeatureSetting) { showMaxFeaturesExceeded(); @@ -954,23 +854,20 @@ private void subscribeGeoPackageRecycler() { // if the detail page has been used, send the updated active list for it to update itself if (detailPageAdapter != null) { - detailPageAdapter.updateActiveTables(active); + detailPageAdapter.updateActiveTables(model.getActive()); } // if the layer detail page has been created, send the updated active list for it to update itself if (layerAdapter != null) { - layerAdapter.updateActiveTables(active); + layerAdapter.updateActiveTables(model.getActive()); } // Update the map - if (newTables.isEmpty()) { - if (map != null) { + if (map != null) { + if (newTables.isEmpty()) { map.clear(); } - } else { - if (map != null) { - updateInBackground(true, false); - } + updateInBackground(true); } }); } @@ -1006,12 +903,12 @@ private void createGeoPackageDetailAdapter(GeoPackageDatabase db) { View.OnClickListener detailBackListener = (View view) -> populateRecyclerWithGeoPackages(); // Generate a list to pass to the adapter. Should contain: - // - A heaader: DetailPageHeaderObject + // - A header: DetailPageHeaderObject // - N number of DetailPageLayerObject objects generated from the GeoPackageDatabase object DetailPageHeaderObject detailHeader = new DetailPageHeaderObject(db); List detailList = new ArrayList<>(); detailList.add(detailHeader); - detailList.addAll(db.getLayerObjects(active.getDatabase(db.getDatabase()))); + detailList.addAll(db.getLayerObjects(model.getActive().getDatabase(db.getDatabase()))); detailPageAdapter = new DetailPageAdapter(detailList, layerListener, detailBackListener, detailActionListener, activeLayerListener, enableAllListener, db); @@ -1122,7 +1019,7 @@ public void onDetailGP(String gpName) { /** * Implement OnDialogButtonClickListener Rename button confirm click - * Rename a GeoPackage and recreate the detailview adapter to make it refresh + * Rename a GeoPackage and recreate the detail view adapter to make it refresh * * @param oldName - GeoPackage original name * @param newName - New GeoPackage name @@ -1173,7 +1070,7 @@ public void onShareGP(String gpName) { /** * Implement OnDialogButtonClickListener Copy button confirm click - * Copy a GeoPackage in the repository and replace the recyclerview with the geopackages list + * Copy a GeoPackage in the repository and replace the recyclerview with the geoPackages list * * @param gpName - GeoPackage name */ @@ -1237,7 +1134,7 @@ public void onLayerDeleted(String geoPackageName) { } /** - * Ask the viewmodel to rename a layer in the given geopackage + * Ask the view model to rename a layer in the given geopackage */ public void onRenameLayer(String gpName, String layerName, String newLayerName) { // First remove it from the active layers @@ -1250,14 +1147,14 @@ public void onRenameLayer(String gpName, String layerName, String newLayerName) GeoPackageDatabase newDb = geoPackageViewModel.getGeoByName(gpName); // createGeoPackageDetailAdapter(db); - DetailPageLayerObject newLayerObject = newDb.getLayerObject(active.getDatabase(gpName), gpName, newLayerName); + DetailPageLayerObject newLayerObject = newDb.getLayerObject(model.getActive().getDatabase(gpName), gpName, newLayerName); if (newLayerObject != null) createGeoPackageLayerDetailAdapter(newLayerObject); } } /** - * Ask the viewmodel to copy a layer in a given geopackage + * Ask the view model to copy a layer in a given geopackage */ public void onCopyLayer(String gpName, String oldLayer, String newLayerName) { Log.i("click", "Copy Layer"); @@ -1277,14 +1174,14 @@ public void onCopyLayer(String gpName, String oldLayer, String newLayerName) { } /** - * Ask the viewmodel to create a new layer feature column + * Ask the view model to create a new layer feature column */ public void onAddFeatureField(String gpName, String layerName, String fieldName, GeoPackageDataType type) { try { if (geoPackageViewModel.createFeatureColumnLayer(gpName, layerName, fieldName, type)) { GeoPackageDatabase newDb = geoPackageViewModel.getGeoByName(gpName); - DetailPageLayerObject newLayerObject = newDb.getLayerObject(active.getDatabase(gpName), gpName, layerName); + DetailPageLayerObject newLayerObject = newDb.getLayerObject(model.getActive().getDatabase(gpName), gpName, layerName); if (newLayerObject != null) createGeoPackageLayerDetailAdapter(newLayerObject); } else { @@ -1299,13 +1196,13 @@ public void onAddFeatureField(String gpName, String layerName, String fieldName, } /** - * Remove a Feature Column from a layer via the viewmodel + * Remove a Feature Column from a layer via the view model */ public void onDeleteFeatureColumn(String gpName, String layerName, String columnName) { try { if (geoPackageViewModel.deleteFeatureColumnLayer(gpName, layerName, columnName)) { GeoPackageDatabase newDb = geoPackageViewModel.getGeoByName(gpName); - DetailPageLayerObject newLayerObject = newDb.getLayerObject(active.getDatabase(gpName), gpName, layerName); + DetailPageLayerObject newLayerObject = newDb.getLayerObject(model.getActive().getDatabase(gpName), gpName, layerName); if (newLayerObject != null) createGeoPackageLayerDetailAdapter(newLayerObject); } else { @@ -1332,7 +1229,7 @@ public void onCancelButtonClicked() { * Pop up menu for map view type icon button - selector for map, satellite, terrain */ public void openMapSelect() { - if(getActivity() != null) { + if (getActivity() != null) { PopupMenu pm = new PopupMenu(getActivity(), mapSelectButton); // Needed to make the icons visible try { @@ -1349,24 +1246,19 @@ public void openMapSelect() { if (item.getItemId() == R.id.map) { setMapType(GoogleMap.MAP_TYPE_NORMAL); return true; - } - else if (item.getItemId() == R.id.satellite) { + } else if (item.getItemId() == R.id.satellite) { setMapType(GoogleMap.MAP_TYPE_SATELLITE); return true; - } - else if (item.getItemId() == R.id.terrain) { + } else if (item.getItemId() == R.id.terrain) { setMapType(GoogleMap.MAP_TYPE_TERRAIN); return true; - } - else if (item.getItemId() == R.id.NoGrid) { + } else if (item.getItemId() == R.id.NoGrid) { setGridType(GridType.NONE); return true; - } - else if (item.getItemId() == R.id.GARSGrid) { + } else if (item.getItemId() == R.id.GARSGrid) { setGridType(GridType.GARS); return true; - } - else if (item.getItemId() == R.id.MGRSGrid) { + } else if (item.getItemId() == R.id.MGRSGrid) { setGridType(GridType.MGRS); return true; } @@ -1379,10 +1271,10 @@ else if (item.getItemId() == R.id.MGRSGrid) { /** - * Pop up menu for editing geoapackage - drawing features, bounding box, etc + * Pop up menu for editing geoPackage - drawing features, bounding box, etc */ public void openEditMenu() { - if(getActivity() != null) { + if (getActivity() != null) { PopupMenu pm = new PopupMenu(getActivity(), editFeaturesButton); // Needed to make the icons visible try { @@ -1397,7 +1289,7 @@ public void openEditMenu() { // Set text for edit features mode MenuItem editFeaturesItem = pm.getMenu().findItem(R.id.features); - if (editFeaturesMode) { + if (model.isEditFeaturesMode()) { editFeaturesItem.setTitle("Stop editing"); } else { editFeaturesItem.setTitle("Edit Features"); @@ -1419,33 +1311,31 @@ public void openEditMenu() { showBearing.setTitle("Show Bearing"); } - int totalFeaturesAndTiles = active.getAllFeaturesAndTilesCount(); + int totalFeaturesAndTiles = model.getActive().getAllFeaturesAndTilesCount(); if (totalFeaturesAndTiles == 0) { MenuItem zoomToActive = pm.getMenu().findItem(R.id.zoomToActive); zoomToActive.setEnabled(false); } pm.setOnMenuItemClickListener((MenuItem item) -> { - if(item.getItemId() == R.id.zoomToActive) { - zoomToActive(); - return true; - } - else if(item.getItemId() == R.id.features) { + if (item.getItemId() == R.id.zoomToActive) { + zoomer.zoomToActive(); + return true; + } else if (item.getItemId() == R.id.features) { editFeaturesMenuItem = item; - if (!editFeaturesMode) { + if (!model.isEditFeaturesMode()) { selectEditFeatures(); } else { resetEditFeatures(); - updateInBackground(false, true); + updateInBackground(false); } return true; - } - else if(item.getItemId() == R.id.boundingBox) { + } else if (item.getItemId() == R.id.boundingBox) { boundingBoxMenuItem = item; if (!boundingBoxMode) { - if (editFeaturesMode) { + if (model.isEditFeaturesMode()) { resetEditFeatures(); - updateInBackground(false, true); + updateInBackground(false); } boundingBoxMode = true; @@ -1453,20 +1343,16 @@ else if(item.getItemId() == R.id.boundingBox) { resetBoundingBox(); } return true; - } - else if(item.getItemId() == R.id.maxFeatures) { + } else if (item.getItemId() == R.id.maxFeatures) { setMaxFeatures(); return true; - } - else if(item.getItemId() == R.id.clearAllActive) { + } else if (item.getItemId() == R.id.clearAllActive) { clearAllActive(); return true; - } - else if(item.getItemId() == R.id.showMyLocation) { + } else if (item.getItemId() == R.id.showMyLocation) { showMyLocation(); return true; - } - else if(item.getItemId() == R.id.showBearing) { + } else if (item.getItemId() == R.id.showBearing) { setMapBearing(); return true; } @@ -1526,7 +1412,7 @@ private void showMyLocation() { * Gets current location from fused location provider and zooms to that location */ private void zoomToMyLocation() { - if(getContext() != null && getActivity() != null) { + if (getContext() != null && getActivity() != null) { // Verify permissions first if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, @@ -1559,7 +1445,7 @@ private void setMapBearing() { * Enable map bearing compass */ private void showMapBearing() { - if(getContext() != null && getActivity() != null) { + if (getContext() != null && getActivity() != null) { // Verify permissions first if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, @@ -1647,7 +1533,7 @@ private void setNewLayerFab() { /** - * Sets the visibility of the recycler view vs "no geopackages found" message bases on the + * Sets the visibility of the recycler view vs "no geoPackages found" message bases on the * recycler view being empty */ private void setListVisibility(boolean empty) { @@ -1703,7 +1589,7 @@ public void setIconListeners() { * Disclaimer popup */ private void showDisclaimer() { - if(getActivity() != null) { + if (getActivity() != null) { // Only show it if the user hasn't already accepted it before SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); boolean disclaimerPref = sharedPreferences.getBoolean(getString(R.string.disclaimerPref), false); @@ -1717,15 +1603,15 @@ private void showDisclaimer() { .setView(disclaimerView); final AlertDialog alertDialog = dialogBuilder.create(); acceptButton.setOnClickListener((View view) -> { - sharedPreferences.edit().putBoolean(getString(R.string.disclaimerPref), true).apply(); - alertDialog.dismiss(); + sharedPreferences.edit().putBoolean(getString(R.string.disclaimerPref), true).apply(); + alertDialog.dismiss(); }); exitButton.setOnClickListener((View view) -> getActivity().finish()); // Prevent the dialog from closing when clicking outside the dialog or the back button alertDialog.setCanceledOnTouchOutside(false); alertDialog.setOnKeyListener((DialogInterface arg0, int keyCode, - KeyEvent event) -> true); + KeyEvent event) -> true); alertDialog.show(); } } @@ -1736,7 +1622,7 @@ private void showDisclaimer() { * Show a warning that the user has selected more features than the current max features setting */ private void showMaxFeaturesExceeded() { - if(getActivity() != null) { + if (getActivity() != null) { // First check the settings to see if they disabled the message if (displayMaxFeatureWarning) { @@ -1760,20 +1646,20 @@ private void showMaxFeaturesExceeded() { dontShowAgain.setVisibility(View.VISIBLE); AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) - .setView(alertView) - .setPositiveButton(getString(R.string.button_ok_label), - (DialogInterface d, int whichButton) -> { - if (dontShowAgain.isChecked()) { - // Update the preference for showing this message in the future - SharedPreferences settings = PreferenceManager - .getDefaultSharedPreferences(getActivity()); - SharedPreferences.Editor editor = settings.edit(); - editor.putBoolean(MAX_FEATURES_MESSAGE_KEY, !dontShowAgain.isChecked()); - editor.apply(); - settingsUpdate(); - } - d.cancel(); - }); + .setView(alertView) + .setPositiveButton(getString(R.string.button_ok_label), + (DialogInterface d, int whichButton) -> { + if (dontShowAgain.isChecked()) { + // Update the preference for showing this message in the future + SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(getActivity()); + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean(MAX_FEATURES_MESSAGE_KEY, !dontShowAgain.isChecked()); + editor.apply(); + settingsUpdate(); + } + d.cancel(); + }); dialog.show(); } } @@ -1784,7 +1670,7 @@ private void showMaxFeaturesExceeded() { * Create wizard for Import or Create GeoPackage */ private void createNewWizard() { - if(getActivity() != null) { + if (getActivity() != null) { // Create Alert window with basic input text layout LayoutInflater inflater = LayoutInflater.from(getActivity()); View alertView = inflater.inflate(R.layout.new_geopackage_wizard, null); @@ -1800,24 +1686,24 @@ private void createNewWizard() { // Click listener for "Create New" alertView.findViewById(R.id.new_wizard_create_card) - .setOnClickListener((View v) -> { - createGeoPackage(); - alertDialog.dismiss(); - }); + .setOnClickListener((View v) -> { + createGeoPackage(); + alertDialog.dismiss(); + }); // Click listener for "Import URL" alertView.findViewById(R.id.new_wizard_download_card) - .setOnClickListener((View v) -> { + .setOnClickListener((View v) -> { importGeopackageFromUrl(); alertDialog.dismiss(); - }); + }); // Click listener for "Import from file" alertView.findViewById(R.id.new_wizard_file_card) - .setOnClickListener((View v) -> { + .setOnClickListener((View v) -> { getImportPermissions(MainActivity.MANAGER_PERMISSIONS_REQUEST_ACCESS_IMPORT_EXTERNAL); alertDialog.dismiss(); - }); + }); alertDialog.show(); } @@ -1828,7 +1714,7 @@ private void createNewWizard() { * Create a new GeoPackage */ private void createGeoPackage() { - if(getActivity() != null) { + if (getActivity() != null) { // Create Alert window with basic input text layout LayoutInflater inflater = LayoutInflater.from(getActivity()); View alertView = inflater.inflate(R.layout.basic_edit_alert, null); @@ -1845,25 +1731,25 @@ private void createGeoPackage() { AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) .setView(alertView) .setPositiveButton(getString(R.string.button_create_label), - (DialogInterface d, int whichButton) -> { - String value = inputName.getText() != null ? inputName.getText().toString() : null; - if (value != null && !value.isEmpty()) { - try { - if (!geoPackageViewModel.createGeoPackage(value)) { - GeoPackageUtils - .showMessage( - getActivity(), - getString(R.string.geopackage_create_label), - "Failed to create GeoPackage: " - + value); + (DialogInterface d, int whichButton) -> { + String value = inputName.getText() != null ? inputName.getText().toString() : null; + if (value != null && !value.isEmpty()) { + try { + if (!geoPackageViewModel.createGeoPackage(value)) { + GeoPackageUtils + .showMessage( + getActivity(), + getString(R.string.geopackage_create_label), + "Failed to create GeoPackage: " + + value); + } + } catch (Exception e) { + GeoPackageUtils.showMessage( + getActivity(), "Create " + + value, e.getMessage()); } - } catch (Exception e) { - GeoPackageUtils.showMessage( - getActivity(), "Create " - + value, e.getMessage()); } - } - }) + }) .setNegativeButton(getString(R.string.button_discard_label), (DialogInterface d, int whichButton) -> d.cancel()); @@ -1876,7 +1762,7 @@ private void createGeoPackage() { * Pop up dialog for creating a new feature or tile layer from the geopackage detail view FAB */ public void newLayerWizard() { - if(getActivity() != null) { + if (getActivity() != null) { // Create Alert window with basic input text layout LayoutInflater inflater = LayoutInflater.from(getActivity()); View alertView = inflater.inflate(R.layout.new_layer_wizard, null); @@ -1921,7 +1807,7 @@ public void newLayerWizard() { * Create feature layer menu */ private void createFeatureOption() { - if(getActivity() != null) { + if (getActivity() != null) { LayoutInflater inflater = LayoutInflater.from(getActivity()); View createFeaturesView = inflater.inflate(R.layout.create_features, null); @@ -2009,7 +1895,7 @@ private void createFeatureOption() { e.getMessage()); } }).setNegativeButton(getString(R.string.button_cancel_label), - (DialogInterface d, int id) -> d.cancel()); + (DialogInterface d, int id) -> d.cancel()); dialog.show(); } } @@ -2048,10 +1934,10 @@ this, getActivity(), getContext(), this, /** * Make sure we have permissions to read/write to external before importing. The result will * send MANAGER_PERMISSIONS_REQUEST_ACCESS_IMPORT_EXTERNAL or MANAGER_PERMISSIONS_REQUEST_ACCESS_EXPORT_DATABASE - * back up to mainactivity, and should call importGeopackageFromFile or exportGeoPackageToExternal + * back up to main activity, and should call importGeopackageFromFile or exportGeoPackageToExternal */ private void getImportPermissions(int returnCode) { - if(getActivity() != null) { + if (getActivity() != null) { if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) { new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) .setTitle(R.string.storage_access_rational_title) @@ -2100,7 +1986,6 @@ public void exportGeoPackageToExternal() { */ private void clearAllActive() { geoPackageViewModel.clearAllActive(); -// zoomToZero(); zoomOut(); } @@ -2109,7 +1994,7 @@ private void clearAllActive() { * Import a GeoPackage from a URL */ private void importGeopackageFromUrl() { - if(getActivity() != null) { + if (getActivity() != null) { LayoutInflater inflater = LayoutInflater.from(getActivity()); View importUrlView = inflater.inflate(R.layout.import_url, null); AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle); @@ -2161,37 +2046,35 @@ public void afterTextChanged(Editable editable) { }; inputUrl.addTextChangedListener(inputUrlWatcher); - // Example Geopackages link handler - ((TextView) importUrlView.findViewById(R.id.import_examples)) - .setOnClickListener((View v) -> { - ArrayAdapter adapter = new ArrayAdapter<>( - getActivity(), android.R.layout.select_dialog_item); - adapter.addAll(getResources().getStringArray( - R.array.preloaded_geopackage_url_labels)); - AlertDialog.Builder builder = new AlertDialog.Builder( - getActivity(), R.style.AppCompatAlertDialogStyle); - builder.setTitle(getString(R.string.import_url_preloaded_label)); - builder.setAdapter(adapter, - (DialogInterface d, int item) -> { - if (item >= 0) { - String[] urls = getResources() - .getStringArray( - R.array.preloaded_geopackage_urls); - String[] names = getResources() - .getStringArray( - R.array.preloaded_geopackage_url_names); - inputName.setText(names[item]); - inputUrl.setText(urls[item]); - } - }); + // Example GeoPackages link handler + importUrlView.findViewById(R.id.import_examples) + .setOnClickListener((View v) -> { + ArrayAdapter adapter = new ArrayAdapter<>( + getActivity(), android.R.layout.select_dialog_item); + // Download sample geopackages from our github server, and combine that list + // with our own locally provided preloaded geopackages + SampleDownloader sampleDownloader = new SampleDownloader(getActivity(), adapter); + sampleDownloader.loadLocalGeoPackageSamples(); + sampleDownloader.getExampleData(getString(R.string.sample_geopackage_url)); + AlertDialog.Builder builder = new AlertDialog.Builder( + getActivity(), R.style.AppCompatAlertDialogStyle); + builder.setTitle(getString(R.string.import_url_preloaded_label)); + builder.setAdapter(adapter, + (DialogInterface d, int item) -> { + if (item >= 0) { + String name = adapter.getItem(item); + inputName.setText(name); + inputUrl.setText(sampleDownloader.getSampleList().get(name)); + } + }); - AlertDialog alert = builder.create(); - alert.show(); - }); + AlertDialog alert = builder.create(); + alert.show(); + }); dialog.setPositiveButton(getString(R.string.geopackage_import_label), (DialogInterface d, int id) -> { - // This will be overridden by click listener after show is called + // This will be overridden by click listener after show is called }).setNegativeButton(getString(R.string.button_cancel_label), (DialogInterface d, int id) -> d.cancel()); @@ -2302,7 +2185,7 @@ private void initializeMap() { if (map == null) return; setEditFeaturesView(); - + zoomer = new Zoomer(this.model, this.geoPackageViewModel, getActivity(), map, getView()); map.setOnMapLongClickListener(this); map.setOnMapClickListener(this); map.setOnMarkerClickListener(this); @@ -2401,17 +2284,17 @@ public BasemapApplier getBaseApplier() { public void onCameraIdle() { // If visible & not editing a shape, update the feature shapes for the current map view region - if (visible && (!editFeaturesMode || editFeatureType == null || (editPoints.isEmpty() && editFeatureMarker == null))) { + if (visible && (!model.isEditFeaturesMode() || editFeatureType == null || (editPoints.isEmpty() && editFeatureMarker == null))) { int previousZoom = currentZoom; int zoom = (int) MapUtils.getCurrentZoom(map); currentZoom = zoom; if (zoom != previousZoom) { // Zoom level changed, remove all feature shapes except for markers - featureShapes.removeShapesExcluding(GoogleMapShapeType.MARKER, GoogleMapShapeType.MULTI_MARKER); + model.getFeatureShapes().removeShapesExcluding(GoogleMapShapeType.MARKER, GoogleMapShapeType.MULTI_MARKER); } else { // Remove shapes no longer visible on the map view - featureShapes.removeShapesNotWithinMap(map); + model.getFeatureShapes().removeShapesNotWithinMap(map); } BoundingBox mapViewBoundingBox = MapUtils.getBoundingBox(map); @@ -2421,10 +2304,10 @@ public void onCameraIdle() { updateLock.lock(); try { if (updateFeaturesTask != null) { - updateFeaturesTask.cancel(false); + updateFeaturesTask.cancel(); } - updateFeaturesTask = new MapFeaturesUpdateTask(); - updateFeaturesTask.execute(false, maxFeatures, mapViewBoundingBox, toleranceDistance, true); + updateFeaturesTask = new MapFeaturesUpdateTask(getActivity(), map, model, geoPackageViewModel); + updateFeaturesTask.execute(maxFeatures, mapViewBoundingBox, toleranceDistance, true); } finally { updateLock.unlock(); } @@ -2443,7 +2326,7 @@ private SupportMapFragment getMapFragment() { fm = getChildFragmentManager(); } SupportMapFragment frag = null; - if(fm != null) { + if (fm != null) { frag = (SupportMapFragment) fm.findFragmentById(R.id.fragment_map_view_ui); } @@ -2561,26 +2444,26 @@ private void validateAndClearEditFeatures(final EditType editTypeClicked) { if (editPoints.isEmpty() && editFeatureType != EditType.EDIT_FEATURE) { clearEditFeaturesAndUpdateType(editTypeClicked); } else { - if(getActivity() != null) { + if (getActivity() != null) { AlertDialog deleteDialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) .setTitle( getString(R.string.edit_features_clear_validation_label)) .setMessage( getString(R.string.edit_features_clear_validation_message)) .setPositiveButton(getString(R.string.button_ok_label), - (DialogInterface dialog, int which) -> { + (DialogInterface dialog, int which) -> { if (editFeatureType == EditType.EDIT_FEATURE) { editFeatureType = null; } clearEditFeaturesAndUpdateType(editTypeClicked); - }) + }) .setOnCancelListener( - (DialogInterface dialog) -> tempEditFeatureMarker = null) + (DialogInterface dialog) -> tempEditFeatureMarker = null) .setNegativeButton(getString(R.string.button_cancel_label), - (DialogInterface dialog, int which) -> { + (DialogInterface dialog, int which) -> { tempEditFeatureMarker = null; dialog.dismiss(); - }).create(); + }).create(); deleteDialog.show(); } } @@ -2635,11 +2518,11 @@ private void setEditType(EditType previousType, EditType editType) { case EDIT_FEATURE: editFeatureMarker = tempEditFeatureMarker; tempEditFeatureMarker = null; - Long featureId = editFeatureIds.get(editFeatureMarker.getId()); - if(featureId != null) { - final GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(editFeaturesDatabase); + Long featureId = model.getEditFeatureIds().get(editFeatureMarker.getId()); + if (featureId != null) { + final GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(model.getEditFeaturesDatabase()); final FeatureDao featureDao = geoPackage - .getFeatureDao(editFeaturesTable); + .getFeatureDao(model.getEditFeaturesTable()); final FeatureRow featureRow = featureDao .queryForIdRow(featureId); Geometry geometry = featureRow.getGeometry().getGeometry(); @@ -2648,7 +2531,7 @@ private void setEditType(EditType previousType, EditType editType) { GoogleMapShape shape = converter.toShape(geometry); editFeatureMarker.remove(); - GoogleMapShape featureObject = editFeatureObjects + GoogleMapShape featureObject = model.getEditFeatureObjects() .remove(editFeatureMarker.getId()); if (featureObject != null) { featureObject.remove(); @@ -2674,11 +2557,11 @@ shape, getEditFeatureMarker(), */ private void addEditableShapeBack() { - Long featureId = editFeatureIds.get(editFeatureMarker.getId()); - if(featureId != null) { - final GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(editFeaturesDatabase); + Long featureId = model.getEditFeatureIds().get(editFeatureMarker.getId()); + if (featureId != null) { + final GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(model.getEditFeaturesDatabase()); final FeatureDao featureDao = geoPackage - .getFeatureDao(editFeaturesTable); + .getFeatureDao(model.getEditFeaturesTable()); final FeatureRow featureRow = featureDao.queryForIdRow(featureId); GeoPackageGeometryData geomData = featureRow.getGeometry(); if (geomData != null) { @@ -2688,10 +2571,17 @@ private void addEditableShapeBack() { featureDao.getProjection()); GoogleMapShape shape = converter.toShape(geometry); StyleCache styleCache = new StyleCache(geoPackage, getResources().getDisplayMetrics().density); - prepareShapeOptions(shape, styleCache, featureRow, true, true); + ShapeHelper.getInstance().prepareShapeOptions( + shape, + styleCache, + featureRow, + true, + true, + getContext()); GoogleMapShape mapShape = GoogleMapShapeConverter .addShapeToMap(map, shape); - addEditableShape(featureId, mapShape); + ShapeHelper.getInstance().addEditableShape( + getContext(), map, model, featureId, mapShape); styleCache.clear(); } } @@ -2714,7 +2604,7 @@ private MarkerOptions getEditFeatureMarker() { } /** - * Get the feature marker options to edit polylines and polygons + * Get the feature marker options to edit poly lines and polygons * * @return The edit feature shape marker. */ @@ -2762,10 +2652,10 @@ private void saveEditFeatures() { boolean changesMade = false; - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(editFeaturesDatabase); + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(model.getEditFeaturesDatabase()); EditType tempEditFeatureType = editFeatureType; try { - FeatureDao featureDao = geoPackage.getFeatureDao(editFeaturesTable); + FeatureDao featureDao = geoPackage.getFeatureDao(model.getEditFeaturesTable()); long srsId = featureDao.getGeometryColumns().getSrsId(); FeatureIndexManager indexer = new FeatureIndexManager(getActivity(), geoPackage, featureDao); List indexedTypes = indexer.getIndexedTypes(); @@ -2832,9 +2722,9 @@ private void saveEditFeatures() { case EDIT_FEATURE: editFeatureType = null; - Long featureId = editFeatureIds.get(editFeatureMarker.getId()); + Long featureId = model.getEditFeatureIds().get(editFeatureMarker.getId()); - if(featureId != null) { + if (featureId != null) { Geometry geometry = converter.toGeometry(editFeatureShape .getShape()); if (geometry != null) { @@ -2859,7 +2749,7 @@ private void saveEditFeatures() { indexer.deleteIndex(featureId, indexedTypes); } } - active.setModified(true); + model.getActive().setModified(true); } break; @@ -2872,7 +2762,7 @@ private void saveEditFeatures() { .showMessage( getActivity(), getString(R.string.edit_features_save_label) - + " " + editFeaturesTable, + + " " + model.getEditFeaturesTable(), "GeoPackage contains unsupported SQLite function, module, or trigger for writing: " + e.getMessage()); } else { GeoPackageUtils.showMessage(getActivity(), @@ -2884,8 +2774,8 @@ private void saveEditFeatures() { clearEditFeaturesAndPreserveType(); if (changesMade) { - active.setModified(true); - updateInBackground(false, true); + model.getActive().setModified(true); + updateInBackground(false); } } @@ -2919,8 +2809,8 @@ public void onHiddenChanged(boolean hidden) { visible = !hidden; - if (visible && active.isModified()) { - active.setModified(false); + if (visible && model.getActive().isModified()) { + model.getActive().setModified(false); resetBoundingBox(); resetEditFeatures(); if (mapLoaded) { @@ -2930,17 +2820,13 @@ public void onHiddenChanged(boolean hidden) { updateLock.lock(); try { if (updateTask != null) { - if (updateTask.getStatus() != AsyncTask.Status.FINISHED) { - updateTask.cancel(false); - active.setModified(true); - } + updateTask.cancel(); + model.getActive().setModified(true); updateTask = null; } if (updateFeaturesTask != null) { - if (updateFeaturesTask.getStatus() != AsyncTask.Status.FINISHED) { - updateFeaturesTask.cancel(false); - active.setModified(true); - } + updateFeaturesTask.cancel(); + model.getActive().setModified(true); updateFeaturesTask = null; } } finally { @@ -2974,26 +2860,24 @@ public boolean handleMenuClick(MenuItem item) { boolean handled = false; if (item.getItemId() == R.id.map_zoom) { - zoomToActive(); + zoomer.zoomToActive(); handled = true; - } - else if (item.getItemId() == R.id.map_features) { + } else if (item.getItemId() == R.id.map_features) { editFeaturesMenuItem = item; - if (!editFeaturesMode) { + if (!model.isEditFeaturesMode()) { selectEditFeatures(); } else { resetEditFeatures(); - updateInBackground(false, true); + updateInBackground(false); } handled = true; - } - else if (item.getItemId() == R.id.map_bounding_box) { + } else if (item.getItemId() == R.id.map_bounding_box) { boundingBoxMenuItem = item; if (!boundingBoxMode) { - if (editFeaturesMode) { + if (model.isEditFeaturesMode()) { resetEditFeatures(); - updateInBackground(false, true); + updateInBackground(false); } boundingBoxMode = true; @@ -3002,8 +2886,7 @@ else if (item.getItemId() == R.id.map_bounding_box) { resetBoundingBox(); } handled = true; - } - else if (item.getItemId() == R.id.max_features) { + } else if (item.getItemId() == R.id.max_features) { setMaxFeatures(); handled = true; } @@ -3023,14 +2906,14 @@ private void openEditFeatures(String geoPackage, String layer) { resetBoundingBox(); } - editFeaturesDatabase = geoPackage; - editFeaturesTable = layer; + model.setEditFeaturesDatabase(geoPackage); + model.setEditFeaturesTable(layer); - editFeaturesMode = true; + model.setEditFeaturesMode(true); editFeaturesView.setVisibility(View.VISIBLE); - updateInBackground(false, true); + updateInBackground(false); } catch (Exception e) { GeoPackageUtils @@ -3069,17 +2952,15 @@ private void selectEditFeatures() { resetBoundingBox(); } - editFeaturesDatabase = geoPackageInput - .getSelectedItem().toString(); - editFeaturesTable = featuresInput.getSelectedItem() - .toString(); + model.setEditFeaturesDatabase(geoPackageInput.getSelectedItem().toString()); + model.setEditFeaturesTable(featuresInput.getSelectedItem().toString()); - editFeaturesMode = true; + model.setEditFeaturesMode(true); editFeaturesView.setVisibility(View.VISIBLE); editFeaturesMenuItem .setIcon(R.drawable.ic_features_active); - updateInBackground(false, true); + updateInBackground(false); } catch (Exception e) { GeoPackageUtils @@ -3099,7 +2980,7 @@ private void selectEditFeatures() { * Update the features selection based upon the database * * @param featuresInput The feature input spinner. - * @param database The name of the geoPackage. + * @param database The name of the geoPackage. */ private void updateFeaturesSelection(Spinner featuresInput, String database) { @@ -3122,12 +3003,12 @@ private void resetBoundingBox() { * Reset the edit features state */ private void resetEditFeatures() { - editFeaturesMode = false; + model.setEditFeaturesMode(false); editFeaturesView.setVisibility(View.INVISIBLE); - editFeaturesDatabase = null; - editFeaturesTable = null; - editFeatureIds.clear(); - editFeatureObjects.clear(); + model.setEditFeaturesDatabase(null); + model.setEditFeaturesTable(null); + model.getEditFeatureIds().clear(); + model.getEditFeatureObjects().clear(); editFeatureShape = null; editFeatureShapeMarkers = null; editFeatureMarker = null; @@ -3222,7 +3103,7 @@ private void clearEditHoleFeatures() { * Let the user set the max number of features to draw */ private void setMaxFeatures() { - if(getActivity() != null) { + if (getActivity() != null) { // Create Alert window with basic input text layout LayoutInflater inflater = LayoutInflater.from(getActivity()); View alertView = inflater.inflate(R.layout.basic_edit_alert, null); @@ -3254,7 +3135,7 @@ private void setMaxFeatures() { Editor editor = settings.edit(); editor.putInt(MAX_FEATURES_KEY, maxFeature); editor.apply(); - updateInBackground(false, true); + updateInBackground(false); // ignoreHighFeatures will tell if the user previously checked the // 'do not show this warning again' checkbox last time boolean ignoreHighFeatures = settings.getBoolean(String.valueOf(R.string.ignore_high_features), false); @@ -3306,7 +3187,7 @@ public void afterTextChanged(Editable editable) { * Makes a warning popup to alert the user that the max features setting is high */ public void maxFeatureWarning() { - if(getActivity() != null) { + if (getActivity() != null) { View checkBoxView = View.inflate(getContext(), R.layout.checkbox, null); CheckBox checkBox = checkBoxView.findViewById(R.id.showHighFeatureBox); @@ -3367,1497 +3248,164 @@ private void setGridType(GridType gridType) { * @param zoom zoom flag */ private void updateInBackground(boolean zoom) { - updateInBackground(zoom, false); - } - - /** - * Update the map by kicking off a background task - * - * @param zoom zoom flag - * @param filter filter features flag - */ - private void updateInBackground(boolean zoom, boolean filter) { - if(getActivity() != null) { - getActivity().runOnUiThread(() -> map.clear()); - featureDaos.clear(); + if (getActivity() != null) { + model.getFeatureDaos().clear(); basemapApplier.clear(); if (zoom) { - zoomToActiveBounds(); + zoomer.zoomToActiveBounds(); } - featuresBoundingBox = null; - tilesBoundingBox = null; - featureOverlayTiles = false; - featureOverlayQueries.clear(); - featureShapes.clear(); - markerIds.clear(); - int maxFeatures = getMaxFeatures(); + model.setFeaturesBoundingBox(null); + model.setTilesBoundingBox(null); + model.setFeatureOverlayTiles(false); + model.getFeatureOverlayQueries().clear(); + model.getFeatureShapes().clear(); + model.getMarkerIds().clear(); getActivity().runOnUiThread(() -> { - BoundingBox mapViewBoundingBox = MapUtils.getBoundingBox(map); - double toleranceDistance = MapUtils.getToleranceDistance(view, map); - + map.clear(); MapUpdateTask localUpdateTask; updateLock.lock(); try { if (updateTask != null) { - updateTask.cancel(false); + updateTask.cancel(); } if (updateFeaturesTask != null) { - updateFeaturesTask.cancel(false); + updateFeaturesTask.cancel(); } - updateTask = new MapUpdateTask(); + updateTask = new MapUpdateTask(getActivity(), map, basemapApplier, model, geoPackageViewModel); localUpdateTask = updateTask; + + BoundingBox mapViewBoundingBox = MapUtils.getBoundingBox(map); + double toleranceDistance = MapUtils.getToleranceDistance(view, map); + int maxFeatures = getMaxFeatures(); + updateFeaturesTask = new MapFeaturesUpdateTask(getActivity(), map, model, geoPackageViewModel); + updateTask.setFinishListener(() -> updateFeaturesTask.execute(maxFeatures, mapViewBoundingBox, toleranceDistance, true)); } finally { updateLock.unlock(); } - localUpdateTask.execute(zoom, maxFeatures, mapViewBoundingBox, toleranceDistance, filter); + localUpdateTask.execute(); }); } } /** - * Zoom to the active feature and tile table data bounds + * Draw a bounding box with boundingBoxStartCorner and boundingBoxEndCorner */ - private void zoomToActiveBounds() { - - featuresBoundingBox = null; - tilesBoundingBox = null; - - // Pre zoom - List activeDatabases = new ArrayList<>(active.getDatabases()); - for (GeoPackageDatabase database : activeDatabases) { - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(database.getDatabase()); - if (geoPackage != null) { - - Set featureTableDaos = new HashSet<>(); - Collection features = database.getFeatures(); - if (!features.isEmpty()) { - for (GeoPackageFeatureTable featureTable : features) { - featureTableDaos.add(featureTable.getName()); - } - } - - for (GeoPackageFeatureOverlayTable featureOverlay : database.getFeatureOverlays()) { - if (featureOverlay.isActive()) { - featureTableDaos.add(featureOverlay.getFeatureTable()); - } - } + public boolean drawBoundingBox() { + PolygonOptions polygonOptions = new PolygonOptions(); - if (!featureTableDaos.isEmpty()) { + if (getActivity() != null) { + polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_color)); + polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_fill_color)); + } - ContentsDao contentsDao = geoPackage.getContentsDao(); + List points = getPolygonPoints(boundingBoxStartCorner, + boundingBoxEndCorner); + polygonOptions.addAll(points); + boundingBox = map.addPolygon(polygonOptions); + setDrawing(true); + return true; + } - for (String featureTable : featureTableDaos) { + /** + * {@inheritDoc} + */ + @Override + public void onMapLongClick(@NonNull LatLng point) { + if (getActivity() != null) { + if (boundingBoxMode) { - if (featureTable != null && !featureTable.isEmpty()) { - try { - Contents contents = contentsDao.queryForId(featureTable); - BoundingBox contentsBoundingBox = contents.getBoundingBox(); + vibrator.vibrate(getActivity().getResources().getInteger( + R.integer.map_tiles_long_click_vibrate)); - if (contentsBoundingBox != null) { + // Check to see if editing any of the bounding box corners + if (boundingBox != null && boundingBoxEndCorner != null) { + Projection projection = map.getProjection(); - contentsBoundingBox = transformBoundingBoxToWgs84(contentsBoundingBox, contents.getSrs()); + double allowableScreenPercentage = (getActivity() + .getResources() + .getInteger( + R.integer.map_tiles_long_click_screen_percentage) / 100.0); + Point screenPoint = projection.toScreenLocation(point); - if (featuresBoundingBox != null) { - featuresBoundingBox = featuresBoundingBox.union(contentsBoundingBox); - } else { - featuresBoundingBox = contentsBoundingBox; - } - } - } catch (SQLException e) { - Log.e(GeoPackageMapFragment.class.getSimpleName(), - e.getMessage()); - } + if (isWithinDistance(projection, screenPoint, + boundingBoxEndCorner, allowableScreenPercentage)) { + setDrawing(true); + } else if (isWithinDistance(projection, screenPoint, + boundingBoxStartCorner, allowableScreenPercentage)) { + LatLng temp = boundingBoxStartCorner; + boundingBoxStartCorner = boundingBoxEndCorner; + boundingBoxEndCorner = temp; + setDrawing(true); + } else { + LatLng corner1 = new LatLng( + boundingBoxStartCorner.latitude, + boundingBoxEndCorner.longitude); + LatLng corner2 = new LatLng(boundingBoxEndCorner.latitude, + boundingBoxStartCorner.longitude); + if (isWithinDistance(projection, screenPoint, corner1, + allowableScreenPercentage)) { + boundingBoxStartCorner = corner2; + boundingBoxEndCorner = corner1; + setDrawing(true); + } else if (isWithinDistance(projection, screenPoint, + corner2, allowableScreenPercentage)) { + boundingBoxStartCorner = corner1; + boundingBoxEndCorner = corner2; + setDrawing(true); } } } - Collection tileTables = database.getTiles(); - if (!tileTables.isEmpty()) { - - TileMatrixSetDao tileMatrixSetDao = geoPackage.getTileMatrixSetDao(); - - for (GeoPackageTileTable tileTable : tileTables) { - - try { - TileMatrixSet tileMatrixSet = tileMatrixSetDao.queryForId(tileTable.getName()); - BoundingBox tileMatrixSetBoundingBox = tileMatrixSet.getBoundingBox(); - - tileMatrixSetBoundingBox = transformBoundingBoxToWgs84(tileMatrixSetBoundingBox, tileMatrixSet.getSrs()); - - if (tilesBoundingBox != null) { - tilesBoundingBox = tilesBoundingBox.union(tileMatrixSetBoundingBox); - } else { - tilesBoundingBox = tileMatrixSetBoundingBox; - } - } catch (SQLException e) { - Log.e(GeoPackageMapFragment.class.getSimpleName(), - e.getMessage()); - } + // Start drawing a new polygon + if (!drawing) { + if (boundingBox != null) { + boundingBox.remove(); + } + boundingBoxStartCorner = point; + boundingBoxEndCorner = point; + PolygonOptions polygonOptions = new PolygonOptions(); + polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_color)); + polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_fill_color)); + List points = getPolygonPoints(boundingBoxStartCorner, + boundingBoxEndCorner); + polygonOptions.addAll(points); + boundingBox = map.addPolygon(polygonOptions); + setDrawing(true); + } + } else if (editFeatureType != null) { + if (editFeatureType == EditType.EDIT_FEATURE) { + if (editFeatureShapeMarkers != null) { + vibrator.vibrate(getActivity().getResources().getInteger( + R.integer.edit_features_add_long_click_vibrate)); + Marker marker = addEditPoint(point); + editFeatureShapeMarkers.addNew(marker); + editFeatureShape.add(marker, editFeatureShapeMarkers); + updateEditState(true); } + } else { + vibrator.vibrate(getActivity().getResources().getInteger( + R.integer.edit_features_add_long_click_vibrate)); + Marker marker = addEditPoint(point); + if (editFeatureType == EditType.POLYGON_HOLE) { + editHolePoints.put(marker.getId(), marker); + } else { + editPoints.put(marker.getId(), marker); + } + updateEditState(true); } } } - - zoomToActive(); } /** - * Transform the bounding box in the spatial reference to a WGS84 bounding box + * Get the edit point marker options * - * @param boundingBox bounding box - * @param srs spatial reference system - * @return bounding box - */ - private BoundingBox transformBoundingBoxToWgs84(BoundingBox boundingBox, SpatialReferenceSystem srs) { - - mil.nga.proj.Projection projection = srs.getProjection(); - if (projection.isUnit(Units.DEGREES)) { - boundingBox = TileBoundingBoxUtils.boundDegreesBoundingBoxWithWebMercatorLimits(boundingBox); - } - ProjectionTransform transformToWebMercator = projection - .getTransformation( - ProjectionConstants.EPSG_WEB_MERCATOR); - BoundingBox webMercatorBoundingBox = boundingBox.transform(transformToWebMercator); - ProjectionTransform transform = ProjectionFactory.getProjection( - ProjectionConstants.EPSG_WEB_MERCATOR) - .getTransformation( - ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM); - boundingBox = webMercatorBoundingBox.transform(transform); - return boundingBox; - } - - /** - * Update the map in the background - */ - private class MapUpdateTask extends AsyncTask { - - /** - * {@inheritDoc} - */ - @Override - protected Void doInBackground(Object... params) { - boolean zoom = (Boolean) params[0]; - int maxFeatures = (Integer) params[1]; - BoundingBox mapViewBoundingBox = (BoundingBox) params[2]; - double toleranceDistance = (Double) params[3]; - boolean filter = (Boolean) params[4]; - update(this, zoom, maxFeatures, mapViewBoundingBox, toleranceDistance, filter); - if(getActivity() != null) { - getActivity().runOnUiThread(() -> basemapApplier.applyBasemaps(map)); - } - return null; - } - - } - - /** - * Update the map - * - * @param zoom The current zoom level. - * @param task The update task. - * @param maxFeatures The total number of max features allowed on map. - * @param mapViewBoundingBox The bounding box of the current view of the map. - * @param toleranceDistance Used to simplify any geometries being drawn on map. - * @param filter The filter if any. - */ - private void update(MapUpdateTask task, boolean zoom, final int maxFeatures, BoundingBox mapViewBoundingBox, double toleranceDistance, boolean filter) { - - if (active != null) { - - // Open active GeoPackages and create feature DAOS, display tiles and feature tiles - List activeDatabases = new ArrayList<>(active.getDatabases()); - for (GeoPackageDatabase database : activeDatabases) { - - if (task.isCancelled()) { - break; - } - - try { - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(database.getDatabase()); - - if (geoPackage != null) { - - Set featureTableDaos = new HashSet<>(); - Collection features = database.getFeatures(); - if (!features.isEmpty()) { - for (GeoPackageFeatureTable featureTable : features) { - featureTableDaos.add(featureTable.getName()); - } - } - - for (GeoPackageFeatureOverlayTable featureOverlay : database.getFeatureOverlays()) { - if (featureOverlay.isActive()) { - featureTableDaos.add(featureOverlay.getFeatureTable()); - } - } - - if (!featureTableDaos.isEmpty()) { - Map databaseFeatureDaos = new HashMap<>(); - featureDaos.put(database.getDatabase(), databaseFeatureDaos); - for (String featureTable : featureTableDaos) { - - if (task.isCancelled()) { - break; - } - - FeatureDao featureDao = geoPackage.getFeatureDao(featureTable); - databaseFeatureDaos.put(featureTable, featureDao); - } - } - - // Display the tiles - for (GeoPackageTileTable tiles : database.getTiles()) { - if (task.isCancelled()) { - break; - } - try { - displayTiles(tiles); - } catch (Exception e) { - Log.e(GeoPackageMapFragment.class.getSimpleName(), - e.getMessage()); - } - } - - // Display the feature tiles - for (GeoPackageFeatureOverlayTable featureOverlay : database.getFeatureOverlays()) { - if (task.isCancelled()) { - break; - } - if (featureOverlay.isActive()) { - try { - displayFeatureTiles(featureOverlay); - } catch (Exception e) { - Log.e(GeoPackageMapFragment.class.getSimpleName(), - e.getMessage()); - } - } - } - - } else { - active.removeDatabase(database.getDatabase(), false); - } - } catch (Exception e) { - Log.e(GeoPackageMapFragment.class.getSimpleName(), "Error opening geopackage: " + database.getDatabase(), e); - } - } - - // Add features - if (!task.isCancelled()) { - updateLock.lock(); - try { - if (updateFeaturesTask != null) { - updateFeaturesTask.cancel(false); - } - updateFeaturesTask = new MapFeaturesUpdateTask(); - updateFeaturesTask.execute(zoom, maxFeatures, mapViewBoundingBox, toleranceDistance, filter); - } finally { - updateLock.unlock(); - } - } - - } - - } - - /** - * Update the map features in the background - */ - private class MapFeaturesUpdateTask extends AsyncTask { - - /** - * Zoom after update flag - */ - private boolean zoom; - - /** - * {@inheritDoc} - */ - @Override - protected Integer doInBackground(Object... params) { - zoom = (Boolean) params[0]; - int maxFeatures = (Integer) params[1]; - BoundingBox mapViewBoundingBox = (BoundingBox) params[2]; - double toleranceDistance = (Double) params[3]; - boolean filter = (Boolean) params[4]; - return addFeatures(this, maxFeatures, mapViewBoundingBox, toleranceDistance, filter); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onProgressUpdate(Object... shapeUpdate) { - - long featureId = (Long) shapeUpdate[0]; - String database = (String) shapeUpdate[1]; - String tableName = (String) shapeUpdate[2]; - GoogleMapShape shape = (GoogleMapShape) shapeUpdate[3]; - - synchronized (featureShapes) { - - if (!featureShapes.exists(featureId, database, tableName)) { - - GoogleMapShape mapShape = GoogleMapShapeConverter.addShapeToMap( - map, shape); - - if (editFeaturesMode) { - Marker marker = addEditableShape(featureId, mapShape); - if (marker != null) { - GoogleMapShape mapPointShape = new GoogleMapShape(GeometryType.POINT, GoogleMapShapeType.MARKER, marker); - featureShapes.addMapMetadataShape(mapPointShape, featureId, database, tableName); - } - } else { - addMarkerShape(featureId, database, tableName, mapShape); - } - featureShapes.addMapShape(mapShape, featureId, database, tableName); - } - } - } - - /** - * {@inheritDoc} - */ - @Override - protected void onPostExecute(Integer count) { - - if (needsInitialZoom || zoom) { - zoomToActive(true); - needsInitialZoom = false; - } - } - - /** - * Add a shape to the map - * - * @param featureId The id of the feature. - * @param database The name of the geopackage. - * @param tableName The name of the layer. - * @param shape The type of shape to add. - */ - public void addToMap(long featureId, String database, String tableName, GoogleMapShape shape) { - publishProgress(featureId, database, tableName, shape); - } - - } - - /** - * Add features to the map - * - * @param task udpate features task - * @param maxFeatures max features - * @param mapViewBoundingBox map view bounding box - * @param toleranceDistance tolerance distance - * @param filter filter - * @return feature count - */ - private int addFeatures(MapFeaturesUpdateTask task, final int maxFeatures, BoundingBox mapViewBoundingBox, double toleranceDistance, boolean filter) { - - AtomicInteger count = new AtomicInteger(); - - Map> featureTables = new HashMap<>(); - if (editFeaturesMode) { - List databaseFeatures = new ArrayList<>(); - databaseFeatures.add(editFeaturesTable); - featureTables.put(editFeaturesDatabase, databaseFeatures); - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(editFeaturesDatabase); - Map databaseFeatureDaos = featureDaos.get(editFeaturesDatabase); - if (databaseFeatureDaos == null) { - databaseFeatureDaos = new HashMap<>(); - featureDaos.put(editFeaturesDatabase, databaseFeatureDaos); - } - FeatureDao featureDao = databaseFeatureDaos.get(editFeaturesTable); - if (featureDao == null) { - featureDao = geoPackage.getFeatureDao(editFeaturesTable); - databaseFeatureDaos.put(editFeaturesTable, featureDao); - } - } else { - for (GeoPackageDatabase database : active.getDatabases()) { - if (!database.getFeatures().isEmpty()) { - List databaseFeatures = new ArrayList<>(); - featureTables.put(database.getDatabase(), - databaseFeatures); - for (GeoPackageTable features : database.getFeatures()) { - databaseFeatures.add(features.getName()); - } - } - } - } - - for (Map.Entry> databaseFeaturesEntry : featureTables - .entrySet()) { - - if (count.get() >= maxFeatures) { - break; - } - - String databaseName = databaseFeaturesEntry.getKey(); - - List databaseFeatures = databaseFeaturesEntry.getValue(); - Map databaseFeatureDaos = featureDaos.get(databaseName); - - if (databaseFeatureDaos != null) { - - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(databaseName); - StyleCache styleCache = new StyleCache(geoPackage, getResources().getDisplayMetrics().density); - - for (String features : databaseFeatures) { - - if (databaseFeatureDaos.containsKey(features)) { - - displayFeatures(task, - geoPackage, styleCache, features, count, - maxFeatures, editFeaturesMode, mapViewBoundingBox, toleranceDistance, filter); - if (task.isCancelled() || count.get() >= maxFeatures) { - break; - } - } - } - - styleCache.clear(); - } - - if (task.isCancelled()) { - break; - } - } - - return Math.min(count.get(), maxFeatures); - } - - /** - * Zoom to features on the map, or tiles if no features - */ - private void zoomToActive() { - zoomToActive(false); - } - - /** - * Zoom to features on the map, or tiles if no features - * - * @param nothingVisible zoom only if nothing is currently visible - */ - private void zoomToActive(boolean nothingVisible) { - - BoundingBox bbox = featuresBoundingBox; - boolean tileBox = false; - - float paddingPercentage = 0f; - if(getActivity() != null) { - if (bbox == null) { - bbox = tilesBoundingBox; - tileBox = true; - if (featureOverlayTiles) { - paddingPercentage = getActivity().getResources().getInteger( - R.integer.map_feature_tiles_zoom_padding_percentage) * .01f; - } else { - paddingPercentage = getActivity().getResources().getInteger( - R.integer.map_tiles_zoom_padding_percentage) * .01f; - } - } else { - paddingPercentage = getActivity().getResources().getInteger( - R.integer.map_features_zoom_padding_percentage) * .01f; - } - } - - if (bbox != null) { - - boolean zoomToActive = true; - if (nothingVisible) { - BoundingBox mapViewBoundingBox = MapUtils.getBoundingBox(map); - if (TileBoundingBoxUtils.overlap(bbox, mapViewBoundingBox, ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH) != null) { - - double longitudeDistance = TileBoundingBoxMapUtils.getLongitudeDistance(bbox); - double latitudeDistance = TileBoundingBoxMapUtils.getLatitudeDistance(bbox); - double mapViewLongitudeDistance = TileBoundingBoxMapUtils.getLongitudeDistance(mapViewBoundingBox); - double mapViewLatitudeDistance = TileBoundingBoxMapUtils.getLatitudeDistance(mapViewBoundingBox); - - if (mapViewLongitudeDistance > longitudeDistance && mapViewLatitudeDistance > latitudeDistance) { - - double longitudeRatio = longitudeDistance / mapViewLongitudeDistance; - double latitudeRatio = latitudeDistance / mapViewLatitudeDistance; - - double zoomAlreadyVisiblePercentage; - if (tileBox) { - zoomAlreadyVisiblePercentage = getActivity().getResources().getInteger( - R.integer.map_tiles_zoom_already_visible_percentage) * .01f; - } else { - zoomAlreadyVisiblePercentage = getActivity().getResources().getInteger( - R.integer.map_features_zoom_already_visible_percentage) * .01f; - } - - if (longitudeRatio >= zoomAlreadyVisiblePercentage && latitudeRatio >= zoomAlreadyVisiblePercentage) { - zoomToActive = false; - } - } - } - } - - if (zoomToActive) { - double minLatitude = Math.max(bbox.getMinLatitude(), ProjectionConstants.WEB_MERCATOR_MIN_LAT_RANGE); - double maxLatitude = Math.min(bbox.getMaxLatitude(), ProjectionConstants.WEB_MERCATOR_MAX_LAT_RANGE); - - LatLng lowerLeft = new LatLng(minLatitude, bbox.getMinLongitude()); - LatLng lowerRight = new LatLng(minLatitude, bbox.getMaxLongitude()); - LatLng topLeft = new LatLng(maxLatitude, bbox.getMinLongitude()); - LatLng topRight = new LatLng(maxLatitude, bbox.getMaxLongitude()); - - if (lowerLeft.longitude == lowerRight.longitude) { - double adjustLongitude = lowerRight.longitude - .0000000000001; - lowerRight = new LatLng(minLatitude, adjustLongitude); - topRight = new LatLng(maxLatitude, adjustLongitude); - } - - final LatLngBounds.Builder boundsBuilder = new LatLngBounds.Builder(); - boundsBuilder.include(lowerLeft); - boundsBuilder.include(lowerRight); - boundsBuilder.include(topLeft); - boundsBuilder.include(topRight); - - View view = getView(); - int minViewLength = view != null ? Math.min(view.getWidth(), view.getHeight()) : 1; - final int padding = (int) Math.floor(minViewLength - * paddingPercentage); - - try { - map.animateCamera(CameraUpdateFactory.newLatLngBounds( - boundsBuilder.build(), padding)); - } catch (Exception e) { - Log.w(GeoPackageMapFragment.class.getSimpleName(), - "Unable to move camera", e); - } - } - } - } - - /** - * Display features - * - * @param task The update task. - * @param geoPackage The geopackage to display. - * @param styleCache the style cache. - * @param features The features. - * @param count The number of features. - * @param maxFeatures The maximum number of features the map will display. - * @param editable True if its editable. - * @param mapViewBoundingBox The views bounding box. - * @param toleranceDistance Used to simplify geometries for performance. - * @param filter True if features should be filtered. - */ - private void displayFeatures(MapFeaturesUpdateTask task, GeoPackage geoPackage, StyleCache styleCache, String features, - AtomicInteger count, final int maxFeatures, final boolean editable, - BoundingBox mapViewBoundingBox, double toleranceDistance, boolean filter) { - - // Get the GeoPackage and feature DAO - String database = geoPackage.getName(); - Map dataAccessObjects= featureDaos.get(database); - if(dataAccessObjects != null) { - FeatureDao featureDao = dataAccessObjects.get(features); - if(featureDao != null) { - GoogleMapShapeConverter converter = new GoogleMapShapeConverter(featureDao.getProjection()); - - converter.setSimplifyTolerance(toleranceDistance); - - if (!styleCache.getFeatureStyleExtension().has(features)) { - styleCache = null; - } - - count.getAndAdd(featureShapes.getFeatureIdsCount(database, features)); - - if (!task.isCancelled() && count.get() < maxFeatures) { - - mil.nga.proj.Projection mapViewProjection = ProjectionFactory.getProjection(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM); - - String[] columns = featureDao.getIdAndGeometryColumnNames(); - - FeatureIndexManager indexer = new FeatureIndexManager(getActivity(), geoPackage, featureDao); - if (filter && indexer.isIndexed()) { - - FeatureIndexResults indexResults = indexer.query(columns, mapViewBoundingBox, mapViewProjection); - BoundingBox complementary = mapViewBoundingBox.complementaryWgs84(); - if (complementary != null) { - FeatureIndexResults indexResults2 = indexer.query(columns, complementary, mapViewProjection); - indexResults = new MultipleFeatureIndexResults(indexResults, indexResults2); - } - - processFeatureIndexResults(task, indexResults, database, featureDao, converter, styleCache, - count, maxFeatures, editable, filter); - - } else { - - BoundingBox filterBoundingBox = null; - double filterMaxLongitude = 0; - - if (filter) { - mil.nga.proj.Projection featureProjection = featureDao.getProjection(); - ProjectionTransform projectionTransform = mapViewProjection.getTransformation(featureProjection); - BoundingBox boundedMapViewBoundingBox = mapViewBoundingBox.boundWgs84Coordinates(); - BoundingBox transformedBoundingBox = boundedMapViewBoundingBox.transform(projectionTransform); - if (featureProjection.isUnit(Units.DEGREES)) { - filterMaxLongitude = ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH; - } else if (featureProjection.isUnit(Units.METRES)) { - filterMaxLongitude = ProjectionConstants.WEB_MERCATOR_HALF_WORLD_WIDTH; - } - filterBoundingBox = transformedBoundingBox.expandCoordinates(filterMaxLongitude); - } - - // Query for all rows - try(FeatureCursor cursor = featureDao.query(columns)) { - while (!task.isCancelled() && count.get() < maxFeatures - && cursor.moveToNext()) { - try { - FeatureRow row = cursor.getRow(); - - // Process the feature row in the thread pool - FeatureRowProcessor processor = new FeatureRowProcessor( - task, database, featureDao, row, count, maxFeatures, editable, converter, - styleCache, filterBoundingBox, filterMaxLongitude, filter); - ThreadUtils.getInstance().runBackground(processor); - } catch (Exception e) { - Log.e(GeoPackageMapFragment.class.getSimpleName(), - "Failed to display feature. database: " + database - + ", feature table: " + features - + ", row: " + cursor.getPosition(), e); - } - } - - } - } - indexer.close(); - - } - } - } - } - - /** - * Process the feature index results - * - * @param task The feature update task. - * @param indexResults The index results. - * @param database The geoPackage to process features for. - * @param featureDao The feature data access object. - * @param converter Convert the features shapes to those that can go on a google map. - * @param styleCache The style cache. - * @param count Keeps track of how many features we have added to the map. - * @param maxFeatures The maximum number of features we can add to the map. - * @param editable True if the feature added to the map should look editable. - * @param filter True if we should filter the features based on a bounding box. - */ - private void processFeatureIndexResults(MapFeaturesUpdateTask task, FeatureIndexResults indexResults, String database, FeatureDao featureDao, - GoogleMapShapeConverter converter, StyleCache styleCache, AtomicInteger count, final int maxFeatures, final boolean editable, - boolean filter) { - - try { - for (FeatureRow row : indexResults) { - - if (task.isCancelled() || count.get() >= maxFeatures) { - break; - } - - try { - - // Process the feature row in the thread pool - FeatureRowProcessor processor = new FeatureRowProcessor( - task, database, featureDao, row, count, maxFeatures, editable, converter, - styleCache, null, 0, filter); - ThreadUtils.getInstance().runBackground(processor); - - } catch (Exception e) { - Log.e(GeoPackageMapFragment.class.getSimpleName(), - "Failed to display feature. database: " + database - + ", feature table: " + featureDao.getTableName() - + ", row id: " + row.getId(), e); - } - } - } finally { - indexResults.close(); - } - } - - /** - * Single feature row processor - * - * @author osbornb - */ - private class FeatureRowProcessor implements Runnable { - - /** - * Map update task - */ - private final MapFeaturesUpdateTask task; - - /** - * Database - */ - private final String database; - - /** - * Feature DAO - */ - private final FeatureDao featureDao; - - /** - * Feature row - */ - private final FeatureRow row; - - /** - * Total feature count - */ - private final AtomicInteger count; - - /** - * Total max features - */ - private final int maxFeatures; - - /** - * Editable shape flag - */ - private final boolean editable; - - /** - * Shape converter - */ - private final GoogleMapShapeConverter converter; - - /** - * Style Cache - */ - private final StyleCache styleCache; - - /** - * Filter bounding box - */ - private final BoundingBox filterBoundingBox; - - /** - * Max projection longitude - */ - private final double maxLongitude; - - /** - * Filter flag - */ - private final boolean filter; - - /** - * Constructor - * - * @param task The update task. - * @param database The name of the geopackage the features belong too. - * @param featureDao The feature data access object. - * @param row The row to process. - * @param count The current total count of features. - * @param maxFeatures The maximum features to display on the map. - * @param editable True if the feature should look editable on the map. - * @param converter Convertes the feature's shape to one to use on the map. - * @param styleCache The style cache. - * @param filterBoundingBox The bounding box to use for filtering. - * @param maxLongitude The maximum longitude. - * @param filter True if we should filter using the passed in bounding box. - */ - public FeatureRowProcessor(MapFeaturesUpdateTask task, String database, FeatureDao featureDao, - FeatureRow row, AtomicInteger count, int maxFeatures, - boolean editable, GoogleMapShapeConverter converter, StyleCache styleCache, - BoundingBox filterBoundingBox, double maxLongitude, boolean filter) { - this.task = task; - this.database = database; - this.featureDao = featureDao; - this.row = row; - this.count = count; - this.maxFeatures = maxFeatures; - this.editable = editable; - this.converter = converter; - this.styleCache = styleCache; - this.filterBoundingBox = filterBoundingBox; - this.maxLongitude = maxLongitude; - this.filter = filter; - } - - /** - * {@inheritDoc} - */ - @Override - public void run() { - processFeatureRow(task, database, featureDao, converter, styleCache, row, count, maxFeatures, - editable, filterBoundingBox, maxLongitude, filter); - } - - } - - /** - * Process the feature row - * - * @param task The map update task. - * @param database The geopackage name the feature row belongs too. - * @param featureDao The feature data access object. - * @param converter Converts the feature shap to one that can be used on a google map. - * @param styleCache The style cache. - * @param row The row to process. - * @param count The current feature count displayed on map. - * @param maxFeatures The maximum features to display on the map. - * @param editable True if the feature should look editable on the map. - * @param boundingBox The bounding box to use to filter features. - * @param maxLongitude The maximum longitude. - * @param filter True if we should filer using the bounding box. - */ - private void processFeatureRow(MapFeaturesUpdateTask task, String database, FeatureDao featureDao, - GoogleMapShapeConverter converter, StyleCache styleCache, FeatureRow row, AtomicInteger count, - int maxFeatures, boolean editable, BoundingBox boundingBox, double maxLongitude, - boolean filter) { - - boolean exists; - synchronized (featureShapes) { - exists = featureShapes.exists(row.getId(), database, featureDao.getTableName()); - } - - if (!exists) { - - try { - GeoPackageGeometryData geometryData = row.getGeometry(); - if (geometryData != null && !geometryData.isEmpty()) { - - final Geometry geometry = geometryData.getGeometry(); - - if (geometry != null) { - - boolean passesFilter = true; - - if (filter && boundingBox != null) { - GeometryEnvelope envelope = geometryData.getEnvelope(); - if (envelope == null) { - envelope = GeometryEnvelopeBuilder.buildEnvelope(geometry); - } - if (envelope != null) { - if (geometry.getGeometryType() == GeometryType.POINT) { - mil.nga.sf.Point point = (mil.nga.sf.Point) geometry; - passesFilter = TileBoundingBoxUtils.isPointInBoundingBox(point, boundingBox, maxLongitude); - } else { - BoundingBox geometryBoundingBox = new BoundingBox(envelope); - passesFilter = TileBoundingBoxUtils.overlap(boundingBox, geometryBoundingBox, maxLongitude) != null; - } - } - } - - if (passesFilter && count.getAndIncrement() < maxFeatures) { - final long featureId = row.getId(); - final GoogleMapShape shape = converter.toShape(geometry); - updateFeaturesBoundingBox(shape); - prepareShapeOptions(shape, styleCache, row, editable, true); - task.addToMap(featureId, database, featureDao.getTableName(), shape); - } - } - } - } catch(Exception e){ - new Handler(Looper.getMainLooper()).post(() -> { - Toast toast = Toast.makeText(getContext(), "Error loading geometry", Toast.LENGTH_SHORT); - toast.show(); - }); - } - } - } - - /** - * Update the features bounding box with the shape - * - * @param shape The shape to use to expand the features bounding box. - */ - private void updateFeaturesBoundingBox(GoogleMapShape shape) { - try { - featuresBoundingBoxLock.lock(); - if (featuresBoundingBox != null) { - shape.expandBoundingBox(featuresBoundingBox); - } else { - featuresBoundingBox = shape.boundingBox(); - } - } finally { - featuresBoundingBoxLock.unlock(); - } - } - - /** - * Prepare the shape options - * - * @param shape map shape - * @param styleCache style cache - * @param featureRow feature row - * @param editable editable flag - * @param topLevel top level flag - */ - private void prepareShapeOptions(GoogleMapShape shape, StyleCache styleCache, FeatureRow featureRow, boolean editable, - boolean topLevel) { - - FeatureStyle featureStyle = null; - if (styleCache != null) { - featureStyle = styleCache.getFeatureStyleExtension().getFeatureStyle(featureRow, shape.getGeometryType()); - } - - switch (shape.getShapeType()) { - - case LAT_LNG: - LatLng latLng = (LatLng) shape.getShape(); - MarkerOptions markerOptions = getMarkerOptions(styleCache, featureStyle, editable, topLevel); - markerOptions.position(latLng); - shape.setShape(markerOptions); - shape.setShapeType(GoogleMapShapeType.MARKER_OPTIONS); - break; - - case POLYLINE_OPTIONS: - PolylineOptions polylineOptions = (PolylineOptions) shape - .getShape(); - setPolylineOptions(styleCache, featureStyle, editable, polylineOptions); - break; - - case POLYGON_OPTIONS: - PolygonOptions polygonOptions = (PolygonOptions) shape.getShape(); - setPolygonOptions(styleCache, featureStyle, editable, polygonOptions); - break; - - case MULTI_LAT_LNG: - MultiLatLng multiLatLng = (MultiLatLng) shape.getShape(); - MarkerOptions sharedMarkerOptions = getMarkerOptions(styleCache, featureStyle, editable, - false); - multiLatLng.setMarkerOptions(sharedMarkerOptions); - break; - - case MULTI_POLYLINE_OPTIONS: - MultiPolylineOptions multiPolylineOptions = (MultiPolylineOptions) shape - .getShape(); - PolylineOptions sharedPolylineOptions = new PolylineOptions(); - setPolylineOptions(styleCache, featureStyle, editable, sharedPolylineOptions); - multiPolylineOptions.setOptions(sharedPolylineOptions); - break; - - case MULTI_POLYGON_OPTIONS: - MultiPolygonOptions multiPolygonOptions = (MultiPolygonOptions) shape - .getShape(); - PolygonOptions sharedPolygonOptions = new PolygonOptions(); - setPolygonOptions(styleCache, featureStyle, editable, sharedPolygonOptions); - multiPolygonOptions.setOptions(sharedPolygonOptions); - break; - - case COLLECTION: - @SuppressWarnings("unchecked") - List shapes = (List) shape - .getShape(); - for (int i = 0; i < shapes.size(); i++) { - prepareShapeOptions(shapes.get(i), styleCache, featureRow, editable, false); - } - break; - default: - } - - } - - /** - * Get marker options - * - * @param styleCache style cache - * @param featureStyle feature style - * @param editable editable flag - * @param clickable clickable flag - * @return marker options - */ - private MarkerOptions getMarkerOptions(StyleCache styleCache, FeatureStyle featureStyle, boolean editable, boolean clickable) { - MarkerOptions markerOptions = new MarkerOptions(); - if (editable) { - TypedValue typedValue = new TypedValue(); - if (clickable) { - getResources().getValue(R.dimen.marker_edit_color, typedValue, - true); - } else { - getResources().getValue(R.dimen.marker_edit_read_only_color, - typedValue, true); - } - markerOptions.icon(BitmapDescriptorFactory.defaultMarker(typedValue - .getFloat())); - - } else if (styleCache == null || !styleCache.setFeatureStyle(markerOptions, featureStyle)) { - - TypedValue typedValue = new TypedValue(); - getResources().getValue(R.dimen.marker_color, typedValue, true); - markerOptions.icon(BitmapDescriptorFactory.defaultMarker(typedValue.getFloat())); - } - - return markerOptions; - } - - /** - * Set the Polyline Option attributes - * - * @param styleCache style cache - * @param featureStyle feature style - * @param editable editable flag - * @param polylineOptions polyline options - */ - private void setPolylineOptions(StyleCache styleCache, FeatureStyle featureStyle, boolean editable, - PolylineOptions polylineOptions) { - if(getActivity() != null) { - if (editable) { - polylineOptions.color(ContextCompat.getColor(getActivity(), R.color.polyline_edit_color)); - } else if (styleCache == null || !styleCache.setFeatureStyle(polylineOptions, featureStyle)) { - polylineOptions.color(ContextCompat.getColor(getActivity(), R.color.polyline_color)); - } - } - } - - /** - * Set the Polygon Option attributes - * - * @param styleCache style cache - * @param featureStyle feature style - * @param editable True if it should be displayed as editable. - * @param polygonOptions The polygon options to set. - */ - private void setPolygonOptions(StyleCache styleCache, FeatureStyle featureStyle, boolean editable, - PolygonOptions polygonOptions) { - if(getActivity() != null) { - if (editable) { - polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.polygon_edit_color)); - polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.polygon_edit_fill_color)); - } else if (styleCache == null || !styleCache.setFeatureStyle(polygonOptions, featureStyle)) { - polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.polygon_color)); - polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.polygon_fill_color)); - } - } - } - - /** - * Add editable shape - * - * @param featureId The id of the feature. - * @param shape The shape to add. - * @return marker The google map marker to add. - */ - private Marker addEditableShape(long featureId, GoogleMapShape shape) { - - Marker marker; - - if (shape.getShapeType() == GoogleMapShapeType.MARKER) { - marker = (Marker) shape.getShape(); - } else { - marker = getMarker(shape); - if (marker != null) { - editFeatureObjects.put(marker.getId(), shape); - } - } - - if (marker != null) { - editFeatureIds.put(marker.getId(), featureId); - } - - return marker; - } - - /** - * Add marker shape - * - * @param featureId The id of the feature. - * @param database The name of the geopackage the feature belongs to. - * @param tableName The name of the layer the feature belongs to. - * @param shape The shape to add. - */ - private void addMarkerShape(long featureId, String database, String tableName, GoogleMapShape shape) { - - if (shape.getShapeType() == GoogleMapShapeType.MARKER) { - Marker marker = (Marker) shape.getShape(); - MarkerFeature markerFeature = new MarkerFeature(featureId, database, tableName); - markerIds.put(marker.getId(), markerFeature); - } - } - - /** - * Get the first marker of the shape or create one at the location - * - * @param shape The shape to get the marker for. - * @return The marker to add to the map. - */ - private Marker getMarker(GoogleMapShape shape) { - - Marker marker = null; - - switch (shape.getShapeType()) { - - case MARKER: - Marker shapeMarker = (Marker) shape.getShape(); - marker = createEditMarker(shapeMarker.getPosition()); - break; - - case POLYLINE: - Polyline polyline = (Polyline) shape.getShape(); - LatLng polylinePoint = polyline.getPoints().get(0); - marker = createEditMarker(polylinePoint); - break; - - case POLYGON: - Polygon polygon = (Polygon) shape.getShape(); - LatLng polygonPoint = polygon.getPoints().get(0); - marker = createEditMarker(polygonPoint); - break; - - case MULTI_MARKER: - MultiMarker multiMarker = (MultiMarker) shape.getShape(); - marker = createEditMarker(multiMarker.getMarkers().get(0) - .getPosition()); - break; - - case MULTI_POLYLINE: - MultiPolyline multiPolyline = (MultiPolyline) shape.getShape(); - LatLng multiPolylinePoint = multiPolyline.getPolylines().get(0) - .getPoints().get(0); - marker = createEditMarker(multiPolylinePoint); - break; - - case MULTI_POLYGON: - MultiPolygon multiPolygon = (MultiPolygon) shape.getShape(); - LatLng multiPolygonPoint = multiPolygon.getPolygons().get(0) - .getPoints().get(0); - marker = createEditMarker(multiPolygonPoint); - break; - - case COLLECTION: - @SuppressWarnings("unchecked") - List shapes = (List) shape - .getShape(); - for (GoogleMapShape listShape : shapes) { - marker = getMarker(listShape); - if (marker != null) { - break; - } - } - break; - default: - } - - return marker; - } - - /** - * Create an edit marker to edit polylines and polygons - * - * @param latLng The latitude and longitude of the markers location. - * @return The marker to add to the map. - */ - private Marker createEditMarker(LatLng latLng) { - MarkerOptions markerOptions = new MarkerOptions(); - markerOptions.position(latLng); - markerOptions.icon(BitmapDescriptorFactory - .fromResource(R.drawable.ic_shape_edit)); - TypedValue typedValueWidth = new TypedValue(); - getResources().getValue(R.dimen.shape_edit_icon_anchor_width, - typedValueWidth, true); - TypedValue typedValueHeight = new TypedValue(); - getResources().getValue(R.dimen.shape_edit_icon_anchor_height, - typedValueHeight, true); - markerOptions.anchor(typedValueWidth.getFloat(), - typedValueHeight.getFloat()); - return map.addMarker(markerOptions); - } - - /** - * Display tiles - * - * @param tiles The tiles to display. - */ - private void displayTiles(GeoPackageTileTable tiles) { - - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(tiles.getDatabase()); - - TileDao tileDao = geoPackage.getTileDao(tiles.getName()); - - TileTableScaling tileTableScaling = new TileTableScaling(geoPackage, tileDao); - TileScaling tileScaling = tileTableScaling.get(); - - BoundedOverlay overlay = GeoPackageOverlayFactory - .getBoundedOverlay(tileDao, getResources().getDisplayMetrics().density, tileScaling); - - TileMatrixSet tileMatrixSet = tileDao.getTileMatrixSet(); - - FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage); - List featureDaos = linker.getFeatureDaosForTileTable(tileDao.getTableName()); - - if(getActivity() != null) { - for (FeatureDao featureDao : featureDaos) { - - // Create the feature tiles - FeatureTiles featureTiles = new DefaultFeatureTiles(getActivity(), geoPackage, featureDao, - getResources().getDisplayMetrics().density); - - featureOverlayTiles = true; - - // Add the feature overlay query - FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(getActivity(), overlay, featureTiles); - featureOverlayQuery.calculateStylePixelBounds(); - featureOverlayQueries.add(featureOverlayQuery); - } - } - - // Set the tiles index to be -2 of it is behind features and tiles drawn from features - int zIndex = -2; - - // If these tiles are linked to features, set the zIndex to -1 so they are placed before imagery tiles - if (!featureDaos.isEmpty()) { - zIndex = -1; - } - - BoundingBox displayBoundingBox = tileMatrixSet.getBoundingBox(); - Contents contents = tileMatrixSet.getContents(); - BoundingBox contentsBoundingBox = contents.getBoundingBox(); - if (contentsBoundingBox != null) { - ProjectionTransform transform = contents.getSrs().getProjection().getTransformation(tileMatrixSet.getSrs().getProjection()); - BoundingBox transformedContentsBoundingBox = contentsBoundingBox; - if (!transform.isSameProjection()) { - transformedContentsBoundingBox = transformedContentsBoundingBox.transform(transform); - } - displayBoundingBox = displayBoundingBox.overlap(transformedContentsBoundingBox); - } - - displayTiles(overlay, displayBoundingBox, tileMatrixSet.getSrs(), zIndex, null); - } - - /** - * Display feature tiles - * - * @param featureOverlayTable The overlay table to display. - */ - private void displayFeatureTiles(GeoPackageFeatureOverlayTable featureOverlayTable) { - - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(featureOverlayTable.getDatabase()); - Map daos = featureDaos.get(featureOverlayTable.getDatabase()); - if(daos != null && getActivity() != null) { - FeatureDao featureDao = daos.get(featureOverlayTable.getFeatureTable()); - - BoundingBox boundingBox = new BoundingBox(featureOverlayTable.getMinLon(), - featureOverlayTable.getMinLat(), featureOverlayTable.getMaxLon(), featureOverlayTable.getMaxLat()); - - // Load tiles - FeatureTiles featureTiles = new DefaultFeatureTiles(getActivity(), geoPackage, featureDao, - getResources().getDisplayMetrics().density); - if (featureOverlayTable.isIgnoreGeoPackageStyles()) { - featureTiles.ignoreFeatureTableStyles(); - } - - featureTiles.setMaxFeaturesPerTile(featureOverlayTable.getMaxFeaturesPerTile()); - if (featureOverlayTable.getMaxFeaturesPerTile() != null) { - featureTiles.setMaxFeaturesTileDraw(new NumberFeaturesTile(getActivity())); - } - - Paint pointPaint = featureTiles.getPointPaint(); - pointPaint.setColor(Color.parseColor(featureOverlayTable.getPointColor())); - - pointPaint.setAlpha(featureOverlayTable.getPointAlpha()); - featureTiles.setPointRadius(featureOverlayTable.getPointRadius()); - - Paint linePaint = featureTiles.getLinePaintCopy(); - linePaint.setColor(Color.parseColor(featureOverlayTable.getLineColor())); - - linePaint.setAlpha(featureOverlayTable.getLineAlpha()); - linePaint.setStrokeWidth(featureOverlayTable.getLineStrokeWidth()); - featureTiles.setLinePaint(linePaint); - - Paint polygonPaint = featureTiles.getPolygonPaintCopy(); - polygonPaint.setColor(Color.parseColor(featureOverlayTable.getPolygonColor())); - - polygonPaint.setAlpha(featureOverlayTable.getPolygonAlpha()); - polygonPaint.setStrokeWidth(featureOverlayTable.getPolygonStrokeWidth()); - featureTiles.setPolygonPaint(polygonPaint); - - featureTiles.setFillPolygon(featureOverlayTable.isPolygonFill()); - if (featureTiles.isFillPolygon()) { - Paint polygonFillPaint = featureTiles.getPolygonFillPaintCopy(); - polygonFillPaint.setColor(Color.parseColor(featureOverlayTable.getPolygonFillColor())); - - polygonFillPaint.setAlpha(featureOverlayTable.getPolygonFillAlpha()); - featureTiles.setPolygonFillPaint(polygonFillPaint); - } - - featureTiles.calculateDrawOverlap(); - - FeatureOverlay featureOverlay = new FeatureOverlay(featureTiles); - featureOverlay.setBoundingBox(boundingBox, ProjectionFactory.getProjection(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM)); - featureOverlay.setMinZoom(featureOverlayTable.getMinZoom()); - featureOverlay.setMaxZoom(featureOverlayTable.getMaxZoom()); - - // Get the tile linked overlay - BoundedOverlay overlay = GeoPackageOverlayFactory.getLinkedFeatureOverlay(featureOverlay, geoPackage); - - if(featureDao != null) { - GeometryColumns geometryColumns = featureDao.getGeometryColumns(); - Contents contents = geometryColumns.getContents(); - - GeoPackageUtils.prepareFeatureTiles(featureTiles); - - featureOverlayTiles = true; - - FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(getActivity(), overlay, featureTiles); - featureOverlayQuery.calculateStylePixelBounds(); - featureOverlayQueries.add(featureOverlayQuery); - - displayTiles(overlay, contents.getBoundingBox(), contents.getSrs(), -1, boundingBox); - } - } - } - - /** - * Display tiles - * - * @param overlay The tile overlay. - * @param dataBoundingBox The bounding box of the data. - * @param srs The spatial reference system of the tiles. - * @param zIndex The zoom level. - * @param specifiedBoundingBox The specified bounding box. - */ - private void displayTiles(TileProvider overlay, BoundingBox dataBoundingBox, SpatialReferenceSystem srs, int zIndex, BoundingBox specifiedBoundingBox) { - - final TileOverlayOptions overlayOptions = new TileOverlayOptions(); - overlayOptions.tileProvider(overlay); - overlayOptions.zIndex(zIndex); - - BoundingBox boundingBox = dataBoundingBox; - if (boundingBox != null) { - boundingBox = transformBoundingBoxToWgs84(boundingBox, srs); - } else { - boundingBox = new BoundingBox(-ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH, - ProjectionConstants.WEB_MERCATOR_MIN_LAT_RANGE, - ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH, - ProjectionConstants.WEB_MERCATOR_MAX_LAT_RANGE); - } - - if (specifiedBoundingBox != null) { - boundingBox = boundingBox.overlap(specifiedBoundingBox); - } - - if (tilesBoundingBox == null) { - tilesBoundingBox = boundingBox; - } else { - tilesBoundingBox = tilesBoundingBox.union(boundingBox); - } - - if(getActivity() != null) { - getActivity().runOnUiThread(() -> map.addTileOverlay(overlayOptions)); - } - } - - /** - * Draw a bounding box with boundingBoxStartCorner and boundingBoxEndCorner - */ - public boolean drawBoundingBox() { - PolygonOptions polygonOptions = new PolygonOptions(); - - if(getActivity() != null) { - polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_color)); - polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_fill_color)); - } - - List points = getPolygonPoints(boundingBoxStartCorner, - boundingBoxEndCorner); - polygonOptions.addAll(points); - boundingBox = map.addPolygon(polygonOptions); - setDrawing(true); - return true; - } - - /** - * {@inheritDoc} - */ - @Override - public void onMapLongClick(@NonNull LatLng point) { - if(getActivity() != null) { - if (boundingBoxMode) { - - vibrator.vibrate(getActivity().getResources().getInteger( - R.integer.map_tiles_long_click_vibrate)); - - // Check to see if editing any of the bounding box corners - if (boundingBox != null && boundingBoxEndCorner != null) { - Projection projection = map.getProjection(); - - double allowableScreenPercentage = (getActivity() - .getResources() - .getInteger( - R.integer.map_tiles_long_click_screen_percentage) / 100.0); - Point screenPoint = projection.toScreenLocation(point); - - if (isWithinDistance(projection, screenPoint, - boundingBoxEndCorner, allowableScreenPercentage)) { - setDrawing(true); - } else if (isWithinDistance(projection, screenPoint, - boundingBoxStartCorner, allowableScreenPercentage)) { - LatLng temp = boundingBoxStartCorner; - boundingBoxStartCorner = boundingBoxEndCorner; - boundingBoxEndCorner = temp; - setDrawing(true); - } else { - LatLng corner1 = new LatLng( - boundingBoxStartCorner.latitude, - boundingBoxEndCorner.longitude); - LatLng corner2 = new LatLng(boundingBoxEndCorner.latitude, - boundingBoxStartCorner.longitude); - if (isWithinDistance(projection, screenPoint, corner1, - allowableScreenPercentage)) { - boundingBoxStartCorner = corner2; - boundingBoxEndCorner = corner1; - setDrawing(true); - } else if (isWithinDistance(projection, screenPoint, - corner2, allowableScreenPercentage)) { - boundingBoxStartCorner = corner1; - boundingBoxEndCorner = corner2; - setDrawing(true); - } - } - } - - // Start drawing a new polygon - if (!drawing) { - if (boundingBox != null) { - boundingBox.remove(); - } - boundingBoxStartCorner = point; - boundingBoxEndCorner = point; - PolygonOptions polygonOptions = new PolygonOptions(); - polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_color)); - polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.bounding_box_draw_fill_color)); - List points = getPolygonPoints(boundingBoxStartCorner, - boundingBoxEndCorner); - polygonOptions.addAll(points); - boundingBox = map.addPolygon(polygonOptions); - setDrawing(true); - } - } else if (editFeatureType != null) { - if (editFeatureType == EditType.EDIT_FEATURE) { - if (editFeatureShapeMarkers != null) { - vibrator.vibrate(getActivity().getResources().getInteger( - R.integer.edit_features_add_long_click_vibrate)); - Marker marker = addEditPoint(point); - editFeatureShapeMarkers.addNew(marker); - editFeatureShape.add(marker, editFeatureShapeMarkers); - updateEditState(true); - } - } else { - vibrator.vibrate(getActivity().getResources().getInteger( - R.integer.edit_features_add_long_click_vibrate)); - Marker marker = addEditPoint(point); - if (editFeatureType == EditType.POLYGON_HOLE) { - editHolePoints.put(marker.getId(), marker); - } else { - editPoints.put(marker.getId(), marker); - } - updateEditState(true); - } - } - } - } - - /** - * Get the edit point marker options - * - * @param point The location of the point. - * @return The marker to be put on the map. + * @param point The location of the point. + * @return The marker to be put on the map. */ private Marker addEditPoint(LatLng point) { MarkerOptions markerOptions = new MarkerOptions(); @@ -4935,7 +3483,8 @@ private void setEditPointShapeHoleOptions(MarkerOptions markerOptions) { /** * Update the current edit state, buttons, and visuals * - * @param updateAcceptClear + * @param updateAcceptClear True if the accept and clear buttons active appearance should be + * updated. */ private void updateEditState(boolean updateAcceptClear) { boolean accept = false; @@ -5055,46 +3604,52 @@ private void updateEditState(boolean updateAcceptClear) { /** * Get draw polyline options * - * @return + * @return The polyline options. */ private PolylineOptions getDrawPolylineOptions() { PolylineOptions polylineOptions = new PolylineOptions(); - polylineOptions.color(ContextCompat.getColor(getActivity(), R.color.polyline_draw_color)); + if (this.getActivity() != null) { + polylineOptions.color(ContextCompat.getColor(getActivity(), R.color.polyline_draw_color)); + } return polylineOptions; } /** * Get draw polygon options * - * @return + * @return The polygon options. */ private PolygonOptions getDrawPolygonOptions() { PolygonOptions polygonOptions = new PolygonOptions(); - polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.polygon_draw_color)); - polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.polygon_draw_fill_color)); + if (this.getActivity() != null) { + polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.polygon_draw_color)); + polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.polygon_draw_fill_color)); + } return polygonOptions; } /** * Get hold draw polygon options * - * @return + * @return The polygon options. */ private PolygonOptions getHoleDrawPolygonOptions() { PolygonOptions polygonOptions = new PolygonOptions(); - polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.polygon_hole_draw_color)); - polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.polygon_hole_draw_fill_color)); + if (this.getActivity() != null) { + polygonOptions.strokeColor(ContextCompat.getColor(getActivity(), R.color.polygon_hole_draw_color)); + polygonOptions.fillColor(ContextCompat.getColor(getActivity(), R.color.polygon_hole_draw_fill_color)); + } return polygonOptions; } /** * Get a list of points as LatLng * - * @param markers - * @return + * @param markers The markers to collect points from. + * @return The list of points of the marker locations. */ private List getLatLngPoints(Map markers) { - List points = new ArrayList(); + List points = new ArrayList<>(); for (Marker editPoint : markers.values()) { points.add(editPoint.getPosition()); } @@ -5104,7 +3659,7 @@ private List getLatLngPoints(Map markers) { /** * Set the drawing value * - * @param drawing + * @param drawing True if drawing false if not. */ private void setDrawing(boolean drawing) { this.drawing = drawing; @@ -5114,11 +3669,12 @@ private void setDrawing(boolean drawing) { /** * Check if the point is within clicking distance to the lat lng corner * - * @param projection - * @param point - * @param latLng - * @param allowableScreenPercentage - * @return + * @param projection The projection to use. + * @param point The point to check. + * @param latLng The corner to check. + * @param allowableScreenPercentage The percentage of the screen distance the point and corner + * must be within. + * @return True if the point an corner are within the specified screen distance percentage. */ private boolean isWithinDistance(Projection projection, Point point, LatLng latLng, double allowableScreenPercentage) { @@ -5126,23 +3682,21 @@ private boolean isWithinDistance(Projection projection, Point point, double distance = Math.sqrt(Math.pow(point.x - point2.x, 2) + Math.pow(point.y - point2.y, 2)); - boolean withinDistance = distance - / Math.min(view.getWidth(), view.getHeight()) <= allowableScreenPercentage; - return withinDistance; + return distance / Math.min(view.getWidth(), view.getHeight()) <= allowableScreenPercentage; } /** * {@inheritDoc} */ @Override - public void onMapClick(LatLng point) { + public void onMapClick(@NonNull LatLng point) { - if (!editFeaturesMode) { + if (!model.isEditFeaturesMode()) { StringBuilder clickMessage = new StringBuilder(); - if (!featureOverlayQueries.isEmpty()) { - for (FeatureOverlayQuery query : featureOverlayQueries) { + if (!model.getFeatureOverlayQueries().isEmpty()) { + for (FeatureOverlayQuery query : model.getFeatureOverlayQueries()) { String message = query.buildMapClickMessage(point, view, map); if (message != null) { if (clickMessage.length() > 0) { @@ -5153,7 +3707,7 @@ public void onMapClick(LatLng point) { } } - for (GeoPackageDatabase database : active.getDatabases()) { + for (GeoPackageDatabase database : model.getActive().getDatabases()) { if (!database.getFeatures().isEmpty()) { TypedValue screenPercentage = new TypedValue(); @@ -5169,7 +3723,7 @@ public void onMapClick(LatLng point) { for (GeoPackageTable features : database.getFeatures()) { GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(database.getDatabase()); - Map databaseFeatureDaos = featureDaos.get(database.getDatabase()); + Map databaseFeatureDaos = model.getFeatureDaos().get(database.getDatabase()); if (geoPackage != null && databaseFeatureDaos != null) { @@ -5177,7 +3731,7 @@ public void onMapClick(LatLng point) { if (featureDao != null) { - FeatureIndexResults indexResults = null; + FeatureIndexResults indexResults; FeatureIndexManager indexer = new FeatureIndexManager(getActivity(), geoPackage, featureDao); if (indexer.isIndexed()) { @@ -5205,8 +3759,7 @@ public void onMapClick(LatLng point) { FeatureIndexListResults listResults = new FeatureIndexListResults(); // Query for all rows - FeatureCursor cursor = featureDao.query(); - try { + try (FeatureCursor cursor = featureDao.query()) { while (cursor.moveToNext()) { @@ -5243,15 +3796,13 @@ public void onMapClick(LatLng point) { } } - } finally { - cursor.close(); } indexResults = listResults; } indexer.close(); - if (indexResults.count() > 0) { + if (indexResults.count() > 0 && this.getActivity() != null) { FeatureInfoBuilder featureInfoBuilder = new FeatureInfoBuilder(getActivity(), featureDao); featureInfoBuilder.ignoreGeometryType(GeometryType.POINT); String message = featureInfoBuilder.buildResultsInfoMessageAndClose(indexResults, tolerance, point); @@ -5269,15 +3820,12 @@ public void onMapClick(LatLng point) { } } - if (clickMessage.length() > 0) { + if (clickMessage.length() > 0 && this.getActivity() != null) { new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) .setMessage(clickMessage.toString()) .setPositiveButton(android.R.string.yes, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - } - } - ) + (DialogInterface dialog, int which) -> { + }) .show(); } } @@ -5292,7 +3840,7 @@ public boolean onMarkerClick(Marker marker) { String markerId = marker.getId(); - if (editFeaturesMode) { + if (model.isEditFeaturesMode()) { // Handle clicks to edit contents of an existing feature if (editFeatureShape != null && editFeatureShape.contains(markerId)) { @@ -5301,7 +3849,7 @@ public boolean onMarkerClick(Marker marker) { } // Handle clicks on an existing feature in edit mode - Long featureId = editFeatureIds.get(markerId); + Long featureId = model.getEditFeatureIds().get(markerId); if (featureId != null) { editExistingFeatureClick(marker, featureId); return true; @@ -5323,9 +3871,9 @@ public boolean onMarkerClick(Marker marker) { } else { // Handle clicks on point markers - MarkerFeature markerFeature = markerIds.get(markerId); + MarkerFeature markerFeature = model.getMarkerIds().get(markerId); if (markerFeature != null) { - infoFeatureClick(marker, markerFeature); + infoFeatureClick(markerFeature); return true; } } @@ -5336,7 +3884,7 @@ public boolean onMarkerClick(Marker marker) { * {@inheritDoc} */ @Override - public void onMarkerDrag(Marker marker) { + public void onMarkerDrag(@NonNull Marker marker) { updateEditState(false); } @@ -5344,7 +3892,7 @@ public void onMarkerDrag(Marker marker) { * {@inheritDoc} */ @Override - public void onMarkerDragEnd(Marker marker) { + public void onMarkerDragEnd(@NonNull Marker marker) { updateEditState(false); } @@ -5352,23 +3900,25 @@ public void onMarkerDragEnd(Marker marker) { * {@inheritDoc} */ @Override - public void onMarkerDragStart(Marker marker) { - vibrator.vibrate(getActivity().getResources().getInteger( - R.integer.edit_features_drag_long_click_vibrate)); + public void onMarkerDragStart(@NonNull Marker marker) { + if (getActivity() != null) { + vibrator.vibrate(getActivity().getResources().getInteger( + R.integer.edit_features_drag_long_click_vibrate)); + } } /** * Edit feature shape marker click * - * @param marker + * @param marker The marker to edit. */ private void editFeatureShapeClick(final Marker marker) { final ShapeMarkers shapeMarkers = editFeatureShape .getShapeMarkers(marker); - if (shapeMarkers != null) { + if (shapeMarkers != null && getActivity() != null) { - ArrayAdapter adapter = new ArrayAdapter( + ArrayAdapter adapter = new ArrayAdapter<>( getActivity(), android.R.layout.select_dialog_item); adapter.add(getString(R.string.edit_features_shape_point_delete_label)); adapter.add(getString(R.string.edit_features_shape_add_points_label)); @@ -5382,25 +3932,25 @@ private void editFeatureShapeClick(final Marker marker) { final String title = "(lat=" + formatter.format(position.latitude) + ", lon=" + formatter.format(position.longitude) + ")"; builder.setTitle(title); - builder.setAdapter(adapter, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int item) { + builder.setAdapter(adapter, (DialogInterface dialog, int item) -> { - if (item >= 0) { - switch (item) { - case 0: - editFeatureShape.delete(marker); - updateEditState(true); - break; - case 1: - editFeatureShapeMarkers = shapeMarkers; - break; - case 2: + if (item >= 0) { + switch (item) { + case 0: + editFeatureShape.delete(marker); + updateEditState(true); + break; + case 1: + editFeatureShapeMarkers = shapeMarkers; + break; + case 2: + if (shapeMarkers instanceof ShapeWithChildrenMarkers) { ShapeWithChildrenMarkers shapeWithChildrenMarkers = (ShapeWithChildrenMarkers) shapeMarkers; editFeatureShapeMarkers = shapeWithChildrenMarkers .createChild(); - break; - default: - } + } + break; + default: } } }); @@ -5413,70 +3963,61 @@ public void onClick(DialogInterface dialog, int item) { /** * Edit marker click * - * @param marker - * @param points + * @param marker The marker to edit. + * @param points The points to edit. */ private void editMarkerClick(final Marker marker, final Map points) { - LatLng position = marker.getPosition(); - String message = editFeatureType.name(); - if (editFeatureType != EditType.POINT) { - message += " " + EditType.POINT.name(); - } - AlertDialog deleteDialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) - .setCancelable(false) - .setTitle(getString(R.string.edit_features_delete_label)) - .setMessage( - getString(R.string.edit_features_delete_label) + " " - + message + " (lat=" + position.latitude - + ", lon=" + position.longitude + ") ?") - .setPositiveButton( - getString(R.string.edit_features_delete_label), - - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { + if (getActivity() != null) { + LatLng position = marker.getPosition(); + String message = editFeatureType.name(); + if (editFeatureType != EditType.POINT) { + message += " " + EditType.POINT.name(); + } + AlertDialog deleteDialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) + .setCancelable(false) + .setTitle(getString(R.string.edit_features_delete_label)) + .setMessage( + getString(R.string.edit_features_delete_label) + " " + + message + " (lat=" + position.latitude + + ", lon=" + position.longitude + ") ?") + .setPositiveButton( + getString(R.string.edit_features_delete_label), + + (DialogInterface dialog, int which) -> { points.remove(marker.getId()); marker.remove(); updateEditState(true); + }) - } - }) - - .setNegativeButton(getString(R.string.button_cancel_label), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - dialog.dismiss(); - } - }).create(); - deleteDialog.show(); + .setNegativeButton(getString(R.string.button_cancel_label), + (DialogInterface dialog, int which) -> dialog.dismiss()).create(); + deleteDialog.show(); + } } /** * Edit existing feature click * - * @param marker - * @param featureId + * @param marker The marker to edit. + * @param featureId The id of the feature being edited. */ private void editExistingFeatureClick(final Marker marker, long featureId) { - final GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(editFeaturesDatabase); + final GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(model.getEditFeaturesDatabase()); final FeatureDao featureDao = geoPackage - .getFeatureDao(editFeaturesTable); + .getFeatureDao(model.getEditFeaturesTable()); final FeatureRow featureRow = featureDao.queryForIdRow(featureId); - if (featureRow != null) { + if (featureRow != null && getActivity() != null) { final GeoPackageGeometryData geomData = featureRow.getGeometry(); final GeometryType geometryType = geomData.getGeometry() .getGeometryType(); - ArrayAdapter adapter = new ArrayAdapter( + ArrayAdapter adapter = new ArrayAdapter<>( getActivity(), android.R.layout.select_dialog_item); adapter.add(getString(R.string.edit_features_info_label)); adapter.add(getString(R.string.edit_features_edit_label)); @@ -5484,25 +4025,21 @@ private void editExistingFeatureClick(final Marker marker, long featureId) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle); final String title = getTitle(geometryType, marker); builder.setTitle(title); - builder.setAdapter(adapter, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int item) { - - if (item >= 0) { - switch (item) { - case 0: - infoExistingFeatureOption(geoPackage, featureRow, title, geomData); - break; - case 1: - tempEditFeatureMarker = marker; - validateAndClearEditFeatures(EditType.EDIT_FEATURE); - break; - case 2: - deleteExistingFeatureOption(title, editFeaturesDatabase, - editFeaturesTable, featureRow, marker, - geometryType); - break; - default: - } + builder.setAdapter(adapter, (DialogInterface dialog, int item) -> { + + if (item >= 0) { + switch (item) { + case 0: + infoExistingFeatureOption(geoPackage, featureRow, title, geomData); + break; + case 1: + tempEditFeatureMarker = marker; + validateAndClearEditFeatures(EditType.EDIT_FEATURE); + break; + case 2: + deleteExistingFeatureOption(title, featureRow, marker, geometryType); + break; + default: } } }); @@ -5516,26 +4053,24 @@ public void onClick(DialogInterface dialog, int item) { /** * Get a title from the Geometry Type and marker * - * @param geometryType - * @param marker - * @return + * @param geometryType The geometry type of the marker. + * @param marker The marker to get the title for. + * @return The title. */ private String getTitle(GeometryType geometryType, Marker marker) { LatLng position = marker.getPosition(); DecimalFormat formatter = new DecimalFormat("0.0###"); - String title = geometryType.getName() + "\n(lat=" + return geometryType.getName() + "\n(lat=" + formatter.format(position.latitude) + ", lon=" + formatter.format(position.longitude) + ")"; - return title; } /** * Info feature click * - * @param marker - * @param markerFeature + * @param markerFeature The feature of the marker. */ - private void infoFeatureClick(final Marker marker, MarkerFeature markerFeature) { + private void infoFeatureClick(MarkerFeature markerFeature) { Intent intent = new Intent(getContext(), FeatureViewActivity.class); intent.putExtra(String.valueOf(R.string.marker_feature_param), markerFeature); startActivity(intent); @@ -5544,10 +4079,10 @@ private void infoFeatureClick(final Marker marker, MarkerFeature markerFeature) /** * Info existing feature option * - * @param geoPackage - * @param featureRow - * @param title - * @param geomData + * @param geoPackage The geoPackage. + * @param featureRow The feature row. + * @param title The title for the pop up. + * @param geomData The geometry info. */ private void infoExistingFeatureOption(final GeoPackage geoPackage, FeatureRow featureRow, @@ -5601,71 +4136,62 @@ private void infoExistingFeatureOption(final GeoPackage geoPackage, message.append(GeometryPrinter.getGeometryString(geomData .getGeometry())); - AlertDialog viewDialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) - .setTitle(title) - .setPositiveButton(getString(R.string.button_ok_label), - - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }).setMessage(message).create(); - viewDialog.show(); + if (getActivity() != null) { + AlertDialog viewDialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) + .setTitle(title) + .setPositiveButton(getString(R.string.button_ok_label), (dialog, which) -> dialog.dismiss()) + .setMessage(message).create(); + viewDialog.show(); + } } /** * Delete existing feature options * - * @param title - * @param database - * @param table - * @param featureRow - * @param marker - * @param geometryType + * @param title The title for the pop up. + * @param featureRow The feature row. + * @param marker The marker. + * @param geometryType The geometry type. */ private void deleteExistingFeatureOption(final String title, - final String database, final String table, final FeatureRow featureRow, final Marker marker, final GeometryType geometryType) { - final LatLng position = marker.getPosition(); - - AlertDialog deleteDialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) - .setCancelable(false) - .setTitle( - getString(R.string.edit_features_delete_label) + " " - + title) - .setMessage( - getString(R.string.edit_features_delete_label) + " " - + geometryType.getName() + " from " - + editFeaturesDatabase + " - " - + editFeaturesTable + " (lat=" - + position.latitude + ", lon=" - + position.longitude + ") ?") - .setPositiveButton( - getString(R.string.edit_features_delete_label), - - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(editFeaturesDatabase); + if (getActivity() != null) { + final LatLng position = marker.getPosition(); + + AlertDialog deleteDialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) + .setCancelable(false) + .setTitle( + getString(R.string.edit_features_delete_label) + " " + + title) + .setMessage( + getString(R.string.edit_features_delete_label) + " " + + geometryType.getName() + " from " + + model.getEditFeaturesDatabase() + " - " + + model.getEditFeaturesTable() + " (lat=" + + position.latitude + ", lon=" + + position.longitude + ") ?") + .setPositiveButton( + getString(R.string.edit_features_delete_label), + + (dialog, which) -> { + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(model.getEditFeaturesDatabase()); try { FeatureDao featureDao = geoPackage - .getFeatureDao(editFeaturesTable); + .getFeatureDao(model.getEditFeaturesTable()); featureDao.delete(featureRow); marker.remove(); - editFeatureIds.remove(marker.getId()); - GoogleMapShape featureObject = editFeatureObjects + model.getEditFeatureIds().remove(marker.getId()); + GoogleMapShape featureObject = model.getEditFeatureObjects() .remove(marker.getId()); if (featureObject != null) { featureObject.remove(); } updateLastChange(geoPackage, featureDao); - active.setModified(true); + model.getActive().setModified(true); } catch (Exception e) { if (GeoPackageUtils .isUnsupportedSQLiteException(e)) { @@ -5688,18 +4214,11 @@ public void onClick(DialogInterface dialog, e.getMessage()); } } - } - }) - - .setNegativeButton(getString(R.string.button_cancel_label), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - dialog.dismiss(); - } - }).create(); - deleteDialog.show(); + }) + + .setNegativeButton(getString(R.string.button_cancel_label), (dialog, which) -> dialog.dismiss()).create(); + deleteDialog.show(); + } } /** @@ -5733,8 +4252,8 @@ private static void expandBounds(GeoPackage geoPackage, FeatureDao featureDao, G /** * Update the last change date of the contents * - * @param geoPackage - * @param featureDao + * @param geoPackage The geoPackage. + * @param featureDao The feature data access object. */ private static void updateLastChange(GeoPackage geoPackage, FeatureDao featureDao) { try { @@ -5752,12 +4271,12 @@ private static void updateLastChange(GeoPackage geoPackage, FeatureDao featureDa /** * Get a list of the polygon points for the bounding box * - * @param point1 - * @param point2 - * @return + * @param point1 The first point. + * @param point2 The second point. + * @return The bounding box corners. */ private List getPolygonPoints(LatLng point1, LatLng point2) { - List points = new ArrayList(); + List points = new ArrayList<>(); points.add(new LatLng(point1.latitude, point1.longitude)); points.add(new LatLng(point1.latitude, point2.longitude)); points.add(new LatLng(point2.latitude, point2.longitude)); @@ -5808,84 +4327,89 @@ public boolean dispatchTouchEvent(MotionEvent ev) { /** * Get feature selection dialog * - * @param editFeaturesSelectionView - * @param featuresInput - * @param geoPackageInput - * @return + * @param editFeaturesSelectionView The view. + * @param featuresInput The features input spinner. + * @param geoPackageInput The geoPackage input spinner. + * @return The dialog builder. */ private AlertDialog.Builder getFeatureSelectionDialog(View editFeaturesSelectionView, final Spinner geoPackageInput, final Spinner featuresInput) { - AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle); - dialog.setView(editFeaturesSelectionView); - - boolean searchForActive = true; - int defaultDatabase = 0; - int defaultTable = 0; - - List databases = geoPackageViewModel.getDatabases(); - List featureDatabases = new ArrayList(); - if (databases != null) { - for (String database : databases) { - GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(database); - List featureTables = geoPackage.getFeatureTables(); - if (!featureTables.isEmpty()) { - featureDatabases.add(database); - - if (searchForActive) { - for (int i = 0; i < featureTables.size(); i++) { - String featureTable = featureTables.get(i); - boolean isActive = active.exists(database, featureTable, GeoPackageTableType.FEATURE); - if (isActive) { - defaultDatabase = featureDatabases.size() - 1; - defaultTable = i; - searchForActive = false; - break; + AlertDialog.Builder dialog = null; + + if (getActivity() != null) { + dialog = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle); + dialog.setView(editFeaturesSelectionView); + + boolean searchForActive = true; + int defaultDatabase = 0; + int defaultTable = 0; + + List databases = geoPackageViewModel.getDatabases(); + List featureDatabases = new ArrayList<>(); + if (databases != null) { + for (String database : databases) { + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(database); + List featureTables = geoPackage.getFeatureTables(); + if (!featureTables.isEmpty()) { + featureDatabases.add(database); + + if (searchForActive) { + for (int i = 0; i < featureTables.size(); i++) { + String featureTable = featureTables.get(i); + boolean isActive = model.getActive().exists(database, featureTable, GeoPackageTableType.FEATURE); + if (isActive) { + defaultDatabase = featureDatabases.size() - 1; + defaultTable = i; + searchForActive = false; + break; + } } } } } } - } - if (featureDatabases.isEmpty()) { - GeoPackageUtils.showMessage(getActivity(), - getString(R.string.edit_features_selection_features_label), - "No GeoPackages with features"); - return null; - } - ArrayAdapter geoPackageAdapter = new ArrayAdapter( - getActivity(), R.layout.spinner_item, - featureDatabases); - geoPackageInput.setAdapter(geoPackageAdapter); + if (featureDatabases.isEmpty()) { + GeoPackageUtils.showMessage(getActivity(), + getString(R.string.edit_features_selection_features_label), + "No GeoPackages with features"); + return null; + } + ArrayAdapter geoPackageAdapter = new ArrayAdapter<>( + getActivity(), R.layout.spinner_item, + featureDatabases); + geoPackageInput.setAdapter(geoPackageAdapter); - updateFeaturesSelection(featuresInput, featureDatabases.get(defaultDatabase)); + updateFeaturesSelection(featuresInput, featureDatabases.get(defaultDatabase)); - geoPackageInput.setSelection(defaultDatabase); - featuresInput.setSelection(defaultTable); + geoPackageInput.setSelection(defaultDatabase); + featuresInput.setSelection(defaultTable); - geoPackageInput - .setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + geoPackageInput + .setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - boolean firstTime = true; + boolean firstTime = true; - @Override - public void onItemSelected(AdapterView parentView, - View selectedItemView, int position, long id) { + @Override + public void onItemSelected(AdapterView parentView, + View selectedItemView, int position, long id) { - if (firstTime) { - firstTime = false; - } else { - String geoPackage = geoPackageInput.getSelectedItem() - .toString(); - updateFeaturesSelection(featuresInput, geoPackage); + if (firstTime) { + firstTime = false; + } else { + String geoPackage = geoPackageInput.getSelectedItem() + .toString(); + updateFeaturesSelection(featuresInput, geoPackage); + } } - } - @Override - public void onNothingSelected(AdapterView parentView) { - } - }); + @Override + public void onNothingSelected(AdapterView parentView) { + } + }); + + } return dialog; } @@ -5894,7 +4418,7 @@ public void onNothingSelected(AdapterView parentView) { * {@inheritDoc} */ @Override - public void onLoadTilesCancelled(String result) { + public void onLoadTilesCancelled() { loadTilesFinished(); } @@ -5903,10 +4427,10 @@ public void onLoadTilesCancelled(String result) { */ @Override public void onLoadTilesPostExecute(String result) { - if (result != null) { + if (result != null && getActivity() != null) { getActivity().runOnUiThread(() -> - GeoPackageUtils.showMessage(getActivity(), - getString(R.string.geopackage_create_tiles_label), result)); + GeoPackageUtils.showMessage(getActivity(), + getString(R.string.geopackage_create_tiles_label), result)); } loadTilesFinished(); } @@ -5938,7 +4462,7 @@ private void loadTilesFinished() { // Make sure the geopackage source is being repopulated to get the new layer geoPackageViewModel.regenerateGeoPackageTableList(); - if (active.isModified()) { + if (model.getActive().isModified()) { updateInBackground(false); if (boundingBox != null) { PolygonOptions polygonOptions = new PolygonOptions(); diff --git a/mapcache/src/main/java/mil/nga/mapcache/MapFeaturesUpdateTask.java b/mapcache/src/main/java/mil/nga/mapcache/MapFeaturesUpdateTask.java new file mode 100644 index 00000000..a328f416 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/MapFeaturesUpdateTask.java @@ -0,0 +1,410 @@ +package mil.nga.mapcache; + +import android.app.Activity; +import android.util.Log; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.Marker; + +import org.locationtech.proj4j.units.Units; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import mil.nga.geopackage.BoundingBox; +import mil.nga.geopackage.GeoPackage; +import mil.nga.geopackage.features.index.FeatureIndexManager; +import mil.nga.geopackage.features.index.FeatureIndexResults; +import mil.nga.geopackage.features.index.MultipleFeatureIndexResults; +import mil.nga.geopackage.features.user.FeatureCursor; +import mil.nga.geopackage.features.user.FeatureDao; +import mil.nga.geopackage.features.user.FeatureRow; +import mil.nga.geopackage.map.features.StyleCache; +import mil.nga.geopackage.map.geom.GoogleMapShape; +import mil.nga.geopackage.map.geom.GoogleMapShapeConverter; +import mil.nga.geopackage.map.geom.GoogleMapShapeType; +import mil.nga.mapcache.data.GeoPackageDatabase; +import mil.nga.mapcache.data.GeoPackageTable; +import mil.nga.mapcache.data.MarkerFeature; +import mil.nga.mapcache.utils.ThreadUtils; +import mil.nga.mapcache.viewmodel.GeoPackageViewModel; +import mil.nga.proj.ProjectionConstants; +import mil.nga.proj.ProjectionFactory; +import mil.nga.proj.ProjectionTransform; +import mil.nga.sf.GeometryType; + +/** + * Update the map features in the background + */ +public class MapFeaturesUpdateTask implements Runnable { + + /** + * The application context. + */ + private final Activity activity; + + /** + * The model used by the map. + */ + private final MapModel model; + + /** + * Contains the geoPackages. + */ + private final GeoPackageViewModel geoPackageViewModel; + + /** + * The map showing the geoPackages. + */ + private final GoogleMap map; + + /** + * Flag indicating if it was cancelled. + */ + private boolean cancelled = false; + + /** + * The maximum number of features. + */ + private int maxFeatures; + + /** + * The extent of the maps view. + */ + private BoundingBox mapViewBoundingBox; + + /** + * The tolerance to apply when filtering out features that are outside the maps view. + */ + private double toleranceDistance; + + /** + * Flag indicating if we should filter out records not in view. + */ + private boolean filter; + + /** + * Constructor. + * + * @param activity The application activity. + * @param map The map showing the geoPackages. + * @param model The model used by the map. + * @param geoPackageViewModel Contains the geoPackages. + */ + public MapFeaturesUpdateTask(Activity activity, GoogleMap map, MapModel model, GeoPackageViewModel geoPackageViewModel) { + this.activity = activity; + this.map = map; + this.model = model; + this.geoPackageViewModel = geoPackageViewModel; + } + + /** + * Add a shape to the map + * + * @param featureId The id of the feature. + * @param database The name of the geopackage. + * @param tableName The name of the layer. + * @param shape The type of shape to add. + */ + public void addToMap(long featureId, String database, String tableName, GoogleMapShape shape) { + this.activity.runOnUiThread(() -> { + synchronized (model.getFeatureShapes()) { + + if (NotCancelled() && !model.getFeatureShapes().exists(featureId, database, tableName)) { + + GoogleMapShape mapShape = GoogleMapShapeConverter.addShapeToMap( + map, shape); + + if (model.isEditFeaturesMode()) { + Marker marker = ShapeHelper.getInstance().addEditableShape( + activity, map, model, featureId, mapShape); + if (marker != null) { + GoogleMapShape mapPointShape = new GoogleMapShape(GeometryType.POINT, GoogleMapShapeType.MARKER, marker); + model.getFeatureShapes().addMapMetadataShape(mapPointShape, featureId, database, tableName); + } + } else { + addMarkerShape(featureId, database, tableName, mapShape); + } + model.getFeatureShapes().addMapShape(mapShape, featureId, database, tableName); + } + } + }); + } + + /** + * Cancels the task. + */ + public void cancel() { + cancelled = true; + } + + /** + * Indicates if this task has been cancelled. + * + * @return True if its been cancelled false if not cancelled. + */ + public boolean NotCancelled() { + return !cancelled; + } + + /** + * Performs this task on a background thread. + * + * @param maxFeatures The maximum number of features. + * @param mapViewBoundingBox The maps view extent. + * @param toleranceDistance The tolerance to apply when filtering out features that are outside the maps view. + * @param filter Flag indicating if we should filter out records not in view. + */ + public void execute(int maxFeatures, BoundingBox mapViewBoundingBox, double toleranceDistance, boolean filter) { + this.maxFeatures = maxFeatures; + this.mapViewBoundingBox = mapViewBoundingBox; + this.toleranceDistance = toleranceDistance; + this.filter = filter; + ThreadUtils.getInstance().runBackground(this); + } + + /** + * Add features to the map + * + * @param maxFeatures max features + * @param mapViewBoundingBox map view bounding box + * @param toleranceDistance tolerance distance + * @param filter filter + */ + private void addFeatures(final int maxFeatures, BoundingBox mapViewBoundingBox, double toleranceDistance, boolean filter) { + + AtomicInteger count = new AtomicInteger(); + + Map> featureTables = new HashMap<>(); + if (model.isEditFeaturesMode()) { + List databaseFeatures = new ArrayList<>(); + databaseFeatures.add(model.getEditFeaturesTable()); + featureTables.put(model.getEditFeaturesDatabase(), databaseFeatures); + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(model.getEditFeaturesDatabase()); + Map databaseFeatureDaos = model.getFeatureDaos().get(model.getEditFeaturesDatabase()); + if (databaseFeatureDaos == null) { + databaseFeatureDaos = new HashMap<>(); + model.getFeatureDaos().put(model.getEditFeaturesDatabase(), databaseFeatureDaos); + } + FeatureDao featureDao = databaseFeatureDaos.get(model.getEditFeaturesTable()); + if (featureDao == null) { + featureDao = geoPackage.getFeatureDao(model.getEditFeaturesTable()); + databaseFeatureDaos.put(model.getEditFeaturesTable(), featureDao); + } + } else { + for (GeoPackageDatabase database : model.getActive().getDatabases()) { + if (!database.getFeatures().isEmpty()) { + List databaseFeatures = new ArrayList<>(); + featureTables.put(database.getDatabase(), + databaseFeatures); + for (GeoPackageTable features : database.getFeatures()) { + databaseFeatures.add(features.getName()); + } + } + } + } + + for (Map.Entry> databaseFeaturesEntry : featureTables + .entrySet()) { + + if (count.get() >= maxFeatures) { + break; + } + + String databaseName = databaseFeaturesEntry.getKey(); + + List databaseFeatures = databaseFeaturesEntry.getValue(); + Map databaseFeatureDaos = model.getFeatureDaos().get(databaseName); + + if (databaseFeatureDaos != null) { + + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(databaseName); + StyleCache styleCache = new StyleCache(geoPackage, activity.getResources().getDisplayMetrics().density); + + for (String features : databaseFeatures) { + + if (databaseFeatureDaos.containsKey(features)) { + + displayFeatures(geoPackage, styleCache, features, count, + maxFeatures, model.isEditFeaturesMode(), mapViewBoundingBox, + toleranceDistance, filter); + if (cancelled || count.get() >= maxFeatures) { + break; + } + } + } + + styleCache.clear(); + } + + if (cancelled) { + break; + } + } + } + + /** + * Display features + * + * @param geoPackage The geopackage to display. + * @param styleCache the style cache. + * @param features The features. + * @param count The number of features. + * @param maxFeatures The maximum number of features the map will display. + * @param editable True if its editable. + * @param mapViewBoundingBox The views bounding box. + * @param toleranceDistance Used to simplify geometries for performance. + * @param filter True if features should be filtered. + */ + private void displayFeatures(GeoPackage geoPackage, StyleCache styleCache, String features, + AtomicInteger count, final int maxFeatures, final boolean editable, + BoundingBox mapViewBoundingBox, double toleranceDistance, boolean filter) { + + // Get the GeoPackage and feature DAO + String database = geoPackage.getName(); + Map dataAccessObjects = model.getFeatureDaos().get(database); + if (dataAccessObjects != null) { + FeatureDao featureDao = dataAccessObjects.get(features); + if (featureDao != null) { + GoogleMapShapeConverter converter = new GoogleMapShapeConverter(featureDao.getProjection()); + + converter.setSimplifyTolerance(toleranceDistance); + + if (!styleCache.getFeatureStyleExtension().has(features)) { + styleCache = null; + } + + count.getAndAdd(model.getFeatureShapes().getFeatureIdsCount(database, features)); + + if (!cancelled && count.get() < maxFeatures) { + + mil.nga.proj.Projection mapViewProjection = ProjectionFactory.getProjection(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM); + + String[] columns = featureDao.getIdAndGeometryColumnNames(); + + FeatureIndexManager indexer = new FeatureIndexManager(activity, geoPackage, featureDao); + if (filter && indexer.isIndexed()) { + + FeatureIndexResults indexResults = indexer.query(columns, mapViewBoundingBox, mapViewProjection); + BoundingBox complementary = mapViewBoundingBox.complementaryWgs84(); + if (complementary != null) { + FeatureIndexResults indexResults2 = indexer.query(columns, complementary, mapViewProjection); + indexResults = new MultipleFeatureIndexResults(indexResults, indexResults2); + } + + processFeatureIndexResults(indexResults, database, featureDao, converter, styleCache, + count, maxFeatures, editable); + + } else { + + BoundingBox filterBoundingBox = null; + double filterMaxLongitude = 0; + + if (filter) { + mil.nga.proj.Projection featureProjection = featureDao.getProjection(); + ProjectionTransform projectionTransform = mapViewProjection.getTransformation(featureProjection); + BoundingBox boundedMapViewBoundingBox = mapViewBoundingBox.boundWgs84Coordinates(); + BoundingBox transformedBoundingBox = boundedMapViewBoundingBox.transform(projectionTransform); + if (featureProjection.isUnit(Units.DEGREES)) { + filterMaxLongitude = ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH; + } else if (featureProjection.isUnit(Units.METRES)) { + filterMaxLongitude = ProjectionConstants.WEB_MERCATOR_HALF_WORLD_WIDTH; + } + filterBoundingBox = transformedBoundingBox.expandCoordinates(filterMaxLongitude); + } + + // Query for all rows + try (FeatureCursor cursor = featureDao.query(columns)) { + while (!cancelled && count.get() < maxFeatures + && cursor.moveToNext()) { + try { + FeatureRow row = cursor.getRow(); + + // Process the feature row in the thread pool + FeatureRowProcessor processor = new FeatureRowProcessor( + this, database, featureDao, row, count, maxFeatures, editable, converter, + styleCache, filterBoundingBox, filterMaxLongitude, + filter, model, activity); + ThreadUtils.getInstance().runBackground(processor); + } catch (Exception e) { + Log.e(GeoPackageMapFragment.class.getSimpleName(), + "Failed to display feature. database: " + database + + ", feature table: " + features + + ", row: " + cursor.getPosition(), e); + } + } + + } + } + indexer.close(); + + } + } + } + } + + /** + * Process the feature index results + * + * @param indexResults The index results. + * @param database The geoPackage to process features for. + * @param featureDao The feature data access object. + * @param converter Convert the features shapes to those that can go on a google map. + * @param styleCache The style cache. + * @param count Keeps track of how many features we have added to the map. + * @param maxFeatures The maximum number of features we can add to the map. + * @param editable True if the feature added to the map should look editable. + */ + private void processFeatureIndexResults(FeatureIndexResults indexResults, String database, FeatureDao featureDao, + GoogleMapShapeConverter converter, StyleCache styleCache, AtomicInteger count, final int maxFeatures, final boolean editable) { + try { + for (FeatureRow row : indexResults) { + + if (cancelled || count.get() >= maxFeatures) { + break; + } + + try { + + // Process the feature row in the thread pool + FeatureRowProcessor processor = new FeatureRowProcessor( + this, database, featureDao, row, count, maxFeatures, editable, converter, + styleCache, null, 0, true, model, activity); + ThreadUtils.getInstance().runBackground(processor); + + } catch (Exception e) { + Log.e(GeoPackageMapFragment.class.getSimpleName(), + "Failed to display feature. database: " + database + + ", feature table: " + featureDao.getTableName() + + ", row id: " + row.getId(), e); + } + } + } finally { + indexResults.close(); + } + } + + /** + * Add marker shape + * + * @param featureId The id of the feature. + * @param database The name of the geopackage the feature belongs to. + * @param tableName The name of the layer the feature belongs to. + * @param shape The shape to add. + */ + private void addMarkerShape(long featureId, String database, String tableName, GoogleMapShape shape) { + if (shape.getShapeType() == GoogleMapShapeType.MARKER) { + Marker marker = (Marker) shape.getShape(); + MarkerFeature markerFeature = new MarkerFeature(featureId, database, tableName); + model.getMarkerIds().put(marker.getId(), markerFeature); + } + } + + @Override + public void run() { + addFeatures(maxFeatures, mapViewBoundingBox, toleranceDistance, filter); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/MapModel.java b/mapcache/src/main/java/mil/nga/mapcache/MapModel.java new file mode 100644 index 00000000..2cb1ad82 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/MapModel.java @@ -0,0 +1,264 @@ +package mil.nga.mapcache; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import mil.nga.geopackage.BoundingBox; +import mil.nga.geopackage.features.user.FeatureDao; +import mil.nga.geopackage.map.geom.FeatureShapes; +import mil.nga.geopackage.map.geom.GoogleMapShape; +import mil.nga.geopackage.map.tiles.overlay.FeatureOverlayQuery; +import mil.nga.mapcache.data.GeoPackageDatabases; +import mil.nga.mapcache.data.MarkerFeature; + +/** + * Contains various states involving the map and its data. + */ +public class MapModel { + /** + * Bounding box around the features on the map + */ + private BoundingBox featuresBoundingBox; + + /** + * Bounding box around the tiles on the map + */ + private BoundingBox tilesBoundingBox; + + /** + * Active GeoPackages + */ + private GeoPackageDatabases active; + + /** + * True when a tile layer is drawn from features + */ + private boolean featureOverlayTiles = false; + + /** + * Feature shapes + */ + private final FeatureShapes featureShapes = new FeatureShapes(); + + /** + * Edit features mode + */ + private boolean editFeaturesMode = false; + + /** + * Mapping between marker ids and the feature ids + */ + private final Map editFeatureIds = new HashMap<>(); + + /** + * Mapping between marker ids and feature objects + */ + private final Map editFeatureObjects = new HashMap<>(); + + /** + * Edit features table + */ + private String editFeaturesTable; + + /** + * Edit features database + */ + private String editFeaturesDatabase; + + /** + * Mapping of open GeoPackage feature DAOs + */ + private final Map> featureDaos = new HashMap<>(); + + /** + * Mapping between marker ids and the features + */ + private final Map markerIds = new HashMap<>(); + + /** + * List of Feature Overlay Queries for querying tile overlay clicks + */ + private final List featureOverlayQueries = new ArrayList<>(); + + /** + * Gets the bounding box around the features on the map + * + * @return The bounding box around the features on the map + */ + public BoundingBox getFeaturesBoundingBox() { + return featuresBoundingBox; + } + + /** + * Sets the bounding box around the features on the map + * + * @param featuresBoundingBox The bounding box around the features on the map + */ + public void setFeaturesBoundingBox(BoundingBox featuresBoundingBox) { + this.featuresBoundingBox = featuresBoundingBox; + } + + /** + * Gets the bounding box around the tiles on the map + * + * @return The bounding box around the tiles on the map + */ + public BoundingBox getTilesBoundingBox() { + return tilesBoundingBox; + } + + /** + * Sets the bounding box around the tiles on the map + * + * @param tilesBoundingBox The bounding box around the tiles on the map + */ + public void setTilesBoundingBox(BoundingBox tilesBoundingBox) { + this.tilesBoundingBox = tilesBoundingBox; + } + + /** + * Gets the active geoPackages. + * + * @return The active geoPackages. + */ + public GeoPackageDatabases getActive() { + return active; + } + + /** + * Sets the active geoPackages. + * + * @param active The active geoPackages. + */ + public void setActive(GeoPackageDatabases active) { + this.active = active; + } + + /** + * Gets the flag indicating if a layer is drawn from features. + * + * @return True when a tile layer is drawn from features. + */ + public boolean isFeatureOverlayTiles() { + return featureOverlayTiles; + } + + /** + * Sets the flag indicating if a layer is drawn from features. + * + * @param featureOverlayTiles True when a tile layer is drawn from features. + */ + public void setFeatureOverlayTiles(boolean featureOverlayTiles) { + this.featureOverlayTiles = featureOverlayTiles; + } + + /** + * Gets the feature shapes. + * + * @return The feature shapes. + */ + public FeatureShapes getFeatureShapes() { + return featureShapes; + } + + /** + * Indicates if we are editing features. + * + * @return True if editing features, false if not. + */ + public boolean isEditFeaturesMode() { + return editFeaturesMode; + } + + /** + * Indicates if we are editing features. + * + * @param editFeaturesMode True if editing features, false if not. + */ + public void setEditFeaturesMode(boolean editFeaturesMode) { + this.editFeaturesMode = editFeaturesMode; + } + + /** + * Gets the mapping between marker ids and the feature ids + * + * @return The mapping between marker ids and the feature ids + */ + public Map getEditFeatureIds() { + return editFeatureIds; + } + + /** + * Gets the mapping between marker ids and feature objects + * + * @return The mapping between marker ids and feature objects + */ + public Map getEditFeatureObjects() { + return editFeatureObjects; + } + + /** + * Gets the edit feature table. + * + * @return The name of the table being edited. + */ + public String getEditFeaturesTable() { + return editFeaturesTable; + } + + /** + * Sets the edit feature table. + * + * @param editFeaturesTable The name of the table being edited. + */ + public void setEditFeaturesTable(String editFeaturesTable) { + this.editFeaturesTable = editFeaturesTable; + } + + /** + * Gets the geoPackage name being edited. + * + * @return The geoPackage name. + */ + public String getEditFeaturesDatabase() { + return editFeaturesDatabase; + } + + /** + * Sets the geoPackage name being edited. + * + * @param editFeaturesDatabase The geoPackage name. + */ + public void setEditFeaturesDatabase(String editFeaturesDatabase) { + this.editFeaturesDatabase = editFeaturesDatabase; + } + + /** + * Gets the feature data access objects. + * + * @return The feature data access objects. + */ + public Map> getFeatureDaos() { + return featureDaos; + } + + /** + * Gets the mapping between marker ids and the features. + * + * @return The mapping between marker ids and the features. + */ + public Map getMarkerIds() { + return markerIds; + } + + /** + * Gets the list of Feature Overlay Queries for querying tile overlay clicks. + * + * @return List of feature overlay queries. + */ + public List getFeatureOverlayQueries() { + return featureOverlayQueries; + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/MapUpdateTask.java b/mapcache/src/main/java/mil/nga/mapcache/MapUpdateTask.java new file mode 100644 index 00000000..552f39df --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/MapUpdateTask.java @@ -0,0 +1,410 @@ +package mil.nga.mapcache; + +import android.app.Activity; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.Log; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.TileOverlayOptions; +import com.google.android.gms.maps.model.TileProvider; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import mil.nga.geopackage.BoundingBox; +import mil.nga.geopackage.GeoPackage; +import mil.nga.geopackage.contents.Contents; +import mil.nga.geopackage.extension.nga.link.FeatureTileTableLinker; +import mil.nga.geopackage.extension.nga.scale.TileScaling; +import mil.nga.geopackage.extension.nga.scale.TileTableScaling; +import mil.nga.geopackage.features.columns.GeometryColumns; +import mil.nga.geopackage.features.user.FeatureDao; +import mil.nga.geopackage.map.tiles.overlay.BoundedOverlay; +import mil.nga.geopackage.map.tiles.overlay.FeatureOverlay; +import mil.nga.geopackage.map.tiles.overlay.FeatureOverlayQuery; +import mil.nga.geopackage.map.tiles.overlay.GeoPackageOverlayFactory; +import mil.nga.geopackage.srs.SpatialReferenceSystem; +import mil.nga.geopackage.tiles.features.DefaultFeatureTiles; +import mil.nga.geopackage.tiles.features.FeatureTiles; +import mil.nga.geopackage.tiles.features.custom.NumberFeaturesTile; +import mil.nga.geopackage.tiles.matrixset.TileMatrixSet; +import mil.nga.geopackage.tiles.user.TileDao; +import mil.nga.mapcache.data.GeoPackageDatabase; +import mil.nga.mapcache.data.GeoPackageFeatureOverlayTable; +import mil.nga.mapcache.data.GeoPackageFeatureTable; +import mil.nga.mapcache.data.GeoPackageTileTable; +import mil.nga.mapcache.utils.ProjUtils; +import mil.nga.mapcache.utils.ThreadUtils; +import mil.nga.mapcache.view.map.BasemapApplier; +import mil.nga.mapcache.viewmodel.GeoPackageViewModel; +import mil.nga.proj.ProjectionConstants; +import mil.nga.proj.ProjectionFactory; +import mil.nga.proj.ProjectionTransform; + +/** + * Update the map in the background + */ +public class MapUpdateTask implements Runnable { + + /** + * The main activity. + */ + private final Activity activity; + + /** + * The map to update. + */ + private final GoogleMap map; + + /** + * The applies the selected basemap to the map. + */ + private final BasemapApplier basemapApplier; + + /** + * The model used by the map. + */ + private final MapModel model; + + /** + * Contains all the geoPackages. + */ + private final GeoPackageViewModel geoPackageViewModel; + + /** + * Indicates if this task has been cancelled. + */ + private boolean isCancelled = false; + + /** + * Notified when this task is done. + */ + private Runnable finishListener; + + /** + * Constructor. + * + * @param activity The main activity. + * @param map The map to update. + * @param basemapApplier The applies the selected basemap to the map. + * @param model The model used by the map. + * @param geoPackageViewModel Contains all the geoPackages. + */ + public MapUpdateTask( + Activity activity, + GoogleMap map, + BasemapApplier basemapApplier, + MapModel model, + GeoPackageViewModel geoPackageViewModel) { + this.activity = activity; + this.map = map; + this.basemapApplier = basemapApplier; + this.model = model; + this.geoPackageViewModel = geoPackageViewModel; + } + + /** + * Runs this task within a background thread. + */ + public void execute() { + ThreadUtils.getInstance().runBackground(this); + } + + /** + * Cancels the running of this task. + */ + public void cancel() { + this.isCancelled = true; + } + + /** + * Sets the finish listener. + * + * @param listener The object to be notified when this task is done. + */ + public void setFinishListener(Runnable listener) { + this.finishListener = listener; + } + + @Override + public void run() { + update(); + activity.runOnUiThread(() -> basemapApplier.applyBasemaps(map)); + if(finishListener != null) { + finishListener.run(); + } + } + + /** + * Update the map + */ + private void update() { + + if (model.getActive() != null) { + + // Open active GeoPackages and create feature DAOS, display tiles and feature tiles + List activeDatabases = new ArrayList<>(model.getActive().getDatabases()); + for (GeoPackageDatabase database : activeDatabases) { + + if (this.isCancelled) { + break; + } + + try { + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(database.getDatabase()); + + if (geoPackage != null) { + + Set featureTableDaos = new HashSet<>(); + Collection features = database.getFeatures(); + if (!features.isEmpty()) { + for (GeoPackageFeatureTable featureTable : features) { + featureTableDaos.add(featureTable.getName()); + } + } + + for (GeoPackageFeatureOverlayTable featureOverlay : database.getFeatureOverlays()) { + if (featureOverlay.isActive()) { + featureTableDaos.add(featureOverlay.getFeatureTable()); + } + } + + if (!featureTableDaos.isEmpty()) { + Map databaseFeatureDaos = new HashMap<>(); + model.getFeatureDaos().put(database.getDatabase(), databaseFeatureDaos); + for (String featureTable : featureTableDaos) { + + if (this.isCancelled) { + break; + } + + FeatureDao featureDao = geoPackage.getFeatureDao(featureTable); + databaseFeatureDaos.put(featureTable, featureDao); + } + } + + // Display the tiles + for (GeoPackageTileTable tiles : database.getTiles()) { + if (this.isCancelled) { + break; + } + try { + displayTiles(tiles); + } catch (Exception e) { + Log.e(GeoPackageMapFragment.class.getSimpleName(), + e.getMessage()); + } + } + + // Display the feature tiles + for (GeoPackageFeatureOverlayTable featureOverlay : database.getFeatureOverlays()) { + if (this.isCancelled) { + break; + } + if (featureOverlay.isActive()) { + try { + displayFeatureTiles(featureOverlay); + } catch (Exception e) { + Log.e(GeoPackageMapFragment.class.getSimpleName(), + e.getMessage()); + } + } + } + + } else { + model.getActive().removeDatabase(database.getDatabase(), false); + } + } catch (Exception e) { + Log.e(GeoPackageMapFragment.class.getSimpleName(), "Error opening geopackage: " + database.getDatabase(), e); + } + } + } + + } + + /** + * Display tiles + * + * @param tiles The tiles to display. + */ + private void displayTiles(GeoPackageTileTable tiles) { + + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(tiles.getDatabase()); + + TileDao tileDao = geoPackage.getTileDao(tiles.getName()); + + TileTableScaling tileTableScaling = new TileTableScaling(geoPackage, tileDao); + TileScaling tileScaling = tileTableScaling.get(); + + BoundedOverlay overlay = GeoPackageOverlayFactory + .getBoundedOverlay(tileDao, activity.getResources().getDisplayMetrics().density, tileScaling); + + TileMatrixSet tileMatrixSet = tileDao.getTileMatrixSet(); + + FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage); + List featureDaos = linker.getFeatureDaosForTileTable(tileDao.getTableName()); + + for (FeatureDao featureDao : featureDaos) { + + // Create the feature tiles + FeatureTiles featureTiles = new DefaultFeatureTiles(activity, geoPackage, featureDao, + activity.getResources().getDisplayMetrics().density); + + model.setFeatureOverlayTiles(true); + + // Add the feature overlay query + FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(activity, overlay, featureTiles); + featureOverlayQuery.calculateStylePixelBounds(); + model.getFeatureOverlayQueries().add(featureOverlayQuery); + } + + // Set the tiles index to be -2 of it is behind features and tiles drawn from features + int zIndex = -2; + + // If these tiles are linked to features, set the zIndex to -1 so they are placed before imagery tiles + if (!featureDaos.isEmpty()) { + zIndex = -1; + } + + BoundingBox displayBoundingBox = tileMatrixSet.getBoundingBox(); + Contents contents = tileMatrixSet.getContents(); + BoundingBox contentsBoundingBox = contents.getBoundingBox(); + if (contentsBoundingBox != null) { + ProjectionTransform transform = contents.getSrs().getProjection().getTransformation(tileMatrixSet.getSrs().getProjection()); + BoundingBox transformedContentsBoundingBox = contentsBoundingBox; + if (!transform.isSameProjection()) { + transformedContentsBoundingBox = transformedContentsBoundingBox.transform(transform); + } + displayBoundingBox = displayBoundingBox.overlap(transformedContentsBoundingBox); + } + + displayTiles(overlay, displayBoundingBox, tileMatrixSet.getSrs(), zIndex, null); + } + + /** + * Display feature tiles + * + * @param featureOverlayTable The overlay table to display. + */ + private void displayFeatureTiles(GeoPackageFeatureOverlayTable featureOverlayTable) { + + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(featureOverlayTable.getDatabase()); + Map daos = model.getFeatureDaos().get(featureOverlayTable.getDatabase()); + if (daos != null) { + FeatureDao featureDao = daos.get(featureOverlayTable.getFeatureTable()); + + BoundingBox boundingBox = new BoundingBox(featureOverlayTable.getMinLon(), + featureOverlayTable.getMinLat(), featureOverlayTable.getMaxLon(), featureOverlayTable.getMaxLat()); + + // Load tiles + FeatureTiles featureTiles = new DefaultFeatureTiles(activity, geoPackage, featureDao, + activity.getResources().getDisplayMetrics().density); + if (featureOverlayTable.isIgnoreGeoPackageStyles()) { + featureTiles.ignoreFeatureTableStyles(); + } + + featureTiles.setMaxFeaturesPerTile(featureOverlayTable.getMaxFeaturesPerTile()); + if (featureOverlayTable.getMaxFeaturesPerTile() != null) { + featureTiles.setMaxFeaturesTileDraw(new NumberFeaturesTile(activity)); + } + + Paint pointPaint = featureTiles.getPointPaint(); + pointPaint.setColor(Color.parseColor(featureOverlayTable.getPointColor())); + + pointPaint.setAlpha(featureOverlayTable.getPointAlpha()); + featureTiles.setPointRadius(featureOverlayTable.getPointRadius()); + + Paint linePaint = featureTiles.getLinePaintCopy(); + linePaint.setColor(Color.parseColor(featureOverlayTable.getLineColor())); + + linePaint.setAlpha(featureOverlayTable.getLineAlpha()); + linePaint.setStrokeWidth(featureOverlayTable.getLineStrokeWidth()); + featureTiles.setLinePaint(linePaint); + + Paint polygonPaint = featureTiles.getPolygonPaintCopy(); + polygonPaint.setColor(Color.parseColor(featureOverlayTable.getPolygonColor())); + + polygonPaint.setAlpha(featureOverlayTable.getPolygonAlpha()); + polygonPaint.setStrokeWidth(featureOverlayTable.getPolygonStrokeWidth()); + featureTiles.setPolygonPaint(polygonPaint); + + featureTiles.setFillPolygon(featureOverlayTable.isPolygonFill()); + if (featureTiles.isFillPolygon()) { + Paint polygonFillPaint = featureTiles.getPolygonFillPaintCopy(); + polygonFillPaint.setColor(Color.parseColor(featureOverlayTable.getPolygonFillColor())); + + polygonFillPaint.setAlpha(featureOverlayTable.getPolygonFillAlpha()); + featureTiles.setPolygonFillPaint(polygonFillPaint); + } + + featureTiles.calculateDrawOverlap(); + + FeatureOverlay featureOverlay = new FeatureOverlay(featureTiles); + featureOverlay.setBoundingBox(boundingBox, ProjectionFactory.getProjection(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM)); + featureOverlay.setMinZoom(featureOverlayTable.getMinZoom()); + featureOverlay.setMaxZoom(featureOverlayTable.getMaxZoom()); + + // Get the tile linked overlay + BoundedOverlay overlay = GeoPackageOverlayFactory.getLinkedFeatureOverlay(featureOverlay, geoPackage); + + if (featureDao != null) { + GeometryColumns geometryColumns = featureDao.getGeometryColumns(); + Contents contents = geometryColumns.getContents(); + + GeoPackageUtils.prepareFeatureTiles(featureTiles); + + model.setFeatureOverlayTiles(true); + + FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(activity, overlay, featureTiles); + featureOverlayQuery.calculateStylePixelBounds(); + model.getFeatureOverlayQueries().add(featureOverlayQuery); + + displayTiles(overlay, contents.getBoundingBox(), contents.getSrs(), -1, boundingBox); + } + } + } + + /** + * Display tiles + * + * @param overlay The tile overlay. + * @param dataBoundingBox The bounding box of the data. + * @param srs The spatial reference system of the tiles. + * @param zIndex The zoom level. + * @param specifiedBoundingBox The specified bounding box. + */ + private void displayTiles(TileProvider overlay, BoundingBox dataBoundingBox, SpatialReferenceSystem srs, int zIndex, BoundingBox specifiedBoundingBox) { + + final TileOverlayOptions overlayOptions = new TileOverlayOptions(); + overlayOptions.tileProvider(overlay); + overlayOptions.zIndex(zIndex); + + BoundingBox boundingBox = dataBoundingBox; + if (boundingBox != null) { + boundingBox = ProjUtils.getInstance().transformBoundingBoxToWgs84(boundingBox, srs); + } else { + boundingBox = new BoundingBox(-ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH, + ProjectionConstants.WEB_MERCATOR_MIN_LAT_RANGE, + ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH, + ProjectionConstants.WEB_MERCATOR_MAX_LAT_RANGE); + } + + if (specifiedBoundingBox != null) { + boundingBox = boundingBox.overlap(specifiedBoundingBox); + } + + if (model.getTilesBoundingBox() == null) { + model.setTilesBoundingBox(boundingBox); + } else { + model.setTilesBoundingBox(model.getTilesBoundingBox().union(boundingBox)); + } + + activity.runOnUiThread(() -> map.addTileOverlay(overlayOptions)); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/ShapeHelper.java b/mapcache/src/main/java/mil/nga/mapcache/ShapeHelper.java new file mode 100644 index 00000000..f0981368 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/ShapeHelper.java @@ -0,0 +1,335 @@ +package mil.nga.mapcache; + +import android.content.Context; +import android.util.TypedValue; + +import androidx.core.content.ContextCompat; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; +import com.google.android.gms.maps.model.Polyline; +import com.google.android.gms.maps.model.PolylineOptions; + +import java.util.List; + +import mil.nga.geopackage.extension.nga.style.FeatureStyle; +import mil.nga.geopackage.features.user.FeatureRow; +import mil.nga.geopackage.map.features.StyleCache; +import mil.nga.geopackage.map.geom.GoogleMapShape; +import mil.nga.geopackage.map.geom.GoogleMapShapeType; +import mil.nga.geopackage.map.geom.MultiLatLng; +import mil.nga.geopackage.map.geom.MultiMarker; +import mil.nga.geopackage.map.geom.MultiPolygon; +import mil.nga.geopackage.map.geom.MultiPolygonOptions; +import mil.nga.geopackage.map.geom.MultiPolyline; +import mil.nga.geopackage.map.geom.MultiPolylineOptions; + +/** + * Prepares the shapes and its options. + */ +public class ShapeHelper { + + /** + * The instance of this class. + */ + private static final ShapeHelper instance = new ShapeHelper(); + + /** + * Gets the instance of this class. + * + * @return The instance of this class. + */ + public static ShapeHelper getInstance() { + return instance; + } + + /** + * Prepare the shape options + * + * @param shape map shape + * @param styleCache style cache + * @param featureRow feature row + * @param editable editable flag + * @param topLevel top level flag + * @param context The application context. + */ + public void prepareShapeOptions(GoogleMapShape shape, StyleCache styleCache, FeatureRow featureRow, boolean editable, + boolean topLevel, Context context) { + + FeatureStyle featureStyle = null; + if (styleCache != null) { + featureStyle = styleCache.getFeatureStyleExtension().getFeatureStyle(featureRow, shape.getGeometryType()); + } + + switch (shape.getShapeType()) { + + case LAT_LNG: + LatLng latLng = (LatLng) shape.getShape(); + MarkerOptions markerOptions = getMarkerOptions(styleCache, featureStyle, editable, topLevel, context); + markerOptions.position(latLng); + shape.setShape(markerOptions); + shape.setShapeType(GoogleMapShapeType.MARKER_OPTIONS); + break; + + case POLYLINE_OPTIONS: + PolylineOptions polylineOptions = (PolylineOptions) shape + .getShape(); + setPolylineOptions(styleCache, featureStyle, editable, polylineOptions, context); + break; + + case POLYGON_OPTIONS: + PolygonOptions polygonOptions = (PolygonOptions) shape.getShape(); + setPolygonOptions(styleCache, featureStyle, editable, polygonOptions, context); + break; + + case MULTI_LAT_LNG: + MultiLatLng multiLatLng = (MultiLatLng) shape.getShape(); + MarkerOptions sharedMarkerOptions = getMarkerOptions(styleCache, featureStyle, editable, + false, context); + multiLatLng.setMarkerOptions(sharedMarkerOptions); + break; + + case MULTI_POLYLINE_OPTIONS: + MultiPolylineOptions multiPolylineOptions = (MultiPolylineOptions) shape + .getShape(); + PolylineOptions sharedPolylineOptions = new PolylineOptions(); + setPolylineOptions(styleCache, featureStyle, editable, sharedPolylineOptions, context); + multiPolylineOptions.setOptions(sharedPolylineOptions); + break; + + case MULTI_POLYGON_OPTIONS: + MultiPolygonOptions multiPolygonOptions = (MultiPolygonOptions) shape + .getShape(); + PolygonOptions sharedPolygonOptions = new PolygonOptions(); + setPolygonOptions(styleCache, featureStyle, editable, sharedPolygonOptions, context); + multiPolygonOptions.setOptions(sharedPolygonOptions); + break; + + case COLLECTION: + @SuppressWarnings("unchecked") + List shapes = (List) shape + .getShape(); + for (int i = 0; i < shapes.size(); i++) { + prepareShapeOptions(shapes.get(i), styleCache, featureRow, editable, false, context); + } + break; + default: + } + } + + /** + * Set the Polyline Option attributes + * + * @param styleCache style cache + * @param featureStyle feature style + * @param editable editable flag + * @param polylineOptions polyline options + * @param context The application context. + */ + private void setPolylineOptions(StyleCache styleCache, FeatureStyle featureStyle, boolean editable, + PolylineOptions polylineOptions, Context context) { + if (editable) { + polylineOptions.color(ContextCompat.getColor(context, R.color.polyline_edit_color)); + } else if (styleCache == null || !styleCache.setFeatureStyle(polylineOptions, featureStyle)) { + polylineOptions.color(ContextCompat.getColor(context, R.color.polyline_color)); + } + } + + /** + * Set the Polygon Option attributes + * + * @param styleCache style cache + * @param featureStyle feature style + * @param editable True if it should be displayed as editable. + * @param polygonOptions The polygon options to set. + * @param context The application context. + */ + private void setPolygonOptions(StyleCache styleCache, FeatureStyle featureStyle, boolean editable, + PolygonOptions polygonOptions, Context context) { + if (editable) { + polygonOptions.strokeColor(ContextCompat.getColor(context, R.color.polygon_edit_color)); + polygonOptions.fillColor(ContextCompat.getColor(context, R.color.polygon_edit_fill_color)); + } else if (styleCache == null || !styleCache.setFeatureStyle(polygonOptions, featureStyle)) { + polygonOptions.strokeColor(ContextCompat.getColor(context, R.color.polygon_color)); + polygonOptions.fillColor(ContextCompat.getColor(context, R.color.polygon_fill_color)); + } + } + + /** + * Get marker options + * + * @param styleCache style cache + * @param featureStyle feature style + * @param editable editable flag + * @param clickable clickable flag + * @param context The application context + * @return marker options + */ + private MarkerOptions getMarkerOptions( + StyleCache styleCache, + FeatureStyle featureStyle, + boolean editable, + boolean clickable, + Context context) { + MarkerOptions markerOptions = new MarkerOptions(); + if (editable) { + TypedValue typedValue = new TypedValue(); + if (clickable) { + context.getResources().getValue(R.dimen.marker_edit_color, typedValue, + true); + } else { + context.getResources().getValue(R.dimen.marker_edit_read_only_color, + typedValue, true); + } + markerOptions.icon(BitmapDescriptorFactory.defaultMarker(typedValue + .getFloat())); + + } else if (styleCache == null || !styleCache.setFeatureStyle(markerOptions, featureStyle)) { + + TypedValue typedValue = new TypedValue(); + context.getResources().getValue(R.dimen.marker_color, typedValue, true); + markerOptions.icon(BitmapDescriptorFactory.defaultMarker(typedValue.getFloat())); + } + + return markerOptions; + } + + /** + * Add editable shape + * + * @param context The application context. + * @param map The map to add the marker to. + * @param model The model used by the map. + * @param featureId The id of the feature. + * @param shape The shape to add. + * @return marker The google map marker to add. + */ + public Marker addEditableShape( + Context context, + GoogleMap map, + MapModel model, + long featureId, + GoogleMapShape shape) { + + Marker marker; + + if (shape.getShapeType() == GoogleMapShapeType.MARKER) { + marker = (Marker) shape.getShape(); + } else { + marker = getMarker(context, map, shape); + if (marker != null) { + model.getEditFeatureObjects().put(marker.getId(), shape); + } + } + + if (marker != null) { + model.getEditFeatureIds().put(marker.getId(), featureId); + } + + return marker; + } + + /** + * Get the first marker of the shape or create one at the location + * + * @param context The application context. + * @param map The map to add the marker to. + * @param shape The shape to get the marker for. + * @return The marker to add to the map. + */ + private Marker getMarker(Context context, GoogleMap map, GoogleMapShape shape) { + + Marker marker = null; + + switch (shape.getShapeType()) { + + case MARKER: + Marker shapeMarker = (Marker) shape.getShape(); + marker = createEditMarker(context, map, shapeMarker.getPosition()); + break; + + case POLYLINE: + Polyline polyline = (Polyline) shape.getShape(); + LatLng polylinePoint = polyline.getPoints().get(0); + marker = createEditMarker(context, map, polylinePoint); + break; + + case POLYGON: + Polygon polygon = (Polygon) shape.getShape(); + LatLng polygonPoint = polygon.getPoints().get(0); + marker = createEditMarker(context, map, polygonPoint); + break; + + case MULTI_MARKER: + MultiMarker multiMarker = (MultiMarker) shape.getShape(); + marker = createEditMarker(context, map, multiMarker.getMarkers().get(0) + .getPosition()); + break; + + case MULTI_POLYLINE: + MultiPolyline multiPolyline = (MultiPolyline) shape.getShape(); + LatLng multiPolylinePoint = multiPolyline.getPolylines().get(0) + .getPoints().get(0); + marker = createEditMarker(context, map, multiPolylinePoint); + break; + + case MULTI_POLYGON: + MultiPolygon multiPolygon = (MultiPolygon) shape.getShape(); + LatLng multiPolygonPoint = multiPolygon.getPolygons().get(0) + .getPoints().get(0); + marker = createEditMarker(context, map, multiPolygonPoint); + break; + + case COLLECTION: + @SuppressWarnings("unchecked") + List shapes = (List) shape + .getShape(); + for (GoogleMapShape listShape : shapes) { + marker = getMarker(context, map, listShape); + if (marker != null) { + break; + } + } + break; + default: + } + + return marker; + } + + /** + * Create an edit marker to edit polylines and polygons + * + * @param context The application context. + * @param map The map to add the marker to. + * @param latLng The latitude and longitude of the markers location. + * @return The marker to add to the map. + */ + private Marker createEditMarker(Context context, GoogleMap map, LatLng latLng) { + MarkerOptions markerOptions = new MarkerOptions(); + markerOptions.position(latLng); + markerOptions.icon(BitmapDescriptorFactory + .fromResource(R.drawable.ic_shape_edit)); + TypedValue typedValueWidth = new TypedValue(); + context.getResources().getValue(R.dimen.shape_edit_icon_anchor_width, + typedValueWidth, true); + TypedValue typedValueHeight = new TypedValue(); + context.getResources().getValue(R.dimen.shape_edit_icon_anchor_height, + typedValueHeight, true); + markerOptions.anchor(typedValueWidth.getFloat(), + typedValueHeight.getFloat()); + return map.addMarker(markerOptions); + } + + /** + * Helps ensure a singleton. + */ + private ShapeHelper() { + + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/Zoomer.java b/mapcache/src/main/java/mil/nga/mapcache/Zoomer.java new file mode 100644 index 00000000..1a9f2046 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/Zoomer.java @@ -0,0 +1,323 @@ +package mil.nga.mapcache; + +import android.content.Context; +import android.util.Log; +import android.view.View; + +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import mil.nga.geopackage.BoundingBox; +import mil.nga.geopackage.GeoPackage; +import mil.nga.geopackage.features.user.FeatureCursor; +import mil.nga.geopackage.features.user.FeatureDao; +import mil.nga.geopackage.features.user.FeatureRow; +import mil.nga.geopackage.map.MapUtils; +import mil.nga.geopackage.map.tiles.TileBoundingBoxMapUtils; +import mil.nga.geopackage.tiles.TileBoundingBoxUtils; +import mil.nga.geopackage.tiles.matrixset.TileMatrixSet; +import mil.nga.geopackage.tiles.matrixset.TileMatrixSetDao; +import mil.nga.mapcache.data.GeoPackageDatabase; +import mil.nga.mapcache.data.GeoPackageFeatureOverlayTable; +import mil.nga.mapcache.data.GeoPackageFeatureTable; +import mil.nga.mapcache.data.GeoPackageTileTable; +import mil.nga.mapcache.utils.ProjUtils; +import mil.nga.mapcache.viewmodel.GeoPackageViewModel; +import mil.nga.proj.ProjectionConstants; +import mil.nga.sf.GeometryEnvelope; +import mil.nga.sf.GeometryType; + +/** + * Controls zooming to various parts on the map. + */ +public class Zoomer { + + /** + * Debug logging flag. + */ + private static final boolean isDebug = false; + + /** + * Contains the various states of the map. + */ + private final MapModel model; + + /** + * Contains the geoPackages. + */ + private final GeoPackageViewModel geoPackageViewModel; + + /** + * The application context. + */ + private final Context context; + + /** + * The map to zoom around. + */ + private final GoogleMap map; + + /** + * The view containing the map. + */ + private final View mainView; + + /** + * Constructor. + * + * @param model Contains the various states of the map. + * @param geoPackageViewModel Contains the geoPackages. + * @param context The application context. + * @param map The map to zoom around. + * @param mainView The view containing the map. + */ + public Zoomer(MapModel model, GeoPackageViewModel geoPackageViewModel, Context context, GoogleMap map, View mainView) { + this.model = model; + this.geoPackageViewModel = geoPackageViewModel; + this.context = context; + this.map = map; + this.mainView = mainView; + } + + /** + * Zoom to the active feature and tile table data bounds + */ + public void zoomToActiveBounds() { + + model.setFeaturesBoundingBox(null); + model.setTilesBoundingBox(null); + + // Pre zoom + List activeDatabases = new ArrayList<>(model.getActive().getDatabases()); + for (GeoPackageDatabase database : activeDatabases) { + GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(database.getDatabase()); + if (geoPackage != null) { + + Set featureTableDaos = new HashSet<>(); + Collection features = database.getFeatures(); + if (!features.isEmpty()) { + for (GeoPackageFeatureTable featureTable : features) { + featureTableDaos.add(featureTable.getName()); + } + } + + for (GeoPackageFeatureOverlayTable featureOverlay : database.getFeatureOverlays()) { + if (featureOverlay.isActive()) { + featureTableDaos.add(featureOverlay.getFeatureTable()); + } + } + + if (!featureTableDaos.isEmpty()) { + for (String featureTable : featureTableDaos) { + if (featureTable != null && !featureTable.isEmpty()) { + FeatureDao featureDao = geoPackage.getFeatureDao(featureTable); + String[] columns = featureDao.getIdAndGeometryColumnNames(); + BoundingBox contentsBoundingBox = null; + try (FeatureCursor cursor = featureDao.query(columns)) { + List boxes = new ArrayList<>(); + while (cursor.moveToNext()) { + FeatureRow row = cursor.getRow(); + try { + GeometryType type = row.getGeometryType(); + GeometryEnvelope envelope = row.getGeometry().buildEnvelope(); + BoundingBox rowBox = new BoundingBox(envelope); + if (isDebug) { + boxes.add(new BoundingBox(rowBox)); + } + if (contentsBoundingBox == null) { + contentsBoundingBox = rowBox; + } else { + contentsBoundingBox = contentsBoundingBox.union(rowBox); + } + } catch (Exception e){ + Log.e("Opening GeometryEnvelope: ", e.toString()); + } + } + + for (BoundingBox box : boxes) { + Log.d(Zoomer.class.getSimpleName(), + "BBox " + + box.getMinLatitude() + " " + + box.getMinLongitude() + " " + + box.getMaxLatitude() + " " + + box.getMaxLongitude()); + } + + if (isDebug && contentsBoundingBox != null) { + Log.d(Zoomer.class.getSimpleName(), + "Contents BBox " + + contentsBoundingBox.getMinLatitude() + " " + + contentsBoundingBox.getMinLongitude() + " " + + contentsBoundingBox.getMaxLatitude() + " " + + contentsBoundingBox.getMaxLongitude()); + } + } + + if (contentsBoundingBox != null) { + contentsBoundingBox = ProjUtils.getInstance() + .transformBoundingBoxToWgs84( + contentsBoundingBox, + featureDao.getSrs()); + + if (model.getFeaturesBoundingBox() != null) { + model.setFeaturesBoundingBox(model.getFeaturesBoundingBox().union(contentsBoundingBox)); + } else { + model.setFeaturesBoundingBox(contentsBoundingBox); + } + } + } + } + } + + Collection tileTables = database.getTiles(); + if (!tileTables.isEmpty()) { + + TileMatrixSetDao tileMatrixSetDao = geoPackage.getTileMatrixSetDao(); + + for (GeoPackageTileTable tileTable : tileTables) { + + try { + TileMatrixSet tileMatrixSet = tileMatrixSetDao.queryForId(tileTable.getName()); + BoundingBox tileMatrixSetBoundingBox = tileMatrixSet.getContents().getBoundingBox(); + + tileMatrixSetBoundingBox = ProjUtils.getInstance() + .transformBoundingBoxToWgs84( + tileMatrixSetBoundingBox, + tileMatrixSet.getSrs()); + + if (model.getTilesBoundingBox() != null) { + model.setTilesBoundingBox(model.getTilesBoundingBox().union(tileMatrixSetBoundingBox)); + } else { + model.setTilesBoundingBox(tileMatrixSetBoundingBox); + } + } catch (SQLException e) { + Log.e(GeoPackageMapFragment.class.getSimpleName(), + e.getMessage()); + } + } + } + } + } + + zoomToActive(); + } + + /** + * Zoom to features on the map, or tiles if no features + */ + public void zoomToActive() { + zoomToActive(false); + } + + /** + * Zoom to features on the map, or tiles if no features + * + * @param nothingVisible zoom only if nothing is currently visible + */ + public void zoomToActive(boolean nothingVisible) { + + BoundingBox bbox = model.getFeaturesBoundingBox(); + boolean tileBox = false; + + float paddingPercentage; + if (bbox == null) { + bbox = model.getTilesBoundingBox(); + tileBox = true; + if (model.isFeatureOverlayTiles()) { + paddingPercentage = context.getResources().getInteger( + R.integer.map_feature_tiles_zoom_padding_percentage) * .01f; + } else { + paddingPercentage = context.getResources().getInteger( + R.integer.map_tiles_zoom_padding_percentage) * .01f; + } + } else { + paddingPercentage = context.getResources().getInteger( + R.integer.map_features_zoom_padding_percentage) * .01f; + } + + if (bbox != null) { + + boolean zoomToActive = true; + if (nothingVisible) { + BoundingBox mapViewBoundingBox = MapUtils.getBoundingBox(map); + if (TileBoundingBoxUtils.overlap(bbox, mapViewBoundingBox, ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH) != null) { + + double longitudeDistance = TileBoundingBoxMapUtils.getLongitudeDistance(bbox); + double latitudeDistance = TileBoundingBoxMapUtils.getLatitudeDistance(bbox); + double mapViewLongitudeDistance = TileBoundingBoxMapUtils.getLongitudeDistance(mapViewBoundingBox); + double mapViewLatitudeDistance = TileBoundingBoxMapUtils.getLatitudeDistance(mapViewBoundingBox); + + if (mapViewLongitudeDistance > longitudeDistance && mapViewLatitudeDistance > latitudeDistance) { + + double longitudeRatio = longitudeDistance / mapViewLongitudeDistance; + double latitudeRatio = latitudeDistance / mapViewLatitudeDistance; + + double zoomAlreadyVisiblePercentage; + if (tileBox) { + zoomAlreadyVisiblePercentage = context.getResources().getInteger( + R.integer.map_tiles_zoom_already_visible_percentage) * .01f; + } else { + zoomAlreadyVisiblePercentage = context.getResources().getInteger( + R.integer.map_features_zoom_already_visible_percentage) * .01f; + } + + if (longitudeRatio >= zoomAlreadyVisiblePercentage && latitudeRatio >= zoomAlreadyVisiblePercentage) { + zoomToActive = false; + } + } + } + } + + if (zoomToActive) { + double minLatitude = Math.max(bbox.getMinLatitude(), ProjectionConstants.WEB_MERCATOR_MIN_LAT_RANGE); + double maxLatitude = Math.min(bbox.getMaxLatitude(), ProjectionConstants.WEB_MERCATOR_MAX_LAT_RANGE); + + LatLng lowerLeft = new LatLng(minLatitude, bbox.getMinLongitude()); + LatLng lowerRight = new LatLng(minLatitude, bbox.getMaxLongitude()); + LatLng topLeft = new LatLng(maxLatitude, bbox.getMinLongitude()); + LatLng topRight = new LatLng(maxLatitude, bbox.getMaxLongitude()); + + if (lowerLeft.longitude == lowerRight.longitude) { + double adjustLongitude = lowerRight.longitude - .0000000000001; + lowerRight = new LatLng(minLatitude, adjustLongitude); + topRight = new LatLng(maxLatitude, adjustLongitude); + } + + final LatLngBounds.Builder boundsBuilder = new LatLngBounds.Builder(); + boundsBuilder.include(lowerLeft); + boundsBuilder.include(lowerRight); + boundsBuilder.include(topLeft); + boundsBuilder.include(topRight); + + int minViewLength = mainView != null ? Math.min(mainView.getWidth(), mainView.getHeight()) : 1; + final int padding = (int) Math.floor(minViewLength + * paddingPercentage); + + try { + LatLngBounds bounds = boundsBuilder.build(); + if (isDebug) { + Log.d(Zoomer.class.getSimpleName(), "LatLngBounds " + + bounds.southwest.latitude + " " + + bounds.southwest.longitude + " " + + bounds.northeast.latitude + " " + + bounds.northeast.longitude); + } + map.animateCamera(CameraUpdateFactory.newLatLngBounds( + bounds, padding)); + } catch (Exception e) { + Log.w(GeoPackageMapFragment.class.getSimpleName(), + "Unable to move camera", e); + } + } + } + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/auth/Authenticator.java b/mapcache/src/main/java/mil/nga/mapcache/auth/Authenticator.java index 93adcab2..8dbdce19 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/auth/Authenticator.java +++ b/mapcache/src/main/java/mil/nga/mapcache/auth/Authenticator.java @@ -15,5 +15,12 @@ public interface Authenticator { * @param password The user's password. * @return True if authentication is successful, false if it failed. */ - public boolean authenticate(URL url, String userName, String password); + boolean authenticate(URL url, String userName, String password); + + /** + * Indicates if we should save the account to the phone after a successful authentication. + * + * @return True if the account should be saved after successful authentication, false if it should not. + */ + boolean shouldSaveAccount(); } diff --git a/mapcache/src/main/java/mil/nga/mapcache/auth/UserLoggerInner.java b/mapcache/src/main/java/mil/nga/mapcache/auth/UserLoggerInner.java index 7f489d87..e72c6b4d 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/auth/UserLoggerInner.java +++ b/mapcache/src/main/java/mil/nga/mapcache/auth/UserLoggerInner.java @@ -15,12 +15,12 @@ public class UserLoggerInner { /** * Unique string identifying map cache saved accounts. */ - private static String MAPCACHE_ACCOUNT_TYPE = "mil.nga.mapcache"; + private final static String MAPCACHE_ACCOUNT_TYPE = "mil.nga.mapcache"; /** * The applications activity. */ - private Activity activity; + private final Activity activity; /** * Contains what the user entered for their username and password if we needed to prompt them. @@ -42,10 +42,10 @@ public UserLoggerInner(Activity activity) { * and password is not found in the AccountManager or if authentication fails, we will then * prompt the user for a username and password authenticate and then store it for later use. * - * @param url The url to login to. - * @param IAuthenticator Object that knows how to authenticate user against the url. + * @param url The url to login to. + * @param authenticator Object that knows how to authenticate user against the url. */ - public void login(URL url, Authenticator IAuthenticator) { + public void login(URL url, Authenticator authenticator) { String host = url.getAuthority(); AccountManager accountManager = AccountManager.get(this.activity); Account[] accounts = accountManager.getAccountsByType(MAPCACHE_ACCOUNT_TYPE); @@ -57,7 +57,7 @@ public void login(URL url, Authenticator IAuthenticator) { if (split[1].equals(host)) { String userName = split[0]; String password = accountManager.getPassword(account); - authenticated = IAuthenticator.authenticate(url, userName, password); + authenticated = authenticator.authenticate(url, userName, password); if (!authenticated) { accountManager.removeAccount(account, null, null); } @@ -71,10 +71,9 @@ public void login(URL url, Authenticator IAuthenticator) { String username = model.getUsername(); String password = model.getPassword(); if (username != null && password != null) { - authenticated = IAuthenticator.authenticate(url, username, password); - if (authenticated) { - Account newAccount = new Account(username + " - " + host, MAPCACHE_ACCOUNT_TYPE); - accountManager.addAccountExplicitly(newAccount, password, null); + authenticated = authenticator.authenticate(url, username, password); + if (authenticated && authenticator.shouldSaveAccount()) { + saveLastAccount(); } } else { // Not really authenticated, user cancelled the login @@ -83,6 +82,22 @@ public void login(URL url, Authenticator IAuthenticator) { } } + /** + * Saves the account to the phone. + */ + public void saveLastAccount() { + if (model != null) { + String userName = model.getUsername(); + String password = model.getPassword(); + String host = model.getLoginTo(); + if (userName != null && password != null && host != null) { + AccountManager accountManager = AccountManager.get(this.activity); + Account newAccount = new Account(userName + " - " + host, MAPCACHE_ACCOUNT_TYPE); + accountManager.addAccountExplicitly(newAccount, password, null); + } + } + } + /** * Prompts the user for their username and password, on the UI thread, and waits for the users * response before returning. @@ -106,7 +121,7 @@ private synchronized void askUserAndWait(String host) { private void askUser(String host) { UserPassDialog dialog = new UserPassDialog(this.activity, host); model = dialog.getModel(); - model.addObserver((model, property) -> onUpdate(model, property)); + model.addObserver(this::onUpdate); dialog.show(); } diff --git a/mapcache/src/main/java/mil/nga/mapcache/auth/UserPassDialog.java b/mapcache/src/main/java/mil/nga/mapcache/auth/UserPassDialog.java index 1095ee20..7fefa7c1 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/auth/UserPassDialog.java +++ b/mapcache/src/main/java/mil/nga/mapcache/auth/UserPassDialog.java @@ -17,12 +17,12 @@ public class UserPassDialog { /** * The applications activity. */ - private Activity activity; + private final Activity activity; /** * The model for the dialog. */ - private UserPassModel model = new UserPassModel(); + private final UserPassModel model = new UserPassModel(); /** * Constructs a new username password dialog. @@ -45,8 +45,8 @@ public void show() { TextView password = (TextView) userPassView.findViewById(R.id.password); AlertDialog.Builder dialog = new AlertDialog.Builder(activity) .setView(userPassView); - dialog.setNegativeButton("Cancel", (var1, var2) -> onCancelClicked(var1, var2)); - dialog.setPositiveButton("Login", (var1, var2) -> onLoginClicked(var1, var2, username, password)); + dialog.setNegativeButton("Cancel", this::onCancelClicked); + dialog.setPositiveButton("Login", (var1, var2) -> onLoginClicked(username, password)); dialog.setTitle("Login To"); dialog.setMessage(model.getLoginTo()); dialog.create().show(); @@ -55,7 +55,7 @@ public void show() { /** * Gets the login information. * - * @return + * @return The model containing the login info. */ public UserPassModel getModel() { return model; @@ -64,12 +64,10 @@ public UserPassModel getModel() { /** * Called when login is clicked. * - * @param dialog The login dialog. - * @param var2 Button pressed state. * @param username The username text input. * @param password The password text input. */ - private void onLoginClicked(DialogInterface dialog, int var2, TextView username, TextView password) { + private void onLoginClicked(TextView username, TextView password) { model.setUsername(username.getText().toString()); model.setPassword(password.getText().toString()); } diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/CookieJar.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/CookieJar.java deleted file mode 100644 index 1614ac4a..00000000 --- a/mapcache/src/main/java/mil/nga/mapcache/io/network/CookieJar.java +++ /dev/null @@ -1,25 +0,0 @@ -package mil.nga.mapcache.io.network; - -import java.util.Map; - -/** - * Stores cookies for specific servers. - */ -public interface CookieJar { - - /** - * Stores the cookies for the specified host. - * - * @param host The host to store cookies for. - * @param cookies The cookies to store. - */ - public void storeCookies(String host, Map cookies); - - /** - * Gets the cookies for the specified host. - * - * @param host The host to get the cookies for. - * @return The cookies for the host, or null if there aren't any. - */ - public Map getCookies(String host); -} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpClient.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpClient.java index 1b932e2f..08bbe623 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpClient.java +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpClient.java @@ -1,25 +1,47 @@ package mil.nga.mapcache.io.network; import android.app.Activity; +import android.util.Log; + +import java.net.MalformedURLException; +import java.net.URL; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import mil.nga.mapcache.io.network.slowserver.SlowServerNotifier; import mil.nga.mapcache.utils.ThreadUtils; /** * Makes http requests asynchronously. */ -public class HttpClient implements CookieJar { +public class HttpClient implements SessionManager { /** * The instance of this class. */ private static final HttpClient instance = new HttpClient(); + /** + * Debug logging flag. + */ + private static final boolean isDebug = false; + /** * Any cookies being stored for http requests. */ - private Map> allCookies = new HashMap<>(); + private final Map> allCookies = new HashMap<>(); + + /** + * The hosts that require a web view in order to download from given urls. + */ + private final Set webViewHosts = new HashSet<>(); + + /** + * If the server is slow, this will notify the user of that. + */ + private SlowServerNotifier notifier = null; /** * Gets the instance of this class. @@ -37,9 +59,27 @@ public static HttpClient getInstance() { * @param handler The response handler, called when request is complete. * @param activity Used to get the app name and version for the user agent. */ - public void sendGet(String url, IResponseHandler handler, Activity activity) { - HttpGetRequest request = new HttpGetRequest(url, handler, this, activity); - ThreadUtils.getInstance().runBackground(request); + public synchronized void sendGet(String url, IResponseHandler handler, Activity activity) { + try { + URL theUrl = new URL(url); + String host = theUrl.getHost(); + boolean requiresWebView = webViewHosts.contains(host); + + if(notifier == null) { + notifier = new SlowServerNotifier(activity); + } + + ResponseMonitor monitor = new ResponseMonitor(host, handler, notifier); + monitor.start(); + if(requiresWebView) { + requestRequiresWebView(url, monitor, activity); + } else { + HttpGetRequest request = new HttpGetRequest(url, monitor, this, activity); + ThreadUtils.getInstance().runBackground(request); + } + } catch (MalformedURLException e) { + Log.e(HttpClient.class.getSimpleName(), e.getMessage(), e); + } } /** @@ -57,4 +97,21 @@ public synchronized void storeCookies(String host, Map cookies) public synchronized Map getCookies(String host) { return allCookies.get(host); } + + @Override + public synchronized void requestRequiresWebView(String url, IResponseHandler handler, Activity activity) { + try { + URL theUrl = new URL(url); + webViewHosts.add(theUrl.getHost()); + } catch (MalformedURLException e) { + Log.e(HttpClient.class.getSimpleName(), e.getMessage(), e); + } + if(isDebug) { + Log.d(HttpClient.class.getSimpleName(), "Using web view for request " + url); + } + activity.runOnUiThread(()->{ + WebViewRequest request = new WebViewRequest(url, handler, activity); + request.execute(); + }); + } } diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpGetRequest.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpGetRequest.java index 4a873281..21a23b95 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpGetRequest.java +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/HttpGetRequest.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.util.Base64; import android.util.Log; +import android.webkit.CookieManager; import java.io.IOException; import java.io.InputStream; @@ -12,7 +13,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.zip.GZIPInputStream; @@ -28,22 +28,22 @@ public class HttpGetRequest implements Runnable, Authenticator { /** * Used to turn debug logging on. */ - private static boolean isDebug = false; + private static final boolean isDebug = false; /** * The url of the get request. */ - private String urlString; + private final String urlString; /** * Object that is called when request is completed. */ - private IResponseHandler handler; + private final IResponseHandler handler; /** * Used to get the app name and version for the user agent. */ - private Activity activity; + private final Activity activity; /** * Retrieves the user's username and password then calls back for authentication. @@ -68,61 +68,68 @@ public class HttpGetRequest implements Runnable, Authenticator { /** * Contains any previously saved cookies. */ - private CookieJar allCookies; + private final SessionManager sessionManager; + + /** + * True if the WebViewRequest is handling the request. + */ + private boolean webViewHandlingRequest = false; /** * Constructs a new HttpGetRequest. * * @param url The url of the get request. * @param handler Object this is called when request is completed. + * @param sessionManager Contains any saved cookies. * @param activity Used to get the app name and version for the user agent. */ - public HttpGetRequest(String url, IResponseHandler handler, CookieJar allCookies, Activity activity) { + public HttpGetRequest(String url, IResponseHandler handler, SessionManager sessionManager, Activity activity) { this.urlString = url; this.handler = handler; this.activity = activity; - this.allCookies = allCookies; + this.sessionManager = sessionManager; } @Override public void run() { try { authorization = null; + webViewHandlingRequest = false; URL url = new URL(urlString); - cookies = allCookies.getCookies(url.getHost()); + cookies = sessionManager.getCookies(url.getHost()); connect(url); - int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED - && (urlString.startsWith("https") || urlString.contains("10.0.2.2"))) { - addBasicAuth(connection.getURL()); - responseCode = connection.getResponseCode(); - } - - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - this.handler.handleResponse(null, responseCode); - } else { - InputStream stream = connection.getInputStream(); - String encoding = connection.getHeaderField(HttpUtils.getInstance().getContentEncodingKey()); - if (encoding != null && encoding.equals("gzip")) { - stream = new GZIPInputStream(stream); + if(!webViewHandlingRequest) { + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED + && (urlString.startsWith("https") || urlString.contains("10.0.2.2"))) { + addBasicAuth(connection.getURL()); + responseCode = connection.getResponseCode(); } - this.handler.handleResponse(stream, responseCode); - if (this.handler instanceof RequestHeaderConsumer) { - Map> headers = new HashMap<>(); - if (this.authorization != null) { - headers.put(HttpUtils.getInstance().getBasicAuthKey(), new ArrayList<>()); - headers.get(HttpUtils.getInstance().getBasicAuthKey()).add(this.authorization); + + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + this.handler.handleResponse(null, responseCode); + } else { + InputStream stream = connection.getInputStream(); + String encoding = connection.getHeaderField(HttpUtils.getInstance().getContentEncodingKey()); + if (encoding != null && encoding.equals("gzip")) { + stream = new GZIPInputStream(stream); } + this.handler.handleResponse(stream, responseCode); + if (this.handler instanceof RequestHeaderConsumer) { + Map> headers = new HashMap<>(); + if (this.authorization != null) { + List authorizations = new ArrayList<>(); + authorizations.add(this.authorization); + headers.put(HttpUtils.getInstance().getBasicAuthKey(), authorizations); + } - if (this.cookies != null) { - List cookieValues = new ArrayList<>(); - for (String cookie : this.cookies.values()) { - cookieValues.add(cookie); + if (this.cookies != null) { + List cookieValues = new ArrayList<>(this.cookies.values()); + headers.put(HttpUtils.getInstance().getCookieKey(), cookieValues); } - headers.put(HttpUtils.getInstance().getCookieKey(), cookieValues); + ((RequestHeaderConsumer) this.handler).setRequestHeaders(headers); } - ((RequestHeaderConsumer) this.handler).setRequestHeaders(headers); } } } catch (IOException e) { @@ -157,6 +164,11 @@ public boolean authenticate(URL url, String userName, String password) { return authorized; } + @Override + public boolean shouldSaveAccount() { + return true; + } + /** * Adds basic auth to the connection. * @@ -174,7 +186,7 @@ private void addBasicAuth(URL url) { * * @param connection The connection to add the user agent to. */ - private void configureRequest(HttpURLConnection connection) { + private void configureRequest(HttpURLConnection connection, boolean isRedirect) { connection.addRequestProperty( HttpUtils.getInstance().getUserAgentKey(), HttpUtils.getInstance().getUserAgentValue(activity)); @@ -192,6 +204,14 @@ private void configureRequest(HttpURLConnection connection) { if (authorization != null) { connection.addRequestProperty(HttpUtils.getInstance().getBasicAuthKey(), authorization); + } + + String cookieString = CookieManager.getInstance().getCookie(connection.getURL().toString()); + if(!isRedirect && cookieString != null) { + String [] allCookies = cookieString.split(";"); + for(String cookie : allCookies) { + connection.addRequestProperty(HttpUtils.getInstance().getCookieKey(), cookie); + } } else if (cookies != null) { for (String cookie : cookies.values()) { connection.addRequestProperty(HttpUtils.getInstance().getCookieKey(), cookie); @@ -208,7 +228,7 @@ private void connect(URL url) { try { connection = (HttpURLConnection) url.openConnection(); connection.setInstanceFollowRedirects(false); - configureRequest(connection); + configureRequest(connection, false); if (isDebug) { Log.d(HttpGetRequest.class.getSimpleName(), "Connecting to " + url); for (Map.Entry> entry : connection.getRequestProperties().entrySet()) { @@ -226,7 +246,6 @@ private void connect(URL url) { } } checkCookie(); - int index = 0; while (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_SEE_OTHER) { @@ -244,7 +263,7 @@ private void connect(URL url) { connection = (HttpURLConnection) url.openConnection(); connection.setInstanceFollowRedirects(false); - configureRequest(connection); + configureRequest(connection, true); if (isDebug) { Log.d(HttpGetRequest.class.getSimpleName(), "Redirecting to " + url); for (Map.Entry> entry : connection.getRequestProperties().entrySet()) { @@ -260,7 +279,11 @@ private void connect(URL url) { } } checkCookie(); - index++; + + if(responseCode == HttpURLConnection.HTTP_OK) { + sessionManager.requestRequiresWebView(urlString, handler, activity); + webViewHandlingRequest = true; + } } } catch (IOException e) { Log.e(HttpGetRequest.class.getSimpleName(), e.getMessage(), e); @@ -283,7 +306,7 @@ private void checkCookie() throws MalformedURLException { this.cookies.put(nameValue[0], cookie); } URL originalUrl = new URL(urlString); - allCookies.storeCookies(originalUrl.getHost(), this.cookies); + sessionManager.storeCookies(originalUrl.getHost(), this.cookies); } } } diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/IResponseHandler.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/IResponseHandler.java index 3a1a925b..3d07475c 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/io/network/IResponseHandler.java +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/IResponseHandler.java @@ -11,15 +11,22 @@ public interface IResponseHandler { /** * Handles the stream returned from an http request. * - * @param stream The response from the server, or null if bad response from server. + * @param stream The response from the server, or null if bad response from server. * @param responseCode The http response code from the server. */ - public void handleResponse(InputStream stream, int responseCode); + void handleResponse(InputStream stream, int responseCode); /** * Handles the exception when trying to perform an http request. * * @param exception The exception to handle. */ - public void handleException(IOException exception); + void handleException(IOException exception); + + /** + * Indicates if the request has been cancelled. + * + * @return True if the request is cancelled false if not cancelled. + */ + boolean notCancelled(); } diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/ResponseMonitor.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/ResponseMonitor.java new file mode 100644 index 00000000..9561428f --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/ResponseMonitor.java @@ -0,0 +1,71 @@ +package mil.nga.mapcache.io.network; + +import java.io.IOException; +import java.io.InputStream; + +import mil.nga.mapcache.io.network.slowserver.SlowServerNotifier; + +/** + * Monitors for a download response and records the time. Routes all handler calls to the original + * handler. + */ +public class ResponseMonitor implements IResponseHandler { + + /** + * The host waiting for download from. + */ + private final String host; + + /** + * The original response handler. + */ + private final IResponseHandler handler; + + /** + * If the server is slow, this will notify the user of that. + */ + private final SlowServerNotifier notifier; + + /** + * The start time of the download. + */ + private long startTime; + + /** + * Constructor. + * + * @param host The host we are downloading from. + * @param handler The original handler to route calls too. + * @param notifier If the server is slow, this will notify the user of that. + */ + public ResponseMonitor(String host, IResponseHandler handler, SlowServerNotifier notifier) { + this.host = host; + this.handler = handler; + this.notifier = notifier; + } + + /** + * Records the start time of now. + */ + public void start() { + startTime = System.currentTimeMillis(); + } + + @Override + public void handleResponse(InputStream stream, int responseCode) { + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + notifier.responseTime(host, duration); + handler.handleResponse(stream, responseCode); + } + + @Override + public void handleException(IOException exception) { + handler.handleException(exception); + } + + @Override + public boolean notCancelled() { + return handler.notCancelled(); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/SessionManager.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/SessionManager.java new file mode 100644 index 00000000..95b9a4f8 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/SessionManager.java @@ -0,0 +1,36 @@ +package mil.nga.mapcache.io.network; + +import android.app.Activity; + +import java.util.Map; + +/** + * Stores cookies for specific servers. + */ +public interface SessionManager { + + /** + * Stores the cookies for the specified host. + * + * @param host The host to store cookies for. + * @param cookies The cookies to store. + */ + void storeCookies(String host, Map cookies); + + /** + * Gets the cookies for the specified host. + * + * @param host The host to get the cookies for. + * @return The cookies for the host, or null if there aren't any. + */ + Map getCookies(String host); + + /** + * Executes the request at the specified url using a WebView. + * + * @param url The request url. + * @param handler The response handler. + * @param activity The activity that initiated the request. + */ + void requestRequiresWebView(String url, IResponseHandler handler, Activity activity); +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewContentRetriever.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewContentRetriever.java new file mode 100644 index 00000000..535229a1 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewContentRetriever.java @@ -0,0 +1,172 @@ +package mil.nga.mapcache.io.network; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.util.Log; +import android.webkit.ValueCallback; +import android.webkit.WebView; + +import java.io.InputStream; +import java.util.Observable; +import java.util.Observer; + +import mil.nga.mapcache.utils.ThreadUtils; + +/** + * Class that is called from within the web view page. + */ +public class WebViewContentRetriever implements Observer, ValueCallback { + + /** + * Debug logging flag. + */ + private static final boolean isDebug = false; + + /** + * The javascript that returns the urls html content. + */ + private static final String theJavaScript = "(function() { return " + + "(document.getElementsByTagName('html')[0].innerText); })();"; + + /** + * Contains the current url and html. + */ + private final WebViewRequestModel model; + + /** + * The web view being used to load urls. + */ + private final WebView webView; + + /** + * Creates extractors to be used on the web view and grab certain contents within it. + */ + private final WebViewExtractorFactory extractorFactory; + + /** + * Used to run background tasks back on the UI thread. + */ + private final Activity activity; + + /** + * The html that goes with the extractor. + */ + private String currentHtml; + + /** + * The current extractor needing execution. + */ + private WebViewExtractor currentExtractor; + + /** + * Constructor. + * + * @param activity Used to run background tasks back on the UI thread. + * @param webView The web view to get the content from. + * @param model Contains the current url and html. + */ + @SuppressLint("SetJavaScriptEnabled") + public WebViewContentRetriever(Activity activity, WebView webView, WebViewRequestModel model) { + this.activity = activity; + this.webView = webView; + this.webView.getSettings().setJavaScriptEnabled(true); + this.model = model; + extractorFactory = new WebViewExtractorFactory(this.webView, this.model); + this.model.addObserver(this); + } + + @Override + public void update(Observable observable, Object o) { + if (WebViewRequestModel.CURRENT_URL_PROP.equals(o)) { + if (isDebug) { + Log.d( + WebViewContentRetriever.class.getSimpleName(), + "Evaluating javascript " + this.model.getCurrentUrl()); + } + this.webView.evaluateJavascript(theJavaScript, this); + } + } + + /** + * Stops getting the html for urls. + */ + public void close() { + this.model.deleteObserver(this); + } + + @Override + public void onReceiveValue(String html) { + if (isDebug) { + Log.d( + WebViewContentRetriever.class.getSimpleName(), + "Evaluated javascript " + this.model.getCurrentUrl()); + } + WebViewExtractor extractor = extractorFactory.createExtractor(html); + synchronized (this) { + currentExtractor = extractor; + currentHtml = html; + } + + if (extractor != null) { + ThreadUtils.getInstance().runBackground(this::waitBackBeforeExtract); + } + } + + /** + * Waits in a background thread for the web page to fully load before extracting its content. + */ + private void waitBackBeforeExtract() { + try { + if (isDebug) { + Log.d( + WebViewContentRetriever.class.getSimpleName(), + "Wait back before extract " + this.model.getCurrentUrl()); + } + Thread.sleep(100); + } catch (InterruptedException e) { + Log.d(WebViewContentRetriever.class.getSimpleName(), e.getMessage(), e); + } + + this.activity.runOnUiThread(this::extractContent); + } + + /** + * Extracts the contents from the web page. + */ + private void extractContent() { + synchronized (this) { + if (isDebug) { + Log.d( + WebViewContentRetriever.class.getSimpleName(), + "Extract content " + this.model.getCurrentUrl()); + } + if (currentExtractor != null) { + if (currentExtractor.readyForExtraction(currentHtml)) { + if(isDebug) { + Log.d( + WebViewContentRetriever.class.getSimpleName(), + "Extracting content " + this.model.getCurrentUrl()); + } + InputStream content = currentExtractor.extractContent(currentHtml); + if (content != null) { + if (isDebug) { + Log.d(WebViewContentRetriever.class.getSimpleName(), "Extracted content " + this.model.getCurrentUrl()); + } + this.model.setCurrentContent(content); + } else if (isDebug) { + Log.d( + WebViewContentRetriever.class.getSimpleName(), + "Content null " + this.model.getCurrentUrl()); + } + } else { + if(isDebug) { + Log.d( + WebViewContentRetriever.class.getSimpleName(), + "Not ready for extraction " + this.model.getCurrentUrl()); + } + ThreadUtils.getInstance().runBackground(this::waitBackBeforeExtract); + } + } + } + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewExtractor.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewExtractor.java new file mode 100644 index 00000000..0de5a4d8 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewExtractor.java @@ -0,0 +1,26 @@ +package mil.nga.mapcache.io.network; + +import java.io.InputStream; + +/** + * Given a web pages html, the extractor will retrieve certain content from the web page to be used + * within an InputStream. + */ +public interface WebViewExtractor { + + /** + * Indicates if the web page is ready for its contents to be extracted. + * + * @param html The html of the web page. + * @return True if we can extract, false if we need to wait a while longer. + */ + boolean readyForExtraction(String html); + + /** + * Extracts certain contents from the given html. + * + * @param html The html to extract data from. + * @return The extracted content. + */ + InputStream extractContent(String html); +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewExtractorFactory.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewExtractorFactory.java new file mode 100644 index 00000000..1d8a5411 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewExtractorFactory.java @@ -0,0 +1,49 @@ +package mil.nga.mapcache.io.network; + +import android.webkit.WebView; + +/** + * Based on the html content this class will return the appropriate WebViewExtractor. + */ +public class WebViewExtractorFactory { + + /** + * The web view to get the image from. + */ + private final WebView webView; + + /** + * Keeps track of the current url. + */ + private final WebViewRequestModel model; + + /** + * Constructor. + * + * @param webView The web view to get the image from. + * @param model Keeps track of the current url. + */ + public WebViewExtractorFactory(WebView webView, WebViewRequestModel model) { + this.webView = webView; + this.model = model; + } + + /** + * Creates the appropriate extractor based on the html content. + * + * @param html The html to inspect. + * @return The extractor or null if it couldn't find one. + */ + public WebViewExtractor createExtractor(String html) { + + WebViewExtractor extractor = null; + + if (html.startsWith("\"This XML file")) { + extractor = new WebViewXmlExtractor(); + } else if (html.contains("\"\"") && this.model.getCurrentUrl().contains("format=image")) { + extractor = new WebViewImageExtractor(this.webView); + } + + return extractor; + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewImageExtractor.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewImageExtractor.java new file mode 100644 index 00000000..9fbd9929 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewImageExtractor.java @@ -0,0 +1,169 @@ +package mil.nga.mapcache.io.network; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.Log; +import android.view.View.MeasureSpec; +import android.webkit.WebView; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Gets the image displayed on the web page and returns it in an input stream. + */ +public class WebViewImageExtractor implements WebViewExtractor { + + /** + * The web view to get the image from. + */ + private final WebView webView; + + /** + * The bitmap of the web page. + */ + private Bitmap bitmap; + + /** + * The x, y start locations and the width and height of the image within the page. + */ + private int[] offsetsWidthHeight; + + /** + * Constructor. + * + * @param view The web view to get the image from. + */ + public WebViewImageExtractor(WebView view) { + this.webView = view; + } + + @Override + public boolean readyForExtraction(String html) { + boolean isReady = false; + + webView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int width = webView.getMeasuredWidth(); + int height = webView.getMeasuredHeight(); + webView.layout(0, 0, width, height); + webView.setDrawingCacheEnabled(true); + webView.buildDrawingCache(); + if (height > 0 && width > 0) { + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bitmap); + + Paint paint = new Paint(); + int iHeight = bitmap.getHeight(); + c.drawBitmap(bitmap, 0, iHeight, paint); + webView.draw(c); + + int halfY = height / 2; + int halfX = width / 2; + int pixel = bitmap.getPixel(halfX, halfY); + isReady = isPixelNotEmpty(pixel); + + if(isReady) { + offsetsWidthHeight = getOffsetsWidthHeight(bitmap, width, height); + isReady = offsetsWidthHeight[0] >= 0; + } + } + + return isReady; + } + + @Override + public InputStream extractContent(String html) { + InputStream is = null; + + bitmap = Bitmap.createBitmap( + bitmap, + offsetsWidthHeight[0], + offsetsWidthHeight[1], + offsetsWidthHeight[2], + offsetsWidthHeight[3]); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os); + try { + os.flush(); + byte[] theBytes = os.toByteArray(); + os.close(); + is = new ByteArrayInputStream(theBytes); + } catch (IOException e) { + Log.e(WebViewImageExtractor.class.getSimpleName(), e.getMessage(), e); + } + + return is; + } + + /** + * Gets the offsets and width and height of just the image within the web page. + * + * @param b The current bitmap of the web page. + * @param width The width of the web page bitmap. + * @param height The height of the web page bitmap. + * @return The offsets, width, and height to use when extracting just the image from the web page bitmap. + */ + private int[] getOffsetsWidthHeight(Bitmap b, int width, int height) { + int[] offsetsWidthHeight = {0, 0, width, height}; + + int halfX = width / 2; + int topMiddlePixel = b.getPixel(halfX, 0); + + if (isPixelNotEmpty(topMiddlePixel)) { + offsetsWidthHeight[0] = (width - height) / 2 + 1; + offsetsWidthHeight[2] = height - 1; + } else { + int topOffset = 0; + int halfY = height / 2; + for (int i = 1; i < halfY; i++) { + int pixel = b.getPixel(halfX, i); + if (isPixelNotEmpty(pixel)) { + topOffset = i; + break; + } + } + + int leftOffset = 0; + for (int i = 0; i < halfX; i++) { + int pixel = b.getPixel(i, halfY); + if (isPixelNotEmpty(pixel)) { + leftOffset = i; + break; + } + } + + int newWidth = width - leftOffset * 2; + int newHeight = height - topOffset * 2; + + offsetsWidthHeight[0] = leftOffset; + offsetsWidthHeight[1] = topOffset; + offsetsWidthHeight[2] = newWidth; + offsetsWidthHeight[3] = newHeight; + } + + if(offsetsWidthHeight[0] < 0) { + Log.e(WebViewImageExtractor.class.getSimpleName(), "Left offset is negative"); + } + + return offsetsWidthHeight; + } + + /** + * Checks to see if the current pixel has any color info. + * + * @param pixel The pixel to check. + * @return False if it has no color info, true if it has a color. + */ + private boolean isPixelNotEmpty(int pixel) { + int redValue = Color.red(pixel); + int blueValue = Color.blue(pixel); + int greenValue = Color.green(pixel); + + return redValue > 14 || blueValue > 14 || greenValue > 14; + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequest.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequest.java new file mode 100644 index 00000000..069e9a90 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequest.java @@ -0,0 +1,199 @@ +package mil.nga.mapcache.io.network; + +import android.app.Activity; +import android.app.AlertDialog; +import android.util.Log; +import android.webkit.WebView; + +import java.net.HttpURLConnection; +import java.util.Observable; +import java.util.Observer; + +import mil.nga.mapcache.utils.ThreadUtils; + +/** + * Performs a get request using a WebView to do so. + */ +public class WebViewRequest implements Observer { + + /** + * Debug logging flag. + */ + private static final boolean isDebug = false; + + /** + * The get request url. + */ + private final String urlString; + + /** + * The object to notify of the response values. + */ + private final IResponseHandler handler; + + /** + * The activity that initiated the request. + */ + private final Activity activity; + + /** + * The WebView used to make the request. + */ + private final WebView webView; + + /** + * The dialog showing the web page. + */ + private AlertDialog alert; + + /** + * The model used to keep track of the requests state. + */ + private final WebViewRequestModel model = new WebViewRequestModel(); + + /** + * True if the web view is being shown to the user currently. + */ + private boolean isShown = false; + + /** + * Indicates if the original url is a download imager url. + */ + private boolean isImageUrl = false; + + /** + * Used to get the html for the current url. + */ + private final WebViewContentRetriever jsInterface; + + /** + * True if this request has already received a response. + */ + private boolean receivedResponse = false; + + /** + * The number of times we have attempted reconnection. + */ + private int retryCount = 0; + + /** + * Constructor. + * + * @param url The request url. + * @param handler The object to be notified of the results. + * @param activity The activity that initiated the request. + */ + public WebViewRequest(String url, IResponseHandler handler, Activity activity) { + this.urlString = url; + this.handler = handler; + this.activity = activity; + this.webView = new WebView(activity); + this.model.addObserver(this); + WebViewRequestClient client = new WebViewRequestClient(this.activity, this.model); + this.webView.setWebViewClient(client); + this.jsInterface = new WebViewContentRetriever(this.activity, this.webView, this.model); + } + + /** + * Executes the request. + */ + public void execute() { + if (urlString.contains("format=image")) { + isImageUrl = true; + } + this.webView.layout(0, 0, 1000, 1000); + if (isDebug) { + Log.d(WebViewRequest.class.getSimpleName(), "Executing request " + this.urlString); + } + this.webView.loadUrl(this.urlString); + ThreadUtils.getInstance().runBackground(this::checkIfDead); + } + + /** + * Checks to see if the WebView object crashed and we need to restart it. + */ + private void checkIfDead() { + try { + Thread.sleep(60000); + } catch (InterruptedException e) { + Log.d(WebViewRequest.class.getSimpleName(), e.getMessage(), e); + } + + if (!receivedResponse && !isCurrentPageALogin() && this.handler.notCancelled()) { + if (retryCount < 3) { + retryCount++; + Log.i( + WebViewRequest.class.getSimpleName(), + "Connection went stale attempting to reconnect " + this.urlString); + this.activity.runOnUiThread(() -> { + this.webView.stopLoading(); + this.execute(); + }); + } else { + Log.w( + WebViewRequest.class.getSimpleName(), + "Attempted to connect " + retryCount + + " times. Giving up on " + this.urlString); + } + } + } + + /** + * Shows the login web page in an alert dialog. + */ + private void show() { + if (isDebug) { + Log.d(WebViewRequest.class.getSimpleName(), "Showing web page " + this.urlString); + } + + if(this.webView.getParent() == null) { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this.activity); + alertBuilder.setView(this.webView); + alert = alertBuilder.create(); + } + + alert.show(); + isShown = true; + } + + @Override + public void update(Observable observable, Object o) { + if (isDebug) { + Log.d(WebViewRequest.class.getSimpleName(), "Model updated " + o); + Log.d(WebViewRequest.class.getSimpleName(), "Current url " + this.model.getCurrentUrl()); + Log.d(WebViewRequest.class.getSimpleName(), "Current content " + this.model.getCurrentContent()); + } + if (WebViewRequestModel.CURRENT_URL_PROP.equals(o) && handler.notCancelled()) { + if (!isShown && isCurrentPageALogin()) { + // redirected to a login page so we need to show the WebView. + show(); + } else if (isShown && !isImageUrl) { + alert.dismiss(); + isShown = false; + } + } else if (WebViewRequestModel.CURRENT_CONTENT_PROP.equals(o)) { + jsInterface.close(); + if (isShown) { + alert.dismiss(); + isShown = false; + } + + if (isDebug) { + Log.d( + WebViewRequest.class.getSimpleName(), + "Send response to handler " + this.model.getCurrentUrl()); + } + handler.handleResponse(this.model.getCurrentContent(), HttpURLConnection.HTTP_OK); + this.receivedResponse = true; + } + } + + /** + * Checks to see if the current web page is a login page. + * + * @return True if its a login page, false otherwise. + */ + private boolean isCurrentPageALogin() { + return this.model.getCurrentUrl().contains("login."); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequestClient.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequestClient.java new file mode 100644 index 00000000..3b21c427 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequestClient.java @@ -0,0 +1,135 @@ +package mil.nga.mapcache.io.network; + +import android.app.Activity; +import android.util.Log; +import android.webkit.HttpAuthHandler; +import android.webkit.WebView; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Observable; +import java.util.Observer; + +import mil.nga.mapcache.auth.Authenticator; +import mil.nga.mapcache.auth.UserLoggerInner; +import mil.nga.mapcache.utils.ThreadUtils; + +/** + * Launches a WebView to allow the user to login to a specified url. + */ +public class WebViewRequestClient extends android.webkit.WebViewClient implements Authenticator, Observer { + + /** + * Debug logging flag. + */ + private static final boolean isDebug = false; + + /** + * The activity this was launched from. + */ + private final Activity activity; + + /** + * The current auth handler. + */ + private HttpAuthHandler currentHandler; + + /** + * The web view request model. + */ + private final WebViewRequestModel model; + + /** + * The request url. + */ + private URL url = null; + + /** + * Pops up a dialog asking user for username and password. + */ + private UserLoggerInner loggerInner; + + /** + * Indicates if the users account needs to be saved to phone. + */ + private boolean accountNeedsSaving = false; + + /** + * Constructor. + * + * @param activity The activity this was launched from. + * @param model The web view request model. + */ + public WebViewRequestClient(Activity activity, WebViewRequestModel model) { + this.activity = activity; + this.model = model; + this.model.addObserver(this); + } + + @Override + public boolean authenticate(URL url, String userName, String password) { + if(isDebug) { + Log.d(WebViewRequestClient.class.getSimpleName(), "Authenticate " + url); + } + this.currentHandler.proceed(userName, password); + accountNeedsSaving = true; + return true; + } + + @Override + public boolean shouldSaveAccount() { + return false; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if(isDebug) { + Log.d(WebViewRequestClient.class.getSimpleName(), "On Page Finished " + url); + } + + if (this.url == null) { + try { + this.url = new URL(url); + } catch (MalformedURLException e) { + Log.e(WebViewRequestClient.class.getSimpleName(), e.getMessage(), e); + } + } + + this.activity.runOnUiThread(() -> this.model.setCurrentUrl(url)); + } + + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + this.currentHandler = handler; + if(isDebug) { + Log.d(WebViewRequestClient.class.getSimpleName(), "On receive http auth request " + url); + } + ThreadUtils.getInstance().runBackground(() -> { + if(loggerInner == null) { + loggerInner = new UserLoggerInner(this.activity); + } + loggerInner.login(this.url, this); + }); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if(isDebug) { + Log.d(WebViewRequestClient.class.getSimpleName(), "should override url loading " + url); + } + view.loadUrl(url); + + return true; + } + + @Override + public void update(Observable observable, Object o) { + if (WebViewRequestModel.CURRENT_CONTENT_PROP.equals(o) + && accountNeedsSaving + && loggerInner != null) { + loggerInner.saveLastAccount(); + accountNeedsSaving = false; + } + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequestModel.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequestModel.java new file mode 100644 index 00000000..e02b2530 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewRequestModel.java @@ -0,0 +1,70 @@ +package mil.nga.mapcache.io.network; + +import java.io.InputStream; +import java.util.Observable; + +/** + * The model used by the WebViewRequest classes. + */ +public class WebViewRequestModel extends Observable { + + /** + * The current url property. + */ + public static String CURRENT_URL_PROP = "currentUrl"; + + /** + * The current html property. + */ + public static String CURRENT_CONTENT_PROP = "currentContent"; + + /** + * The current url that the web view is at. + */ + private String currentUrl; + + /** + * The current content displayed on the web view page. + */ + private InputStream currentContent; + + /** + * Gets the current url. + * + * @return The current url the web view is at. + */ + public String getCurrentUrl() { + return currentUrl; + } + + /** + * Sets the current url the web view is at. + * + * @param currentUrl The current url. + */ + public void setCurrentUrl(String currentUrl) { + this.currentUrl = currentUrl; + setChanged(); + notifyObservers(CURRENT_URL_PROP); + } + + /** + * Gets the content of the current url. + * + * @return The content of the current url. + */ + public InputStream getCurrentContent() { + return currentContent; + } + + /** + * Sets the content for the current url. + * + * @param currentContent The content for the current url. + */ + public void setCurrentContent(InputStream currentContent) { + this.currentContent = currentContent; + setChanged(); + notifyObservers(CURRENT_CONTENT_PROP); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewXmlExtractor.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewXmlExtractor.java new file mode 100644 index 00000000..18b2c99c --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/WebViewXmlExtractor.java @@ -0,0 +1,27 @@ +package mil.nga.mapcache.io.network; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Extracts the xml from within the web page. + */ +public class WebViewXmlExtractor implements WebViewExtractor { + + @Override + public boolean readyForExtraction(String html) { + return true; + } + + @Override + public InputStream extractContent(String html) { + int newLineIndex = html.indexOf("\\n\\n") + 4; + String xml = html.substring(newLineIndex, html.length() - 1); + xml = xml.replace("\\u003C", "<"); + xml = xml.replace("\\\"", "\""); + xml = xml.replace("\\n", ""); + + return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerModel.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerModel.java new file mode 100644 index 00000000..5109a110 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerModel.java @@ -0,0 +1,30 @@ +package mil.nga.mapcache.io.network.slowserver; + +/** + * Model containing the message to display to the user to notify them of a slow server. + */ +public class SlowServerModel { + + /** + * The message to display to the user. + */ + private String message; + + /** + * Gets the message to display to the user. + * + * @return The message to display to the user. + */ + public String getMessage() { + return message; + } + + /** + * Sets the message to display to the user. + * + * @param message The message to display to the user. + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerNotifier.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerNotifier.java new file mode 100644 index 00000000..d80882bf --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerNotifier.java @@ -0,0 +1,74 @@ +package mil.nga.mapcache.io.network.slowserver; + +import android.app.Activity; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Notifies the user of a slow server depending on its response times. + */ +public class SlowServerNotifier { + + /** + * The time in milliseconds we consider a slow response. + */ + private static final long slowResponseTime = 2000; + + /** + * The number of slow responses until we consider the server as being slow. + */ + private static final int slowResponseCount = 10; + + /** + * The current slow response counts per server. + */ + private final Map slowResponseCounts = new HashMap<>(); + + /** + * The slow servers we have already notified the user about. + */ + private final Set notified = new HashSet<>(); + + /** + * The applications activity. + */ + private final Activity activity; + + /** + * Constructor. + * + * @param activity The applications activity. + */ + public SlowServerNotifier(Activity activity) { + this.activity = activity; + } + + /** + * Takes note of the host's response time. If there are too many slow responses, this class will + * notify the user that this server is slow. + * + * @param host The host data was just downloaded from. + * @param responseTime The response time in milliseconds of that download. + */ + public void responseTime(String host, long responseTime) { + if (responseTime >= slowResponseTime) { + Integer slowCount = slowResponseCounts.get(host); + if (slowCount == null) { + slowCount = 0; + } + slowCount++; + slowResponseCounts.put(host, slowCount); + if (slowCount >= slowResponseCount && !notified.contains(host)) { + SlowServerModel model = new SlowServerModel(); + model.setMessage("Downloads from " + host + " are taking a long time. " + + "Either your connection is poor or the server's performance is slow."); + SlowServerView view = new SlowServerView(model); + view.show(this.activity); + notified.add(host); + } + } + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerView.java b/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerView.java new file mode 100644 index 00000000..a2d9e034 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/io/network/slowserver/SlowServerView.java @@ -0,0 +1,40 @@ +package mil.nga.mapcache.io.network.slowserver; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; + +/** + * The dialog informing the user of a slow server. + */ +public class SlowServerView { + + /** + * The model. + */ + private final SlowServerModel model; + + /** + * Constructor. + * + * @param model The model for the view. + */ + public SlowServerView(SlowServerModel model) { + this.model = model; + } + + /** + * Shows the dialog informing the user of a slow server. + * + * @param activity The main activity. + */ + public void show(Activity activity) { + activity.runOnUiThread(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("Slow Downloads"); + builder.setMessage(this.model.getMessage()); + builder.setPositiveButton("Ok", (DialogInterface dialog, int arg1) -> dialog.dismiss()); + builder.show(); + }); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersModel.java b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersModel.java index 244f137a..e99b283a 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersModel.java +++ b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersModel.java @@ -131,7 +131,7 @@ public String[] getImageFormats() { /** * Sets the available image formats for the layers tiles. * - * @param imageFormats The availabel image formats. + * @param imageFormats The available image formats. */ public void setImageFormats(String[] imageFormats) { this.imageFormats = imageFormats; @@ -156,6 +156,6 @@ public String getTitle() { public void setTitle(String title) { this.title = title; setChanged(); - notifyObservers(); + notifyObservers(TITLE_PROP); } } diff --git a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersProvider.java b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersProvider.java index 576496c3..ecd20a40 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersProvider.java +++ b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersProvider.java @@ -31,7 +31,7 @@ public class LayersProvider implements IResponseHandler, RequestHeaderConsumer { /** * The activity used to get back on the main thread. */ - private Activity activity; + private final Activity activity; /** * The server url. @@ -41,7 +41,12 @@ public class LayersProvider implements IResponseHandler, RequestHeaderConsumer { /** * The model to populate with layer information. */ - private LayersModel model; + private final LayersModel model; + + /** + * True if the retrieve layers step was cancelled by the user. + */ + private boolean isCancelled = false; /** * Constructs a new layer provider. @@ -71,60 +76,56 @@ public void retrieveLayers(String url) { } } + /** + * Cancels the retrieving of the layers. + */ + public void cancel() { + isCancelled = true; + } + @Override public void handleResponse(InputStream stream, int responseCode) { - if (stream != null && responseCode == HttpURLConnection.HTTP_OK) { - CapabilitiesParser parser = new CapabilitiesParser(); - try { - final WMSCapabilities capabilities = parser.parse(stream); - List allLayers = new ArrayList<>(); - Stack parents = new Stack<>(); - for (Layer layer : capabilities.getCapability().getLayer()) { - getLayers(allLayers, layer, parents); - } - final LayerModel[] allLayersArray = allLayers.toArray(new LayerModel[allLayers.size()]); - activity.runOnUiThread(new Runnable() { - @Override - public void run() { + if (!isCancelled) { + if (stream != null && responseCode == HttpURLConnection.HTTP_OK) { + CapabilitiesParser parser = new CapabilitiesParser(); + try { + final WMSCapabilities capabilities = parser.parse(stream); + List allLayers = new ArrayList<>(); + Stack parents = new Stack<>(); + for (Layer layer : capabilities.getCapability().getLayer()) { + getLayers(allLayers, layer, parents); + } + final LayerModel[] allLayersArray = allLayers.toArray(new LayerModel[0]); + activity.runOnUiThread(() -> { model.setImageFormats(capabilities.getCapability().getRequest().getGetMap() .getFormat().toArray(new String[0])); model.setLayers(allLayersArray); - } - }); - } catch (ParserConfigurationException | SAXException | IOException e) { - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - model.setLayers(new LayerModel[0]); - } - }); - Log.e(LayersProvider.class.getSimpleName(), - "Unable to parse WMS GetCapabilities document for " + this.url, e); - } - } else { - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - model.setLayers(new LayerModel[0]); + }); + } catch (ParserConfigurationException | SAXException | IOException e) { + activity.runOnUiThread(() -> model.setLayers(new LayerModel[0])); + Log.e(LayersProvider.class.getSimpleName(), + "Unable to parse WMS GetCapabilities document for " + this.url, e); } - }); - Log.e( - LayersProvider.class.getSimpleName(), - "Unable to download WMS GetCapabilities document from " + url + " http response " + responseCode); + } else { + activity.runOnUiThread(() -> model.setLayers(new LayerModel[0])); + Log.e( + LayersProvider.class.getSimpleName(), + "Unable to download WMS GetCapabilities document from " + url + " http response " + responseCode); + } } } @Override public void handleException(IOException exception) { - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - model.setLayers(new LayerModel[0]); - } - }); + activity.runOnUiThread(() -> model.setLayers(new LayerModel[0])); Log.e(LayersProvider.class.getSimpleName(), "WMS GetCapabilities failed for " + url + ": ", exception); } + @Override + public boolean notCancelled() { + return !this.isCancelled; + } + /** * Recursively gets the layer's and their info. * @@ -136,21 +137,22 @@ private void getLayers(List allLayers, Layer layer, Stack par if (!layer.getName().isEmpty()) { LayerModel model = new LayerModel(); String title = parents.get(0).getTitle(); - String description = ""; + StringBuilder descriptionBuilder = new StringBuilder(); for (int i = 1; i < parents.size(); i++) { - description += parents.get(i).getTitle() + " "; + descriptionBuilder.append(parents.get(i).getTitle()); + descriptionBuilder.append(" "); } - description += layer.getTitle(); + descriptionBuilder.append(layer.getTitle()); model.setName(layer.getName()); model.setTitle(title); - model.setDescription(description); + model.setDescription(descriptionBuilder.toString()); int index = 0; long[] epsgs = new long[layer.getCRS().size()]; for (String crs : layer.getCRS()) { String[] splitCRS = crs.split(":"); try { - epsgs[index] = Long.valueOf(splitCRS[splitCRS.length - 1]); + epsgs[index] = Long.parseLong(splitCRS[splitCRS.length - 1]); } catch (NumberFormatException e) { Log.e( LayersProvider.class.getSimpleName(), diff --git a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersView.java b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersView.java index af8bad1c..56a29365 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersView.java +++ b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersView.java @@ -3,13 +3,10 @@ import android.content.Context; import android.view.LayoutInflater; import android.view.View; -import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; -import androidx.appcompat.app.AlertDialog; - import mil.nga.mapcache.R; /** @@ -20,17 +17,17 @@ public abstract class LayersView { /** * The application context. */ - private Context context; + private final Context context; /** * Contains the layers to select from. */ - private LayersModel model; + private final LayersModel model; /** * Used as a custom adapter for the list view. */ - private LayersList adapter; + private final LayersList adapter; /** * The android view. @@ -67,14 +64,10 @@ public void show() { title.setText(model.getTitle()); - - layersView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - LayerModel layer = model.getLayers()[i]; - LayerModel[] layers = {layer}; - model.setSelectedLayers(layers); - } + layersView.setOnItemClickListener((adapterView, view, i, l) -> { + LayerModel layer = model.getLayers()[i]; + LayerModel[] layers = {layer}; + model.setSelectedLayers(layers); }); closeLogo = (ImageView) view.findViewById(R.id.new_layer_close_logo); @@ -82,6 +75,7 @@ public void onItemClick(AdapterView adapterView, View view, int i, long l) { /** * Gets the application context. + * * @return The application context. */ protected Context getContext() { @@ -90,6 +84,7 @@ protected Context getContext() { /** * Gets the main view. + * * @return The layers pick view. */ protected View getView() { @@ -98,6 +93,7 @@ protected View getView() { /** * Gets the close x button in top left. + * * @return The close button. */ protected ImageView getCloseLogo() { diff --git a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersViewDialog.java b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersViewDialog.java index cfb0ad69..fd71d655 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersViewDialog.java +++ b/mapcache/src/main/java/mil/nga/mapcache/layersprovider/LayersViewDialog.java @@ -1,7 +1,6 @@ package mil.nga.mapcache.layersprovider; import android.content.Context; -import android.view.View; import androidx.appcompat.app.AlertDialog; @@ -20,11 +19,6 @@ public class LayersViewDialog extends LayersView implements Observer { */ private AlertDialog alertDialog; - /** - * The layers model. - */ - private LayersModel model; - /** * Constructor. * @@ -33,7 +27,6 @@ public class LayersViewDialog extends LayersView implements Observer { */ public LayersViewDialog(Context context, LayersModel model) { super(context, model); - this.model = model; model.addObserver(this); } @@ -46,13 +39,7 @@ public void show() { .setView(getView()); alertDialog = dialog.create(); alertDialog.setCanceledOnTouchOutside(false); - getCloseLogo().setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - model.setSelectedLayers(model.getSelectedLayers()); - alertDialog.dismiss(); - } - }); + getCloseLogo().setOnClickListener((v) -> alertDialog.dismiss()); alertDialog.show(); } diff --git a/mapcache/src/main/java/mil/nga/mapcache/load/ILoadTilesTask.java b/mapcache/src/main/java/mil/nga/mapcache/load/ILoadTilesTask.java index 3ff7d22f..71af4d58 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/load/ILoadTilesTask.java +++ b/mapcache/src/main/java/mil/nga/mapcache/load/ILoadTilesTask.java @@ -9,16 +9,15 @@ public interface ILoadTilesTask { /** * On cancellation of loading tiles - * - * @param result - */ - public void onLoadTilesCancelled(String result); + * + * */ + void onLoadTilesCancelled(); /** * On completion of loading tiles * - * @param result + * @param result A message to display to the user if needed. */ - public void onLoadTilesPostExecute(String result); + void onLoadTilesPostExecute(String result); } diff --git a/mapcache/src/main/java/mil/nga/mapcache/load/LoadTilesTask.java b/mapcache/src/main/java/mil/nga/mapcache/load/LoadTilesTask.java index 54a661e2..b58444e5 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/load/LoadTilesTask.java +++ b/mapcache/src/main/java/mil/nga/mapcache/load/LoadTilesTask.java @@ -18,7 +18,6 @@ import mil.nga.geopackage.io.GeoPackageProgress; import mil.nga.geopackage.tiles.TileBoundingBoxUtils; import mil.nga.geopackage.tiles.TileGenerator; -import mil.nga.geopackage.tiles.UrlTileGenerator; import mil.nga.geopackage.tiles.features.FeatureTileGenerator; import mil.nga.mapcache.R; import mil.nga.mapcache.utils.HttpUtils; @@ -68,7 +67,7 @@ public static void loadTiles(Activity activity, ILoadTilesTask callback, Projection projection = ProjectionFactory.getProjection(authority, code); BoundingBox bBox = transform(boundingBox, projection); - UrlTileGenerator tileGenerator = new UrlTileGenerator(activity, geoPackage, + WebViewTileGenerator tileGenerator = new WebViewTileGenerator(activity, geoPackage, tableName, tileUrl, minZoom, maxZoom, bBox, projection); tileGenerator.addHTTPHeaderValue( HttpUtils.getInstance().getUserAgentKey(), @@ -152,7 +151,7 @@ private static void loadTiles(Activity activity, ILoadTilesTask callback, ProgressDialog progressDialog = new ProgressDialog(activity); final LoadTilesTask loadTilesTask = new LoadTilesTask(activity, - callback, progressDialog, viewModel); + callback, progressDialog, viewModel, geoPackage, tableName); tileGenerator.setProgress(loadTilesTask); @@ -181,6 +180,8 @@ private static void loadTiles(Activity activity, ILoadTilesTask callback, private final ILoadTilesTask callback; private final ProgressDialog progressDialog; private final GeoPackageViewModel viewModel; + private final GeoPackage geoPackage; + private final String tableName; private PowerManager.WakeLock wakeLock; private boolean isCancelled = false; @@ -191,13 +192,18 @@ private static void loadTiles(Activity activity, ILoadTilesTask callback, * @param callback Called when the load tiles task has completed or was cancelled. * @param progressDialog The progress dialog. * @param viewModel Used to get the geoPackage. + * @param geoPackage The geoPackage we are creating a tile layer for. + * @param tableName The name of the tile layer. */ public LoadTilesTask(Activity activity, ILoadTilesTask callback, - ProgressDialog progressDialog, GeoPackageViewModel viewModel) { + ProgressDialog progressDialog, GeoPackageViewModel viewModel, + GeoPackage geoPackage, String tableName) { this.activity = activity; this.callback = callback; this.progressDialog = progressDialog; this.viewModel = viewModel; + this.geoPackage = geoPackage; + this.tableName = tableName; } /** @@ -259,23 +265,28 @@ public void run() { wakeLock.acquire(43200000); int count = tileGenerator.generateTiles(); - String result = null; - if (count == 0) { - result = "No tiles were generated for your new layer. " + - "This could be an issue with your tile URL or the tile server. " + - "Please verify the server URL and try again."; - } - if (count > 0 && viewModel.getActive().getValue() != null) { - viewModel.getActive().getValue().setModified(true); - } - if (count < max && !(tileGenerator instanceof FeatureTileGenerator)) { - result = "Fewer tiles were generated than " + - "expected. Expected: " + max + ", Actual: " + count + - ". This is likely an issue with the tile server or a slow / " + - "intermittent network connection."; - } + if(!isCancelled) { + String result = null; + if (count == 0) { + result = "No tiles were generated for your new layer. " + + "This could be an issue with your tile URL or the tile server. " + + "Please verify the server URL and try again."; + } + if (count > 0 && viewModel.getActive().getValue() != null) { + viewModel.getActive().getValue().setModified(true); + } + if (count < max && !(tileGenerator instanceof FeatureTileGenerator)) { + result = "Fewer tiles were generated than " + + "expected. Expected: " + max + ", Actual: " + count + + ". This is likely an issue with the tile server or a slow / " + + "intermittent network connection."; + } - callback.onLoadTilesPostExecute(result); + callback.onLoadTilesPostExecute(result); + } else { + this.geoPackage.deleteTable(tableName); + callback.onLoadTilesCancelled(); + } } catch (final Exception e) { Log.e(LoadTilesTask.class.getSimpleName(), e.getMessage(), e); } finally { diff --git a/mapcache/src/main/java/mil/nga/mapcache/load/WebViewResponseHandler.java b/mapcache/src/main/java/mil/nga/mapcache/load/WebViewResponseHandler.java new file mode 100644 index 00000000..9bbd396d --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/load/WebViewResponseHandler.java @@ -0,0 +1,110 @@ +package mil.nga.mapcache.load; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; + +import mil.nga.geopackage.io.GeoPackageIOUtils; +import mil.nga.mapcache.io.network.IResponseHandler; + +/** + * Handles the response from the specified url. + */ +public class WebViewResponseHandler implements IResponseHandler { + + /** + * Debug logging flag. + */ + private final static boolean isDebug = false; + + /** + * The bytes to return from createTile call. + */ + private byte[] theBytes = null; + + /** + * If an exception occurred trying to download the tile. + */ + private IOException exception = null; + + /** + * The current url we are using to download a tile image. + */ + private final String currentUrl; + + /** + * Constructor. + * + * @param url The url we are awaiting the response for. + */ + public WebViewResponseHandler(String url) { + this.currentUrl = url; + } + + /** + * Gets the data returned from the get call. + * + * @return The bytes of the response from the get call. + */ + public byte[] getBytes() { + if(isDebug) { + Log.d( + WebViewResponseHandler.class.getSimpleName(), + "Getting byte for " + this.currentUrl); + } + return theBytes; + } + + /** + * Gets the exception if any from the response. + * + * @return The exception from the response or null if there wasn't one. + */ + public IOException getException() { + return exception; + } + + @Override + public void handleResponse(InputStream stream, int responseCode) { + if(isDebug) { + Log.d( + WebViewResponseHandler.class.getSimpleName(), + "Handling response for " + this.currentUrl); + } + try { + if (stream != null) { + theBytes = GeoPackageIOUtils.streamBytes(stream); + } else { + Log.w( + WebViewResponseHandler.class.getSimpleName(), + "Stream is null for url " + currentUrl); + } + } catch (IOException e) { + exception = e; + } + + synchronized (this) { + notifyAll(); + } + + if(isDebug) { + Log.d( + WebViewResponseHandler.class.getSimpleName(), + "Notified all for " + this.currentUrl); + } + } + + @Override + public void handleException(IOException exception) { + this.exception = exception; + synchronized (this) { + notifyAll(); + } + } + + @Override + public boolean notCancelled() { + return true; + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/load/WebViewTileGenerator.java b/mapcache/src/main/java/mil/nga/mapcache/load/WebViewTileGenerator.java new file mode 100644 index 00000000..61715876 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/load/WebViewTileGenerator.java @@ -0,0 +1,240 @@ +package mil.nga.mapcache.load; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; + +import org.locationtech.proj4j.units.Units; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import mil.nga.geopackage.BoundingBox; +import mil.nga.geopackage.GeoPackage; +import mil.nga.geopackage.GeoPackageException; +import mil.nga.geopackage.tiles.TileBoundingBoxUtils; +import mil.nga.geopackage.tiles.UrlTileGenerator; +import mil.nga.mapcache.io.network.HttpClient; +import mil.nga.proj.Projection; + +/** + * Uses the mapcache applications HttpClient which in turn will popup a WebView if the url needs + * the user to interact with some sort of web page for login. + */ +public class WebViewTileGenerator extends UrlTileGenerator { + + /** + * Debug logging flag. + */ + private static final boolean isDebug = false; + + /** + * Tile URL + */ + private String tileUrl; + + /** + * True if the URL has x, y, or z variables + */ + private boolean urlHasXYZ; + + /** + * True if the URL has bounding box variables + */ + private boolean urlHasBoundingBox; + + /** + * TMS URL flag, when true x,y,z converted to TMS when requesting the tile + */ + private boolean tms = false; + + /** + * Constructor. + * + * @param context The application context. + * @param geoPackage The GeoPackage. + * @param tableName The table name. + * @param tileUrl The tile url. + * @param minZoom The min zoom. + * @param maxZoom The max zoom. + * @param boundingBox The bounding box. + * @param projection The projection. + */ + public WebViewTileGenerator(Context context, GeoPackage geoPackage, String tableName, String tileUrl, int minZoom, int maxZoom, BoundingBox boundingBox, Projection projection) { + super(context, geoPackage, tableName, tileUrl, minZoom, maxZoom, boundingBox, projection); + initialize(tileUrl); + } + + @Override + public boolean isTms() { + return this.tms; + } + + @Override + public void setTms(boolean tms) { + this.tms = tms; + } + + /** + * Initialize the tile URL + * + * @param tileUrl tile URL + */ + private void initialize(String tileUrl) { + if (isDebug) { + Log.d(WebViewTileGenerator.class.getSimpleName(), "Initializing for " + tileUrl); + } + try { + this.tileUrl = URLDecoder.decode(tileUrl, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new GeoPackageException("Failed to decode tile url: " + + tileUrl, e); + } + + this.urlHasXYZ = hasXYZ(tileUrl); + this.urlHasBoundingBox = hasBoundingBox(tileUrl); + + if (!this.urlHasXYZ && !this.urlHasBoundingBox) { + throw new GeoPackageException( + "URL does not contain x,y,z or bounding box variables: " + + tileUrl); + } + } + + + /** + * Determine if the url has bounding box variables + * + * @param url The url to check for bounding box. + * @return True if it has bounding box false if not. + */ + private boolean hasBoundingBox(String url) { + + String replacedUrl = replaceBoundingBox(url, boundingBox); + return !replacedUrl.equals(url); + } + + + /** + * Replace x, y, and z in the url + * + * @param url The url to replace the x y z values. + * @param z The z value to replace it with. + * @param x The x value to replace it with. + * @param y The y value to replace it with. + * @return The url with x,y,z populated with values. + */ + private String replaceXYZ(String url, int z, long x, long y) { + + url = url.replace("{z}", String.valueOf(z)); + url = url.replace("{x}", String.valueOf(x)); + url = url.replace("{y}", String.valueOf(y)); + return url; + } + + /** + * Determine if the url has x, y, or z variables + * + * @param url The url to check if it contains x,y,z values. + * @return True if the url is xyz, false if not. + */ + private boolean hasXYZ(String url) { + String replacedUrl = replaceXYZ(url, 0, 0, 0); + return !replacedUrl.equals(url); + } + + + /** + * Replace the bounding box coordinates in the url + * + * @param url The url to replace. + * @param z The z value. + * @param x The x value. + * @param y The y value. + * @return The url containing the x, y, z values. + */ + private String replaceBoundingBox(String url, int z, long x, long y) { + + BoundingBox boundingBox; + + if (projection.isUnit(Units.DEGREES)) { + boundingBox = TileBoundingBoxUtils + .getProjectedBoundingBoxFromWGS84(projection, x, y, z); + } else { + boundingBox = TileBoundingBoxUtils + .getProjectedBoundingBox(projection, x, y, z); + } + + url = replaceBoundingBox(url, boundingBox); + + return url; + } + + /** + * Replace the url parts with the bounding box + * + * @param url The url to replace. + * @param boundingBox The bounding box values to put within the url. + * @return The url containing the bounding box values. + */ + private String replaceBoundingBox(String url, BoundingBox boundingBox) { + + url = url.replace("{minLat}", String.valueOf(boundingBox.getMinLatitude())); + url = url.replace("{maxLat}", String.valueOf(boundingBox.getMaxLatitude())); + url = url.replace("{minLon}", String.valueOf(boundingBox.getMinLongitude())); + url = url.replace("{maxLon}", String.valueOf(boundingBox.getMaxLongitude())); + + return url; + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + @Override + protected byte[] createTile(int z, long x, long y) { + String zoomUrl = tileUrl; + + // Replace x, y, and z + if (urlHasXYZ) { + long yRequest = y; + + // If TMS, flip the y value + if (tms) { + yRequest = TileBoundingBoxUtils.getYAsOppositeTileFormat(z, + (int) y); + } + + zoomUrl = replaceXYZ(zoomUrl, z, x, yRequest); + } + + // Replace bounding box + if (urlHasBoundingBox) { + zoomUrl = replaceBoundingBox(zoomUrl, z, x, y); + } + + WebViewResponseHandler handler = new WebViewResponseHandler(zoomUrl); + if (isDebug) { + Log.d(WebViewTileGenerator.class.getSimpleName(), "Sending Get to " + zoomUrl); + } + HttpClient.getInstance().sendGet(zoomUrl, handler, (Activity) context); + synchronized (handler) { + try { + if (isDebug) { + Log.d(WebViewTileGenerator.class.getSimpleName(), "Waiting for response from " + zoomUrl); + } + handler.wait(); + } catch (InterruptedException e) { + Log.d(WebViewTileGenerator.class.getSimpleName(), e.getMessage(), e); + } + } + + if (isDebug) { + Log.d(WebViewTileGenerator.class.getSimpleName(), "Done waiting from " + zoomUrl); + } + + if (handler.getException() != null) { + throw new GeoPackageException("Failed to download tile. URL: " + + zoomUrl + ", z=" + z + ", x=" + x + ", y=" + y, handler.getException()); + } + + return handler.getBytes(); + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/ogc/wms/CapabilitiesParser.java b/mapcache/src/main/java/mil/nga/mapcache/ogc/wms/CapabilitiesParser.java index bd1dc7b7..90efe8ab 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/ogc/wms/CapabilitiesParser.java +++ b/mapcache/src/main/java/mil/nga/mapcache/ogc/wms/CapabilitiesParser.java @@ -21,35 +21,35 @@ public class CapabilitiesParser extends DefaultHandler { /** * The name of the title element. */ - private static String TITLE_ELEMENT = "Title"; + private static final String TITLE_ELEMENT = "Title"; /** * The name of the name element. */ - private static String NAME_ELEMENT = "Name"; + private static final String NAME_ELEMENT = "Name"; /** * The name of the layer element. */ - private static String LAYER_ELEMENT = "Layer"; + private static final String LAYER_ELEMENT = "Layer"; /** * The name of the GetMap element. */ - private static String GETMAP_ELEMENT = "GetMap"; + private static final String GET_MAP_ELEMENT = "GetMap"; /** * The name of the GetMap Format element. */ - private static String FORMAT_ELEMENT = "Format"; + private static final String FORMAT_ELEMENT = "Format"; /** * The name of the CRS element within the Layer element. */ - private static String CRS_ELEMENT = "CRS"; + private static final String CRS_ELEMENT = "CRS"; /** - * The current WMSCapabilties being populated. + * The current WMSCapabilities being populated. */ private WMSCapabilities current = null; @@ -71,12 +71,12 @@ public class CapabilitiesParser extends DefaultHandler { /** * The current stack of all parent layers. */ - private Stack parentLayers = new Stack<>(); + private final Stack parentLayers = new Stack<>(); /** * The parents elements we are currently parsing. */ - private Stack currentElements = new Stack<>(); + private final Stack currentElements = new Stack<>(); /** * Parses the wms getCapabilities document and returns a new WMSCapabilities populated @@ -99,16 +99,16 @@ public WMSCapabilities parse(InputStream stream) throws ParserConfigurationExcep @Override public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); - if (currentLayer != null) { - if (TITLE_ELEMENT.equals(currentElementName) && currentLayer.getTitle().isEmpty()) { - currentLayer.setTitle(new String(ch, start, length)); - } else if (NAME_ELEMENT.equals(currentElementName) && currentLayer.getName().isEmpty()) { - currentLayer.setName(new String(ch, start, length)); + if (currentLayer != null && LAYER_ELEMENT.equals(currentElements.lastElement())) { + if (TITLE_ELEMENT.equals(currentElementName)) { + currentLayer.setTitle(currentLayer.getTitle() + new String(ch, start, length)); + } else if (NAME_ELEMENT.equals(currentElementName)) { + currentLayer.setName(currentLayer.getName() + new String(ch, start, length)); } } if (FORMAT_ELEMENT.equals(currentElementName)) { - if (currentElements.lastElement().equals(GETMAP_ELEMENT)) { + if (currentElements.lastElement().equals(GET_MAP_ELEMENT)) { current.getCapability().getRequest().getGetMap().getFormat().add( new String(ch, start, length)); } diff --git a/mapcache/src/main/java/mil/nga/mapcache/preferences/TileUrlFragment.java b/mapcache/src/main/java/mil/nga/mapcache/preferences/TileUrlFragment.java index 8359f36a..4eb35f5f 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/preferences/TileUrlFragment.java +++ b/mapcache/src/main/java/mil/nga/mapcache/preferences/TileUrlFragment.java @@ -5,6 +5,8 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.text.Editable; import android.text.TextWatcher; import android.view.Gravity; @@ -17,6 +19,7 @@ import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -28,8 +31,11 @@ import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import mil.nga.mapcache.R; +import mil.nga.mapcache.utils.HttpUtils; import mil.nga.mapcache.utils.ViewAnimation; /** @@ -78,6 +84,10 @@ public class TileUrlFragment extends PreferenceFragmentCompat implements Prefere * Tracks the state of delete visibility */ private boolean editMode = false; + /** + * For testing http connections + */ + HttpUtils httpUtils = HttpUtils.getInstance(); /** * Create the parent view and set up listeners @@ -283,15 +293,16 @@ private void addUrlView(String text){ LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.HORIZONTAL)); itemRow.setGravity(Gravity.CENTER); -// itemRow.setPadding(0,48,0, 48); -// itemRow.setBackground(getResources().getDrawable(R.drawable.delete_bg)); + ImageView rowIcon = new ImageView(getContext()); + rowIcon.setImageResource(R.drawable.cloud_layers_grey); + rowIcon.setPadding(0,48,16, 48); ImageButton deleteButton = new ImageButton(getContext()); deleteButton.setImageResource(R.drawable.delete_forever); deleteButton.setBackground(null); deleteButton.setVisibility(View.GONE); - deleteButton.setPadding(48,48,48, 48); + deleteButton.setPadding(0,0,48, 0); deleteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -346,10 +357,43 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean b) { // Add everything itemRow.addView(deleteButton); + itemRow.addView(rowIcon); // itemRow.addView(check); itemRow.addView(nameText); ViewAnimation.setSlideInFromRightAnimation(itemRow, 250); labelHolder.addView(itemRow); + + testConnection(text, rowIcon); + } + + /** + * Tests connection to a url + */ + private boolean testConnection(String url, ImageView icon){ + // ThreadUtils.getInstance().runBackground(loadTilesTask); + ExecutorService threadEx = Executors.newSingleThreadExecutor(); + Handler handler = new Handler(Looper.getMainLooper()); + threadEx.execute(new Runnable() { + @Override + public void run() { + // Test connection + boolean connected = httpUtils.isServerAvailable(url); + + // Update icon to show connection status + handler.post(new Runnable() { + @Override + public void run() { + if(connected) { + icon.setImageResource(R.drawable.cloud_layers_blue); + } else { + icon.setImageResource(R.drawable.cloud_layers_red); + } + ViewAnimation.setBounceAnimatiom(icon, 200); + } + }); + } + }); + return true; } /** diff --git a/mapcache/src/main/java/mil/nga/mapcache/repository/GeoPackageRepository.java b/mapcache/src/main/java/mil/nga/mapcache/repository/GeoPackageRepository.java index 2f4ad9bf..e48b7399 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/repository/GeoPackageRepository.java +++ b/mapcache/src/main/java/mil/nga/mapcache/repository/GeoPackageRepository.java @@ -116,6 +116,7 @@ public GeoPackageRepository(@NonNull Application application) { cache = new GeoPackageCache(manager); active.setValue(new GeoPackageDatabases(context, "active")); geos.setValue(new GeoPackageDatabases(context, "all")); + String test = "test"; } /** @@ -200,8 +201,19 @@ public void removeLayerFromGeos(String geoPackageName, String layerName){ GeoPackageDatabases currentGeos = geos.getValue(); if(currentGeos != null) { currentGeos.removeTable(geoPackageName, layerName); - regenerateTableList(); -// geos.postValue(currentGeos); + geos.postValue(currentGeos); + } + } + + /** + * Delete a GeoPackage from the open geos list + * @param geoPackageName Name of the GeoPackage containing the layer + */ + public void removeGeo(String geoPackageName){ + GeoPackageDatabases currentGeos = geos.getValue(); + if(currentGeos != null) { + currentGeos.removeDatabase(geoPackageName, false); + geos.postValue(currentGeos); } } @@ -536,6 +548,7 @@ public List> regenerateTableList() { public boolean deleteGeoPackage(String geoPackageName) { removeActiveForGeoPackage(geoPackageName); cache.removeAndClose(geoPackageName); + removeGeo(geoPackageName); return manager.delete(geoPackageName); } @@ -646,9 +659,9 @@ public FeatureViewObjects getFeatureViewObjects(MarkerFeature markerFeature){ if(dataColumnsDao != null){ DataColumns dataColumn = dataColumnsDao.getDataColumn( featureRow.getTable().getTableName(), columnName); - if (dataColumn != null){ - columnName = dataColumn.getName(); - } +// if (dataColumn != null){ +// columnName = dataColumn.getName(); +// } } if (value == null) { diff --git a/mapcache/src/main/java/mil/nga/mapcache/utils/HttpUtils.java b/mapcache/src/main/java/mil/nga/mapcache/utils/HttpUtils.java index a4494c84..a856098d 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/utils/HttpUtils.java +++ b/mapcache/src/main/java/mil/nga/mapcache/utils/HttpUtils.java @@ -3,6 +3,11 @@ import android.app.Activity; import android.os.Build; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import mil.nga.geopackage.GeoPackageException; import mil.nga.mapcache.R; /** @@ -99,6 +104,34 @@ public String getUserAgentValue(Activity activity) { + " Android " + Build.VERSION.RELEASE_OR_CODENAME; } + /** + * Tests a connection to the given url + * + * @return True if we get a 200 response + */ + public boolean isServerAvailable(String urlString){ + boolean connected = false; + URL url; + try { + url = new URL(urlString); + } catch (MalformedURLException e) { + return connected; +// throw new GeoPackageException("bad url"); + } + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.connect(); + int responseCode = connection.getResponseCode(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + connected = true; + } + } catch (Exception e){ + String exception = e.toString(); + } + return connected; + } + /** * Private constructor. */ diff --git a/mapcache/src/main/java/mil/nga/mapcache/utils/ProjUtils.java b/mapcache/src/main/java/mil/nga/mapcache/utils/ProjUtils.java new file mode 100644 index 00000000..ee54be37 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/utils/ProjUtils.java @@ -0,0 +1,59 @@ +package mil.nga.mapcache.utils; + +import org.locationtech.proj4j.units.Units; + +import mil.nga.geopackage.BoundingBox; +import mil.nga.geopackage.srs.SpatialReferenceSystem; +import mil.nga.geopackage.tiles.TileBoundingBoxUtils; +import mil.nga.proj.ProjectionConstants; +import mil.nga.proj.ProjectionFactory; +import mil.nga.proj.ProjectionTransform; + +/** + * Utility class for transforming different geometries into different projections. + */ +public class ProjUtils { + + /** + * The instance of this class. + */ + private static final ProjUtils instance = new ProjUtils(); + + /** + * Gets the instance of this class. + * + * @return The instance of this class. + */ + public static ProjUtils getInstance() { + return instance; + } + + /** + * Transform the bounding box in the spatial reference to a WGS84 bounding box + * + * @param boundingBox bounding box + * @param srs spatial reference system + * @return bounding box + */ + public BoundingBox transformBoundingBoxToWgs84(BoundingBox boundingBox, SpatialReferenceSystem srs) { + + mil.nga.proj.Projection projection = srs.getProjection(); + if (projection.isUnit(Units.DEGREES)) { + boundingBox = TileBoundingBoxUtils.boundDegreesBoundingBoxWithWebMercatorLimits(boundingBox); + } + ProjectionTransform transformToWebMercator = projection + .getTransformation( + ProjectionConstants.EPSG_WEB_MERCATOR); + BoundingBox webMercatorBoundingBox = boundingBox.transform(transformToWebMercator); + ProjectionTransform transform = ProjectionFactory.getProjection( + ProjectionConstants.EPSG_WEB_MERCATOR) + .getTransformation( + ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM); + boundingBox = webMercatorBoundingBox.transform(transform); + return boundingBox; + } + + private ProjUtils() { + + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/utils/SampleDownloader.java b/mapcache/src/main/java/mil/nga/mapcache/utils/SampleDownloader.java new file mode 100644 index 00000000..4610018d --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/utils/SampleDownloader.java @@ -0,0 +1,154 @@ +package mil.nga.mapcache.utils; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; +import android.widget.ArrayAdapter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.android.gms.common.api.Response; +import com.google.gson.Gson; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.DataInput; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import mil.nga.mapcache.R; +import mil.nga.mapcache.io.network.HttpClient; +import mil.nga.mapcache.io.network.IResponseHandler; +import mil.nga.mapcache.layersprovider.LayerModel; +import mil.nga.mapcache.layersprovider.LayersProvider; + +/** + * Downloads sample files in JSON format into hashmaps. Used for sample tile urls and geopackages, + * retreived from mapcache github + */ +public class SampleDownloader implements IResponseHandler { + + /** + * The activity used to get back on the main thread. + */ + private final Activity activity; + + /** + * True if the retrieve layers step was cancelled by the user. + */ + private boolean isCancelled = false; + + /** + * This array adapter should be populated with the results + */ + ArrayAdapter adapter; + + /** + * Hashmap to hold results from the download request + */ + HashMap sampleList = new HashMap<>(); + + + public SampleDownloader(Activity activity, ArrayAdapter adapter) { + this.activity = activity; + this.adapter = adapter; + } + + /** + * Make a request with the given url to download sample data + * @param url + */ + public void getExampleData(String url){ + // Get our sample data from github + HttpClient.getInstance().sendGet(url, this, this.activity); + } + + /** + * Provide a way to cancel in case it's taking too long + */ + public void cancel() { + isCancelled = true; + } + + /** + * Response handler to parse the json data and populate the adapter + * @param stream The response from the server, or null if bad response from server. + * @param responseCode The http response code from the server. + */ + @Override + public void handleResponse(InputStream stream, int responseCode) { + if (!isCancelled) { + try { + if (stream != null && responseCode == HttpURLConnection.HTTP_OK) { + + BufferedReader br = new BufferedReader(new InputStreamReader(stream)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line + "\n"); + } + br.close(); + JSONObject mainObject = new JSONObject(sb.toString()); + sampleList.putAll(new Gson().fromJson(sb.toString(), HashMap.class)); + activity.runOnUiThread(new Runnable() { + + @Override + public void run() { + adapter.addAll(sampleList.keySet()); + + } + }); + } + } catch (Exception e){ + Log.e("error", e.toString()); + } + + } + + + + } + + @Override + public void handleException(IOException exception) { + Log.e(SampleDownloader.class.getSimpleName(), "Failed to get sample data: ", exception); + } + + @Override + public boolean notCancelled() { + return false; + } + + public HashMap getSampleList() { + return sampleList; + } + + public void setSampleList(HashMap sampleList) { + this.sampleList = sampleList; + } + + /** + * Pulls our local geopackage example urls + */ + public void loadLocalGeoPackageSamples(){ + // Get our local sample data + HashMap map = new HashMap<>(); + + // Pull local string resources, shipped with the app + String[] labels = activity.getResources() + .getStringArray( + R.array.preloaded_geopackage_url_labels); + String[] urls = activity.getResources() + .getStringArray( + R.array.preloaded_geopackage_urls); + for(int i=0; i { - private static String[] preferredImageFormats = {"png", "jpeg", "tiff", "gif"}; + private static final String[] preferredImageFormats = {"png", "jpeg", "tiff", "gif"}; /** * The model. */ - private NewTileLayerModel model; + private final NewTileLayerModel model; /** * Used to validate the layer name. */ - private GeoPackageViewModel geoPackageViewModel; + private final GeoPackageViewModel geoPackageViewModel; /** * Used to get string constants. */ - private Fragment fragment; + private final Fragment fragment; /** * Used to get saved urls. */ - private SharedPreferences settings; + private final SharedPreferences settings; /** * Constructor. @@ -71,7 +71,11 @@ public NewTileLayerController(NewTileLayerModel model, GeoPackageViewModel geoPa public void loadSavedUrls() { Set existing = settings.getStringSet(fragment.getString(R.string.geopackage_create_tiles_label), new HashSet()); String[] urlChoices = existing.toArray(new String[existing.size()]); - model.setSavedUrls(urlChoices); + ArrayList urlList = new ArrayList<>(); + for(String url : urlChoices){ + urlList.add(new SavedUrl(url)); + } + model.setSavedUrlObjects(urlList); } /** @@ -135,7 +139,7 @@ public int compare(String s, String t1) { * @return The best format to use for tile downloads. */ private String getFormat(LayersModel layersModel) { - List formats = new ArrayList(Arrays.asList(layersModel.getImageFormats())); + List formats = new ArrayList<>(Arrays.asList(layersModel.getImageFormats())); Collections.sort(formats, this); return formats.get(0); diff --git a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerModel.java b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerModel.java index 7d9d5b79..0af1077c 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerModel.java +++ b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerModel.java @@ -1,5 +1,6 @@ package mil.nga.mapcache.wizards.createtile; +import java.util.ArrayList; import java.util.Observable; public class NewTileLayerModel extends Observable { @@ -79,6 +80,11 @@ public class NewTileLayerModel extends Observable { */ private String[] savedUrls; + /** + * List of saved urls with connection status + */ + private ArrayList savedUrlObjects = new ArrayList<>(); + /** * If anything is wrong with the inputs, this message will be populated. */ @@ -202,6 +208,28 @@ public String[] getSavedUrls() { return savedUrls; } + public String getUrlAtPosition(int position){ + return savedUrlObjects.get(position).getmUrl(); + } + + /** + * Get a list of SavedUrl objects out of the saved url string list + */ + public ArrayList getSavedUrlObjectList(){ + return savedUrlObjects; + } + + /** + * Sets the saved url objects + * + * @param savedUrls The saved urls. + */ + public void setSavedUrlObjects(ArrayList savedUrls) { + this.savedUrlObjects = savedUrls; + setChanged(); + notifyObservers(SAVED_URLS_PROP); + } + /** * Sets the saved urls. * diff --git a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerUI.java b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerUI.java index ddc4163c..4ef15d66 100644 --- a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerUI.java +++ b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerUI.java @@ -9,7 +9,9 @@ import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; +import android.widget.ArrayAdapter; import android.widget.ImageView; +import android.widget.ListView; import android.widget.TextView; import androidx.appcompat.app.AlertDialog; @@ -21,6 +23,7 @@ import com.google.android.material.button.MaterialButton; import com.google.android.material.textfield.TextInputEditText; +import java.util.ArrayList; import java.util.Observable; import mil.nga.mapcache.R; @@ -29,6 +32,7 @@ import mil.nga.mapcache.layersprovider.LayersView; import mil.nga.mapcache.layersprovider.LayersViewDialog; import mil.nga.mapcache.load.ILoadTilesTask; +import mil.nga.mapcache.utils.SampleDownloader; import mil.nga.mapcache.utils.ViewAnimation; import mil.nga.mapcache.viewmodel.GeoPackageViewModel; @@ -109,6 +113,11 @@ public class NewTileLayerUI implements Observer { */ private ProgressDialog progressDialog; + /** + * Retrieves all the layers from a given server. + */ + private LayersProvider provider; + /** * Constructor * @@ -171,7 +180,7 @@ public void afterTextChanged(Editable editable) { @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - if(inputName.getText() != null) { + if (inputName.getText() != null) { String givenName = inputName.getText().toString(); model.setLayerName(givenName); } @@ -195,7 +204,7 @@ public void afterTextChanged(Editable editable) { @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - if(inputUrl.getText() != null) { + if (inputUrl.getText() != null) { String givenUrl = inputUrl.getText().toString(); model.setUrl(givenUrl); } @@ -206,6 +215,28 @@ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { TextView defaultText = (TextView) alertView.findViewById(R.id.default_url); defaultText.setOnClickListener((View view) -> controller.loadSavedUrls()); + // Example URLs from github + TextView exampleUrlText = (TextView) alertView.findViewById(R.id.example_urls); + exampleUrlText.setOnClickListener(view -> { + AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AppCompatAlertDialogStyle); + builder.setTitle(fragment.getString(R.string.example_url_header)); + ArrayAdapter adapter = new ArrayAdapter<>( + context, android.R.layout.select_dialog_item); + + SampleDownloader sampleDownloader = new SampleDownloader(fragment.getActivity(), adapter); + sampleDownloader.getExampleData(activity.getString(R.string.sample_tile_urls)); + builder.setAdapter(adapter, + (DialogInterface d, int item) -> { + + if (item >= 0) { + String name = adapter.getItem(item); + inputName.setText(name); + inputUrl.setText(sampleDownloader.getSampleList().get(name)); + } + }); + builder.show(); + }); + // URL help menu TextView urlHelpText = (TextView) alertView.findViewById(R.id.url_help); urlHelpText.setOnClickListener((View view) -> { @@ -242,7 +273,7 @@ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { LayersModel layers = new LayersModel(); layers.addObserver(NewTileLayerUI.this); showSpinningDialog(); - LayersProvider provider = new LayersProvider(fragment.getActivity(), layers); + provider = new LayersProvider(fragment.getActivity(), layers); provider.retrieveLayers(model.getUrl()); } }); @@ -291,18 +322,22 @@ private void drawTileBoundingBox(LayersModel layers) { * Shows the saved urls the user can choose from, or a message stating they have none. */ private void showSavedUrls() { + View view = LayoutInflater.from(context).inflate(R.layout.layout_saved_url_list, null); + ListView listView = view.findViewById(R.id.list_view); + ArrayList urlList = model.getSavedUrlObjectList(); + SavedUrlAdapter adapter = new SavedUrlAdapter(context, urlList); + listView.setAdapter(adapter); AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle("Saved Tile URLs"); - if (model.getSavedUrls().length > 0) { - builder.setItems(model.getSavedUrls(), (DialogInterface dialog, int which) -> { - inputUrl.setText(model.getSavedUrls()[which]); - inputUrl.setError(null); - ViewAnimation.setBounceAnimatiom(inputUrl, 200); - }); - } else { - builder.setMessage(fragment.getString(R.string.no_saved_urls_message)); - } - builder.show(); + builder.setView(view); + AlertDialog ad = builder.show(); + listView.setOnItemClickListener((adapterView, view1, i, l) -> { + inputUrl.setText(model.getUrlAtPosition(i)); + inputUrl.setError(null); + ViewAnimation.setBounceAnimatiom(inputUrl, 200); + ad.dismiss(); + }); + adapter.updateConnections(urlList); } /** @@ -313,9 +348,25 @@ private void showSpinningDialog() { progressDialog.setTitle("Retrieving Layers"); progressDialog.setCancelable(false); progressDialog.setIndeterminate(true); + progressDialog.setButton( + DialogInterface.BUTTON_NEGATIVE, + "Cancel", + (dialog, which) -> this.cancelRetrieveLayers()); progressDialog.show(); } + /** + * Cancels Retrieving layers. + */ + private void cancelRetrieveLayers() { + if (provider != null) { + this.provider.cancel(); + } + if (progressDialog != null) { + this.progressDialog.dismiss(); + } + } + /** * Hides the spinning dialog. */ diff --git a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/SavedUrl.java b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/SavedUrl.java new file mode 100644 index 00000000..fc3aa772 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/SavedUrl.java @@ -0,0 +1,42 @@ +package mil.nga.mapcache.wizards.createtile; + +import mil.nga.mapcache.R; + +/** + * Object containing a url string and an icon to represent it. used in the saved url menu + */ +class SavedUrl { + + private int mIcon; + private String mUrl; + private boolean connected = false; + + public SavedUrl(String url){ + this.mUrl = url; + this.mIcon = R.drawable.cloud_layers_grey; + } + + public String getmUrl() { + return mUrl; + } + + public void setmUrl(String mUrl) { + this.mUrl = mUrl; + } + + public int getmIcon() { + return mIcon; + } + + public void setmIcon(int mIcon) { + this.mIcon = mIcon; + } + + public boolean isConnected() { + return connected; + } + + public void setConnected(boolean connected) { + this.connected = connected; + } +} diff --git a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/SavedUrlAdapter.java b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/SavedUrlAdapter.java new file mode 100644 index 00000000..0e5a8e08 --- /dev/null +++ b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/SavedUrlAdapter.java @@ -0,0 +1,111 @@ +package mil.nga.mapcache.wizards.createtile; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import mil.nga.mapcache.R; +import mil.nga.mapcache.utils.HttpUtils; + +/** + * Adapter for the alertview that pops up during the new tile layer wizard showing + * your currently saved urls + */ +class SavedUrlAdapter extends ArrayAdapter { + + private final Context mContext; + private final List urlList; + /** + * For testing http connections + */ + private final HttpUtils httpUtils = HttpUtils.getInstance(); + + public SavedUrlAdapter(@NonNull Context context, ArrayList list) { + super(context, 0, list); + mContext = context; + urlList = list; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent){ + View listItem = convertView; + if(listItem == null) { + listItem = LayoutInflater.from(mContext).inflate(R.layout.layout_saved_url, parent, false); + } + SavedUrl savedUrl = urlList.get(position); + + // Icon + ImageView urlIcon = (ImageView)listItem.findViewById(R.id.url_icon); + urlIcon.setImageResource(savedUrl.getmIcon()); + + // Url text + TextView urlText = (TextView)listItem.findViewById(R.id.saved_url); + urlText.setText(savedUrl.getmUrl()); + + return listItem; + } + + /** + * Takes a list of SavedUrl objects, and tests the connection of each url. Once complete, it + * will call the adapter's updateIcon method to change the icon to the connection result and + * refresh + * @param list list of SavedUrl objects + */ + public void updateConnections(ArrayList list){ + if(!list.isEmpty()) { + for (SavedUrl savedUrl : list) { + testConnection(savedUrl, this); + } + } + } + + /** + * Finds the given url in the urlList and updates the icon in that SavedUrl object, then refresh + * @param url url to find in the SavedUrl list + * @param icon new icon to set + */ + private void updateIcon(String url, int icon){ + for (SavedUrl savedUrl : urlList) { + if(savedUrl.getmUrl().equalsIgnoreCase(url)){ + savedUrl.setmIcon(icon); + this.notifyDataSetChanged(); + } + } + } + + /** + * Tests connection to a url and call for an icon update + */ + private void testConnection(SavedUrl newUrl, SavedUrlAdapter adapter){ + ExecutorService threadEx = Executors.newSingleThreadExecutor(); + Handler handler = new Handler(Looper.getMainLooper()); + threadEx.execute(() -> { + // Test connection + boolean connected = httpUtils.isServerAvailable(newUrl.getmUrl()); + // Update icon to show connection status + handler.post(() -> { + if(connected) { + adapter.updateIcon(newUrl.getmUrl(), R.drawable.cloud_layers_blue); + } else { + adapter.updateIcon(newUrl.getmUrl(), R.drawable.cloud_layers_red); + + } + }); + }); + } +} diff --git a/mapcache/src/main/res/drawable-hdpi/cloud_layers_blue.png b/mapcache/src/main/res/drawable-hdpi/cloud_layers_blue.png new file mode 100644 index 00000000..4b3f5318 Binary files /dev/null and b/mapcache/src/main/res/drawable-hdpi/cloud_layers_blue.png differ diff --git a/mapcache/src/main/res/drawable-hdpi/cloud_layers_grey.png b/mapcache/src/main/res/drawable-hdpi/cloud_layers_grey.png new file mode 100644 index 00000000..518be8ac Binary files /dev/null and b/mapcache/src/main/res/drawable-hdpi/cloud_layers_grey.png differ diff --git a/mapcache/src/main/res/drawable-hdpi/cloud_layers_lock.png b/mapcache/src/main/res/drawable-hdpi/cloud_layers_lock.png new file mode 100644 index 00000000..18353c8e Binary files /dev/null and b/mapcache/src/main/res/drawable-hdpi/cloud_layers_lock.png differ diff --git a/mapcache/src/main/res/drawable-hdpi/cloud_layers_red.png b/mapcache/src/main/res/drawable-hdpi/cloud_layers_red.png new file mode 100644 index 00000000..84b2c400 Binary files /dev/null and b/mapcache/src/main/res/drawable-hdpi/cloud_layers_red.png differ diff --git a/mapcache/src/main/res/drawable-mdpi/cloud_layers_blue.png b/mapcache/src/main/res/drawable-mdpi/cloud_layers_blue.png new file mode 100644 index 00000000..bc7fe2f4 Binary files /dev/null and b/mapcache/src/main/res/drawable-mdpi/cloud_layers_blue.png differ diff --git a/mapcache/src/main/res/drawable-mdpi/cloud_layers_grey.png b/mapcache/src/main/res/drawable-mdpi/cloud_layers_grey.png new file mode 100644 index 00000000..c83a82dd Binary files /dev/null and b/mapcache/src/main/res/drawable-mdpi/cloud_layers_grey.png differ diff --git a/mapcache/src/main/res/drawable-mdpi/cloud_layers_lock.png b/mapcache/src/main/res/drawable-mdpi/cloud_layers_lock.png new file mode 100644 index 00000000..830ba775 Binary files /dev/null and b/mapcache/src/main/res/drawable-mdpi/cloud_layers_lock.png differ diff --git a/mapcache/src/main/res/drawable-mdpi/cloud_layers_red.png b/mapcache/src/main/res/drawable-mdpi/cloud_layers_red.png new file mode 100644 index 00000000..171ecd8c Binary files /dev/null and b/mapcache/src/main/res/drawable-mdpi/cloud_layers_red.png differ diff --git a/mapcache/src/main/res/drawable-xhdpi/cloud_layers_blue.png b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_blue.png new file mode 100644 index 00000000..c86740de Binary files /dev/null and b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_blue.png differ diff --git a/mapcache/src/main/res/drawable-xhdpi/cloud_layers_grey.png b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_grey.png new file mode 100644 index 00000000..60c19c8b Binary files /dev/null and b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_grey.png differ diff --git a/mapcache/src/main/res/drawable-xhdpi/cloud_layers_lock.png b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_lock.png new file mode 100644 index 00000000..96634aa0 Binary files /dev/null and b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_lock.png differ diff --git a/mapcache/src/main/res/drawable-xhdpi/cloud_layers_red.png b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_red.png new file mode 100644 index 00000000..67ea4b7c Binary files /dev/null and b/mapcache/src/main/res/drawable-xhdpi/cloud_layers_red.png differ diff --git a/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_blue.png b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_blue.png new file mode 100644 index 00000000..e0b815a1 Binary files /dev/null and b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_blue.png differ diff --git a/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_grey.png b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_grey.png new file mode 100644 index 00000000..fb34e9fd Binary files /dev/null and b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_grey.png differ diff --git a/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_lock.png b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_lock.png new file mode 100644 index 00000000..51ed59cc Binary files /dev/null and b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_lock.png differ diff --git a/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_red.png b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_red.png new file mode 100644 index 00000000..e4d63e1c Binary files /dev/null and b/mapcache/src/main/res/drawable-xxhdpi/cloud_layers_red.png differ diff --git a/mapcache/src/main/res/layout/layout_saved_url.xml b/mapcache/src/main/res/layout/layout_saved_url.xml new file mode 100644 index 00000000..eeebbca7 --- /dev/null +++ b/mapcache/src/main/res/layout/layout_saved_url.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/mapcache/src/main/res/layout/layout_saved_url_list.xml b/mapcache/src/main/res/layout/layout_saved_url_list.xml new file mode 100644 index 00000000..73bd44d0 --- /dev/null +++ b/mapcache/src/main/res/layout/layout_saved_url_list.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/mapcache/src/main/res/layout/new_tile_layer_wizard.xml b/mapcache/src/main/res/layout/new_tile_layer_wizard.xml index 8909e07d..755da9cd 100644 --- a/mapcache/src/main/res/layout/new_tile_layer_wizard.xml +++ b/mapcache/src/main/res/layout/new_tile_layer_wizard.xml @@ -77,7 +77,7 @@ android:id="@+id/default_url" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/url_help" + android:layout_alignTop="@+id/example_urls" android:paddingBottom="16dp" android:paddingTop="16dp" android:paddingRight="16dp" @@ -85,6 +85,19 @@ android:textAppearance="@style/textAppearanceSubtitle2_light_heavy" android:text="Choose from saved URLs" /> + + + MapCache - Version 2.1.4 - Released Jul 2022 + Version 2.1.5 + Released Nov 2022 MapCache Map Manager @@ -221,6 +221,7 @@ Delete this saved URL? Map Tile URLs + Example Tile URLs \nFor XYZ tile servers, make sure the URL is formatted with the template on the end: \n\nhttps://yourtileserver.com/{z}/{x}/{y}.png \n\n\nFor WMS tile servers, make sure the URL is formatted with the bounding box coordinates as a template in the query parameters: @@ -248,10 +249,12 @@ http://portal.opengeospatial.org/files/73648 + hurricane_ian washington_dc port_au_prince blue_marble + https://raw.githubusercontent.com/ngageoint/GeoPackage/master/docs/examples/android/hostedGeopackages.json GEOINT OpenStreetMap @@ -378,9 +381,11 @@ <a href=http://ngageoint.github.io/geopackage-android>GeoPackage Android on Github</a> <a href=http://www.geopackage.org/#implementations_nga>OGC GeoPackage</a> - Release Notes - 2.1.4\n \n - - Backend updates for improved gpkg handling\n - - Bug fixes + Release Notes - 2.1.5\n \n + - New status display for tile downloads\n + - New server status display\n + - New sample data\n + - Bug fixes and usability updates @@ -457,5 +462,7 @@ Zoom Level %1$.1f Max Active Features Limit the number of features to display in active layers for faster processing + https://raw.githubusercontent.com/ngageoint/GeoPackage/master/docs/examples/android/exampleTileUrls.json + https://raw.githubusercontent.com/ngageoint/GeoPackage/master/docs/examples/android/hostedGeopackages.json