package mil.nga.giat.mage.map; import android.Manifest; import android.app.Activity; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.location.Location; import android.location.LocationListener; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.content.res.AppCompatResources; import android.support.v7.widget.SearchView; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; import android.widget.ImageButton; import android.widget.Toast; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.CancelableCallback; import com.google.android.gms.maps.GoogleMap.OnInfoWindowClickListener; import com.google.android.gms.maps.GoogleMap.OnMapClickListener; import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener; import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener; import com.google.android.gms.maps.GoogleMap.OnMyLocationButtonClickListener; import com.google.android.gms.maps.LocationSource; import com.google.android.gms.maps.MapView; import com.google.android.gms.maps.MapsInitializer; import com.google.android.gms.maps.OnMapReadyCallback; 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.Marker; import com.google.android.gms.maps.model.TileOverlay; import com.google.android.gms.maps.model.TileOverlayOptions; import com.google.android.gms.maps.model.TileProvider; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.Point; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import mil.nga.geopackage.BoundingBox; import mil.nga.geopackage.GeoPackage; import mil.nga.geopackage.GeoPackageCache; import mil.nga.geopackage.GeoPackageManager; import mil.nga.geopackage.core.contents.Contents; import mil.nga.geopackage.core.contents.ContentsDao; import mil.nga.geopackage.extension.link.FeatureTileTableLinker; import mil.nga.geopackage.factory.GeoPackageFactory; import mil.nga.geopackage.features.index.FeatureIndexManager; 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.geom.GeoPackageGeometryData; import mil.nga.geopackage.geom.map.GoogleMapShape; import mil.nga.geopackage.geom.map.GoogleMapShapeConverter; import mil.nga.geopackage.projection.Projection; import mil.nga.geopackage.projection.ProjectionConstants; import mil.nga.geopackage.projection.ProjectionFactory; import mil.nga.geopackage.projection.ProjectionTransform; import mil.nga.geopackage.tiles.TileBoundingBoxUtils; import mil.nga.geopackage.tiles.features.FeatureTiles; import mil.nga.geopackage.tiles.features.MapFeatureTiles; import mil.nga.geopackage.tiles.features.custom.NumberFeaturesTile; import mil.nga.geopackage.tiles.overlay.BoundedOverlay; import mil.nga.geopackage.tiles.overlay.FeatureOverlay; import mil.nga.geopackage.tiles.overlay.FeatureOverlayQuery; import mil.nga.geopackage.tiles.overlay.GeoPackageOverlayFactory; import mil.nga.geopackage.tiles.user.TileDao; import mil.nga.giat.mage.MAGE; import mil.nga.giat.mage.R; import mil.nga.giat.mage.filter.DateTimeFilter; import mil.nga.giat.mage.filter.Filter; import mil.nga.giat.mage.filter.FilterActivity; import mil.nga.giat.mage.map.GoogleMapWrapper.OnMapPanListener; import mil.nga.giat.mage.map.cache.CacheOverlay; import mil.nga.giat.mage.map.cache.CacheOverlayType; import mil.nga.giat.mage.map.cache.CacheProvider; import mil.nga.giat.mage.map.cache.CacheProvider.OnCacheOverlayListener; import mil.nga.giat.mage.map.cache.GeoPackageCacheOverlay; import mil.nga.giat.mage.map.cache.GeoPackageFeatureTableCacheOverlay; import mil.nga.giat.mage.map.cache.GeoPackageTileTableCacheOverlay; import mil.nga.giat.mage.map.cache.XYZDirectoryCacheOverlay; import mil.nga.giat.mage.map.marker.LocationMarkerCollection; import mil.nga.giat.mage.map.marker.MyHistoricalLocationMarkerCollection; import mil.nga.giat.mage.map.marker.ObservationMarkerCollection; import mil.nga.giat.mage.map.marker.PointCollection; import mil.nga.giat.mage.map.marker.StaticGeometryCollection; import mil.nga.giat.mage.map.preference.MapPreferencesActivity; import mil.nga.giat.mage.observation.ObservationEditActivity; import mil.nga.giat.mage.sdk.Temporal; import mil.nga.giat.mage.sdk.datastore.layer.Layer; import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; import mil.nga.giat.mage.sdk.datastore.location.LocationHelper; import mil.nga.giat.mage.sdk.datastore.location.LocationProperty; import mil.nga.giat.mage.sdk.datastore.observation.Observation; import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper; import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureHelper; import mil.nga.giat.mage.sdk.datastore.user.EventHelper; import mil.nga.giat.mage.sdk.datastore.user.User; import mil.nga.giat.mage.sdk.datastore.user.UserHelper; import mil.nga.giat.mage.sdk.event.ILocationEventListener; import mil.nga.giat.mage.sdk.event.IObservationEventListener; import mil.nga.giat.mage.sdk.event.IStaticFeatureEventListener; import mil.nga.giat.mage.sdk.exceptions.LayerException; import mil.nga.giat.mage.sdk.exceptions.UserException; import mil.nga.giat.mage.sdk.location.LocationService; import mil.nga.wkb.geom.GeometryType; public class MapFragment extends Fragment implements OnMapReadyCallback, OnMapClickListener, OnMapLongClickListener, OnMarkerClickListener, OnInfoWindowClickListener, OnMapPanListener, OnMyLocationButtonClickListener, OnClickListener, LocationSource, LocationListener, OnCacheOverlayListener, IObservationEventListener, ILocationEventListener, IStaticFeatureEventListener { private static final String LOG_NAME = MapFragment.class.getName(); private static final String MAP_VIEW_STATE = "MAP_VIEW_STATE"; private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1; private MAGE mage; private MapView mapView; private GoogleMap map; private boolean mapInitialized = false; private View searchLayout; private SearchView searchView; private Location location; private boolean followMe = false; private GoogleMapWrapper mapWrapper; protected User currentUser = null; private OnLocationChangedListener locationChangedListener; private static final int REFRESHMARKERINTERVALINSECONDS = 300; private RefreshMarkersTask refreshLocationsMarkersTask; private RefreshMarkersTask refreshMyHistoricLocationsMarkersTask; private final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(64); private final ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 8, 10, TimeUnit.SECONDS, queue); private PointCollection<Observation> observations; private PointCollection<mil.nga.giat.mage.sdk.datastore.location.Location> locations; private PointCollection<mil.nga.giat.mage.sdk.datastore.location.Location> historicLocations; private StaticGeometryCollection staticGeometryCollection; private List<Marker> searchMarkers = new ArrayList<>(); private Map<String, CacheOverlay> cacheOverlays = new HashMap<>(); // GeoPackage cache of open GeoPackage connections private GeoPackageCache geoPackageCache; private BoundingBox addedCacheBoundingBox; private FloatingActionButton searchButton; private FloatingActionButton zoomToLocationButton; private LocationService locationService; SharedPreferences preferences; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_map, container, false); setHasOptionsMenu(true); zoomToLocationButton = (FloatingActionButton) view.findViewById(R.id.zoom_button); searchButton = (FloatingActionButton) view.findViewById(R.id.map_search_button); Drawable drawable = DrawableCompat.wrap(searchButton.getDrawable()); DrawableCompat.setTintList(drawable, AppCompatResources.getColorStateList(getContext(), R.color.map_search_icon)); searchButton.setImageDrawable(drawable); searchButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { search(); } }); view.findViewById(R.id.new_observation_button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onNewObservation(); } }); mage = (MAGE) getActivity().getApplication(); locationService = mage.getLocationService(); preferences = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); searchLayout = view.findViewById(R.id.search_layout); searchView = (SearchView) view.findViewById(R.id.search_view); searchView.setIconifiedByDefault(false); searchView.setIconified(false); searchView.clearFocus(); MapsInitializer.initialize(getActivity().getApplicationContext()); ImageButton mapSettings = (ImageButton) view.findViewById(R.id.map_settings); mapSettings.setOnClickListener(this); mapWrapper = new GoogleMapWrapper(getActivity().getApplicationContext()); mapWrapper.addView(view); mapView = (MapView) view.findViewById(R.id.mapView); Bundle mapState = (savedInstanceState != null) ? savedInstanceState.getBundle(MAP_VIEW_STATE): null; mapView.onCreate(mapState); mapView.getMapAsync(this); // Initialize the GeoPackage cache with a GeoPackage manager GeoPackageManager geoPackageManager = GeoPackageFactory.getManager(getActivity().getApplicationContext()); geoPackageCache = new GeoPackageCache(geoPackageManager); return mapWrapper; } @Override public void onDestroyView() { super.onDestroyView(); mapView.onDestroy(); ObservationHelper.getInstance(getActivity().getApplicationContext()).removeListener(this); LocationHelper.getInstance(getActivity().getApplicationContext()).removeListener(this); if (observations != null) { observations.clear(); observations = null; } if (locations != null) { locations.clear(); locations = null; } if (historicLocations != null) { historicLocations.clear(); historicLocations = null; } if (searchMarkers != null) { for (Marker m : searchMarkers) { m.remove(); } searchMarkers.clear(); } // Close all open GeoPackages geoPackageCache.closeAll(); staticGeometryCollection = null; currentUser = null; map = null; mapInitialized = false; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.filter, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.filter_button: Intent intent = new Intent(getActivity(), FilterActivity.class); startActivity(intent); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onMapReady(GoogleMap googleMap) { map = googleMap; initializeMap(); } private void initializeMap() { if (map == null) return; if (!mapInitialized) { mapInitialized = true; map.getUiSettings().setMyLocationButtonEnabled(false); map.setOnMapClickListener(this); map.setOnMarkerClickListener(this); map.setOnMapLongClickListener(this); map.setOnMyLocationButtonClickListener(this); map.setOnInfoWindowClickListener(this); zoomToLocationButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Location location = locationService.getLocation(); LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 18); map.animateCamera(cameraUpdate); } }); staticGeometryCollection = new StaticGeometryCollection(); historicLocations = new MyHistoricalLocationMarkerCollection(getActivity(), map); HistoricLocationLoadTask myHistoricLocationLoad = new HistoricLocationLoadTask(getActivity(), historicLocations); myHistoricLocationLoad.executeOnExecutor(executor); ObservationHelper.getInstance(getActivity().getApplicationContext()).addListener(this); LocationHelper.getInstance(getActivity().getApplicationContext()).addListener(this); } if (observations != null) { observations.clear(); } observations = new ObservationMarkerCollection(getActivity(), map); ObservationLoadTask observationLoad = new ObservationLoadTask(getActivity(), observations); observationLoad.addFilter(getTemporalFilter("timestamp")); observationLoad.executeOnExecutor(executor); if (locations != null) { locations.clear(); } locations = new LocationMarkerCollection(getActivity(), map); LocationLoadTask locationLoad = new LocationLoadTask(getActivity(), locations); locationLoad.setFilter(getTemporalFilter("timestamp")); locationLoad.executeOnExecutor(executor); updateMapView(); // Set visibility on map markers as preferences may have changed observations.setVisibility(preferences.getBoolean(getResources().getString(R.string.showObservationsKey), true)); locations.setVisibility(preferences.getBoolean(getResources().getString(R.string.showLocationsKey), true)); historicLocations.setVisibility(preferences.getBoolean(getResources().getString(R.string.showMyLocationHistoryKey), false)); // task to refresh location markers every x seconds refreshLocationsMarkersTask = new RefreshMarkersTask(locations); if (!refreshLocationsMarkersTask.isCancelled()) { refreshLocationsMarkersTask.executeOnExecutor(executor, REFRESHMARKERINTERVALINSECONDS); } // task to refresh my historic location markers every x seconds refreshMyHistoricLocationsMarkersTask = new RefreshMarkersTask(historicLocations); if (!refreshMyHistoricLocationsMarkersTask.isCancelled()) { refreshMyHistoricLocationsMarkersTask.executeOnExecutor(executor, REFRESHMARKERINTERVALINSECONDS); } updateStaticFeatureLayers(); CacheProvider.getInstance(getActivity().getApplicationContext()).registerCacheOverlayListener(this); StaticFeatureHelper.getInstance(getActivity().getApplicationContext()).addListener(this); // Check if any map preferences changed that I care about if (locationService != null && ContextCompat.checkSelfPermission(getActivity().getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { map.setMyLocationEnabled(true); map.setLocationSource(this); locationService.registerOnLocationListener(this); } else { map.setMyLocationEnabled(false); map.setLocationSource(null); } } @Override public void onLowMemory() { super.onLowMemory(); mapView.onLowMemory(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Bundle mapState = new Bundle(); mapView.onSaveInstanceState(mapState); outState.putBundle(MAP_VIEW_STATE, mapState); } @Override public void onResume() { super.onResume(); try { currentUser = UserHelper.getInstance(getActivity().getApplicationContext()).readCurrentUser(); } catch (UserException ue) { Log.e(LOG_NAME, "Could not find current user.", ue); } mapView.onResume(); initializeMap(); ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(getFilterTitle()); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { if (StringUtils.isNoneBlank(query)) { new GeocoderTask(getActivity(), map, searchMarkers).execute(query); } searchView.clearFocus(); return true; } @Override public boolean onQueryTextChange(String newText) { if (StringUtils.isEmpty(newText)) { if (searchMarkers != null) { for (Marker m : searchMarkers) { m.remove(); } searchMarkers.clear(); } } return true; } }); } @Override public void onPause() { super.onPause(); if (refreshLocationsMarkersTask != null) { refreshLocationsMarkersTask.cancel(true); refreshLocationsMarkersTask = null; } if (refreshMyHistoricLocationsMarkersTask != null) { refreshMyHistoricLocationsMarkersTask.cancel(true); refreshMyHistoricLocationsMarkersTask = null; } mapView.onPause(); CacheProvider.getInstance(getActivity().getApplicationContext()).unregisterCacheOverlayListener(this); StaticFeatureHelper.getInstance(getActivity().getApplicationContext()).removeListener(this); if (map != null) { saveMapView(); map.setLocationSource(null); if (locationService != null) { locationService.unregisterOnLocationListener(this); } } } private void search() { boolean isVisible = searchLayout.getVisibility() == View.VISIBLE; searchLayout.setVisibility(isVisible ? View.GONE : View.VISIBLE); searchButton.setSelected(!isVisible); if (isVisible) { searchView.clearFocus(); } else { searchView.requestFocus(); InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); } } private void onNewObservation() { Intent intent = new Intent(getActivity().getApplicationContext(), ObservationEditActivity.class); Location l = locationService.getLocation(); // if there is not a location from the location service, then try to pull one from the database. if (l == null) { List<mil.nga.giat.mage.sdk.datastore.location.Location> tLocations = LocationHelper.getInstance(getActivity().getApplicationContext()).getCurrentUserLocations(getActivity().getApplicationContext(), 1, true); if (!tLocations.isEmpty()) { mil.nga.giat.mage.sdk.datastore.location.Location tLocation = tLocations.get(0); Geometry geo = tLocation.getGeometry(); Map<String, LocationProperty> propertiesMap = tLocation.getPropertiesMap(); if (geo instanceof Point) { Point point = (Point) geo; String provider = "manual"; if (propertiesMap.get("provider").getValue() != null) { provider = propertiesMap.get("provider").getValue().toString(); } l = new Location(provider); l.setTime(tLocation.getTimestamp().getTime()); if (propertiesMap.get("accuracy").getValue() != null) { l.setAccuracy(Float.valueOf(propertiesMap.get("accuracy").getValue().toString())); } l.setLatitude(point.getY()); l.setLongitude(point.getX()); } } } else { l = new Location(l); } if (!UserHelper.getInstance(getActivity().getApplicationContext()).isCurrentUserPartOfCurrentEvent()) { new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getResources().getString(R.string.location_no_event_title)) .setMessage(getActivity().getResources().getString(R.string.location_no_event_message)) .setPositiveButton(android.R.string.ok, null) .show(); } else if (l != null) { intent.putExtra(ObservationEditActivity.LOCATION, l); intent.putExtra(ObservationEditActivity.INITIAL_LOCATION, map.getCameraPosition().target); intent.putExtra(ObservationEditActivity.INITIAL_ZOOM, map.getCameraPosition().zoom); startActivity(intent); } else { if (ContextCompat.checkSelfPermission(getActivity().getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getResources().getString(R.string.location_missing_title)) .setMessage(getActivity().getResources().getString(R.string.location_missing_message)) .setPositiveButton(android.R.string.ok, null) .show(); } else { new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getResources().getString(R.string.location_access_observation_title)) .setMessage(getActivity().getResources().getString(R.string.location_access_observation_message)) .setPositiveButton(android.R.string.ok, new Dialog.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION); } }) .show(); } } } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { onNewObservation(); } break; } } } @Override public void onObservationCreated(Collection<Observation> o, Boolean sendUserNotifcations) { if (observations != null) { ObservationTask task = new ObservationTask(getActivity(), ObservationTask.Type.ADD, observations); task.addFilter(getTemporalFilter("last_modified")); task.executeOnExecutor(executor, o.toArray(new Observation[o.size()])); } } @Override public void onObservationUpdated(Observation o) { if (observations != null) { ObservationTask task = new ObservationTask(getActivity(), ObservationTask.Type.UPDATE, observations); task.addFilter(getTemporalFilter("last_modified")); task.executeOnExecutor(executor, o); } } @Override public void onObservationDeleted(Observation o) { if (observations != null) { new ObservationTask(getActivity(), ObservationTask.Type.DELETE, observations).executeOnExecutor(executor, o); } } @Override public void onLocationCreated(Collection<mil.nga.giat.mage.sdk.datastore.location.Location> ls) { for (mil.nga.giat.mage.sdk.datastore.location.Location l : ls) { if (currentUser != null && !currentUser.getRemoteId().equals(l.getUser().getRemoteId())) { if (locations != null) { LocationTask task = new LocationTask(LocationTask.Type.ADD, locations); task.setFilter(getTemporalFilter("timestamp")); task.executeOnExecutor(executor, l); } } else { if (historicLocations != null) { new LocationTask(LocationTask.Type.ADD, historicLocations).executeOnExecutor(executor, l); } } } } @Override public void onLocationUpdated(mil.nga.giat.mage.sdk.datastore.location.Location l) { if (currentUser != null && !currentUser.getRemoteId().equals(l.getUser().getRemoteId())) { if (locations != null) { LocationTask task = new LocationTask(LocationTask.Type.UPDATE, locations); task.setFilter(getTemporalFilter("timestamp")); task.executeOnExecutor(executor, l); } } else { if (historicLocations != null) { new LocationTask(LocationTask.Type.UPDATE, historicLocations).executeOnExecutor(executor, l); } } } @Override public void onLocationDeleted(Collection<mil.nga.giat.mage.sdk.datastore.location.Location> l) { // this is slowing the app down a lot! Moving the delete like code into the add methods of the collections /* if (currentUser != null && !currentUser.getRemoteId().equals(l.getUser().getRemoteId())) { if (locations != null) { new LocationTask(LocationTask.Type.DELETE, locations).execute(l); } } else { if (myHistoricLocations != null) { new LocationTask(LocationTask.Type.DELETE, myHistoricLocations).execute(l); } } */ } @Override public void onInfoWindowClick(Marker marker) { observations.onInfoWindowClick(marker); locations.onInfoWindowClick(marker); } @Override public boolean onMarkerClick(Marker marker) { hideKeyboard(); // search marker if(searchMarkers != null) { for(Marker m :searchMarkers) { if(marker.getId().equals(m.getId())) { m.showInfoWindow(); return true; } } } // You can only have one marker click listener per map. // Lets listen here and shell out the click event to all // my marker collections. Each one need to handle // gracefully if it does not actually contain the marker if (observations.onMarkerClick(marker)) { return true; } if (locations.onMarkerClick(marker)) { return true; } if (historicLocations.onMarkerClick(marker)) { return true; } // static layer if(marker.getSnippet() != null) { View markerInfoWindow = LayoutInflater.from(getActivity()).inflate(R.layout.static_feature_infowindow, null, false); WebView webView = ((WebView) markerInfoWindow.findViewById(R.id.static_feature_infowindow_content)); webView.loadData(marker.getSnippet(), "text/html; charset=UTF-8", null); new AlertDialog.Builder(getActivity()) .setView(markerInfoWindow) .setPositiveButton(android.R.string.yes, null) .show(); } return true; } @Override public void onMapClick(LatLng latLng) { hideKeyboard(); // remove old accuracy circle ((LocationMarkerCollection) locations).offMarkerClick(); staticGeometryCollection.onMapClick(map, latLng, getActivity()); if(!cacheOverlays.isEmpty()) { StringBuilder clickMessage = new StringBuilder(); for (CacheOverlay cacheOverlay : cacheOverlays.values()) { String message = cacheOverlay.onMapClick(latLng, mapView, map); if(message != null){ if(clickMessage.length() > 0){ clickMessage.append("\n\n"); } clickMessage.append(message); } } if(clickMessage.length() > 0) { new AlertDialog.Builder(getActivity()) .setMessage(clickMessage.toString()) .setPositiveButton(android.R.string.yes, null) .show(); } } } @Override public void onMapLongClick(LatLng point) { hideKeyboard(); if(!UserHelper.getInstance(getActivity().getApplicationContext()).isCurrentUserPartOfCurrentEvent()) { new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getResources().getString(R.string.location_no_event_title)) .setMessage(getActivity().getResources().getString(R.string.location_no_event_message)) .setPositiveButton(android.R.string.ok, null) .show(); } else { Intent intent = new Intent(getActivity().getApplicationContext(), ObservationEditActivity.class); Location l = new Location("manual"); l.setAccuracy(0.0f); l.setLatitude(point.latitude); l.setLongitude(point.longitude); l.setTime(new Date().getTime()); intent.putExtra(ObservationEditActivity.LOCATION, l); startActivity(intent); } } @Override public void onClick(View view) { // close keyboard hideKeyboard(); switch (view.getId()) { case R.id.map_settings: { Intent intent = new Intent(getActivity(), MapPreferencesActivity.class); startActivity(intent); break; } } } @Override public boolean onMyLocationButtonClick() { if (location != null) { LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); float zoom = map.getCameraPosition().zoom < 15 ? 15 : map.getCameraPosition().zoom; map.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoom), new CancelableCallback() { @Override public void onCancel() { mapWrapper.setOnMapPanListener(MapFragment.this); followMe = true; } @Override public void onFinish() { mapWrapper.setOnMapPanListener(MapFragment.this); followMe = true; } }); } return true; } @Override public void activate(OnLocationChangedListener listener) { Log.i(LOG_NAME, "map location, activate"); locationChangedListener = listener; if (location != null) { Log.i(LOG_NAME, "map location, activate we have a location, let our listener know"); locationChangedListener.onLocationChanged(location); } } @Override public void deactivate() { Log.i(LOG_NAME, "map location, deactivate"); locationChangedListener = null; } @Override public void onMapPan() { mapWrapper.setOnMapPanListener(null); followMe = false; } @Override public void onLocationChanged(Location location) { this.location = location; Log.d(LOG_NAME, "Map location updated."); if (locationChangedListener != null) { locationChangedListener.onLocationChanged(location); } if (followMe) { LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds; LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); if (!bounds.contains(latLng)) { // Move the camera to the user's location once it's available! map.animateCamera(CameraUpdateFactory.newLatLng(latLng)); } } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } @Override public void onCacheOverlay(List<CacheOverlay> cacheOverlays) { // Add all overlays that are in the preferences // Track enabled cache overlays Map<String, CacheOverlay> enabledCacheOverlays = new HashMap<>(); // Track enabled GeoPackages Set<String> enabledGeoPackages = new HashSet<>(); // Reset the bounding box for newly added caches addedCacheBoundingBox = null; for (CacheOverlay cacheOverlay : cacheOverlays) { // If this cache overlay potentially replaced by a new version if(cacheOverlay.isAdded()){ if(cacheOverlay.getType() == CacheOverlayType.GEOPACKAGE){ geoPackageCache.close(cacheOverlay.getName()); } } // The user has asked for this overlay if (cacheOverlay.isEnabled()) { // Handle each type of cache overlay switch(cacheOverlay.getType()) { case XYZ_DIRECTORY: addXYZDirectoryCacheOverlay(enabledCacheOverlays, (XYZDirectoryCacheOverlay) cacheOverlay); break; case GEOPACKAGE: addGeoPackageCacheOverlay(enabledCacheOverlays, enabledGeoPackages, (GeoPackageCacheOverlay)cacheOverlay); break; } } cacheOverlay.setAdded(false); } // Remove any overlays that are on the map but no longer selected in // preferences, update the tile overlays to the enabled tile overlays for (CacheOverlay cacheOverlay : this.cacheOverlays.values()) { cacheOverlay.removeFromMap(); } this.cacheOverlays = enabledCacheOverlays; // Close GeoPackages no longer enabled geoPackageCache.closeRetain(enabledGeoPackages); // If a new cache was added, zoom to the bounding box area if(addedCacheBoundingBox != null){ final LatLngBounds.Builder boundsBuilder = new LatLngBounds.Builder(); boundsBuilder.include(new LatLng(addedCacheBoundingBox.getMinLatitude(), addedCacheBoundingBox .getMinLongitude())); boundsBuilder.include(new LatLng(addedCacheBoundingBox.getMinLatitude(), addedCacheBoundingBox .getMaxLongitude())); boundsBuilder.include(new LatLng(addedCacheBoundingBox.getMaxLatitude(), addedCacheBoundingBox .getMinLongitude())); boundsBuilder.include(new LatLng(addedCacheBoundingBox.getMaxLatitude(), addedCacheBoundingBox .getMaxLongitude())); try { map.animateCamera(CameraUpdateFactory.newLatLngBounds( boundsBuilder.build(), 0)); } catch (Exception e) { Log.e(LOG_NAME, "Unable to move camera to newly added cache location", e); } } } /** * Add XYZ Directory tile cache overlay * @param enabledCacheOverlays * @param xyzDirectoryCacheOverlay */ private void addXYZDirectoryCacheOverlay(Map<String, CacheOverlay> enabledCacheOverlays, XYZDirectoryCacheOverlay xyzDirectoryCacheOverlay){ // Retrieve the cache overlay if it already exists (and remove from cache overlays) CacheOverlay cacheOverlay = cacheOverlays.remove(xyzDirectoryCacheOverlay.getCacheName()); if(cacheOverlay == null){ // Create a new tile provider and add to the map TileProvider tileProvider = new FileSystemTileProvider(256, 256, xyzDirectoryCacheOverlay.getDirectory().getAbsolutePath()); TileOverlayOptions overlayOptions = createTileOverlayOptions(tileProvider); // Set the tile overlay in the cache overlay TileOverlay tileOverlay = map.addTileOverlay(overlayOptions); xyzDirectoryCacheOverlay.setTileOverlay(tileOverlay); cacheOverlay = xyzDirectoryCacheOverlay; } // Add the cache overlay to the enabled cache overlays enabledCacheOverlays.put(cacheOverlay.getCacheName(), cacheOverlay); } /** * Add a GeoPackage cache overlay, which contains tile and feature tables * @param enabledCacheOverlays * @param enabledGeoPackages * @param geoPackageCacheOverlay */ private void addGeoPackageCacheOverlay(Map<String, CacheOverlay> enabledCacheOverlays, Set<String> enabledGeoPackages, GeoPackageCacheOverlay geoPackageCacheOverlay){ // Check each GeoPackage table for(CacheOverlay tableCacheOverlay: geoPackageCacheOverlay.getChildren()){ // Check if the table is enabled if(tableCacheOverlay.isEnabled()){ // Get and open if needed the GeoPackage GeoPackage geoPackage = geoPackageCache.getOrOpen(geoPackageCacheOverlay.getName()); enabledGeoPackages.add(geoPackage.getName()); // Handle tile and feature tables switch(tableCacheOverlay.getType()){ case GEOPACKAGE_TILE_TABLE: addGeoPackageTileCacheOverlay(enabledCacheOverlays, (GeoPackageTileTableCacheOverlay)tableCacheOverlay, geoPackage, false); break; case GEOPACKAGE_FEATURE_TABLE: addGeoPackageFeatureCacheOverlay(enabledCacheOverlays, (GeoPackageFeatureTableCacheOverlay)tableCacheOverlay, geoPackage); break; default: throw new UnsupportedOperationException("Unsupported GeoPackage type: " + tableCacheOverlay.getType()); } // If a newly added cache, update the bounding box for zooming if(geoPackageCacheOverlay.isAdded()){ try { ContentsDao contentsDao = geoPackage.getContentsDao(); Contents contents = contentsDao.queryForId(tableCacheOverlay.getName()); BoundingBox contentsBoundingBox = contents.getBoundingBox(); Projection projection = ProjectionFactory .getProjection(contents.getSrs().getOrganizationCoordsysId()); ProjectionTransform transform = projection.getTransformation(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM); BoundingBox boundingBox = transform.transform(contentsBoundingBox); boundingBox = TileBoundingBoxUtils.boundWgs84BoundingBoxWithWebMercatorLimits(boundingBox); if (addedCacheBoundingBox == null) { addedCacheBoundingBox = boundingBox; } else { addedCacheBoundingBox = TileBoundingBoxUtils.union(addedCacheBoundingBox, boundingBox); } }catch(Exception e){ Log.e(LOG_NAME, "Failed to retrieve GeoPackage Table bounding box. GeoPackage: " + geoPackage.getName() + ", Table: " + tableCacheOverlay.getName(), e); } } } } } /** * Add the GeoPackage Tile Table Cache Overlay * @param enabledCacheOverlays * @param tileTableCacheOverlay * @param geoPackage * @param linkedToFeatures */ private void addGeoPackageTileCacheOverlay(Map<String, CacheOverlay> enabledCacheOverlays, GeoPackageTileTableCacheOverlay tileTableCacheOverlay, GeoPackage geoPackage, boolean linkedToFeatures){ // Retrieve the cache overlay if it already exists (and remove from cache overlays) CacheOverlay cacheOverlay = cacheOverlays.remove(tileTableCacheOverlay.getCacheName()); if(cacheOverlay != null){ // If the existing cache overlay is being replaced, create a new cache overlay if(tileTableCacheOverlay.getParent().isAdded()){ cacheOverlay = null; } } if(cacheOverlay == null){ // Create a new GeoPackage tile provider and add to the map TileDao tileDao = geoPackage.getTileDao(tileTableCacheOverlay.getName()); BoundedOverlay geoPackageTileProvider = GeoPackageOverlayFactory.getBoundedOverlay(tileDao); TileOverlayOptions overlayOptions = null; if(linkedToFeatures){ overlayOptions = createFeatureTileOverlayOptions(geoPackageTileProvider); }else { overlayOptions = createTileOverlayOptions(geoPackageTileProvider); } TileOverlay tileOverlay = map.addTileOverlay(overlayOptions); tileTableCacheOverlay.setTileOverlay(tileOverlay); // Check for linked feature tables tileTableCacheOverlay.clearFeatureOverlayQueries(); FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage); List<FeatureDao> featureDaos = linker.getFeatureDaosForTileTable(tileDao.getTableName()); for(FeatureDao featureDao: featureDaos){ // Create the feature tiles FeatureTiles featureTiles = new MapFeatureTiles(getActivity(), featureDao); // Create an index manager FeatureIndexManager indexer = new FeatureIndexManager(getActivity(), geoPackage, featureDao); featureTiles.setIndexManager(indexer); // Add the feature overlay query FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(getActivity(), geoPackageTileProvider, featureTiles); tileTableCacheOverlay.addFeatureOverlayQuery(featureOverlayQuery); } cacheOverlay = tileTableCacheOverlay; } // Add the cache overlay to the enabled cache overlays enabledCacheOverlays.put(cacheOverlay.getCacheName(), cacheOverlay); } /** * Add the GeoPackage Feature Table Cache Overlay * @param enabledCacheOverlays * @param featureTableCacheOverlay * @param geoPackage */ private void addGeoPackageFeatureCacheOverlay(Map<String, CacheOverlay> enabledCacheOverlays, GeoPackageFeatureTableCacheOverlay featureTableCacheOverlay, GeoPackage geoPackage){ // Retrieve the cache overlay if it already exists (and remove from cache overlays) CacheOverlay cacheOverlay = cacheOverlays.remove(featureTableCacheOverlay.getCacheName()); if(cacheOverlay != null){ // If the existing cache overlay is being replaced, create a new cache overlay if(featureTableCacheOverlay.getParent().isAdded()){ cacheOverlay = null; } for(GeoPackageTileTableCacheOverlay linkedTileTable: featureTableCacheOverlay.getLinkedTileTables()){ if(cacheOverlay != null){ // Add the existing linked tile cache overlays addGeoPackageTileCacheOverlay(enabledCacheOverlays, linkedTileTable, geoPackage, true); } cacheOverlays.remove(linkedTileTable.getCacheName()); } } if(cacheOverlay == null) { // Add the features to the map FeatureDao featureDao = geoPackage.getFeatureDao(featureTableCacheOverlay.getName()); // If indexed, add as a tile overlay if(featureTableCacheOverlay.isIndexed()){ FeatureTiles featureTiles = new MapFeatureTiles(getActivity(), featureDao); Integer maxFeaturesPerTile = null; if(featureDao.getGeometryType() == GeometryType.POINT){ maxFeaturesPerTile = getResources().getInteger(R.integer.geopackage_feature_tiles_max_points_per_tile); }else{ maxFeaturesPerTile = getResources().getInteger(R.integer.geopackage_feature_tiles_max_features_per_tile); } featureTiles.setMaxFeaturesPerTile(maxFeaturesPerTile); NumberFeaturesTile numberFeaturesTile = new NumberFeaturesTile(getActivity()); // Adjust the max features number tile draw paint attributes here as needed to // change how tiles are drawn when more than the max features exist in a tile featureTiles.setMaxFeaturesTileDraw(numberFeaturesTile); featureTiles.setIndexManager(new FeatureIndexManager(getActivity(), geoPackage, featureDao)); // Adjust the feature tiles draw paint attributes here as needed to change how // features are drawn on tiles FeatureOverlay featureOverlay = new FeatureOverlay(featureTiles); featureOverlay.setMinZoom(featureTableCacheOverlay.getMinZoom()); FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage); List<TileDao> tileDaos = linker.getTileDaosForFeatureTable(featureDao.getTableName()); featureOverlay.ignoreTileDaos(tileDaos); FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(getActivity(), featureOverlay); featureTableCacheOverlay.setFeatureOverlayQuery(featureOverlayQuery); TileOverlayOptions overlayOptions = createFeatureTileOverlayOptions(featureOverlay); TileOverlay tileOverlay = map.addTileOverlay(overlayOptions); featureTableCacheOverlay.setTileOverlay(tileOverlay); } // Not indexed, add the features to the map else { int maxFeaturesPerTable = 0; if(featureDao.getGeometryType() == GeometryType.POINT){ maxFeaturesPerTable = getResources().getInteger(R.integer.geopackage_features_max_points_per_table); }else{ maxFeaturesPerTable = getResources().getInteger(R.integer.geopackage_features_max_features_per_table); } Projection projection = featureDao.getProjection(); GoogleMapShapeConverter shapeConverter = new GoogleMapShapeConverter(projection); FeatureCursor featureCursor = featureDao.queryForAll(); try { final int totalCount = featureCursor.getCount(); int count = 0; while (featureCursor.moveToNext()) { FeatureRow featureRow = featureCursor.getRow(); GeoPackageGeometryData geometryData = featureRow.getGeometry(); if (geometryData != null && !geometryData.isEmpty()) { mil.nga.wkb.geom.Geometry geometry = geometryData.getGeometry(); if (geometry != null) { GoogleMapShape shape = shapeConverter.toShape(geometry); // Set the Shape Marker, PolylineOptions, and PolygonOptions here if needed to change color and style featureTableCacheOverlay.addShapeToMap(featureRow.getId(), shape, map); if(++count >= maxFeaturesPerTable){ if(count < totalCount){ Toast.makeText(getActivity().getApplicationContext(), featureTableCacheOverlay.getCacheName() + "- added " + count + " of " + totalCount, Toast.LENGTH_LONG).show(); } break; } } } } } finally { featureCursor.close(); } } // Add linked tile tables for(GeoPackageTileTableCacheOverlay linkedTileTable: featureTableCacheOverlay.getLinkedTileTables()){ addGeoPackageTileCacheOverlay(enabledCacheOverlays, linkedTileTable, geoPackage, true); } cacheOverlay = featureTableCacheOverlay; } // Add the cache overlay to the enabled cache overlays enabledCacheOverlays.put(cacheOverlay.getCacheName(), cacheOverlay); } /** * Create Feature Tile Overlay Options with the default z index for tile layers drawn from features * @param tileProvider * @return */ private TileOverlayOptions createFeatureTileOverlayOptions(TileProvider tileProvider){ return createTileOverlayOptions(tileProvider, -1); } /** * Create Tile Overlay Options with the default z index for tile layers * @param tileProvider * @return */ private TileOverlayOptions createTileOverlayOptions(TileProvider tileProvider){ return createTileOverlayOptions(tileProvider, -2); } /** * Create Tile Overlay Options for the Tile Provider using the z index * @param tileProvider * @param zIndex * @return */ private TileOverlayOptions createTileOverlayOptions(TileProvider tileProvider, int zIndex){ TileOverlayOptions overlayOptions = new TileOverlayOptions(); overlayOptions.tileProvider(tileProvider); overlayOptions.zIndex(zIndex); return overlayOptions; } private void updateStaticFeatureLayers() { removeStaticFeatureLayers(); try { for (Layer l : LayerHelper.getInstance(getActivity().getApplicationContext()).readByEvent(EventHelper.getInstance(getActivity().getApplicationContext()).getCurrentEvent())) { onStaticFeatureLayer(l); } } catch (LayerException e) { Log.e(LOG_NAME, "Problem updating static features.", e); } } private void removeStaticFeatureLayers() { Set<String> selectedLayerIds = preferences.getStringSet(getResources().getString(R.string.staticFeatureLayersKey), Collections.<String> emptySet()); for (String currentLayerId : staticGeometryCollection.getLayers()) { if (!selectedLayerIds.contains(currentLayerId)) { staticGeometryCollection.removeLayer(currentLayerId); } } } @Override public void onStaticFeaturesCreated(final Layer layer) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { onStaticFeatureLayer(layer); } }); } private void onStaticFeatureLayer(Layer layer) { Set<String> layers = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()).getStringSet(getString(R.string.staticFeatureLayersKey), Collections.<String> emptySet()); // The user has asked for this feature layer String layerId = layer.getId().toString(); if (layers.contains(layerId) && layer.isLoaded()) { new StaticFeatureLoadTask(getActivity().getApplicationContext(), staticGeometryCollection, map).executeOnExecutor(executor, layer); } } private void updateMapView() { // Check the map type map.setMapType(preferences.getInt(getString(R.string.baseLayerKey), getResources().getInteger(R.integer.baseLayerDefaultValue))); // Check the map location and zoom String xyz = preferences.getString(getString(R.string.recentMapXYZKey), getString(R.string.recentMapXYZDefaultValue)); if (xyz != null) { String[] values = StringUtils.split(xyz, ","); LatLng latLng = new LatLng(0.0, 0.0); if(values.length > 1) { try { latLng = new LatLng(Double.valueOf(values[1]), Double.valueOf(values[0])); } catch (NumberFormatException nfe) { Log.e(LOG_NAME, "Could not parse lon,lat: " + String.valueOf(values[1]) + ", " + String.valueOf(values[0])); } } float zoom = 1.0f; if(values.length > 2) { try { zoom = Float.valueOf(values[2]); } catch (NumberFormatException nfe) { Log.e(LOG_NAME, "Could not parse zoom level: " + String.valueOf(values[2])); } } map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoom)); } } private void saveMapView() { CameraPosition position = map.getCameraPosition(); String xyz = new StringBuilder().append(Double.valueOf(position.target.longitude).toString()).append(",").append(Double.valueOf(position.target.latitude).toString()).append(",").append(Float.valueOf(position.zoom).toString()).toString(); preferences.edit().putString(getResources().getString(R.string.recentMapXYZKey), xyz).commit(); } @Override public void onError(Throwable error) { } private int getTimeFilterId() { return preferences.getInt(getResources().getString(R.string.activeTimeFilterKey), getResources().getInteger(R.integer.time_filter_none)); } private Filter<Temporal> getTemporalFilter(String columnName) { Filter<Temporal> filter = null; int filterId = getTimeFilterId(); Calendar c = Calendar.getInstance(); if (filterId == getResources().getInteger(R.integer.time_filter_last_month)) { c.add(Calendar.MONTH, -1); } else if (filterId == getResources().getInteger(R.integer.time_filter_last_week)) { c.add(Calendar.DAY_OF_MONTH, -7); } else if (filterId == getResources().getInteger(R.integer.time_filter_last_24_hours)) { c.add(Calendar.HOUR, -24); } else if (filterId == getResources().getInteger(R.integer.time_filter_today)) { c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); } else { // no filter c = null; } if (c != null) { filter = new DateTimeFilter(c.getTime(), null, columnName); } return filter; } private String getFilterTitle() { List<String> filters = new ArrayList<>(); switch (getTimeFilterId()) { case R.integer.time_filter_last_month: filters.add("Last Month"); break; case R.integer.time_filter_last_week: filters.add("Last Week"); break; case R.integer.time_filter_last_24_hours: filters.add("Last 24 Hours"); break; case R.integer.time_filter_today: filters.add("Since Midnight"); break; default: break; } List<String> actionFilters = new ArrayList<>(); if (preferences.getBoolean(getResources().getString(R.string.activeFavoritesFilterKey), false)) { actionFilters.add("Favorites"); } if (preferences.getBoolean(getResources().getString(R.string.activeImportantFilterKey), false)) { actionFilters.add("Important"); } if (!actionFilters.isEmpty()) { filters.add(StringUtils.join(actionFilters, " & ")); } return StringUtils.join(filters, ", "); } private void hideKeyboard() { InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); if (getActivity().getCurrentFocus() != null) { inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), 0); } } }