[Maps] Replaces gMaps with MapBox

In order to circumvent the usage quota of Google Maps
the map provider was changed to be MapBox now. Additionally
the maps implementation was refactored to be better extendable
in future.
This commit is contained in:
Alexander Dörflinger
2025-04-23 14:04:24 +02:00
parent b69abac2a8
commit e482093832
15 changed files with 733 additions and 168 deletions

1
.idea/gradle.xml generated
View File

@@ -15,6 +15,5 @@
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
<option name="offlineMode" value="true" />
</component>
</project>

View File

@@ -64,6 +64,9 @@ dependencies {
implementation 'com.google.android.gms:play-services-maps:19.1.0'
implementation 'com.google.android.gms:play-services-location:21.3.0'
//MapBox SDK
implementation 'com.mapbox.maps:android:11.11.0'
// Glide
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'androidx.preference:preference:1.2.1'

View File

@@ -15,7 +15,6 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -23,27 +22,26 @@ import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.familyhelpers.model.LocationObject;
import com.aldo.apps.familyhelpers.model.User;
import com.aldo.apps.familyhelpers.ui.ActiveShareAdapter;
import com.aldo.apps.familyhelpers.ui.map.AbstractMap;
import com.aldo.apps.familyhelpers.ui.map.IMapMovementListener;
import com.aldo.apps.familyhelpers.ui.map.mapbox.MapBoxView;
import com.aldo.apps.familyhelpers.workers.DatabaseHelper;
import com.aldo.apps.familyhelpers.workers.LocationHelper;
import com.aldo.apps.familyhelpers.workers.ShareLocationBackgroundWorker;
import com.bumptech.glide.Glide;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.materialswitch.MaterialSwitch;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.mapbox.maps.MapView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -53,87 +51,80 @@ import io.reactivex.rxjava3.disposables.Disposable;
/**
* Activity showing a Map to display the shared location plus additional information.
*/
public class ShareLocationActivity extends AppCompatActivity implements OnMapReadyCallback {
public class ShareLocationActivity extends AppCompatActivity implements IMapMovementListener {
/**
* Tag for debugging purpose.
*/
private static final String TAG = "ShareLocationActivity";
/**
* Map containing all markers and the corresponding ID.
*/
private final Map<String, Marker> mMarkerMap = new HashMap<>();
/**
* {@link List} of all {@link User}s.
*/
private final List<User> mAllUsers = new ArrayList<>();
/**
* The {@link GoogleMap} view.
*/
private GoogleMap mGmap;
/**
* The {@link DatabaseHelper} to read and load data from.
*/
private DatabaseHelper mDbHelper;
/**
* The currently logged in {@link FirebaseUser}.
*/
private FirebaseUser mCurrentUser;
/**
* The ID of the location updates to listen to.
*/
private String mShareId;
/**
* The InfoBox {@link ImageView} showing the user icon.
*/
private ImageView mInfoBoxIcon;
/**
* The InfoBox {@link TextView} showing the title.
*/
private TextView mInfoBoxTitle;
/**
* The InfoBox {@link TextView} showing the Location information.
*/
private TextView mInfoBoxLocation;
/**
* The InfoBox {@link TextView} showing the altitude..
*/
private TextView mInfoBoxAltitude;
/**
* The InfoBox {@link TextView} showing the last received speed.
*/
private TextView mInfoBoxSpeed;
/**
* The InfoBox {@link TextView} showing the timestamp of the last update.
*/
private TextView mInfoBoxTimeStamp;
/**
* The {@link Disposable} holding the subscription to the {@link LocationObject}
* {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable.
*/
private Disposable mLocationUpdateSubscription;
/**
* The {@link Disposable} holding the subscription to the {@link String}
* {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to know whom to follow.
*/
private Disposable mCurrentlyFollowingSubscription;
/**
* The {@link Disposable} holding the subscription to {@link User}
* {@link io.reactivex.rxjava3.subjects.BehaviorSubject} in order to have it cancellable.
*/
private Disposable mUserSubscription;
/**
* Boolean flag indicating whether the system is currently in night mode or not.
*/
private boolean mIsNightMode;
/**
* Reference to the Google Maps Marker, to update it's position rather than adding a new one.
*/
private Marker mGmapsMarker;
/**
* The ID of who the user is currently following.
*/
private String mCurrentlyFollowing;
/**
* Displays an error if no active shares.
@@ -150,25 +141,21 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
*/
private FloatingActionButton mRecenterFollowBtn;
private LocationObject mCurrentlyFollowingLocation;
/**
* The {@link ActiveShareAdapter} to populate the activeShare view.
*/
private ActiveShareAdapter mActiveShareAdapter;
/**
* Flag indicating whether currently the user wants to be following another user or not.
*/
private boolean mIsFollowing;
private AbstractMap<MapView> mMap;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_location);
final SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mMap = new MapBoxView(findViewById(R.id.mapbox), this);
initMapView();
mCurrentUser = FirebaseAuth.getInstance().getCurrentUser();
mInfoBoxIcon = findViewById(R.id.share_location_info_user_icon);
mInfoBoxTitle = findViewById(R.id.share_location_info_title);
@@ -185,7 +172,7 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
mRecenterFollowBtn.setOnClickListener(v -> startFollowingAndCenterCamera());
mActiveShares.setAdapter(mActiveShareAdapter);
mapFragment.getMapAsync(this);
mDbHelper = new DatabaseHelper();
}
@@ -194,8 +181,9 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
super.onResume();
final Configuration configuration = getResources().getConfiguration();
mIsNightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
boolean isNightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
mMap.applyDayNightMode(isNightMode);
if (mShareId == null) {
mShareId = mCurrentUser.getUid();
@@ -257,32 +245,30 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
* @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s.
*/
private void handleReceivedLocations(final Map<String, LocationObject> locationObjectMap) {
if (TextUtils.isEmpty(mCurrentlyFollowing)) {
if (TextUtils.isEmpty(mMap.getCurrentlyFollowing())) {
clearInfoBox();
}
Log.d(TAG, "handleReceivedLocations() called with: locationObjectMap = [" + locationObjectMap + "]");
// If empty received, remove all markers.
if (locationObjectMap.isEmpty()) {
for (final Marker marker : mMarkerMap.values()) {
marker.remove();
}
mMarkerMap.clear();
Log.d(TAG, "handleReceivedLocations: Noone sharing");
mMap.clearAllMarkers();
clearInfoBox();
mNoActiveShares.setVisibility(View.VISIBLE);
mActiveShares.setVisibility(View.GONE);
} else {
Log.d(TAG, "handleReceivedLocations: Sharing active");
mNoActiveShares.setVisibility(View.GONE);
mActiveShares.setVisibility(View.VISIBLE);
}
if (mGmapsMarker != null) {
mGmapsMarker.remove();
}
//Update the available active shares.
updateActiveShares(locationObjectMap);
// If non-empty received, remove all invalid existing markers
removeInvalidExistingMarkers(locationObjectMap);
if (mMap.removeInvalidExistingMarkers(locationObjectMap)) {
clearInfoBox();
}
// Update/Insert all existing/new markers
updateExistingOrCreateValidMarkers(locationObjectMap);
updateInfoBox(mMap.updateOrAddCustomMarker(locationObjectMap));
}
/**
@@ -298,72 +284,6 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
mActiveShareAdapter.applyNewData(activeShares);
}
/**
* Removed all markers that became invalid because the sharing was stopped.
*
* @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s.
*/
private void removeInvalidExistingMarkers(final Map<String, LocationObject> locationObjectMap) {
final List<String> toBeRemovedKeys = new ArrayList<>();
for (Map.Entry<String, Marker> markerEntry : mMarkerMap.entrySet()) {
if (!locationObjectMap.containsKey(markerEntry.getKey())) {
Log.d(TAG, "handleReceivedLocations: Marker not valid anymore, remove");
markerEntry.getValue().remove();
toBeRemovedKeys.add(markerEntry.getKey());
}
}
// Remove from the map as well
for (final String removeKey : toBeRemovedKeys) {
if (TextUtils.equals(removeKey, mCurrentlyFollowing)) {
clearInfoBox();
mCurrentlyFollowing = null;
mIsFollowing = false;
mCurrentlyFollowingLocation = null;
}
mMarkerMap.remove(removeKey);
}
}
/**
* Helper method to update or create the valid markers on the map. Updates the position of a
* marker if it is existing before, adds a new marker if nor yet existing.
*
* @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s.
*/
private void updateExistingOrCreateValidMarkers(final Map<String, LocationObject> locationObjectMap) {
for (final Map.Entry<String, LocationObject> locationSet : locationObjectMap.entrySet()) {
final LocationObject newReceived = locationSet.getValue();
final LatLng receivedLocation = new LatLng(newReceived.getLatitude(), newReceived.getLongitude());
final String sharingId = locationSet.getKey();
final User locationUser = mDbHelper.getUserForId(sharingId);
if (mMarkerMap.containsKey(sharingId)) {
final Marker existing = mMarkerMap.get(sharingId);
if (existing == null) {
Log.w(TAG, "updateExistingOrCreateValidMarkers: Marker somehow not existing, skip");
continue;
}
Log.d(TAG, "handleReceivedLocations: Marker still available, update position");
existing.setPosition(receivedLocation);
} else {
Log.d(TAG, "handleReceivedLocations: New Marker to be created.");
final Marker marker = mGmap.addMarker(new MarkerOptions()
.position(receivedLocation)
.title(locationUser == null ? "" : locationUser.getDisplayName()));
mMarkerMap.put(locationSet.getKey(), marker);
}
if (TextUtils.equals(mCurrentlyFollowing, sharingId)) {
final LocationObject locationObject = locationSet.getValue();
mCurrentlyFollowingLocation = locationObject;
updateInfoBox(locationObject);
if (mIsFollowing) {
startFollowingAndCenterCamera();
}
}
}
}
/**
* Helper method to clear the info box.
*/
@@ -382,8 +302,12 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
* @param locationObject The {@link LocationObject} holding the information.
*/
private void updateInfoBox(final LocationObject locationObject) {
if (mCurrentlyFollowing != null) {
final User followed = mDbHelper.getUserForId(mCurrentlyFollowing);
if (locationObject == null) {
Log.d(TAG, "updateInfoBox: Location is null, skip update...");
return;
}
if (mMap.getCurrentlyFollowing() != null) {
final User followed = mDbHelper.getUserForId(mMap.getCurrentlyFollowing());
if (followed != null) {
mInfoBoxTitle.setText(String.format(getString(R.string.share_location_info_title),
followed.getDisplayName()));
@@ -436,19 +360,18 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
}
/**
* Handlles the callback when the selection of currently sharedd Id changes. Re-Established the
* Handles the callback when the selection of currently sharedd Id changes. Re-Established the
* subscription to all locations and updates the info box.
*
* @param shareId The ID of the user to be followed.
*/
private void handleCurrentlyFollowingChanged(final String shareId) {
Log.d(TAG, "onCreate: Currently selected = [" + shareId + "]");
mCurrentlyFollowing = shareId;
mIsFollowing = !TextUtils.isEmpty(mCurrentlyFollowing);
Log.d(TAG, "handleCurrentlyFollowingChanged: Currently selected = [" + shareId + "]");
// Re-setup the subscription.
if (mLocationUpdateSubscription != null) {
mLocationUpdateSubscription.dispose();
}
mMap.setCurrentlyFollowing(shareId);
mLocationUpdateSubscription = mDbHelper.subscribeToAllLocationUpdates()
.subscribe(this::handleReceivedLocations, this::handleSubscriptionError);
}
@@ -466,43 +389,18 @@ public class ShareLocationActivity extends AppCompatActivity implements OnMapRea
* Helper method to start following and recenter the camera on the location followed.
*/
private void startFollowingAndCenterCamera() {
mIsFollowing = true;
mGmap.animateCamera(CameraUpdateFactory.newLatLngZoom(
new LatLng(mCurrentlyFollowingLocation.getLatitude(),
mCurrentlyFollowingLocation.getLongitude()),
15.0f));
mMap.startFollowingAndCenterCamera();
mRecenterFollowBtn.setVisibility(View.GONE);
}
@Override
public void onMapReady(@NonNull final GoogleMap googleMap) {
if (googleMap == null) {
Log.w(TAG, "onMapReady: Map null, cannot continue");
return;
}
mGmap = googleMap;
if (mIsNightMode) {
mGmap.setMapStyle(MapStyleOptions.loadRawResourceStyle(this, R.raw.map_style_night));
} else {
mGmap.setMapStyle(MapStyleOptions.loadRawResourceStyle(this, R.raw.map_style_day));
}
final LatLng defaultHome = new LatLng(DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE);
private void initMapView() {
mMap.zoomOnDefaultLocation();
mMap.displayOwnLocation();
mMap.registerMapMovementListener(this);
}
if (mGmapsMarker == null) {
mGmapsMarker = mGmap.addMarker(new MarkerOptions()
.position(defaultHome)
.title("Default - Home"));
} else {
mGmapsMarker.setPosition(defaultHome);
}
mGmap.setOnCameraMoveStartedListener(reason -> {
if (GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == reason
&& !TextUtils.isEmpty(mCurrentlyFollowing)) {
Log.d(TAG, "onCameraMoveStarted: User moved map, set following to false.");
mIsFollowing = false;
mRecenterFollowBtn.setVisibility(View.VISIBLE);
}
});
mGmap.moveCamera(CameraUpdateFactory.newLatLngZoom(defaultHome, 8.0f));
@Override
public void onMapMovementDetected() {
mRecenterFollowBtn.setVisibility(View.VISIBLE);
}
}

View File

@@ -41,6 +41,11 @@ public class LocationObject {
*/
private long timestamp;
/**
* Last received bearing.
*/
private float bearing;
/**
* Empty C'Tor for database usage.
*/
@@ -57,9 +62,10 @@ public class LocationObject {
* @param altitude Last received altitude.
* @param speed Last received speed.
* @param timestamp Last received timestamp.
* @param bearing Last received bearing.
*/
public LocationObject(final String shareId, final double latitude, final double longitude,
final double altitude, final float speed, final long timestamp) {
final double altitude, final float speed, final long timestamp, final float bearing) {
this.shareId = shareId;
this.latitude = latitude;
this.longitude = longitude;
@@ -78,7 +84,7 @@ public class LocationObject {
final FirebaseUser firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
return new LocationObject(firebaseUser.getUid(), location.getLatitude(),
location.getLongitude(), location.getAltitude(), location.getSpeed(),
location.getTime());
location.getTime(), location.getBearing());
}
/**
@@ -146,11 +152,20 @@ public class LocationObject {
return timestamp;
}
/**
* Returns the last received bearing.
*
* @return The last received bearing.
*/
public float getBearing() {
return bearing;
}
@NonNull
@Override
public String toString() {
return "LocationObject with shareId = [" + shareId + "], lat = [" + latitude
+ "], long = [" + longitude + "], alt = [" + altitude + "], speed = [" + speed
+ "], time = [" + timestamp + "]";
+ "], time = [" + timestamp + "], bearing = [" + bearing + "]";
}
}

View File

@@ -0,0 +1,152 @@
package com.aldo.apps.familyhelpers.ui.map;
import android.content.Context;
import android.util.Log;
import com.aldo.apps.familyhelpers.model.LocationObject;
import com.aldo.apps.familyhelpers.model.User;
import com.aldo.apps.familyhelpers.workers.DatabaseHelper;
import java.lang.ref.WeakReference;
import java.util.Map;
/**
*
* @param <T> The generic type of MapFragment to be used.
*/
public abstract class AbstractMap<T> {
/**
* Tag for debugging purpose.
*/
private static final String TAG = "AbstractMap";
/**
* The {@link DatabaseHelper} to read data from.
*/
protected final DatabaseHelper mDbHelper = new DatabaseHelper();
/**
* The {@link WeakReference} to the calling {@link Context}.
*/
protected final WeakReference<Context> mContextRef;
/**
* The ID of who the user is currently following.
*/
protected String mCurrentlyFollowing;
/**
* The {@link LocationObject} of the currently following user.
*/
protected LocationObject mCurrentlyFollowingLocation;
/**
* Listener to be invoked when the map moved.
*/
protected IMapMovementListener mMapMovementListener;
/**
* The MapFragment to be shown.
*/
protected T mMapFragment;
/**
* C'Tor.
*
* @param mapFragment The inflated Map-Fragment.
* @param context The calling {@link Context}.
*/
public AbstractMap(final T mapFragment, final Context context) {
Log.d(TAG, "AbstractMap() called with: mapFragment = [" + mapFragment + "]");
mMapFragment = mapFragment;
mContextRef = new WeakReference<>(context);
}
/**
* Helper method to display and zoom onto the default location until Map is loaded and location
* is received.
*/
public abstract void zoomOnDefaultLocation();
/**
* Helper method to start following and recenter the camera on the location followed.
*/
public abstract void startFollowingAndCenterCamera();
/**
* Helper method to show and display the own location.
*/
public abstract void displayOwnLocation();
/**
* Helper method to clear all markers.
*/
public abstract void clearAllMarkers();
/**
* Helper method to update or create the valid markers on the map. Updates the position of a
* marker if it is existing before, adds a new marker if nor yet existing.
*
* @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s.
*
* @return The {@link LocationObject} that is currently being followed.
*/
public abstract LocationObject updateOrAddCustomMarker(final Map<String, LocationObject> locationObjectMap);
/**
* Removed all markers that became invalid because the sharing was stopped.
*
* @param locationObjectMap The {@link Map} matching shareIds to {@link LocationObject}s.
*
* @return true if currently following was removed too, false otherwise.
*/
public abstract boolean removeInvalidExistingMarkers(final Map<String, LocationObject> locationObjectMap);
/**
* Helper method to animate the camera to a specific location.
*
* @param locationObject The {@link LocationObject} to zoom to.
*/
public abstract void animateCamera(final LocationObject locationObject);
/**
* Helper method to apply day/night mode. As per standard, this is handled via resources directly,
* but may be overridden.
*
* @param isNightMode true if night mode is active, false otherwise.
*/
public void applyDayNightMode(final boolean isNightMode) {
// Standard implementation: Do nothing as handled in resources.
}
/**
* Sets the ID of who the user is currently following.
*
* @param currentlyFollowing The ID of who the user is currently following.
*/
public void setCurrentlyFollowing(final String currentlyFollowing) {
mCurrentlyFollowing = currentlyFollowing;
}
/**
* Returns the ID of who the user is currently following.
*
* @return The ID of who the user is currently following.
*/
public String getCurrentlyFollowing() {
return mCurrentlyFollowing;
}
/**
* Registers a {@link IMapMovementListener} to be invoked whenever the user moves the map
* to properly show the re-center button.
*
* @param mapMovementListener The {@link IMapMovementListener} to be invoked.
*/
public void registerMapMovementListener(final IMapMovementListener mapMovementListener) {
mMapMovementListener = mapMovementListener;
}
}

View File

@@ -0,0 +1,6 @@
package com.aldo.apps.familyhelpers.ui.map;
public interface IMapMovementListener {
void onMapMovementDetected();
}

View File

@@ -0,0 +1,189 @@
package com.aldo.apps.familyhelpers.ui.map.gmaps;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_HOME_LATITUDE;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_HOME_LONGITUDE;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import com.aldo.apps.familyhelpers.R;
import com.aldo.apps.familyhelpers.model.LocationObject;
import com.aldo.apps.familyhelpers.model.User;
import com.aldo.apps.familyhelpers.ui.map.AbstractMap;
import com.aldo.apps.familyhelpers.ui.map.IMapMovementListener;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Google Maps specific implementation of the {@link AbstractMap}.
*/
public class GoogleMapsView extends AbstractMap<GoogleMap> {
/**
* Tag for debugging purposes.
*/
private static final String TAG = "GoogleMapsView";
/**
* The zoom level to be applied per default.
*/
private static final float DEFAULT_ZOOM_LEVEL = 15.0f;
/**
* The default tilt level.
*/
private static final float DEFAULT_TILT = 0.0f;
/**
* Map containing all markers and the corresponding ID.
*/
private final Map<String, Marker> mMarkerMap = new HashMap<>();
/**
* Reference to the Google Maps Marker, to update it's position rather than adding a new one.
*/
private Marker mGmapsMarker;
/**
* C'Tor.
*
* @param gMap The inflated Map-Fragment.
* @param context The calling {@link Context}.
*/
public GoogleMapsView(final GoogleMap gMap, final Context context) {
super(gMap, context);
}
@Override
public void zoomOnDefaultLocation() {
final LatLng defaultHome = new LatLng(DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE);
if (mGmapsMarker == null) {
mGmapsMarker = mMapFragment.addMarker(new MarkerOptions()
.position(defaultHome)
.title("Default - Home"));
} else {
mGmapsMarker.setPosition(defaultHome);
}
mMapFragment.moveCamera(CameraUpdateFactory.newLatLngZoom(defaultHome, DEFAULT_ZOOM_LEVEL));
}
@Override
public void startFollowingAndCenterCamera() {
animateCamera(mCurrentlyFollowingLocation);
}
@Override
public void displayOwnLocation() {
// Do nothing for GMaps
}
@Override
public LocationObject updateOrAddCustomMarker(final Map<String, LocationObject> locationObjectMap) {
for (final Map.Entry<String, LocationObject> locationSet : locationObjectMap.entrySet()) {
final LocationObject newReceived = locationSet.getValue();
final LatLng receivedLocation = new LatLng(newReceived.getLatitude(), newReceived.getLongitude());
final String sharingId = locationSet.getKey();
final User locationUser = mDbHelper.getUserForId(sharingId);
if (mMarkerMap.containsKey(sharingId)) {
final Marker existing = mMarkerMap.get(sharingId);
if (existing == null) {
Log.w(TAG, "updateOrAddCustomMarker: Marker somehow not existing, skip");
continue;
}
Log.d(TAG, "updateOrAddCustomMarker: Marker still available, update position");
existing.setPosition(receivedLocation);
} else {
Log.d(TAG, "updateOrAddCustomMarker: New Marker to be created.");
final Marker marker = mMapFragment.addMarker(new MarkerOptions()
.position(receivedLocation)
.title(locationUser == null ? "" : locationUser.getDisplayName()));
mMarkerMap.put(locationSet.getKey(), marker);
}
if (TextUtils.equals(mCurrentlyFollowing, sharingId)) {
mCurrentlyFollowingLocation = locationSet.getValue();
if (mCurrentlyFollowing != null) {
startFollowingAndCenterCamera();
}
}
}
return mCurrentlyFollowingLocation;
}
@Override
public void clearAllMarkers() {
for (final Marker marker : mMarkerMap.values()) {
marker.remove();
}
mMarkerMap.clear();
}
@Override
public boolean removeInvalidExistingMarkers(final Map<String, LocationObject> locationObjectMap) {
final List<String> toBeRemovedKeys = new ArrayList<>();
for (Map.Entry<String, Marker> markerEntry : mMarkerMap.entrySet()) {
if (!locationObjectMap.containsKey(markerEntry.getKey())) {
Log.d(TAG, "removeInvalidExistingMarkers: Marker not valid anymore, remove");
markerEntry.getValue().remove();
toBeRemovedKeys.add(markerEntry.getKey());
}
}
// Remove from the map as well
for (final String removeKey : toBeRemovedKeys) {
if (TextUtils.equals(removeKey, mCurrentlyFollowing)) {
mCurrentlyFollowing = null;
mCurrentlyFollowingLocation = null;
}
mMarkerMap.remove(removeKey);
}
return mCurrentlyFollowing == null;
}
@Override
public void applyDayNightMode(final boolean isNightMode) {
final Context context = mContextRef.get();
if (context == null) {
Log.w(TAG, "applyDayNightMode: Context is null, cannot continue");
return;
}
if (isNightMode) {
mMapFragment.setMapStyle(MapStyleOptions.loadRawResourceStyle(context, R.raw.map_style_night));
} else {
mMapFragment.setMapStyle(MapStyleOptions.loadRawResourceStyle(context, R.raw.map_style_day));
}
}
@Override
public void animateCamera(final LocationObject locationObject) {
mMapFragment.animateCamera(CameraUpdateFactory.newCameraPosition(new CameraPosition(
new LatLng(locationObject.getLatitude(), locationObject.getLongitude()),
DEFAULT_ZOOM_LEVEL,
DEFAULT_TILT,
locationObject.getBearing())));
}
@Override
public void registerMapMovementListener(final IMapMovementListener mapMovementListener) {
super.registerMapMovementListener(mapMovementListener);
mMapFragment.setOnCameraMoveStartedListener(reason -> {
if (GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == reason
&& !TextUtils.isEmpty(mCurrentlyFollowing)) {
Log.d(TAG, "onCameraMoveStarted: User moved map...");
mMapMovementListener.onMapMovementDetected();
}
});
}
}

View File

@@ -0,0 +1,276 @@
package com.aldo.apps.familyhelpers.ui.map.mapbox;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_HOME_LATITUDE;
import static com.aldo.apps.familyhelpers.utils.GlobalConstants.DEFAULT_HOME_LONGITUDE;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.aldo.apps.familyhelpers.model.LocationObject;
import com.aldo.apps.familyhelpers.model.User;
import com.aldo.apps.familyhelpers.ui.map.AbstractMap;
import com.aldo.apps.familyhelpers.ui.map.IMapMovementListener;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.mapbox.android.gestures.MoveGestureDetector;
import com.mapbox.geojson.Point;
import com.mapbox.maps.CameraOptions;
import com.mapbox.maps.MapView;
import com.mapbox.maps.plugin.Plugin;
import com.mapbox.maps.plugin.PuckBearing;
import com.mapbox.maps.plugin.annotation.AnnotationConfig;
import com.mapbox.maps.plugin.annotation.AnnotationManager;
import com.mapbox.maps.plugin.annotation.AnnotationPlugin;
import com.mapbox.maps.plugin.annotation.AnnotationType;
import com.mapbox.maps.plugin.annotation.generated.PointAnnotation;
import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager;
import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions;
import com.mapbox.maps.plugin.gestures.GesturesPlugin;
import com.mapbox.maps.plugin.gestures.OnMoveListener;
import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin;
import com.mapbox.maps.plugin.viewport.ViewportPlugin;
import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions;
import com.mapbox.maps.plugin.viewport.state.FollowPuckViewportState;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* MapBox specific implementation of the {@link AbstractMap}.
*/
public class MapBoxView extends AbstractMap<MapView> implements OnMoveListener {
/**
* Tag for debugging purpose.
*/
private static final String TAG = "MapBoxView";
/**
* The default zoom level.
*/
private static final double DEFAULT_ZOOM_LEVEL = 15.0;
/**
* {@link Map} holding the mapping of sharing ID to {@link PointAnnotation}.
*/
private final Map<String, PointAnnotation> mMarkerMap = new HashMap<>();
/**
* The {@link LocationComponentPlugin} to enable specific UI features.
*/
private final LocationComponentPlugin mLocCompPlugin;
/**
* The {@link ViewportPlugin} to follow own location.
*/
private final ViewportPlugin mViewportPlugin;
/**
* The {@link GesturesPlugin} to listen to ptential swipes.
*/
private final GesturesPlugin mGesturesPlugin;
/**
* The {@link AnnotationPlugin} to add/remove markers.
*/
private final AnnotationPlugin mAnnotationPlugin;
/**
* The {@link AnnotationManager} to actually display markers.
*/
private final PointAnnotationManager mAnnotationManager;
/**
* The ID of the currently logged in user.
*/
private final String mCurrentUserId;
/**
* C'Tor.
*
* @param mapView The inflated Map-Fragment.
* @param context The calling {@link Context}.
*/
public MapBoxView(final MapView mapView, final Context context) {
super(mapView, context);
mLocCompPlugin = mapView.getPlugin(Plugin.MAPBOX_LOCATION_COMPONENT_PLUGIN_ID);
mViewportPlugin = mapView.getPlugin(Plugin.MAPBOX_VIEWPORT_PLUGIN_ID);
mGesturesPlugin = mapView.getPlugin(Plugin.MAPBOX_GESTURES_PLUGIN_ID);
mAnnotationPlugin = mapView.getPlugin(Plugin.MAPBOX_ANNOTATION_PLUGIN_ID);
mCurrentUserId = FirebaseAuth.getInstance().getCurrentUser().getUid();
if (mAnnotationPlugin != null) {
mAnnotationManager = (PointAnnotationManager) mAnnotationPlugin.createAnnotationManager(
AnnotationType.PointAnnotation, new AnnotationConfig());
} else {
Log.d(TAG, "MapBoxView: AnnotationManager is null, no markers will be set.");
mAnnotationManager = null;
}
if (mLocCompPlugin != null) {
mLocCompPlugin.setPulsingEnabled(true);
}
}
@Override
public void zoomOnDefaultLocation() {
animateCamera(mCurrentlyFollowingLocation);
}
@Override
public void startFollowingAndCenterCamera() {
if (mCurrentlyFollowingLocation == null || TextUtils.equals(mCurrentlyFollowingLocation.getShareId(), mCurrentUserId)) {
displayOwnLocation();
} else {
animateCamera(mCurrentlyFollowingLocation);
}
}
@Override
public void displayOwnLocation() {
if (mViewportPlugin != null) {
final FollowPuckViewportState state = mViewportPlugin.makeFollowPuckViewportState(new FollowPuckViewportStateOptions.Builder()
.zoom(DEFAULT_ZOOM_LEVEL)
.build());
mViewportPlugin.transitionTo(state, mViewportPlugin.getDefaultTransition(),
done -> Log.d(TAG, "Movement finished = [" + done + "]"));
}
}
@Override
public void clearAllMarkers() {
if (mAnnotationManager != null) {
mAnnotationManager.deleteAll();
mMarkerMap.clear();
}
}
@Override
public LocationObject updateOrAddCustomMarker(final Map<String, LocationObject> locationObjectMap) {
final Context context = mContextRef.get();
if (context == null) {
Log.w(TAG, "updateOrAddCustomMarker: No valid context, cannot continue.");
return null;
}
for (final Map.Entry<String, LocationObject> locationSet : locationObjectMap.entrySet()) {
final LocationObject newReceived = locationSet.getValue();
final String sharingId = locationSet.getKey();
final User locationUser = mDbHelper.getUserForId(sharingId);
if (mMarkerMap.containsKey(sharingId)) {
final PointAnnotation existing = mMarkerMap.get(sharingId);
if (existing == null) {
Log.w(TAG, "updateOrAddCustomMarker: Marker somehow not existing, skip");
continue;
}
Log.d(TAG, "updateOrAddCustomMarker: Marker still available, update position");
existing.setPoint(Point.fromLngLat(newReceived.getLongitude(), newReceived.getLatitude()));
}else if (TextUtils.equals(locationUser.getuId(), mCurrentUserId)) {
Log.d(TAG, "updateOrAddCustomMarker: Marker to be added is from own user, no need to add");
} else {
Log.d(TAG, "updateOrAddCustomMarker: New Marker to be created.");
Glide.with(context)
.asBitmap()
.circleCrop()
.load(locationUser.getPhotoUrl())
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull final Bitmap resource,
@Nullable final Transition<? super Bitmap> transition) {
final PointAnnotationOptions newMarker = new PointAnnotationOptions()
.withPoint(Point.fromLngLat(newReceived.getLongitude(), newReceived.getLatitude()))
.withIconImage(resource);
final PointAnnotation pointAnnotation = mAnnotationManager.create(newMarker);
mMarkerMap.put(sharingId, pointAnnotation);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
}
if (TextUtils.equals(mCurrentlyFollowing, sharingId)) {
mCurrentlyFollowingLocation = locationSet.getValue();
if (mCurrentlyFollowing != null) {
startFollowingAndCenterCamera();
}
}
}
return null;
}
@Override
public boolean removeInvalidExistingMarkers(final Map<String, LocationObject> locationObjectMap) {
final List<String> toBeRemovedKeys = new ArrayList<>();
for (Map.Entry<String, PointAnnotation> markerEntry : mMarkerMap.entrySet()) {
if (!locationObjectMap.containsKey(markerEntry.getKey())) {
Log.d(TAG, "removeInvalidExistingMarkers: Marker not valid anymore, remove");
mAnnotationManager.delete(markerEntry.getValue());
toBeRemovedKeys.add(markerEntry.getKey());
}
}
// Remove from the map as well
for (final String removeKey : toBeRemovedKeys) {
if (TextUtils.equals(removeKey, mCurrentlyFollowing)) {
mCurrentlyFollowing = null;
mCurrentlyFollowingLocation = null;
}
mMarkerMap.remove(removeKey);
}
return mCurrentlyFollowing == null;
}
@Override
public void animateCamera(final LocationObject locationObject) {
if (locationObject == null) {
Log.w(TAG, "animateCamera: Invalid LocationObject, skip update");
return;
}
mMapFragment.getMapboxMap().setCamera(
new CameraOptions.Builder()
.center(Point.fromLngLat(locationObject.getLongitude(), locationObject.getLatitude()))
.zoom(DEFAULT_ZOOM_LEVEL)
.bearing((double) locationObject.getBearing())
.build()
);
}
@Override
public void registerMapMovementListener(IMapMovementListener mapMovementListener) {
super.registerMapMovementListener(mapMovementListener);
if (mGesturesPlugin != null) {
mGesturesPlugin.addOnMoveListener(this);
}
}
@Override
public boolean onMove(@NonNull final MoveGestureDetector moveGestureDetector) {
return false;
}
@Override
public void onMoveBegin(@NonNull final MoveGestureDetector moveGestureDetector) {
if (mMapMovementListener != null) {
mMapMovementListener.onMapMovementDetected();
}
}
@Override
public void onMoveEnd(@NonNull final MoveGestureDetector moveGestureDetector) {
// Do nothing.
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:mapbox="http://schemas.android.com/apk/res-auto"
tools:context=".ShareLocationActivity">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -36,26 +37,39 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_compass"
app:layout_constraintBottom_toBottomOf="@id/map"
app:layout_constraintEnd_toEndOf="@id/map"
app:layout_constraintBottom_toBottomOf="@id/mapbox"
app:layout_constraintEnd_toEndOf="@id/mapbox"
android:layout_marginBottom="25dp"
android:layout_marginEnd="25dp"
android:visibility="gone"/>
<com.mapbox.maps.MapView
android:id="@+id/mapbox"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/active_share_container"
app:layout_constraintBottom_toTopOf="@id/share_location_info_box"
app:mapbox_locationComponentEnabled="true"
app:mapbox_locationComponentPuckBearing="course"
mapbox:mapbox_styleUri="@string/map_box_style_uri" />
<!--
<androidx.fragment.app.FragmentContainerView
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/active_share_container"
app:layout_constraintBottom_toTopOf="@id/share_location_info_box"/>
app:layout_constraintBottom_toTopOf="@id/share_location_info_box"
android:visibility="gone"/>
-->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_start_stop_sharing_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/map"
app:layout_constraintEnd_toEndOf="@id/map"
app:layout_constraintTop_toTopOf="@id/mapbox"
app:layout_constraintEnd_toEndOf="@id/mapbox"
android:text="@string/share_location_toggle_button"/>
<androidx.constraintlayout.widget.ConstraintLayout

View File

@@ -48,6 +48,7 @@
<item name="colorSurfaceContainerHigh">@color/md_theme_surfaceContainerHigh</item>
<item name="colorSurfaceContainerHighest">@color/md_theme_surfaceContainerHighest</item>
</style>
<string name="map_box_style_uri" translatable="false">mapbox://styles/mapbox/navigation-night-v1</string>
<style name="HelperGroupTileStyke" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/helperGroupTileBackground_NightMode</item>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="mapbox_access_token" translatable="false" tools:ignore="UnusedResources">sk.eyJ1IjoiemFuZGVyMTYwOSIsImEiOiJjbTl0bDE4Y3cwMDB1MmxyMHRmd3hnNng5In0.Je5tLeuVjl8Y2-ssvqXxLg</string>
</resources>

View File

@@ -49,5 +49,7 @@
<item name="colorSurfaceContainerHighest">@color/md_theme_surfaceContainerHighest</item>
</style>
<string name="map_box_style_uri" translatable="false">mapbox://styles/mapbox/navigation-day-v1</string>
<style name="Theme.MyApplication" parent="Base.Theme.MyApplication" />
</resources>

View File

@@ -9,5 +9,8 @@ allprojects {
google()
mavenCentral()
maven { url "https://jitpack.io" }
maven {
url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
}
}
}

View File

@@ -10,6 +10,9 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
}
}
}