Merge pull request #1241 from commons-app/realTimeLocation

Real time location track
This commit is contained in:
neslihanturan 2018-03-15 12:29:47 +02:00 committed by GitHub
commit 41e11b7f20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 486 additions and 273 deletions

View file

@ -86,6 +86,7 @@ public class ContributionController {
return;
}
Timber.d("startGalleryPick() called with pickImageIntent");
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
}
@ -115,6 +116,7 @@ public class ContributionController {
if (isDirectUpload) {
shareIntent.putExtra("isDirectUpload", true);
}
break;
default:
break;

View file

@ -159,3 +159,4 @@ public class LatLng {
return Uri.parse("geo:0,0?q=" + latitude + "," + longitude);
}
}

View file

@ -10,6 +10,7 @@ import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@ -19,7 +20,8 @@ import timber.log.Timber;
public class LocationServiceManager implements LocationListener {
public static final int LOCATION_REQUEST = 1;
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000;
// Maybe these values can be improved for efficiency
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 100;
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
private Context context;
@ -120,12 +122,14 @@ public class LocationServiceManager implements LocationListener {
*
* @param location the location to be tested
* @param currentBestLocation the current best location
* @return true if the given location is better
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
*/
protected boolean isBetterLocation(Location location, Location currentBestLocation) {
protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) {
// A new location is always better than no location
return true;
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
}
// Check whether the new location fix is newer or older
@ -134,15 +138,6 @@ public class LocationServiceManager implements LocationListener {
boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
boolean isNewer = timeDelta > 0;
// If it's been more than two minutes since the current location, use the new location
// because the user has likely moved
if (isSignificantlyNewer) {
return true;
// If the new location is more than two minutes older, it must be worse
} else if (isSignificantlyOlder) {
return false;
}
// Check whether the new location fix is more or less accurate
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
boolean isLessAccurate = accuracyDelta > 0;
@ -153,15 +148,28 @@ public class LocationServiceManager implements LocationListener {
boolean isFromSameProvider = isSameProvider(location.getProvider(),
currentBestLocation.getProvider());
// Determine location quality using a combination of timeliness and accuracy
if (isMoreAccurate) {
return true;
} else if (isNewer && !isLessAccurate) {
return true;
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
return true;
float[] results = new float[5];
Location.distanceBetween(
currentBestLocation.getLatitude(),
currentBestLocation.getLongitude(),
location.getLatitude(),
location.getLongitude(),
results);
// If it's been more than two minutes since the current location, use the new location
// because the user has likely moved
if (isSignificantlyNewer
|| isMoreAccurate
|| (isNewer && !isLessAccurate)
|| (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)) {
if (results[0] < 1000) { // Means change is smaller than 1000 meter
return LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
} else {
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
}
} else{
return LocationChangeType.LOCATION_NOT_CHANGED;
}
return false;
}
/**
@ -208,12 +216,19 @@ public class LocationServiceManager implements LocationListener {
@Override
public void onLocationChanged(Location location) {
if (isBetterLocation(location, lastLocation)) {
lastLocation = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChanged(LatLng.from(lastLocation));
if (isBetterLocation(location, lastLocation)
.equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
lastLocation = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSignificantly(LatLng.from(lastLocation));
}
} else if (isBetterLocation(location, lastLocation)
.equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
lastLocation = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSlightly(LatLng.from(lastLocation));
}
}
}
}
@Override
@ -230,4 +245,10 @@ public class LocationServiceManager implements LocationListener {
public void onProviderDisabled(String provider) {
Timber.d("Provider %s disabled", provider);
}
public enum LocationChangeType{
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
LOCATION_NOT_CHANGED
}
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.location;
public interface LocationUpdateListener {
void onLocationChanged(LatLng latLng);
void onLocationChangedSignificantly(LatLng latLng);
void onLocationChangedSlightly(LatLng latLng);
}

View file

@ -7,9 +7,8 @@ import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuInflater;
@ -28,17 +27,19 @@ import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.UriSerializer;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -68,7 +69,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet
private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet
private NearbyMapFragment nearbyMapFragment;
private static final String TAG_RETAINED_FRAGMENT = "RetainedFragment";
private NearbyListFragment nearbyListFragment;
private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -83,21 +86,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
private void resumeFragment() {
// find the retained fragment on activity restarts
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
nearbyMapFragment = (NearbyMapFragment) fm.findFragmentByTag(TAG_RETAINED_FRAGMENT);
// create the fragment and data the first time
if (nearbyMapFragment == null) {
// add the fragment
nearbyMapFragment = new NearbyMapFragment();
fm.beginTransaction().add(nearbyMapFragment, TAG_RETAINED_FRAGMENT).commit();
// load data from a data source or perform any calculation
}
// Find the retained fragment on activity restarts
nearbyMapFragment = getMapFragment();
nearbyListFragment = getListFragment();
}
private void initBottomSheetBehaviour() {
transparentView.setAlpha(0);
bottomSheet.getLayoutParams().height = getWindowManager()
@ -154,7 +149,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
switch (requestCode) {
case LOCATION_REQUEST: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
refreshView();
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
} else {
//If permission not granted, go to page that says Nearby Places cannot be displayed
hideProgressBar();
@ -209,7 +204,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
refreshView();
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
@ -235,7 +230,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
} else {
refreshView();
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
}
}
@ -244,7 +239,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
Timber.d("User is back from Settings page");
refreshView();
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
}
}
@ -252,6 +247,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
protected void onStart() {
super.onStart();
locationManager.addLocationListener(this);
locationManager.registerLocationManager();
}
@Override
@ -282,10 +278,10 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
// this means that this activity will not be recreated now, user is leaving it
// or the activity is otherwise finishing
if(isFinishing()) {
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
// we will not need this fragment anymore, this may also be a good place to signal
// to the retained fragment object to perform its own cleanup.
fm.beginTransaction().remove(nearbyMapFragment).commit();
removeMapFragment();
removeListFragment();
}
}
@ -294,13 +290,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
/**
* This method should be the single point to load/refresh nearby places
*
* @param locationChangeType defines if location shanged significantly or slightly
*/
private void refreshView() {
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
if (lockNearbyView) {
return;
}
locationManager.registerLocationManager();
LatLng lastLocation = locationManager.getLastLocation();
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
return;
@ -312,20 +310,34 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return;
}
progressBar.setVisibility(View.VISIBLE);
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLang))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces);
if (locationChangeType
.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
progressBar.setVisibility(View.VISIBLE);
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLang))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces);
} else if (locationChangeType
.equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
String gsonCurLatLng = gson.toJson(curLatLang);
bundle.putString("CurLatLng", gsonCurLatLng);
updateMapFragment(true);
}
}
private void populatePlaces(List<Place> placeList) {
private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
List<Place> placeList = nearbyPlacesInfo.placeList;
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
String gsonPlaceList = gson.toJson(placeList);
String gsonCurLatLng = gson.toJson(curLatLang);
String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates);
if (placeList.size() == 0) {
int duration = Toast.LENGTH_SHORT;
@ -336,12 +348,20 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
bundle.clear();
bundle.putString("PlaceList", gsonPlaceList);
bundle.putString("CurLatLng", gsonCurLatLng);
bundle.putString("BoundaryCoord", gsonBoundaryCoordinates);
lockNearbyView(true);
setMapFragment();
setListFragment();
hideProgressBar();
// First time to init fragments
if (nearbyMapFragment == null) {
lockNearbyView(true);
setMapFragment();
setListFragment();
hideProgressBar();
lockNearbyView(false);
} else {
// There are fragments, just update the map and list
updateMapFragment(false);
updateListFragment();
}
}
private void lockNearbyView(boolean lock) {
@ -362,14 +382,92 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
private NearbyMapFragment getMapFragment() {
return (NearbyMapFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT);
}
private void removeMapFragment() {
if (nearbyMapFragment != null) {
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().remove(nearbyMapFragment).commit();
}
}
private NearbyListFragment getListFragment() {
return (NearbyListFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT);
}
private void removeListFragment() {
if (nearbyListFragment != null) {
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().remove(nearbyListFragment).commit();
}
}
private void updateMapFragment(boolean isSlightUpdate) {
/*
* Significant update means updating nearby place markers. Slightly update means only
* updating current location marker and camera target.
* We update our map Significantly on each 1000 meter change, but we can't never know
* the frequency of nearby places. Thus we check if we are close to the boundaries of
* our nearby markers, we update our map Significantly.
* */
NearbyMapFragment nearbyMapFragment = getMapFragment();
if (nearbyMapFragment != null && curLatLang != null) {
hideProgressBar(); // In case it is visible (this happens, not an impossible case)
/*
* If we are close to nearby places boundaries, we need a significant update to
* get new nearby places. Check order is south, north, west, east
* */
if (nearbyMapFragment.boundaryCoordinates != null
&& (curLatLang.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|| curLatLang.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|| curLatLang.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|| curLatLang.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
// populate places
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLang))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces);
nearbyMapFragment.setArguments(bundle);
nearbyMapFragment.updateMapSignificantly();
updateListFragment();
return;
}
if (isSlightUpdate) {
nearbyMapFragment.setArguments(bundle);
nearbyMapFragment.updateMapSlightly();
} else {
nearbyMapFragment.setArguments(bundle);
nearbyMapFragment.updateMapSignificantly();
updateListFragment();
}
} else {
lockNearbyView(true);
setMapFragment();
setListFragment();
hideProgressBar();
lockNearbyView(false);
}
}
private void updateListFragment() {
nearbyListFragment.setArguments(bundle);
nearbyListFragment.updateNearbyListSignificantly();
}
/**
* Calls fragment for map view.
*/
private void setMapFragment() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyMapFragment();
fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
nearbyMapFragment = new NearbyMapFragment();
nearbyMapFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT);
fragmentTransaction.commitAllowingStateLoss();
}
@ -378,16 +476,22 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
*/
private void setListFragment() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyListFragment();
fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container_sheet, fragment);
nearbyListFragment = new NearbyListFragment();
nearbyListFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT);
initBottomSheetBehaviour();
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
fragmentTransaction.commitAllowingStateLoss();
}
@Override
public void onLocationChanged(LatLng latLng) {
refreshView();
public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED);
}
public void prepareViewsForSheetPosition(int bottomSheetState) {

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.nearby;
import android.support.v4.app.Fragment;
import com.pedrogomez.renderers.ListAdapteeCollection;
@ -32,4 +33,9 @@ class NearbyAdapterFactory {
placeList != null ? placeList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection);
}
public void updateAdapterData(List<Place> newPlaceList, RVRendererAdapter<Place> rendererAdapter) {
rendererAdapter.notifyDataSetChanged();
rendererAdapter.diffUpdate(newPlaceList);
}
}

View file

@ -41,19 +41,42 @@ public class NearbyController {
* Prepares Place list to make their distance information update later.
*
* @param curLatLng current location for user
* @return Place list without distance information
* @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List
*/
public List<Place> loadAttractionsFromLocation(LatLng curLatLng) {
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) {
Timber.d("Loading attractions near %s", curLatLng);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
if (curLatLng == null) {
return Collections.emptyList();
return null;
}
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage());
LatLng[] boundaryCoordinates = {places.get(0).location, // south
places.get(0).location, // north
places.get(0).location, // west
places.get(0).location};// east, init with a random location
if (curLatLng != null) {
Timber.d("Sorting places by distance...");
final Map<Place, Double> distances = new HashMap<>();
for (Place place: places) {
distances.put(place, computeDistanceBetween(place.location, curLatLng));
// Find boundaries with basic find max approach
if (place.location.getLatitude() < boundaryCoordinates[0].getLatitude()) {
boundaryCoordinates[0] = place.location;
}
if (place.location.getLatitude() > boundaryCoordinates[1].getLatitude()) {
boundaryCoordinates[1] = place.location;
}
if (place.location.getLongitude() < boundaryCoordinates[2].getLongitude()) {
boundaryCoordinates[2] = place.location;
}
if (place.location.getLongitude() > boundaryCoordinates[3].getLongitude()) {
boundaryCoordinates[3] = place.location;
}
}
Collections.sort(places,
(lhs, rhs) -> {
@ -63,7 +86,9 @@ public class NearbyController {
}
);
}
return places;
nearbyPlacesInfo.placeList = places;
nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates;
return nearbyPlacesInfo;
}
/**
@ -128,4 +153,9 @@ public class NearbyController {
}
return baseMarkerOptions;
}
public class NearbyPlacesInfo {
List<Place> placeList; // List of nearby places
LatLng[] boundaryCoordinates; // Corners of nearby area
}
}

View file

@ -15,6 +15,7 @@ import android.view.ViewGroup;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.lang.reflect.Type;
import java.util.Collections;
@ -64,6 +65,7 @@ public class NearbyListFragment extends DaggerFragment {
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
recyclerView = view.findViewById(R.id.listView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
controller = new ContributionController(this);
adapterFactory = new NearbyAdapterFactory(this, controller);
return view;
@ -73,8 +75,19 @@ public class NearbyListFragment extends DaggerFragment {
public void onViewCreated(View view, Bundle savedInstanceState) {
// Check that this is the first time view is created,
// to avoid double list when screen orientation changed
List<Place> placeList = Collections.emptyList();
Bundle bundle = this.getArguments();
recyclerView.setAdapter(adapterFactory.create(getPlaceListFromBundle(bundle)));
}
public void updateNearbyListSignificantly() {
Bundle bundle = this.getArguments();
adapterFactory.updateAdapterData(getPlaceListFromBundle(bundle),
(RVRendererAdapter<Place>) recyclerView.getAdapter());
}
private List<Place> getPlaceListFromBundle(Bundle bundle) {
List<Place> placeList = Collections.emptyList();
if (bundle != null) {
String gsonPlaceList = bundle.getString("PlaceList", "[]");
placeList = gson.fromJson(gsonPlaceList, LIST_TYPE);
@ -84,9 +97,9 @@ public class NearbyListFragment extends DaggerFragment {
placeList = NearbyController.loadAttractionsFromLocationToPlaces(curLatLng, placeList);
}
recyclerView.setAdapter(adapterFactory.create(placeList));
}
return placeList;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
@ -126,4 +139,5 @@ public class NearbyListFragment extends DaggerFragment {
requestCode, resultCode, data);
}
}
}

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.nearby;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@ -11,6 +15,7 @@ import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@ -29,11 +34,13 @@ import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.annotations.MarkerOptions;
import com.mapbox.mapboxsdk.annotations.PolygonOptions;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.constants.Style;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.services.android.telemetry.MapboxTelemetry;
import java.lang.reflect.Type;
@ -57,6 +64,8 @@ public class NearbyMapFragment extends DaggerFragment {
private MapView mapView;
private List<NearbyBaseMarker> baseMarkerOptions;
private fr.free.nrw.commons.location.LatLng curLatLng;
public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates;
private View bottomSheetList;
private View bottomSheetDetails;
@ -89,6 +98,12 @@ public class NearbyMapFragment extends DaggerFragment {
private Place place;
private Marker selected;
private Marker currentLocationMarker;
private MapboxMap mapboxMap;
private PolygonOptions currentLocationPolygonOptions;
private boolean isBottomListSheetExpanded;
private final double CAMERA_TARGET_SHIFT_FACTOR = 0.06;
@Inject @Named("prefs") SharedPreferences prefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@ -100,22 +115,23 @@ public class NearbyMapFragment extends DaggerFragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = this.getArguments();
initViews();
setListeners();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer())
.create();
if (bundle != null) {
String gsonPlaceList = bundle.getString("PlaceList");
String gsonLatLng = bundle.getString("CurLatLng");
String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord");
Type listType = new TypeToken<List<Place>>() {}.getType();
List<Place> placeList = gson.fromJson(gsonPlaceList, listType);
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
Type gsonBoundaryCoordinatesType = new TypeToken<fr.free.nrw.commons.location.LatLng[]>() {}.getType();
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
baseMarkerOptions = NearbyController
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng,
placeList,
getActivity());
boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType);
}
Mapbox.getInstance(getActivity(),
getString(R.string.mapbox_commons_app_token));
@ -160,12 +176,123 @@ public class NearbyMapFragment extends DaggerFragment {
});
}
public void updateMapSlightly() {
// Get arguments from bundle for new location
Bundle bundle = this.getArguments();
if (mapboxMap != null) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer())
.create();
if (bundle != null) {
String gsonLatLng = bundle.getString("CurLatLng");
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
}
updateMapToTrackPosition();
}
}
public void updateMapSignificantly() {
Bundle bundle = this.getArguments();
if (mapboxMap != null) {
if (bundle != null) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer())
.create();
String gsonPlaceList = bundle.getString("PlaceList");
String gsonLatLng = bundle.getString("CurLatLng");
String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord");
Type listType = new TypeToken<List<Place>>() {}.getType();
List<Place> placeList = gson.fromJson(gsonPlaceList, listType);
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
Type gsonBoundaryCoordinatesType = new TypeToken<fr.free.nrw.commons.location.LatLng[]>() {}.getType();
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
baseMarkerOptions = NearbyController
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng,
placeList,
getActivity());
boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType);
}
mapboxMap.clear();
addCurrentLocationMarker(mapboxMap);
updateMapToTrackPosition();
addNearbyMarkerstoMapBoxMap();
}
}
// Only update current position marker and camera view
private void updateMapToTrackPosition() {
if (currentLocationMarker != null) {
LatLng curMapBoxLatLng = new LatLng(curLatLng.getLatitude(),curLatLng.getLongitude());
ValueAnimator markerAnimator = ObjectAnimator.ofObject(currentLocationMarker, "position",
new LatLngEvaluator(), currentLocationMarker.getPosition(),
curMapBoxLatLng);
markerAnimator.setDuration(1000);
markerAnimator.start();
List<LatLng> circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
curLatLng.getAccuracy() * 2, 100);
if (currentLocationPolygonOptions != null){
mapboxMap.removePolygon(currentLocationPolygonOptions.getPolygon());
currentLocationPolygonOptions = new PolygonOptions()
.addAll(circle)
.strokeColor(Color.parseColor("#55000000"))
.fillColor(Color.parseColor("#11000000"));
mapboxMap.addPolygon(currentLocationPolygonOptions);
}
// Make camera to follow user on location change
CameraPosition position = new CameraPosition.Builder()
.target(isBottomListSheetExpanded ?
new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR,
curMapBoxLatLng.getLongitude())
: curMapBoxLatLng ) // Sets the new camera position
.zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level
.build();
mapboxMap.animateCamera(CameraUpdateFactory
.newCameraPosition(position), 1000);
}
}
private void updateMapCameraAccordingToBottomSheet(boolean isBottomListSheetExpanded) {
CameraPosition position;
this.isBottomListSheetExpanded = isBottomListSheetExpanded;
if (mapboxMap != null && curLatLng != null) {
if (isBottomListSheetExpanded) {
// Make camera to follow user on location change
position = new CameraPosition.Builder()
.target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR,
curLatLng.getLongitude())) // Sets the new camera target above
// current to make it visible when sheet is expanded
.zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level
.build();
} else {
// Make camera to follow user on location change
position = new CameraPosition.Builder()
.target(new LatLng(curLatLng.getLatitude(),
curLatLng.getLongitude())) // Sets the new camera target to curLatLng
.zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level
.build();
}
mapboxMap.animateCamera(CameraUpdateFactory
.newCameraPosition(position), 1000);
}
}
private void initViews() {
bottomSheetList = getActivity().findViewById(R.id.bottom_sheet);
bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList);
bottomSheetDetails = getActivity().findViewById(R.id.bottom_sheet_details);
bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetails.setVisibility(View.VISIBLE);
fabPlus = getActivity().findViewById(R.id.fab_plus);
fabCamera = getActivity().findViewById(R.id.fab_camera);
@ -233,6 +360,9 @@ public class NearbyMapFragment extends DaggerFragment {
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED){
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
updateMapCameraAccordingToBottomSheet(true);
} else {
updateMapCameraAccordingToBottomSheet(false);
}
}
@ -267,30 +397,13 @@ public class NearbyMapFragment extends DaggerFragment {
// create map
mapView = new MapView(getActivity(), options);
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(mapboxMap -> {
mapboxMap.addMarkers(baseMarkerOptions);
mapboxMap.setOnInfoWindowCloseListener(marker -> {
if (marker == selected){
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
});
mapboxMap.setOnMarkerClickListener(marker -> {
if (marker instanceof NearbyMarker) {
this.selected = marker;
NearbyMarker nearbyMarker = (NearbyMarker) marker;
Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
passInfoToSheet(place);
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
return false;
});
addCurrentLocationMarker(mapboxMap);
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(MapboxMap mapboxMap) {
NearbyMapFragment.this.mapboxMap = mapboxMap;
updateMapSignificantly();
}
});
mapView.setStyleUrl("asset://mapstyle.json");
}
@ -299,23 +412,55 @@ public class NearbyMapFragment extends DaggerFragment {
* circle which uses the accuracy * 2, to draw a circle
* which represents the user's position with an accuracy
* of 95%.
*
* Should be called only on creation of mapboxMap, there
* is other method to update markers location with users
* move.
*/
private void addCurrentLocationMarker(MapboxMap mapboxMap) {
MarkerOptions currentLocationMarker = new MarkerOptions()
if (currentLocationMarker != null) {
currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel
}
MarkerOptions currentLocationMarkerOptions = new MarkerOptions()
.position(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()));
mapboxMap.addMarker(currentLocationMarker);
currentLocationMarker = mapboxMap.addMarker(currentLocationMarkerOptions);
List<LatLng> circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
curLatLng.getAccuracy() * 2, 100);
mapboxMap.addPolygon(
new PolygonOptions()
.addAll(circle)
.strokeColor(Color.parseColor("#55000000"))
.fillColor(Color.parseColor("#11000000"))
);
currentLocationPolygonOptions = new PolygonOptions()
.addAll(circle)
.strokeColor(Color.parseColor("#55000000"))
.fillColor(Color.parseColor("#11000000"));
mapboxMap.addPolygon(currentLocationPolygonOptions);
}
private void addNearbyMarkerstoMapBoxMap() {
mapboxMap.addMarkers(baseMarkerOptions);
mapboxMap.setOnInfoWindowCloseListener(marker -> {
if (marker == selected){
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
});
mapboxMap.setOnMarkerClickListener(marker -> {
if (marker instanceof NearbyMarker) {
this.selected = marker;
NearbyMarker nearbyMarker = (NearbyMarker) marker;
Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
passInfoToSheet(place);
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
return false;
});
}
/**
* Creates a series of points that create a circle on the map.
* Takes the center latitude, center longitude of the circle,
@ -401,6 +546,7 @@ public class NearbyMapFragment extends DaggerFragment {
commonsButton.setOnClickListener(view -> openWebView(place.siteLinks.getCommonsLink()));
icon.setImageResource(place.getLabel().getIcon());
title.setText(place.name);
distance.setText(place.distance);
description.setText(place.getLongDescription());
@ -410,6 +556,7 @@ public class NearbyMapFragment extends DaggerFragment {
fabCamera.setOnClickListener(view -> {
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
controller = new ContributionController(this);
DirectUpload directUpload = new DirectUpload(this, controller);
storeSharedPrefs();
directUpload.initiateCameraUpload();
@ -418,12 +565,14 @@ public class NearbyMapFragment extends DaggerFragment {
fabGallery.setOnClickListener(view -> {
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
controller = new ContributionController(this);
DirectUpload directUpload = new DirectUpload(this, controller);
storeSharedPrefs();
directUpload.initiateGalleryUpload();
//TODO: App crashes after image upload completes
//TODO: Handle onRequestPermissionsResult
//TODO: App crashes after image upload completes
//TODO: Handle onRequestPermissionsResult
});
}
@ -523,10 +672,14 @@ public class NearbyMapFragment extends DaggerFragment {
@Override
public void onResume() {
super.onResume();
if (mapView != null) {
mapView.onResume();
}
super.onResume();
initViews();
setListeners();
transparentView.setClickable(false);
transparentView.setAlpha(0);
}
@Override
@ -544,4 +697,19 @@ public class NearbyMapFragment extends DaggerFragment {
}
super.onDestroyView();
}
private static class LatLngEvaluator implements TypeEvaluator<LatLng> {
// Method is used to interpolate the marker animation.
private LatLng latLng = new LatLng();
@Override
public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
latLng.setLatitude(startValue.getLatitude()
+ ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
latLng.setLongitude(startValue.getLongitude()
+ ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
return latLng;
}
}
}

View file

@ -34,12 +34,12 @@ public class Place {
this.siteLinks = siteLinks;
}
public String getName() { return name; }
public Label getLabel() {
return label;
}
public String getName() { return name; }
public String getLongDescription() { return longDescription; }
public void setDistance(String distance) {

View file

@ -1,10 +1,11 @@
package fr.free.nrw.commons.nearby;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.support.transition.TransitionManager;
import android.content.SharedPreferences;
import android.support.v4.app.Fragment;
import android.support.transition.TransitionManager;
import android.support.v7.widget.PopupMenu;
import android.util.Log;
import android.view.LayoutInflater;
@ -37,17 +38,20 @@ public class PlaceRenderer extends Renderer<Place> {
@BindView(R.id.icon) ImageView icon;
@BindView(R.id.buttonLayout) LinearLayout buttonLayout;
@BindView(R.id.cameraButton) LinearLayout cameraButton;
@BindView(R.id.galleryButton) LinearLayout galleryButton;
@BindView(R.id.directionsButton) LinearLayout directionsButton;
@BindView(R.id.iconOverflow) LinearLayout iconOverflow;
@BindView(R.id.cameraButtonText) TextView cameraButtonText;
@BindView(R.id.galleryButtonText) TextView galleryButtonText;
@BindView(R.id.directionsButtonText) TextView directionsButtonText;
@BindView(R.id.iconOverflowText) TextView iconOverflowText;
private View view;
private static ArrayList<LinearLayout> openedItems;
private Place place;
private Fragment fragment;
private ContributionController controller;
@ -83,9 +87,9 @@ public class PlaceRenderer extends Renderer<Place> {
Log.d("Renderer", "clicked");
TransitionManager.beginDelayedTransition(buttonLayout);
if(buttonLayout.isShown()) {
if(buttonLayout.isShown()){
closeLayout(buttonLayout);
} else {
}else {
openLayout(buttonLayout);
}
@ -121,6 +125,7 @@ public class PlaceRenderer extends Renderer<Place> {
editor.putString("Title", place.getName());
editor.putString("Desc", place.getLongDescription());
editor.apply();
}
private void closeLayout(LinearLayout buttonLayout){
@ -133,10 +138,11 @@ public class PlaceRenderer extends Renderer<Place> {
@Override
public void render() {
// ((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
place = getContent();
tvName.setText(place.name);
String descriptionText = place.getLongDescription();
if (descriptionText.equals("?")) {
descriptionText = getContext().getString(R.string.no_description_found);
}
@ -154,6 +160,7 @@ public class PlaceRenderer extends Renderer<Place> {
iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE);
iconOverflow.setOnClickListener(v -> popupMenuListener());
}
private void popupMenuListener() {

View file

@ -54,6 +54,7 @@ import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
@ -111,8 +112,10 @@ public class ShareActivity
private String description;
private Snackbar snackbar;
private boolean duplicateCheckPassed = false;
private boolean haveCheckedForOtherImages = false;
private boolean isNearbyUpload = false;
/**
* Called when user taps the submit button.
*/

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
</selector>

View file

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_below="@id/toolbar">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"></FrameLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/transparentView"
android:layout_below="@id/toolbar"
android:background="#aa969696"
android:elevation="6dp"
/>
</RelativeLayout>
<include layout="@layout/bottom_sheet_nearby" />
<include layout="@layout/bottom_sheet_details" android:id="@+id/bottom_sheet_details" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="normal"
android:layout_margin="16dp"
android:visibility="invisible"
android:clickable="true"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:backgroundTint="@color/button_blue"
app:layout_anchor="@id/bottom_sheet_details"
app:layout_anchorGravity="top|right|end"
app:srcCompat="@drawable/ic_add_white_24dp"/>
<View
android:id = "@+id/empty_view2"
android:layout_height = "306dip"
android:layout_width = "56dp"
android:visibility="invisible"
app:layout_anchor="@id/fab_plus"
app:layout_anchorGravity="center_horizontal"
/>
<View
android:id = "@+id/empty_view1"
android:layout_height = "186dip"
android:layout_width = "56dp"
android:visibility="invisible"
app:layout_anchor="@id/fab_plus"
app:layout_anchorGravity="center_horizontal"
/>
<View
android:id = "@+id/empty_view"
android:layout_height = "66dip"
android:layout_width = "56dp"
android:visibility="invisible"
app:layout_anchor="@id/fab_plus"
app:layout_anchorGravity="center_horizontal"
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="mini"
android:visibility="invisible"
app:backgroundTint="@color/main_background_light"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:layout_anchor="@id/empty_view1"
app:layout_anchorGravity="center_horizontal"
app:srcCompat="@drawable/ic_photo_camera_white_24dp"
android:tint="@color/button_blue"
android:scaleType="center"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_galery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="mini"
android:visibility="invisible"
app:backgroundTint="@color/main_background_light"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:layout_anchor="@id/empty_view"
app:layout_anchorGravity="center_horizontal"
app:srcCompat="@drawable/ic_photo_white_24dp"
android:tint="@color/button_blue"
android:scaleType="center"
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_commons_page"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="mini"
android:visibility="invisible"
app:backgroundTint="@color/main_background_light"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:layout_anchor="@id/empty_view2"
app:layout_anchorGravity="center_horizontal"
app:srcCompat="@drawable/ic_commons_icon_vector"
android:scaleType="center"
/>
</android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.NavigationView
android:id="@+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/drawer_header"
app:menu="@menu/drawer"/>
</android.support.v4.widget.DrawerLayout>

View file

@ -62,6 +62,7 @@
android:background="#aa969696"
android:elevation="6dp"
/>
</RelativeLayout>
<include layout="@layout/bottom_sheet_nearby" />
<include layout="@layout/bottom_sheet_details" android:id="@+id/bottom_sheet_details" />

View file

@ -8,6 +8,8 @@
app:layout_behavior="@string/bottom_sheet_behavior"
app:behavior_peekHeight="72dp"
app:behavior_hideable="true"
android:visibility="gone"
>
<LinearLayout

View file

@ -33,12 +33,8 @@ public class NearbyAdapterFactoryTest {
private static final Place PLACE = new Place("name", Place.Label.AIRPORT,
"desc", null, new LatLng(38.6270, -90.1994, 0), null);
private static final Place UNKNOWN_PLACE = new Place("name", Place.Label.UNKNOWN,
<<<<<<< 27ac8ae0d72011bc651affecf7b1da2100ec927e
"?", null, new LatLng(39.7392, -104.9903, 0), null);
// ^ "?" is a special value for unknown class names from Wikidata query results
=======
"desc", null, new LatLng(39.7392, -104.9903, 0), null);
>>>>>>> Rename Description to Label to prevent misunderstandings
private Place clickedPlace;
@Test