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 +