diff --git a/mapcache/build.gradle b/mapcache/build.gradle
index 8c004eb4..bb85176c 100644
--- a/mapcache/build.gradle
+++ b/mapcache/build.gradle
@@ -10,13 +10,21 @@ android {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
+
+ // Verbose logging for debug
+// allprojects {
+// tasks.withType(JavaCompile) {
+// options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+// }
+// }
defaultConfig {
applicationId "mil.nga.mapcache"
resValue "string", "applicationId", applicationId
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
minSdkVersion 28
targetSdkVersion 33
- versionCode 56
- versionName '2.1.9'
+ versionCode 59
+ versionName '2.1.10'
multiDexEnabled true
}
buildTypes {
@@ -41,7 +49,7 @@ android {
sourceSets {
main {
java {
- srcDirs 'src/main/java', 'src/test'
+ srcDirs 'src/main/java', 'src/test', 'src/androidTest'
}
}
}
@@ -60,7 +68,7 @@ dependencies {
api 'com.google.android.material:material:1.6.0'
api 'androidx.preference:preference:1.2.1'
api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
- api 'mil.nga.geopackage.map:geopackage-android-map:6.7.1' // comment out to build locally
+ api 'mil.nga.geopackage.map:geopackage-android-map:6.7.2' // comment out to build locally
//api project(':geopackage-map') // uncomment me to build locally
api 'mil.nga.mgrs:mgrs-android:2.2.2'
api 'mil.nga.gars:gars-android:1.2.2'
@@ -71,11 +79,16 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'org.locationtech.jts:jts-core:1.18.2'
+ implementation 'com.github.matomo-org:matomo-sdk-android:v2.0.0'
implementation 'junit:junit:4.12'
testImplementation 'androidx.multidex:multidex:2.0.1'
- testImplementation 'junit:junit:4.13.1'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- implementation 'com.github.matomo-org:matomo-sdk-android:v2.0.0'
+ testImplementation 'junit:junit:4.12'
+ testImplementation "org.robolectric:robolectric:4.7.3"
+ testImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ testImplementation 'androidx.test:core:1.5.0'
+ testImplementation 'androidx.test.ext:junit:1.1.5'
+ testImplementation 'androidx.test:runner:1.5.2'
+ testImplementation 'androidx.test:rules:1.5.0'
}
configure extensions.android, {
diff --git a/mapcache/src/main/AndroidManifest.xml b/mapcache/src/main/AndroidManifest.xml
index 200847ca..aa8459f5 100644
--- a/mapcache/src/main/AndroidManifest.xml
+++ b/mapcache/src/main/AndroidManifest.xml
@@ -3,6 +3,10 @@
package="mil.nga.mapcache"
android:installLocation="auto">
+
+
diff --git a/mapcache/src/main/java/mil/nga/mapcache/FeatureRowProcessor.java b/mapcache/src/main/java/mil/nga/mapcache/FeatureRowProcessor.java
index d589d81b..c944f12e 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/FeatureRowProcessor.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/FeatureRowProcessor.java
@@ -216,10 +216,8 @@ private void processFeatureRow(MapFeaturesUpdateTask task, String database, Feat
}
}
} catch (Exception e) {
- new Handler(Looper.getMainLooper()).post(() -> {
- Toast toast = Toast.makeText(context, "Error loading geometry", Toast.LENGTH_SHORT);
- toast.show();
- });
+ // set task error
+ task.setErrorCount(task.getErrorCount() + 1);
}
}
}
diff --git a/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java b/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java
index f46d9b01..2e52a5b3 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/GeoPackageMapFragment.java
@@ -8,9 +8,7 @@
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageManager;
-import android.graphics.Color;
import android.graphics.Point;
-import android.graphics.PorterDuff;
import android.hardware.SensorEvent;
import android.location.Location;
import android.net.Uri;
@@ -105,19 +103,15 @@
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import java.util.regex.Pattern;
import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
-import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.contents.Contents;
import mil.nga.geopackage.contents.ContentsDao;
import mil.nga.geopackage.db.GeoPackageDataType;
@@ -160,7 +154,6 @@
import mil.nga.mapcache.listeners.LayerActiveSwitchListener;
import mil.nga.mapcache.listeners.OnDialogButtonClickListener;
import mil.nga.mapcache.listeners.SensorCallback;
-import mil.nga.mapcache.load.DownloadTask;
import mil.nga.mapcache.load.Downloader;
import mil.nga.mapcache.load.ILoadTilesTask;
import mil.nga.mapcache.load.ImportTask;
@@ -168,8 +161,7 @@
import mil.nga.mapcache.preferences.GridType;
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.repository.sensors.SensorHandler;
import mil.nga.mapcache.utils.SampleDownloader;
import mil.nga.mapcache.utils.SwipeController;
import mil.nga.mapcache.utils.ViewAnimation;
@@ -584,6 +576,16 @@ private enum EditType {
*/
private Zoomer zoomer;
+ /**
+ * Disclaimer message when first launching the app
+ */
+ AlertDialog disclaimerDialog;
+
+ /**
+ * Max features warning popup dialog
+ */
+ AlertDialog maxFeaturesDialog;
+
/**
* Model that contains various states involving the map.
*/
@@ -1630,29 +1632,42 @@ private void showDisclaimer() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean disclaimerPref = sharedPreferences.getBoolean(getString(R.string.disclaimerPref), false);
if (!disclaimerPref) {
- LayoutInflater inflater = LayoutInflater.from(getActivity());
- View disclaimerView = inflater.inflate(R.layout.disclaimer_window, null);
- Button acceptButton = disclaimerView.findViewById(R.id.accept_button);
- Button exitButton = disclaimerView.findViewById(R.id.exit_button);
-
- AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle)
- .setView(disclaimerView);
- final AlertDialog alertDialog = dialogBuilder.create();
- acceptButton.setOnClickListener((View view) -> {
- sharedPreferences.edit().putBoolean(getString(R.string.disclaimerPref), true).apply();
- alertDialog.dismiss();
- });
- exitButton.setOnClickListener((View view) -> getActivity().finish());
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ View disclaimerView = inflater.inflate(R.layout.disclaimer_window, null);
+ Button acceptButton = disclaimerView.findViewById(R.id.accept_button);
+ Button exitButton = disclaimerView.findViewById(R.id.exit_button);
+
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle)
+ .setView(disclaimerView);
+ disclaimerDialog = dialogBuilder.create();
+ acceptButton.setOnClickListener((View view) -> {
+ sharedPreferences.edit().putBoolean(getString(R.string.disclaimerPref), true).apply();
+ disclaimerDialog.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);
- alertDialog.show();
+ // Prevent the dialog from closing when clicking outside the dialog or the back button
+ disclaimerDialog.setCanceledOnTouchOutside(false);
+ disclaimerDialog.setOnKeyListener((DialogInterface arg0, int keyCode,
+ KeyEvent event) -> true);
+ disclaimerDialog.show();
}
}
}
+ /**
+ * Manually dismiss the disclaimer dialog on stop, otherwise we have a leak
+ */
+ @Override
+ public void onStop() {
+ if(disclaimerDialog != null) {
+ disclaimerDialog.dismiss();
+ }
+ if(maxFeaturesDialog != null){
+ maxFeaturesDialog.dismiss();
+ }
+ super.onStop();
+ }
/**
* Show a warning that the user has selected more features than the current max features setting
@@ -1696,7 +1711,8 @@ private void showMaxFeaturesExceeded() {
}
d.cancel();
});
- dialog.show();
+ maxFeaturesDialog = dialog.create();
+ maxFeaturesDialog.show();
}
}
}
diff --git a/mapcache/src/main/java/mil/nga/mapcache/MapFeaturesUpdateTask.java b/mapcache/src/main/java/mil/nga/mapcache/MapFeaturesUpdateTask.java
index a328f416..481357f6 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/MapFeaturesUpdateTask.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/MapFeaturesUpdateTask.java
@@ -1,7 +1,10 @@
package mil.nga.mapcache;
import android.app.Activity;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
+import android.widget.Toast;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Marker;
@@ -86,6 +89,11 @@ public class MapFeaturesUpdateTask implements Runnable {
*/
private boolean filter;
+ /**
+ * Keep track of any errors when displaying features on the map
+ */
+ private int errorCount = 0;
+
/**
* Constructor.
*
@@ -265,6 +273,8 @@ private void displayFeatures(GeoPackage geoPackage, StyleCache styleCache, Strin
// Get the GeoPackage and feature DAO
String database = geoPackage.getName();
+ setErrorCount(0);
+
Map dataAccessObjects = model.getFeatureDaos().get(database);
if (dataAccessObjects != null) {
FeatureDao featureDao = dataAccessObjects.get(features);
@@ -336,8 +346,16 @@ private void displayFeatures(GeoPackage geoPackage, StyleCache styleCache, Strin
+ ", row: " + cursor.getPosition(), e);
}
}
-
+ int totalErrors = getErrorCount();
+ if(totalErrors > 0){
+ new Handler(Looper.getMainLooper()).post(() -> {
+ Toast toast = Toast.makeText(activity, "Error loading geometry", Toast.LENGTH_SHORT);
+ toast.show();
+ });
+ setErrorCount(0);
+ }
}
+
}
indexer.close();
@@ -361,6 +379,8 @@ private void displayFeatures(GeoPackage geoPackage, StyleCache styleCache, Strin
private void processFeatureIndexResults(FeatureIndexResults indexResults, String database, FeatureDao featureDao,
GoogleMapShapeConverter converter, StyleCache styleCache, AtomicInteger count, final int maxFeatures, final boolean editable) {
try {
+ setErrorCount(0);
+
for (FeatureRow row : indexResults) {
if (cancelled || count.get() >= maxFeatures) {
@@ -384,6 +404,15 @@ private void processFeatureIndexResults(FeatureIndexResults indexResults, String
}
} finally {
indexResults.close();
+ int totalErrors = getErrorCount();
+ if(totalErrors > 0){
+ new Handler(Looper.getMainLooper()).post(() -> {
+ Toast toast = Toast.makeText(activity, totalErrors + " Geometries failed to load", Toast.LENGTH_SHORT);
+ toast.show();
+ });
+ setErrorCount(0);
+
+ }
}
}
@@ -402,6 +431,15 @@ private void addMarkerShape(long featureId, String database, String tableName, G
model.getMarkerIds().put(marker.getId(), markerFeature);
}
}
+ /**
+ * Update the number of errors encountered while processing features
+ */
+ public int getErrorCount() {
+ return errorCount;
+ }
+ public void setErrorCount(int errorCount) {
+ this.errorCount = errorCount;
+ }
@Override
public void run() {
diff --git a/mapcache/src/main/java/mil/nga/mapcache/load/DownloadTask.java b/mapcache/src/main/java/mil/nga/mapcache/load/DownloadTask.java
deleted file mode 100644
index c0e1e075..00000000
--- a/mapcache/src/main/java/mil/nga/mapcache/load/DownloadTask.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package mil.nga.mapcache.load;
-
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.AsyncTask;
-import android.os.PowerManager;
-
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.ViewModelProvider;
-
-import java.net.URL;
-
-import mil.nga.geopackage.io.GeoPackageIOUtils;
-import mil.nga.geopackage.io.GeoPackageProgress;
-import mil.nga.mapcache.GeoPackageUtils;
-import mil.nga.mapcache.R;
-import mil.nga.mapcache.viewmodel.GeoPackageViewModel;
-
-/**
- * Download a GeoPackage from a URL in the background
- */
-public class DownloadTask extends AsyncTask
- implements GeoPackageProgress {
-
- private Integer max = null;
- private int progress = 0;
- private final String database;
- private final String url;
- private PowerManager.WakeLock wakeLock;
- private Activity activity;
- private GeoPackageViewModel geoPackageViewModel;
- private String cancel;
- private String importLabel;
-
- /**
- * Progress dialog for network operations
- */
- private ProgressDialog progressDialog;
-
- /**
- * Constructor
- *
- * @param database
- * @param url
- */
- public DownloadTask(String database, String url, FragmentActivity activity) {
- this.activity = activity;
- this.database = database;
- this.url = url;
- geoPackageViewModel = new ViewModelProvider(activity).get(GeoPackageViewModel.class);
- cancel = activity.getApplicationContext().getResources().getString(R.string.button_cancel_label);
- importLabel = activity.getApplicationContext().getResources().getString(R.string.geopackage_import_label);
- progressDialog = createDownloadProgressDialog(database, url, this, null);
- }
-
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setMax(int max) {
- this.max = max;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void addProgress(int progress) {
- this.progress += progress;
- if (max != null) {
- int total = (int) (this.progress / ((double) max) * 100);
- publishProgress(total);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean isActive() {
- return !isCancelled();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean cleanupOnCancel() {
- return true;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- progressDialog.show();
- PowerManager pm = (PowerManager) activity.getSystemService(
- Context.POWER_SERVICE);
- wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
- getClass().getName());
- wakeLock.acquire();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void onProgressUpdate(Integer... progress) {
- super.onProgressUpdate(progress);
-
- // If the indeterminate progress dialog is still showing, swap to a
- // determinate horizontal bar
- if (progressDialog.isIndeterminate()) {
-
- String messageSuffix = "\n\n"
- + GeoPackageIOUtils.formatBytes(max);
-
- ProgressDialog newProgressDialog = createDownloadProgressDialog(
- database, url, this, messageSuffix);
- newProgressDialog
- .setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- newProgressDialog.setIndeterminate(false);
- newProgressDialog.setMax(100);
-
- newProgressDialog.show();
- progressDialog.dismiss();
- progressDialog = newProgressDialog;
- }
-
- // Set the progress
- progressDialog.setProgress(progress[0]);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void onCancelled(String result) {
- wakeLock.release();
- progressDialog.dismiss();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void onPostExecute(String result) {
- wakeLock.release();
- progressDialog.dismiss();
- if (result != null) {
- GeoPackageUtils.showMessage(activity, importLabel, result);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected String doInBackground(String... params) {
- try {
- URL theUrl = new URL(url);
- if (!geoPackageViewModel.importGeoPackage(database, theUrl, this)) {
- return "Failed to import GeoPackage '" + database
- + "' at url '" + url + "'";
- }
- } catch (final Exception e) {
- return "Couldn't download GeoPackage from: " + url + "\n\nFull error:\n" + e.toString();
- }
- return null;
- }
-
- /**
- * Create a download progress dialog
- *
- * @param database
- * @param url
- * @param downloadTask
- * @param suffix
- * @return
- */
- public ProgressDialog createDownloadProgressDialog(String database,
- String url, final DownloadTask downloadTask, String suffix) {
- ProgressDialog dialog = new ProgressDialog(activity);
- dialog.setMessage(importLabel + " "
- + database + "\n\n" + url + (suffix != null ? suffix : ""));
- dialog.setCancelable(false);
- dialog.setButton(ProgressDialog.BUTTON_NEGATIVE,
- cancel,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- downloadTask.cancel(true);
- }
- });
- dialog.setIndeterminate(true);
-
- return dialog;
- }
-
-}
-
-
diff --git a/mapcache/src/main/java/mil/nga/mapcache/load/ImportTask.java b/mapcache/src/main/java/mil/nga/mapcache/load/ImportTask.java
index 73e68c3d..cb035791 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/load/ImportTask.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/load/ImportTask.java
@@ -10,6 +10,7 @@
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.PowerManager;
import android.view.LayoutInflater;
import android.view.View;
@@ -162,31 +163,35 @@ public void onClick(DialogInterface dialog,
*/
public void importGeoPackageExternalLinkWithPermissions(final String name, final Uri uri, String path) {
- // Check for permission
- if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
- importGeoPackageExternalLink(name, uri, path);
- } else {
- // Save off the values and ask for permission
- importExternalName = name;
- importExternalUri = uri;
- importExternalPath = path;
-
- if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
- new AlertDialog.Builder(activity, R.style.AppCompatAlertDialogStyle)
- .setTitle(R.string.storage_access_rational_title)
- .setMessage(R.string.storage_access_rational_message)
- .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MainActivity.MANAGER_PERMISSIONS_REQUEST_ACCESS_IMPORT_EXTERNAL);
- }
- })
- .create()
- .show();
-
+ if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+ // Check for permission
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+ importGeoPackageExternalLink(name, uri, path);
} else {
- ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MainActivity.MANAGER_PERMISSIONS_REQUEST_ACCESS_IMPORT_EXTERNAL);
+ // Save off the values and ask for permission
+ importExternalName = name;
+ importExternalUri = uri;
+ importExternalPath = path;
+
+ if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ new AlertDialog.Builder(activity, R.style.AppCompatAlertDialogStyle)
+ .setTitle(R.string.storage_access_rational_title)
+ .setMessage(R.string.storage_access_rational_message)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MainActivity.MANAGER_PERMISSIONS_REQUEST_ACCESS_IMPORT_EXTERNAL);
+ }
+ })
+ .create()
+ .show();
+
+ } else {
+ ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MainActivity.MANAGER_PERMISSIONS_REQUEST_ACCESS_IMPORT_EXTERNAL);
+ }
}
+ } else {
+ importGeoPackageExternalLink(name, uri, path);
}
}
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 fbc68874..93dda819 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/preferences/TileUrlFragment.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/preferences/TileUrlFragment.java
@@ -37,6 +37,7 @@
import mil.nga.mapcache.R;
import mil.nga.mapcache.utils.HttpUtils;
+import mil.nga.mapcache.utils.UrlValidator;
import mil.nga.mapcache.utils.ViewAnimation;
/**
@@ -243,6 +244,9 @@ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
} else{
addButton.setEnabled(true);
}
+ if (!UrlValidator.isValidTileUrl(getContext(), url)){
+ inputText.setError("warning: poor url format. This may not work.");
+ }
}
});
diff --git a/mapcache/src/main/java/mil/nga/mapcache/sensors/SensorHandler.java b/mapcache/src/main/java/mil/nga/mapcache/repository/sensors/SensorHandler.java
similarity index 98%
rename from mapcache/src/main/java/mil/nga/mapcache/sensors/SensorHandler.java
rename to mapcache/src/main/java/mil/nga/mapcache/repository/sensors/SensorHandler.java
index f949f410..610aea74 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/sensors/SensorHandler.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/repository/sensors/SensorHandler.java
@@ -1,4 +1,4 @@
-package mil.nga.mapcache.sensors;
+package mil.nga.mapcache.repository.sensors;
import android.app.Service;
diff --git a/mapcache/src/main/java/mil/nga/mapcache/utils/UrlValidator.java b/mapcache/src/main/java/mil/nga/mapcache/utils/UrlValidator.java
index c0a2b0c8..742cf711 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/utils/UrlValidator.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/utils/UrlValidator.java
@@ -11,27 +11,98 @@
public class UrlValidator {
/**
- * Determine if the url is valid based on whether it has xyz or not
- * @param context
- * @param url
- * @return true if the url has xyz info
+ * A URL is valid if it has either xyz or wms params
+ * @param context - context for grabbing string resources
+ * @param url url to validate
+ * @return true if the url has xyz or wms info
*/
public static boolean isValidTileUrl(Context context, String url){
- return hasXYZ(context, url);
+ if(hasXYZ(context, url) || hasWms(url)){
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determine if the url is valid based on whether it has proper wms or not. Looking at required
+ * variables: https://enterprise.arcgis.com/en/server/latest/publish-services/windows/communicating-with-a-wms-service-in-a-web-browser.htm#GUID-90AAB875-0337-451B-86BD-FC5E30F6990A
+ * @param url url to validate
+ * @return true if the url has proper wms variables
+ */
+ public static boolean hasWms(String url){
+ url = url.toLowerCase();
+ boolean valid = true;
+ if (!url.contains("request")) {
+ valid = false;
+ }
+ // GetCapabilities request
+ if(url.contains("capabilities")){
+ if(!url.contains("service=")){
+ valid = false;
+ }
+ }
+ // GetMap request
+ if(url.contains("request=getmap") || url.contains("request=map")){
+ String[] getMapVars = {"layers", "styles", "crs", "bbox", "width", "height", "format"};
+ for (String item : getMapVars) {
+ if (!url.contains(item)) {
+ valid = false;
+ break;
+ }
+ }
+ if(!(url.contains("version") || url.contains("wmtver"))){
+ valid = false;
+ }
+ if(!(url.contains("crs") || url.contains("srs"))){
+ valid = false;
+ }
+ }
+ // GetFeatureInfo request
+ if(url.contains("request=getfeatureinfo") || url.contains("request=feature_info")){
+ if(!(url.contains("version") || url.contains("wmtver"))){
+ valid = false;
+ }
+ if(!url.contains("query_layers")){
+ valid = false;
+ }
+ if(!(url.contains("&i=") || url.contains("&x="))){
+ valid = false;
+ }
+ if(!(url.contains("j=") || url.contains("y="))){
+ valid = false;
+ }
+ }
+ // GetStyles request
+ if(url.contains("request=getstyles")) {
+ if (!url.contains("version=")) {
+ valid = false;
+ }
+ if (!url.contains("layers=")) {
+ valid = false;
+ }
+ }
+ // GetLegendGraphic request
+ if(url.contains("request=getlegendgraphic")) {
+ if (!url.contains("version=")) {
+ valid = false;
+ }
+ if (!url.contains("layer=")) {
+ valid = false;
+ }
+ }
+ return valid;
}
/**
* Determine if the url has x, y, or z variables
*
- * @param url
+ * @param url url to look for xyz data
* @return true if it's a valid xyz url
*/
public static boolean hasXYZ(Context context, String url) {
-
String replacedUrl = replaceXYZ(context, url, 0, 0, 0);
boolean hasXYZ = !replacedUrl.equals(url);
-
return hasXYZ;
}
@@ -39,14 +110,13 @@ public static boolean hasXYZ(Context context, String url) {
/**
* Replace x, y, and z in the url
*
- * @param url
- * @param z
- * @param x
- * @param y
- * @return
+ * @param url url to modify
+ * @param z integer to replace the Z param with
+ * @param x integer to replace the X param with
+ * @param y integer to replace the Y param with
+ * @return string with the xyz portion replaced with the given int values
*/
private static String replaceXYZ(Context context, String url, int z, long x, long y) {
-
url = url.replaceAll(
context.getResources().getString(R.string.tile_generator_variable_z),
String.valueOf(z));
diff --git a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerController.java b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerController.java
index f981f58e..f8da72a7 100644
--- a/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerController.java
+++ b/mapcache/src/main/java/mil/nga/mapcache/wizards/createtile/NewTileLayerController.java
@@ -19,6 +19,7 @@
import mil.nga.mapcache.R;
import mil.nga.mapcache.layersprovider.LayersModel;
import mil.nga.mapcache.ogc.wms.WMSUrlProvider;
+import mil.nga.mapcache.utils.UrlValidator;
import mil.nga.mapcache.viewmodel.GeoPackageViewModel;
/**
@@ -92,6 +93,12 @@ public void setUrl(LayersModel layersModel) {
}
}
+ /**
+ * Validate name and url fields on change. Set error msg if invalid
+ * @param observable the observable object.
+ * @param o an argument passed to the {@code notifyObservers}
+ * method. either name or url field
+ */
@Override
public void update(Observable observable, Object o) {
if (NewTileLayerModel.LAYER_NAME_PROP.equals(o)) {
@@ -115,6 +122,8 @@ public void update(Observable observable, Object o) {
model.setUrlError("URL is required");
} else if (!URLUtil.isValidUrl(givenUrl)) {
model.setUrlError("URL is not valid");
+ } else if (!UrlValidator.isValidTileUrl(fragment.getContext(), givenUrl)){
+ model.setUrlError("Bad URL format");
} else if (givenUrl.contains("${")) {
givenUrl = givenUrl.replaceAll("\\$\\{", "{");
model.setUrl(givenUrl);
diff --git a/mapcache/src/main/res/layout/detail_header_layout.xml b/mapcache/src/main/res/layout/detail_header_layout.xml
index c51fde29..477a2948 100644
--- a/mapcache/src/main/res/layout/detail_header_layout.xml
+++ b/mapcache/src/main/res/layout/detail_header_layout.xml
@@ -80,9 +80,19 @@
android:id="@+id/header_text_features"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
android:text="Feature layers"
android:textAppearance="@style/textAppearanceSubtitle2_light_heavy" />
+
+
+
@color/white87
@color/white75
+ @color/yellowWarningTextNight
@color/nga_accent_primary
@color/nav_not_selected_dark
@color/white50
diff --git a/mapcache/src/main/res/values/color.xml b/mapcache/src/main/res/values/color.xml
index 89868b5b..79d02235 100644
--- a/mapcache/src/main/res/values/color.xml
+++ b/mapcache/src/main/res/values/color.xml
@@ -70,6 +70,8 @@
#C4FFFFFF
+ #FFC107
+
@@ -83,6 +85,8 @@
#7EFFFFFF
#05FFFFFF
#954A37
+ #CFAE4A
+
#1E5659
diff --git a/mapcache/src/main/res/values/colors.xml b/mapcache/src/main/res/values/colors.xml
index ce0e8aee..d0198464 100644
--- a/mapcache/src/main/res/values/colors.xml
+++ b/mapcache/src/main/res/values/colors.xml
@@ -23,6 +23,7 @@
@color/black87
@color/black50
+ @color/yellowWarningText
@color/nga_primary_light
@color/nav_not_selected
@color/nav_not_selected
diff --git a/mapcache/src/main/res/values/strings.xml b/mapcache/src/main/res/values/strings.xml
index 262bf79e..7abc4919 100644
--- a/mapcache/src/main/res/values/strings.xml
+++ b/mapcache/src/main/res/values/strings.xml
@@ -2,8 +2,8 @@
MapCache
- Version 2.1.9
- Released Sept 2023
+ Version 2.1.10
+ Released Oct 2023
MapCache
Map
Manager
@@ -386,9 +386,12 @@
<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.9\n \n
- - Fixes for file and camera permissions on android 13\n
- - Fix for show my location
+ Release Notes - 2.1.10\n \n
+ - Improved geometry error handling\n
+ - Fix for file linking errors\n
+ - Url validation improvements for tiles\n
+ - GeoPackage android map library 6.7.2\n
+ - Background improvements
diff --git a/mapcache/src/main/res/values/styles.xml b/mapcache/src/main/res/values/styles.xml
index 4ed532c8..5af09988 100644
--- a/mapcache/src/main/res/values/styles.xml
+++ b/mapcache/src/main/res/values/styles.xml
@@ -191,6 +191,12 @@
- @color/textSecondaryColor
- 0.0071
+