package org.thoughtcrime.securesms.map; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.util.Log; import com.b44t.messenger.DcArray; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcEventCenter; import com.google.gson.JsonObject; import com.mapbox.geojson.Feature; import com.mapbox.geojson.FeatureCollection; import com.mapbox.geojson.LineString; import com.mapbox.geojson.Point; import com.mapbox.mapboxsdk.maps.Style; import com.mapbox.mapboxsdk.style.expressions.Expression; import com.mapbox.mapboxsdk.style.layers.LineLayer; import com.mapbox.mapboxsdk.style.layers.Property; import com.mapbox.mapboxsdk.style.layers.PropertyFactory; import com.mapbox.mapboxsdk.style.layers.SymbolLayer; import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.R; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import static com.b44t.messenger.DcContext.DC_EVENT_LOCATION_CHANGED; import static com.mapbox.mapboxsdk.style.expressions.Expression.get; import static com.mapbox.mapboxsdk.style.expressions.Expression.literal; import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase; import static com.mapbox.mapboxsdk.style.expressions.Expression.toBool; import static com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize; /** * Created by cyberta on 07.03.19. */ public class MapDataManager implements DcEventCenter.DcEventDelegate { public static final String MARKER_SELECTED = "MARKER_SELECTED"; public static final String CONTACT_ID = "CONTACT_ID"; public static final String TIMESTAMP = "TIMESTAMP"; public static final String MESSAGE_ID = "MESSAGE_ID"; public static final String ACCURACY = "ACCURACY"; private static final String TAG = MapDataManager.class.getSimpleName(); private Style mapboxStyle; private HashMap contactMapSources; private HashMap> featureCollections; private Feature selectedFeature; private int chatId; private Context context; public MapDataManager(Context context, @NonNull Style mapboxMapStyle, int chatId) { this.mapboxStyle = mapboxMapStyle; this.context = context; this.chatId = chatId; contactMapSources = new HashMap<>(); featureCollections = new HashMap<>(); int[] contactIds = ApplicationContext.getInstance(context).dcContext.getChatContacts(chatId); for (int contactId : contactIds) { addContactMapSource(contactId); updateSource(contactId); } } public void onResume() { ApplicationContext.getInstance(context).dcContext.eventCenter.addObserver(DC_EVENT_LOCATION_CHANGED, this); updateSources(); } public void onPause() { ApplicationContext.getInstance(context).dcContext.eventCenter.removeObserver(DC_EVENT_LOCATION_CHANGED, this); } public void addContactMapSource(int contactId) { DcContact contact = ApplicationContext.getInstance(context).dcContext.getContact(contactId); MapSource contactMapSource = new MapSource(contactId); contactMapSource.setColor(contact.getColor()); contactMapSources.put(contactId, contactMapSource); initGeoJsonSources(contactMapSource); initLayers(contactMapSource); } private Bitmap generateColoredLastPositionIcon(int colorFilter) { return generateColoredBitmap(colorFilter, R.drawable.ic_location_on_white_24dp); } private Bitmap generateColoredLocationIcon(int colorFilter) { return generateColoredBitmap(colorFilter, R.drawable.ic_location_dot); } private Bitmap generateColoredBitmap(int colorFilter, @DrawableRes int res) { Bitmap icon = getBitmap(res); Paint paint = new Paint(); ColorFilter filter = new PorterDuffColorFilter(colorFilter, PorterDuff.Mode.SRC_IN); paint.setColorFilter(filter); Canvas canvas = new Canvas(icon); canvas.drawBitmap(icon, 0, 0, paint); return icon; } private Bitmap getBitmap(@DrawableRes int res) { Drawable drawable = ContextCompat.getDrawable(context, res); Canvas canvas = new Canvas(); Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); canvas.setBitmap(bitmap); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); drawable.draw(canvas); return bitmap; } private void updateSources() { for (Integer contactId : contactMapSources.keySet()) { updateSource(contactId); } } private void refreshSource(int contactId) { MapSource source = contactMapSources.get(contactId); ArrayList collection = featureCollections.get(source.getMarkerFeatureCollection()); GeoJsonSource pointSource = (GeoJsonSource) mapboxStyle.getSource(source.getMarkerSource()); pointSource.setGeoJson(FeatureCollection.fromFeatures(collection)); } private void updateSource(int contactId) { DcArray locations = ApplicationContext.getInstance(context).dcContext.getLocations(chatId, contactId); int count = locations.getCnt(); if (count == 0) { return; } ArrayList coordinateList = new ArrayList<>(); ArrayList pointFeatureList = new ArrayList<>(); MapSource contactMapMetadata = contactMapSources.get(contactId); for (int i = 0; i < count; i++) { Point p = Point.fromLngLat(locations.getLongitude(i), locations.getLatitude(i)); coordinateList.add(p); Feature pointFeature = Feature.fromGeometry(p, new JsonObject(), contactId+"_"+i); pointFeature.addBooleanProperty(MARKER_SELECTED, false); pointFeature.addNumberProperty(CONTACT_ID, contactId); pointFeature.addNumberProperty(TIMESTAMP, locations.getTimestamp(i)); pointFeature.addNumberProperty(MESSAGE_ID, locations.getMsgId(i)); pointFeature.addNumberProperty(ACCURACY, locations.getAccuracy(i)); pointFeatureList.add(pointFeature); } FeatureCollection pointFeatureCollection = FeatureCollection.fromFeatures(pointFeatureList); FeatureCollection lineFeatureCollection = FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry( LineString.fromLngLats(coordinateList) )}); GeoJsonSource lineSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getLineSource()); lineSource.setGeoJson(lineFeatureCollection); GeoJsonSource pointSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getMarkerSource()); pointSource.setGeoJson(pointFeatureCollection); GeoJsonSource lastPostionSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getLastPositionSource()); lastPostionSource.setGeoJson(pointFeatureList.get(0)); featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), pointFeatureList); } private void initGeoJsonSources(MapSource source) { GeoJsonSource lastPositionSource = new GeoJsonSource(source.getLastPositionSource()); GeoJsonSource markerPositionSource = new GeoJsonSource(source.getMarkerSource()); GeoJsonSource linePositionSource = new GeoJsonSource(source.getLineSource()); try { mapboxStyle.addSource(lastPositionSource); mapboxStyle.addSource(markerPositionSource); mapboxStyle.addSource(linePositionSource); } catch (RuntimeException e) { //TODO: specify exception more Log.e(TAG, "Unable to init GeoJsonSources. Already added to mapBoxMap? " + e.getMessage()); } } private void initLayers(MapSource source) { mapboxStyle.addImage(source.getMarkerLastPositon(), generateColoredLastPositionIcon(source.getColorArgb())); mapboxStyle.addImage(source.getMarkerIcon(), generateColoredLocationIcon(source.getColorArgb())); Expression markerSizeExpression = switchCase(toBool(get(MARKER_SELECTED)), literal(1.5f), literal(1.0f)); mapboxStyle.addLayer(new SymbolLayer(source.getMarkerLayer(), source.getMarkerSource()) .withProperties( iconImage(source.getMarkerIcon()), iconSize(markerSizeExpression)) ); mapboxStyle.addLayer(new SymbolLayer(source.getLastPositionLayer(), source.getLastPositionSource()) .withProperties( iconImage(source.getMarkerLastPositon()), iconAnchor(ICON_ANCHOR_BOTTOM), iconSize(markerSizeExpression)) ); mapboxStyle.addLayer(new LineLayer(source.getLineLayer(), source.getLineSource()) .withProperties(PropertyFactory.lineCap(Property.LINE_CAP_ROUND), PropertyFactory.lineJoin(Property.LINE_JOIN_ROUND), PropertyFactory.lineWidth(3f), PropertyFactory.lineColor(source.getColorArgb()))); } @Override public void handleEvent(int eventId, Object data1, Object data2) { int contactId = (Integer) data1; if (contactMapSources.containsKey(contactId)) { updateSource(contactId); } } @Override public boolean runOnMain() { return false; } public String[] getMarkerLayers() { String markerLayers[] = new String[contactMapSources.size()*2]; int i = 0; for (Map.Entry entry : contactMapSources.entrySet()) { markerLayers[i] = entry.getValue().getMarkerLayer(); markerLayers[i+1] = entry.getValue().getLastPositionLayer(); i += 2; } return markerLayers; } public void unselectMarker() { if (selectedFeature != null) { selectedFeature.addBooleanProperty(MARKER_SELECTED, false); refreshSource(selectedFeature.getNumberProperty(CONTACT_ID).intValue()); selectedFeature = null; } } public void setMarkerSelected(String featureId) { if (selectedFeature == null) { setNewMarkerSelected(featureId); } else if (selectedFeature.id().equals(featureId)) { updateSelectedMarker(); } else { replaceSelectedMarker(featureId); } } private void replaceSelectedMarker(String featureId) { Feature feature = getFeatureWithId(featureId); feature.addBooleanProperty(MARKER_SELECTED, true); selectedFeature.addBooleanProperty(MARKER_SELECTED, false); int lastContactId = selectedFeature.getNumberProperty(CONTACT_ID).intValue(); int currentContactId = feature.getNumberProperty(CONTACT_ID).intValue(); selectedFeature = feature; refreshSource(currentContactId); if (lastContactId != currentContactId) { refreshSource(lastContactId); } } private void updateSelectedMarker() { boolean isSelected = selectedFeature.getBooleanProperty(MARKER_SELECTED); selectedFeature.addBooleanProperty(MARKER_SELECTED, !isSelected); refreshSource(selectedFeature.getNumberProperty(CONTACT_ID).intValue()); } private void setNewMarkerSelected(String featureId) { Feature feature = getFeatureWithId(featureId); feature.addBooleanProperty(MARKER_SELECTED, true); selectedFeature = feature; refreshSource(selectedFeature.getNumberProperty(CONTACT_ID).intValue()); } private Feature getFeatureWithId(String id) { for (String key : featureCollections.keySet()) { ArrayList featureCollection = featureCollections.get(key); for (Feature f : featureCollection) { if (f.id().equals(id)) { return f; } } } return null; } }