Merge pull request #791 from deltachat/map_filtering_2

Map filtering 2
This commit is contained in:
cyBerta
2019-03-28 16:01:35 +01:00
committed by GitHub
22 changed files with 848 additions and 40 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+4 -3
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -11,4 +10,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v7.widget.LinearLayoutCompat>
<include layout="@layout/bottom_map_sheet" />
</android.support.design.widget.CoordinatorLayout>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="165dp"
android:id="@+id/bottom_sheet"
app:behavior_hideable="false"
app:behavior_peekHeight="35dp"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
android:background="@color/transparent"
>
<RelativeLayout
android:id="@+id/bottomSheetSlider"
android:layout_width="wrap_content"
android:layout_height="70dp"
android:layout_gravity="end"
android:layout_marginRight="15dp"
android:layout_marginEnd="15dp"
>
<ImageView
android:layout_width="70dp"
android:layout_height="70dp"
android:background="@drawable/ic_location_dot"
android:layout_centerHorizontal="true"
/>
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_timeline"
android:layout_centerHorizontal="true"
/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="135dp"
android:orientation="vertical"
android:clickable="true"
android:focusable="true"
android:background="@android:color/white"
android:layout_marginTop="-35dp"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/filter_map_on_time"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:background="@color/white"
android:textColor="@android:color/black" />
<org.thoughtcrime.securesms.components.rangeslider.TimeRangeSlider
android:id="@+id/timeRangeSlider"
android:layout_marginBottom="15dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:paddingLeft="40dp"
android:paddingRight="40dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
</LinearLayout>
+3 -1
View File
@@ -17,7 +17,9 @@
android:id="@+id/menu_deaddrop" />
<item android:title="@string/menu_show_global_map"
android:id="@+id/menu_global_map" />
android:id="@+id/menu_global_map"
android:visible="false"
/>
<item android:title="@string/menu_settings"
android:id="@+id/menu_settings" />
+15
View File
@@ -239,4 +239,19 @@
<attr name="conversationThumbnail_maxHeight" format="dimension" />
</declare-styleable>
<declare-styleable name="RangeSliderView">
<attr name="trackTintColor" format="color|reference"/>
<attr name="trackHighlightTintColor" format="color|reference"/>
<attr name="trackHeight" format="dimension|reference"/>
<attr name="thumbRadius" format="dimension|reference"/>
<attr name="thumbOutlineSize" format="dimension|reference"/>
<attr name="displayTextFontSize" format="dimension|reference"/>
<attr name="minValue" format="integer"/>
<attr name="maxValue" format="integer"/>
</declare-styleable>
<declare-styleable name="TimeRangeSlider">
<attr name="timeFrame" format="integer"/>
</declare-styleable>
</resources>
+4
View File
@@ -50,4 +50,8 @@
<color name="universal_overlay">#44000000</color>
<color name="trackTintColor">#552090ea</color>
<color name="trackHighlightTintColor">#ff2090ea</color>
</resources>
+8
View File
@@ -53,4 +53,12 @@
<dimen name="scribble_stroke_size">2dp</dimen>
<dimen name="slider_trackHeight">6dp</dimen>
<dimen name="slider_thumbRadius">14dp</dimen>
<dimen name="slider_thumbOutlineSize">4dp</dimen>
<dimen name="slider_displayTextFontSize">12sp</dimen>
<dimen name="slider_displayTextBasicOffsetY">8dp</dimen>
<integer name="slider_minValue">100</integer>
<integer name="slider_maxValue">100</integer>
</resources>
+5
View File
@@ -219,6 +219,11 @@
<string name="chat_contact_request">Contact request</string>
<string name="chat_no_contact_requests">No contact requests.\n\nIf you want classic emails to appear here as contact requests, you can change the corresponding setting in the app settings.</string>
<!-- map -->
<string name="filter_map_on_time">Show location traces in time frame</string>
<string name="filter_last_position">Last position</string>
<!-- search -->
<string name="search">Search</string>
<string name="search_explain">Search for chats, contacts, and messages</string>
@@ -92,6 +92,10 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
menu.clear();
inflater.inflate(R.menu.text_secure_normal, menu);
MenuItem item = menu.findItem(R.id.menu_global_map);
if (Prefs.isLocationStreamingEnabled(this)) {
item.setVisible(true);
}
if (!Prefs.isLocationStreamingEnabled(this)) {
menu.findItem(R.id.menu_global_map).setVisible(false);
@@ -0,0 +1,322 @@
package org.thoughtcrime.securesms.components.rangeslider;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import org.thoughtcrime.securesms.R;
import java.util.ArrayList;
public class RangeSliderView extends View {
private static final String TAG = RangeSliderView.class.getName();
protected int trackTintColor;
protected int trackHighlightTintColor;
protected float trackHeight;
protected float thumbInnerRadius;
protected float thumbOutlineSize;
protected float displayTextFontSize;
protected float displayTextBasicOffsetY;
protected int sliderPaddingLeft;
protected int sliderPaddingRight;
private ThumbLayer minValueThumb;
private TextLayer minValueDisplayLabel;
private ThumbLayer maxValueThumb;
private TextLayer maxValueDisplayLabel;
private TrackLayer track;
private ArrayList<Integer> values;
protected int minValue;
protected int maxValue;
protected float delta;
protected float offsetY;
protected float maxValueDisplayLabelOffsetY;
protected float minValueDisplayLabelOffsetY;
protected float trackOffsetY;
protected float thumbRadius;
protected float sliderWidth;
private float beginTrackOffsetX;
private boolean isThumbViewLocked;
private OnValueChangedListener onValueChangedListener;
GestureDetector longPressDetector;
public interface OnValueChangedListener {
void onValueChanged(int minValue, int maxValue);
String parseMinValueDisplayText(int minValue);
String parseMaxValueDisplayText(int maxValue);
}
public RangeSliderView(Context context) {
this(context, null);
}
public RangeSliderView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RangeSliderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
int [] attributes = new int [] {android.R.attr.paddingLeft,
android.R.attr.paddingStart,
android.R.attr.paddingRight,
android.R.attr.paddingEnd};
TypedArray arr = context.obtainStyledAttributes(attrs, attributes);
sliderPaddingLeft = arr.getDimensionPixelOffset(0,
arr.getDimensionPixelOffset(1, -1));
sliderPaddingRight = arr.getDimensionPixelOffset(2,
arr.getDimensionPixelOffset(3, -1));
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RangeSliderView);
trackTintColor = typedArray.getColor(
R.styleable.RangeSliderView_trackTintColor,
ContextCompat.getColor(context, R.color.trackTintColor)
);
trackHighlightTintColor = typedArray.getColor(
R.styleable.RangeSliderView_trackHighlightTintColor,
ContextCompat.getColor(context, R.color.trackHighlightTintColor)
);
Resources resources = context.getResources();
trackHeight = typedArray.getDimension(
R.styleable.RangeSliderView_trackHeight,
resources.getDimension(R.dimen.slider_trackHeight)
);
thumbInnerRadius = typedArray.getDimension(
R.styleable.RangeSliderView_thumbRadius,
resources.getDimension(R.dimen.slider_thumbRadius)
);
thumbOutlineSize = typedArray.getDimension(
R.styleable.RangeSliderView_thumbOutlineSize,
resources.getDimension(R.dimen.slider_thumbOutlineSize)
);
displayTextFontSize = typedArray.getDimension(
R.styleable.RangeSliderView_displayTextFontSize,
resources.getDimension(R.dimen.slider_displayTextFontSize)
);
displayTextBasicOffsetY = resources.getDimension(R.dimen.slider_displayTextBasicOffsetY);
minValue = typedArray.getInt(
R.styleable.RangeSliderView_minValue,
100
);
maxValue = typedArray.getInt(
R.styleable.RangeSliderView_maxValue,
100
);
minValueThumb = new ThumbLayer(thumbInnerRadius, thumbOutlineSize, trackHighlightTintColor, trackTintColor);
minValueDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
maxValueThumb = new ThumbLayer(thumbInnerRadius, thumbOutlineSize, trackHighlightTintColor, trackTintColor);
maxValueDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
track = new TrackLayer(sliderPaddingLeft, sliderPaddingRight, trackHeight, trackTintColor, trackHighlightTintColor);
values = new ArrayList<>();
for (int index = 1; index <= 100; index++) {
values.add(index);
}
delta = 0;
longPressDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
public void onLongPress(MotionEvent e) {
if (minValue == maxValue && (minValueThumb.isHighlight || maxValueThumb.isHighlight)) {
isThumbViewLocked = true;
minValueThumb.isHighlight = true;
maxValueThumb.isHighlight = true;
invalidate();
}
}
});
}
void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) {
this.onValueChangedListener = onValueChangedListener;
invalidate();
}
public void setRangeValues(ArrayList<Integer> values) {
this.values = values;
setMinAndMaxValue(this.values.get(0), this.values.get(this.values.size() - 1));
}
public void setMinAndMaxValue(int minValue, int maxValue) {
this.minValue = minValue;
this.maxValue = maxValue;
invalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.offsetY = getHeight() / 5 * 3;
maxValueDisplayLabelOffsetY = offsetY - thumbInnerRadius - thumbOutlineSize - trackHeight / 2 - displayTextBasicOffsetY - displayTextFontSize;
minValueDisplayLabelOffsetY = maxValueDisplayLabelOffsetY + displayTextFontSize + displayTextFontSize / 2;
trackOffsetY = offsetY - trackHeight / 2;
sliderWidth = getWidth() - sliderPaddingLeft - sliderPaddingRight;
thumbRadius = thumbInnerRadius + thumbOutlineSize / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float minValueOffsetX = getPositionFromIndex(minValue);
float maxValueOffsetX = getPositionFromIndex(maxValue);
track.draw(canvas, getWidth(), minValueOffsetX, maxValueOffsetX, trackOffsetY, getDeltaInPixel());
minValueThumb.draw(canvas, minValueOffsetX, offsetY);
maxValueThumb.draw(canvas, maxValueOffsetX, offsetY);
if (onValueChangedListener != null) {
minValueDisplayLabel.draw(
canvas,
onValueChangedListener.parseMinValueDisplayText(minValue),
minValueOffsetX,
minValueDisplayLabelOffsetY
);
maxValueDisplayLabel.draw(
canvas,
onValueChangedListener.parseMaxValueDisplayText(maxValue),
maxValueOffsetX,
maxValueDisplayLabelOffsetY
);
} else {
minValueDisplayLabel.draw(canvas, String.valueOf(minValue), minValueOffsetX, minValueDisplayLabelOffsetY);
maxValueDisplayLabel.draw(canvas, String.valueOf(maxValue), maxValueOffsetX, maxValueDisplayLabelOffsetY);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
longPressDetector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_CANCEL) {
minValueThumb.isHighlight = false;
maxValueThumb.isHighlight = false;
isThumbViewLocked = false;
invalidate();
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
isThumbViewLocked = false;
if (minValueThumb.isHighlight || maxValueThumb.isHighlight) {
if (onValueChangedListener != null) {
onValueChangedListener.onValueChanged(minValue, maxValue);
}
minValueThumb.isHighlight = false;
maxValueThumb.isHighlight = false;
invalidate();
return true;
}
}
int offsetX = (int) event.getX();
if (offsetX < (sliderPaddingLeft - thumbRadius) || offsetX > getWidth() - sliderPaddingRight + thumbRadius) {
//Log.d(TAG, "ignore touch in padding");
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float minValuePosition = getPositionFromIndex(minValue);
Log.d(TAG, "action_down minVal: " + minValue + ", " + minValuePosition );
if (offsetX >= minValuePosition - thumbRadius && offsetX <= minValuePosition + thumbRadius) {
minValueThumb.isHighlight = true;
} else {
float maxValuePosition = getPositionFromIndex(maxValue);
if (offsetX >= maxValuePosition - thumbRadius && offsetX <= maxValuePosition + thumbRadius) {
maxValueThumb.isHighlight = true;
}
}
if (minValueThumb.isHighlight || maxValueThumb.isHighlight) {
beginTrackOffsetX = offsetX;
invalidate();
} else {
beginTrackOffsetX = -1;
}
break;
case MotionEvent.ACTION_MOVE:
if (minValue == maxValue && beginTrackOffsetX > -1 && offsetX > beginTrackOffsetX && !maxValueThumb.isHighlight) {
minValueThumb.isHighlight = false;
maxValueThumb.isHighlight = true;
}
int count = values.size();
int index = getIndexFromPosition(offsetX);
Log.d(TAG, "move - index: " + index + ", offsetX: " + offsetX);
if (index < 0) {
index = 0;
} else if (index > count - 1) {
index = count - 1;
}
if (isThumbViewLocked) {
minValue = values.get(index);
maxValue = values.get(index);
} else if (minValueThumb.isHighlight) {
if (index > values.indexOf(maxValue)) {
minValue = maxValue;
} else {
minValue = values.get(index);
}
} else if (maxValueThumb.isHighlight) {
if (index < values.indexOf(minValue)) {
maxValue = minValue;
} else {
maxValue = values.get(index);
}
}
if (minValueThumb.isHighlight || maxValueThumb.isHighlight) {
invalidate();
}
break;
}
return true;
}
protected int getIndexFromPosition(int offsetX) {
return (int) (((offsetX - sliderPaddingLeft) * getCount()) / sliderWidth);
}
protected float getPositionFromIndex(int value) {
return ((sliderWidth / (float) getCount()) * value) + sliderPaddingLeft;
}
public float getDeltaInPixel() {
// '* 2' is just an offset factor to increase the visibility
return (sliderWidth / getCount()) * delta + (2 * thumbInnerRadius + thumbOutlineSize);
}
public float getDeltaInValues() {
return delta;
}
public int getCount() {
return values.size();
}
}
@@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.components.rangeslider;
import android.graphics.Canvas;
import android.graphics.Paint;
class TextLayer {
private Paint paint;
TextLayer(float fontSize, int color) {
paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(color);
paint.setTextSize(fontSize);
paint.setTextAlign(Paint.Align.CENTER);
}
void draw(Canvas canvas, String text, float offsetX, float offsetY) {
canvas.drawText(text, offsetX, offsetY, paint);
}
}
@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.components.rangeslider;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
class ThumbLayer {
private float radius;
private float outlineSize;
private int highlightColor;
private Paint outlinePaint;
private Paint fillPaint;
boolean isHighlight = false;
ThumbLayer(float radius, float outlineSize, int outlineColor, int highlightColor) {
this.radius = radius;
this.outlineSize = outlineSize;
this.highlightColor = highlightColor;
outlinePaint = new Paint();
outlinePaint.setAntiAlias(true);
outlinePaint.setColor(outlineColor);
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setStrokeWidth(outlineSize);
fillPaint = new Paint();
fillPaint.setAntiAlias(true);
fillPaint.setStyle(Paint.Style.FILL);
fillPaint.setStrokeWidth(0);
}
void draw(Canvas canvas, float cx, float cy) {
canvas.drawCircle(cx, cy, radius, outlinePaint);
if (isHighlight) {
fillPaint.setColor(highlightColor);
} else {
fillPaint.setColor(Color.WHITE);
}
canvas.drawCircle(cx, cy, radius - outlineSize / 2, fillPaint);
}
}
@@ -0,0 +1,154 @@
package org.thoughtcrime.securesms.components.rangeslider;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import java.util.Locale;
/**
* Created by cyberta on 24.03.19.
*/
public class TimeRangeSlider extends RangeSliderView implements RangeSliderView.OnValueChangedListener {
private static final int DEFAULT_TIMEFRAME_2D = 60 * 60 * 24 * 2; // 2d
private static final float DEFAULT_DELTA = 1000*60*30; // 30 min
int timeFrame; // timeframe in seconds
Locale locale;
OnTimestampChangedListener listener;
private TextLayer minRangeDisplayLabel;
private TextLayer maxRangeDisplayLabel;
float displayLabelOffsetBelow;
public TimeRangeSlider(Context context) {
this(context, null, 0);
}
public TimeRangeSlider(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TimeRangeSlider(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RangeSliderView);
timeFrame = typedArray.getInt(
R.styleable.TimeRangeSlider_timeFrame,
DEFAULT_TIMEFRAME_2D
);
locale = DynamicLanguage.getSelectedLocale(context);
super.setOnValueChangedListener(this);
minRangeDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
maxRangeDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
delta = DEFAULT_DELTA / (timeFrame * 1000) * getCount();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
displayLabelOffsetBelow = offsetY + thumbInnerRadius + thumbOutlineSize + trackHeight / 2 + displayTextBasicOffsetY;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float minValueOffsetX = getPositionFromIndex(minValue);
float maxValueOffsetX = getPositionFromIndex(maxValue);
if (minValue == maxValue) {
if (minValueOffsetX - getDeltaInPixel() >= sliderPaddingLeft) {
minRangeDisplayLabel.draw(
canvas,
getDeltaInTime(getContext()),
minValueOffsetX - getDeltaInPixel(),
displayLabelOffsetBelow
);
}
if (maxValueOffsetX + getDeltaInPixel() <= (getWidth() - sliderPaddingRight)) {
maxRangeDisplayLabel.draw(
canvas,
getDeltaInTime(getContext()),
maxValueOffsetX + getDeltaInPixel(),
displayLabelOffsetBelow
);
}
}
}
public interface OnTimestampChangedListener {
void onTimestampChanged(long startTimestamp, long stopTimestamp);
/**
* filter for lastPosition beginning from startTimestamp to now
* @param startTimestamp begin of time frame
*/
void onFilterLastPosition(long startTimestamp);
}
public void setOnTimestampChangedListener(OnTimestampChangedListener timestampChangedListener) {
listener = timestampChangedListener;
}
@Override
public void onValueChanged(int minValue, int maxValue) {
if (listener != null) {
long minTimeStamp = getTimestampForValue(minValue);
if (minValue == maxValue) {
if (minValue == getCount()) {
// filter last location
listener.onFilterLastPosition(System.currentTimeMillis() - (long) DEFAULT_DELTA);
} else {
// filter for time of event with delta before and after
listener.onTimestampChanged(minTimeStamp - (long) DEFAULT_DELTA, minTimeStamp + (long) DEFAULT_DELTA);
}
} else {
//filter for time span
listener.onTimestampChanged(minTimeStamp, getTimestampForValue(maxValue));
}
}
}
@Override
public String parseMinValueDisplayText(int minValue) {
if (minValue == maxValue) {
return "";
}
return getStringForValue(minValue);
}
@Override
public String parseMaxValueDisplayText(int maxValue) {
if (minValue == maxValue && maxValue == getCount()) {
return getContext().getResources().getString(R.string.filter_last_position);
}
return getStringForValue(maxValue);
}
private String getStringForValue(int value) {
return DateUtils.getExtendedRelativeTimeSpanString(this.getContext(), locale, getTimestampForValue(value));
}
private long getTimestampForValue(int value) {
return System.currentTimeMillis() - ((timeFrame / getCount()) * (getCount() - value) * 1000);
}
public String getDeltaInTime(Context context) {
return DateUtils.getFormattedTimespan(context, (int) (timeFrame * 1000 / getCount() * getDeltaInValues()));
}
}
@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.components.rangeslider;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
class TrackLayer {
private float height;
private float radius;
private int paddingLeft = 0;
private int paddingRight = 0;
private Paint centerPaint;
private Paint backgoundPaint;
TrackLayer(int paddingLeft, int paddingRight, float height, int color, int highlightColor) {
this.height = height;
radius = this.height / 2;
centerPaint = new Paint();
centerPaint.setAntiAlias(true);
centerPaint.setColor(highlightColor);
centerPaint.setStyle(Paint.Style.FILL);
backgoundPaint = new Paint();
backgoundPaint.setAntiAlias(true);
backgoundPaint.setColor(color);
backgoundPaint.setStyle(Paint.Style.FILL);
this.paddingLeft = paddingLeft;
this.paddingRight = paddingRight;
}
void draw(Canvas canvas, int width, float offsetLeft, float offsetRight, float offsetY, float delta) {
draw(canvas, backgoundPaint, paddingLeft, width - paddingRight, offsetY);
if (offsetLeft == offsetRight) {
float maxEndX = width - paddingRight;
float minStartX = paddingLeft;
float offsetDeltaLeft = offsetLeft - delta;
float offsetDeltaRight = offsetRight + delta;
if (offsetLeft - delta < minStartX) {
offsetDeltaLeft = minStartX;
} else if (offsetRight + delta > maxEndX) {
offsetDeltaRight = maxEndX;
}
draw(canvas, centerPaint, offsetDeltaLeft, offsetDeltaRight, offsetY);
}
else {
draw(canvas, centerPaint, offsetLeft, offsetRight, offsetY);
}
}
void draw(Canvas canvas, Paint paint, float startX, float endX, float offsetY) {
canvas.drawRoundRect(
new RectF(startX, offsetY, endX, offsetY + height),
radius,
radius,
paint
);
}
}
@@ -719,20 +719,4 @@ 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;
}
}
@@ -116,7 +116,7 @@ public class GenerateInfoWindowTask extends AsyncTask<ArrayList<Feature>, HashMa
String id = feature.getStringProperty(INFO_WINDOW_ID);
imagesMap.put(id, bitmap);
if (i % 10 == 0) {
if (i % 20 == 0) {
publishProgress(new HashMap<>(imagesMap));
imagesMap.clear();
}
@@ -3,15 +3,19 @@ package org.thoughtcrime.securesms.map;
import android.content.Intent;
import android.graphics.PointF;
import android.os.Bundle;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.View;
import android.widget.RelativeLayout;
import com.b44t.messenger.DcMsg;
import com.mapbox.geojson.Feature;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.maps.SupportMapFragment;
@@ -20,6 +24,7 @@ import org.thoughtcrime.securesms.BaseActivity;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.rangeslider.TimeRangeSlider;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.geolocation.DcLocation;
@@ -27,11 +32,13 @@ import java.util.List;
import java.util.Observable;
import java.util.Observer;
import static android.support.design.widget.BottomSheetBehavior.STATE_COLLAPSED;
import static android.support.design.widget.BottomSheetBehavior.STATE_EXPANDED;
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;
public class MapActivity extends BaseActivity implements Observer {
public class MapActivity extends BaseActivity implements Observer, TimeRangeSlider.OnTimestampChangedListener {
public static final String TAG = MapActivity.class.getSimpleName();
public static final String CHAT_ID = "chat_id";
@@ -40,6 +47,7 @@ public class MapActivity extends BaseActivity implements Observer {
private DcLocation dcLocation;
private MapDataManager mapDataManager;
private MapboxMap mapboxMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -67,6 +75,7 @@ public class MapActivity extends BaseActivity implements Observer {
mapFragment.getMapAsync(mapboxMap -> mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
this.mapboxMap = mapboxMap;
mapboxMap.setCameraPosition(new CameraPosition.Builder()
.target(new LatLng(dcLocation.getLastLocation().getLatitude(), dcLocation.getLastLocation().getLongitude()))
.zoom(9)
@@ -80,6 +89,7 @@ public class MapActivity extends BaseActivity implements Observer {
}
mapDataManager = new MapDataManager(this, mapBoxStyle, chatId, (latLngBounds) -> {
Log.d(TAG, "on Data initialized");
mapboxMap.easeCamera(CameraUpdateFactory.newLatLngBounds(latLngBounds, 50), 1000);
});
@@ -126,6 +136,11 @@ public class MapActivity extends BaseActivity implements Observer {
}
}
if (chatId == 0) {
Log.e(TAG, "Chat id is 0. Cannot open chat");
return true;
}
Intent intent = new Intent(MapActivity.this, ConversationActivity.class);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, chatId);
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, 0);
@@ -151,6 +166,25 @@ public class MapActivity extends BaseActivity implements Observer {
}
}));
TimeRangeSlider timeRangeSlider = this.findViewById(R.id.timeRangeSlider);
timeRangeSlider.setOnTimestampChangedListener(this);
View bottomSheet = this.findViewById(R.id.bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
RelativeLayout bottomSheetSlider = this.findViewById(R.id.bottomSheetSlider);
bottomSheetSlider.setOnClickListener(v -> {
switch (behavior.getState()) {
case STATE_EXPANDED:
behavior.setState(STATE_COLLAPSED);
break;
default:
behavior.setState(STATE_EXPANDED);
break;
}
});
}
@Override
@@ -182,16 +216,17 @@ public class MapActivity extends BaseActivity implements Observer {
}
}
private int[] getChatIds(Intent intent) {
if (intent == null) {
return new int[]{-1};
@Override
public void onTimestampChanged(long startTimestamp, long stopTimestamp) {
if (this.mapboxMap == null) {
return;
}
mapDataManager.filter(startTimestamp, stopTimestamp);
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;
}
@Override
public void onFilterLastPosition(long startTimestamp) {
mapDataManager.filterLastPositions(startTimestamp);
}
}
@@ -46,9 +46,13 @@ 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.all;
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.gt;
import static com.mapbox.mapboxsdk.style.expressions.Expression.gte;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
import static com.mapbox.mapboxsdk.style.expressions.Expression.lte;
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_LEFT;
@@ -73,6 +77,8 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
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 long DEFAULT_LAST_POSITION_DELTA = 1000*60*30; // 30 min
private static final String TAG = MapDataManager.class.getSimpleName();
private Style mapboxStyle;
private HashMap<Integer, MapSource> contactMapSources;
@@ -98,9 +104,11 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
int[] contactIds = getContactIds(chatId);
long now = System.currentTimeMillis();
for (int contactId : contactIds) {
updateSource(chatId, contactId, boundingBuilder);
generateInfoWindows(contactId);
filterLastPosition(contactId, now - DEFAULT_LAST_POSITION_DELTA);
}
dcContext.eventCenter.addObserver(DC_EVENT_LOCATION_CHANGED, this);
@@ -204,12 +212,50 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
}
}
public void filter(long startTimestamp, long endTimestamp) {
int[] contactIds = getContactIds(chatId);
for (int contactId : contactIds) {
MapSource contactMapMetadata = contactMapSources.get(contactId);
SymbolLayer markerLayer = (SymbolLayer) mapboxStyle.getLayer(contactMapMetadata.getMarkerLayer());
Expression filter = all(
lte(get(TIMESTAMP), endTimestamp),
gte(get(TIMESTAMP), startTimestamp));
markerLayer.withFilter(filter);
LineLayer lineLayer = (LineLayer) mapboxStyle.getLayer(contactMapMetadata.getLineLayer());
lineLayer.withFilter(filter);
}
}
public void filterLastPositions(long startTimestamp) {
int[] contactIds = getContactIds(chatId);
for (int contactId : contactIds) {
filterLastPosition(contactId, startTimestamp);
}
}
private void filterLastPosition(int contactId, long startTimestamp) {
MapSource contactMapMetadata = contactMapSources.get(contactId);
SymbolLayer markerLayer = (SymbolLayer) mapboxStyle.getLayer(contactMapMetadata.getMarkerLayer());
Expression filter = all(
eq((get(LAST_LOCATION)), literal(true)),
gte(get(TIMESTAMP), startTimestamp));
markerLayer.withFilter(filter);
LineLayer lineLayer = (LineLayer) mapboxStyle.getLayer(contactMapMetadata.getLineLayer());
lineLayer.withFilter(filter);
}
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);
updateSource(chatId, contactId, System.currentTimeMillis() - TIMEOUT, TIMESTAMP_NOW, boundingBuilder );
}
private void updateSource(int chatId, int contactId, long startTimestamp, long endTimestamp, LatLngBounds.Builder boundingBuilder) {
//long start = System.currentTimeMillis();
DcArray locations = dcContext.getLocations(chatId, contactId, startTimestamp, endTimestamp);
MapSource contactMapMetadata = contactMapSources.get(contactId);
if (contactMapMetadata == null) {
contactMapMetadata = addContactMapSource(contactId);
@@ -242,10 +288,21 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
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())
)});
ArrayList<Point> sortedPointList = sortedPointFeatures.getPointList();
ArrayList<Feature> sortedFeatureList = sortedPointFeatures.getFeatureList();
FeatureCollection pointFeatureCollection = FeatureCollection.fromFeatures(sortedFeatureList);
ArrayList<Feature> lineFeatures = new ArrayList<>();
for (int i = 0; i < sortedPointFeatures.size() - 1; i++) {
Feature f = sortedFeatureList.get(i);
Number numberProperty = f.getNumberProperty(TIMESTAMP);
LineString l = LineString.fromLngLats(sortedPointList.subList(i, i+2));
Feature lineFeature = Feature.fromGeometry(l, new JsonObject(), "l_" + f.id());
lineFeature.addNumberProperty(TIMESTAMP, numberProperty);
lineFeatures.add(lineFeature);
}
FeatureCollection lineFeatureCollection = FeatureCollection.fromFeatures(lineFeatures);
GeoJsonSource lineSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getLineSource());
lineSource.setGeoJson(lineFeatureCollection);
@@ -254,6 +311,8 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), sortedPointFeatures);
generateMissingInfoWindows(contactId);
//Log.d(TAG, "update Source took " + (System.currentTimeMillis() - start) + " ms");
}
private void generateMissingInfoWindows(int contactId) {
@@ -281,7 +340,9 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
private void generateInfoWindows(int contactId) {
MapSource contactMapMetadata = contactMapSources.get(contactId);
FeatureTreeSet collection = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
new GenerateInfoWindowTask(this, contactId).execute(collection.getFeatureList());
if (collection.size() > 0) {
new GenerateInfoWindowTask(this, contactId).execute(collection.getFeatureList());
}
}
private void initGeoJsonSources(MapSource source) {
@@ -328,12 +389,10 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
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))
iconSize(markerSize))
);
mapboxStyle.addLayer(new LineLayer(source.getLineLayer(), source.getLineSource())
@@ -405,6 +464,14 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
}
}
private void resetSource(int contactId) {
MapSource contactMapMetadata = contactMapSources.get(contactId);
if (contactMapMetadata != null) {
featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), new FeatureTreeSet());
GeoJsonSource pointSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getMarkerSource());
pointSource.setGeoJson(FeatureCollection.fromFeatures(new ArrayList<>()));
}
}
private void replaceSelectedMarker(String featureId) {
Feature feature = getFeatureWithId(featureId);
@@ -175,4 +175,9 @@ public class DateUtils extends android.text.format.DateUtils {
TimeUnit.MILLISECONDS.toMinutes(millis),
TimeUnit.MILLISECONDS.toSeconds(millis-(TimeUnit.MILLISECONDS.toMinutes(millis)*60000)));
}
public static String getFormattedTimespan(Context c, int timestamp) {
int mins = timestamp / (1000 * 60);
return c.getResources().getQuantityString(R.plurals.n_minutes, mins, mins);
}
}