diff --git a/app/build.gradle b/app/build.gradle
index 271ba45c..bf1b748f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,10 +16,9 @@ repositories {
//}
ext {
- androidMapsUtilsVersion = '0.3.4'
assertjVersion = '3.9.0'
butterknifeVersion = '8.8.1'
- constraintLayoutVersion = '1.1.0'
+ constraintLayoutVersion = '1.1.2'
daggerVersion = '2.13'
gsonVersion = '2.8.1'
guavaVersion = '20.0'
@@ -27,11 +26,13 @@ ext {
junitVersion = '4.12'
materialDialogsVersion = '0.9.6.0' // Depends on the android support lib. Their version must match ours.
multidexVersion = '1.0.3'
+ osmbonuspackVersion = '6.5.2'
+ osmdroidVersion = '6.0.2'
picassoVersion = '2.5.2'
playServicesVersion = '11.6.2'
retrofitVersion = '2.3.0'
robolectricVersion = '3.7'
- rxAndroidVersion = '2.0.1'
+ rxAndroidVersion = '2.0.2'
rxJavaVersion = '2.1.7'
supportLibVersion = '27.0.1'
}
@@ -49,15 +50,13 @@ dependencies {
implementation "com.android.support:multidex:$multidexVersion"
implementation "com.android.support:preference-v14:$supportLibVersion"
implementation "com.android.support:support-v4:$supportLibVersion"
+ implementation "com.github.MKergall:osmbonuspack:$osmbonuspackVersion"
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
- implementation "com.google.android.gms:play-services-location:$playServicesVersion"
- implementation "com.google.android.gms:play-services-maps:$playServicesVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "com.google.dagger:dagger:$daggerVersion"
implementation "com.google.dagger:dagger-android:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
implementation "com.google.guava:guava:$guavaVersion"
- implementation "com.google.maps.android:android-maps-utils:$androidMapsUtilsVersion"
implementation "com.jakewharton:butterknife:$butterknifeVersion"
implementation "com.squareup.picasso:picasso:$picassoVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
@@ -65,13 +64,14 @@ dependencies {
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
+ implementation "org.osmdroid:osmdroid-android:$osmdroidVersion"
+ testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:$assertjVersion"
testImplementation "org.json:json:$jsonVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation "org.robolectric:shadows-multidex:$robolectricVersion"
testImplementation "org.robolectric:shadows-supportv4:$robolectricVersion"
- testImplementation "junit:junit:$junitVersion"
}
android {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 01eb6fff..af906f65 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -17,6 +17,12 @@
+
+
@@ -28,8 +34,6 @@
android:label="@string/app_name"
android:theme="@style/wsAppTheme">
-
-
-
diff --git a/app/src/main/java/fi/bitrite/android/ws/api/model/ApiUser.java b/app/src/main/java/fi/bitrite/android/ws/api/model/ApiUser.java
index 97e7d2b5..2af882f3 100644
--- a/app/src/main/java/fi/bitrite/android/ws/api/model/ApiUser.java
+++ b/app/src/main/java/fi/bitrite/android/ws/api/model/ApiUser.java
@@ -1,8 +1,9 @@
package fi.bitrite.android.ws.api.model;
-import com.google.android.gms.maps.model.LatLng;
import com.google.gson.annotations.SerializedName;
+import org.osmdroid.util.GeoPoint;
+
import java.util.Date;
import fi.bitrite.android.ws.model.SimpleUser;
@@ -85,7 +86,7 @@ public static class CommentNotifySettings {
public User toUser() {
return new User(id, name, fullname, street, additionalAddress, city, province, postalCode,
- countryCode, new LatLng(latitude, longitude), mobilePhone, homePhone, workPhone,
+ countryCode, new GeoPoint(latitude, longitude), mobilePhone, homePhone, workPhone,
comments, preferredNotice, maximalCyclistCount, distanceToMotel,
distanceToCampground, distanceToBikeshop, hasStorage, hasShower, hasKitchen,
hasLawnspace, hasSag, hasBed, hasLaundry, hasFood, spokenLanguages,
diff --git a/app/src/main/java/fi/bitrite/android/ws/api/response/UserSearchByLocationResponse.java b/app/src/main/java/fi/bitrite/android/ws/api/response/UserSearchByLocationResponse.java
index 0cd7da66..df1bf896 100644
--- a/app/src/main/java/fi/bitrite/android/ws/api/response/UserSearchByLocationResponse.java
+++ b/app/src/main/java/fi/bitrite/android/ws/api/response/UserSearchByLocationResponse.java
@@ -1,8 +1,9 @@
package fi.bitrite.android.ws.api.response;
-import com.google.android.gms.maps.model.LatLng;
import com.google.gson.annotations.SerializedName;
+import org.osmdroid.util.GeoPoint;
+
import java.util.Date;
import java.util.List;
@@ -59,7 +60,7 @@ public static class User {
public SimpleUser toSimpleUser() {
return new SimpleUser(id, name, fullname, street, city, province, postalCode,
- countryCode, new LatLng(latitude, longitude), !notCurrentlyAvailable,
+ countryCode, new GeoPoint(latitude, longitude), !notCurrentlyAvailable,
new SimpleUser.Picture(profilePictureUrl_179x200, profilePictureUrl_400x400),
created, lastAccess);
}
diff --git a/app/src/main/java/fi/bitrite/android/ws/model/SimpleUser.java b/app/src/main/java/fi/bitrite/android/ws/model/SimpleUser.java
index 30ff9dee..68e8ba2d 100644
--- a/app/src/main/java/fi/bitrite/android/ws/model/SimpleUser.java
+++ b/app/src/main/java/fi/bitrite/android/ws/model/SimpleUser.java
@@ -2,7 +2,7 @@
import android.text.TextUtils;
-import com.google.android.gms.maps.model.LatLng;
+import org.osmdroid.api.IGeoPoint;
import java.util.Date;
@@ -34,7 +34,7 @@ public String getLargeUrl() {
public final String province;
public final String postalCode;
public final String countryCode;
- public final LatLng location;
+ public final IGeoPoint location;
public final boolean isCurrentlyAvailable;
public final Picture profilePicture;
@@ -43,7 +43,7 @@ public String getLargeUrl() {
public final Date lastAccess;
public SimpleUser(int id, String name, String fullname, String street, String city,
- String province, String postalCode, String countryCode, LatLng location,
+ String province, String postalCode, String countryCode, IGeoPoint location,
boolean isCurrentlyAvailable, Picture profilePicture, Date created,
Date lastAccess) {
this.id = id;
diff --git a/app/src/main/java/fi/bitrite/android/ws/model/User.java b/app/src/main/java/fi/bitrite/android/ws/model/User.java
index 75a9afb7..1bf76719 100644
--- a/app/src/main/java/fi/bitrite/android/ws/model/User.java
+++ b/app/src/main/java/fi/bitrite/android/ws/model/User.java
@@ -4,7 +4,7 @@
import android.content.res.Resources;
import android.text.TextUtils;
-import com.google.android.gms.maps.model.LatLng;
+import org.osmdroid.api.IGeoPoint;
import java.util.Date;
@@ -37,7 +37,7 @@ public class User extends SimpleUser {
public User(int id, String name, String fullname, String street, String additionalAddress,
String city, String province, String postalCode, String countryCode,
- LatLng location, String mobilePhone, String homePhone, String workPhone,
+ IGeoPoint location, String mobilePhone, String homePhone, String workPhone,
String comments, String preferredNotice, int maximalCyclistCount,
String distanceToMotel, String distanceToCampground, String distanceToBikeshop,
boolean hasStorage, boolean hasShower, boolean hasKitchen, boolean hasLawnspace,
diff --git a/app/src/main/java/fi/bitrite/android/ws/model/ZoomedLocation.java b/app/src/main/java/fi/bitrite/android/ws/model/ZoomedLocation.java
new file mode 100644
index 00000000..c6ef480e
--- /dev/null
+++ b/app/src/main/java/fi/bitrite/android/ws/model/ZoomedLocation.java
@@ -0,0 +1,17 @@
+package fi.bitrite.android.ws.model;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.util.GeoPoint;
+
+public class ZoomedLocation {
+ public final IGeoPoint location;
+ public final double zoom;
+
+ public ZoomedLocation(double lat, double lon, double zoom) {
+ this(new GeoPoint(lat, lon), zoom);
+ }
+ public ZoomedLocation(IGeoPoint location, double zoom) {
+ this.location = location;
+ this.zoom = zoom;
+ }
+}
diff --git a/app/src/main/java/fi/bitrite/android/ws/persistence/UserDao.java b/app/src/main/java/fi/bitrite/android/ws/persistence/UserDao.java
index f3273544..cb7c471d 100644
--- a/app/src/main/java/fi/bitrite/android/ws/persistence/UserDao.java
+++ b/app/src/main/java/fi/bitrite/android/ws/persistence/UserDao.java
@@ -5,7 +5,7 @@
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
-import com.google.android.gms.maps.model.LatLng;
+import org.osmdroid.util.GeoPoint;
import java.util.ArrayList;
import java.util.List;
@@ -144,8 +144,8 @@ public void save(SQLiteDatabase db, User user) {
cv.put("created", DateConverter.dateToLong(user.created));
cv.put("currently_available", user.isCurrentlyAvailable);
cv.put("spoken_languages", user.spokenLanguages);
- cv.put("latitude", user.location.latitude);
- cv.put("longitude", user.location.longitude);
+ cv.put("latitude", user.location.getLatitude());
+ cv.put("longitude", user.location.getLongitude());
cv.put("profile_picture_small", user.profilePicture.getSmallUrl());
cv.put("profile_picture_large", user.profilePicture.getLargeUrl());
@@ -170,7 +170,7 @@ private static User getUserFromCursor(@NonNull Cursor c) {
c.getString(COL_IDX_PROVINCE),
c.getString(COL_IDX_POSTAL_CODE),
c.getString(COL_IDX_COUNTRY_CODE),
- new LatLng(c.getDouble(COL_IDX_LATITUDE), c.getDouble(COL_IDX_LONGITUDE)),
+ new GeoPoint(c.getDouble(COL_IDX_LATITUDE), c.getDouble(COL_IDX_LONGITUDE)),
c.getString(COL_IDX_MOBILE_PHONE),
c.getString(COL_IDX_HOME_PHONE),
c.getString(COL_IDX_WORK_PHONE),
diff --git a/app/src/main/java/fi/bitrite/android/ws/repository/SettingsRepository.java b/app/src/main/java/fi/bitrite/android/ws/repository/SettingsRepository.java
index 1e999232..b50ce985 100644
--- a/app/src/main/java/fi/bitrite/android/ws/repository/SettingsRepository.java
+++ b/app/src/main/java/fi/bitrite/android/ws/repository/SettingsRepository.java
@@ -6,13 +6,13 @@
import android.preference.PreferenceManager;
import android.text.TextUtils;
-import com.google.android.gms.maps.model.CameraPosition;
-import com.google.android.gms.maps.model.LatLng;
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import javax.inject.Inject;
import fi.bitrite.android.ws.R;
import fi.bitrite.android.ws.di.AppScope;
+import fi.bitrite.android.ws.model.ZoomedLocation;
@AppScope
public class SettingsRepository {
@@ -29,11 +29,13 @@ public enum DistanceUnit {
private final static String KEYSUFFIX_LOCATION_ZOOM = "_zoom";
private final String mKeyDistanceUnit;
+ private final String mKeyTileSource;
private final String mKeyMessageRefreshInterval;
private final String mKeyGaCollectStats;
private final String mKeyDevSimulateNoNetwork;
private final String mDefaultDistanceUnit;
+ private final String mDefaultTileSource = TileSourceFactory.DEFAULT_TILE_SOURCE.name();
private final int mDefaultMessageRefreshInterval;
private final boolean mDefaultGaCollectStats;
private final boolean mDefaultDevSimulateNoNetwork;
@@ -55,6 +57,7 @@ public enum DistanceUnit {
final Resources res = context.getResources();
mKeyDistanceUnit = res.getString(R.string.prefs_distance_unit_key);
+ mKeyTileSource = res.getString(R.string.prefs_tile_source_key);
mKeyMessageRefreshInterval = res.getString(R.string.prefs_message_refresh_interval_min_key);
mKeyGaCollectStats = res.getString(R.string.prefs_ga_collect_stats_key);
mKeyDevSimulateNoNetwork = res.getString(R.string.prefs_dev_simulate_no_network_key);
@@ -113,17 +116,21 @@ public String getDistanceUnitShort() {
}
}
- private void setLocation(String key, CameraPosition position) {
+ public String getTileSourceStr() {
+ return mSharedPreferences.getString(mKeyTileSource, mDefaultTileSource);
+ }
+
+ private void setLocation(String key, ZoomedLocation position) {
mSharedPreferences
.edit()
- .putFloat(key + KEYSUFFIX_LOCATION_LATITUDE, (float) position.target.latitude)
- .putFloat(key + KEYSUFFIX_LOCATION_LONGITUDE, (float) position.target.longitude)
- .putFloat(key + KEYSUFFIX_LOCATION_ZOOM, position.zoom)
+ .putFloat(key + KEYSUFFIX_LOCATION_LATITUDE, (float) position.location.getLatitude())
+ .putFloat(key + KEYSUFFIX_LOCATION_LONGITUDE, (float) position.location.getLongitude())
+ .putFloat(key + KEYSUFFIX_LOCATION_ZOOM, (float) position.zoom)
.apply();
}
- public CameraPosition getLastMapLocation(boolean defaultIfNone) {
+ public ZoomedLocation getLastMapLocation(boolean defaultIfNone) {
if (!mSharedPreferences.contains(KEY_MAP_LAST_LOCATION + KEYSUFFIX_LOCATION_LATITUDE)
&& !defaultIfNone) {
return null;
@@ -135,9 +142,9 @@ public CameraPosition getLastMapLocation(boolean defaultIfNone) {
float zoom = mSharedPreferences.getFloat(
KEY_MAP_LAST_LOCATION + KEYSUFFIX_LOCATION_ZOOM, mDefaultMapLocationZoom);
- return new CameraPosition(new LatLng(latitude, longitude), zoom, 0, 0);
+ return new ZoomedLocation(latitude, longitude, zoom);
}
- public void setLastMapLocation(CameraPosition position) {
+ public void setLastMapLocation(ZoomedLocation position) {
setLocation(KEY_MAP_LAST_LOCATION, position);
}
diff --git a/app/src/main/java/fi/bitrite/android/ws/repository/UserRepository.java b/app/src/main/java/fi/bitrite/android/ws/repository/UserRepository.java
index 76a02484..7a125cd1 100644
--- a/app/src/main/java/fi/bitrite/android/ws/repository/UserRepository.java
+++ b/app/src/main/java/fi/bitrite/android/ws/repository/UserRepository.java
@@ -2,7 +2,7 @@
import android.support.annotation.NonNull;
-import com.google.android.gms.maps.model.LatLng;
+import org.osmdroid.util.BoundingBox;
import java.util.ArrayList;
import java.util.Collection;
@@ -60,9 +60,9 @@ public Observable> searchByKeyword(String keyword) {
return getAppUserRepository().searchByKeyword(keyword);
}
- public Observable> searchByLocation(LatLng northEast,
- LatLng southWest) {
- return getAppUserRepository().searchByLocation(northEast, southWest);
+ public Observable> searchByLocation(
+ BoundingBox boundingBox) {
+ return getAppUserRepository().searchByLocation(boundingBox);
}
/**
@@ -151,13 +151,13 @@ Observable> searchByKeyword(String keyword) {
});
}
- Observable> searchByLocation(LatLng northEast,
- LatLng southWest) {
+ Observable> searchByLocation(
+ BoundingBox boundingBox) {
- final double minLat = southWest.latitude;
- final double minLon = southWest.longitude;
- final double maxLat = northEast.latitude;
- final double maxLon = northEast.longitude;
+ final double minLat = boundingBox.getLatSouth();
+ final double minLon = boundingBox.getLonWest();
+ final double maxLat = boundingBox.getLatNorth();
+ final double maxLon = boundingBox.getLonEast();
final double centerLat = (minLat + maxLat) / 2.0f;
final double centerLon = (minLon + maxLon) / 2.0f;
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/BaseFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/BaseFragment.java
index 0a593bd9..3667fec8 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/BaseFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/BaseFragment.java
@@ -93,4 +93,4 @@ CompositeDisposable getStartStopDisposable() {
CompositeDisposable getResumePauseDisposable() {
return mResumePauseDisposable.get();
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/FavoriteUsersFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/FavoriteUsersFragment.java
index 65617a2f..c5b63867 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/FavoriteUsersFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/FavoriteUsersFragment.java
@@ -22,6 +22,7 @@
import butterknife.ButterKnife;
import butterknife.OnItemClick;
import fi.bitrite.android.ws.R;
+import fi.bitrite.android.ws.model.SimpleUser;
import fi.bitrite.android.ws.model.User;
import fi.bitrite.android.ws.repository.FavoriteRepository;
import fi.bitrite.android.ws.repository.Resource;
@@ -74,7 +75,7 @@ public void onResume() {
@OnItemClick(R.id.favorites_lst_users)
public void onUserClicked(int position) {
- User selectedUser = mUserListAdapter.getUser(position);
+ SimpleUser selectedUser = mUserListAdapter.getUser(position);
getNavigationController().navigateToUser(selectedUser.id);
}
@@ -82,7 +83,7 @@ public void onUserClicked(int position) {
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
if (view.getId() == mLstUsers.getId()) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
- User user = mUserListAdapter.getUser(info.position);
+ SimpleUser user = mUserListAdapter.getUser(info.position);
menu.setHeaderTitle(user.fullname);
menu.add(Menu.NONE, CONTEXT_MENU_DELETE, 0, R.string.delete);
@@ -96,7 +97,7 @@ public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case CONTEXT_MENU_DELETE:
- User user = mUserListAdapter.getUser(info.position);
+ SimpleUser user = mUserListAdapter.getUser(info.position);
mFavoriteRepository.remove(user.id);
updateFavoriteUsersList();
return true;
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java
index ee0ce6d2..e6cbe78c 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java
@@ -1,24 +1,22 @@
package fi.bitrite.android.ws.ui;
import android.Manifest;
-import android.annotation.SuppressLint;
-import android.app.Dialog;
+import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
import android.location.Location;
import android.os.Bundle;
+import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
+import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.ActivityCompat;
-import android.support.v4.app.DialogFragment;
-import android.support.v7.app.AlertDialog;
import android.support.v7.widget.SearchView;
-import android.text.Html;
-import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
@@ -26,69 +24,63 @@
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
-import com.google.android.gms.common.GooglePlayServicesUtil;
-import com.google.android.gms.location.FusedLocationProviderClient;
-import com.google.android.gms.location.LocationCallback;
-import com.google.android.gms.location.LocationRequest;
-import com.google.android.gms.location.LocationResult;
-import com.google.android.gms.location.LocationServices;
-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.SupportMapFragment;
-import com.google.android.gms.maps.model.BitmapDescriptor;
-import com.google.android.gms.maps.model.BitmapDescriptorFactory;
-import com.google.android.gms.maps.model.CameraPosition;
-import com.google.android.gms.maps.model.LatLng;
-import com.google.android.gms.maps.model.LatLngBounds;
-import com.google.android.gms.maps.model.Marker;
-import com.google.android.gms.maps.model.MarkerOptions;
-import com.google.maps.android.clustering.Cluster;
-import com.google.maps.android.clustering.ClusterManager;
-import com.google.maps.android.clustering.algo.PreCachingAlgorithmDecorator;
-import com.google.maps.android.clustering.view.DefaultClusterRenderer;
-import com.google.maps.android.ui.IconGenerator;
+import com.google.common.collect.Lists;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.api.IMapController;
+import org.osmdroid.bonuspack.clustering.StaticCluster;
+import org.osmdroid.config.Configuration;
+import org.osmdroid.events.MapListener;
+import org.osmdroid.events.ScrollEvent;
+import org.osmdroid.events.ZoomEvent;
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
+import org.osmdroid.util.BoundingBox;
+import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.MapView;
+import org.osmdroid.views.overlay.Marker;
+import org.osmdroid.views.overlay.mylocation.IMyLocationConsumer;
+import org.osmdroid.views.overlay.mylocation.IMyLocationProvider;
+import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
-import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
+import butterknife.BindColor;
+import butterknife.BindDrawable;
import butterknife.BindView;
import butterknife.ButterKnife;
+import butterknife.OnClick;
import butterknife.Unbinder;
import fi.bitrite.android.ws.R;
import fi.bitrite.android.ws.api.response.UserSearchByLocationResponse;
+import fi.bitrite.android.ws.model.SimpleUser;
import fi.bitrite.android.ws.model.User;
+import fi.bitrite.android.ws.model.ZoomedLocation;
import fi.bitrite.android.ws.repository.FavoriteRepository;
+import fi.bitrite.android.ws.repository.Resource;
import fi.bitrite.android.ws.repository.SettingsRepository;
import fi.bitrite.android.ws.repository.UserRepository;
-import fi.bitrite.android.ws.ui.model.ClusterUser;
+import fi.bitrite.android.ws.ui.listadapter.UserListAdapter;
+import fi.bitrite.android.ws.ui.util.NavigationController;
+import fi.bitrite.android.ws.ui.util.UserMarker;
+import fi.bitrite.android.ws.ui.util.UserMarkerClusterer;
+import fi.bitrite.android.ws.util.LocationManager;
import fi.bitrite.android.ws.util.LoggedInUserHelper;
import fi.bitrite.android.ws.util.Tools;
-import fi.bitrite.android.ws.util.WSNonHierarchicalDistanceBasedAlgorithm;
+import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
-import io.reactivex.subjects.CompletableSubject;
-
-public class MapFragment extends BaseFragment implements
- ClusterManager.OnClusterClickListener,
- ClusterManager.OnClusterInfoWindowClickListener,
- ClusterManager.OnClusterItemClickListener,
- ClusterManager.OnClusterItemInfoWindowClickListener,
- GoogleMap.OnCameraChangeListener {
+import io.reactivex.subjects.BehaviorSubject;
+public class MapFragment extends BaseFragment {
private static final String KEY_MAP_TARGET_LAT_LNG = "map_target_lat_lng";
- private static final int REQUEST_RESOLVE_ERROR = 1001;
- private static final String DIALOG_ERROR = "dialog_error";
private static final String TAG = "MapFragment";
@Inject LoggedInUserHelper mLoggedInUserHelper;
@@ -96,16 +88,23 @@ public class MapFragment extends BaseFragment implements
@Inject FavoriteRepository mFavoriteRepository;
@Inject SettingsRepository mSettingsRepository;
- private Unbinder mUnbinder;
+ @BindColor(R.color.primaryColor) int mColorPrimary;
+ @BindColor(R.color.primaryWhite) int mColorPrimaryWhite;
+ @BindColor(R.color.primaryButtonDisable) int mColorPrimaryButtonDisable;
+ @BindDrawable(R.drawable.ic_my_location_white_24dp) Drawable mIcMyLocationWhite;
+ @BindDrawable(R.drawable.ic_my_location_grey600_24dp) Drawable mIcMyLocationGrey;
+ @BindView(R.id.map) MapView mMap;
+ @BindView(R.id.map_btn_goto_current_location) FloatingActionButton mBtnGotoCurrentLocation;
+ private IMapController mMapController;
- private GoogleMap mMap; // Might be null if Google Play services APK is not available.
- private ConcurrentSkipListSet mClusteredUsers = new ConcurrentSkipListSet<>();
- private ClusterManager mClusterManager;
- private Cluster mLastClickedCluster;
+ private Unbinder mUnbinder;
- private boolean mIsOffline = false;
+ private SparseArray mClusteredUsers = new SparseArray<>();
+ private UserMarkerClusterer mMarkerClusterer;
private Disposable mLoadOfflineUserDisposable;
+ private final List mOfflineUserIds = new ArrayList<>();
+
private Toast mLastToast = null;
private SettingsRepository.DistanceUnit mDistanceUnit;
@@ -123,26 +122,14 @@ public class MapFragment extends BaseFragment implements
private static final int POSITION_PRIORITY_LAST_STORED = 2;
private static final int POSITION_PRIORITY_FORCED = 100;
- private int mLastPositionType = -1;
- private CameraPosition mLastCameraPosition = null;
- private FusedLocationProviderClient mFusedLocationClient;
- private Location mLastDeviceLocation;
- private final LocationCallback mLocationCallback = new LocationCallback() {
- @Override
- public void onLocationResult(LocationResult locationResult) {
- if (locationResult == null) {
- return;
- }
+ private int mLastPositionType;
+ private ZoomedLocation mLastPosition;
+ private boolean mOsmdroidBug_suppressCallbacks;
+ private final LocationManager mLocationManager = new LocationManager();
- mLastDeviceLocation = locationResult.getLastLocation();
- // FIXME(saemy): Clear cluster info window cache as the distance to this distance is not
- // re-calculated.
-
- // As we know more location details, we do (another) initial map move. This does not
- // affect the current location, if we already moved to a more detailed location.
- doInitialMapMove();
- }
- };
+ private boolean mHasEnabledLocationProviders;
+ private final BehaviorSubject mLastDeviceLocation = BehaviorSubject.create();
+ private MyLocationNewOverlay mDeviceLocationOverlay;
public static MapFragment create() {
MapFragment mapFragment = new MapFragment();
@@ -150,9 +137,10 @@ public static MapFragment create() {
mapFragment.setArguments(bundle);
return mapFragment;
}
- public static MapFragment create(LatLng latLng) {
+ public static MapFragment create(IGeoPoint latLng) {
MapFragment mapFragment = create();
- mapFragment.getArguments().putParcelable(KEY_MAP_TARGET_LAT_LNG, latLng);
+ mapFragment.getArguments().putParcelable(KEY_MAP_TARGET_LAT_LNG,
+ new GeoPoint(latLng.getLatitude(), latLng.getLongitude()));
return mapFragment;
}
@@ -162,17 +150,85 @@ public void onCreate(Bundle savedInstanceState) {
setHasOptionsMenu(true);
- mFusedLocationClient = LocationServices.getFusedLocationProviderClient(getContext());
+ UserMarkerClusterer.MarkerFactory singleLocationMarkerFactory =
+ new UserMarkerClusterer.MarkerFactory(
+ getResources().getDrawable(R.drawable.map_markers_multiple));
+ singleLocationMarkerFactory.setTextAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_TOP);
+ singleLocationMarkerFactory.setTextPadding(0, 15);
+
+ UserMarkerClusterer.MarkerFactory multiLocationMarkerFactory =
+ new UserMarkerClusterer.MarkerFactory(
+ getResources().getDrawable(R.drawable.ic_cluster_multi_location_38dp));
+
+ mMarkerClusterer = new UserMarkerClusterer(getContext());
+ mMarkerClusterer.setSingleLocationMarkerFactory(singleLocationMarkerFactory);
+ mMarkerClusterer.setMultiLocationMarkerFactory(multiLocationMarkerFactory);
+ mMarkerClusterer.setOnClusterClickListener(this::onClusterClick);
+
+ loadOfflineUsers();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
+ Context context = getContext();
+ Configuration.getInstance().load(
+ context, PreferenceManager.getDefaultSharedPreferences(context));
+ //setting this before the layout is inflated is a good idea
+ //it 'should' ensure that the map has a writable location for the map cache, even without permissions
+ //if no tiles are displayed, you can try overriding the cache path using Configuration.getInstance().setCachePath
+ //see also StorageUtils
+ //note, the load method also sets the HTTP User Agent to your application's package name, abusing osm's tile servers will get you banned based on this string
+
View view = inflater.inflate(R.layout.fragment_map, container, false);
mUnbinder = ButterKnife.bind(this, view);
+ mMapController = mMap.getController();
+
+ mLastPositionType = -1;
+ mLastPosition = null;
+
+ mMap.setVerticalMapRepetitionEnabled(false);
+ mMap.setBuiltInZoomControls(false);
+ mMap.setMultiTouchControls(true);
+
+ String tileSourceStr = mSettingsRepository.getTileSourceStr();
+ if (!TileSourceFactory.containsTileSource(tileSourceStr)) {
+ tileSourceStr = TileSourceFactory.DEFAULT_TILE_SOURCE.name();
+ }
+ mMap.setTileSource(TileSourceFactory.getTileSource(tileSourceStr));
+
+ if (hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ handleAccessFineLocationGranted();
+ // The else case is handled in onResume.
+ }
+
+ mMap.getOverlays().add(mMarkerClusterer);
- setUpMapIfNeeded();
+ mMap.addOnFirstLayoutListener((v, left, top, right, bottom) -> {
+ // We add this only here due to issues in osmdroid's setCenter/setZoom when the first
+ // layout of the map is not yet done.
+ mMap.addMapListener(new MapListener() {
+ @Override
+ public boolean onScroll(ScrollEvent event) {
+ if (mOsmdroidBug_suppressCallbacks) {
+ return false;
+ }
+ onPositionChange(mMap.getZoomLevelDouble());
+ return true;
+ }
+ @Override
+ public boolean onZoom(ZoomEvent event) {
+ if (mOsmdroidBug_suppressCallbacks) {
+ return false;
+ }
+ onPositionChange(event.getZoomLevel());
+ return true;
+ }
+ });
+
+ doInitialMapMove();
+ });
return view;
}
@@ -181,30 +237,52 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
public void onResume() {
super.onResume();
+ mMap.onResume();
+
// Register the settings change listener. That does an initial call to the handler.
mSettingsRepository.registerOnChangeListener(mOnSettingsChangeListener);
- LocationRequest locationRequest = new LocationRequest()
- .setInterval(60000)
- .setFastestInterval(60000)
- .setPriority(LocationRequest.PRIORITY_LOW_POWER);
- mFusedLocationClient.requestLocationUpdates(locationRequest, mLocationCallback, null);
+ // Adds a button to navigate to the current GPS position.
+ if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ requestPermissions(new String[]{ Manifest.permission.ACCESS_FINE_LOCATION }, 0);
+ } else {
+ startLocationManager();
+ }
+
+ getResumePauseDisposable().add(mLocationManager.getHasEnabledProviders()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(hasEnabledProviders -> {
+ mHasEnabledLocationProviders = hasEnabledProviders;
+ setGotoCurrentLocationStatus();
+ }));
+ getResumePauseDisposable().add(mLocationManager.getBestLocation()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(location -> {
+ mLastDeviceLocation.onNext(location);
+ // FIXME(saemy): Clear cluster info window cache as the distance to this distance is not
+ // re-calculated.
+
+ setGotoCurrentLocationStatus();
- setUpMapIfNeeded();
+ // As we know more location details, we do (another) initial map move. This does not
+ // affect the current location, if we already moved to a more detailed location.
+ doInitialMapMove();
+ }));
}
@Override
public void onPause() {
- mFusedLocationClient.removeLocationUpdates(mLocationCallback);
+ mLocationManager.stop();
mSettingsRepository.unregisterOnChangeListener(mOnSettingsChangeListener);
+ mMap.onPause();
super.onPause();
}
@Override
public void onStop() {
- if (mLastCameraPosition != null) {
- mSettingsRepository.setLastMapLocation(mLastCameraPosition);
+ if (mLastPosition != null) {
+ mSettingsRepository.setLastMapLocation(mLastPosition);
}
super.onStop();
@@ -216,65 +294,6 @@ public void onDestroyView() {
mUnbinder.unbind();
}
- /**
- * Sets up the map if it is possible to do so (i.e., the Google Play services APK is correctly
- * installed) and the map has not already been instantiated. This will ensure that we only ever
- * call {@link #setUpMap()} once when {@link #mMap} is null.
- *
- * If it isn't installed {@link SupportMapFragment} (and
- * {@link com.google.android.gms.maps.MapView MapView}) will show a prompt for the user to
- * install/update the Google Play services APK on their device.
- *
- * A user can return to this FragmentActivity after following the prompt and correctly
- * installing/updating/enabling the Google Play services. Since the FragmentActivity may not
- * have been completely destroyed during this process (it is likely that it would only be
- * stopped or paused), {@link #onCreate(android.os.Bundle)} may not be called again so we should
- * call this method in {@link #onResume()} to guarantee that it will be called.
- */
- private void setUpMapIfNeeded() {
- // Do a null check to confirm that we have not already instantiated the map.
- if (mMap == null) {
- SupportMapFragment supportMapFragment =
- (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
- supportMapFragment.getMapAsync(map -> {
- mMap = map;
- setUpMap();
- });
- }
- }
-
- private void setUpMap() {
- // Rotate gestures probably aren't needed here and can be disorienting for some of our users.
- mMap.getUiSettings().setRotateGesturesEnabled(false);
-
- // Adds a button to navigate to the current GPS position.
- if (hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
- mMap.setMyLocationEnabled(true); // Requires ACCESS_FINE_LOCATION
- } else {
- requestPermissions(new String[]{ Manifest.permission.ACCESS_FINE_LOCATION }, 0);
- }
-
- mMap.setOnCameraChangeListener(this);
-
- doInitialMapMove();
-
- mClusterManager = new ClusterManager<>(getContext(), mMap);
- mClusterManager.setAlgorithm(new PreCachingAlgorithmDecorator<>(
- new WSNonHierarchicalDistanceBasedAlgorithm<>(getContext())));
- mMap.setOnMarkerClickListener(mClusterManager);
- mMap.setOnInfoWindowClickListener(mClusterManager);
- mClusterManager.setOnClusterClickListener(this);
- mClusterManager.setOnClusterInfoWindowClickListener(this);
- mClusterManager.setOnClusterItemClickListener(this);
- mClusterManager.setOnClusterItemInfoWindowClickListener(this);
- mClusterManager.setRenderer(new UserRenderer());
- mMap.setInfoWindowAdapter(mClusterManager.getMarkerManager());
- mClusterManager.getClusterMarkerCollection()
- .setOnInfoWindowAdapter(new ClusterInfoWindowAdapter(getLayoutInflater()));
- mClusterManager.getMarkerCollection()
- .setOnInfoWindowAdapter(new SingleUserInfoWindowAdapter(getLayoutInflater()));
- }
-
private boolean hasPermission(String permission) {
return PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(getContext(), permission);
@@ -284,50 +303,101 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
@NonNull int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// ACCESS_FINE_LOCATION is granted.
- if (mMap != null) {
- mMap.setMyLocationEnabled(true); // Requires ACCESS_FINE_LOCATION
- }
+ handleAccessFineLocationGranted();
+ }
+ }
+ private void handleAccessFineLocationGranted() {
+ mDeviceLocationOverlay = new MyLocationNewOverlay(mLocationProvider, mMap);
+ mDeviceLocationOverlay.enableMyLocation();
+ mDeviceLocationOverlay.setDrawAccuracyEnabled(true);
+ mDeviceLocationOverlay.getEnableAutoStop(); // Stop following location on map move by user.
+ mDeviceLocationOverlay.disableFollowLocation(); // Initially do not follow the current location.
+ mDeviceLocationOverlay.setOptionsMenuEnabled(true);
+ mMap.getOverlays().add(mDeviceLocationOverlay);
+
+ startLocationManager();
+ }
+
+ private void startLocationManager() {
+ mLocationManager.start((android.location.LocationManager) getActivity().getSystemService(
+ Context.LOCATION_SERVICE));
+ }
+
+ @OnClick(R.id.map_btn_goto_current_location)
+ void onGotoCurrentLocationClicked() {
+ mDeviceLocationOverlay.enableFollowLocation(); // Follow the current location.
+ if (mLastDeviceLocation.getValue() == null) {
+ Toast.makeText(getContext(), R.string.unknown_location, Toast.LENGTH_SHORT)
+ .show();
+ } else {
+ double zoom = Math.max(13, Math.min(17, mMap.getZoomLevelDouble())); // zoom \in [13,17]
+ moveMapToLocation(Tools.locationToLatLng(mLastDeviceLocation.getValue()), zoom,
+ POSITION_PRIORITY_FORCED);
}
}
+ private void setGotoCurrentLocationStatus() {
+ mBtnGotoCurrentLocation.setEnabled(mLastDeviceLocation.getValue() != null
+ || mHasEnabledLocationProviders);
+
+ int fillColor;
+ Drawable icon;
+ if (mLastDeviceLocation.getValue() != null) {
+ icon = mIcMyLocationWhite;
+ fillColor = mColorPrimary;
+ } else if (mHasEnabledLocationProviders) {
+ icon = mIcMyLocationGrey;
+ fillColor = mColorPrimaryWhite;
+ } else {
+ icon = mIcMyLocationWhite;
+ fillColor = mColorPrimaryButtonDisable;
+ }
+ mBtnGotoCurrentLocation.setImageDrawable(icon);
+ mBtnGotoCurrentLocation.setBackgroundTintList(ColorStateList.valueOf(fillColor));
+ }
/**
* Moves the map to the given location if the given priority is newer than the current one.
* A default value for zoom is used if the given value is less than zero.
*/
- private void moveMapToLocation(LatLng latLng, float zoom, int positionPriority) {
- if (mMap == null || latLng == null) {
+ private void moveMapToLocation(IGeoPoint center, double zoom, int positionPriority) {
+ if (center == null) {
return;
}
- if (mLastPositionType < positionPriority) {
+ if (mLastPositionType < positionPriority || positionPriority == POSITION_PRIORITY_FORCED) {
mLastPositionType = positionPriority;
if (zoom < 0) {
zoom = getResources().getInteger(R.integer.prefs_map_location_zoom_default);
}
- mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoom));
+ // FIXME(saemy): osmdroid does not provide a possibility to just move to a center+zoom
+ // which issues the callbacks only once. So we suppress any spurious callback calls.
+ mOsmdroidBug_suppressCallbacks = true;
+ mMapController.setZoom(zoom);
+ mOsmdroidBug_suppressCallbacks = false;
+ mMapController.setCenter(center);
}
}
private void doInitialMapMove() {
float showUserZoom = getResources().getInteger(R.integer.map_showuser_zoom);
// If we were launched with an intent asking us to zoom to a member
- LatLng targetLatLng = getArguments().getParcelable(KEY_MAP_TARGET_LAT_LNG);
+ IGeoPoint targetLatLng = getArguments().getParcelable(KEY_MAP_TARGET_LAT_LNG);
if (targetLatLng != null) {
moveMapToLocation(targetLatLng, showUserZoom, POSITION_PRIORITY_FORCED);
return;
}
// Fetches the last location from the settings (but no default yet).
- CameraPosition savedLocation = mSettingsRepository.getLastMapLocation(false);
+ ZoomedLocation savedLocation = mSettingsRepository.getLastMapLocation(false);
if (savedLocation != null) {
moveMapToLocation(
- savedLocation.target, savedLocation.zoom, POSITION_PRIORITY_LAST_STORED);
+ savedLocation.location, savedLocation.zoom, POSITION_PRIORITY_LAST_STORED);
return;
}
- if (mLastDeviceLocation != null) {
- moveMapToLocation(Tools.locationToLatLng(mLastDeviceLocation), showUserZoom,
+ if (mLastDeviceLocation.getValue() != null) {
+ moveMapToLocation(Tools.locationToLatLng(mLastDeviceLocation.getValue()), showUserZoom,
POSITION_PRIORITY_LAST_DEVICE_POSITION);
return;
}
@@ -341,95 +411,99 @@ private void doInitialMapMove() {
// Fetches the default last location from the settings.
savedLocation = mSettingsRepository.getLastMapLocation(true);
- moveMapToLocation(savedLocation.target, savedLocation.zoom, POSITION_PRIORITY_ESTIMATE);
+ moveMapToLocation(savedLocation.location, savedLocation.zoom, POSITION_PRIORITY_ESTIMATE);
}
- @Override
- public void onCameraChange(CameraPosition position) {
- mLastCameraPosition = position;
+ private final static long FETCH_USERS_DELAY_MS = 700;
+ private Disposable mDelayedUserFetchDisposable;
+ private void onPositionChange(double zoom) {
+ IGeoPoint mapCenter = mMap.getMapCenter();
+ mLastPosition = new ZoomedLocation(mapCenter.getLatitude(), mapCenter.getLongitude(), zoom);
- // If not connected, we'll switch to offline/starred user mode
+ // If not connected, we'll switch to offline/starred users mode
if (!Tools.isNetworkConnected(getContext())) {
sendMessage(R.string.map_network_not_connected);
- // If we already knew we were offline, return
- if (mIsOffline) {
- return;
- }
- // Otherwise, set state to offline and load only offline user
- mIsOffline = true;
- loadOfflineUsers();
return;
}
- // If we were offline, switch back on, but remove the offline markers
- if (mIsOffline) {
- mIsOffline = false;
-
- // Stop listening for the favorite users.
- if (mLoadOfflineUserDisposable != null) {
- mLoadOfflineUserDisposable.dispose();
- mLoadOfflineUserDisposable = null;
- }
-
- mClusterManager.clearItems();
- mClusterManager.getMarkerCollection().clear();
- mClusteredUsers.clear();
+ // We delay the execution for some time as we get a burst of updates once the user moves the
+ // map.
+ if (mDelayedUserFetchDisposable != null) {
+ mDelayedUserFetchDisposable.dispose();
}
-
- // And get standard user list for region from server
- if (position.zoom < getResources().getInteger(R.integer.map_zoom_min_load)) {
+ mDelayedUserFetchDisposable = Completable.complete()
+ .delay(FETCH_USERS_DELAY_MS, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(this::fetchUsersForCurrentMapPosition);
+ getResumePauseDisposable().add(mDelayedUserFetchDisposable);
+ }
+ private void fetchUsersForCurrentMapPosition() {
+ if (mLastPosition.zoom < getResources().getInteger(R.integer.map_zoom_min_load)) {
sendMessage(R.string.users_dont_load);
} else {
sendMessage(R.string.loading_users);
- LatLngBounds curScreen = mMap.getProjection().getVisibleRegion().latLngBounds;
getResumePauseDisposable().add(
- mUserRepository.searchByLocation(curScreen.northeast, curScreen.southwest)
+ mUserRepository.searchByLocation(mMap.getBoundingBox())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(searchResult -> {
if (searchResult.isEmpty()) {
- sendMessage(R.string.no_results);
+ return;
}
for (UserSearchByLocationResponse.User user : searchResult) {
- boolean isNew = mClusteredUsers.add(user.id);
- // Only add to the cluster if it wasn't before.
- if (isNew) {
- mClusterManager.addItem(ClusterUser.from(user));
- }
+ addUserToCluster(user.toSimpleUser());
}
- mClusterManager.cluster();
+ mMarkerClusterer.invalidate();
+ mMap.invalidate();
}, throwable -> {
// TODO(saemy): Error handling.
Log.e(TAG, throwable.getMessage());
}));
}
+
}
private void loadOfflineUsers() {
- mClusterManager.clearItems();
- mClusterManager.getMarkerCollection().clear();
- mClusteredUsers.clear();
-
- // We'll use the starred users when network is offline.
- List loadedUserIds = new ArrayList<>();
+ // We'll show the starred users until we load a fresh version of them.
mLoadOfflineUserDisposable = Observable.merge(mFavoriteRepository.getFavorites())
+ .filter(Resource::hasData)
+ .map(userResource -> userResource.data)
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(userResource -> {
- if (userResource.hasData() && mIsOffline) {
- // Users pop up twice as one is the error since we cannot load it from the
- // network.
- User user = userResource.data;
- if (loadedUserIds.contains(user.id)) {
- return;
- }
- loadedUserIds.add(user.id);
-
- mClusterManager.addItem(ClusterUser.from(user));
- mClusterManager.cluster();
+ .subscribe(user -> {
+ // Users pop up twice as one is the error since we might not be able to load it
+ // from the network.
+ boolean added = mOfflineUserIds.add(user.id);
+ if (!added) {
+ return;
}
+
+ addUserToCluster(user);
+ mMarkerClusterer.invalidate();
});
- getResumePauseDisposable().add(mLoadOfflineUserDisposable);
+ getCreateDestroyDisposable().add(mLoadOfflineUserDisposable);
+ }
+
+ private void addUserToCluster(SimpleUser user) {
+ // Only add to the cluster if it wasn't before or when its location changed.
+ final Marker existingMarker = mClusteredUsers.get(user.id);
+ boolean isNew = existingMarker == null
+ || !existingMarker.getPosition().equals(user.location);
+ if (isNew) {
+ if (existingMarker != null) {
+ mMarkerClusterer.remove(existingMarker);
+ }
+
+ UserMarker marker = new UserMarker(getContext(), mMap, user);
+ marker.setAnchor(UserMarker.ANCHOR_CENTER, UserMarker.ANCHOR_BOTTOM);
+ marker.setIcon(getResources().getDrawable(R.drawable.map_markers_single));
+ marker.setOnMarkerClickListener((m, mapView) -> {
+ new MultiUserSelectDialog().show(Lists.newArrayList(user));
+ return true;
+ });
+ mMarkerClusterer.add(marker);
+ mClusteredUsers.put(user.id, marker);
+ }
}
/**
@@ -438,104 +512,40 @@ private void loadOfflineUsers() {
* - If the bounds are empty (all users at same place) then let it pop the info window
* - Otherwise, move the camera to show the bounds of the map
*/
- @Override
- public boolean onClusterClick(Cluster cluster) {
- mLastClickedCluster = cluster; // remember for use later in the Adapter
-
+ public boolean onClusterClick(MapView mapView, StaticCluster cluster) {
// Find out the bounds of the users currently in cluster
- LatLngBounds.Builder builder = new LatLngBounds.Builder();
- for (ClusterUser user : cluster.getItems()) {
- builder.include(user.latLng);
+ List users = new ArrayList<>(cluster.getSize());
+ List locations = new ArrayList<>(cluster.getSize());
+ for (int i = 0; i < cluster.getSize(); ++i) {
+ UserMarker userMarker = (UserMarker) cluster.getItem(i);
+ users.add(userMarker.getUser());
+ locations.add(userMarker.getPosition());
}
- LatLngBounds bounds = builder.build();
+ BoundingBox bounds = BoundingBox.fromGeoPoints(locations);
// If the users are not all at the same location, then change bounds of map.
- if (!bounds.southwest.equals(bounds.northeast)) {
+ if (bounds.getDiagonalLengthInMeters() > 0) { // TODO(saemy): something bigger than 0?
// Offset from edge of map in pixels when exploding cluster
- View mapView = getChildFragmentManager().findFragmentById(R.id.map).getView();
int padding_percent =
getResources().getInteger(R.integer.cluster_explode_padding_percent);
int padding = Math.min(mapView.getHeight(), mapView.getWidth()) * padding_percent / 100;
- CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, mapView.getWidth(),
- mapView.getHeight(), padding);
- mMap.animateCamera(cu);
- return true;
- }
- showMultiUserSelectDialog((ArrayList) cluster.getItems());
- return true;
- }
-
- /**
- * Start the Search tab with the members we have at this exact location.
- */
- @Override
- public void onClusterInfoWindowClick(Cluster cluster) {
- ArrayList userIds = new ArrayList<>(cluster.getSize());
- for (ClusterUser user : cluster.getItems()) {
- userIds.add(user.id);
+ mapView.zoomToBoundingBox(bounds, true, padding);
+ } else {
+ new MultiUserSelectDialog().show(users);
}
- getNavigationController().navigateToUserList(userIds);
- }
-
- @Override
- public boolean onClusterItemClick(ClusterUser user) {
- return false;
- }
-
- @Override
- public void onClusterItemInfoWindowClick(ClusterUser user) {
- getNavigationController().navigateToUser(user.id);
- }
- /* Creates a dialog for an error message */
- private void showErrorDialog(int errorCode) {
- ErrorDialogFragment dialogFragment = ErrorDialogFragment.create(errorCode);
- dialogFragment.show(getChildFragmentManager(), "errordialog");
+ return true;
}
/**
* Returns the distance to given point or -1 if the current position is unknown.
*/
- private double calculateDistanceTo(LatLng latLng) {
- return mLastDeviceLocation != null
+ private double calculateDistanceTo(IGeoPoint latLng) {
+ return mLastDeviceLocation.getValue() != null
? Tools.calculateDistanceBetween(
- Tools.latLngToLocation(latLng), mLastDeviceLocation, mDistanceUnit)
+ Tools.latLngToLocation(latLng), mLastDeviceLocation.getValue(), mDistanceUnit)
: -1;
}
-
- public void showMultiUserSelectDialog(final ArrayList users) {
- String[] mPossibleItems = new String[users.size()];
-
- double distance = calculateDistanceTo(users.get(0).latLng);
- String distanceSummary =
- getString(R.string.distance_from_current, (int) distance, mDistanceUnitShort);
-
- LinearLayout customTitleView = (LinearLayout) getLayoutInflater().inflate(
- R.layout.view_multiuser_dialog_header, null);
- TextView titleView = customTitleView.findViewById(R.id.title);
- titleView.setText(getResources().getQuantityString(R.plurals.users_at_location,
- users.size(), users.size(), users.get(0).getStreetCityAddressStr()));
-
- TextView distanceView = customTitleView.findViewById(R.id.distance_from_current);
- distanceView.setText(distance >= 0 ? distanceSummary : "");
-
- for (int i = 0; i < users.size(); i++) {
- mPossibleItems[i] = users.get(i).fullname;
- }
- AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getContext());
- alertDialogBuilder.setCustomTitle(customTitleView);
-
- alertDialogBuilder
- .setNegativeButton(R.string.ok, (dialog, which) -> {
- })
- .setItems(mPossibleItems, (dialog, index) -> {
- ClusterUser user = users.get(index);
- getNavigationController().navigateToUser(user.id);
- });
- AlertDialog alertDialog = alertDialogBuilder.create();
- alertDialog.show();
- }
-
private void sendMessage(@StringRes final int messageId) {
if (mLastToast != null) {
mLastToast.cancel();
@@ -564,226 +574,73 @@ protected CharSequence getTitle() {
return getString(R.string.app_name);
}
- enum ClusterStatus {none, some, all}
-
- /* A fragment to display an error dialog */
- public static class ErrorDialogFragment extends DialogFragment {
- private final CompletableSubject mCompletable = CompletableSubject.create();
-
- public static ErrorDialogFragment create(int errorCode) {
- Bundle args = new Bundle();
- args.putInt(DIALOG_ERROR, errorCode);
-
- ErrorDialogFragment dialogFragment = new ErrorDialogFragment();
- dialogFragment.setArguments(args);
- return dialogFragment;
- }
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- // Get the error code and retrieve the appropriate dialog
- int errorCode = getArguments().getInt(DIALOG_ERROR);
- return GooglePlayServicesUtil.getErrorDialog(
- errorCode, getActivity(), REQUEST_RESOLVE_ERROR);
- }
-
- public CompletableSubject getCompletable() {
- return mCompletable;
- }
-
- @Override
- public void onDismiss(DialogInterface dialog) {
- mCompletable.onComplete();
- }
- }
-
- /**
- * Add the title and snippet to the marker so that infoWindow can be rendered.
- */
- private class UserRenderer extends DefaultClusterRenderer {
- private final IconGenerator mSingleLocationClusterIconGenerator =
- new IconGenerator(getActivity().getApplicationContext());
- private final IconGenerator mSingleUserIconGenerator =
- new IconGenerator(getActivity().getApplicationContext());
- private SparseArray mIcons = new SparseArray<>();
- private BitmapDescriptor mSingleUserBitmapDescriptor;
-
- public UserRenderer() {
- super(getActivity().getApplicationContext(), mMap, mClusterManager);
-
- View sameLocationMultiUserClusterView =
- getLayoutInflater().inflate(R.layout.marker_location_cluster, null);
- View singleUserMarkerView = getLayoutInflater().inflate(R.layout.marker_location, null);
- mSingleLocationClusterIconGenerator.setContentView(sameLocationMultiUserClusterView);
- mSingleLocationClusterIconGenerator.setBackground(null);
- mSingleUserIconGenerator.setContentView(singleUserMarkerView);
- mSingleUserIconGenerator.setBackground(null);
- mSingleUserBitmapDescriptor =
- BitmapDescriptorFactory.fromBitmap(mSingleUserIconGenerator.makeIcon());
-
- }
-
- @Override
- protected void onBeforeClusterRendered(Cluster cluster,
- MarkerOptions markerOptions) {
-
- if (clusterLocationStatus(cluster) == ClusterStatus.all) {
- int size = cluster.getSize();
- BitmapDescriptor descriptor = mIcons.get(size);
- if (descriptor == null) {
- // Cache new bitmaps
- descriptor = BitmapDescriptorFactory.fromBitmap(
- mSingleLocationClusterIconGenerator.makeIcon(String.valueOf(size)));
- mIcons.put(size, descriptor);
- }
- markerOptions.icon(descriptor);
- } else {
- super.onBeforeClusterRendered(cluster, markerOptions);
- }
- }
-
- @Override
- protected void onBeforeClusterItemRendered(ClusterUser user, MarkerOptions markerOptions) {
- StringBuilder snippet = new StringBuilder();
- if (!TextUtils.isEmpty(user.street)) {
- snippet.append(user.street).append("
");
- }
- snippet.append(user.city).append(", ").append(user.province.toUpperCase());
-
- double distance = calculateDistanceTo(user.latLng);
- if (distance >= 0) {
- snippet.append("
").append(getString(
- R.string.distance_from_current, (int) distance, mDistanceUnitShort));
- }
-
- markerOptions.title(user.fullname).snippet(snippet.toString());
- markerOptions.icon(mSingleUserBitmapDescriptor);
- }
-
- @Override
- protected boolean shouldRenderAsCluster(Cluster cluster) {
- // Render as a cluster if all the items are at the exact same location, or if there are more than
- // min_cluster_size in the cluster.
- ClusterStatus status = clusterLocationStatus(cluster);
- return status == ClusterStatus.all || status == ClusterStatus.some
- || cluster.getSize() >= getResources().getInteger(R.integer.min_cluster_size);
- }
-
- /*
- * Attempt to determine the location status of items in the cluster, whether all in one location
- * or in a variety of locations.
- */
- protected ClusterStatus clusterLocationStatus(Cluster cluster) {
- HashSet latLngs = new HashSet<>();
- for (ClusterUser item : cluster.getItems()) {
- latLngs.add(item.latLng.toString());
+ class MultiUserSelectDialog {
+ @BindView(R.id.title) TextView mTxtTitle;
+ @BindView(R.id.distance_from_current) TextView mTxtDistanceFromCurrent;
+
+ void show(final List extends SimpleUser> users) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext());
+ Context dialogContext = dialogBuilder.getContext();
+
+ View titleView = null;
+ if (users.size() > 1) {
+ titleView = LayoutInflater.from(dialogContext).inflate(
+ R.layout.view_multiuser_dialog_header, null, false);
+ ButterKnife.bind(this, titleView);
+
+ final SimpleUser representative = users.get(0);
+ mTxtTitle.setText(getResources().getQuantityString(R.plurals.users_at_location,
+ users.size(), users.size(), representative.getStreetCityAddress()));
+
+ double distance = calculateDistanceTo(representative.location);
+ String distanceSummary = getString(
+ R.string.distance_from_current, (int) distance, mDistanceUnitShort);
+ mTxtDistanceFromCurrent.setText(distanceSummary);
+ mTxtDistanceFromCurrent.setVisibility(distance >= 0 ? View.VISIBLE : View.GONE);
}
- // if cluster size and latLngs size are same, all are unique locations, so 'none'
- if (cluster.getSize() == latLngs.size()) {
- return ClusterStatus.none;
- }
- // If there is only one unique location, then all are in same location.
- else if (latLngs.size() == 1) {
- return ClusterStatus.all;
- }
- // Otherwise it's a mix of same and other location
- return ClusterStatus.some;
+ UserListAdapter userListAdapter = new UserListAdapter(
+ dialogContext, UserListAdapter.COMPERATOR_FULLNAME_ASC, null);
+ userListAdapter.resetDataset(users);
+ // Remember the navigationController here as sometimes the activity is no longer set
+ // in the listener.
+ final NavigationController navigationController = getNavigationController();
+ dialogBuilder
+ .setCustomTitle(titleView)
+ .setNegativeButton(R.string.ok, (dialog, which) -> {})
+ .setAdapter(userListAdapter, (dialog, index) -> {
+ SimpleUser user = users.get(index);
+ navigationController.navigateToUser(user.id);
+ })
+ .create()
+ .show();
}
}
- class ClusterInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {
- private View mPopup = null;
- private LayoutInflater mInflater;
-
- ClusterInfoWindowAdapter(LayoutInflater inflater) {
- this.mInflater = inflater;
- }
+ private IMyLocationProvider mLocationProvider = new IMyLocationProvider() {
+ private Disposable mDisposable;
@Override
- public View getInfoWindow(Marker marker) {
- return null;
- }
-
- @Override
- public View getInfoContents(Marker marker) {
- StringBuilder userList = new StringBuilder();
- if (mPopup == null) {
- mPopup = mInflater.inflate(R.layout.view_user_info_multiple, null);
+ public boolean startLocationProvider(IMyLocationConsumer locationConsumer) {
+ if (locationConsumer == null) {
+ return false;
}
- TextView tv = mPopup.findViewById(R.id.title);
-
- if (mLastClickedCluster != null) {
- double distance = calculateDistanceTo(marker.getPosition());
- TextView distance_tv = mPopup.findViewById(R.id.distance_from_current);
- distance_tv.setText(distance >= 0
- ? Html.fromHtml(getString(
- R.string.distance_from_current, (int) distance, mDistanceUnitShort))
- : "");
-
- ArrayList users =
- (ArrayList) mLastClickedCluster.getItems();
- Collections.sort(users, (left, right) -> {
- int ncaLeft = left.isCurrentlyAvailable ? 0 : 1;
- int ncaRight = right.isCurrentlyAvailable ? 0 : 1;
-
- return ncaLeft != ncaRight
- ? ncaLeft - ncaRight
- : left.fullname.compareTo(right.fullname);
-
- });
-
- for (ClusterUser user : users) {
- userList.append(user.fullname).append("
");
- }
- userList.append(getString(R.string.click_to_view_all));
-
- String title = getResources().getQuantityString(R.plurals.users_at_location,
- users.size(), users.size(), users.get(0).getLocationStr());
-
- tv.setText(Html.fromHtml(title));
- tv = mPopup.findViewById(R.id.snippet);
- tv.setText(Html.fromHtml(userList.toString()));
- }
-
- return (mPopup);
+ mDisposable = mLastDeviceLocation
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(location -> locationConsumer.onLocationChanged(location, this));
+ return true;
}
- }
-
- /**
- * InfoWindowAdapter to present info about a single user marker.
- * Implemented here so we can have multiple lines, which the maps-provided one prevents.
- */
- class SingleUserInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {
- @BindView(R.id.title) TextView mLblTitle;
- @BindView(R.id.snippet) TextView mLblSnippet;
-
- private View mPopup = null;
- private LayoutInflater mInflater;
-
- SingleUserInfoWindowAdapter(LayoutInflater inflater) {
- mInflater = inflater;
+ @Override
+ public void stopLocationProvider() {
+ mDisposable.dispose();
}
-
@Override
- public View getInfoWindow(Marker marker) {
- return null;
+ public Location getLastKnownLocation() {
+ return mLastDeviceLocation.getValue();
}
-
- @SuppressLint("InflateParams")
@Override
- public View getInfoContents(Marker marker) {
- if (mPopup == null) {
- mPopup = mInflater.inflate(R.layout.view_user_info_single, null);
- }
- ButterKnife.bind(this, mPopup);
-
- mLblTitle.setText(marker.getTitle());
- mLblSnippet.setText(Html.fromHtml(marker.getSnippet()));
-
- return mPopup;
+ public void destroy() {
}
- }
+ };
}
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/SearchFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/SearchFragment.java
index 0d6d5ad3..0eb9970f 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/SearchFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/SearchFragment.java
@@ -28,6 +28,7 @@
import butterknife.OnItemClick;
import fi.bitrite.android.ws.R;
import fi.bitrite.android.ws.WSAndroidApplication;
+import fi.bitrite.android.ws.model.SimpleUser;
import fi.bitrite.android.ws.model.User;
import fi.bitrite.android.ws.repository.Resource;
import fi.bitrite.android.ws.repository.UserRepository;
@@ -175,7 +176,7 @@ protected CharSequence getTitle() {
return title;
}
- private final static Comparator mComparator = (left, right) -> {
+ private final static Comparator super SimpleUser> mComparator = (left, right) -> {
int caLeft = left.isCurrentlyAvailable ? 1 : 0;
int caRight = right.isCurrentlyAvailable ? 1 : 0;
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java
index 1cc5b601..5ef9808e 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java
@@ -5,10 +5,17 @@
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
+import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.text.TextUtils;
+import org.osmdroid.tileprovider.tilesource.ITileSource;
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
+
+import java.util.LinkedList;
+import java.util.List;
+
import javax.inject.Inject;
import butterknife.BindString;
@@ -27,6 +34,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Inject
@Inject SettingsRepository mSettingsRepository;
@BindString(R.string.prefs_distance_unit_key) String mKeyDistanceUnit;
+ @BindString(R.string.prefs_tile_source_key) String mTileMapSource;
@BindString(R.string.prefs_message_refresh_interval_min_key) String mKeyMessageRefreshInterval;
public static Fragment create() {
@@ -79,6 +87,28 @@ private void setSummary() {
R.string.prefs_distance_unit_summary,
mSettingsRepository.getDistanceUnitLong()));
+ // Sets the available map sources.
+ final ListPreference tileSourcePreference = (ListPreference) findPreference(mTileMapSource);
+ tileSourcePreference.setSummary(getString(
+ R.string.prefs_tile_source_summary,
+ mSettingsRepository.getTileSourceStr()));
+ final List tileSourceNames = new LinkedList<>();
+ for (ITileSource tileSource : TileSourceFactory.getTileSources()) {
+ if(tileSource == TileSourceFactory.PUBLIC_TRANSPORT
+ || tileSource == TileSourceFactory.ChartbundleENRL
+ || tileSource == TileSourceFactory.ChartbundleENRH
+ || tileSource == TileSourceFactory.ChartbundleWAC) {
+ // Blacklisted tile sources.
+ continue;
+ }
+ tileSourceNames.add(tileSource.name());
+ }
+ CharSequence[] tileSourceNamesCS =
+ tileSourceNames.toArray(new CharSequence[tileSourceNames.size()]);
+ tileSourcePreference.setEntries(tileSourceNamesCS);
+ tileSourcePreference.setEntryValues(tileSourceNamesCS);
+ tileSourcePreference.setDefaultValue(TileSourceFactory.DEFAULT_TILE_SOURCE.name());
+
Resources res = getResources();
int intervalMin = mSettingsRepository.getMessageRefreshIntervalMin();
findPreference(mKeyMessageRefreshInterval).setSummary(intervalMin > 0
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java
index ea9d4892..9886d594 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/UserFragment.java
@@ -324,8 +324,8 @@ private void showUserOnMap(@NonNull User user) {
* Send a geo intent so that we can view the user on external maps application
*/
public void sendGeoIntent(@NonNull User user) {
- String lat = Double.toString(user.location.latitude);
- String lng = Double.toString(user.location.longitude);
+ String lat = Double.toString(user.location.getLatitude());
+ String lng = Double.toString(user.location.getLongitude());
String query = Uri.encode(lat + "," + lng + "(" + user.fullname + ")");
Uri uri = Uri.parse("geo:" + lat + "," + lng + "?q=" + query);
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/listadapter/UserListAdapter.java b/app/src/main/java/fi/bitrite/android/ws/ui/listadapter/UserListAdapter.java
index 322d94e5..ee630c6a 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/listadapter/UserListAdapter.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/listadapter/UserListAdapter.java
@@ -34,15 +34,15 @@
import io.reactivex.disposables.Disposable;
import io.reactivex.subjects.BehaviorSubject;
-public class UserListAdapter extends ArrayAdapter {
+public class UserListAdapter extends ArrayAdapter {
public final static Comparator super SimpleUser> COMPERATOR_FULLNAME_ASC =
(left, right) -> left.fullname.compareTo(right.fullname);
- private final Comparator super User> mComparator;
+ private final Comparator super SimpleUser> mComparator;
private final Decorator mDecorator;
- private BehaviorSubject> mUsers = BehaviorSubject.create();
+ private BehaviorSubject> mUsers = BehaviorSubject.create();
@BindView(R.id.user_list_layout) LinearLayout mLayout;
@BindView(R.id.user_list_icon) UserCircleImageView mIcon;
@@ -50,7 +50,8 @@ public class UserListAdapter extends ArrayAdapter {
@BindView(R.id.user_list_lbl_location) TextView mLblLocation;
@BindView(R.id.user_list_lbl_member_info) TextView mMemberInfo;
- public UserListAdapter(@NonNull Context context, @Nullable Comparator super User> comparator,
+ public UserListAdapter(@NonNull Context context,
+ @Nullable Comparator super SimpleUser> comparator,
@Nullable Decorator decorator) {
super(context, R.layout.item_user_list);
@@ -77,7 +78,7 @@ public void resetDataset(List>> users, Object ignored)
});
}
- public void resetDataset(List users) {
+ public void resetDataset(List extends SimpleUser> users) {
if (mComparator != null) {
Collections.sort(users, mComparator);
}
@@ -90,7 +91,7 @@ public void resetDataset(List users) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
- final User user = getItem(position);
+ final SimpleUser user = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
@@ -125,13 +126,13 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
return convertView;
}
- public BehaviorSubject> getUsers() {
+ public BehaviorSubject> getUsers() {
return mUsers;
}
@Nullable
- public User getUser(int pos) {
- List users = mUsers.getValue();
+ public SimpleUser getUser(int pos) {
+ List extends SimpleUser> users = mUsers.getValue();
return users != null && users.size() > pos
? users.get(pos)
: null;
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/model/ClusterUser.java b/app/src/main/java/fi/bitrite/android/ws/ui/model/ClusterUser.java
deleted file mode 100644
index 26eac4a9..00000000
--- a/app/src/main/java/fi/bitrite/android/ws/ui/model/ClusterUser.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package fi.bitrite.android.ws.ui.model;
-
-
-import android.text.TextUtils;
-
-import com.google.android.gms.maps.model.LatLng;
-import com.google.maps.android.clustering.ClusterItem;
-
-import fi.bitrite.android.ws.api.response.UserSearchByLocationResponse;
-import fi.bitrite.android.ws.model.User;
-
-/**
- * This entity is used in the {@link fi.bitrite.android.ws.ui.MapFragment}. It acts as a compatible
- * type to all the different representations of a user that we are given when using the REST API.
- *
- * TODO(saemy): Eventually remove this or make it a child of User (while still implementing ClusterItem).
- */
-public class ClusterUser implements ClusterItem {
- public final int id;
- public final String fullname;
-
- public final String street;
- public final String additionalAddress;
- public final String postalCode;
- public final String city;
- public final String province;
- public final String countryCode;
- public final LatLng latLng;
-
- public final boolean isCurrentlyAvailable;
-
- public ClusterUser(
- int id, String fullname, String street, String additionalAddress, String postalCode,
- String city, String province, String countryCode, LatLng latLng,
- boolean isCurrentlyAvailable) {
- this.id = id;
- this.fullname = fullname;
- this.street = street;
- this.additionalAddress = additionalAddress;
- this.postalCode = postalCode;
- this.city = city;
- this.province = province;
- this.countryCode = countryCode;
- this.latLng = latLng;
- this.isCurrentlyAvailable = isCurrentlyAvailable;
- }
-
- public static ClusterUser from(User user) {
- return new ClusterUser(
- user.id, user.fullname, user.street, user.additionalAddress, user.postalCode,
- user.city, user.province, user.countryCode, user.location,
- user.isCurrentlyAvailable);
- }
- public static ClusterUser from(UserSearchByLocationResponse.User user) {
- return new ClusterUser(
- user.id, user.fullname, user.street, "", user.postalCode, user.city, user.province,
- user.countryCode, new LatLng(user.latitude, user.longitude),
- !user.notCurrentlyAvailable);
- }
-
- public String getLocationStr() {
- StringBuilder location = new StringBuilder();
- if (!TextUtils.isEmpty(street)) {
- location.append(street).append('\n');
- }
-
- if (!TextUtils.isEmpty(additionalAddress)) {
- location.append(additionalAddress).append('\n');
- }
-
- location.append(city).append(", ").append(province.toUpperCase());
- if (!TextUtils.isEmpty(postalCode)) {
- location.append(' ').append(postalCode);
- }
-
- if (!TextUtils.isEmpty(countryCode)) {
- location.append(", ").append(countryCode.toUpperCase());
- }
-
- return location.toString();
- }
-
- public String getStreetCityAddressStr() {
- StringBuilder result = new StringBuilder();
-
- if (!TextUtils.isEmpty(street)) {
- result.append(street).append(", ");
- }
- result.append(city).append(", ").append(province.toUpperCase());
-
- return result.toString();
- }
-
- @Override
- public LatLng getPosition() {
- return latLng;
- }
-}
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/util/NavigationController.java b/app/src/main/java/fi/bitrite/android/ws/ui/util/NavigationController.java
index 4d7201fa..4d37a14c 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/util/NavigationController.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/util/NavigationController.java
@@ -5,7 +5,7 @@
import android.support.v4.app.FragmentManager.BackStackEntry;
import android.support.v4.app.FragmentTransaction;
-import com.google.android.gms.maps.model.LatLng;
+import org.osmdroid.api.IGeoPoint;
import java.util.ArrayList;
@@ -114,7 +114,7 @@ public void navigateToMap() {
// This is the main fragment.
navigateTo(NAVIGATION_TAG_MAP, MapFragment.create(), false, true);
}
- public void navigateToMap(LatLng latLng) {
+ public void navigateToMap(IGeoPoint latLng) {
navigateTo(NAVIGATION_TAG_MAP, MapFragment.create(latLng), false);
}
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarker.java b/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarker.java
new file mode 100644
index 00000000..467f4ca2
--- /dev/null
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarker.java
@@ -0,0 +1,24 @@
+package fi.bitrite.android.ws.ui.util;
+
+import android.content.Context;
+
+import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.MapView;
+import org.osmdroid.views.overlay.Marker;
+
+import fi.bitrite.android.ws.model.SimpleUser;
+
+public class UserMarker extends Marker {
+ private final SimpleUser mUser;
+
+ public UserMarker(Context context, MapView mapView, SimpleUser user) {
+ super(mapView, context);
+
+ mUser = user;
+ setPosition(new GeoPoint(user.location.getLatitude(), user.location.getLongitude()));
+ }
+
+ public SimpleUser getUser() {
+ return mUser;
+ }
+}
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarkerClusterer.java b/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarkerClusterer.java
new file mode 100644
index 00000000..b6a17ebe
--- /dev/null
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarkerClusterer.java
@@ -0,0 +1,221 @@
+package fi.bitrite.android.ws.ui.util;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.bonuspack.clustering.RadiusMarkerClusterer;
+import org.osmdroid.bonuspack.clustering.StaticCluster;
+import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.MapView;
+import org.osmdroid.views.overlay.Marker;
+
+public class UserMarkerClusterer extends RadiusMarkerClusterer {
+ public interface OnClusterClickListener{
+ boolean onClusterClick(MapView mapView, StaticCluster cluster);
+ }
+ private OnClusterClickListener mOnClusterClickListener;
+
+ private MarkerFactory mSingleLocationMarkerFactory;
+ private MarkerFactory mMultiLocationMarkerFactory;
+
+ public UserMarkerClusterer(Context ctx) {
+ super(ctx);
+ }
+
+ /**
+ * Sets the marker factory for when a cluster contains markers with different locations.
+ */
+ public void setSingleLocationMarkerFactory(MarkerFactory markerFactory) {
+ mSingleLocationMarkerFactory = markerFactory;
+ }
+ /**
+ * Sets the marker factory for when a cluster contains markers with the same location.
+ */
+ public void setMultiLocationMarkerFactory(MarkerFactory markerFactory) {
+ mMultiLocationMarkerFactory = markerFactory;
+ }
+
+ public boolean remove(Marker marker){
+ return mItems.remove(marker);
+ }
+
+ @Override
+ public Marker buildClusterMarker(StaticCluster cluster, MapView mapView) {
+ boolean isSingleLocationCluster = true;
+ IGeoPoint firstPos = cluster.getItem(0).getPosition();
+ for (int i = 1; i < cluster.getSize() && isSingleLocationCluster; ++i) {
+ isSingleLocationCluster = firstPos.equals(cluster.getItem(i).getPosition());
+ }
+
+ MarkerFactory markerFactory = isSingleLocationCluster
+ ? mSingleLocationMarkerFactory
+ : mMultiLocationMarkerFactory;
+ Marker marker = markerFactory.createMarker(
+ mapView, cluster.getPosition(), cluster.getSize(), !isSingleLocationCluster);
+
+ marker.setOnMarkerClickListener((m, mv) ->
+ mOnClusterClickListener != null
+ && mOnClusterClickListener.onClusterClick(mv, cluster));
+ return marker;
+ }
+
+ public void setOnClusterClickListener(OnClusterClickListener onClusterClickListener) {
+ mOnClusterClickListener = onClusterClickListener;
+ }
+
+ public static class MarkerFactory {
+ private static final int[] BUCKETS = {10, 20, 50, 100, 200, 500, 1000};
+
+ private float mAnchorH = Marker.ANCHOR_CENTER;
+ private float mAnchorV = Marker.ANCHOR_CENTER;
+ private float mTextAnchorH = Marker.ANCHOR_CENTER;
+ private float mTextAnchorV = Marker.ANCHOR_CENTER;
+ private int mTextPaddingX = 0;
+ private int mTextPaddingY = 0;
+
+ private Drawable mIconDrawable;
+
+ public MarkerFactory(Drawable iconDrawable) {
+ mIconDrawable = iconDrawable;
+ }
+
+ public void setMarkerAnchor(float anchorH, float anchorV) {
+ mAnchorH = anchorH;
+ mAnchorV = anchorV;
+ }
+
+ public void setTextAnchor(float textAnchorH, float textAnchorV) {
+ mTextAnchorH = textAnchorH;
+ mTextAnchorV = textAnchorV;
+ }
+
+ public void setTextPadding(int paddingX, int paddingY) {
+ mTextPaddingX = paddingX;
+ mTextPaddingY = paddingY;
+ }
+
+ Marker createMarker(MapView mapView, GeoPoint position, int clusterSize, boolean useBuckets) {
+ Marker marker = new Marker(mapView);
+ marker.setPosition(position);
+ marker.setInfoWindow(null);
+ marker.setAnchor(mAnchorH, mAnchorV);
+
+ String iconText;
+ Drawable iconDrawable;
+ if (useBuckets) {
+ int bucket = getBucket(clusterSize);
+ iconDrawable = mIconDrawable.getConstantState().newDrawable().mutate();
+ iconDrawable.setColorFilter(getClusterColor(bucket), PorterDuff.Mode.SRC);
+ iconText = getClusterText(bucket);
+ } else {
+ iconText = Integer.toString(clusterSize);
+ iconDrawable = mIconDrawable;
+ }
+ Drawable textDrawable = new TextDrawable(mapView.getContext(), iconText);
+ Drawable[] layers = { iconDrawable, textDrawable };
+ marker.setIcon(new LayerDrawable(layers));
+ return marker;
+ }
+
+ /**
+ * Gets the "bucket" for a particular cluster. By default, uses the number of points within
+ * the cluster, bucketed to some set points.
+ */
+ private int getBucket(int size) {
+ if (size <= BUCKETS[0]) {
+ return size;
+ }
+ for (int i = 0; i < BUCKETS.length - 1; ++i) {
+ if (size < BUCKETS[i + 1]) {
+ return BUCKETS[i];
+ }
+ }
+ return BUCKETS[BUCKETS.length - 1];
+ }
+
+ @ColorInt
+ private int getClusterColor(int clusterSize) {
+ final float hueRange = 220;
+ final float sizeRange = 300;
+ final float size = Math.min(clusterSize, sizeRange);
+ final float hue =
+ (sizeRange - size)*(sizeRange - size) / (sizeRange*sizeRange) * hueRange;
+ return Color.HSVToColor(new float[]{ hue, 1f, .6f });
+ }
+
+ private String getClusterText(int bucket) {
+ if (bucket < BUCKETS[0]) {
+ return String.valueOf(bucket);
+ }
+ return String.valueOf(bucket) + "+";
+ }
+
+ private class TextDrawable extends Drawable {
+ private static final int TEXT_PADDING = 32;
+
+ private final String mText;
+
+ private final Paint mPaint;
+ private final Rect mTextBounds = new Rect();
+
+ TextDrawable(Context context, String text) {
+ mText = text;
+
+ mPaint = new Paint();
+ mPaint.setColor(Color.WHITE);
+ mPaint.setTextSize(15 * context.getResources().getDisplayMetrics().density);
+ mPaint.setFakeBoldText(true);
+ mPaint.setTextAlign(Paint.Align.LEFT);
+ mPaint.setAntiAlias(true);
+ mPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ final Rect bounds = getBounds();
+ float x = bounds.left + mTextPaddingX
+ + mTextAnchorH * (bounds.width() - mTextBounds.width())
+ - mTextBounds.left;
+ float y = bounds.top + mTextBounds.height() + mTextPaddingY
+ + mTextAnchorV*(bounds.height() - mTextBounds.height())
+ - mTextBounds.bottom;
+ canvas.drawText(mText, x, y, mPaint);
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return getIntrinsicSize();
+ }
+ @Override
+ public int getIntrinsicHeight() {
+ return getIntrinsicSize();
+ }
+ private int getIntrinsicSize() {
+ return Math.max(mTextBounds.width(), mTextBounds.height()) + TEXT_PADDING;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ }
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ }
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/fi/bitrite/android/ws/util/LocationManager.java b/app/src/main/java/fi/bitrite/android/ws/util/LocationManager.java
new file mode 100644
index 00000000..1a34e45f
--- /dev/null
+++ b/app/src/main/java/fi/bitrite/android/ws/util/LocationManager.java
@@ -0,0 +1,143 @@
+package fi.bitrite.android.ws.util;
+
+import android.location.Location;
+import android.os.Bundle;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import io.reactivex.subjects.BehaviorSubject;
+
+public class LocationManager {
+ private static final int MIN_TIME_BETWEEN_LOCATION_UPDATES_MS = 1000 * 20; // 20s
+ private static final int TWO_MINUTES_MS = 1000 * 60 * 2;
+
+ private android.location.LocationManager mAndroidLocationManager;
+
+ private final BehaviorSubject mCurrentBestLocation = BehaviorSubject.create();
+ private final BehaviorSubject mHasEnabledProviders =
+ BehaviorSubject.createDefault(false);
+
+ public LocationManager() {
+ }
+
+ public void start(android.location.LocationManager androidLocationManager) {
+ mAndroidLocationManager = androidLocationManager;
+ startProvider(android.location.LocationManager.NETWORK_PROVIDER);
+ startProvider(android.location.LocationManager.GPS_PROVIDER);
+ }
+ public void stop() {
+ mAndroidLocationManager.removeUpdates(mLocationListener);
+ mAndroidLocationManager = null;
+ }
+
+ public BehaviorSubject getBestLocation() {
+ return mCurrentBestLocation;
+ }
+ public BehaviorSubject getHasEnabledProviders() {
+ return mHasEnabledProviders;
+ }
+ private void startProvider(String provider) {
+ updateLocationIfBetter(mAndroidLocationManager.getLastKnownLocation(provider));
+ mAndroidLocationManager.requestLocationUpdates(
+ provider, MIN_TIME_BETWEEN_LOCATION_UPDATES_MS, 0, mLocationListener);
+ boolean isEnabled = mAndroidLocationManager.isProviderEnabled(provider);
+ if (isEnabled) {
+ mLocationListener.onProviderEnabled(provider);
+ } else {
+ mLocationListener.onProviderDisabled(provider);
+ }
+ }
+
+ private final android.location.LocationListener mLocationListener =
+ new android.location.LocationListener() {
+ private Set mEnabledProviders = new HashSet<>();
+
+ @Override
+ public void onLocationChanged(Location location) {
+ updateLocationIfBetter(location);
+ }
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ @Override
+ public void onProviderEnabled(String provider) {
+ boolean wasOff = mEnabledProviders.add(provider);
+ if (wasOff) {
+ mHasEnabledProviders.onNext(!mEnabledProviders.isEmpty());
+ }
+ }
+ @Override
+ public void onProviderDisabled(String provider) {
+ boolean wasOn = mEnabledProviders.remove(provider);
+ if (wasOn) {
+ mHasEnabledProviders.onNext(!mEnabledProviders.isEmpty());
+ }
+ }
+ };
+
+ private boolean updateLocationIfBetter(Location newLocation) {
+ boolean isBetter = isBetterLocation(newLocation, mCurrentBestLocation.getValue());
+ if (isBetter) {
+ mCurrentBestLocation.onNext(newLocation);
+ }
+ return isBetter;
+ }
+
+ /** Determines whether one Location reading is better than the current Location fix
+ * @param location The new Location that you want to evaluate
+ * @param currentBestLocation The current Location fix, to which you want to compare the new one
+ */
+ private static boolean isBetterLocation(Location location, Location currentBestLocation) {
+ if (location == null) {
+ return false;
+ }
+ if (currentBestLocation == null) {
+ // A new location is always better than no location
+ return true;
+ }
+
+ // Check whether the new location fix is newer or older
+ long timeDelta = location.getTime() - currentBestLocation.getTime();
+ boolean isSignificantlyNewer = timeDelta > TWO_MINUTES_MS;
+ boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES_MS;
+ 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;
+ boolean isMoreAccurate = accuracyDelta < 0;
+ boolean isSignificantlyLessAccurate = accuracyDelta > 200;
+
+ // Check if the old and new location are from the same provider
+ 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;
+ }
+ return false;
+ }
+
+ /** Checks whether two providers are the same */
+ private static boolean isSameProvider(String provider1, String provider2) {
+ if (provider1 == null) {
+ return provider2 == null;
+ }
+ return provider1.equals(provider2);
+ }
+}
diff --git a/app/src/main/java/fi/bitrite/android/ws/util/Tools.java b/app/src/main/java/fi/bitrite/android/ws/util/Tools.java
index 8f1cfe1e..23c13dcb 100644
--- a/app/src/main/java/fi/bitrite/android/ws/util/Tools.java
+++ b/app/src/main/java/fi/bitrite/android/ws/util/Tools.java
@@ -21,7 +21,9 @@
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;
-import com.google.android.gms.maps.model.LatLng;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.util.GeoPoint;
import java.util.Locale;
@@ -53,21 +55,24 @@ static public int calculateDistanceBetween(Location l1, Location l2,
float meters = l1.distanceTo(l2);
return (int) (meters / factor);
}
- static public int calculateDistanceBetween(LatLng l1, LatLng l2,
+ static public int calculateDistanceBetween(IGeoPoint l1, IGeoPoint l2,
SettingsRepository.DistanceUnit distanceUnit) {
return calculateDistanceBetween(latLngToLocation(l1), latLngToLocation(l2), distanceUnit);
}
- static public LatLng locationToLatLng(Location location) {
- return new LatLng(location.getLatitude(), location.getLongitude());
+ static public GeoPoint locationToLatLng(Location location) {
+ if (location == null) {
+ return null;
+ }
+ return new GeoPoint(location.getLatitude(), location.getLongitude());
}
- static public Location latLngToLocation(LatLng latLng) {
+ static public Location latLngToLocation(IGeoPoint latLng) {
if (latLng == null) {
return null;
}
Location location = new Location("fromlatlng");
- location.setLatitude(latLng.latitude);
- location.setLongitude(latLng.longitude);
+ location.setLatitude(latLng.getLatitude());
+ location.setLongitude(latLng.getLongitude());
return location;
}
diff --git a/app/src/main/java/fi/bitrite/android/ws/util/WSNonHierarchicalDistanceBasedAlgorithm.java b/app/src/main/java/fi/bitrite/android/ws/util/WSNonHierarchicalDistanceBasedAlgorithm.java
deleted file mode 100644
index 1f46c2e3..00000000
--- a/app/src/main/java/fi/bitrite/android/ws/util/WSNonHierarchicalDistanceBasedAlgorithm.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package fi.bitrite.android.ws.util;
-
-import android.content.Context;
-
-import com.google.android.gms.maps.model.LatLng;
-import com.google.maps.android.clustering.Cluster;
-import com.google.maps.android.clustering.ClusterItem;
-import com.google.maps.android.clustering.algo.Algorithm;
-import com.google.maps.android.clustering.algo.StaticCluster;
-import com.google.maps.android.geometry.Bounds;
-import com.google.maps.android.geometry.Point;
-import com.google.maps.android.projection.SphericalMercatorProjection;
-import com.google.maps.android.quadtree.PointQuadTree;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import fi.bitrite.android.ws.R;
-
-/**
- * This is just copied from the provided NonHierarchicalDistanceBasedAlgorithm, changing
- * MAX_DISTANCE_AT_ZOOM.
- *
- * A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not
- * hierarchical.
- *
- * High level algorithm:
- * 1. Iterate over items in the order they were added (candidate clusters).
- * 2. Create a cluster with the center of the item.
- * 3. Add all items that are within a certain distance to the cluster.
- * 4. Move any items out of an existing cluster if they are closer to another cluster.
- * 5. Remove those items from the list of candidate clusters.
- *
- * Clusters have the center of the first element (not the centroid of the items within it).
- */
-public class WSNonHierarchicalDistanceBasedAlgorithm
- implements Algorithm {
- private Context mContext;
-
- // Turning this down makes for more markers and less clusters
- // 20 seems to make too many clusters. 100 was the original default.
- private static int MAX_DISTANCE_AT_ZOOM = 80;
-
- public WSNonHierarchicalDistanceBasedAlgorithm(Context context) {
- mContext = context;
- MAX_DISTANCE_AT_ZOOM =
- mContext.getResources().getInteger(R.integer.map_algo_max_distance_at_zoom);
- }
-
- /**
- * Any modifications should be synchronized on mQuadTree.
- */
- private final Collection> mItems = new ArrayList<>();
-
- /**
- * Any modifications should be synchronized on mQuadTree.
- */
- private final PointQuadTree> mQuadTree = new PointQuadTree<>(0, 1, 0, 1);
-
- private static final SphericalMercatorProjection PROJECTION =
- new SphericalMercatorProjection(1);
-
- @Override
- public void addItem(T item) {
- final QuadItem quadItem = new QuadItem<>(item);
- synchronized (mQuadTree) {
- mItems.add(quadItem);
- mQuadTree.add(quadItem);
- }
- }
-
- @Override
- public void addItems(Collection items) {
- for (T item : items) {
- addItem(item);
- }
- }
-
- @Override
- public void clearItems() {
- synchronized (mQuadTree) {
- mItems.clear();
- mQuadTree.clear();
- }
- }
-
- @Override
- public void removeItem(T item) {
- throw new UnsupportedOperationException(
- "NonHierarchicalDistanceBasedAlgorithm.remove not implemented");
- }
-
- @Override
- public Set extends Cluster> getClusters(double zoom) {
- final int discreteZoom = (int) zoom;
-
- final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom) / 256;
-
- final Set> visitedCandidates = new HashSet<>();
- final Set> results = new HashSet<>();
- final Map, Double> distanceToCluster = new HashMap<>();
- final Map, StaticCluster> itemToCluster = new HashMap<>();
-
- synchronized (mQuadTree) {
- for (QuadItem candidate : mItems) {
- if (visitedCandidates.contains(candidate)) {
- // Candidate is already part of another cluster.
- continue;
- }
-
- Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
- Collection> clusterItems;
- clusterItems = mQuadTree.search(searchBounds);
- if (clusterItems.size() == 1) {
- // Only the current marker is in range. Just add the single item to the results.
- results.add(candidate);
- visitedCandidates.add(candidate);
- distanceToCluster.put(candidate, 0d);
- continue;
- }
- StaticCluster cluster =
- new StaticCluster<>(candidate.mClusterItem.getPosition());
- results.add(cluster);
-
- for (QuadItem clusterItem : clusterItems) {
- Double existingDistance = distanceToCluster.get(clusterItem);
- double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
- if (existingDistance != null) {
- // Item already belongs to another cluster. Check if it's closer to this cluster.
- if (existingDistance < distance) {
- continue;
- }
- // Move item to the closer cluster.
- itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
- }
- distanceToCluster.put(clusterItem, distance);
- cluster.add(clusterItem.mClusterItem);
- itemToCluster.put(clusterItem, cluster);
- }
- visitedCandidates.addAll(clusterItems);
- }
- }
- return results;
- }
-
- @Override
- public Collection getItems() {
- final List items = new ArrayList<>();
- synchronized (mQuadTree) {
- for (QuadItem quadItem : mItems) {
- items.add(quadItem.mClusterItem);
- }
- }
- return items;
- }
-
- private double distanceSquared(Point a, Point b) {
- return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
- }
-
- private Bounds createBoundsFromSpan(Point p, double span) {
- double halfSpan = span / 2;
- return new Bounds(
- p.x - halfSpan, p.x + halfSpan,
- p.y - halfSpan, p.y + halfSpan);
- }
-
- private static class QuadItem implements PointQuadTree.Item, Cluster {
- private final T mClusterItem;
- private final Point mPoint;
- private final LatLng mPosition;
- private Set singletonSet;
-
- private QuadItem(T item) {
- mClusterItem = item;
- mPosition = item.getPosition();
- mPoint = PROJECTION.toPoint(mPosition);
- singletonSet = Collections.singleton(mClusterItem);
- }
-
- @Override
- public Point getPoint() {
- return mPoint;
- }
-
- @Override
- public LatLng getPosition() {
- return mPosition;
- }
-
- @Override
- public Set getItems() {
- return singletonSet;
- }
-
- @Override
- public int getSize() {
- return 1;
- }
- }
-}
diff --git a/app/src/main/res/drawable-hdpi/ic_cluster_multi_location_38dp.xml b/app/src/main/res/drawable-hdpi/ic_cluster_multi_location_38dp.xml
new file mode 100644
index 00000000..835d4364
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_cluster_multi_location_38dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-mdpi/ic_my_location_grey600_24dp.xml b/app/src/main/res/drawable-mdpi/ic_my_location_grey600_24dp.xml
new file mode 100644
index 00000000..520d56aa
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_my_location_grey600_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_my_location_white_24dp.xml b/app/src/main/res/drawable/ic_my_location_white_24dp.xml
new file mode 100644
index 00000000..373237bf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_my_location_white_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml
index 9582f42c..37dc86ca 100644
--- a/app/src/main/res/layout/fragment_map.xml
+++ b/app/src/main/res/layout/fragment_map.xml
@@ -1,26 +1,26 @@
-
-
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
-
+
+
+
diff --git a/app/src/main/res/layout/view_user_info_multiple.xml b/app/src/main/res/layout/view_user_info_multiple.xml
index dbc48a50..1acf20da 100644
--- a/app/src/main/res/layout/view_user_info_multiple.xml
+++ b/app/src/main/res/layout/view_user_info_multiple.xml
@@ -1,6 +1,7 @@
+ android:textStyle="bold"
+ tools:text="Demo text" />
-
-
+ android:textSize="12sp"
+ tools:text="1500 miles from here" />
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 79d06a8b..9c1ac913 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -100,6 +100,9 @@
km
mi
+ Kartentyp
+ %1$s
+
Aktualisierungsintervall für Nachrichten
- Jede Minute
@@ -237,4 +240,6 @@
Anzahl Meldungen
Mitglieder
Mitglied
+
+ Position noch nicht bekannt
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index deb72f94..871dae2c 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -223,4 +223,6 @@
Error al obtener mensajes.
Error al crear una nueva conversación.
El comentario no fue enviado.
+
+ Ubicación desconocida
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 99737bc0..bfc57db5 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -229,4 +229,6 @@
Erreur lors de l\'obtention de messages.
Erreur lors de la création d’une nouvelle conversation.
L’envoi du commentaire a échoué.
+
+ Position inconnue
diff --git a/app/src/main/res/values/google_maps_api.xml b/app/src/main/res/values/google_maps_api.xml
deleted file mode 100644
index cad8eb51..00000000
--- a/app/src/main/res/values/google_maps_api.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- AIzaSyDCE2v0T3GIRk7QFVkq1aW4x6QbGfdEjOg
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f13b163c..6f8e47be 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -104,6 +104,10 @@
km
mi
+ tile_source
+ Map type
+ %1$s
+
message_reload_interval_min
Messages poll frequency
@@ -246,4 +250,6 @@
Show profiles
Enabled
+
+ Position not yet known
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 751b1ae1..86a43ed2 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -10,6 +10,11 @@
android:summary="@string/prefs_distance_unit_summary"
android:title="@string/prefs_distance_unit_title" />
+
+