package mil.nga.giat.mage.map.marker; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; import com.bumptech.glide.request.target.CustomViewTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.InfoWindowAdapter; import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import org.ocpsoft.prettytime.PrettyTime; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; import mil.nga.giat.mage.R; import mil.nga.giat.mage.filter.Filter; import mil.nga.giat.mage.glide.GlideApp; import mil.nga.giat.mage.glide.model.Avatar; import mil.nga.giat.mage.sdk.Temporal; import mil.nga.giat.mage.sdk.datastore.location.Location; import mil.nga.giat.mage.sdk.datastore.location.LocationProperty; import mil.nga.giat.mage.sdk.datastore.user.User; import mil.nga.sf.Geometry; import mil.nga.sf.Point; import mil.nga.sf.util.GeometryUtils; public class LocationMarkerCollection implements PointCollection<Pair<Location, User>>, OnMarkerClickListener { private static final String LOG_NAME = LocationMarkerCollection.class.getName(); protected GoogleMap map; protected Context context; protected Date latestLocationDate = new Date(0); protected Long clickedAccuracyCircleUserId; protected Circle clickedAccuracyCircle; protected InfoWindowAdapter infoWindowAdapter = new LocationInfoWindowAdapter(); protected boolean visible = true; protected Map<Long, Marker> userIdToMarker = new HashMap<>(); protected Map<String, Pair<Location, User>> markerIdToPair = new HashMap<>(); public LocationMarkerCollection(Context context, GoogleMap map) { this.context = context; this.map = map; } @Override public void add(MarkerOptions options, Pair<Location, User> pair) { Location location = pair.first; User user = pair.second; final Geometry g = location.getGeometry(); if (g != null) { // one user has one location Marker marker = userIdToMarker.get(user.getId()); if (marker != null) { markerIdToPair.remove(marker.getId()); marker.remove(); if (clickedAccuracyCircleUserId != null && clickedAccuracyCircleUserId.equals(user.getId())) { if (clickedAccuracyCircle != null) { clickedAccuracyCircle.remove(); clickedAccuracyCircle = null; } } } options.visible(visible); marker = map.addMarker(options); userIdToMarker.put(user.getId(), marker); markerIdToPair.put(marker.getId(), pair); if (location.getTimestamp().after(latestLocationDate)) { latestLocationDate = location.getTimestamp(); } } } // TODO: this should preserve latestLocationDate @Override public void remove(Pair<Location, User> pair) { Marker marker = userIdToMarker.remove(pair.second.getId()); if (marker != null) { markerIdToPair.remove(marker.getId()); marker.remove(); if (clickedAccuracyCircle != null && pair.second.getId().equals(clickedAccuracyCircleUserId)) { clickedAccuracyCircle.remove(); clickedAccuracyCircle = null; } } } @Override public Pair<Location, User> pointForMarker(Marker marker) { return markerIdToPair.get(marker.getId()); } @Override public boolean onMarkerClick(Marker marker) { Pair<Location, User> pair = markerIdToPair.get(marker.getId()); if (pair == null) { return false; } Location location = pair.first; User user = pair.second; final Geometry g = location.getGeometry(); if (g != null) { Point point = GeometryUtils.getCentroid(g); LatLng latLng = new LatLng(point.getY(), point.getX()); LocationProperty accuracyProperty = location.getPropertiesMap().get("accuracy"); if (accuracyProperty != null && !accuracyProperty.getValue().toString().trim().isEmpty()) { try { float accuracy = Float.parseFloat(accuracyProperty.getValue().toString()); if (clickedAccuracyCircle != null) { clickedAccuracyCircle.remove(); } int color = LocationBitmapFactory.locationColor(context, location); clickedAccuracyCircle = map.addCircle(new CircleOptions() .center(latLng) .radius(accuracy) .fillColor(ColorUtils.setAlphaComponent(color, (int) (256 * .20))) .strokeColor(ColorUtils.setAlphaComponent(color, (int) (256 * .87))) .strokeWidth(2.0f)); clickedAccuracyCircleUserId = user.getId(); } catch (NumberFormatException nfe) { Log.e(LOG_NAME, "Problem adding accuracy circle to the map.", nfe); } } } map.setInfoWindowAdapter(infoWindowAdapter); // make sure to set the Anchor after this call as well, because the size of the icon might have changed marker.setIcon(LocationBitmapFactory.bitmapDescriptor(context, location, user)); marker.setAnchor(0.5f, 1.0f); marker.showInfoWindow(); return true; } @Override public void offMarkerClick() { if (clickedAccuracyCircle != null) { clickedAccuracyCircle.remove(); clickedAccuracyCircle = null; } } @Override public void refresh(Pair<Location, User> pair) { // TODO Maybe a different generic for this case // TODO implementing room might help solve this problem // In this case I know just the user is coming in // grab the location based on that User user = pair.second; Marker marker = userIdToMarker.get(pair.second.getId()); if (marker != null) { Location location = markerIdToPair.get(marker.getId()).first; markerIdToPair.put(marker.getId(), new Pair(location, user)); marker.setIcon(LocationBitmapFactory.bitmapDescriptor(context, location, user)); } } @Override public void refreshMarkerIcons(Filter<Temporal> filter) { Collection<Marker> markers = new ArrayList<>(userIdToMarker.values()); for (Marker m : markers) { Pair<Location, User> pair = markerIdToPair.get(m.getId()); Location location = pair.first; User user = pair.second; if (location != null) { if (filter != null && !filter.passesFilter(location)) { remove(pair); } else { boolean showWindow = m.isInfoWindowShown(); try { // make sure to set the Anchor after this call as well, because the size of the icon might have changed m.setIcon(LocationBitmapFactory.bitmapDescriptor(context, location, user)); m.setAnchor(0.5f, 1.0f); } catch (Exception ue) { Log.e(LOG_NAME, "Error refreshing the icon for user: " + user.getId(), ue); } if (showWindow) { m.showInfoWindow(); } } } } } @Override public void onMapClick(LatLng latLng) { } @Override public int count() { return userIdToMarker.size(); } @Override public void clear() { for (Marker marker : userIdToMarker.values()) { marker.remove(); } if (clickedAccuracyCircle != null) { clickedAccuracyCircle.remove(); clickedAccuracyCircle = null; } userIdToMarker.clear(); markerIdToPair.clear(); latestLocationDate = new Date(0); } @Override public void onCameraIdle() { // Don't care about this, I am not clustered } @Override public void setVisibility(boolean visible) { if (this.visible == visible) return; this.visible = visible; for (Marker m : userIdToMarker.values()) { m.setVisible(visible); } if (clickedAccuracyCircle != null) { clickedAccuracyCircle.setVisible(visible); } } @Override public boolean isVisible() { return this.visible; } @Override public Date getLatestDate() { return latestLocationDate; } private class LocationInfoWindowAdapter implements InfoWindowAdapter { private final Map<Long, Bitmap> avatars = new HashMap<>(); private final Map<Long, Target<Bitmap>> targets = new HashMap<>(); @Override public View getInfoWindow(final Marker marker) { Pair<Location, User> pair = markerIdToPair.get(marker.getId()); final Location location = pair.first; final User user = pair.second; if (user == null || location == null) { return null; } LayoutInflater inflater = LayoutInflater.from(context); final View v = inflater.inflate(R.layout.people_info_window, null); TextView name = v.findViewById(R.id.name); name.setText(user.getDisplayName()); TextView date = v.findViewById(R.id.date); date.setText(new PrettyTime().format(location.getTimestamp())); final ImageView avatarView = v.findViewById(R.id.avatarImageView); Bitmap avatar = avatars.get(user.getId()); if (avatar == null) { GlideApp.with(context) .asBitmap() .load(Avatar.Companion.forUser(user)) .placeholder(R.drawable.ic_person_gray_24dp) .circleCrop() .into(getTarget(avatarView, marker, user)); } else { avatarView.setImageBitmap(avatar); } return v; } @Override public View getInfoContents(Marker marker) { return null; // Use default info window } private Target<Bitmap> getTarget(ImageView view, Marker marker, User user) { Target<Bitmap> target = targets.get(user.getId()); if (target == null) { target = new AvatarTarget(view, marker, user); targets.put(user.getId(), target); } return target; } private class AvatarTarget extends CustomViewTarget<ImageView, Bitmap> { Marker marker; User user; AvatarTarget(ImageView view, Marker marker, User user) { super(view); this.marker = marker; this.user = user; } @Override protected void onResourceCleared(@Nullable Drawable placeholder) { avatars.remove(user.getId()); } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { } @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { avatars.put(user.getId(), resource); marker.showInfoWindow(); } } } }