diff --git a/build.gradle b/build.gradle
index 3bfdefcac..61d10c7b5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -64,6 +64,7 @@ dependencies {
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' // glues the current time segment text in the gallery to the top.
implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:7.2.0'
+ implementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.mockito:mockito-core:1.9.5'
diff --git a/res/layout/map_bubble_layout.xml b/res/layout/map_bubble_layout.xml
index 19e663e55..bd3c6fc4f 100644
--- a/res/layout/map_bubble_layout.xml
+++ b/res/layout/map_bubble_layout.xml
@@ -17,33 +17,6 @@
android:background="@color/white"
tools:backgroundTint="@color/core_light_10">
-
-
-
-
-
-
-
+ android:orientation="horizontal">
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/menu/text_secure_normal.xml b/res/menu/text_secure_normal.xml
index 4f12ae3ed..f65d02225 100644
--- a/res/menu/text_secure_normal.xml
+++ b/res/menu/text_secure_normal.xml
@@ -19,4 +19,7 @@
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1981447ab..3c8dfe476 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -112,6 +112,7 @@
Toggle emoji keyboard
Edit group
Show map
+ Show all locations
Archive chat
Unarchive chat
Add attachment
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index 4ac442105..2af1c0247 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -63,7 +63,6 @@ import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEventCenter;
import com.b44t.messenger.DcMsg;
-import org.thoughtcrime.securesms.map.MapActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
@@ -83,6 +82,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
import org.thoughtcrime.securesms.connect.DcHelper;
+import org.thoughtcrime.securesms.map.MapActivity;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide;
diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java
index 407d6f617..cf43d9cca 100644
--- a/src/org/thoughtcrime/securesms/ConversationListActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java
@@ -27,18 +27,19 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import com.b44t.messenger.DcChat;
-import com.b44t.messenger.DcContext;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import org.thoughtcrime.securesms.components.SearchToolbar;
-import org.thoughtcrime.securesms.connect.DcHelper;
+import org.thoughtcrime.securesms.map.MapActivity;
import org.thoughtcrime.securesms.qr.QrScanHandler;
import org.thoughtcrime.securesms.search.SearchFragment;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
+import static org.thoughtcrime.securesms.map.MapDataManager.ALL_CHATS_GLOBAL_MAP;
+
public class ConversationListActivity extends PassphraseRequiredActionBarActivity
implements ConversationListFragment.ConversationSelectedListener
{
@@ -144,11 +145,18 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
case R.id.menu_qr_scan: handleQrScan(); return true;
case R.id.menu_qr_show: handleQrShow(); return true;
case R.id.menu_deaddrop: handleDeaddrop(); return true;
+ case R.id.menu_global_map: handleShowMap(); return true;
}
return false;
}
+ private void handleShowMap() {
+ Intent intent = new Intent(this, MapActivity.class);
+ intent.putExtra(MapActivity.CHAT_IDS, ALL_CHATS_GLOBAL_MAP);
+ startActivity(intent);
+ }
+
private void handleQrScan() {
new IntentIntegrator(this).setCaptureActivity(QrScanActivity.class).initiateScan();
}
diff --git a/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java b/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java
index 6a6068042..60b900d88 100644
--- a/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java
+++ b/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java
@@ -18,6 +18,7 @@ import android.webkit.MimeTypeMap;
import android.widget.Toast;
import com.b44t.messenger.DcChat;
+import com.b44t.messenger.DcChatlist;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEventCenter;
@@ -36,12 +37,9 @@ import org.thoughtcrime.securesms.util.Util;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
-import java.io.BufferedWriter;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
@@ -720,4 +718,21 @@ public class ApplicationDcContext extends DcContext {
}
return 0;
}
+
+
+ /***********************************************************************************************
+ * core related helper methods
+ **********************************************************************************************/
+
+ public int[] getChatIds() {
+ DcChatlist chats = getChatlist(0, null, 0);
+ int count = chats.getCnt();
+ int[] chatIds = new int[count];
+ for (int i = 0; i < count; i++) {
+ DcChat dcChat = chats.getChat(i);
+ chatIds[i] = dcChat.getId();
+ }
+ return chatIds;
+ }
+
}
diff --git a/src/org/thoughtcrime/securesms/geolocation/DcLocationManager.java b/src/org/thoughtcrime/securesms/geolocation/DcLocationManager.java
index 30d4ec20d..d3307ab45 100644
--- a/src/org/thoughtcrime/securesms/geolocation/DcLocationManager.java
+++ b/src/org/thoughtcrime/securesms/geolocation/DcLocationManager.java
@@ -32,6 +32,7 @@ public class DcLocationManager implements Observer {
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.d(TAG, "background service connected");
serviceBinder = (LocationBackgroundService.LocationBackgroundServiceBinder) service;
while (pendingShareLastLocation.size() > 0) {
shareLastLocation(pendingShareLastLocation.pop());
@@ -40,6 +41,7 @@ public class DcLocationManager implements Observer {
@Override
public void onServiceDisconnected(ComponentName name) {
+ Log.d(TAG, "background service disconnected");
serviceBinder = null;
}
};
@@ -49,7 +51,7 @@ public class DcLocationManager implements Observer {
DcLocation.getInstance().addObserver(this);
DcChatlist chats = DcHelper.getContext(context).getChatlist(0, null, 0);
for (int i = 0; i < chats.getCnt(); i++) {
- if (DcHelper.getContext(context).isSendingLocationsToChat(chats.getChat(0).getId())) {
+ if (DcHelper.getContext(context).isSendingLocationsToChat(chats.getChat(i).getId())) {
initializeLocationEngine();
return;
}
@@ -102,6 +104,7 @@ public class DcLocationManager implements Observer {
DcHelper.getContext(context).deleteAllLocations();
}
+
@Override
public void update(Observable o, Object arg) {
if (o instanceof DcLocation) {
diff --git a/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java b/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java
index 42a6036e8..6f3699d9c 100644
--- a/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java
+++ b/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java
@@ -14,6 +14,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
+import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.mapbox.geojson.Feature;
@@ -32,6 +33,7 @@ import java.util.HashMap;
import java.util.Locale;
import static android.view.View.GONE;
+import static org.thoughtcrime.securesms.map.MapDataManager.CONTACT_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.INFO_WINDOW_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
@@ -78,6 +80,12 @@ public class GenerateInfoWindowTask extends AsyncTask, HashMa
TextView conversationItemBody = bubbleLayout.findViewById(R.id.conversation_item_body);
Locale locale = DynamicLanguage.getSelectedLocale(callbackRef.get().getContext());
int messageId = (int) feature.getNumberProperty(MESSAGE_ID);
+ int contactId = (int) feature.getNumberProperty(CONTACT_ID);
+
+ DcContact contact = DcHelper.getContext(callbackRef.get().getContext()).getContact(contactId);
+ TextView contactTextView = bubbleLayout.findViewById(R.id.message_sender);
+ contactTextView.setText(contact.getFirstName());
+
String msgText;
if (messageId != 0) {
DcContext dcContext = DcHelper.getContext(callbackRef.get().getContext());
diff --git a/src/org/thoughtcrime/securesms/map/MapActivity.java b/src/org/thoughtcrime/securesms/map/MapActivity.java
index 93d9a1cd7..a29cbe566 100644
--- a/src/org/thoughtcrime/securesms/map/MapActivity.java
+++ b/src/org/thoughtcrime/securesms/map/MapActivity.java
@@ -7,9 +7,9 @@ import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.util.Log;
+import com.b44t.messenger.DcMsg;
import com.mapbox.geojson.Feature;
import com.mapbox.mapboxsdk.camera.CameraPosition;
-import com.mapbox.mapboxsdk.camera.CameraUpdate;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.Style;
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Observable;
import java.util.Observer;
+import static com.b44t.messenger.DcChat.DC_CHAT_NO_CHAT;
import static org.thoughtcrime.securesms.map.MapDataManager.MARKER_SELECTED;
import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
@@ -34,6 +35,7 @@ public class MapActivity extends BaseActivity implements Observer {
public static final String TAG = MapActivity.class.getSimpleName();
public static final String CHAT_ID = "chat_id";
+ public static final String CHAT_IDS = "chat_id";
public static final String MAP_TAG = "org.thoughtcrime.securesms.map";
private DcLocation dcLocation;
@@ -44,7 +46,8 @@ public class MapActivity extends BaseActivity implements Observer {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map);
- int chatId = getIntent().getIntExtra(CHAT_ID, -1);
+ final int chatId = getIntent().getIntExtra(CHAT_ID, -1);
+
if (chatId == -1) {
finish();
return;
@@ -80,6 +83,7 @@ public class MapActivity extends BaseActivity implements Observer {
mapboxMap.easeCamera(CameraUpdateFactory.newLatLngBounds(latLngBounds, 50), 1000);
});
+
mapboxMap.addOnMapClickListener(point -> {
final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
Log.d(TAG, "on item clicked.");
@@ -99,15 +103,21 @@ public class MapActivity extends BaseActivity implements Observer {
mapboxMap.addOnMapClickListener(point -> {
final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
- Log.d(TAG, "on info window clicked.");
List features = mapboxMap.queryRenderedFeatures(pixel, mapDataManager.getInfoWindowLayers());
+ Log.d(TAG, "on info window clicked." + features.size());
+
for (Feature feature : features) {
Log.d(TAG, "found feature: " + feature.toJson());
if (feature.hasProperty(MARKER_SELECTED) && feature.getBooleanProperty(MARKER_SELECTED)) {
int messageId = feature.getNumberProperty(MESSAGE_ID).intValue();
+ DcMsg dcMsg = ApplicationContext.getInstance(this).dcContext.getMsg(messageId);
+ int dcMsgChatId = dcMsg.getChatId();
+ if (dcMsgChatId == DC_CHAT_NO_CHAT) {
+ continue;
+ }
- int msgs[] = DcHelper.getContext(MapActivity.this).getChatMsgs(chatId, 0, 0);
+ int msgs[] = DcHelper.getContext(MapActivity.this).getChatMsgs(dcMsgChatId, 0, 0);
int startingPosition = -1;
for(int i=0; i< msgs.length; i++ ) {
if(msgs[i] == messageId) {
@@ -171,4 +181,17 @@ public class MapActivity extends BaseActivity implements Observer {
//TODO: consider implementing a button -> center map to current location
}
}
+
+ private int[] getChatIds(Intent intent) {
+ if (intent == null) {
+ return new int[]{-1};
+ }
+
+ int[] chatIds = intent.getIntArrayExtra(CHAT_IDS);
+ if (chatIds == null || chatIds.length == 0) {
+ chatIds = new int[1];
+ chatIds[0] = getIntent().getIntExtra(CHAT_ID, -1);
+ }
+ return chatIds;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/map/MapDataManager.java b/src/org/thoughtcrime/securesms/map/MapDataManager.java
index a47556367..2cf377ea6 100644
--- a/src/org/thoughtcrime/securesms/map/MapDataManager.java
+++ b/src/org/thoughtcrime/securesms/map/MapDataManager.java
@@ -10,7 +10,6 @@ import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
@@ -33,14 +32,20 @@ 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 org.thoughtcrime.securesms.connect.ApplicationDcContext;
+import org.thoughtcrime.securesms.connect.DcHelper;
+import org.thoughtcrime.securesms.map.model.FeatureTreeSet;
+import org.thoughtcrime.securesms.map.model.MapSource;
+import org.thoughtcrime.securesms.map.model.TimeComparableFeature;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static com.b44t.messenger.DcContext.DC_EVENT_LOCATION_CHANGED;
+import static com.b44t.messenger.DcContext.DC_GCL_ADD_SELF;
import static com.mapbox.mapboxsdk.style.expressions.Expression.eq;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
@@ -65,13 +70,18 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
public static final String TIMESTAMP = "TIMESTAMP";
public static final String MESSAGE_ID = "MESSAGE_ID";
public static final String ACCURACY = "ACCURACY";
+ public static final int ALL_CHATS_GLOBAL_MAP = 0;
+ public static final long TIMESTAMP_NOW = 0L;
+ public static final long TIMEOUT = 3 * 60 * 60 * 1000;
private static final String TAG = MapDataManager.class.getSimpleName();
private Style mapboxStyle;
private HashMap contactMapSources;
- private HashMap> featureCollections;
+ private HashMap featureCollections;
private Feature selectedFeature;
private int chatId;
private Context context;
+ private ApplicationDcContext dcContext;
+ private boolean isInitial = true;
public interface MapDataState {
void onDataInitialized(LatLngBounds bounds);
@@ -80,26 +90,20 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
public MapDataManager(Context context, @NonNull Style mapboxMapStyle, int chatId, MapDataState updateCallback) {
this.mapboxStyle = mapboxMapStyle;
this.context = context;
+ this.dcContext = DcHelper.getContext(context);
this.chatId = chatId;
contactMapSources = new HashMap<>();
featureCollections = new HashMap<>();
LatLngBounds.Builder boundingBuilder = new LatLngBounds.Builder();
- int[] contactIds = ApplicationContext.getInstance(context).dcContext.getChatContacts(chatId);
+
+ int[] contactIds = getContactIds(chatId);
for (int contactId : contactIds) {
- if (contactId == 1) {
- //skip self, it is explicitely added as 1:1 don't include self whereas groups and selftalk do
- continue;
- }
- addContactMapSource(contactId);
- updateSource(contactId, boundingBuilder);
+ updateSource(chatId, contactId, boundingBuilder);
generateInfoWindows(contactId);
}
- addContactMapSource(1);
- updateSource(1, boundingBuilder);
- generateMissingInfoWindows(1);
-
+ dcContext.eventCenter.addObserver(DC_EVENT_LOCATION_CHANGED, this);
try {
updateCallback.onDataInitialized(boundingBuilder.build());
@@ -109,57 +113,15 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
}
public void onResume() {
- ApplicationContext.getInstance(context).dcContext.eventCenter.addObserver(DC_EVENT_LOCATION_CHANGED, this);
- updateSources();
+ dcContext.eventCenter.addObserver(DC_EVENT_LOCATION_CHANGED, this);
+ if (!isInitial) {
+ updateSources();
+ }
+ isInitial = false;
}
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);
- generateMissingInfoWindows(contactId);
- }
+ dcContext.eventCenter.removeObserver(DC_EVENT_LOCATION_CHANGED, this);
}
@Override
@@ -181,151 +143,24 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
public void refreshSource(int contactId) {
Log.d(TAG, "refreshSource start");
MapSource source = contactMapSources.get(contactId);
- ArrayList collection = featureCollections.get(source.getMarkerFeatureCollection());
+ FeatureTreeSet collection = featureCollections.get(source.getMarkerFeatureCollection());
GeoJsonSource pointSource = (GeoJsonSource) mapboxStyle.getSource(source.getMarkerSource());
- pointSource.setGeoJson(FeatureCollection.fromFeatures(collection));
+ pointSource.setGeoJson(FeatureCollection.fromFeatures(collection.getFeatureList()));
Log.d(TAG, "refreshSource finished");
}
- private void updateSource(int contactId) {
- updateSource(contactId, null);
- }
-
- private void updateSource(int contactId, @Nullable LatLngBounds.Builder boundingBuilder) {
- DcArray locations = ApplicationContext.getInstance(context).dcContext.getLocations(chatId, contactId, System.currentTimeMillis()-3*60*60*1000, 0);
- int count = locations.getCnt();
- ArrayList pointFeatureList = new ArrayList<>();
- MapSource contactMapMetadata = contactMapSources.get(contactId);
-
- if (count == 0) {
- featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), pointFeatureList);
- return;
- }
-
- ArrayList coordinateList = new ArrayList<>();
- 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.addBooleanProperty(LAST_LOCATION, false);
- pointFeature.addNumberProperty(CONTACT_ID, contactId);
- pointFeature.addStringProperty(INFO_WINDOW_ID, contactId + "_info_" + (count - 1 - i));
- pointFeature.addNumberProperty(TIMESTAMP, locations.getTimestamp(i));
- pointFeature.addNumberProperty(MESSAGE_ID, locations.getMsgId(i));
- pointFeature.addNumberProperty(ACCURACY, locations.getAccuracy(i));
- pointFeatureList.add(pointFeature);
- if (boundingBuilder != null) {
- boundingBuilder.include(new LatLng(locations.getLatitude(i), locations.getLongitude(i)));
- }
- }
-
- if (pointFeatureList.size() > 0) {
- pointFeatureList.get(0).addBooleanProperty(LAST_LOCATION, true);
- }
-
- 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);
- featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), pointFeatureList);
- }
-
- private void generateMissingInfoWindows(int contactId) {
- MapSource contactMapMetadata = contactMapSources.get(contactId);
- ArrayList featureList = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
- ArrayList missingWindows = new ArrayList<>();
-
- for (Feature f : featureList) {
- String infoWindowId = f.getStringProperty(INFO_WINDOW_ID);
- if (mapboxStyle.getImage(infoWindowId) == null) {
- Log.d(TAG, "create new infoWindow for " + infoWindowId);
- missingWindows.add(f);
- } else {
- // the list is ordered and thus, all older features should already have an info window
- break;
- }
- }
-
- if (missingWindows.size() > 0) {
- new GenerateInfoWindowTask(this, contactId).execute(missingWindows);
- }
- }
-
-
- private void generateInfoWindows(int contactId) {
- MapSource contactMapMetadata = contactMapSources.get(contactId);
- ArrayList featureList = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
- new GenerateInfoWindowTask(this, contactId).execute(featureList);
- }
-
- private void initGeoJsonSources(MapSource source) {
- GeoJsonSource markerPositionSource = new GeoJsonSource(source.getMarkerSource());
- GeoJsonSource linePositionSource = new GeoJsonSource(source.getLineSource());
-
- try {
- 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 markerSize = switchCase(toBool(get(MARKER_SELECTED)), literal(1.5f), literal(1.0f));
- Expression markerIcon = switchCase(toBool(get(LAST_LOCATION)), literal(source.getMarkerLastPositon()), literal(source.getMarkerIcon()));
- Expression allowOverlap = eq(get(LAST_LOCATION), literal(true));
- mapboxStyle.addLayer(new SymbolLayer(source.getMarkerLayer(), source.getMarkerSource())
- .withProperties(
- iconImage(markerIcon),
- iconSize(markerSize),
- iconAllowOverlap(allowOverlap))
- );
-
- 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())));
-
- Expression filterInfoWindow = eq((get(MARKER_SELECTED)), literal(true));
- Expression iconOffset = switchCase(toBool(get(LAST_LOCATION)), literal(new Float[] {-2f, -25f}), literal(new Float[] {-2f, -15f}));
- mapboxStyle.addLayer(new SymbolLayer(source.getInfoWindowLayer(), source.getMarkerSource()).
- withProperties(
- iconImage("{"+INFO_WINDOW_ID+"}"),
- iconAnchor(ICON_ANCHOR_BOTTOM_LEFT),
- /* all info window and marker image to appear at the same time*/
- iconAllowOverlap(true),
- /* offset the info window to be above the marker */
- iconOffset(iconOffset)
- ).withFilter(filterInfoWindow));
- }
-
@Override
public void handleEvent(int eventId, Object data1, Object data2) {
Log.d(TAG, "updateEvent in MapDataManager called. eventId: " + eventId);
- int contactId = (Integer) data1;
+ int contactId = ((Long) data1).intValue();
if (contactMapSources.containsKey(contactId)) {
- updateSource(contactId);
- generateMissingInfoWindows(contactId);
+ updateSource(chatId, contactId);
}
}
@Override
public boolean runOnMain() {
- return false;
+ return true;
}
@@ -369,6 +204,208 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
}
}
+ private void updateSource(int chatId, int contactId) {
+ updateSource(chatId, contactId, null);
+ }
+
+ private void updateSource(int chatId, int contactId, LatLngBounds.Builder boundingBuilder) {
+ DcArray locations = dcContext.getLocations(chatId, contactId, System.currentTimeMillis() - TIMEOUT, TIMESTAMP_NOW);
+ MapSource contactMapMetadata = contactMapSources.get(contactId);
+ if (contactMapMetadata == null) {
+ contactMapMetadata = addContactMapSource(contactId);
+ }
+
+ FeatureTreeSet sortedPointFeatures = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
+ if (sortedPointFeatures == null) {
+ sortedPointFeatures = new FeatureTreeSet();
+ }
+
+ int count = locations.getCnt();
+ for (int i = 0; i < count; i++) {
+ Point p = Point.fromLngLat(locations.getLongitude(i), locations.getLatitude(i));
+ Feature pointFeature = Feature.fromGeometry(p, new JsonObject(), String.valueOf(locations.getLocationId(i)));
+ pointFeature.addBooleanProperty(MARKER_SELECTED, false);
+ pointFeature.addBooleanProperty(LAST_LOCATION, false);
+ pointFeature.addNumberProperty(CONTACT_ID, contactId);
+ pointFeature.addStringProperty(INFO_WINDOW_ID, locations.getLocationId(i) + "_info_" + locations.getMsgId(i));
+ pointFeature.addNumberProperty(TIMESTAMP, locations.getTimestamp(i));
+ pointFeature.addNumberProperty(MESSAGE_ID, locations.getMsgId(i));
+ pointFeature.addNumberProperty(ACCURACY, locations.getAccuracy(i));
+ sortedPointFeatures.add(new TimeComparableFeature(pointFeature));
+
+ if (boundingBuilder != null) {
+ boundingBuilder.include(new LatLng(locations.getLatitude(i), locations.getLongitude(i)));
+ }
+ }
+
+ if (sortedPointFeatures.size() > 0) {
+ sortedPointFeatures.first().getFeature().addBooleanProperty(LAST_LOCATION, true);
+ }
+
+ FeatureCollection pointFeatureCollection = FeatureCollection.fromFeatures(sortedPointFeatures.getFeatureList());
+ FeatureCollection lineFeatureCollection = FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry(
+ LineString.fromLngLats(sortedPointFeatures.getPointList())
+ )});
+
+ GeoJsonSource lineSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getLineSource());
+ lineSource.setGeoJson(lineFeatureCollection);
+ GeoJsonSource pointSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getMarkerSource());
+ pointSource.setGeoJson(pointFeatureCollection);
+ featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), sortedPointFeatures);
+
+ generateMissingInfoWindows(contactId);
+ }
+
+ private void generateMissingInfoWindows(int contactId) {
+ MapSource contactMapMetadata = contactMapSources.get(contactId);
+ FeatureTreeSet featureList = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
+
+ ArrayList missingWindows = new ArrayList<>();
+ for (TimeComparableFeature tcf : featureList) {
+ String infoWindowId = tcf.getFeature().getStringProperty(INFO_WINDOW_ID);
+ if (mapboxStyle.getImage(infoWindowId) == null) {
+ Log.d(TAG, "create new infoWindow for " + infoWindowId);
+ missingWindows.add(tcf.getFeature());
+ } else {
+ // the list is ordered and thus, all older features should already have an info window
+ break;
+ }
+ }
+
+ if (missingWindows.size() > 0) {
+ new GenerateInfoWindowTask(this, contactId).execute(missingWindows);
+ }
+ }
+
+
+ private void generateInfoWindows(int contactId) {
+ MapSource contactMapMetadata = contactMapSources.get(contactId);
+ FeatureTreeSet collection = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
+ new GenerateInfoWindowTask(this, contactId).execute(collection.getFeatureList());
+ }
+
+ private void initGeoJsonSources(MapSource source) {
+ GeoJsonSource markerPositionSource = new GeoJsonSource(source.getMarkerSource());
+ GeoJsonSource linePositionSource = new GeoJsonSource(source.getLineSource());
+
+ try {
+ 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 int[] getContactIds(int chatId) {
+ if (chatId == ALL_CHATS_GLOBAL_MAP) {
+ return dcContext.getContacts(DC_GCL_ADD_SELF, "");
+ } else {
+ int[] contactIds = dcContext.getChatContacts(chatId);
+ boolean hasSelf = false;
+ for (int contact : contactIds) {
+ if (contact == 1) {
+ hasSelf = true;
+ break;
+ }
+ }
+ if (!hasSelf) {
+ contactIds = Arrays.copyOf(contactIds, contactIds.length + 1);
+ contactIds[contactIds.length - 1] = 1;
+ }
+ return contactIds;
+ }
+ }
+
+ private void initLayers(MapSource source) {
+ mapboxStyle.addImage(source.getMarkerLastPositon(),
+ generateColoredLastPositionIcon(source.getColorArgb()));
+ mapboxStyle.addImage(source.getMarkerIcon(),
+ generateColoredLocationIcon(source.getColorArgb()));
+
+ Expression markerSize =
+ switchCase(toBool(get(MARKER_SELECTED)), literal(1.5f),
+ switchCase(toBool(get(LAST_LOCATION)), literal(1.0f),
+ switchCase(eq(get(MESSAGE_ID), literal(0)), literal(0.7f), literal(1.1f))));
+ Expression markerIcon = switchCase(toBool(get(LAST_LOCATION)), literal(source.getMarkerLastPositon()), literal(source.getMarkerIcon()));
+ Expression allowOverlap = eq(get(LAST_LOCATION), literal(true));
+ mapboxStyle.addLayer(new SymbolLayer(source.getMarkerLayer(), source.getMarkerSource())
+ .withProperties(
+ iconImage(markerIcon),
+ iconSize(markerSize),
+ iconAllowOverlap(allowOverlap))
+ );
+
+ 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.lineOpacity(0.5f),
+ PropertyFactory.lineColor(source.getColorArgb())));
+
+ Expression filterInfoWindow = eq((get(MARKER_SELECTED)), literal(true));
+ Expression iconOffset = switchCase(toBool(get(LAST_LOCATION)), literal(new Float[] {-2f, -25f}), literal(new Float[] {-2f, -15f}));
+ mapboxStyle.addLayer(new SymbolLayer(source.getInfoWindowLayer(), source.getMarkerSource()).
+ withProperties(
+ iconImage("{"+INFO_WINDOW_ID+"}"),
+ iconAnchor(ICON_ANCHOR_BOTTOM_LEFT),
+ /* all info window and marker image to appear at the same time*/
+ iconAllowOverlap(true),
+ /* offset the info window to be above the marker */
+ iconOffset(iconOffset)
+ ).withFilter(filterInfoWindow));
+ }
+
+ private MapSource addContactMapSource(int contactId) {
+ if (contactMapSources.get(contactId) != null) {
+ return contactMapSources.get(contactId);
+ }
+
+ DcContact contact = dcContext.getContact(contactId);
+ MapSource contactMapSource = new MapSource(contactId);
+ contactMapSource.setColor(contact.getColor());
+ contactMapSources.put(contactId, contactMapSource);
+ initGeoJsonSources(contactMapSource);
+ initLayers(contactMapSource);
+ return 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() {
+ int[] contactIds = getContactIds(chatId);
+ for (int contactId : contactIds) {
+ updateSource(chatId, contactId);
+ }
+ }
+
+
private void replaceSelectedMarker(String featureId) {
Feature feature = getFeatureWithId(featureId);
feature.addBooleanProperty(MARKER_SELECTED, true);
@@ -398,10 +435,9 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
refreshSource(selectedFeature.getNumberProperty(CONTACT_ID).intValue());
}
-
private Feature getFeatureWithId(String id) {
for (String key : featureCollections.keySet()) {
- ArrayList featureCollection = featureCollections.get(key);
+ ArrayList featureCollection = featureCollections.get(key).getFeatureList();
for (Feature f : featureCollection) {
if (f.id().equals(id)) {
return f;
diff --git a/src/org/thoughtcrime/securesms/map/model/FeatureTreeSet.java b/src/org/thoughtcrime/securesms/map/model/FeatureTreeSet.java
new file mode 100644
index 000000000..0a5a80c00
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/map/model/FeatureTreeSet.java
@@ -0,0 +1,55 @@
+package org.thoughtcrime.securesms.map.model;
+
+import android.support.annotation.NonNull;
+
+import com.google.gson.JsonSyntaxException;
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.Point;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * Created by cyberta on 21.03.19.
+ */
+
+public class FeatureTreeSet extends TreeSet {
+
+ /**
+ *
+ * @param obj
+ * @return true if element was added newly, false if element was replaced
+ */
+ @Override
+ public boolean add(@NonNull TimeComparableFeature obj) {
+ boolean existed = remove(obj);
+ super.add(obj);
+ return !existed;
+ }
+
+ public ArrayList getFeatureList() {
+ ArrayList featureList = new ArrayList<>();
+ Iterator iterator = this.iterator();
+ while (iterator.hasNext()) {
+ TimeComparableFeature timeComparableFeature = iterator.next();
+ featureList.add(timeComparableFeature.getFeature());
+ }
+ return featureList;
+ }
+
+ public ArrayList getPointList() {
+ ArrayList points = new ArrayList<>();
+ Iterator iterator = this.iterator();
+ while (iterator.hasNext()) {
+ TimeComparableFeature timeComparableFeature = iterator.next();
+ try {
+ points.add(Point.fromJson(timeComparableFeature.getFeature().geometry().toJson()));
+ } catch (JsonSyntaxException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+ return points;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/map/MapSource.java b/src/org/thoughtcrime/securesms/map/model/MapSource.java
similarity index 98%
rename from src/org/thoughtcrime/securesms/map/MapSource.java
rename to src/org/thoughtcrime/securesms/map/model/MapSource.java
index 6851429d0..60ecbcb16 100644
--- a/src/org/thoughtcrime/securesms/map/MapSource.java
+++ b/src/org/thoughtcrime/securesms/map/model/MapSource.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.map;
+package org.thoughtcrime.securesms.map.model;
import android.graphics.Color;
diff --git a/src/org/thoughtcrime/securesms/map/model/TimeComparableFeature.java b/src/org/thoughtcrime/securesms/map/model/TimeComparableFeature.java
new file mode 100644
index 000000000..7b80185d1
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/map/model/TimeComparableFeature.java
@@ -0,0 +1,59 @@
+package org.thoughtcrime.securesms.map.model;
+
+import android.support.annotation.NonNull;
+
+import com.mapbox.geojson.Feature;
+
+import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
+
+/**
+ * Created by cyberta on 21.03.19.
+ */
+
+public class TimeComparableFeature implements Comparable {
+ private Feature feature;
+
+ public TimeComparableFeature(@NonNull Feature feature) {
+ if (!feature.hasProperty(TIMESTAMP)) {
+ throw new IllegalArgumentException("Time comparable features need to have a TIMESTAMP property");
+ }
+
+ this.feature = feature;
+ }
+
+ public Feature getFeature() {
+ return feature;
+ }
+
+ /**
+ *
+ * @param o for same timestamps but different featureIds, this object is considered lower
+ * @return
+ */
+ @Override
+ public int compareTo(@NonNull TimeComparableFeature o) {
+ if (this.equals(o) && this.feature.getNumberProperty(TIMESTAMP).longValue() == o.getFeature().getNumberProperty(TIMESTAMP).longValue()) {
+ return 0;
+ }
+ return this.feature.getNumberProperty(TIMESTAMP).longValue() > o.getFeature().getNumberProperty(TIMESTAMP).longValue() ? -1 : 1;
+ }
+
+
+ @Override
+ public int hashCode() {
+ return this.feature.id().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof TimeComparableFeature)) {
+ return false;
+ }
+
+ TimeComparableFeature compare = (TimeComparableFeature) obj;
+ return this.feature.id() != null &&
+ compare.feature.id() != null &&
+ this.feature.id().equals(compare.feature.id());
+ }
+
+}
diff --git a/src/test/java/org/thoughtcrime/securesms/map/model/FeatureTreeSetTest.java b/src/test/java/org/thoughtcrime/securesms/map/model/FeatureTreeSetTest.java
new file mode 100644
index 000000000..6b7a52b52
--- /dev/null
+++ b/src/test/java/org/thoughtcrime/securesms/map/model/FeatureTreeSetTest.java
@@ -0,0 +1,131 @@
+package org.thoughtcrime.securesms.map.model;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.Point;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
+import static utils.TestUtils.getPointFeature;
+
+/**
+ * Created by cyberta on 22.03.19.
+ */
+public class FeatureTreeSetTest {
+
+ @Test
+ public void add_differentFeaturesIds_differentTimestamps_descendingOrder() {
+ FeatureTreeSet featureTreeSet = new FeatureTreeSet();
+
+ Feature point2 = getPointFeature("3");
+ point2.addNumberProperty(TIMESTAMP, 2);
+ featureTreeSet.add(new TimeComparableFeature(point2));
+
+ Feature point = getPointFeature("2");
+ point.addNumberProperty(TIMESTAMP, 1);
+ featureTreeSet.add(new TimeComparableFeature(point));
+
+ featureTreeSet.add(new TimeComparableFeature(getPointFeature("1")));
+
+ Iterator iterator = featureTreeSet.iterator();
+ assertEquals(123456789, iterator.next().getFeature().getNumberProperty(TIMESTAMP).longValue());
+ assertEquals(2, iterator.next().getFeature().getNumberProperty(TIMESTAMP).longValue());
+ assertEquals(1, iterator.next().getFeature().getNumberProperty(TIMESTAMP).longValue());
+
+ }
+
+
+ @Test
+ public void add_sameFeaturesIds_differentTimestamps_descendingOrder() {
+ FeatureTreeSet featureTreeSet = new FeatureTreeSet();
+
+ Feature point2 = getPointFeature();
+ point2.addNumberProperty(TIMESTAMP, 2);
+ featureTreeSet.add(new TimeComparableFeature(point2));
+
+ Feature point = getPointFeature();
+ point.addNumberProperty(TIMESTAMP, 1);
+ featureTreeSet.add(new TimeComparableFeature(point));
+
+ featureTreeSet.add(new TimeComparableFeature(getPointFeature("1")));
+
+
+ Iterator iterator = featureTreeSet.iterator();
+ assertEquals(123456789, iterator.next().getFeature().getNumberProperty(TIMESTAMP).longValue());
+ assertEquals(2, iterator.next().getFeature().getNumberProperty(TIMESTAMP).longValue());
+ assertEquals(1, iterator.next().getFeature().getNumberProperty(TIMESTAMP).longValue());
+ }
+
+ @Test
+ public void add_sameFeaturesIds_sameTimestamps_noDuplicatedEntries() {
+ FeatureTreeSet featureTreeSet = new FeatureTreeSet();
+ featureTreeSet.add(new TimeComparableFeature(getPointFeature()));
+ featureTreeSet.add(new TimeComparableFeature(getPointFeature()));
+ assertEquals(1, featureTreeSet.size());
+ }
+
+ @Test
+ public void add_sameFeaturesIds_sameTimestamps_elementReplaced() {
+ FeatureTreeSet featureTreeSet = new FeatureTreeSet();
+ Feature feature = getPointFeature();
+ feature.addStringProperty("Test", "element1");
+ featureTreeSet.add(new TimeComparableFeature(feature));
+ featureTreeSet.add(new TimeComparableFeature(getPointFeature()));
+ assertFalse(featureTreeSet.first().getFeature().hasProperty("Test"));
+ }
+
+
+
+ @Test
+ public void getFeatureList_returnsOrderedList() throws Exception {
+ FeatureTreeSet featureTreeSet = new FeatureTreeSet();
+
+ Feature point2 = getPointFeature();
+ point2.addNumberProperty(TIMESTAMP, 2);
+ featureTreeSet.add(new TimeComparableFeature(point2));
+
+ Feature point = getPointFeature();
+ point.addNumberProperty(TIMESTAMP, 1);
+ featureTreeSet.add(new TimeComparableFeature(point));
+
+ featureTreeSet.add(new TimeComparableFeature(getPointFeature("1")));
+
+ ArrayList features = featureTreeSet.getFeatureList();
+ assertEquals(123456789, features.get(0).getNumberProperty(TIMESTAMP).longValue());
+ assertEquals(2, features.get(1).getNumberProperty(TIMESTAMP).longValue());
+ assertEquals(1, features.get(2).getNumberProperty(TIMESTAMP).longValue());
+
+ }
+
+ @Test
+ public void getPointList_returnsOrderedPointList() throws Exception {
+ FeatureTreeSet featureTreeSet = new FeatureTreeSet();
+
+ Feature point = getPointFeature("id2", 1.00, 1.00);
+ point.addNumberProperty(TIMESTAMP, 2);
+ featureTreeSet.add(new TimeComparableFeature(point));
+
+ Feature point2 = getPointFeature("id1", 2.00, 2.00);
+ point2.addNumberProperty(TIMESTAMP, 1);
+ featureTreeSet.add(new TimeComparableFeature(point2));
+
+
+ Feature point3 = getPointFeature("id3", 3.00, 3.00);
+ point3.addNumberProperty(TIMESTAMP, 3);
+
+ featureTreeSet.add(new TimeComparableFeature(point3));
+
+
+ ArrayList points = featureTreeSet.getPointList();
+ assertEquals(new Double(3.00), points.get(0).coordinates().get(0));
+ assertEquals(new Double(1.00), points.get(1).coordinates().get(0));
+ assertEquals(new Double(2.00), points.get(2).coordinates().get(0));
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/thoughtcrime/securesms/map/model/TimeComparableFeatureTest.java b/src/test/java/org/thoughtcrime/securesms/map/model/TimeComparableFeatureTest.java
new file mode 100644
index 000000000..9dd4f48aa
--- /dev/null
+++ b/src/test/java/org/thoughtcrime/securesms/map/model/TimeComparableFeatureTest.java
@@ -0,0 +1,99 @@
+package org.thoughtcrime.securesms.map.model;
+
+import com.mapbox.geojson.Feature;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
+import static utils.TestUtils.getPointFeature;
+
+/**
+ * Created by cyberta on 22.03.19.
+ */
+
+public class TimeComparableFeatureTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void init_noTimestamp_throwIllegalStateException() throws Exception {
+ Feature feature = getPointFeature();
+ feature.removeProperty(TIMESTAMP);
+ new TimeComparableFeature(feature);
+ }
+
+ @Test
+ public void compareTo_sameTimestamp_return0() throws Exception {
+ TimeComparableFeature tcf1 = new TimeComparableFeature(getPointFeature());
+ TimeComparableFeature tcf2 = new TimeComparableFeature(getPointFeature());
+
+ assertEquals(0, tcf1.compareTo(tcf2));
+ assertEquals(0, tcf2.compareTo(tcf1));
+ }
+
+ @Test
+ public void compareTo_biggerTimestamp_return1() throws Exception {
+ Feature feature = getPointFeature();
+ feature.addNumberProperty(TIMESTAMP, 234567890);
+ TimeComparableFeature tcf1 = new TimeComparableFeature(getPointFeature());
+ TimeComparableFeature tcf2 = new TimeComparableFeature(feature);
+
+ assertEquals(1, tcf1.compareTo(tcf2));
+ }
+
+ @Test
+ public void compareTo_biggerTimestamp_returnMinus1() throws Exception {
+ Feature feature = getPointFeature();
+ feature.addNumberProperty(TIMESTAMP, 234567890);
+ TimeComparableFeature tcf1 = new TimeComparableFeature(feature);
+ TimeComparableFeature tcf2 = new TimeComparableFeature(getPointFeature());
+
+ assertEquals(-1, tcf1.compareTo(tcf2));
+ }
+
+ @Test
+ public void compareTo_sameTimestamp_differntId_returnMinus1() throws Exception {
+ Feature feature = getPointFeature();
+ feature.addNumberProperty(TIMESTAMP, 234567890);
+ TimeComparableFeature tcf1 = new TimeComparableFeature(feature);
+ TimeComparableFeature tcf2 = new TimeComparableFeature(getPointFeature());
+
+ assertEquals(-1, tcf1.compareTo(tcf2));
+ }
+
+ @Test
+ public void equals_differentObjects_sameId_returnTrue() throws Exception {
+ TimeComparableFeature tcf1 = new TimeComparableFeature(getPointFeature());
+ TimeComparableFeature tcf2 = new TimeComparableFeature(getPointFeature());
+
+ assertEquals(true, tcf1.equals(tcf2));
+ }
+
+ @Test
+ public void equals_differentObjects_differentId_returnFalse() throws Exception {
+ Feature feature = getPointFeature("id2");
+ TimeComparableFeature tcf1 = new TimeComparableFeature(feature);
+ TimeComparableFeature tcf2 = new TimeComparableFeature(getPointFeature());
+
+ assertEquals(false, tcf1.equals(tcf2));
+ }
+
+ @Test
+ public void equals_differentObjects_differentId_sameTimeStamp_returnFalse() throws Exception {
+ Feature feature = getPointFeature("id2");
+ TimeComparableFeature tcf1 = new TimeComparableFeature(feature);
+ TimeComparableFeature tcf2 = new TimeComparableFeature(getPointFeature());
+
+ assertEquals(false, tcf1.equals(tcf2));
+ }
+
+ @Test
+ public void equals_differentObjects_sameId_differentTimeStamp_returnTrue() throws Exception {
+ Feature feature = getPointFeature();
+ feature.addNumberProperty(TIMESTAMP, 234567890);
+ TimeComparableFeature tcf1 = new TimeComparableFeature(feature);
+ TimeComparableFeature tcf2 = new TimeComparableFeature(getPointFeature());
+
+ assertEquals(true, tcf1.equals(tcf2));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/utils/TestUtils.java b/src/test/java/utils/TestUtils.java
new file mode 100644
index 000000000..8a7b7ccfc
--- /dev/null
+++ b/src/test/java/utils/TestUtils.java
@@ -0,0 +1,42 @@
+package utils;
+
+import com.google.gson.JsonObject;
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.Point;
+
+import static org.thoughtcrime.securesms.map.MapDataManager.ACCURACY;
+import static org.thoughtcrime.securesms.map.MapDataManager.CONTACT_ID;
+import static org.thoughtcrime.securesms.map.MapDataManager.INFO_WINDOW_ID;
+import static org.thoughtcrime.securesms.map.MapDataManager.LAST_LOCATION;
+import static org.thoughtcrime.securesms.map.MapDataManager.MARKER_SELECTED;
+import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
+import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
+
+/**
+ * Created by cyberta on 22.03.19.
+ */
+
+public class TestUtils {
+
+ public static Feature getPointFeature(String id) {
+ return getPointFeature(id, 10.00, 52.00);
+ }
+
+ public static Feature getPointFeature() {
+ return getPointFeature("id1");
+ }
+
+ public static Feature getPointFeature(String id, double latitude, double longitude) {
+ Point p = Point.fromLngLat(longitude, latitude);
+ Feature pointFeature = Feature.fromGeometry(p, new JsonObject(), id);
+ pointFeature.addBooleanProperty(MARKER_SELECTED, false);
+ pointFeature.addBooleanProperty(LAST_LOCATION, false);
+ pointFeature.addNumberProperty(CONTACT_ID, 1);
+ pointFeature.addStringProperty(INFO_WINDOW_ID, "0_1_info_2");
+ pointFeature.addNumberProperty(TIMESTAMP, 123456789);
+ pointFeature.addNumberProperty(MESSAGE_ID, 1);
+ pointFeature.addNumberProperty(ACCURACY, 12);
+ return Feature.fromJson(pointFeature.toJson());
+ }
+
+}