diff --git a/Android/AndroidManifest.xml b/Android/AndroidManifest.xml
index 7e17fcd25d..786c454b11 100644
--- a/Android/AndroidManifest.xml
+++ b/Android/AndroidManifest.xml
@@ -1,10 +1,9 @@
-
+
-
+
@@ -15,7 +14,7 @@
-
+
@@ -50,8 +49,7 @@
android:icon="@drawable/ic_launcher"
android:label="@string/app_title"
android:theme="@style/CustomActionBarTheme"
- android:windowSoftInputMode="adjustPan"
- >
+ android:windowSoftInputMode="adjustPan">
-
+ android:value="${fabricApiKey}"/>
-
+ android:label="@string/account_label"
+ android:launchMode="singleTop"/>
-
+
@@ -100,59 +96,69 @@
-
+ android:windowSoftInputMode="adjustPan"/>
-
+ android:label="@string/settings"
+ android:launchMode="singleTop"/>
+
-
-
+
+ android:parentActivityName=".activities.FlightActivity">
+
-
-
+ android:launchMode="singleTask"
+ android:noHistory="true"
+ android:theme="@style/CustomActionBarTheme.Transparent"/>
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/Android/build.gradle b/Android/build.gradle
index 5df3b2e504..cc51f32026 100644
--- a/Android/build.gradle
+++ b/Android/build.gradle
@@ -21,10 +21,10 @@ dependencies {
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.0.0'
//DroneKit-Android client lib
- compile 'com.o3dr.android:dronekit-android:3.0.0-alpha4'
+ compile 'com.o3dr.android:dronekit-android:3.0.0-alpha6'
//Comment line above, and uncomment line below to use your local version of the dronekit-android client lib
//Don't forget to uncomment the lines in the settings.gradle file as well.
- /* compile project(':ClientLib') */
+// compile project(':ClientLib')
compile 'me.grantland:autofittextview:0.2.1'
compile(name:'shimmer-android-release', ext:'aar')
diff --git a/Android/res/drawable/fastscroller_handle.xml b/Android/res/drawable/fastscroller_handle.xml
new file mode 100644
index 0000000000..5824a5f406
--- /dev/null
+++ b/Android/res/drawable/fastscroller_handle.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/drawable/horizontal_fastscroller_bubble.xml b/Android/res/drawable/horizontal_fastscroller_bubble.xml
new file mode 100644
index 0000000000..b386e5aa86
--- /dev/null
+++ b/Android/res/drawable/horizontal_fastscroller_bubble.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/Android/res/drawable/horizontal_fastscroller_handle.xml b/Android/res/drawable/horizontal_fastscroller_handle.xml
new file mode 100644
index 0000000000..730116fd04
--- /dev/null
+++ b/Android/res/drawable/horizontal_fastscroller_handle.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/drawable/ic_clear_black_24dp.xml b/Android/res/drawable/ic_clear_black_24dp.xml
new file mode 100644
index 0000000000..c2e719a872
--- /dev/null
+++ b/Android/res/drawable/ic_clear_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Android/res/drawable/list_item_tlog_data_bg.xml b/Android/res/drawable/list_item_tlog_data_bg.xml
new file mode 100644
index 0000000000..f30d33266a
--- /dev/null
+++ b/Android/res/drawable/list_item_tlog_data_bg.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/drawable/position_event_thumbnail_background.xml b/Android/res/drawable/position_event_thumbnail_background.xml
new file mode 100644
index 0000000000..d5e158f2d7
--- /dev/null
+++ b/Android/res/drawable/position_event_thumbnail_background.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/drawable/vertical_fastscroller_bubble.xml b/Android/res/drawable/vertical_fastscroller_bubble.xml
new file mode 100644
index 0000000000..b958dede4c
--- /dev/null
+++ b/Android/res/drawable/vertical_fastscroller_bubble.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/Android/res/layout/activity_tlog.xml b/Android/res/layout/activity_tlog.xml
new file mode 100644
index 0000000000..ae0bc13e1b
--- /dev/null
+++ b/Android/res/layout/activity_tlog.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/res/layout/fragment_tlog_data_picker.xml b/Android/res/layout/fragment_tlog_data_picker.xml
new file mode 100644
index 0000000000..92a5c12948
--- /dev/null
+++ b/Android/res/layout/fragment_tlog_data_picker.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/fragment_tlog_event_detail.xml b/Android/res/layout/fragment_tlog_event_detail.xml
new file mode 100644
index 0000000000..dc777d33cd
--- /dev/null
+++ b/Android/res/layout/fragment_tlog_event_detail.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/fragment_tlog_position_viewer.xml b/Android/res/layout/fragment_tlog_position_viewer.xml
new file mode 100644
index 0000000000..eefdaf7ab5
--- /dev/null
+++ b/Android/res/layout/fragment_tlog_position_viewer.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/fragment_tlog_raw_viewer.xml b/Android/res/layout/fragment_tlog_raw_viewer.xml
new file mode 100644
index 0000000000..0253748956
--- /dev/null
+++ b/Android/res/layout/fragment_tlog_raw_viewer.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/horizontal_fastscroller.xml b/Android/res/layout/horizontal_fastscroller.xml
new file mode 100644
index 0000000000..37620a9eb9
--- /dev/null
+++ b/Android/res/layout/horizontal_fastscroller.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/list_item_tlog_data.xml b/Android/res/layout/list_item_tlog_data.xml
new file mode 100644
index 0000000000..ab0e4f6016
--- /dev/null
+++ b/Android/res/layout/list_item_tlog_data.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/list_item_tlog_position_event.xml b/Android/res/layout/list_item_tlog_position_event.xml
new file mode 100644
index 0000000000..336b9dca51
--- /dev/null
+++ b/Android/res/layout/list_item_tlog_position_event.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/list_item_tlog_position_event_loading.xml b/Android/res/layout/list_item_tlog_position_event_loading.xml
new file mode 100644
index 0000000000..533dc00285
--- /dev/null
+++ b/Android/res/layout/list_item_tlog_position_event_loading.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/list_item_tlog_raw_event.xml b/Android/res/layout/list_item_tlog_raw_event.xml
new file mode 100644
index 0000000000..a04dd3e659
--- /dev/null
+++ b/Android/res/layout/list_item_tlog_raw_event.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/layout/nav_header_main.xml b/Android/res/layout/nav_header_main.xml
index 996bc43640..646beaf88b 100644
--- a/Android/res/layout/nav_header_main.xml
+++ b/Android/res/layout/nav_header_main.xml
@@ -2,10 +2,10 @@
@@ -13,12 +13,15 @@
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:src="@drawable/ic_launcher" />
+ android:paddingTop="16dp"
+ android:src="@drawable/ic_account_circle_white_48dp" />
diff --git a/Android/res/layout/progress_bar.xml b/Android/res/layout/progress_bar.xml
new file mode 100644
index 0000000000..537d65c0b7
--- /dev/null
+++ b/Android/res/layout/progress_bar.xml
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/Android/res/layout/vertical_fastscroller.xml b/Android/res/layout/vertical_fastscroller.xml
new file mode 100644
index 0000000000..235e2cf70a
--- /dev/null
+++ b/Android/res/layout/vertical_fastscroller.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/res/menu/navigation_drawer_items.xml b/Android/res/menu/navigation_drawer_items.xml
index b2c4cc8a78..bb1f7306b8 100755
--- a/Android/res/menu/navigation_drawer_items.xml
+++ b/Android/res/menu/navigation_drawer_items.xml
@@ -18,9 +18,7 @@
+ android:title="@string/locator"/>
diff --git a/Android/res/values/colors.xml b/Android/res/values/colors.xml
index fdc5dc8119..576695b64f 100644
--- a/Android/res/values/colors.xml
+++ b/Android/res/values/colors.xml
@@ -1,6 +1,7 @@
+ #90000000
#90ffffff
#fff
#5d5d5d
@@ -11,7 +12,7 @@
#f7f6f6
#212121
- #068FD3
+ #00a6e3
#FAAF4C
#0a5242
diff --git a/Android/res/values/dimens.xml b/Android/res/values/dimens.xml
index 81a0df4049..4659660bc0 100644
--- a/Android/res/values/dimens.xml
+++ b/Android/res/values/dimens.xml
@@ -57,4 +57,13 @@
32dp
90dp
+
+ 8dp
+
+ 20dp
+ 50dp
+ 3dp
+ 6dp
+ 30dp
+ 2dp
\ No newline at end of file
diff --git a/Android/res/values/strings.xml b/Android/res/values/strings.xml
index fdc013fc7b..bd25a74c0f 100644
--- a/Android/res/values/strings.xml
+++ b/Android/res/values/strings.xml
@@ -612,4 +612,12 @@
%1$sº C
%1$s in
%1$s mm
+
+
+ Upload successful! Select to view…
+ Will try again later
+ Droneshare upload
+ Telemetry Logs
+ No tlog data loaded
+ No tlog position data
diff --git a/Android/res/values/styles.xml b/Android/res/values/styles.xml
index 744c04e27a..787af796e4 100644
--- a/Android/res/values/styles.xml
+++ b/Android/res/values/styles.xml
@@ -12,71 +12,73 @@
- ?android:attr/textAppearanceMedium
- @android:color/white
- sans-serif-condensed
- - left
-
+ - left
+
-
+
-
+ - 0dp
+
+ - left
+
+
+
+
+
+
+
+
+
+
-
-
+
-
+
-
+
-
-
+
+
+
+
+
-
-
-
+
-
+
+
-
+
+
+
+
+
+ - 4dp
+ - 4dp
+
-
+
-
+
-
+
-
-
+
+
+ - 16sp
+
+
+
-
+
-
-
-
+
-
-
+
-
+
-
-
@@ -540,7 +552,7 @@
- @drawable/below_shadow
-
+
+
+
+
+
diff --git a/Android/src/org/droidplanner/android/DroidPlannerApp.java b/Android/src/org/droidplanner/android/DroidPlannerApp.java
index 25fecd3360..abc7566eaa 100644
--- a/Android/src/org/droidplanner/android/DroidPlannerApp.java
+++ b/Android/src/org/droidplanner/android/DroidPlannerApp.java
@@ -4,6 +4,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
@@ -28,16 +29,18 @@
import com.squareup.leakcanary.LeakCanary;
import org.droidplanner.android.activities.helpers.BluetoothDevicesActivity;
+import org.droidplanner.android.droneshare.UploaderService;
+import org.droidplanner.android.droneshare.data.DroneShareDB;
+import org.droidplanner.android.droneshare.data.SessionDB;
import org.droidplanner.android.proxy.mission.MissionProxy;
import org.droidplanner.android.utils.LogToFileTree;
+import org.droidplanner.android.utils.TLogUtils;
import org.droidplanner.android.utils.Utils;
import org.droidplanner.android.utils.file.IO.ExceptionWriter;
import org.droidplanner.android.utils.prefs.DroidPlannerPrefs;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import io.fabric.sdk.android.Fabric;
@@ -55,6 +58,8 @@ public class DroidPlannerApp extends MultiDexApplication implements DroneListene
private static final long EVENTS_DISPATCHING_PERIOD = 200L; //MS
+ private static final long INVALID_SESSION_ID = -1L;
+
private static final AtomicBoolean isCellularNetworkOn = new AtomicBoolean(false);
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@@ -127,30 +132,6 @@ public void run() {
}
};
- private final Runnable eventsDispatcher = new Runnable() {
- @Override
- public void run() {
- handler.removeCallbacks(this);
-
- //Go through the events buffer and empty it
- for(Map.Entry entry: eventsBuffer.entrySet()){
- String event = entry.getKey();
- Bundle extras = entry.getValue();
-
- final Intent droneIntent = new Intent(event);
- if (extras != null)
- droneIntent.putExtras(extras);
- lbm.sendBroadcast(droneIntent);
- }
-
- eventsBuffer.clear();
-
- handler.postDelayed(this, EVENTS_DISPATCHING_PERIOD);
- }
- };
-
- private final Map eventsBuffer = new LinkedHashMap<>(200);
-
private final Handler handler = new Handler();
private final List apiListeners = new ArrayList();
@@ -165,10 +146,25 @@ public void run() {
private LogToFileTree logToFileTree;
+ private long currentSessionId = INVALID_SESSION_ID;
+ private SessionDB sessionDB;
+ private DroneShareDB droneShareDb;
+
@Override
public void onCreate() {
super.onCreate();
+ final Context context = getApplicationContext();
+
+ dpPrefs = DroidPlannerPrefs.getInstance(context);
+ lbm = LocalBroadcastManager.getInstance(context);
+
+ initLoggingAndAnalytics();
+ initDronekit();
+ initDatabases();
+ }
+
+ private void initLoggingAndAnalytics(){
//Init leak canary
LeakCanary.install(this);
@@ -195,9 +191,10 @@ public void uncaughtException(Thread thread, Throwable ex) {
if(BuildConfig.ENABLE_CRASHLYTICS) {
Fabric.with(context, new Crashlytics());
}
+ }
- dpPrefs = DroidPlannerPrefs.getInstance(context);
- lbm = LocalBroadcastManager.getInstance(context);
+ private void initDronekit(){
+ Context context = getApplicationContext();
controlTower = new ControlTower(context);
drone = new Drone(context);
@@ -209,6 +206,13 @@ public void uncaughtException(Thread thread, Throwable ex) {
registerReceiver(broadcastReceiver, intentFilter);
}
+ private void initDatabases(){
+ Context context = getApplicationContext();
+ sessionDB = new SessionDB(context);
+ droneShareDb = new DroneShareDB(context);
+ cleanupDroneSessions();
+ }
+
public void addApiListener(ApiListener listener) {
if (listener == null)
return;
@@ -306,26 +310,39 @@ public MissionProxy getMissionProxy() {
}
private ConnectionParameter retrieveConnectionParameters() {
- final int connectionType = dpPrefs.getConnectionParameterType();
+ final @ConnectionType.Type int connectionType = dpPrefs.getConnectionParameterType();
+
+ // Generate the uri for logging the tlog data for this session.
+ Uri tlogLoggingUri = TLogUtils.getTLogLoggingUri(getApplicationContext(),
+ connectionType, System.currentTimeMillis());
ConnectionParameter connParams;
switch (connectionType) {
case ConnectionType.TYPE_USB:
- connParams = ConnectionParameter.newUsbConnection(dpPrefs.getUsbBaudRate());
+ connParams = ConnectionParameter.newUsbConnection(dpPrefs.getUsbBaudRate(),
+ tlogLoggingUri, EVENTS_DISPATCHING_PERIOD);
break;
case ConnectionType.TYPE_UDP:
if (dpPrefs.isUdpPingEnabled()) {
- connParams = ConnectionParameter.newUdpConnection(dpPrefs.getUdpServerPort(),
- dpPrefs.getUdpPingReceiverIp(), dpPrefs.getUdpPingReceiverPort(), "Hello".getBytes());
+ connParams = ConnectionParameter.newUdpWithPingConnection(
+ dpPrefs.getUdpServerPort(),
+ dpPrefs.getUdpPingReceiverIp(),
+ dpPrefs.getUdpPingReceiverPort(),
+ "Hello".getBytes(),
+ ConnectionType.DEFAULT_UDP_PING_PERIOD,
+ tlogLoggingUri,
+ EVENTS_DISPATCHING_PERIOD);
}
else{
- connParams = ConnectionParameter.newUdpConnection(dpPrefs.getUdpServerPort());
+ connParams = ConnectionParameter.newUdpConnection(dpPrefs.getUdpServerPort(),
+ tlogLoggingUri, EVENTS_DISPATCHING_PERIOD);
}
break;
case ConnectionType.TYPE_TCP:
- connParams = ConnectionParameter.newTcpConnection(dpPrefs.getTcpServerIp(), dpPrefs.getTcpServerPort());
+ connParams = ConnectionParameter.newTcpConnection(dpPrefs.getTcpServerIp(),
+ dpPrefs.getTcpServerPort(), tlogLoggingUri, EVENTS_DISPATCHING_PERIOD);
break;
case ConnectionType.TYPE_BLUETOOTH:
@@ -337,7 +354,8 @@ private ConnectionParameter retrieveConnectionParameters() {
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else {
- connParams = ConnectionParameter.newBluetoothConnection(btAddress);
+ connParams = ConnectionParameter.newBluetoothConnection(btAddress,
+ tlogLoggingUri, EVENTS_DISPATCHING_PERIOD);
}
break;
@@ -355,6 +373,9 @@ public void onDroneEvent(String event, Bundle extras) {
switch (event) {
case AttributeEvent.STATE_CONNECTED: {
handler.removeCallbacks(disconnectionTask);
+
+ startDroneSession(System.currentTimeMillis());
+
startService(new Intent(getApplicationContext(), AppService.class));
final boolean isReturnToMeOn = dpPrefs.isReturnToMeEnabled();
@@ -379,27 +400,28 @@ public void onTimeout() {
if (extras != null)
droneIntent.putExtras(extras);
lbm.sendBroadcast(droneIntent);
-
- handler.postDelayed(eventsDispatcher, EVENTS_DISPATCHING_PERIOD);
break;
}
case AttributeEvent.STATE_DISCONNECTED: {
- handler.removeCallbacks(eventsDispatcher);
-
shouldWeTerminate();
final Intent droneIntent = new Intent(event);
if (extras != null)
droneIntent.putExtras(extras);
lbm.sendBroadcast(droneIntent);
+
+ endDroneSession();
+ // Fire the droneshare log uploader
+ UploaderService.kickStart(getApplicationContext());
break;
}
default: {
- //Buffer the remaining events, and only fire them at 30hz
- //TODO: remove this once the buffer is placed on the 3DR Services side
- eventsBuffer.put(event, extras);
+ final Intent droneIntent = new Intent(event);
+ if (extras != null)
+ droneIntent.putExtras(extras);
+ lbm.sendBroadcast(droneIntent);
break;
}
}
@@ -433,4 +455,41 @@ public void closeLogFile() {
logToFileTree.stopLoggingThread();
}
}
+
+ private void startDroneSession(long startTime) {
+ ConnectionParameter connParams = drone.getConnectionParameter();
+ @ConnectionType.Type int connectionType = connParams.getConnectionType();
+ final String connectionTypeLabel = ConnectionType.getConnectionTypeLabel(connectionType);
+ Uri tlogLoggingUri = connParams.getTLogLoggingUri();
+
+ // Record the starting drone session
+ currentSessionId = this.sessionDB.startSession(startTime, connectionTypeLabel, tlogLoggingUri);
+ if(tlogLoggingUri != null && dpPrefs.isDroneshareEnabled()){
+ //Create an entry in the droneshare upload queue
+ droneShareDb.queueDataUploadEntry(dpPrefs.getDroneshareLogin(), currentSessionId);
+ }
+ }
+
+ private void endDroneSession() {
+ //log into the database the disconnection time.
+ if(currentSessionId != INVALID_SESSION_ID) {
+ this.sessionDB.endSessions(System.currentTimeMillis(), currentSessionId);
+ }
+ }
+
+ private void cleanupDroneSessions(){
+ //Cleanup all the opened drone sessions
+ sessionDB.cleanupOpenedSessions(System.currentTimeMillis());
+
+ // Check for droneshare logs to upload.
+ UploaderService.kickStart(getApplicationContext());
+ }
+
+ public DroneShareDB getDroneShareDatabase(){
+ return droneShareDb;
+ }
+
+ public SessionDB getSessionDatabase(){
+ return sessionDB;
+ }
}
diff --git a/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java b/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java
index edc5875c16..a1b559d4d7 100644
--- a/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java
+++ b/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java
@@ -2,6 +2,7 @@
import android.content.Intent;
import android.content.res.Configuration;
+import android.graphics.Typeface;
import android.os.Bundle;
import android.support.design.widget.NavigationView;
import android.support.v4.content.LocalBroadcastManager;
@@ -15,11 +16,14 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
import org.droidplanner.android.R;
import org.droidplanner.android.activities.helpers.SuperUI;
import org.droidplanner.android.fragments.SettingsFragment;
import org.droidplanner.android.fragments.control.BaseFlightControlFragment;
+import org.droidplanner.android.tlog.TLogActivity;
import org.droidplanner.android.view.SlidingDrawer;
/**
@@ -56,6 +60,8 @@ public abstract class DrawerNavigationUI extends SuperUI implements SlidingDrawe
*/
private NavigationView navigationView;
+ private TextView accountLabel;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -121,6 +127,21 @@ public void setContentView(int layoutResID) {
navigationView = (NavigationView) findViewById(R.id.navigation_drawer_view);
navigationView.setNavigationItemSelectedListener(this);
+
+ View navigationHeaderView = navigationView.getHeaderView(0);
+ accountLabel = (TextView) navigationHeaderView.findViewById(R.id.account_screen_label);
+
+ LinearLayout llAccount = (LinearLayout) navigationHeaderView.findViewById(R.id.navigation_account);
+ if(llAccount != null) {
+ llAccount.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(getApplicationContext(), AccountActivity.class));
+ mDrawerLayout.closeDrawer(GravityCompat.START);
+ }
+ });
+ }
+
}
@Override
@@ -162,7 +183,7 @@ public boolean onNavigationItemSelected(MenuItem menuItem) {
break;
case R.id.navigation_locator:
- mNavigationIntent = new Intent(this, LocatorActivity.class);
+ mNavigationIntent = new Intent(this, TLogActivity.class);
break;
case R.id.navigation_params:
@@ -185,8 +206,7 @@ public boolean onNavigationItemSelected(MenuItem menuItem) {
break;
}
- DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
- drawer.closeDrawer(GravityCompat.START);
+ mDrawerLayout.closeDrawer(GravityCompat.START);
return true;
}
@@ -232,6 +252,10 @@ public void onResume(){
private void updateNavigationDrawer() {
final int navDrawerEntryId = getNavigationDrawerMenuItemId();
switch (navDrawerEntryId) {
+ case R.id.navigation_account:
+ accountLabel.setTypeface(null, Typeface.BOLD);
+ break;
+
default:
navigationView.setCheckedItem(navDrawerEntryId);
break;
diff --git a/Android/src/org/droidplanner/android/activities/EditorActivity.java b/Android/src/org/droidplanner/android/activities/EditorActivity.java
index 56d3c4f55f..e2d0401f99 100644
--- a/Android/src/org/droidplanner/android/activities/EditorActivity.java
+++ b/Android/src/org/droidplanner/android/activities/EditorActivity.java
@@ -569,10 +569,6 @@ public void onSelectionUpdate(List selected) {
showItemDetail(selectMissionDetailType(selected));
}
}
-
- final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
- if (planningMapFragment != null)
- planningMapFragment.postUpdate();
}
}
diff --git a/Android/src/org/droidplanner/android/activities/LocatorActivity.java b/Android/src/org/droidplanner/android/activities/LocatorActivity.java
index 5007202b0b..5c1b0a8e10 100644
--- a/Android/src/org/droidplanner/android/activities/LocatorActivity.java
+++ b/Android/src/org/droidplanner/android/activities/LocatorActivity.java
@@ -32,6 +32,9 @@
import java.util.List;
/**
+ * @deprecated
+ * TODO: remove this class, and its dependencies
+ *
* This implements the map locator activity. The map locator activity allows the user to find
* a lost drone using last known GPS positions from the tlogs.
*/
@@ -186,7 +189,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent returnInten
return;
}
- //Get the file's absolute path from the incoming intent
+// //Get the file's absolute path from the incoming intent
// final String tlogAbsolutePath = returnIntent.getStringExtra(ServiceDataContract.EXTRA_TLOG_ABSOLUTE_PATH);
//
// if (tlogOpener != null)
diff --git a/Android/src/org/droidplanner/android/dialogs/OkDialog.java b/Android/src/org/droidplanner/android/dialogs/OkDialog.java
index e9a7bc6692..f8ca9d4e78 100644
--- a/Android/src/org/droidplanner/android/dialogs/OkDialog.java
+++ b/Android/src/org/droidplanner/android/dialogs/OkDialog.java
@@ -18,6 +18,7 @@ public class OkDialog extends DialogFragment {
protected final static String EXTRA_MESSAGE = "message";
protected final static String EXTRA_BUTTON_LABEL = "button_label";
protected final static String EXTRA_DISMISS_DIALOG_WITHOUT_CLICKING = "dismiss_dialog_without_clicking";
+ protected final static String EXTRA_SHOW_CANCEL = "show_cancel";
public interface Listener {
void onOk();
@@ -32,27 +33,32 @@ public static OkDialog newInstance(Context context, String title, String msg) {
}
public static OkDialog newInstance(Context context, String title, String msg, Listener listener) {
- return newInstance(title, msg, DEFAULT_DISMISS_DIALOG_WITHOUT_CLICKING, context.getString(android.R.string.ok), listener);
+ return newInstance(context, title, msg, listener, false);
+ }
+
+ public static OkDialog newInstance(Context context, String title, String msg, Listener listener, boolean showCancel) {
+ return newInstance(title, msg, DEFAULT_DISMISS_DIALOG_WITHOUT_CLICKING, context.getString(android.R.string.ok), listener, showCancel);
}
public static OkDialog newInstance(String title, String msg, String buttonLabel) {
- return newInstance(title, msg, DEFAULT_DISMISS_DIALOG_WITHOUT_CLICKING, buttonLabel, null);
+ return newInstance(title, msg, DEFAULT_DISMISS_DIALOG_WITHOUT_CLICKING, buttonLabel, null, false);
}
public static OkDialog newInstance(String title, String msg, boolean dismissDialogWithoutClicking,
String buttonLabel) {
- return newInstance(title, msg, dismissDialogWithoutClicking, buttonLabel, null);
+ return newInstance(title, msg, dismissDialogWithoutClicking, buttonLabel, null, false);
}
public static OkDialog newInstance(String title, String msg, boolean dismissDialogWithoutClicking,
- String buttonLabel, Listener listener) {
+ String buttonLabel, Listener listener, boolean showCancel) {
OkDialog fragment = new OkDialog();
Bundle b = new Bundle();
b.putString(EXTRA_TITLE, title);
b.putString(EXTRA_MESSAGE, msg);
b.putString(EXTRA_BUTTON_LABEL, buttonLabel);
b.putBoolean(EXTRA_DISMISS_DIALOG_WITHOUT_CLICKING, dismissDialogWithoutClicking);
+ b.putBoolean(EXTRA_SHOW_CANCEL, showCancel);
fragment.setArguments(b);
fragment.listener = listener;
@@ -71,10 +77,35 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
final String buttonLabel = args.getString(EXTRA_BUTTON_LABEL, getString(android.R.string.ok));
dismissDialogWithoutClicking = args.getBoolean(EXTRA_DISMISS_DIALOG_WITHOUT_CLICKING, DEFAULT_DISMISS_DIALOG_WITHOUT_CLICKING);
- return new AlertDialog.Builder(getActivity())
+ boolean showCancel = args.getBoolean(EXTRA_SHOW_CANCEL);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(title)
.setMessage(message)
- .setNeutralButton(buttonLabel,
+ ;
+ if (showCancel) {
+ builder
+ .setPositiveButton(buttonLabel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (listener != null) {
+ listener.onOk();
+ }
+ dismiss();
+ }
+ }
+ )
+ .setNegativeButton(getString(android.R.string.cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if(listener != null){
+ listener.onCancel();
+ }
+ dismiss();
+ }
+ });
+ } else {
+ builder.setNeutralButton(buttonLabel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if (listener != null) {
@@ -83,9 +114,10 @@ public void onClick(DialogInterface dialog, int whichButton) {
dismiss();
}
}
- )
- .create();
+ );
+ }
+ return builder.create();
}
@Override
diff --git a/Android/src/org/droidplanner/android/droneshare/DroneshareClient.java b/Android/src/org/droidplanner/android/droneshare/DroneshareClient.java
new file mode 100644
index 0000000000..8836f8e19f
--- /dev/null
+++ b/Android/src/org/droidplanner/android/droneshare/DroneshareClient.java
@@ -0,0 +1,48 @@
+package org.droidplanner.android.droneshare;
+
+import android.util.Log;
+
+import com.geeksville.apiproxy.GCSHookImpl;
+
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * The Droidplanner specific bindings for the drone api FIXME - need to auto
+ * reconnect as needed like in the posixpilot version
+ *
+ */
+public class DroneshareClient extends GCSHookImpl {
+
+ private static final String TAG = DroneshareClient.class.getSimpleName();
+
+ public int interfaceNum = 0;
+
+ public void connect(String login, String password) {
+ try {
+ super.connect();
+
+ // Create user if necessary/possible
+ if (isUsernameAvailable(login))
+ createUser(login, password, null);
+ else
+ loginUser(login, password);
+
+ int sysId = 1;
+ setVehicleId("550e8400-e29b-41d4-a716-446655440000", interfaceNum, sysId, false);
+
+ startMission(false, UUID.randomUUID());
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed to connect due to " + ex);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ stopMission(true);
+
+ flush();
+ super.close();
+ }
+
+}
diff --git a/Android/src/org/droidplanner/android/droneshare/NetworkConnectivityReceiver.java b/Android/src/org/droidplanner/android/droneshare/NetworkConnectivityReceiver.java
new file mode 100644
index 0000000000..904af4c3d9
--- /dev/null
+++ b/Android/src/org/droidplanner/android/droneshare/NetworkConnectivityReceiver.java
@@ -0,0 +1,66 @@
+package org.droidplanner.android.droneshare;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import timber.log.Timber;
+
+/**
+ * When the device's network connectivity is restored, check and see if there's
+ * anymore data to upload.
+ */
+public class NetworkConnectivityReceiver extends BroadcastReceiver {
+
+ private static final String TAG = NetworkConnectivityReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
+ final boolean noConnectivity = intent.getBooleanExtra(
+ ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+
+ if (noConnectivity) {
+ // No connectivity. Keep the receiver enabled to listen for
+ // possible connectivity changes in the future.
+ return;
+ }
+
+ // There is connectivity! Restart the droneshare uploader service,
+ // and disable this connectivity receiver.
+ UploaderService.kickStart(context);
+
+ Timber.d("Disabling connectivity receiver.");
+ enableConnectivityReceiver(context, false);
+ }
+ }
+
+ public static boolean isNetworkAvailable(Context context) {
+ ConnectivityManager connectivityManager = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
+ return activeNetworkInfo != null && activeNetworkInfo.isConnected();
+ }
+
+ /**
+ * Toggles the connectivity listener component of the app.
+ *
+ * @param context
+ * application context
+ * @param enable
+ * true to enable
+ */
+ public static void enableConnectivityReceiver(Context context, boolean enable) {
+ final ComponentName receiver = new ComponentName(context, NetworkConnectivityReceiver.class);
+ final int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+
+ context.getPackageManager().setComponentEnabledSetting(receiver, newState,
+ PackageManager.DONT_KILL_APP);
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/droneshare/UploaderService.java b/Android/src/org/droidplanner/android/droneshare/UploaderService.java
new file mode 100644
index 0000000000..785e0c31e2
--- /dev/null
+++ b/Android/src/org/droidplanner/android/droneshare/UploaderService.java
@@ -0,0 +1,210 @@
+package org.droidplanner.android.droneshare;
+
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import android.support.v4.util.Pair;
+import android.text.TextUtils;
+
+import com.geeksville.apiproxy.rest.RESTClient;
+
+import org.droidplanner.android.DroidPlannerApp;
+import org.droidplanner.android.R;
+import org.droidplanner.android.droneshare.data.DroneShareDB;
+import org.droidplanner.android.utils.Utils;
+import org.droidplanner.android.utils.prefs.DroidPlannerPrefs;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import timber.log.Timber;
+
+/**
+ * Provides delayed uploads to the DroneShare service.
+ *
+ * If you send any intent to this service it will scan the tlog directory and
+ * upload any complete tlogs it finds.
+ */
+public class UploaderService extends IntentService {
+
+ private static final String DRONESHARE_PRIVACY = "DEFAULT";
+ static final String apiKey = "2d38fb2e.72afe7b3761d5ee6346c178fdd6b680f";
+
+ private static final int ONGOING_UPLOAD_NOTIFICATION_ID = 123;
+ private static final int UPLOAD_STATUS_NOTIFICATION_ID = 124;
+ public static final String ACTION_CHECK_FOR_DRONESHARE_UPLOADS = Utils.PACKAGE_NAME +
+ ".ACTION_CHECK_FOR_DRONESHARE_UPLOADS";
+
+ private DroidPlannerPrefs dpPrefs;
+ private DroneShareDB droneShareDb;
+
+ private NotificationManagerCompat notifyManager;
+ private Notification failedUploadNotification;
+
+ public UploaderService() {
+ super("DroneShare Uploader");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ final Context context = getApplicationContext();
+ dpPrefs = DroidPlannerPrefs.getInstance(context);
+ notifyManager = NotificationManagerCompat.from(context);
+
+ droneShareDb = ((DroidPlannerApp) getApplication()).getDroneShareDatabase();
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ final String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
+
+ // Check if droneshare is enabled, and the login credentials set before trying to do anything.
+ if (dpPrefs.isDroneshareEnabled()) {
+
+ switch (action) {
+ case ACTION_CHECK_FOR_DRONESHARE_UPLOADS:
+ List> dataToUpload = droneShareDb.getDataToUpload(dpPrefs.getDroneshareLogin());
+ if(!dataToUpload.isEmpty()){
+ if (NetworkConnectivityReceiver.isNetworkAvailable(getApplicationContext())) {
+ Timber.i("Preparing droneshare data for upload");
+ doUploads(dataToUpload);
+ } else {
+ Timber.w("Network offline.. Rescheduling droneshare data upload");
+
+ // Activating the network connectivity receiver so we can be restarted when connectivity is restored.
+ Timber.d("Activating connectivity receiver");
+ NetworkConnectivityReceiver.enableConnectivityReceiver(getApplicationContext(), true);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ private NotificationCompat.Builder generateNotificationBuilder() {
+ return new NotificationCompat.Builder(getApplicationContext())
+ .setContentTitle(getString(R.string.uploader_notification_title))
+ .setSmallIcon(R.drawable.ic_stat_notify)
+ .setAutoCancel(true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH);
+ }
+
+ private void doUploads(List> dataToUpload) {
+ String login = dpPrefs.getDroneshareLogin();
+ String password = dpPrefs.getDronesharePassword();
+
+ if (!login.isEmpty() && !password.isEmpty()) {
+ final Notification notification = generateNotificationBuilder()
+ .setContentText("Uploading tlog data")
+ .build();
+ startForeground(ONGOING_UPLOAD_NOTIFICATION_ID, notification);
+
+ try {
+ int numUploaded = 0;
+ for (Pair datumInfo : dataToUpload) {
+ long uploadId = datumInfo.first;
+
+ Uri dataUri = datumInfo.second;
+ File uploadFile = new File(dataUri.getPath());
+ if (uploadFile.isFile()) {
+ Timber.i("Starting upload for " + uploadFile);
+
+ String url = RESTClient.doUpload(uploadFile, login, password, null, apiKey, DRONESHARE_PRIVACY);
+ if (url != null) {
+ numUploaded++;
+ }
+
+ onUploadSuccess(uploadFile, url, numUploaded);
+ }
+ else{
+ Timber.w("TLog data file is not available.");
+ }
+
+ droneShareDb.commitUploadedData(uploadId, System.currentTimeMillis());
+ }
+ } catch (IOException e) {
+ Timber.e(e, "Unable to complete tlog data upload");
+ onUploadFailure(e);
+ }
+ stopForeground(true);
+ }
+ }
+
+ private void onUploadSuccess(File f, String viewURL, int numUploaded) {
+ if (viewURL == null) {
+ Timber.i("Server thought flight was boring");
+ notifyManager.cancel(ONGOING_UPLOAD_NOTIFICATION_ID);
+ } else {
+ Timber.i("Upload success: " + f + " url=" + viewURL);
+
+ // Attach the view URL
+ final PendingIntent pIntent = PendingIntent.getActivity(UploaderService.this, 0,
+ new Intent(Intent.ACTION_VIEW, Uri.parse(viewURL)),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ final Intent sendIntent = new Intent(Intent.ACTION_SEND).putExtra(
+ Intent.EXTRA_TEXT, viewURL).setType("text/plain");
+
+ final PendingIntent sendPIntent = PendingIntent.getActivity(UploaderService.this,
+ 0, sendIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ final NotificationCompat.Builder notifBuilder = generateNotificationBuilder()
+ .setContentText(getString(R.string.uploader_success_message))
+ .setContentIntent(pIntent)
+ // Attach a web link
+ .addAction(android.R.drawable.ic_menu_set_as, "Web", pIntent)
+ // Add a share link
+ .addAction(android.R.drawable.ic_menu_share, "Share", sendPIntent);
+
+ if (numUploaded > 1)
+ notifBuilder.setNumber(numUploaded);
+
+ updateUploadStatusNotification(notifBuilder.build());
+ }
+ }
+
+ private void onUploadFailure(Exception ex) {
+ String msg = ex.getMessage();
+ if(TextUtils.isEmpty(msg)) {
+ msg = "Upload Failed";
+ }
+
+ if (failedUploadNotification == null) {
+ failedUploadNotification = generateNotificationBuilder().setContentText(msg)
+ .setSubText(getString(R.string.uploader_fail_retry_message)).build();
+ }
+ updateUploadStatusNotification(failedUploadNotification);
+
+ if (!NetworkConnectivityReceiver.isNetworkAvailable(getApplicationContext())) {
+ // Activating the network connectivity receiver so we can be
+ // restarted when
+ // connectivity is restored.
+ Timber.d("Activating connectivity receiver");
+ NetworkConnectivityReceiver.enableConnectivityReceiver(getApplicationContext(),
+ true);
+ }
+ }
+
+ private void updateUploadStatusNotification(Notification notification) {
+ notifyManager.notify(UPLOAD_STATUS_NOTIFICATION_ID, notification);
+ }
+
+ public static void kickStart(Context context){
+ if(DroidPlannerPrefs.getInstance(context).isDroneshareEnabled()) {
+ context.startService(
+ new Intent(context, UploaderService.class)
+ .setAction(ACTION_CHECK_FOR_DRONESHARE_UPLOADS));
+ }
+ }
+}
diff --git a/Android/src/org/droidplanner/android/droneshare/data/DroneShareContract.java b/Android/src/org/droidplanner/android/droneshare/data/DroneShareContract.java
new file mode 100644
index 0000000000..0aa03eb68a
--- /dev/null
+++ b/Android/src/org/droidplanner/android/droneshare/data/DroneShareContract.java
@@ -0,0 +1,45 @@
+package org.droidplanner.android.droneshare.data;
+
+import android.provider.BaseColumns;
+
+/**
+ * Defines the schema for the DroneShare database
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+public final class DroneShareContract {
+
+ static final String DB_NAME = "droneshare";
+ static final int DB_VERSION = 2;
+
+ private DroneShareContract(){}
+
+ static String[] getSQLCreateEntries(){
+ return new String[]{
+ UploadData.SQL_CREATE_ENTRIES,
+ };
+ }
+
+ static String[] getSQLDeleteEntries(){
+ return new String[]{
+ UploadData.SQL_DELETE_ENTRIES,
+ };
+ }
+
+ static final class UploadData implements BaseColumns {
+ static final String TABLE_NAME = "upload_data";
+
+ static final String COL_SESSION_ID = "session_id";
+ static final String COL_DSHARE_USER = "drone_share_username";
+ static final String COL_DATA_UPLOAD_TIME = "data_upload_time";
+
+ static final String SQL_CREATE_ENTRIES =
+ "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
+ _ID + " INTEGER PRIMARY KEY," +
+ COL_DSHARE_USER + " TEXT NOT NULL," +
+ COL_SESSION_ID + " INTEGER NOT NULL," +
+ COL_DATA_UPLOAD_TIME + " INTEGER" +
+ " )";
+
+ static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TABLE_NAME;
+ }
+}
diff --git a/Android/src/org/droidplanner/android/droneshare/data/DroneShareDB.kt b/Android/src/org/droidplanner/android/droneshare/data/DroneShareDB.kt
new file mode 100644
index 0000000000..b6952dc737
--- /dev/null
+++ b/Android/src/org/droidplanner/android/droneshare/data/DroneShareDB.kt
@@ -0,0 +1,90 @@
+package org.droidplanner.android.droneshare.data
+
+import android.content.ContentValues
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+import android.net.Uri
+import android.support.v4.util.Pair
+import org.droidplanner.android.droneshare.data.DroneShareContract.UploadData
+import org.droidplanner.android.droneshare.data.SessionContract.SessionData
+import timber.log.Timber
+import java.util.*
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class DroneShareDB(context: Context) :
+ SQLiteOpenHelper(context, DroneShareContract.DB_NAME, null, DroneShareContract.DB_VERSION) {
+
+ override fun onCreate(db: SQLiteDatabase) {
+ Timber.i("Creating ${DroneShareContract.DB_NAME} database.")
+ val sqlCreateEntries = DroneShareContract.getSQLCreateEntries()
+ for(sqlCreateEntry in sqlCreateEntries) {
+ db.execSQL(sqlCreateEntry)
+ }
+ }
+
+ override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
+ Timber.i("Upgrading ${DroneShareContract.DB_NAME} database from version $oldVersion to version $newVersion")
+ val sqlDelEntries = DroneShareContract.getSQLDeleteEntries()
+ for(sqlDelEntry in sqlDelEntries) {
+ db.execSQL(sqlDelEntry)
+ }
+ onCreate(db)
+ }
+
+ fun queueDataUploadEntry(username: String, sessionId: Long){
+ val db = getWritableDatabase()
+
+ val values = ContentValues().apply{
+ put(UploadData.COL_DSHARE_USER, username)
+ put(UploadData.COL_SESSION_ID, sessionId)
+ }
+
+ db.insert(UploadData.TABLE_NAME, null, values)
+ }
+
+ /**
+ * Returns the list of data for the given user that have yet to be uploaded to droneshare
+ */
+ fun getDataToUpload(username: String): List> {
+ val db = getReadableDatabase()
+
+ val query = "SELECT ${UploadData._ID}, ${SessionData.COLUMN_NAME_TLOG_LOGGING_URI} " +
+ "FROM ${UploadData.TABLE_NAME} JOIN ${SessionData.TABLE_NAME} " +
+ "ON ${UploadData.COL_SESSION_ID} = ${SessionData._ID} " +
+ "WHERE ${UploadData.COL_DATA_UPLOAD_TIME} IS NULL " +
+ "AND ${SessionData.COLUMN_NAME_END_TIME} IS NOT NULL " +
+ "AND ${UploadData.COL_DSHARE_USER} LIKE ? " +
+ "ORDER BY ${UploadData._ID} ASC"
+ val selectionArgs = arrayOf(username)
+
+ val cursor = db.rawQuery(query, selectionArgs)
+ val result = ArrayList>(cursor.count)
+ if(cursor.moveToFirst()){
+ do {
+ val uploadId = cursor.getLong(cursor.getColumnIndex(UploadData._ID))
+ val dataUri = Uri.parse(cursor.getString(cursor.getColumnIndex(SessionData.COLUMN_NAME_TLOG_LOGGING_URI)))
+ result.add(Pair(uploadId, dataUri))
+ } while(cursor.moveToNext())
+ }
+
+ cursor.close()
+ return result
+ }
+
+ fun commitUploadedData(uploadId: Long, uploadTimeInMillis: Long){
+ val db = getWritableDatabase()
+
+ val values = ContentValues().apply {
+ put(UploadData.COL_DATA_UPLOAD_TIME, uploadTimeInMillis)
+ }
+
+ val selection = "${UploadData._ID} LIKE ?"
+ val selectionArgs = arrayOf(uploadId.toString())
+
+ db.update(UploadData.TABLE_NAME, values, selection, selectionArgs)
+ }
+
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/droneshare/data/SessionContract.java b/Android/src/org/droidplanner/android/droneshare/data/SessionContract.java
new file mode 100644
index 0000000000..8dbcd0dc78
--- /dev/null
+++ b/Android/src/org/droidplanner/android/droneshare/data/SessionContract.java
@@ -0,0 +1,82 @@
+package org.droidplanner.android.droneshare.data;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ * Defines the schema for the Session database.
+ */
+public final class SessionContract {
+
+ static final String DB_NAME = "session";
+ static final int DB_VERSION = 1;
+
+ //Private constructor to prevent instantiation.
+ private SessionContract(){}
+
+ static String getSqlCreateEntries(){
+ return SessionData.SQL_CREATE_ENTRIES;
+ }
+
+ static String getSqlDeleteEntries(){
+ return SessionData.SQL_DELETE_ENTRIES;
+ }
+
+ /**
+ * Defines the schema for the SessionData table.
+ */
+ public static final class SessionData implements BaseColumns {
+ static final String TABLE_NAME = "session_data";
+
+ static final String COLUMN_NAME_START_TIME ="start_time";
+ static final String COLUMN_NAME_END_TIME = "end_time";
+ static final String COLUMN_NAME_CONNECTION_TYPE = "connection_type";
+ static final String COLUMN_NAME_TLOG_LOGGING_URI = "tlog_logging_uri";
+
+ static final String SQL_CREATE_ENTRIES =
+ "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
+ _ID + " INTEGER PRIMARY KEY," +
+ COLUMN_NAME_START_TIME + " INTEGER NOT NULL," +
+ COLUMN_NAME_END_TIME + " INTEGER," +
+ COLUMN_NAME_CONNECTION_TYPE + " TEXT NOT NULL," +
+ COLUMN_NAME_TLOG_LOGGING_URI + " TEXT" +
+ " )";
+
+ static final String SQL_DELETE_ENTRIES =
+ "DROP TABLE IF EXISTS " + TABLE_NAME;
+
+ public final long id;
+ public final long startTime;
+ public final long endTime;
+ public final String connectionTypeLabel;
+ public final Uri tlogLoggingUri;
+
+ SessionData(long id, long startTime, long endTime, String connectionTypeLabel, Uri tlogLoggingUri) {
+ this.id = id;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.connectionTypeLabel = connectionTypeLabel;
+ this.tlogLoggingUri = tlogLoggingUri;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SessionData)) {
+ return false;
+ }
+
+ SessionData that = (SessionData) o;
+
+ return tlogLoggingUri != null ? tlogLoggingUri.equals(that.tlogLoggingUri) : that.tlogLoggingUri == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return tlogLoggingUri != null ? tlogLoggingUri.hashCode() : 0;
+ }
+ }
+}
diff --git a/Android/src/org/droidplanner/android/droneshare/data/SessionDB.java b/Android/src/org/droidplanner/android/droneshare/data/SessionDB.java
new file mode 100644
index 0000000000..51c5481df8
--- /dev/null
+++ b/Android/src/org/droidplanner/android/droneshare/data/SessionDB.java
@@ -0,0 +1,177 @@
+package org.droidplanner.android.droneshare.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+import org.droidplanner.android.droneshare.data.SessionContract.SessionData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import timber.log.Timber;
+
+/**
+ * Created by fhuya on 12/30/14.
+ */
+public class SessionDB extends SQLiteOpenHelper {
+
+ public SessionDB(Context context) {
+ super(context, SessionContract.DB_NAME, null, SessionContract.DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Timber.i("Creating session database.");
+ db.execSQL(SessionContract.getSqlCreateEntries());
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL(SessionContract.getSqlDeleteEntries());
+ onCreate(db);
+ }
+
+ /**
+ * Return a unique id for the session that can be used to close it
+ * @param startTimeInMillis
+ * @param connectionType
+ */
+ public long startSession(long startTimeInMillis, String connectionType, @Nullable Uri tlogLoggingUri){
+ //Get the data repository in write mode.
+ SQLiteDatabase db = getWritableDatabase();
+
+ ContentValues values = new ContentValues();
+ values.put(SessionContract.SessionData.COLUMN_NAME_START_TIME, startTimeInMillis);
+ values.put(SessionData.COLUMN_NAME_CONNECTION_TYPE, connectionType);
+ if(tlogLoggingUri != null) {
+ values.put(SessionData.COLUMN_NAME_TLOG_LOGGING_URI, tlogLoggingUri.toString());
+ }
+
+ return db.insert(SessionData.TABLE_NAME, null, values);
+ }
+
+ public void endSessions(long endTimeInMillis, long... rowIds){
+ if(rowIds == null || rowIds.length == 0)
+ return;
+
+ SQLiteDatabase db = getWritableDatabase();
+
+ ContentValues values = new ContentValues();
+ values.put(SessionData.COLUMN_NAME_END_TIME, endTimeInMillis);
+
+ boolean isFirst = true;
+ StringBuilder selection = new StringBuilder();
+ String[] selectionArgs = new String[rowIds.length];
+ int argIndex = 0;
+ for(long rowId : rowIds){
+ if(!isFirst) {
+ selection.append(" OR ");
+ }
+ else {
+ isFirst = false;
+ }
+ selection.append(SessionData._ID).append(" LIKE ?");
+
+ selectionArgs[argIndex++] = String.valueOf(rowId);
+ }
+
+ db.update(SessionData.TABLE_NAME, values, selection.toString(), selectionArgs);
+ }
+
+ public void cleanupOpenedSessions(long endTimeInMillis){
+ SQLiteDatabase db = getWritableDatabase();
+
+ ContentValues values = new ContentValues();
+ values.put(SessionData.COLUMN_NAME_END_TIME, endTimeInMillis);
+
+ String selection = SessionData.COLUMN_NAME_END_TIME + " IS NULL";
+ db.update(SessionData.TABLE_NAME, values, selection, null);
+ }
+
+ public SessionData getSessionData(long sessionId){
+ SQLiteDatabase db = getReadableDatabase();
+
+ String[] projection = {SessionData.COLUMN_NAME_START_TIME,
+ SessionData.COLUMN_NAME_END_TIME, SessionData.COLUMN_NAME_CONNECTION_TYPE,
+ SessionData.COLUMN_NAME_TLOG_LOGGING_URI};
+
+ String selection = SessionData._ID + " LIKE ?";
+ String[] selectionArgs = {String.valueOf(sessionId)};
+
+ Cursor cursor = db.query(SessionData.TABLE_NAME, projection, selection, selectionArgs, null,
+ null, null);
+ SessionData sessionData = null;
+ if(cursor.moveToFirst()){
+ long startTime = cursor.getLong(cursor.getColumnIndex(SessionData.COLUMN_NAME_START_TIME));
+ long endTime = cursor.getLong(cursor.getColumnIndex(SessionData.COLUMN_NAME_END_TIME));
+ String connectionTypeLabel = cursor.getString(cursor.getColumnIndex(SessionData.COLUMN_NAME_CONNECTION_TYPE));
+ String tlogEncodedUri = cursor.getString(cursor.getColumnIndex(SessionData.COLUMN_NAME_TLOG_LOGGING_URI));
+ Uri tlogLoggingUri = Uri.parse(tlogEncodedUri);
+ sessionData = new SessionData(sessionId, startTime, endTime, connectionTypeLabel, tlogLoggingUri);
+ }
+
+ cursor.close();
+ return sessionData;
+ }
+
+ public long[] getOpenedSessions(){
+ SQLiteDatabase db = getReadableDatabase();
+
+ String[] projection = {SessionData._ID};
+ String selection = SessionData.COLUMN_NAME_END_TIME + "IS NULL";
+
+ Cursor cursor = db.query(SessionData.TABLE_NAME, projection, selection, null, null, null, null);
+ long[] sessionIds = new long[cursor.getCount()];
+ int index = 0;
+ for(boolean hasNext = cursor.moveToFirst(); hasNext; hasNext = cursor.moveToNext()){
+ sessionIds[index++] = cursor.getLong(cursor.getColumnIndex(SessionData._ID));
+ }
+
+ cursor.close();
+ return sessionIds;
+ }
+
+ public List getCompletedSessions(boolean tlogLogged){
+ SQLiteDatabase db = getReadableDatabase();
+
+ String[] projection = {SessionData._ID, SessionData.COLUMN_NAME_START_TIME,
+ SessionData.COLUMN_NAME_END_TIME, SessionData.COLUMN_NAME_CONNECTION_TYPE,
+ SessionData.COLUMN_NAME_TLOG_LOGGING_URI};
+
+ String selection = SessionData.COLUMN_NAME_END_TIME + " IS NOT NULL";
+ if(tlogLogged){
+ selection += " AND " + SessionData.COLUMN_NAME_TLOG_LOGGING_URI + " IS NOT NULL";
+ }
+ String orderBy = SessionData.COLUMN_NAME_START_TIME + " ASC";
+
+ Cursor cursor = db.query(SessionData.TABLE_NAME, projection, selection, null, null, null, orderBy);
+ List sessionDataList = new ArrayList<>(cursor.getCount());
+ for(boolean hasNext = cursor.moveToFirst(); hasNext; hasNext = cursor.moveToNext()){
+ long id = cursor.getLong(cursor.getColumnIndex(SessionData._ID));
+ long startTime = cursor.getLong(cursor.getColumnIndex(SessionData.COLUMN_NAME_START_TIME));
+ long endTime = cursor.getLong(cursor.getColumnIndex(SessionData.COLUMN_NAME_END_TIME));
+ String connectionTypeLabel = cursor.getString(cursor.getColumnIndex(SessionData.COLUMN_NAME_CONNECTION_TYPE));
+ String tlogEncodedUri = cursor.getString(cursor.getColumnIndex(SessionData.COLUMN_NAME_TLOG_LOGGING_URI));
+ Uri tlogLoggingUri = Uri.parse(tlogEncodedUri);
+
+ sessionDataList.add(new SessionData(id, startTime, endTime, connectionTypeLabel, tlogLoggingUri));
+ }
+
+ cursor.close();
+ return sessionDataList;
+ }
+
+ public void removeSessionData(long id) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ String whereClause = SessionData._ID + " LIKE ?";
+ String[] whereArgs = {String.valueOf(id)};
+
+ db.delete(SessionData.TABLE_NAME, whereClause, whereArgs);
+ }
+}
diff --git a/Android/src/org/droidplanner/android/fragments/DroneMap.java b/Android/src/org/droidplanner/android/fragments/DroneMap.java
index af2d3b34f6..168c69f717 100644
--- a/Android/src/org/droidplanner/android/fragments/DroneMap.java
+++ b/Android/src/org/droidplanner/android/fragments/DroneMap.java
@@ -5,8 +5,8 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.res.Resources;
import android.os.Bundle;
-import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater;
@@ -27,18 +27,20 @@
import org.droidplanner.android.graphic.map.GraphicHome;
import org.droidplanner.android.maps.DPMap;
import org.droidplanner.android.maps.MarkerInfo;
+import org.droidplanner.android.maps.PolylineInfo;
import org.droidplanner.android.maps.providers.DPMapProvider;
import org.droidplanner.android.maps.providers.google_map.tiles.mapbox.offline.MapDownloader;
import org.droidplanner.android.proxy.mission.MissionProxy;
+import org.droidplanner.android.proxy.mission.item.MissionItemProxy;
+import org.droidplanner.android.proxy.mission.item.markers.MissionItemMarkerInfo;
import org.droidplanner.android.utils.Utils;
import org.droidplanner.android.utils.prefs.AutoPanMode;
import org.droidplanner.android.utils.prefs.DroidPlannerPrefs;
-import java.util.ArrayList;
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.Map;
public abstract class DroneMap extends ApiListenerFragment {
@@ -60,8 +62,6 @@ public abstract class DroneMap extends ApiListenerFragment {
eventFilter.addAction(ACTION_UPDATE_MAP);
}
- private static final List NO_EXTERNAL_MARKERS = Collections.emptyList();
-
private final BroadcastReceiver eventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -71,13 +71,20 @@ public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
switch (action) {
case ACTION_UPDATE_MAP:
+ guided.updateMarker(DroneMap.this);
+ break;
+
case AttributeEvent.HOME_UPDATED:
+ home.updateMarker(DroneMap.this);
+ break;
+
case MissionProxy.ACTION_MISSION_PROXY_UPDATE:
- postUpdate();
+ home.updateMarker(DroneMap.this);
+ onMissionUpdate();
break;
case AttributeEvent.GPS_POSITION: {
- mMapFragment.updateMarker(graphicDrone);
+ graphicDrone.updateMarker(DroneMap.this);
mMapFragment.updateDroneLeashPath(guided);
final Gps droneGps = drone.getAttribute(AttributeType.GPS);
if (droneGps != null && droneGps.isValid()) {
@@ -87,19 +94,19 @@ public void onReceive(Context context, Intent intent) {
}
case AttributeEvent.GUIDED_POINT_UPDATED:
- mMapFragment.updateMarker(guided);
+ guided.updateMarker(DroneMap.this);
mMapFragment.updateDroneLeashPath(guided);
break;
case AttributeEvent.HEARTBEAT_FIRST:
case AttributeEvent.HEARTBEAT_RESTORED:
case AttributeEvent.STATE_CONNECTED:
- mMapFragment.updateMarker(graphicDrone);
+ graphicDrone.updateMarker(DroneMap.this);
break;
case AttributeEvent.STATE_DISCONNECTED:
case AttributeEvent.HEARTBEAT_TIMEOUT:
- mMapFragment.updateMarker(graphicDrone);
+ graphicDrone.updateMarker(DroneMap.this);
break;
case AttributeEvent.CAMERA_FOOTPRINTS_UPDATED: {
@@ -130,79 +137,20 @@ public void onReceive(Context context, Intent intent) {
}
};
- private final Handler mHandler = new Handler();
-
- private final Runnable mUpdateMap = new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null && mMapFragment == null)
- return;
-
- final List missionMarkerInfos = missionProxy.getMarkersInfos();
- final List externalMarkers = collectMarkersFromProviders();
-
- final boolean isThereMissionMarkers = !missionMarkerInfos.isEmpty();
- final boolean isThereExternalMarkers = !externalMarkers.isEmpty();
- final boolean isHomeValid = home.isValid();
- final boolean isGuidedVisible = guided.isVisible();
-
- // Get the list of markers currently on the map.
- final Set markersOnTheMap = mMapFragment.getMarkerInfoList();
-
- if (!markersOnTheMap.isEmpty()) {
- if (isHomeValid) {
- markersOnTheMap.remove(home);
- }
-
- if (isGuidedVisible) {
- markersOnTheMap.remove(guided);
- }
-
- if (isThereMissionMarkers) {
- markersOnTheMap.removeAll(missionMarkerInfos);
- }
-
- if(isThereExternalMarkers)
- markersOnTheMap.removeAll(externalMarkers);
-
- mMapFragment.removeMarkers(markersOnTheMap);
- }
-
- if (isHomeValid) {
- mMapFragment.updateMarker(home);
- }
-
- if (isGuidedVisible) {
- mMapFragment.updateMarker(guided);
- }
-
- if (isThereMissionMarkers) {
- mMapFragment.updateMarkers(missionMarkerInfos, isMissionDraggable());
- }
-
- if(isThereExternalMarkers)
- mMapFragment.updateMarkers(externalMarkers, false);
-
- mMapFragment.updateMissionPath(missionProxy);
-
- mMapFragment.updatePolygonsPaths(missionProxy.getPolygonsPath());
-
- mHandler.removeCallbacks(this);
- }
- };
-
- private final ConcurrentLinkedQueue markerProviders = new ConcurrentLinkedQueue<>();
+ private final Map> missionMarkers = new HashMap<>();
+ private final LinkedList externalMarkersToAdd = new LinkedList<>();
+ private final LinkedList externalPolylinesToAdd = new LinkedList<>();
protected DPMap mMapFragment;
protected DroidPlannerPrefs mAppPrefs;
private GraphicHome home;
- public GraphicDrone graphicDrone;
- public GraphicGuided guided;
+ private GraphicDrone graphicDrone;
+ private GraphicGuided guided;
protected MissionProxy missionProxy;
- public Drone drone;
+ protected Drone drone;
protected Context context;
@@ -219,12 +167,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bu
return view;
}
- @Override
- public void onDetach() {
- super.onDetach();
- mHandler.removeCallbacksAndMessages(null);
- }
-
@Override
public void onApiConnected() {
if (mMapFragment != null)
@@ -236,12 +178,61 @@ public void onApiConnected() {
missionProxy = getMissionProxy();
home = new GraphicHome(drone, getContext());
+ mMapFragment.addMarker(home);
+
graphicDrone = new GraphicDrone(drone);
+ mMapFragment.addMarker(graphicDrone);
+
guided = new GraphicGuided(drone);
+ mMapFragment.addMarker(guided);
- postUpdate();
+ onMissionUpdate();
}
+ protected final void onMissionUpdate(){
+ Resources res = getResources();
+
+ mMapFragment.updateMissionPath(missionProxy);
+
+ mMapFragment.updatePolygonsPaths(missionProxy.getPolygonsPath());
+
+ //TODO: improve mission markers rendering performance
+ List proxyMissionItems = missionProxy.getItems();
+ // Clear the previous proxy mission item markers.
+ Map> newMissionMarkers = new HashMap<>(proxyMissionItems.size());
+
+ for(MissionItemProxy proxyItem : proxyMissionItems){
+ List proxyMarkers = missionMarkers.remove(proxyItem);
+ if(proxyMarkers == null){
+ proxyMarkers = MissionItemMarkerInfo.newInstance(proxyItem);
+
+ if(!proxyMarkers.isEmpty()){
+ // Add the new markers to the map.
+ mMapFragment.addMarkers(proxyMarkers, isMissionDraggable());
+ }
+ }
+ else {
+ //Refresh the proxy markers
+ for(MarkerInfo marker: proxyMarkers){
+ if (marker.isOnMap()) {
+ marker.updateMarker(DroneMap.this);
+ } else {
+ mMapFragment.addMarker(marker);
+ }
+ }
+ }
+ newMissionMarkers.put(proxyItem, proxyMarkers);
+ }
+
+ // Remove the now invalid mission items
+ for(List invalidMarkers : missionMarkers.values()) {
+ mMapFragment.removeMarkers(invalidMarkers);
+ }
+ missionMarkers.clear();
+
+ missionMarkers.putAll(newMissionMarkers);
+ }
+
public void downloadMapTiles(MapDownloader mapDownloader, int minimumZ, int maximumZ){
if(mMapFragment == null)
return;
@@ -269,7 +260,22 @@ private void updateMapFragment() {
fm.beginTransaction().replace(R.id.map_fragment_container, (Fragment) mMapFragment)
.commit();
}
- }
+
+ if(!externalMarkersToAdd.isEmpty()){
+ for(MarkerInfo markerInfo = externalMarkersToAdd.poll();
+ markerInfo != null && !externalMarkersToAdd.isEmpty();
+ markerInfo = externalMarkersToAdd.poll()){
+ mMapFragment.addMarker(markerInfo);
+ }
+ }
+ if (!externalPolylinesToAdd.isEmpty()) {
+ for(PolylineInfo polylineInfo = externalPolylinesToAdd.poll();
+ polylineInfo != null && !externalPolylinesToAdd.isEmpty();
+ polylineInfo = externalPolylinesToAdd.poll()){
+ mMapFragment.addPolyline(polylineInfo);
+ }
+ }
+ }
@Override
public void onPause() {
@@ -289,12 +295,6 @@ public void onStart() {
updateMapFragment();
}
- @Override
- public void onStop() {
- super.onStop();
- mHandler.removeCallbacksAndMessages(null);
- }
-
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
@@ -302,10 +302,6 @@ public void onAttach(Activity activity) {
mAppPrefs = DroidPlannerPrefs.getInstance(context);
}
- public final void postUpdate() {
- mHandler.post(mUpdateMap);
- }
-
protected int getMaxFlightPathSize() {
return 0;
}
@@ -379,38 +375,52 @@ public void skipMarkerClickEvents(boolean skip) {
mMapFragment.skipMarkerClickEvents(skip);
}
- public void addMapMarkerProvider(MapMarkerProvider provider){
- if(provider != null) {
- markerProviders.add(provider);
- postUpdate();
- }
- }
+ public void addMarker(MarkerInfo markerInfo){
+ if(markerInfo == null)
+ return;
- public void removeMapMarkerProvider(MapMarkerProvider provider){
- if(provider != null) {
- markerProviders.remove(provider);
- postUpdate();
- }
- }
+ if(mMapFragment != null) {
+ mMapFragment.addMarker(markerInfo);
+ }
+ else{
+ externalMarkersToAdd.add(markerInfo);
+ }
+ }
- public interface MapMarkerProvider {
- MarkerInfo[] getMapMarkers();
+ public void addPolyline(PolylineInfo polylineInfo) {
+ if (polylineInfo == null)
+ return;
+
+ if(mMapFragment != null) {
+ mMapFragment.addPolyline(polylineInfo);
+ }
+ else {
+ externalPolylinesToAdd.add(polylineInfo);
+ }
}
- private List collectMarkersFromProviders(){
- if(markerProviders.isEmpty())
- return NO_EXTERNAL_MARKERS;
+ public void removeMarker(MarkerInfo markerInfo){
+ if(markerInfo == null)
+ return;
- List markers = new ArrayList<>();
- for(MapMarkerProvider provider : markerProviders){
- MarkerInfo[] externalMarkers = provider.getMapMarkers();
- Collections.addAll(markers, externalMarkers);
- }
+ if(mMapFragment != null){
+ mMapFragment.removeMarker(markerInfo);
+ }
+ else{
+ externalMarkersToAdd.remove(markerInfo);
+ }
+ }
- if(markers.isEmpty())
- return NO_EXTERNAL_MARKERS;
+ public void removePolyline(PolylineInfo polylineInfo) {
+ if(polylineInfo == null)
+ return;
- return markers;
+ if(mMapFragment != null){
+ mMapFragment.removePolyline(polylineInfo);
+ }
+ else{
+ externalPolylinesToAdd.remove(polylineInfo);
+ }
}
public DPMap.VisibleMapArea getVisibleMapArea(){
diff --git a/Android/src/org/droidplanner/android/fragments/EditorMapFragment.java b/Android/src/org/droidplanner/android/fragments/EditorMapFragment.java
index 8fd59939c8..a532fe871c 100644
--- a/Android/src/org/droidplanner/android/fragments/EditorMapFragment.java
+++ b/Android/src/org/droidplanner/android/fragments/EditorMapFragment.java
@@ -15,6 +15,8 @@
import org.droidplanner.android.activities.interfaces.OnEditorInteraction;
import org.droidplanner.android.maps.DPMap;
import org.droidplanner.android.maps.MarkerInfo;
+import org.droidplanner.android.proxy.mission.MissionSelection;
+import org.droidplanner.android.proxy.mission.item.MissionItemProxy;
import org.droidplanner.android.proxy.mission.item.markers.MissionItemMarkerInfo;
import org.droidplanner.android.proxy.mission.item.markers.PolygonMarkerInfo;
import org.droidplanner.android.utils.prefs.AutoPanMode;
@@ -23,7 +25,8 @@
import java.util.List;
public class EditorMapFragment extends DroneMap implements DPMap.OnMapLongClickListener,
- DPMap.OnMarkerDragListener, DPMap.OnMapClickListener, DPMap.OnMarkerClickListener {
+ DPMap.OnMarkerDragListener, DPMap.OnMapClickListener, DPMap.OnMarkerClickListener,
+ MissionSelection.OnSelectionUpdateListener{
private OnEditorInteraction editorListener;
@@ -86,9 +89,16 @@ private void checkForWaypointMarker(MarkerInfo markerInfo) {
@Override
public void onApiConnected(){
super.onApiConnected();
+ missionProxy.selection.addSelectionUpdateListener(this);
zoomToFit();
}
+ @Override
+ public void onApiDisconnected(){
+ super.onApiDisconnected();
+ missionProxy.selection.removeSelectionUpdateListener(this);
+ }
+
@Override
public void onMapClick(LatLong point) {
editorListener.onMapClick(point);
@@ -154,4 +164,8 @@ public void zoomToFit(List itemsToFit){
}
}
+ @Override
+ public void onSelectionUpdate(List selected) {
+ onMissionUpdate();
+ }
}
diff --git a/Android/src/org/droidplanner/android/fragments/FlightDataFragment.java b/Android/src/org/droidplanner/android/fragments/FlightDataFragment.java
index 2d5cb4df28..beeb4f0e5b 100644
--- a/Android/src/org/droidplanner/android/fragments/FlightDataFragment.java
+++ b/Android/src/org/droidplanner/android/fragments/FlightDataFragment.java
@@ -33,6 +33,7 @@
import org.droidplanner.android.fragments.control.FlightControlManagerFragment;
import org.droidplanner.android.fragments.helpers.ApiListenerFragment;
import org.droidplanner.android.fragments.mode.FlightModePanel;
+import org.droidplanner.android.maps.MarkerInfo;
import org.droidplanner.android.utils.prefs.AutoPanMode;
import org.droidplanner.android.view.SlidingDrawer;
@@ -508,11 +509,11 @@ public void setGuidedClickListener(FlightMapFragment.OnGuidedClickListener liste
mapFragment.setGuidedClickListener(listener);
}
- public void addMapMarkerProvider(DroneMap.MapMarkerProvider provider) {
- mapFragment.addMapMarkerProvider(provider);
+ public void addMarker(MarkerInfo markerInfo){
+ mapFragment.addMarker(markerInfo);
}
- public void removeMapMarkerProvider(DroneMap.MapMarkerProvider provider) {
- mapFragment.removeMapMarkerProvider(provider);
+ public void removeMarker(MarkerInfo markerInfo){
+ mapFragment.removeMarker(markerInfo);
}
}
diff --git a/Android/src/org/droidplanner/android/fragments/FlightMapFragment.java b/Android/src/org/droidplanner/android/fragments/FlightMapFragment.java
index 11c253d499..569b955c81 100644
--- a/Android/src/org/droidplanner/android/fragments/FlightMapFragment.java
+++ b/Android/src/org/droidplanner/android/fragments/FlightMapFragment.java
@@ -138,7 +138,7 @@ public void onMapLongClick(LatLong coord) {
guidedClickListener.onGuidedClick(coord);
} else {
GuidedDialog dialog = new GuidedDialog();
- dialog.setCoord(DroneHelper.CoordToLatLang(coord));
+ dialog.setCoord(DroneHelper.coordToLatLng(coord));
dialog.setListener(this);
dialog.show(getChildFragmentManager(), "GUIDED dialog");
}
@@ -148,7 +148,7 @@ public void onMapLongClick(LatLong coord) {
@Override
public void onForcedGuidedPoint(LatLng coord) {
try {
- ControlApi.getApi(drone).goTo(DroneHelper.LatLngToCoord(coord), true, null);
+ ControlApi.getApi(drone).goTo(DroneHelper.latLngToCoord(coord), true, null);
} catch (Exception e) {
Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
}
diff --git a/Android/src/org/droidplanner/android/fragments/LocatorMapFragment.java b/Android/src/org/droidplanner/android/fragments/LocatorMapFragment.java
index 67985559e9..2ac8ee4b8b 100644
--- a/Android/src/org/droidplanner/android/fragments/LocatorMapFragment.java
+++ b/Android/src/org/droidplanner/android/fragments/LocatorMapFragment.java
@@ -19,6 +19,12 @@ protected boolean isMissionDraggable() {
return false;
}
+ @Override
+ public void onApiConnected(){
+ super.onApiConnected();
+ mMapFragment.addMarker(graphicLocator);
+ }
+
@Override
public boolean setAutoPanMode(AutoPanMode target) {
if(target == AutoPanMode.DISABLED)
@@ -30,7 +36,7 @@ public boolean setAutoPanMode(AutoPanMode target) {
public void updateLastPosition(LatLong lastPosition) {
graphicLocator.setLastPosition(lastPosition);
- mMapFragment.updateMarker(graphicLocator);
+ graphicLocator.updateMarker(this);
}
public void zoomToFit() {
diff --git a/Android/src/org/droidplanner/android/fragments/mode/ModeAutoFragment.java b/Android/src/org/droidplanner/android/fragments/mode/ModeAutoFragment.java
index e75373f218..2332962d79 100644
--- a/Android/src/org/droidplanner/android/fragments/mode/ModeAutoFragment.java
+++ b/Android/src/org/droidplanner/android/fragments/mode/ModeAutoFragment.java
@@ -100,8 +100,6 @@ public void onViewCreated(View view, Bundle savedInstanceState) {
waypointSelector.addScrollListener(this);
mission = drone.getAttribute(AttributeType.MISSION);
- final DroidPlannerApp dpApp = (DroidPlannerApp) getActivity().getApplication();
-
final MissionProxy missionProxy = getMissionProxy();
waypointSelectorAdapter = new NumericWheelAdapter(getActivity().getApplicationContext(),
R.layout.wheel_text_centered,
diff --git a/Android/src/org/droidplanner/android/fragments/mode/ModeFollowFragment.java b/Android/src/org/droidplanner/android/fragments/mode/ModeFollowFragment.java
index 1c121f1a6f..06221ee11f 100644
--- a/Android/src/org/droidplanner/android/fragments/mode/ModeFollowFragment.java
+++ b/Android/src/org/droidplanner/android/fragments/mode/ModeFollowFragment.java
@@ -28,19 +28,16 @@
import org.droidplanner.android.R;
import org.droidplanner.android.fragments.DroneMap;
import org.droidplanner.android.graphic.map.GuidedScanROIMarkerInfo;
-import org.droidplanner.android.maps.MarkerInfo;
import org.droidplanner.android.utils.Utils;
import org.droidplanner.android.utils.prefs.DroidPlannerPrefs;
import org.droidplanner.android.utils.unit.providers.length.LengthUnitProvider;
import org.droidplanner.android.view.spinnerWheel.CardWheelHorizontalView;
import org.droidplanner.android.view.spinnerWheel.adapters.LengthWheelAdapter;
-public class ModeFollowFragment extends ModeGuidedFragment implements OnItemSelectedListener, DroneMap.MapMarkerProvider {
+public class ModeFollowFragment extends ModeGuidedFragment implements OnItemSelectedListener {
private static final double DEFAULT_MIN_RADIUS = 2; //meters
- private static final int ROI_TARGET_MARKER_INDEX = 0;
-
private static final IntentFilter eventFilter = new IntentFilter(AttributeEvent.FOLLOW_UPDATE);
private final BroadcastReceiver eventReceiver = new BroadcastReceiver() {
@@ -61,16 +58,9 @@ public void onReceive(Context context, Intent intent) {
private final GuidedScanROIMarkerInfo roiMarkerInfo = new GuidedScanROIMarkerInfo();
- private final MarkerInfo[] emptyMarkers = {};
- private final MarkerInfo[] markers = new MarkerInfo[1];
-
private FollowType lastFollowType;
private Bundle lastFollowParams;
- {
- markers[ROI_TARGET_MARKER_INDEX] = roiMarkerInfo;
- }
-
private TextView modeDescription;
private Spinner spinner;
private ArrayAdapter adapter;
@@ -133,7 +123,7 @@ public void onApiConnected() {
onFollowTypeUpdate(followType, followState.getParams());
}
- parent.addMapMarkerProvider(this);
+ parent.addMarker(roiMarkerInfo);
getBroadcastManager().registerReceiver(eventReceiver, eventFilter);
}
@@ -205,7 +195,7 @@ private void updateModeDescription(FollowType followType) {
@Override
public void onApiDisconnected() {
super.onApiDisconnected();
- parent.removeMapMarkerProvider(this);
+ parent.removeMarker(roiMarkerInfo);
getBroadcastManager().unregisterReceiver(eventReceiver);
}
@@ -288,14 +278,6 @@ private void updateROITargetMarker(LatLong target) {
}
}
- @Override
- public MarkerInfo[] getMapMarkers() {
- if (roiMarkerInfo.isVisible())
- return markers;
- else
- return emptyMarkers;
- }
-
private static class FollowTypesAdapter extends ArrayAdapter {
private final LayoutInflater inflater;
diff --git a/Android/src/org/droidplanner/android/graphic/map/GraphicDrone.java b/Android/src/org/droidplanner/android/graphic/map/GraphicDrone.java
index 66dcef779d..2074ef17bc 100644
--- a/Android/src/org/droidplanner/android/graphic/map/GraphicDrone.java
+++ b/Android/src/org/droidplanner/android/graphic/map/GraphicDrone.java
@@ -13,7 +13,7 @@
import com.o3dr.services.android.lib.drone.property.Attitude;
import com.o3dr.services.android.lib.drone.property.Gps;
-public class GraphicDrone extends MarkerInfo.SimpleMarkerInfo {
+public class GraphicDrone extends MarkerInfo {
private Drone drone;
diff --git a/Android/src/org/droidplanner/android/graphic/map/GraphicGuided.java b/Android/src/org/droidplanner/android/graphic/map/GraphicGuided.java
index 72817cb783..98c00c501c 100644
--- a/Android/src/org/droidplanner/android/graphic/map/GraphicGuided.java
+++ b/Android/src/org/droidplanner/android/graphic/map/GraphicGuided.java
@@ -13,13 +13,13 @@
import org.droidplanner.android.R;
import org.droidplanner.android.maps.DPMap.PathSource;
-import org.droidplanner.android.maps.MarkerInfo;
import org.droidplanner.android.maps.MarkerWithText;
+import org.droidplanner.android.maps.MarkerInfo;
import java.util.ArrayList;
import java.util.List;
-public class GraphicGuided extends MarkerInfo.SimpleMarkerInfo implements PathSource {
+public class GraphicGuided extends MarkerInfo implements PathSource {
private final static String TAG = GraphicGuided.class.getSimpleName();
@@ -65,7 +65,6 @@ public LatLong getPosition() {
return guidedPoint == null ? null : guidedPoint.getCoordinate();
}
- @Override
public void setPosition(LatLong coord) {
try {
ControlApi.getApi(drone).goTo(coord, true, null);
diff --git a/Android/src/org/droidplanner/android/graphic/map/GraphicHome.java b/Android/src/org/droidplanner/android/graphic/map/GraphicHome.java
index f97b86a528..28c52da903 100644
--- a/Android/src/org/droidplanner/android/graphic/map/GraphicHome.java
+++ b/Android/src/org/droidplanner/android/graphic/map/GraphicHome.java
@@ -1,8 +1,5 @@
package org.droidplanner.android.graphic.map;
-import org.droidplanner.android.R;
-import org.droidplanner.android.maps.MarkerInfo;
-
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -17,9 +14,12 @@
import com.o3dr.services.android.lib.drone.property.Home;
import com.o3dr.services.android.lib.model.AbstractCommandListener;
+import org.droidplanner.android.R;
+import org.droidplanner.android.maps.MarkerInfo;
+
import timber.log.Timber;
-public class GraphicHome extends MarkerInfo.SimpleMarkerInfo {
+public class GraphicHome extends MarkerInfo {
private final Drone drone;
private final Context context;
@@ -57,7 +57,6 @@ public LatLong getPosition() {
return droneHome.getCoordinate();
}
- @Override
public void setPosition(LatLong position){
//Move the home location
final Home currentHome = drone.getAttribute(AttributeType.HOME);
diff --git a/Android/src/org/droidplanner/android/graphic/map/GraphicLocator.java b/Android/src/org/droidplanner/android/graphic/map/GraphicLocator.java
index 08d2178d8c..d6d38396c2 100644
--- a/Android/src/org/droidplanner/android/graphic/map/GraphicLocator.java
+++ b/Android/src/org/droidplanner/android/graphic/map/GraphicLocator.java
@@ -9,7 +9,7 @@
import org.droidplanner.android.R;
import org.droidplanner.android.maps.MarkerInfo;
-public class GraphicLocator extends MarkerInfo.SimpleMarkerInfo {
+public class GraphicLocator extends MarkerInfo {
private LatLong lastPosition;
diff --git a/Android/src/org/droidplanner/android/graphic/map/GuidedScanROIMarkerInfo.java b/Android/src/org/droidplanner/android/graphic/map/GuidedScanROIMarkerInfo.java
index 7eb4f314b4..76676e9cfa 100644
--- a/Android/src/org/droidplanner/android/graphic/map/GuidedScanROIMarkerInfo.java
+++ b/Android/src/org/droidplanner/android/graphic/map/GuidedScanROIMarkerInfo.java
@@ -8,13 +8,12 @@
import com.o3dr.services.android.lib.coordinate.LatLongAlt;
import org.droidplanner.android.R;
-import org.droidplanner.android.fragments.mode.ModeFollowFragment;
import org.droidplanner.android.maps.MarkerInfo;
/**
* Created by Fredia Huya-Kouadio on 1/27/15.
*/
-public class GuidedScanROIMarkerInfo extends MarkerInfo.SimpleMarkerInfo {
+public class GuidedScanROIMarkerInfo extends MarkerInfo {
public static final double DEFAULT_FOLLOW_ROI_ALTITUDE = 10; //meters
private LatLongAlt roiCoord;
diff --git a/Android/src/org/droidplanner/android/maps/DPMap.java b/Android/src/org/droidplanner/android/maps/DPMap.java
index d7db1d38bb..fc54a9ba82 100644
--- a/Android/src/org/droidplanner/android/maps/DPMap.java
+++ b/Android/src/org/droidplanner/android/maps/DPMap.java
@@ -14,7 +14,6 @@
import java.util.Collection;
import java.util.List;
-import java.util.Set;
/**
* Defines the functionality expected from the map providers.
@@ -57,142 +56,45 @@ public interface DPMap {
String PREF_ZOOM = "pref_map_zoom";
int DEFAULT_ZOOM_LEVEL = 17;
- interface PathSource {
- List getPathPoints();
- }
-
/**
- * Implemented by classes interested in map click events.
+ * Adds a coordinate to the drone's flight path.
+ *
+ * @param coord
+ * drone's coordinate
*/
- interface OnMapClickListener {
- /**
- * Triggered when the map is clicked.
- *
- * @param coord
- * location where the map was clicked.
- */
- void onMapClick(LatLong coord);
- }
+ void addFlightPathPoint(LatLong coord);
/**
- * Implemented by classes interested in map long click events.
+ * Draw the footprint of the camera in the ground
+ * @param footprintToBeDraw
*/
- interface OnMapLongClickListener {
- /**
- * Triggered when the map is long clicked.
- *
- * @param coord
- * location where the map was long clicked.
- */
- void onMapLongClick(LatLong coord);
- }
+ void addCameraFootprint(FootPrint footprintToBeDraw);
/**
- * Implemented by classes interested in marker(s) click events.
+ * Adds the marker corresponding to the given marker info
+ * argument.
+ *
+ * @param markerInfo
+ * used to generate
*/
- interface OnMarkerClickListener {
- /**
- * Triggered when a marker is clicked.
- *
- * @param markerInfo
- * info about the clicked marker
- * @return true if the listener has consumed the event.
- */
- boolean onMarkerClick(MarkerInfo markerInfo);
- }
+ void addMarker(MarkerInfo markerInfo);
/**
- * Callback interface for drag events on markers.
+ * Adds the markers corresponding to the given list of markers
+ * infos.
+ *
+ * @param markerInfoList
+ * source for the new markers to add
*/
- interface OnMarkerDragListener {
- /**
- * Called repeatedly while a marker is being dragged. The marker's
- * location can be accessed via {@link MarkerInfo#getPosition()}
- *
- * @param markerInfo
- * info about the marker that was dragged.
- */
- void onMarkerDrag(MarkerInfo markerInfo);
+ void addMarkers(List markerInfoList);
- /**
- * Called when a marker has finished being dragged. The marker's
- * location can be accessed via {@link MarkerInfo#getPosition()}
- *
- * @param markerInfo
- * info about the marker that was dragged.
- */
- void onMarkerDragEnd(MarkerInfo markerInfo);
-
- /**
- * Called when a marker starts being dragged. The marker's location can
- * be accessed via {@link MarkerInfo#getPosition()}; this position may
- * be different to the position prior to the start of the drag because
- * the marker is popped up above the touch point.
- *
- * @param markerInfo
- * info about the marker that was dragged.
- */
- void onMarkerDragStart(MarkerInfo markerInfo);
-
- }
-
- class VisibleMapArea implements Parcelable {
- public final LatLong nearLeft;
- public final LatLong nearRight;
- public final LatLong farLeft;
- public final LatLong farRight;
-
- public VisibleMapArea(LatLong farLeft, LatLong nearLeft, LatLong nearRight, LatLong farRight) {
- this.farLeft = farLeft;
- this.nearLeft = nearLeft;
- this.nearRight = nearRight;
- this.farRight = farRight;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(this.nearLeft, 0);
- dest.writeParcelable(this.nearRight, 0);
- dest.writeParcelable(this.farLeft, 0);
- dest.writeParcelable(this.farRight, 0);
- }
-
- protected VisibleMapArea(Parcel in) {
- this.nearLeft = in.readParcelable(LatLong.class.getClassLoader());
- this.nearRight = in.readParcelable(LatLong.class.getClassLoader());
- this.farLeft = in.readParcelable(LatLong.class.getClassLoader());
- this.farRight = in.readParcelable(LatLong.class.getClassLoader());
- }
-
- public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
- public VisibleMapArea createFromParcel(Parcel source) {
- return new VisibleMapArea(source);
- }
-
- public VisibleMapArea[] newArray(int size) {
- return new VisibleMapArea[size];
- }
- };
- }
-
- /**
- * Adds a coordinate to the drone's flight path.
- *
- * @param coord
- * drone's coordinate
- */
- void addFlightPathPoint(LatLong coord);
+ void addMarkers(List markerInfoList, boolean isDraggable);
/**
- * Draw the footprint of the camera in the ground
- * @param footprintToBeDraw
- */
- void addCameraFootprint(FootPrint footprintToBeDraw);
+ * Adds the polyline corresponding to the given polyline info
+ * @param polylineInfo Used to generate a map polyline
+ */
+ void addPolyline(PolylineInfo polylineInfo);
/**
* Remove all markers from the map.
@@ -204,6 +106,11 @@ public VisibleMapArea[] newArray(int size) {
*/
void clearFlightPath();
+ /**
+ * Remove all polylines from the map.
+ */
+ void clearPolylines();
+
/**
* Download the map tiles if supported
* @param mapDownloader
@@ -223,11 +130,6 @@ public VisibleMapArea[] newArray(int size) {
*/
float getMapZoomLevel();
- /**
- * @return a list of marker info currently on the map.
- */
- Set getMarkerInfoList();
-
/**
* @return the map maximum zoom level.
*/
@@ -265,6 +167,12 @@ public VisibleMapArea[] newArray(int size) {
List projectPathIntoMap(List pathPoints);
+ /**
+ * Remove the marker described by the given marker info
+ * @param markerInfo
+ */
+ void removeMarker(MarkerInfo markerInfo);
+
/**
* Remove the markers whose info is in the list from the map.
*
@@ -273,6 +181,12 @@ public VisibleMapArea[] newArray(int size) {
*/
void removeMarkers(Collection markerInfoList);
+ /**
+ * Remove the polyline described by the given polyline info
+ * @param polylineInfo
+ */
+ void removePolyline(PolylineInfo polylineInfo);
+
/**
* Stores the map camera settings.
*/
@@ -370,46 +284,6 @@ public VisibleMapArea[] newArray(int size) {
*/
void updateDroneLeashPath(PathSource pathSource);
- /**
- * Adds / updates the marker corresponding to the given marker info
- * argument.
- *
- * @param markerInfo
- * used to generate / update the marker
- */
- void updateMarker(MarkerInfo markerInfo);
-
- /**
- * Adds / updates the marker corresponding to the given marker info
- * argument.
- *
- * @param markerInfo
- * used to generate / update the marker
- * @param isDraggable
- * overwrites markerInfo draggable preference
- */
- void updateMarker(MarkerInfo markerInfo, boolean isDraggable);
-
- /**
- * Adds / updates the markers corresponding to the given list of markers
- * infos.
- *
- * @param markersInfos
- * source for the new markers to add/update
- */
- void updateMarkers(List markersInfos);
-
- /**
- * Adds / updates the markers corresponding to the given list of markers
- * infos.
- *
- * @param markersInfos
- * source for the new markers to add/update
- * @param isDraggable
- * overwrites markerInfo draggable preference
- */
- void updateMarkers(List markersInfos, boolean isDraggable);
-
/**
* Updates the mission path on the map.
*
@@ -445,5 +319,128 @@ public VisibleMapArea[] newArray(int size) {
void skipMarkerClickEvents(boolean skip);
void updateRealTimeFootprint(FootPrint footprint);
+
+ interface PathSource {
+ List getPathPoints();
+ }
+
+ /**
+ * Implemented by classes interested in map click events.
+ */
+ interface OnMapClickListener {
+ /**
+ * Triggered when the map is clicked.
+ *
+ * @param coord
+ * location where the map was clicked.
+ */
+ void onMapClick(LatLong coord);
+ }
+
+ /**
+ * Implemented by classes interested in map long click events.
+ */
+ interface OnMapLongClickListener {
+ /**
+ * Triggered when the map is long clicked.
+ *
+ * @param coord
+ * location where the map was long clicked.
+ */
+ void onMapLongClick(LatLong coord);
+ }
+
+ /**
+ * Implemented by classes interested in marker(s) click events.
+ */
+ interface OnMarkerClickListener {
+ /**
+ * Triggered when a marker is clicked.
+ *
+ * @param markerInfo
+ * info about the clicked marker
+ * @return true if the listener has consumed the event.
+ */
+ boolean onMarkerClick(MarkerInfo markerInfo);
+ }
+
+ /**
+ * Callback interface for drag events on markers.
+ */
+ interface OnMarkerDragListener {
+ /**
+ * Called repeatedly while a marker is being dragged. The marker's
+ * location can be accessed via {@link MarkerInfo#getPosition()}
+ *
+ * @param markerInfo
+ * info about the marker that was dragged.
+ */
+ void onMarkerDrag(MarkerInfo markerInfo);
+
+ /**
+ * Called when a marker has finished being dragged. The marker's
+ * location can be accessed via {@link MarkerInfo#getPosition()}
+ *
+ * @param markerInfo
+ * info about the marker that was dragged.
+ */
+ void onMarkerDragEnd(MarkerInfo markerInfo);
+
+ /**
+ * Called when a marker starts being dragged. The marker's location can
+ * be accessed via {@link MarkerInfo#getPosition()}; this position may
+ * be different to the position prior to the start of the drag because
+ * the marker is popped up above the touch point.
+ *
+ * @param markerInfo
+ * info about the marker that was dragged.
+ */
+ void onMarkerDragStart(MarkerInfo markerInfo);
+
+ }
+
+ class VisibleMapArea implements Parcelable {
+ public final LatLong nearLeft;
+ public final LatLong nearRight;
+ public final LatLong farLeft;
+ public final LatLong farRight;
+
+ public VisibleMapArea(LatLong farLeft, LatLong nearLeft, LatLong nearRight, LatLong farRight) {
+ this.farLeft = farLeft;
+ this.nearLeft = nearLeft;
+ this.nearRight = nearRight;
+ this.farRight = farRight;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(this.nearLeft, 0);
+ dest.writeParcelable(this.nearRight, 0);
+ dest.writeParcelable(this.farLeft, 0);
+ dest.writeParcelable(this.farRight, 0);
+ }
+
+ protected VisibleMapArea(Parcel in) {
+ this.nearLeft = in.readParcelable(LatLong.class.getClassLoader());
+ this.nearRight = in.readParcelable(LatLong.class.getClassLoader());
+ this.farLeft = in.readParcelable(LatLong.class.getClassLoader());
+ this.farRight = in.readParcelable(LatLong.class.getClassLoader());
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public VisibleMapArea createFromParcel(Parcel source) {
+ return new VisibleMapArea(source);
+ }
+
+ public VisibleMapArea[] newArray(int size) {
+ return new VisibleMapArea[size];
+ }
+ };
+ }
}
diff --git a/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java b/Android/src/org/droidplanner/android/maps/GoogleMapFragment.java
similarity index 77%
rename from Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java
rename to Android/src/org/droidplanner/android/maps/GoogleMapFragment.java
index 0a17dd579b..23ededc3d4 100644
--- a/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java
+++ b/Android/src/org/droidplanner/android/maps/GoogleMapFragment.java
@@ -1,4 +1,4 @@
-package org.droidplanner.android.maps.providers.google_map;
+package org.droidplanner.android.maps;
import android.app.Activity;
import android.content.BroadcastReceiver;
@@ -66,31 +66,33 @@
import org.droidplanner.android.R;
import org.droidplanner.android.fragments.SettingsFragment;
import org.droidplanner.android.graphic.map.GraphicHome;
-import org.droidplanner.android.maps.DPMap;
-import org.droidplanner.android.maps.MarkerInfo;
import org.droidplanner.android.maps.providers.DPMapProvider;
+import org.droidplanner.android.maps.providers.google_map.DownloadMapboxMapActivity;
+import org.droidplanner.android.maps.providers.google_map.GoogleMapPrefConstants;
+import org.droidplanner.android.maps.providers.google_map.GoogleMapPrefFragment;
import org.droidplanner.android.maps.providers.google_map.tiles.TileProviderManager;
import org.droidplanner.android.maps.providers.google_map.tiles.arcgis.ArcGISTileProviderManager;
import org.droidplanner.android.maps.providers.google_map.tiles.mapbox.MapboxTileProviderManager;
import org.droidplanner.android.maps.providers.google_map.tiles.mapbox.MapboxUtils;
import org.droidplanner.android.maps.providers.google_map.tiles.mapbox.offline.MapDownloader;
import org.droidplanner.android.utils.DroneHelper;
-import org.droidplanner.android.utils.collection.HashBiMap;
import org.droidplanner.android.utils.prefs.AutoPanMode;
import org.droidplanner.android.utils.prefs.DroidPlannerPrefs;
+import org.jetbrains.annotations.NotNull;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashSet;
+import java.util.HashMap;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import timber.log.Timber;
-public class GoogleMapFragment extends SupportMapFragment implements DPMap, GoogleApiClientManager.ManagerListener {
+public class GoogleMapFragment extends SupportMapFragment implements DPMap,
+ GoogleApiClientManager.ManagerListener {
private static final long USER_LOCATION_UPDATE_INTERVAL = 30000; // ms
private static final long USER_LOCATION_UPDATE_FASTEST_INTERVAL = 5000; // ms
@@ -101,6 +103,10 @@ public class GoogleMapFragment extends SupportMapFragment implements DPMap, Goog
private static final int ONLINE_TILE_PROVIDER_Z_INDEX = -1;
private static final int OFFLINE_TILE_PROVIDER_Z_INDEX = -2;
+ private static final int GET_DRAGGABLE_FROM_MARKER_INFO = -1;
+ private static final int IS_DRAGGABLE = 0;
+ private static final int IS_NOT_DRAGGABLE = 1;
+
private static final IntentFilter eventFilter = new IntentFilter();
static {
@@ -141,7 +147,8 @@ public void onMapReady(GoogleMap googleMap) {
}
};
- private final HashBiMap mBiMarkersMap = new HashBiMap();
+ private final Map markersMap = new HashMap<>();
+ private final Map polylinesMap = new HashMap<>();
private DroidPlannerPrefs mAppPrefs;
@@ -186,7 +193,7 @@ public void onMapReady(GoogleMap googleMap) {
if (mPanMode.get() == AutoPanMode.USER) {
Timber.d("User location changed.");
- updateCamera(DroneHelper.LocationToCoord(location), (int) getMap().getCameraPosition().zoom);
+ updateCamera(DroneHelper.locationToCoord(location), (int) getMap().getCameraPosition().zoom);
}
if (mLocationListener != null) {
@@ -200,7 +207,7 @@ public void onMapReady(GoogleMap googleMap) {
public void doRun() {
final Location myLocation = LocationServices.FusedLocationApi.getLastLocation(getGoogleApiClient());
if (myLocation != null) {
- updateCamera(DroneHelper.LocationToCoord(myLocation), GO_TO_MY_LOCATION_ZOOM);
+ updateCamera(DroneHelper.locationToCoord(myLocation), GO_TO_MY_LOCATION_ZOOM);
if (mLocationListener != null)
mLocationListener.onLocationChanged(myLocation);
@@ -259,7 +266,7 @@ protected void doRun() {
protected boolean useMarkerClickAsMapClick = false;
- private List polygonsPaths = new ArrayList();
+ private List polygonsPaths = new ArrayList<>();
protected DroidPlannerApp dpApp;
private Polygon footprintPoly;
@@ -408,7 +415,7 @@ public void downloadMapTiles(MapDownloader mapDownloader, VisibleMapArea mapRegi
@Override
public LatLong getMapCenter() {
- return DroneHelper.LatLngToCoord(getMap().getCameraPosition().target);
+ return DroneHelper.latLngToCoord(getMap().getCameraPosition().target);
}
@Override
@@ -432,17 +439,13 @@ public void selectAutoPanMode(AutoPanMode target) {
if (currentMode == target)
return;
- setAutoPanMode(currentMode, target);
+ mPanMode.compareAndSet(currentMode, target);
}
private Drone getDroneApi() {
return dpApp.getDrone();
}
- private void setAutoPanMode(AutoPanMode current, AutoPanMode update) {
- mPanMode.compareAndSet(current, update);
- }
-
@Override
public DPMapProvider getProvider() {
return DPMapProvider.GOOGLE_MAP;
@@ -450,7 +453,7 @@ public DPMapProvider getProvider() {
@Override
public void addFlightPathPoint(LatLong coord) {
- final LatLng position = DroneHelper.CoordToLatLang(coord);
+ final LatLng position = DroneHelper.coordToLatLng(coord);
if (maxFlightPathSize > 0) {
if (flightPath == null) {
@@ -461,7 +464,7 @@ public void addFlightPathPoint(LatLong coord) {
}
List oldFlightPath = flightPath.getPoints();
- if (oldFlightPath.size() > maxFlightPathSize) {
+ while (oldFlightPath.size() > maxFlightPathSize) {
oldFlightPath.remove(0);
}
oldFlightPath.add(position);
@@ -471,94 +474,139 @@ public void addFlightPathPoint(LatLong coord) {
@Override
public void clearMarkers() {
- for (Marker marker : mBiMarkersMap.valueSet()) {
- marker.remove();
+ for(MarkerInfo markerInfo : markersMap.values()){
+ markerInfo.removeProxyMarker();
}
- mBiMarkersMap.clear();
+ markersMap.clear();
}
+
@Override
- public void updateMarker(MarkerInfo markerInfo) {
- updateMarker(markerInfo, markerInfo.isDraggable());
+ public void clearPolylines() {
+ for(PolylineInfo info: polylinesMap.values()){
+ info.removeProxy();
+ }
+
+ polylinesMap.clear();
}
- @Override
- public void updateMarker(MarkerInfo markerInfo, boolean isDraggable) {
- // if the drone hasn't received a gps signal yet
+ private PolylineOptions fromPolylineInfo(PolylineInfo info){
+ return new PolylineOptions()
+ .addAll(DroneHelper.coordToLatng(info.getPoints()))
+ .clickable(info.isClickable())
+ .color(info.getColor())
+ .geodesic(info.isGeodesic())
+ .visible(info.isVisible())
+ .width(info.getWidth())
+ .zIndex(info.getZIndex());
+ }
+
+ private MarkerOptions fromMarkerInfo(MarkerInfo markerInfo, boolean isDraggable){
final LatLong coord = markerInfo.getPosition();
if (coord == null) {
- return;
+ return null;
}
- final LatLng position = DroneHelper.CoordToLatLang(coord);
- Marker marker = mBiMarkersMap.getValue(markerInfo);
- if (marker == null) {
- // Generate the marker
- generateMarker(markerInfo, position, isDraggable);
- } else {
- // Update the marker
- updateMarker(marker, markerInfo, position, isDraggable);
- }
- }
-
- private void generateMarker(MarkerInfo markerInfo, LatLng position, boolean isDraggable) {
final MarkerOptions markerOptions = new MarkerOptions()
- .position(position)
- .draggable(isDraggable)
- .alpha(markerInfo.getAlpha())
- .anchor(markerInfo.getAnchorU(), markerInfo.getAnchorV())
- .infoWindowAnchor(markerInfo.getInfoWindowAnchorU(),
- markerInfo.getInfoWindowAnchorV()).rotation(markerInfo.getRotation())
- .snippet(markerInfo.getSnippet()).title(markerInfo.getTitle())
- .flat(markerInfo.isFlat()).visible(markerInfo.isVisible());
+ .position(DroneHelper.coordToLatLng(coord))
+ .draggable(isDraggable)
+ .alpha(markerInfo.getAlpha())
+ .anchor(markerInfo.getAnchorU(), markerInfo.getAnchorV())
+ .infoWindowAnchor(markerInfo.getInfoWindowAnchorU(), markerInfo.getInfoWindowAnchorV())
+ .rotation(markerInfo.getRotation())
+ .snippet(markerInfo.getSnippet())
+ .title(markerInfo.getTitle())
+ .flat(markerInfo.isFlat())
+ .visible(markerInfo.isVisible());
final Bitmap markerIcon = markerInfo.getIcon(getResources());
if (markerIcon != null) {
markerOptions.icon(BitmapDescriptorFactory.fromBitmap(markerIcon));
}
- Marker marker = getMap().addMarker(markerOptions);
- mBiMarkersMap.put(markerInfo, marker);
+ return markerOptions;
}
- private void updateMarker(Marker marker, MarkerInfo markerInfo, LatLng position,
- boolean isDraggable) {
- final Bitmap markerIcon = markerInfo.getIcon(getResources());
- if (markerIcon != null) {
- marker.setIcon(BitmapDescriptorFactory.fromBitmap(markerIcon));
- }
+ private MarkerOptions fromMarkerInfo(MarkerInfo markerInfo){
+ return fromMarkerInfo(markerInfo, markerInfo.isDraggable());
+ }
- marker.setAlpha(markerInfo.getAlpha());
- marker.setAnchor(markerInfo.getAnchorU(), markerInfo.getAnchorV());
- marker.setInfoWindowAnchor(markerInfo.getInfoWindowAnchorU(),
- markerInfo.getInfoWindowAnchorV());
- marker.setPosition(position);
- marker.setRotation(markerInfo.getRotation());
- marker.setSnippet(markerInfo.getSnippet());
- marker.setTitle(markerInfo.getTitle());
- marker.setDraggable(isDraggable);
- marker.setFlat(markerInfo.isFlat());
- marker.setVisible(markerInfo.isVisible());
+ @Override
+ public void addMarker(final MarkerInfo markerInfo){
+ if(markerInfo == null || markerInfo.isOnMap())
+ return;
+
+ final MarkerOptions options = fromMarkerInfo(markerInfo);
+ if(options == null)
+ return;
+
+ getMapAsync(new OnMapReadyCallback() {
+ @Override
+ public void onMapReady(GoogleMap googleMap) {
+ Marker marker = googleMap.addMarker(options);
+ markerInfo.setProxyMarker(new ProxyMapMarker(marker));
+ markersMap.put(marker, markerInfo);
+ }
+ });
}
@Override
- public void updateMarkers(List markersInfos) {
- for (MarkerInfo info : markersInfos) {
- updateMarker(info);
- }
+ public void addMarkers(final List markerInfoList){
+ addMarkers(markerInfoList, GET_DRAGGABLE_FROM_MARKER_INFO);
}
@Override
- public void updateMarkers(List markersInfos, boolean isDraggable) {
- for (MarkerInfo info : markersInfos) {
- updateMarker(info, isDraggable);
- }
+ public void addMarkers(final List markerInfoList, boolean isDraggable){
+ addMarkers(markerInfoList, isDraggable ? IS_DRAGGABLE : IS_NOT_DRAGGABLE);
}
@Override
- public Set getMarkerInfoList() {
- return new HashSet(mBiMarkersMap.keySet());
+ public void addPolyline(final PolylineInfo polylineInfo) {
+ if(polylineInfo == null || polylineInfo.isOnMap())
+ return;
+
+ final PolylineOptions options = fromPolylineInfo(polylineInfo);
+
+ getMapAsync(new OnMapReadyCallback() {
+ @Override
+ public void onMapReady(GoogleMap googleMap) {
+ Polyline polyline = googleMap.addPolyline(options);
+ polylineInfo.setProxyPolyline(new ProxyMapPolyline(polyline));
+ polylinesMap.put(polyline, polylineInfo);
+ }
+ });
+ }
+
+ private void addMarkers(final List markerInfoList, int draggableType){
+ if(markerInfoList == null || markerInfoList.isEmpty())
+ return;
+
+ final int infoCount = markerInfoList.size();
+ final MarkerOptions[] optionsSet = new MarkerOptions[infoCount];
+ for(int i = 0; i < infoCount; i++){
+ MarkerInfo markerInfo = markerInfoList.get(i);
+ boolean isDraggable = draggableType == GET_DRAGGABLE_FROM_MARKER_INFO
+ ? markerInfo.isDraggable()
+ : draggableType == IS_DRAGGABLE;
+ optionsSet[i] = markerInfo.isOnMap() ? null : fromMarkerInfo(markerInfo, isDraggable);
+ }
+
+ getMapAsync(new OnMapReadyCallback() {
+ @Override
+ public void onMapReady(GoogleMap googleMap) {
+ for (int i = 0; i < infoCount; i++) {
+ MarkerOptions options = optionsSet[i];
+ if(options == null)
+ continue;
+
+ Marker marker = googleMap.addMarker(options);
+ MarkerInfo markerInfo = markerInfoList.get(i);
+ markerInfo.setProxyMarker(new ProxyMapMarker(marker));
+ markersMap.put(marker, markerInfo);
+ }
+ }
+ });
}
@Override
@@ -569,12 +617,22 @@ public List projectPathIntoMap(List path) {
for (LatLong point : path) {
LatLng coord = projection.fromScreenLocation(new Point((int) point
.getLatitude(), (int) point.getLongitude()));
- coords.add(DroneHelper.LatLngToCoord(coord));
+ coords.add(DroneHelper.latLngToCoord(coord));
}
return coords;
}
+ @Override
+ public void removeMarker(MarkerInfo markerInfo){
+ if(markerInfo == null || !markerInfo.isOnMap())
+ return;
+
+ ProxyMapMarker proxyMarker = (ProxyMapMarker) markerInfo.getProxyMarker();
+ markerInfo.removeProxyMarker();
+ markersMap.remove(proxyMarker.marker);
+ }
+
@Override
public void removeMarkers(Collection markerInfoList) {
if (markerInfoList == null || markerInfoList.isEmpty()) {
@@ -582,14 +640,20 @@ public void removeMarkers(Collection markerInfoList) {
}
for (MarkerInfo markerInfo : markerInfoList) {
- Marker marker = mBiMarkersMap.getValue(markerInfo);
- if (marker != null) {
- marker.remove();
- mBiMarkersMap.removeKey(markerInfo);
- }
+ removeMarker(markerInfo);
}
}
+ @Override
+ public void removePolyline(PolylineInfo polylineInfo) {
+ if(polylineInfo == null || !polylineInfo.isOnMap())
+ return;
+
+ ProxyMapPolyline proxy = (ProxyMapPolyline) polylineInfo.getProxyPolyline();
+ polylineInfo.removeProxy();
+ polylinesMap.remove(proxy.polyline);
+ }
+
@Override
public void setMapPadding(int left, int top, int right, int bottom) {
getMap().setPadding(left, top, right, bottom);
@@ -631,7 +695,7 @@ private void updateCamera(final LatLong coord) {
@Override
public void onMapReady(GoogleMap googleMap) {
final float zoomLevel = googleMap.getCameraPosition().zoom;
- googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(DroneHelper.CoordToLatLang(coord),
+ googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(DroneHelper.coordToLatLng(coord),
zoomLevel));
}
});
@@ -645,7 +709,7 @@ public void updateCamera(final LatLong coord, final float zoomLevel) {
@Override
public void onMapReady(GoogleMap googleMap) {
googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
- DroneHelper.CoordToLatLang(coord), zoomLevel));
+ DroneHelper.coordToLatLng(coord), zoomLevel));
}
});
}
@@ -653,7 +717,7 @@ public void onMapReady(GoogleMap googleMap) {
@Override
public void updateCameraBearing(float bearing) {
- final CameraPosition cameraPosition = new CameraPosition(DroneHelper.CoordToLatLang
+ final CameraPosition cameraPosition = new CameraPosition(DroneHelper.coordToLatLng
(getMapCenter()), getMapZoomLevel(), 0, bearing);
getMap().animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
@@ -663,7 +727,7 @@ public void updateDroneLeashPath(PathSource pathSource) {
List pathCoords = pathSource.getPathPoints();
final List pathPoints = new ArrayList(pathCoords.size());
for (LatLong coord : pathCoords) {
- pathPoints.add(DroneHelper.CoordToLatLang(coord));
+ pathPoints.add(DroneHelper.coordToLatLng(coord));
}
if (mDroneLeashPath == null) {
@@ -689,7 +753,7 @@ public void updateMissionPath(PathSource pathSource) {
List pathCoords = pathSource.getPathPoints();
final List pathPoints = new ArrayList<>(pathCoords.size());
for (LatLong coord : pathCoords) {
- pathPoints.add(DroneHelper.CoordToLatLang(coord));
+ pathPoints.add(DroneHelper.coordToLatLng(coord));
}
if (missionPath == null) {
@@ -721,7 +785,7 @@ public void updatePolygonsPaths(List> paths) {
POLYGONS_PATH_DEFAULT_WIDTH);
final List pathPoints = new ArrayList(contour.size());
for (LatLong coord : contour) {
- pathPoints.add(DroneHelper.CoordToLatLang(coord));
+ pathPoints.add(DroneHelper.coordToLatLng(coord));
}
pathOptions.addAll(pathPoints);
polygonsPaths.add(getMap().addPolygon(pathOptions));
@@ -736,7 +800,7 @@ public void addCameraFootprint(FootPrint footprintToBeDraw) {
pathOptions.fillColor(FOOTPRINT_FILL_COLOR);
for (LatLong vertex : footprintToBeDraw.getVertexInGlobalFrame()) {
- pathOptions.add(DroneHelper.CoordToLatLang(vertex));
+ pathOptions.add(DroneHelper.coordToLatLng(vertex));
}
getMap().addPolygon(pathOptions);
@@ -780,7 +844,7 @@ public void zoomToFit(List coords) {
if (!coords.isEmpty()) {
final List points = new ArrayList();
for (LatLong coord : coords)
- points.add(DroneHelper.CoordToLatLang(coord));
+ points.add(DroneHelper.coordToLatLng(coord));
final LatLngBounds bounds = getBounds(points);
getMapAsync(new OnMapReadyCallback() {
@@ -814,7 +878,7 @@ protected void doRun() {
final Location myLocation = LocationServices.FusedLocationApi.getLastLocation(getGoogleApiClient());
if (myLocation != null) {
final List updatedCoords = new ArrayList(coords);
- updatedCoords.add(DroneHelper.LocationToCoord(myLocation));
+ updatedCoords.add(DroneHelper.locationToCoord(myLocation));
zoomToFit(updatedCoords);
} else {
zoomToFit(coords);
@@ -852,7 +916,7 @@ private void setupMapListeners(GoogleMap googleMap) {
@Override
public void onMapClick(LatLng latLng) {
if (mMapClickListener != null) {
- mMapClickListener.onMapClick(DroneHelper.LatLngToCoord(latLng));
+ mMapClickListener.onMapClick(DroneHelper.latLngToCoord(latLng));
}
}
};
@@ -862,7 +926,7 @@ public void onMapClick(LatLng latLng) {
@Override
public void onMapLongClick(LatLng latLng) {
if (mMapLongClickListener != null) {
- mMapLongClickListener.onMapLongClick(DroneHelper.LatLngToCoord(latLng));
+ mMapLongClickListener.onMapLongClick(DroneHelper.latLngToCoord(latLng));
}
}
});
@@ -871,9 +935,9 @@ public void onMapLongClick(LatLng latLng) {
@Override
public void onMarkerDragStart(Marker marker) {
if (mMarkerDragListener != null) {
- final MarkerInfo markerInfo = mBiMarkersMap.getKey(marker);
+ final MarkerInfo markerInfo = markersMap.get(marker);
if(!(markerInfo instanceof GraphicHome)) {
- markerInfo.setPosition(DroneHelper.LatLngToCoord(marker.getPosition()));
+ markerInfo.setPosition(DroneHelper.latLngToCoord(marker.getPosition()));
mMarkerDragListener.onMarkerDragStart(markerInfo);
}
}
@@ -882,9 +946,9 @@ public void onMarkerDragStart(Marker marker) {
@Override
public void onMarkerDrag(Marker marker) {
if (mMarkerDragListener != null) {
- final MarkerInfo markerInfo = mBiMarkersMap.getKey(marker);
+ final MarkerInfo markerInfo = markersMap.get(marker);
if(!(markerInfo instanceof GraphicHome)) {
- markerInfo.setPosition(DroneHelper.LatLngToCoord(marker.getPosition()));
+ markerInfo.setPosition(DroneHelper.latLngToCoord(marker.getPosition()));
mMarkerDragListener.onMarkerDrag(markerInfo);
}
}
@@ -893,8 +957,8 @@ public void onMarkerDrag(Marker marker) {
@Override
public void onMarkerDragEnd(Marker marker) {
if (mMarkerDragListener != null) {
- final MarkerInfo markerInfo = mBiMarkersMap.getKey(marker);
- markerInfo.setPosition(DroneHelper.LatLngToCoord(marker.getPosition()));
+ final MarkerInfo markerInfo = markersMap.get(marker);
+ markerInfo.setPosition(DroneHelper.latLngToCoord(marker.getPosition()));
mMarkerDragListener.onMarkerDragEnd(markerInfo);
}
}
@@ -909,7 +973,7 @@ public boolean onMarkerClick(Marker marker) {
}
if (mMarkerClickListener != null) {
- final MarkerInfo markerInfo = mBiMarkersMap.getKey(marker);
+ final MarkerInfo markerInfo = markersMap.get(marker);
if (markerInfo != null)
return mMarkerClickListener.onMarkerClick(markerInfo);
}
@@ -1109,10 +1173,10 @@ public VisibleMapArea getVisibleMapArea(){
return null;
final VisibleRegion mapRegion = map.getProjection().getVisibleRegion();
- return new VisibleMapArea(DroneHelper.LatLngToCoord(mapRegion.farLeft),
- DroneHelper.LatLngToCoord(mapRegion.nearLeft),
- DroneHelper.LatLngToCoord(mapRegion.nearRight),
- DroneHelper.LatLngToCoord(mapRegion.farRight));
+ return new VisibleMapArea(DroneHelper.latLngToCoord(mapRegion.farLeft),
+ DroneHelper.latLngToCoord(mapRegion.nearLeft),
+ DroneHelper.latLngToCoord(mapRegion.nearRight),
+ DroneHelper.latLngToCoord(mapRegion.farRight));
}
@Override
@@ -1139,13 +1203,13 @@ public void updateRealTimeFootprint(FootPrint footprint) {
.fillColor(FOOTPRINT_FILL_COLOR);
for (LatLong vertex : pathPoints) {
- pathOptions.add(DroneHelper.CoordToLatLang(vertex));
+ pathOptions.add(DroneHelper.coordToLatLng(vertex));
}
footprintPoly = getMap().addPolygon(pathOptions);
} else {
List list = new ArrayList();
for (LatLong vertex : pathPoints) {
- list.add(DroneHelper.CoordToLatLang(vertex));
+ list.add(DroneHelper.coordToLatLng(vertex));
}
footprintPoly.setPoints(list);
}
@@ -1194,4 +1258,145 @@ public void onManagerStarted() {
public void onManagerStopped() {
}
+
+ private static class ProxyMapPolyline implements PolylineInfo.ProxyPolyline {
+
+ private final Polyline polyline;
+
+ private ProxyMapPolyline(Polyline polyline) {
+ this.polyline = polyline;
+ }
+
+ @Override
+ public void setPoints(@NotNull List extends LatLong> points) {
+ polyline.setPoints(DroneHelper.coordToLatng(points));
+ }
+
+ @Override
+ public void clickable(boolean clickable) {
+ polyline.setClickable(clickable);
+ }
+
+ @Override
+ public void color(int color) {
+ polyline.setColor(color);
+ }
+
+ @Override
+ public void geodesic(boolean geodesic) {
+ polyline.setGeodesic(geodesic);
+ }
+
+ @Override
+ public void visible(boolean visible) {
+ polyline.setVisible(visible);
+ }
+
+ @Override
+ public void width(float width) {
+ polyline.setWidth(width);
+ }
+
+ @Override
+ public void zIndex(float zIndex) {
+ polyline.setZIndex(zIndex);
+ }
+
+ @Override
+ public void remove() {
+ polyline.remove();
+ }
+ }
+
+ /**
+ * GoogleMap implementation of the ProxyMarker interface.
+ */
+ private static class ProxyMapMarker implements MarkerInfo.ProxyMarker {
+
+ private final Marker marker;
+
+ ProxyMapMarker(Marker marker){
+ this.marker = marker;
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ marker.setAlpha(alpha);
+ }
+
+ @Override
+ public void setAnchor(float anchorU, float anchorV) {
+ marker.setAnchor(anchorU, anchorV);
+ }
+
+ @Override
+ public void setDraggable(boolean draggable) {
+ marker.setDraggable(draggable);
+ }
+
+ @Override
+ public void setFlat(boolean flat) {
+ marker.setFlat(flat);
+ }
+
+ @Override
+ public void setIcon(Bitmap icon) {
+ if(icon != null) {
+ marker.setIcon(BitmapDescriptorFactory.fromBitmap(icon));
+ }
+ }
+
+ @Override
+ public void setInfoWindowAnchor(float anchorU, float anchorV) {
+ marker.setInfoWindowAnchor(anchorU, anchorV);
+ }
+
+ @Override
+ public void setPosition(LatLong coord) {
+ if(coord != null) {
+ marker.setPosition(DroneHelper.coordToLatLng(coord));
+ }
+ }
+
+ @Override
+ public void setRotation(float rotation) {
+ marker.setRotation(rotation);
+ }
+
+ @Override
+ public void setSnippet(String snippet) {
+ marker.setSnippet(snippet);
+ }
+
+ @Override
+ public void setTitle(String title) {
+ marker.setTitle(title);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ marker.setVisible(visible);
+ }
+
+ @Override
+ public void removeMarker(){
+ marker.remove();
+ }
+
+ @Override
+ public boolean equals(Object other){
+ if(this == other)
+ return true;
+
+ if(!(other instanceof ProxyMapMarker))
+ return false;
+
+ return this.marker.equals(((ProxyMapMarker) other).marker);
+ }
+
+ @Override
+ public int hashCode(){
+ return this.marker.hashCode();
+ }
+ }
}
diff --git a/Android/src/org/droidplanner/android/maps/MarkerInfo.java b/Android/src/org/droidplanner/android/maps/MarkerInfo.java
index ba19dff736..0a64ebc1ed 100644
--- a/Android/src/org/droidplanner/android/maps/MarkerInfo.java
+++ b/Android/src/org/droidplanner/android/maps/MarkerInfo.java
@@ -5,163 +5,235 @@
import com.o3dr.services.android.lib.coordinate.LatLong;
+import org.droidplanner.android.fragments.DroneMap;
+
/**
* Defines the methods expected from a MarkerInfo instance. The marker info
* object is used to gather information to generate a marker.
*/
-public interface MarkerInfo {
+public abstract class MarkerInfo {
+
+ private ProxyMarker proxyMarker;
+
+ void setProxyMarker(ProxyMarker proxyMarker){
+ this.proxyMarker = proxyMarker;
+ }
+
+ ProxyMarker getProxyMarker(){
+ return proxyMarker;
+ }
+
+ void removeProxyMarker(){
+ if(proxyMarker != null){
+ proxyMarker.removeMarker();
+ }
+
+ this.proxyMarker = null;
+ }
+
+ public final void updateMarker(DroneMap droneMap){
+ if(proxyMarker == null){
+ droneMap.addMarker(this);
+ }
+ else {
+ Resources res = droneMap.getResources();
+ proxyMarker.setAlpha(getAlpha());
+ proxyMarker.setAnchor(getAnchorU(), getAnchorV());
+ proxyMarker.setInfoWindowAnchor(getInfoWindowAnchorU(), getInfoWindowAnchorV());
+ proxyMarker.setPosition(getPosition());
+ proxyMarker.setRotation(getRotation());
+ proxyMarker.setSnippet(getSnippet());
+ proxyMarker.setTitle(getTitle());
+ proxyMarker.setDraggable(isDraggable());
+ proxyMarker.setFlat(isFlat());
+ proxyMarker.setVisible(isVisible());
+ proxyMarker.setIcon(getIcon(res));
+ }
+ }
+
+ public final boolean isOnMap(){
+ return proxyMarker != null;
+ }
/**
* @return marker's alpha (opacity) value.
*/
- float getAlpha();
+ public float getAlpha() {
+ return 1;
+ }
/**
* @return marker's horizontal distance normalized to [0,1], of the anchor
* from the left edge.
*/
- float getAnchorU();
+ public float getAnchorU() {
+ return 0.5F;
+ }
/**
* @return marker's vertical distance normalized to [0, 1], of the anchor
* from the top edge.
*/
- float getAnchorV();
+ public float getAnchorV() {
+ return 0.5F;
+ }
/**
* @return marker's icon resource id.
*/
- Bitmap getIcon(Resources res);
+ public abstract Bitmap getIcon(Resources res);
/**
* @return horizontal distance normalized to [0, 1] of the info window
* anchor from the left edge.
*/
- float getInfoWindowAnchorU();
+ public float getInfoWindowAnchorU() {
+ return 0;
+ }
/**
* @return vertical distance normalized to [0,1] of the info window anchor
* from the top edge.
*/
- float getInfoWindowAnchorV();
+ public float getInfoWindowAnchorV() {
+ return 0;
+ }
/**
* @return marker's map coordinate.
*/
- LatLong getPosition();
+ public abstract LatLong getPosition();
/**
* Updates the marker info's position.
*
- * @param coord
- * position update.
- */
- void setPosition(LatLong coord);
+ * @param coord
+ * position update.
+ */
+ public void setPosition(LatLong coord) {}
/**
* @return marker's rotation.
*/
- float getRotation();
+ public float getRotation() {
+ return 0;
+ }
/**
* @return string containing the marker's snippet.
*/
- String getSnippet();
+ public String getSnippet() {
+ return null;
+ }
/**
* @return the marker's title.
*/
- String getTitle();
+ public String getTitle() {
+ return null;
+ }
/**
* @return true if the marker's draggable.
*/
- boolean isDraggable();
+ public boolean isDraggable() {
+ return false;
+ }
/**
* @return true if the marker's flat.
*/
- boolean isFlat();
+ public boolean isFlat() {
+ return false;
+ }
/**
* @return true if the marker's visible.
*/
- boolean isVisible();
+ public boolean isVisible() {
+ return false;
+ }
/**
- * Default implementation of the MarkerInfo interface.
- */
- class SimpleMarkerInfo implements MarkerInfo {
-
- @Override
- public float getAlpha() {
- return 1;
- }
-
- @Override
- public float getAnchorU() {
- return 0;
- }
-
- @Override
- public float getAnchorV() {
- return 0;
- }
-
- @Override
- public Bitmap getIcon(Resources res) {
- return null;
- }
-
- @Override
- public float getInfoWindowAnchorU() {
- return 0;
- }
-
- @Override
- public float getInfoWindowAnchorV() {
- return 0;
- }
-
- @Override
- public LatLong getPosition() {
- return null;
- }
-
- @Override
- public void setPosition(LatLong coord) {
- }
-
- @Override
- public float getRotation() {
- return 0;
- }
-
- @Override
- public String getSnippet() {
- return null;
- }
-
- @Override
- public String getTitle() {
- return null;
- }
-
- @Override
- public boolean isDraggable() {
- return false;
- }
-
- @Override
- public boolean isFlat() {
- return false;
- }
-
- @Override
- public boolean isVisible() {
- return false;
- }
+ * Proxy interface to the actual map marker implementaton.
+ */
+ interface ProxyMarker {
+ /**
+ * Sets the alpha (opacity) of the marker.
+ * @param alpha Value from 0 to 1, where 0 means the marker is completely transparent
+ * and 1 means the marker is completely opaque.
+ */
+ void setAlpha(float alpha);
+
+ /**
+ * Sets the anchor point for the marker.
+ *
+ * The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.
+ * @param anchorU u-coordinate of the anchor, as a ratio of the image width (in the range [0, 1]).
+ * @param anchorV v-coordinate of the anchor, as a ratio of the image height (in the range [0, 1]).
+ */
+ void setAnchor (float anchorU, float anchorV);
+
+ /**
+ * Sets the draggability of the marker. When a marker is draggable,
+ * it can be moved by the user by long pressing on the marker.
+ * @param draggable
+ */
+ void setDraggable(boolean draggable);
+
+ /**
+ * Sets whether this marker should be flat against the map true
+ * or a billboard facing the camera false.
+ * @param flat
+ */
+ void setFlat(boolean flat);
+
+ /**
+ * Sets the icon for the marker.
+ * @param icon
+ */
+ void setIcon (Bitmap icon);
+
+ /**
+ * Specifies the point in the marker image at which to anchor the info window when it is displayed.
+ * @param anchorU u-coordinate of the info window anchor, as a ratio of the image width (in the range [0, 1]).
+ * @param anchorV v-coordinate of the info window anchor, as a ratio of the image height (in the range [0, 1]).
+ */
+ void setInfoWindowAnchor (float anchorU, float anchorV);
+
+ /**
+ * Sets the location of the marker.
+ * @param coord
+ */
+ void setPosition(LatLong coord);
+
+ /**
+ * Sets the rotation of the marker in degrees clockwise about the marker's anchor point.
+ * @param rotation
+ */
+ void setRotation (float rotation);
+
+ /**
+ * Sets the snippet of the marker.
+ * @param snippet
+ */
+ void setSnippet (String snippet);
+
+ /**
+ * Sets the title of the marker.
+ * @param title
+ */
+ void setTitle (String title);
+
+ /**
+ * Sets the visibility of this marker.
+ * @param visible
+ */
+ void setVisible (boolean visible);
+
+ /**
+ * Remove the marker from the map.
+ */
+ void removeMarker();
}
-
-
}
diff --git a/Android/src/org/droidplanner/android/maps/PolylineInfo.kt b/Android/src/org/droidplanner/android/maps/PolylineInfo.kt
new file mode 100644
index 0000000000..8eb1780da3
--- /dev/null
+++ b/Android/src/org/droidplanner/android/maps/PolylineInfo.kt
@@ -0,0 +1,87 @@
+package org.droidplanner.android.maps
+
+import android.graphics.Color
+import com.o3dr.services.android.lib.coordinate.LatLong
+
+/**
+ * Abstract representation of a polyline for a map implementing the DPMap interface
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+abstract class PolylineInfo {
+
+ protected var proxyPolyline: ProxyPolyline? = null
+
+ protected fun removeProxy(){
+ proxyPolyline?.remove()
+ proxyPolyline = null
+ }
+
+ fun isOnMap() = proxyPolyline != null
+
+ fun updatePolyline(){
+ proxyPolyline?.apply {
+ setPoints(getPoints())
+ color(getColor())
+ width(getWidth())
+ zIndex(getZIndex())
+ clickable(isClickable())
+ geodesic(isGeodesic())
+ visible(isVisible())
+ }
+ }
+
+ abstract fun getPoints(): List
+
+ open fun getZIndex() = 1F
+ open fun getColor() = Color.BLACK
+ open fun getWidth() = 6F
+ open fun isClickable() = false
+ open fun isGeodesic() = false
+ open fun isVisible() = true
+
+ /**
+ * Proxy interface to the actual map polyline implementation
+ */
+ interface ProxyPolyline {
+
+ /**
+ * Sets the vertices for the polyline
+ */
+ fun setPoints(points: List)
+
+ /**
+ * Specifies whether it's clickable
+ */
+ fun clickable(clickable: Boolean)
+
+ /**
+ * Sets the color for the polyline
+ */
+ fun color(color: Int)
+
+ /**
+ * Specifies whether to draw each segment as a geodesic
+ */
+ fun geodesic(geodesic: Boolean)
+
+ /**
+ * Specifies the visibility
+ */
+ fun visible(visible: Boolean)
+
+ /**
+ * Sets the width of the polyline in screen pixels
+ */
+ fun width(width: Float)
+
+ /**
+ * Specifies the polyline's zIndex, the order in which it will be drawn
+ */
+ fun zIndex(zIndex: Float)
+
+ /**
+ * Remove polyline from the map
+ */
+ fun remove()
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/maps/providers/DPMapProvider.java b/Android/src/org/droidplanner/android/maps/providers/DPMapProvider.java
index 6113b38c7e..3c919531c2 100644
--- a/Android/src/org/droidplanner/android/maps/providers/DPMapProvider.java
+++ b/Android/src/org/droidplanner/android/maps/providers/DPMapProvider.java
@@ -1,7 +1,7 @@
package org.droidplanner.android.maps.providers;
import org.droidplanner.android.maps.DPMap;
-import org.droidplanner.android.maps.providers.google_map.GoogleMapFragment;
+import org.droidplanner.android.maps.GoogleMapFragment;
import org.droidplanner.android.maps.providers.google_map.GoogleMapPrefFragment;
/**
diff --git a/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapActivity.kt b/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapActivity.kt
index 62862ce2ee..02b5be3539 100644
--- a/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapActivity.kt
+++ b/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapActivity.kt
@@ -7,6 +7,7 @@ import android.widget.ProgressBar
import android.widget.Toast
import com.google.android.gms.maps.model.CameraPosition
import org.droidplanner.android.R
+import org.droidplanner.android.maps.GoogleMapFragment
import org.droidplanner.android.maps.providers.google_map.tiles.mapbox.offline.MapDownloader
import org.droidplanner.android.maps.providers.google_map.tiles.offline.MapDownloaderListener
import org.droidplanner.android.utils.prefs.AutoPanMode
diff --git a/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapFragment.kt b/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapFragment.kt
index 767a00ded3..a2de3ed1a0 100644
--- a/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapFragment.kt
+++ b/Android/src/org/droidplanner/android/maps/providers/google_map/DownloadMapboxMapFragment.kt
@@ -7,7 +7,7 @@ import org.droidplanner.android.utils.prefs.AutoPanMode
/**
* Created by Fredia Huya-Kouadio on 6/17/15.
*/
-public class DownloadMapboxMapFragment : DroneMap() {
+class DownloadMapboxMapFragment : DroneMap() {
override fun isMissionDraggable() = false
override fun setAutoPanMode(target: AutoPanMode?): Boolean {
diff --git a/Android/src/org/droidplanner/android/proxy/mission/MissionProxy.java b/Android/src/org/droidplanner/android/proxy/mission/MissionProxy.java
index 0c95d9f247..83e5ad3690 100644
--- a/Android/src/org/droidplanner/android/proxy/mission/MissionProxy.java
+++ b/Android/src/org/droidplanner/android/proxy/mission/MissionProxy.java
@@ -32,11 +32,7 @@
import com.o3dr.services.android.lib.util.MathUtils;
import org.droidplanner.android.maps.DPMap;
-import org.droidplanner.android.maps.MarkerInfo;
import org.droidplanner.android.proxy.mission.item.MissionItemProxy;
-import org.droidplanner.android.proxy.mission.item.markers.MissionItemMarkerInfo;
-import org.droidplanner.android.proxy.mission.item.markers.PolygonMarkerInfo;
-import org.droidplanner.android.proxy.mission.item.markers.SurveyMarkerInfoProvider;
import org.droidplanner.android.utils.Utils;
import org.droidplanner.android.utils.analytics.GAUtils;
import org.droidplanner.android.utils.file.IO.MissionReader;
@@ -69,11 +65,13 @@ public class MissionProxy implements DPMap.PathSource {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (AttributeEvent.MISSION_DRONIE_CREATED.equals(action)
- || AttributeEvent.MISSION_UPDATED.equals(action)
- || AttributeEvent.MISSION_RECEIVED.equals(action)) {
- Mission droneMission = drone.getAttribute(AttributeType.MISSION);
- load(droneMission);
+ switch (action) {
+ case AttributeEvent.MISSION_DRONIE_CREATED:
+ case AttributeEvent.MISSION_UPDATED:
+ case AttributeEvent.MISSION_RECEIVED:
+ Mission droneMission = drone.getAttribute(AttributeType.MISSION);
+ load(droneMission);
+ break;
}
}
};
@@ -154,26 +152,11 @@ public Drone getDrone() {
return this.drone;
}
- /**
- * @return the map markers corresponding to this mission's command set.
- */
- public List getMarkersInfos() {
- List markerInfos = new ArrayList();
-
- for (MissionItemProxy itemProxy : missionItemProxies) {
- List itemMarkerInfos = itemProxy.getMarkerInfos();
- if (itemMarkerInfos != null && !itemMarkerInfos.isEmpty()) {
- markerInfos.addAll(itemMarkerInfos);
- }
- }
- return markerInfos;
- }
-
/**
* Update the state for this object based on the state of the Mission
* object.
*/
- public void load(Mission mission) {
+ private void load(Mission mission) {
load(mission, true);
}
@@ -181,21 +164,23 @@ private void load(Mission mission, boolean isNew) {
if (mission == null)
return;
- if (isNew) {
- currentMission = null;
- clearUndoBuffer();
- }
+ if(!mission.equals(currentMission)) {
+ if (isNew) {
+ currentMission = null;
+ clearUndoBuffer();
+ }
- selection.mSelectedItems.clear();
- missionItemProxies.clear();
+ selection.mSelectedItems.clear();
+ missionItemProxies.clear();
- for (MissionItem item : mission.getMissionItems()) {
- missionItemProxies.add(new MissionItemProxy(this, item));
- }
+ for (MissionItem item : mission.getMissionItems()) {
+ missionItemProxies.add(new MissionItemProxy(this, item));
+ }
- selection.notifySelectionUpdate();
+ selection.notifySelectionUpdate();
- notifyMissionUpdate(isNew);
+ notifyMissionUpdate(isNew);
+ }
}
private void clearUndoBuffer(){
@@ -406,43 +391,21 @@ public int getOrder(MissionItemProxy item) {
* @return The order of the first waypoint.
*/
public int getFirstWaypoint(){
- List markerInfos = getMarkersInfos();
-
- if(!markerInfos.isEmpty()) {
- MarkerInfo markerInfo = markerInfos.get(0);
- if(markerInfo instanceof MissionItemMarkerInfo){
- return getOrder(((MissionItemMarkerInfo)markerInfo).getMarkerOrigin());
- }
- else if(markerInfo instanceof SurveyMarkerInfoProvider){
- return getOrder(((SurveyMarkerInfoProvider)markerInfo).getMarkerOrigin());
- }
- else if(markerInfo instanceof PolygonMarkerInfo){
- return getOrder(((PolygonMarkerInfo)markerInfo).getMarkerOrigin());
- }
- }
+ if(missionItemProxies.isEmpty())
+ return 0;
- return 0;
+ return getOrder(missionItemProxies.get(0));
}
/**
* @return The order for the last waypoint.
*/
public int getLastWaypoint(){
- List markerInfos = getMarkersInfos();
+ int lastIndex = missionItemProxies.size() -1;
+ if(lastIndex < 0)
+ return 0;
- if(!markerInfos.isEmpty()) {
- MarkerInfo markerInfo = markerInfos.get(markerInfos.size() - 1);
- if(markerInfo instanceof MissionItemMarkerInfo){
- return getOrder(((MissionItemMarkerInfo)markerInfo).getMarkerOrigin());
- }
- else if(markerInfo instanceof SurveyMarkerInfoProvider){
- return getOrder(((SurveyMarkerInfoProvider)markerInfo).getMarkerOrigin());
- }
- else if(markerInfo instanceof PolygonMarkerInfo){
- return getOrder(((PolygonMarkerInfo)markerInfo).getMarkerOrigin());
- }
- }
- return 0;
+ return getOrder(missionItemProxies.get(lastIndex));
}
/**
@@ -505,13 +468,6 @@ public void replaceAll(List>> oldN
notifyMissionUpdate();
}
- /**
- * Reverse the order of the mission items renders.
- */
- public void reverse() {
- Collections.reverse(missionItemProxies);
- }
-
public void swap(int fromIndex, int toIndex) {
MissionItemProxy from = missionItemProxies.get(fromIndex);
MissionItemProxy to = missionItemProxies.get(toIndex);
diff --git a/Android/src/org/droidplanner/android/proxy/mission/MissionSelection.java b/Android/src/org/droidplanner/android/proxy/mission/MissionSelection.java
index 811c711c92..8523a7380e 100644
--- a/Android/src/org/droidplanner/android/proxy/mission/MissionSelection.java
+++ b/Android/src/org/droidplanner/android/proxy/mission/MissionSelection.java
@@ -1,10 +1,10 @@
package org.droidplanner.android.proxy.mission;
+import org.droidplanner.android.proxy.mission.item.MissionItemProxy;
+
import java.util.ArrayList;
import java.util.List;
-import org.droidplanner.android.proxy.mission.item.MissionItemProxy;
-
public class MissionSelection {
/**
* Classes interested in getting selection update should implement this
@@ -121,8 +121,10 @@ public List getSelected() {
* Deselects all mission items renders
*/
public void clearSelection() {
- mSelectedItems.clear();
- notifySelectionUpdate();
+ if(!mSelectedItems.isEmpty()) {
+ mSelectedItems.clear();
+ notifySelectionUpdate();
+ }
}
public void notifySelectionUpdate() {
diff --git a/Android/src/org/droidplanner/android/proxy/mission/item/MissionItemProxy.java b/Android/src/org/droidplanner/android/proxy/mission/item/MissionItemProxy.java
index 9bd26b57d5..248934e237 100644
--- a/Android/src/org/droidplanner/android/proxy/mission/item/MissionItemProxy.java
+++ b/Android/src/org/droidplanner/android/proxy/mission/item/MissionItemProxy.java
@@ -2,6 +2,7 @@
import com.o3dr.android.client.Drone;
import com.o3dr.services.android.lib.coordinate.LatLong;
+import com.o3dr.services.android.lib.coordinate.LatLongAlt;
import com.o3dr.services.android.lib.drone.mission.item.MissionItem;
import com.o3dr.services.android.lib.drone.mission.item.complex.SplineSurvey;
import com.o3dr.services.android.lib.drone.mission.item.complex.StructureScanner;
@@ -9,10 +10,7 @@
import com.o3dr.services.android.lib.drone.mission.item.spatial.Circle;
import com.o3dr.services.android.lib.util.MathUtils;
-import org.droidplanner.android.maps.MarkerInfo;
import org.droidplanner.android.proxy.mission.MissionProxy;
-import org.droidplanner.android.proxy.mission.item.fragments.MissionDetailFragment;
-import org.droidplanner.android.proxy.mission.item.markers.MissionItemMarkerInfo;
import java.util.ArrayList;
import java.util.List;
@@ -24,14 +22,6 @@
*/
public class MissionItemProxy {
- private final Drone.OnMissionItemsBuiltCallback missionItemBuiltListener = new Drone.OnMissionItemsBuiltCallback() {
- @Override
- public void onMissionItemsBuilt(MissionItem.ComplexItem[] complexItems) {
- mMission.notifyMissionUpdate(false);
- }
- };
-
-
/**
* This is the mission item object this class is built around.
*/
@@ -42,11 +32,6 @@ public void onMissionItemsBuilt(MissionItem.ComplexItem[] complexItems) {
*/
private final MissionProxy mMission;
- /**
- * This is the marker source for this mission item render.
- */
- private final List mMarkerInfos;
-
/**
* Used by the mission item list adapter to provide drag and drop support.
*/
@@ -57,7 +42,13 @@ public MissionItemProxy(MissionProxy mission, MissionItem missionItem) {
mMission = mission;
mMissionItem = missionItem;
- mMarkerInfos = MissionItemMarkerInfo.newInstance(this);
+
+ final Drone.OnMissionItemsBuiltCallback missionItemBuiltListener = new Drone.OnMissionItemsBuiltCallback() {
+ @Override
+ public void onMissionItemsBuilt(MissionItem.ComplexItem[] complexItems) {
+ mMission.notifyMissionUpdate(false);
+ }
+ };
if(mMissionItem instanceof SplineSurvey){
mMission.getDrone().buildMissionItemsAsync(new SplineSurvey[]{(SplineSurvey) mMissionItem}, missionItemBuiltListener);
@@ -78,8 +69,6 @@ public MissionProxy getMissionProxy() {
return mMission;
}
- public MissionProxy getMission(){return mMission;}
-
/**
* Provides access to the mission item instance.
*
@@ -89,14 +78,6 @@ public MissionItem getMissionItem() {
return mMissionItem;
}
- public MissionDetailFragment getDetailFragment() {
- return MissionDetailFragment.newInstance(mMissionItem.getType());
- }
-
- public List getMarkerInfos() {
- return mMarkerInfos;
- }
-
/**
* @param previousPoint
* Previous point on the path, null if there wasn't a previous
@@ -113,17 +94,17 @@ public List getPath(LatLong previousPoint) {
break;
case CIRCLE:
- Circle circle = (Circle) mMissionItem;
- for (int i = 0; i <= 360*circle.getTurns(); i += 10) {
- double startHeading = 0;
- if (previousPoint != null) {
- startHeading = MathUtils.getHeadingFromCoordinates(circle.getCoordinate(),
- previousPoint);
- }
- pathPoints.add(MathUtils.newCoordFromBearingAndDistance(circle.getCoordinate(),
- startHeading + i, circle.getRadius()));
- }
- break;
+ Circle circle = (Circle) mMissionItem;
+ LatLongAlt circleCenter = circle.getCoordinate();
+ double circleRadius = circle.getRadius();
+ double startHeading = previousPoint == null ? 0
+ : MathUtils.getHeadingFromCoordinates(circleCenter, previousPoint);
+ int circleTurnsAngle = 360 * circle.getTurns();
+ for (int i = 0; i <= circleTurnsAngle; i += 10) {
+ pathPoints.add(MathUtils.newCoordFromBearingAndDistance(circleCenter,
+ startHeading + i, circleRadius));
+ }
+ break;
case SPLINE_SURVEY:
case SURVEY:
@@ -151,4 +132,36 @@ public List getPath(LatLong previousPoint) {
public long getStableId(){
return stableId;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof MissionItemProxy)) {
+ return false;
+ }
+
+ MissionItemProxy that = (MissionItemProxy) o;
+
+ if (stableId != that.stableId) {
+ return false;
+ }
+ if (mMissionItem != null ? !mMissionItem.equals(that.mMissionItem) : that.mMissionItem != null) {
+ return false;
+ }
+ if (mMission != null ? !mMission.equals(that.mMission) : that.mMission != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mMissionItem != null ? mMissionItem.hashCode() : 0;
+ result = 31 * result + (mMission != null ? mMission.hashCode() : 0);
+ result = 31 * result + (int) (stableId ^ (stableId >>> 32));
+ return result;
+ }
}
diff --git a/Android/src/org/droidplanner/android/proxy/mission/item/markers/MissionItemMarkerInfo.java b/Android/src/org/droidplanner/android/proxy/mission/item/markers/MissionItemMarkerInfo.java
index 4944d0044f..09468ec786 100644
--- a/Android/src/org/droidplanner/android/proxy/mission/item/markers/MissionItemMarkerInfo.java
+++ b/Android/src/org/droidplanner/android/proxy/mission/item/markers/MissionItemMarkerInfo.java
@@ -1,23 +1,23 @@
package org.droidplanner.android.proxy.mission.item.markers;
-import java.util.ArrayList;
-import java.util.List;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+
+import com.o3dr.services.android.lib.coordinate.LatLong;
+import com.o3dr.services.android.lib.drone.mission.item.MissionItem;
import org.droidplanner.android.maps.MarkerInfo;
import org.droidplanner.android.maps.MarkerWithText;
import org.droidplanner.android.proxy.mission.MissionProxy;
import org.droidplanner.android.proxy.mission.item.MissionItemProxy;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-
-import com.o3dr.services.android.lib.coordinate.LatLong;
-import com.o3dr.services.android.lib.drone.mission.item.MissionItem;
+import java.util.ArrayList;
+import java.util.List;
/**
* Template class and factory for a mission item's marker source.
*/
-public abstract class MissionItemMarkerInfo extends MarkerInfo.SimpleMarkerInfo {
+public abstract class MissionItemMarkerInfo extends MarkerInfo {
protected final MissionItemProxy mMarkerOrigin;
diff --git a/Android/src/org/droidplanner/android/proxy/mission/item/markers/PolygonMarkerInfo.java b/Android/src/org/droidplanner/android/proxy/mission/item/markers/PolygonMarkerInfo.java
index 71364c0d62..620b82f33d 100644
--- a/Android/src/org/droidplanner/android/proxy/mission/item/markers/PolygonMarkerInfo.java
+++ b/Android/src/org/droidplanner/android/proxy/mission/item/markers/PolygonMarkerInfo.java
@@ -1,5 +1,8 @@
package org.droidplanner.android.proxy.mission.item.markers;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+
import com.o3dr.services.android.lib.coordinate.LatLong;
import com.o3dr.services.android.lib.drone.mission.item.complex.Survey;
@@ -8,7 +11,7 @@
/**
*/
-public class PolygonMarkerInfo extends MarkerInfo.SimpleMarkerInfo {
+public class PolygonMarkerInfo extends MarkerInfo {
private LatLong mPoint;
private final MissionItemProxy markerOrigin;
@@ -41,6 +44,11 @@ public float getAnchorV() {
return 0.5f;
}
+ @Override
+ public Bitmap getIcon(Resources res) {
+ return null;
+ }
+
@Override
public com.o3dr.services.android.lib.coordinate.LatLong getPosition() {
return mPoint;
diff --git a/Android/src/org/droidplanner/android/tlog/TLogActivity.kt b/Android/src/org/droidplanner/android/tlog/TLogActivity.kt
new file mode 100644
index 0000000000..64871a7a90
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/TLogActivity.kt
@@ -0,0 +1,191 @@
+package org.droidplanner.android.tlog
+
+import android.os.Bundle
+import android.os.Handler
+import android.support.design.widget.TabLayout
+import android.support.v4.view.ViewPager
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.TextView
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import org.droidplanner.android.R
+import org.droidplanner.android.activities.DrawerNavigationUI
+import org.droidplanner.android.droneshare.data.SessionContract
+import org.droidplanner.android.droneshare.data.SessionContract.SessionData
+import org.droidplanner.android.tlog.adapters.TLogDataAdapter
+import org.droidplanner.android.tlog.adapters.TLogViewerAdapter
+import org.droidplanner.android.tlog.interfaces.TLogDataProvider
+import org.droidplanner.android.tlog.viewers.TLogViewer
+import timber.log.Timber
+import java.util.*
+
+/**
+ * Created by fredia on 6/12/16.
+ */
+class TLogActivity : DrawerNavigationUI(), TLogDataAdapter.Listener, TLogDataProvider {
+
+ companion object {
+ private const val EXTRA_LOADED_EVENTS = "extra_loaded_events"
+ private const val EXTRA_LOADING_DATA = "extra_loading_data"
+ const val EXTRA_CURRENT_SESSION_ID = "extra_current_session_id"
+ }
+
+ private val handler = Handler()
+
+ private val tlogSubscribers = HashSet()
+ private val loadedEvents = ArrayList(100000)
+
+ private var isLoadingData = false
+ private var dataLoader: TLogDataLoader? = null
+ private var currentSessionData: SessionData? = null
+
+ private val loadingProgress by lazy {
+ findViewById(R.id.progress_bar_container)
+ }
+
+ private val sessionTitleView by lazy {
+ findViewById(R.id.tlog_session_title) as TextView?
+ }
+
+ override fun getNavigationDrawerMenuItemId() = R.id.navigation_locator
+
+ override fun getToolbarId() = R.id.toolbar
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_tlog)
+
+ val viewPager = findViewById(R.id.container) as ViewPager?
+ viewPager?.adapter = TLogViewerAdapter(supportFragmentManager)
+
+ val tabLayout = findViewById(R.id.tabs) as TabLayout?
+ tabLayout?.setupWithViewPager(viewPager)
+
+ // Reload the loaded tlog events (if they exists)
+ if (savedInstanceState != null) {
+
+ val sessionId = savedInstanceState.getLong(EXTRA_CURRENT_SESSION_ID, -1L)
+ if(sessionId != -1L){
+ currentSessionData = dpApp.sessionDatabase.getSessionData(sessionId)
+ }
+
+ val wasLoadingData = savedInstanceState.getBoolean(EXTRA_LOADING_DATA)
+ if(wasLoadingData){
+ if(currentSessionData != null){
+ onTLogSelected(currentSessionData!!, true)
+ }
+ }
+ else{
+ val savedEvents = savedInstanceState.getSerializable(EXTRA_LOADED_EVENTS) as ArrayList?
+ if (savedEvents != null) {
+ loadedEvents.addAll(savedEvents)
+ }
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ super.onCreateOptionsMenu(menu)
+ menuInflater.inflate(R.menu.menu_locator, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.menu_open_tlog_file -> {
+ // Open a dialog showing the app generated tlog files
+ val tlogPicker = TLogDataPicker()
+ tlogPicker.arguments = Bundle().apply{
+ putLong(EXTRA_CURRENT_SESSION_ID, currentSessionData?.id?: -1L)
+ }
+ tlogPicker.show(supportFragmentManager, "TLog Data Picker")
+ return true
+ }
+
+ else -> return super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean(EXTRA_LOADING_DATA, isLoadingData)
+
+ if (!isLoadingData && loadedEvents.isNotEmpty()) {
+ outState.putSerializable(EXTRA_LOADED_EVENTS, loadedEvents)
+ }
+
+ if(currentSessionData != null){
+ outState.putLong(EXTRA_CURRENT_SESSION_ID, currentSessionData!!.id)
+ }
+ }
+
+ override fun onTLogSelected(tlogSession: SessionData){
+ onTLogSelected(tlogSession, false)
+ }
+
+ private fun onTLogSelected(tlogSession: SessionContract.SessionData, force: Boolean) {
+ if(!force && tlogSession.equals(currentSessionData))
+ return
+
+ currentSessionData = tlogSession
+ sessionTitleView?.text = TLogDataAdapter.dateFormatter.format(Date(tlogSession.startTime))
+ sessionTitleView?.visibility = View.VISIBLE
+
+ // Load the events from the selected tlog file
+ Timber.i("Loading tlog data from ${tlogSession.tlogLoggingUri.path}")
+
+ dataLoader?.cancel(true)
+
+ // Show a loading progress bar
+ loadingProgress?.visibility = View.VISIBLE
+ loadedEvents.clear()
+
+ dataLoader = TLogDataLoader(this, handler)
+ dataLoader?.execute(tlogSession.tlogLoggingUri)
+
+ isLoadingData = true
+ notifyTLogSelected(tlogSession)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ dataLoader?.cancel(true)
+ dataLoader = null
+ }
+
+ override fun registerForTLogDataUpdate(subscriber: TLogViewer) {
+ subscriber.onTLogDataLoaded(loadedEvents, isLoadingData)
+ tlogSubscribers.add(subscriber)
+ }
+
+ override fun unregisterForTLogDataUpdate(subscriber: TLogViewer) {
+ tlogSubscribers.remove(subscriber)
+ }
+
+ private fun notifyTLogSelected(tlogSession: SessionContract.SessionData) {
+ for (subscriber in tlogSubscribers) {
+ subscriber.onTLogSelected(tlogSession)
+ }
+ }
+
+ private fun notifyTLogSubscribers(loadedEvents: List, hasMore: Boolean) {
+ for (subscriber in tlogSubscribers) {
+ subscriber.onTLogDataLoaded(loadedEvents, hasMore)
+ }
+ }
+
+ fun onTLogLoadedData(newItems: List, hasMore: Boolean) {
+ loadedEvents.addAll(newItems)
+
+ if (hasMore) {
+ Timber.i("Adding ${newItems.size} items")
+ } else {
+ Timber.i("Loaded ${loadedEvents.size} tlog events")
+ }
+
+ isLoadingData = hasMore
+ notifyTLogSubscribers(newItems, hasMore)
+ loadingProgress?.visibility = if(hasMore) View.VISIBLE else View.GONE
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/TLogDataLoader.kt b/Android/src/org/droidplanner/android/tlog/TLogDataLoader.kt
new file mode 100644
index 0000000000..8ed485ec3f
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/TLogDataLoader.kt
@@ -0,0 +1,92 @@
+package org.droidplanner.android.tlog
+
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Handler
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import com.o3dr.android.client.utils.data.tlog.TLogParser.TLogIterator
+import timber.log.Timber
+import java.io.FileNotFoundException
+import java.lang.ref.WeakReference
+import java.util.concurrent.ConcurrentLinkedQueue
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class TLogDataLoader(activity: TLogActivity, val handler: Handler) : AsyncTask() {
+
+ private companion object {
+ const val EVENT_UPDATE_THRESHOLD = 5000
+ const val MIN_UPDATE_DELAY = 1000L //1 second
+ }
+
+ private val publishProgress = object: Runnable {
+ override fun run() {
+ handler.removeCallbacks(this)
+ activityRef.get()?.onTLogLoadedData(grabData(), true)
+ }
+
+ }
+
+ private val activityRef = WeakReference(activity)
+
+ private val allEvents = ConcurrentLinkedQueue()
+
+ override fun doInBackground(vararg params: Uri): Boolean {
+ try {
+ for (uri in params) {
+ if(isCancelled)
+ break
+
+ val iterator = TLogIterator(uri, handler)
+ iterator.start()
+
+ var eventCounter = 0
+ var lastUpdate = System.currentTimeMillis()
+ var event = iterator.blockingNext()
+ while(event != null && !isCancelled){
+ allEvents.add(event)
+ eventCounter++
+ if(eventCounter >= EVENT_UPDATE_THRESHOLD){
+ val currentTime = System.currentTimeMillis()
+ if(currentTime - lastUpdate >= MIN_UPDATE_DELAY) {
+ handler.post(publishProgress)
+ lastUpdate = currentTime
+ }
+ eventCounter = 0
+ }
+
+ event = iterator.blockingNext()
+ }
+
+ iterator.finish()
+ }
+ return true
+ }catch(e: FileNotFoundException){
+ Timber.e(e, "Error occurred while loading tlog data")
+ return false
+ }
+ finally{
+ handler.removeCallbacks(publishProgress)
+ }
+ }
+
+ private fun grabData(): List {
+ val nextBatch = mutableListOf()
+ var event = allEvents.poll()
+ while (event != null) {
+ nextBatch.add(event)
+ event = allEvents.poll()
+ }
+ return nextBatch
+ }
+
+ override fun onCancelled(){
+ activityRef.get()?.onTLogLoadedData(grabData(), false)
+ }
+
+ override fun onPostExecute(result: Boolean) {
+ activityRef.get()?.onTLogLoadedData(grabData(), false)
+ }
+
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/TLogDataPicker.kt b/Android/src/org/droidplanner/android/tlog/TLogDataPicker.kt
new file mode 100644
index 0000000000..7cde6e74d9
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/TLogDataPicker.kt
@@ -0,0 +1,84 @@
+package org.droidplanner.android.tlog
+
+import android.app.Activity
+import android.os.Bundle
+import android.support.v4.app.DialogFragment
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import org.droidplanner.android.DroidPlannerApp
+import org.droidplanner.android.R
+import org.droidplanner.android.droneshare.data.SessionContract
+import org.droidplanner.android.tlog.adapters.TLogDataAdapter
+import org.droidplanner.android.tlog.adapters.TLogDataAdapter.Listener
+
+/**
+ * TLog data picker dialog
+ * Created by fhuya on 6/12/2016.
+ */
+class TLogDataPicker : DialogFragment(){
+
+ private val noTLogMessageView by lazy {
+ getView()?.findViewById(R.id.no_tlogs_message)
+ }
+
+ private var selectionListener : TLogDataAdapter.Listener? = null
+
+ private val selectionListenerWrapper = object : Listener {
+ override fun onTLogSelected(tlogSession: SessionContract.SessionData) {
+ selectionListener?.onTLogSelected(tlogSession)
+ dismissAllowingStateLoss()
+ }
+ }
+
+ override fun onAttach(activity: Activity){
+ super.onAttach(activity)
+ if(activity !is TLogDataAdapter.Listener){
+ throw IllegalStateException("Parent activity must implement " +
+ "${TLogDataAdapter.Listener::class.java.name}")
+ }
+
+ selectionListener = activity
+ }
+
+ override fun onDetach(){
+ super.onDetach()
+ selectionListener = null
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) : View? {
+ return inflater.inflate(R.layout.fragment_tlog_data_picker, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?){
+ super.onViewCreated(view, savedInstanceState)
+
+ noTLogMessageView?.setOnClickListener {
+ dismissAllowingStateLoss()
+ }
+
+ val currentSessionId = arguments?.getLong(TLogActivity.EXTRA_CURRENT_SESSION_ID, -1L) ?: -1L
+
+ val tlogsView = view.findViewById(R.id.tlogs_selector) as RecyclerView?
+ tlogsView?.setHasFixedSize(true)
+
+ // Use a linear layout manager
+ val layoutMgr = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
+ tlogsView?.setLayoutManager(layoutMgr)
+
+ val adapter = TLogDataAdapter(activity.getApplication() as DroidPlannerApp, childFragmentManager, currentSessionId)
+ adapter.setTLogSelectionListener(selectionListenerWrapper)
+ tlogsView?.adapter = adapter
+
+ if (adapter.itemCount == 0) {
+ tlogsView?.visibility = View.GONE
+ noTLogMessageView?.visibility = View.VISIBLE
+ }
+ else {
+ tlogsView?.visibility = View.VISIBLE
+ noTLogMessageView?.visibility = View.GONE
+ }
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/adapters/TLogDataAdapter.kt b/Android/src/org/droidplanner/android/tlog/adapters/TLogDataAdapter.kt
new file mode 100644
index 0000000000..075025d511
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/adapters/TLogDataAdapter.kt
@@ -0,0 +1,82 @@
+package org.droidplanner.android.tlog.adapters
+
+import android.support.v4.app.FragmentManager
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import org.droidplanner.android.DroidPlannerApp
+import org.droidplanner.android.R
+import org.droidplanner.android.dialogs.OkDialog
+import org.droidplanner.android.droneshare.data.SessionContract.SessionData
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * Created by fhuya on 6/12/2016.
+ */
+class TLogDataAdapter(val app: DroidPlannerApp, val fragmentMgr: FragmentManager, val selectedSessionId: Long) :
+ RecyclerView.Adapter() {
+
+ companion object {
+ val dateFormatter = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US)
+ }
+
+ interface Listener {
+ fun onTLogSelected(tlogSession: SessionData)
+ }
+
+ class ViewHolder(val container: View, val dataTimestamp: TextView, val clearSession: View) : RecyclerView.ViewHolder(container)
+
+ private var tlogSelectionListener: Listener? = null
+ private var completedSessions = app.sessionDatabase.getCompletedSessions(true)
+
+ fun setTLogSelectionListener(listener: Listener?) {
+ this.tlogSelectionListener = listener
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val sessionData = completedSessions[position]
+
+ holder.container.isActivated = sessionData.id == selectedSessionId
+
+ val sessionDate = dateFormatter.format(Date(sessionData.startTime))
+ holder.dataTimestamp.text = sessionDate
+ holder.dataTimestamp.setOnClickListener {
+ // Notify the listener of the selected tlog file
+ tlogSelectionListener?.onTLogSelected(sessionData)
+ }
+
+ holder.clearSession.setOnClickListener {
+ // Confirm before deletion
+ val confirmDialog = OkDialog.newInstance(app.applicationContext, "Delete?", "Delete session ${sessionDate}?", object : OkDialog.Listener{
+ override fun onOk() {
+ // Remove the session data entry from the database.
+ app.sessionDatabase.removeSessionData(sessionData.id)
+ reloadCompletedSessions()
+ }
+
+ override fun onCancel() {}
+
+ override fun onDismiss() {}
+
+ }, true)
+ confirmDialog.show(fragmentMgr, "Delete tlog session")
+ }
+ }
+
+ private fun reloadCompletedSessions() {
+ completedSessions = app.sessionDatabase.getCompletedSessions(true)
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder? {
+ val containerView = LayoutInflater.from(parent.context).inflate(R.layout.list_item_tlog_data, parent, false)
+ val dataTimestamp = containerView.findViewById(R.id.tlog_data_timestamp) as TextView
+ val clearSession = containerView.findViewById(R.id.clear_tlog_session)
+ return ViewHolder(containerView, dataTimestamp, clearSession)
+ }
+
+ override fun getItemCount() = completedSessions.size
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/adapters/TLogPositionEventAdapter.kt b/Android/src/org/droidplanner/android/tlog/adapters/TLogPositionEventAdapter.kt
new file mode 100644
index 0000000000..97a1e17667
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/adapters/TLogPositionEventAdapter.kt
@@ -0,0 +1,82 @@
+package org.droidplanner.android.tlog.adapters
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ProgressBar
+import android.widget.TextView
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import org.droidplanner.android.R
+import org.droidplanner.android.tlog.event.TLogEventListener
+import org.droidplanner.android.view.adapterViews.AbstractRecyclerViewFooterAdapter
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class TLogPositionEventAdapter(recyclerView: RecyclerView) :
+ AbstractRecyclerViewFooterAdapter(recyclerView, null) {
+
+ class ViewHolder(val container: View, val thumbnail: TextView) : RecyclerView.ViewHolder(container)
+
+ companion object {
+ private val dateFormatter = SimpleDateFormat("HH:mm:ss", Locale.US)
+ }
+
+ private var selectedEvent: Pair? = null
+ private var tlogEventListener: TLogEventListener? = null
+
+ fun setTLogEventClickListener(listener: TLogEventListener?){
+ tlogEventListener = listener
+ }
+
+ fun clear(hasMore: Boolean = true){
+ resetItems(null)
+ setHasMoreData(hasMore)
+ }
+
+ override fun onBindBasicItemView(genericHolder: RecyclerView.ViewHolder, position: Int) {
+ val holder = genericHolder as ViewHolder
+
+ val event = getItem(position)
+ holder.container.isActivated = event == selectedEvent?.second
+ holder.thumbnail.text = dateFormatter.format(event.timestamp)
+ holder.thumbnail.setOnClickListener {
+ if(event == selectedEvent?.second){
+ // Unselect the event
+ selectedEvent = null
+ tlogEventListener?.onTLogEventSelected(null)
+ notifyItemChanged(position)
+ }
+ else {
+ val previousPosition = selectedEvent?.first ?: -1
+ selectedEvent = Pair(position, event)
+ tlogEventListener?.onTLogEventSelected(event)
+ notifyItemChanged(position)
+ if(previousPosition != -1)
+ notifyItemChanged(previousPosition)
+ }
+ }
+ }
+
+ override fun onCreateBasicItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val container = LayoutInflater.from(parent.context).inflate(R.layout.list_item_tlog_position_event, parent, false)
+ val thumbnail = container.findViewById(R.id.position_event_thumbnail) as TextView
+ return ViewHolder(container, thumbnail)
+ }
+
+ override fun onCreateFooterViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val container = LayoutInflater.from(parent.context).inflate(R.layout.list_item_tlog_position_event_loading, parent, false)
+ val progressBar = container.findViewById(R.id.progressBar) as ProgressBar
+ return ProgressViewHolder(container, progressBar)
+ }
+
+ override fun onBindFooterView(genericHolder: RecyclerView.ViewHolder, position: Int) {
+ (genericHolder as ProgressViewHolder).progressBar.isIndeterminate = true
+ }
+
+ class ProgressViewHolder(v: View, val progressBar: ProgressBar) : RecyclerView.ViewHolder(v)
+
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/adapters/TLogRawEventAdapter.kt b/Android/src/org/droidplanner/android/tlog/adapters/TLogRawEventAdapter.kt
new file mode 100644
index 0000000000..189d885967
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/adapters/TLogRawEventAdapter.kt
@@ -0,0 +1,60 @@
+package org.droidplanner.android.tlog.adapters
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ProgressBar
+import android.widget.TextView
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import org.droidplanner.android.R
+import org.droidplanner.android.view.adapterViews.AbstractRecyclerViewFooterAdapter
+import org.droidplanner.android.view.adapterViews.OnLoadMoreListener
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class TLogRawEventAdapter(recyclerView: RecyclerView, onLoadMoreListener: OnLoadMoreListener?) :
+ AbstractRecyclerViewFooterAdapter(recyclerView, onLoadMoreListener) {
+
+ class ViewHolder(eventView: View, val eventInfo: TextView, val eventTimestamp: TextView) :
+ RecyclerView.ViewHolder(eventView)
+
+ companion object {
+ private val dateFormatter = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US)
+ }
+
+ fun clear(hasMore: Boolean = true){
+ resetItems(null)
+ setHasMoreData(hasMore)
+ }
+
+ override fun onBindBasicItemView(genericHolder: RecyclerView.ViewHolder, position: Int) {
+ val holder = genericHolder as ViewHolder
+ val event = getItem(position)
+ holder.eventInfo.text = event.mavLinkMessage.toString()
+ holder.eventTimestamp.text = dateFormatter.format(Date(event.timestamp))
+ }
+
+ override fun onCreateBasicItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val eventView = LayoutInflater.from(parent.context).inflate(R.layout.list_item_tlog_raw_event, parent, false)
+ val eventTimestamp = eventView.findViewById(R.id.event_timestamp) as TextView
+ val eventInfo = eventView.findViewById(R.id.event_info) as TextView
+ return ViewHolder(eventView, eventInfo, eventTimestamp)
+ }
+
+ override fun onCreateFooterViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ //noinspection ConstantConditions
+ val v = LayoutInflater.from(parent.context).inflate(R.layout.progress_bar, parent, false)
+ val progressBar = v.findViewById(R.id.progressBar) as ProgressBar
+ return ProgressViewHolder(v, progressBar)
+ }
+
+ override fun onBindFooterView(genericHolder: RecyclerView.ViewHolder, position: Int) {
+ (genericHolder as ProgressViewHolder).progressBar.isIndeterminate = true
+ }
+
+ class ProgressViewHolder(v: View, val progressBar: ProgressBar) : RecyclerView.ViewHolder(v)
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/adapters/TLogViewerAdapter.kt b/Android/src/org/droidplanner/android/tlog/adapters/TLogViewerAdapter.kt
new file mode 100644
index 0000000000..f942aa06d2
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/adapters/TLogViewerAdapter.kt
@@ -0,0 +1,32 @@
+package org.droidplanner.android.tlog.adapters
+
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentManager
+import android.support.v4.app.FragmentPagerAdapter
+import org.droidplanner.android.tlog.viewers.TLogPositionViewer
+import org.droidplanner.android.tlog.viewers.TLogRawViewer
+
+/**
+ * Return the appropriate fragment for the selected tlog data viewer.
+ *
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class TLogViewerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
+ override fun getItem(position: Int): Fragment? {
+ return when(position){
+ 0 -> TLogRawViewer()
+ 1 -> TLogPositionViewer()
+ else -> throw IllegalStateException("Invalid viewer index.")
+ }
+ }
+
+ override fun getCount() = 2
+
+ override fun getPageTitle(position: Int): CharSequence? {
+ return when(position){
+ 0 -> "All"
+ 1 -> "Position"
+ else -> throw IllegalStateException("Invalid viewer index.")
+ }
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/event/TLogEventDetail.kt b/Android/src/org/droidplanner/android/tlog/event/TLogEventDetail.kt
new file mode 100644
index 0000000000..80f36f7dfa
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/event/TLogEventDetail.kt
@@ -0,0 +1,40 @@
+package org.droidplanner.android.tlog.event
+
+import android.os.Bundle
+import android.support.v4.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import org.droidplanner.android.R
+
+/**
+ * TODO: Complete implementation for the TLog event detail class
+ */
+class TLogEventDetail : Fragment(), TLogEventListener {
+
+ private val eventInfoContainer by lazy {
+ getView()?.findViewById(R.id.event_detail_container)
+ }
+
+ private val eventInfoView by lazy {
+ getView()?.findViewById(R.id.event_info_dump) as TextView?
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_tlog_event_detail, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?){
+ super.onViewCreated(view, savedInstanceState)
+ }
+
+ override fun onTLogEventSelected(event: TLogParser.Event?) {
+ eventInfoContainer?.visibility = if(event == null) View.GONE else View.VISIBLE
+ if(event != null) {
+ eventInfoView?.setText(event.mavLinkMessage.toString())
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/event/TLogEventListener.kt b/Android/src/org/droidplanner/android/tlog/event/TLogEventListener.kt
new file mode 100644
index 0000000000..a368b7e8d3
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/event/TLogEventListener.kt
@@ -0,0 +1,10 @@
+package org.droidplanner.android.tlog.event
+
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+interface TLogEventListener {
+ fun onTLogEventSelected(event: TLogParser.Event?)
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/event/TLogEventMapFragment.kt b/Android/src/org/droidplanner/android/tlog/event/TLogEventMapFragment.kt
new file mode 100644
index 0000000000..66fe2a0e74
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/event/TLogEventMapFragment.kt
@@ -0,0 +1,119 @@
+package org.droidplanner.android.tlog.event
+
+import android.content.res.Resources
+import android.graphics.BitmapFactory
+import android.widget.Toast
+import com.MAVLink.common.msg_global_position_int
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import com.o3dr.services.android.lib.coordinate.LatLong
+import org.droidplanner.android.R
+import org.droidplanner.android.droneshare.data.SessionContract
+import org.droidplanner.android.fragments.DroneMap
+import org.droidplanner.android.maps.MarkerInfo
+import org.droidplanner.android.maps.PolylineInfo
+import org.droidplanner.android.tlog.interfaces.TLogDataSubscriber
+import org.droidplanner.android.utils.prefs.AutoPanMode
+import java.util.*
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class TLogEventMapFragment : DroneMap(), TLogDataSubscriber, TLogEventListener {
+
+ private val eventsPolylineInfo = TLogEventsPolylineInfo()
+ private val selectedPositionMarkerInfo = GlobalPositionMarkerInfo()
+
+ override fun isMissionDraggable() = false
+
+ override fun setAutoPanMode(target: AutoPanMode?): Boolean {
+ return when(target){
+ AutoPanMode.DISABLED -> true
+ else -> {
+ Toast.makeText(activity, "Auto pan is not supported on this map.",
+ Toast.LENGTH_LONG).show()
+ false
+ }
+ }
+ }
+
+ override fun onTLogDataLoaded(events: List, hasMore: Boolean) {
+ for(event in events){
+ val globalPositionInt = event.mavLinkMessage as msg_global_position_int
+ eventsPolylineInfo.addCoord(
+ LatLong(globalPositionInt.lat.toDouble()/ 1E7, globalPositionInt.lon.toDouble()/ 1E7))
+ }
+ eventsPolylineInfo.update(this)
+ }
+
+ override fun onTLogEventSelected(event: TLogParser.Event?) {
+ if(event == null){
+ selectedPositionMarkerInfo.selectedGlobalPosition = null
+ eventsPolylineInfo.zoomToFit(this)
+ }
+ else{
+ //Add a marker for the selected event
+ val globalPositionInt = event.mavLinkMessage as msg_global_position_int
+ selectedPositionMarkerInfo.selectedGlobalPosition = globalPositionInt
+ mMapFragment.zoomToFit(listOf(LatLong(globalPositionInt.lat.toDouble()/ 1E7, globalPositionInt.lon.toDouble()/ 1E7)))
+ }
+ selectedPositionMarkerInfo.updateMarker(this)
+ }
+
+ override fun onTLogSelected(tlogSession: SessionContract.SessionData) {
+ eventsPolylineInfo.clear()
+ eventsPolylineInfo.update(this)
+
+ selectedPositionMarkerInfo.selectedGlobalPosition = null
+ selectedPositionMarkerInfo.updateMarker(this)
+ }
+
+ private class TLogEventsPolylineInfo : PolylineInfo() {
+
+ private val eventCoords = ArrayList()
+
+ fun clear(refreshPolyline: Boolean = false) {
+ eventCoords.clear()
+ if(refreshPolyline) {
+ updatePolyline()
+ }
+ }
+
+ fun addCoord(coord: LatLong){
+ eventCoords.add(coord)
+ }
+
+ fun update(mapHandle: TLogEventMapFragment) {
+ if(isOnMap()) {
+ updatePolyline()
+ }
+ else {
+ if(eventCoords.isNotEmpty())
+ mapHandle.addPolyline(this)
+ }
+ zoomToFit(mapHandle)
+ }
+
+ fun zoomToFit(mapHandle: TLogEventMapFragment){
+ mapHandle.mMapFragment.zoomToFit(eventCoords)
+ }
+
+ override fun getPoints(): List {
+ return eventCoords
+ }
+
+ override fun getColor() = 0xfffd693f.toInt()
+
+ }
+
+ private class GlobalPositionMarkerInfo : MarkerInfo() {
+ var selectedGlobalPosition : msg_global_position_int? = null
+
+ override fun isVisible() = selectedGlobalPosition != null
+
+ override fun getPosition() =
+ if(selectedGlobalPosition == null) null
+ else LatLong(selectedGlobalPosition!!.lat.toDouble() / 1E7, selectedGlobalPosition!!.lon.toDouble() / 1E7)
+
+ override fun getIcon(res: Resources) = BitmapFactory.decodeResource(res, R.drawable.ic_wp_map_selected)
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/interfaces/TLogDataProvider.kt b/Android/src/org/droidplanner/android/tlog/interfaces/TLogDataProvider.kt
new file mode 100644
index 0000000000..f72fe89d68
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/interfaces/TLogDataProvider.kt
@@ -0,0 +1,11 @@
+package org.droidplanner.android.tlog.interfaces
+
+import org.droidplanner.android.tlog.viewers.TLogViewer
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+interface TLogDataProvider {
+ fun registerForTLogDataUpdate(subscriber: TLogViewer)
+ fun unregisterForTLogDataUpdate(subscriber: TLogViewer)
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/interfaces/TLogDataSubscriber.kt b/Android/src/org/droidplanner/android/tlog/interfaces/TLogDataSubscriber.kt
new file mode 100644
index 0000000000..a1adab04dc
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/interfaces/TLogDataSubscriber.kt
@@ -0,0 +1,12 @@
+package org.droidplanner.android.tlog.interfaces
+
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import org.droidplanner.android.droneshare.data.SessionContract
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+interface TLogDataSubscriber {
+ fun onTLogSelected(tlogSession: SessionContract.SessionData)
+ fun onTLogDataLoaded(events: List, hasMore: Boolean = true)
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/viewers/TLogPositionViewer.kt b/Android/src/org/droidplanner/android/tlog/viewers/TLogPositionViewer.kt
new file mode 100644
index 0000000000..3247e65526
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/viewers/TLogPositionViewer.kt
@@ -0,0 +1,163 @@
+package org.droidplanner.android.tlog.viewers
+
+import android.os.Bundle
+import android.support.design.widget.FloatingActionButton
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.MAVLink.common.msg_global_position_int
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import org.droidplanner.android.R
+import org.droidplanner.android.droneshare.data.SessionContract
+import org.droidplanner.android.tlog.adapters.TLogPositionEventAdapter
+import org.droidplanner.android.tlog.event.TLogEventDetail
+import org.droidplanner.android.tlog.event.TLogEventListener
+import org.droidplanner.android.tlog.event.TLogEventMapFragment
+import org.droidplanner.android.view.FastScroller
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class TLogPositionViewer : TLogViewer(), TLogEventListener {
+
+ private var tlogPositionAdapter : TLogPositionEventAdapter? = null
+
+ private val noDataView by lazy {
+ getView()?.findViewById(R.id.no_data_message)
+ }
+
+ private val loadingData by lazy {
+ getView()?.findViewById(R.id.loading_tlog_data)
+ }
+
+ private val eventsView by lazy {
+ getView()?.findViewById(R.id.event_list) as RecyclerView?
+ }
+
+ private val fastScroller by lazy {
+ getView()?.findViewById(R.id.fast_scroller) as FastScroller
+ }
+
+ private val newPositionEvents = mutableListOf()
+
+ private var tlogEventMap : TLogEventMapFragment? = null
+ private var tlogEventDetail : TLogEventDetail? = null
+
+ private var lastEventTimestamp = -1L
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?{
+ return inflater.inflate(R.layout.fragment_tlog_position_viewer, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?){
+ super.onViewCreated(view, savedInstanceState)
+
+ val fm = childFragmentManager
+ tlogEventMap = fm.findFragmentById(R.id.tlog_map_container) as TLogEventMapFragment?
+ if(tlogEventMap == null){
+ tlogEventMap = TLogEventMapFragment()
+ fm.beginTransaction().add(R.id.tlog_map_container, tlogEventMap).commit()
+ }
+
+ tlogEventDetail = fm.findFragmentById(R.id.tlog_event_detail) as TLogEventDetail?
+ if(tlogEventDetail == null){
+ tlogEventDetail = TLogEventDetail()
+ fm.beginTransaction().add(R.id.tlog_event_detail, tlogEventDetail).commit()
+ }
+
+ eventsView?.apply {
+ setHasFixedSize(true)
+ layoutManager = LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)
+ }
+
+ tlogPositionAdapter = TLogPositionEventAdapter(eventsView!!)
+ eventsView?.adapter = tlogPositionAdapter
+
+ fastScroller.setRecyclerView(eventsView!!)
+ tlogPositionAdapter?.setTLogEventClickListener(this)
+
+ val goToMyLocation = view.findViewById(R.id.my_location_button) as FloatingActionButton
+ goToMyLocation.setOnClickListener {
+ tlogEventMap?.goToMyLocation();
+ }
+
+ val goToDroneLocation = view.findViewById(R.id.drone_location_button) as FloatingActionButton
+ goToDroneLocation.setOnClickListener {
+ tlogEventMap?.goToDroneLocation()
+ }
+ }
+
+ override fun onTLogSelected(tlogSession: SessionContract.SessionData) {
+ tlogPositionAdapter?.clear()
+ lastEventTimestamp = -1L
+ stateLoadingData()
+
+ // Refresh the map.
+ tlogEventMap?.onTLogSelected(tlogSession)
+ tlogEventDetail?.onTLogEventSelected(null)
+ }
+
+ override fun onTLogDataLoaded(events: List, hasMore: Boolean) {
+ // Parse the event list and retrieve only the position events.
+ newPositionEvents.clear()
+
+ for(event in events){
+ if(event.mavLinkMessage is msg_global_position_int) {
+ // Events should be at least 1 second apart.
+ if(lastEventTimestamp == -1L || (event.timestamp/1000 - lastEventTimestamp/1000) >= 1L){
+ lastEventTimestamp = event.timestamp
+ newPositionEvents.add(event)
+ }
+ }
+ }
+
+ // Refresh the adapter
+ tlogPositionAdapter?.addItems(newPositionEvents)
+ tlogPositionAdapter?.setHasMoreData(hasMore)
+
+ if(tlogPositionAdapter?.itemCount == 0){
+ if(hasMore){
+ stateLoadingData()
+ }
+ else {
+ stateNoData()
+ }
+ } else {
+ stateDataLoaded()
+ }
+
+ tlogEventMap?.onTLogDataLoaded(newPositionEvents, hasMore)
+ }
+
+ override fun onTLogEventSelected(event: TLogParser.Event?) {
+ // Show the detail window for this event
+ tlogEventDetail?.onTLogEventSelected(event)
+
+ //Propagate the click event to the map
+ tlogEventMap?.onTLogEventSelected(event)
+ }
+
+ private fun stateLoadingData() {
+ noDataView?.visibility = View.GONE
+ eventsView?.visibility = View.GONE
+ fastScroller.visibility = View.GONE
+ loadingData?.visibility = View.VISIBLE
+ }
+
+ private fun stateNoData(){
+ noDataView?.visibility = View.VISIBLE
+ eventsView?.visibility = View.GONE
+ fastScroller.visibility = View.GONE
+ loadingData?.visibility = View.GONE
+ }
+
+ private fun stateDataLoaded(){
+ noDataView?.visibility = View.GONE
+ eventsView?.visibility = View.VISIBLE
+ fastScroller.visibility = View.VISIBLE
+ loadingData?.visibility = View.GONE
+ }
+}
+
diff --git a/Android/src/org/droidplanner/android/tlog/viewers/TLogRawViewer.kt b/Android/src/org/droidplanner/android/tlog/viewers/TLogRawViewer.kt
new file mode 100644
index 0000000000..366ac376a7
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/viewers/TLogRawViewer.kt
@@ -0,0 +1,100 @@
+package org.droidplanner.android.tlog.viewers
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.o3dr.android.client.utils.data.tlog.TLogParser
+import org.droidplanner.android.R
+import org.droidplanner.android.droneshare.data.SessionContract
+import org.droidplanner.android.tlog.adapters.TLogRawEventAdapter
+import org.droidplanner.android.view.FastScroller
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class TLogRawViewer : TLogViewer() {
+
+ private var tlogEventsAdapter : TLogRawEventAdapter? = null
+
+ private val loadingData by lazy {
+ getView()?.findViewById(R.id.loading_tlog_data)
+ }
+
+ private val noTLogView by lazy {
+ getView()?.findViewById(R.id.no_tlog_selected) as TextView?
+ }
+
+ private val fastScroller by lazy {
+ getView()?.findViewById(R.id.raw_fastscroller) as FastScroller
+ }
+
+ private val rawData by lazy {
+ getView()?.findViewById(R.id.tlog_raw_data) as RecyclerView?
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_tlog_raw_viewer, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ rawData?.apply{
+ setHasFixedSize(true)
+ layoutManager = LinearLayoutManager(getContext())
+ }
+
+ tlogEventsAdapter = TLogRawEventAdapter(rawData!!, null)
+ rawData?.adapter = tlogEventsAdapter
+
+ fastScroller.setRecyclerView(rawData!!)
+ }
+
+ override fun onTLogSelected(tlogSession: SessionContract.SessionData){
+ tlogEventsAdapter?.clear()
+ stateLoadingData()
+ }
+
+ override fun onTLogDataLoaded(events: List, hasMore: Boolean){
+ // Refresh the recycler view
+ tlogEventsAdapter?.addItems(events)
+ tlogEventsAdapter?.setHasMoreData(hasMore)
+
+ if(tlogEventsAdapter?.itemCount == 0){
+ if(hasMore){
+ stateLoadingData()
+ }
+ else {
+ stateNoData()
+ }
+ }
+ else{
+ stateDataLoaded()
+ }
+ }
+
+ private fun stateLoadingData(){
+ noTLogView?.visibility = View.GONE
+ rawData?.visibility = View.GONE
+ fastScroller.visibility = View.GONE
+ loadingData?.visibility = View.VISIBLE
+ }
+
+ private fun stateNoData(){
+ noTLogView?.visibility = View.VISIBLE
+ rawData?.visibility = View.GONE
+ fastScroller.visibility = View.GONE
+ loadingData?.visibility = View.GONE
+ }
+
+ private fun stateDataLoaded(){
+ noTLogView?.visibility = View.GONE
+ loadingData?.visibility = View.GONE
+ rawData?.visibility = View.VISIBLE
+ fastScroller.visibility = View.VISIBLE
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/tlog/viewers/TLogViewer.kt b/Android/src/org/droidplanner/android/tlog/viewers/TLogViewer.kt
new file mode 100644
index 0000000000..eded44d812
--- /dev/null
+++ b/Android/src/org/droidplanner/android/tlog/viewers/TLogViewer.kt
@@ -0,0 +1,44 @@
+package org.droidplanner.android.tlog.viewers
+
+import android.app.Activity
+import org.droidplanner.android.fragments.helpers.ApiListenerFragment
+import org.droidplanner.android.tlog.interfaces.TLogDataProvider
+import org.droidplanner.android.tlog.interfaces.TLogDataSubscriber
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+abstract class TLogViewer : ApiListenerFragment(), TLogDataSubscriber {
+
+ private var tlogDataProvider : TLogDataProvider? = null
+
+ override fun onAttach(activity: Activity){
+ super.onAttach(activity)
+
+ if(activity !is TLogDataProvider){
+ throw IllegalStateException("Parent activity must implement ${TLogDataProvider::class.java.name}")
+ }
+
+ tlogDataProvider = activity
+ }
+
+ override fun onApiConnected() {}
+
+ override fun onApiDisconnected() {}
+
+ override fun onDetach(){
+ super.onDetach()
+ tlogDataProvider = null
+ }
+
+ override fun onStart(){
+ super.onStart()
+ tlogDataProvider?.registerForTLogDataUpdate(this)
+ }
+
+ override fun onStop(){
+ super.onStop()
+ tlogDataProvider?.unregisterForTLogDataUpdate(this)
+ }
+
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/utils/DroneHelper.java b/Android/src/org/droidplanner/android/utils/DroneHelper.java
index dd2a478844..9368f92a32 100644
--- a/Android/src/org/droidplanner/android/utils/DroneHelper.java
+++ b/Android/src/org/droidplanner/android/utils/DroneHelper.java
@@ -6,16 +6,28 @@
import com.google.android.gms.maps.model.LatLng;
import com.o3dr.services.android.lib.coordinate.LatLong;
+import java.util.ArrayList;
+import java.util.List;
+
public class DroneHelper {
- static public LatLng CoordToLatLang(LatLong coord) {
+ static public LatLng coordToLatLng(LatLong coord) {
return new LatLng(coord.getLatitude(), coord.getLongitude());
}
- public static LatLong LatLngToCoord(LatLng point) {
+ public static List coordToLatng(List extends LatLong> coords){
+ List points = new ArrayList<>(coords.size());
+ for(LatLong coord: coords){
+ points.add(coordToLatLng(coord));
+ }
+
+ return points;
+ }
+
+ public static LatLong latLngToCoord(LatLng point) {
return new LatLong((float)point.latitude, (float) point.longitude);
}
- public static LatLong LocationToCoord(Location location) {
+ public static LatLong locationToCoord(Location location) {
return new LatLong((float) location.getLatitude(), (float) location.getLongitude());
}
diff --git a/Android/src/org/droidplanner/android/utils/TLogUtils.java b/Android/src/org/droidplanner/android/utils/TLogUtils.java
new file mode 100644
index 0000000000..40d584652c
--- /dev/null
+++ b/Android/src/org/droidplanner/android/utils/TLogUtils.java
@@ -0,0 +1,61 @@
+package org.droidplanner.android.utils;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.o3dr.android.client.utils.FileUtils;
+import com.o3dr.services.android.lib.drone.connection.ConnectionType;
+
+import java.io.File;
+
+/**
+ * Created by fredia on 6/11/16.
+ */
+public class TLogUtils {
+
+ private static final String DIRECTORY_TLOGS = "tlogs";
+ private static final String TLOG_FILENAME_EXT = ".tlog";
+ private static final String TLOG_PREFIX = "log";
+
+ // Private to prevent instantiation
+ private TLogUtils(){}
+
+ /**
+ * Return the directory where the generated tlogs logging files are stored.
+ * @return File to the tlogs directory
+ */
+ private static File getTLogsDirectory(Context context){
+ File tlogDir = new File(context.getExternalFilesDir(null), DIRECTORY_TLOGS);
+ if(!tlogDir.isDirectory()){
+ tlogDir.mkdirs();
+ }
+
+ return tlogDir;
+ }
+
+ /**
+ * Generate a tlog filename based on the given parameters
+ * @param connectionTypeLabel Label describing the connection type (i.e: usb, tcp,...)
+ * @param connectionTimestamp Timestamp when the connection was established
+ * @return Filename for a tlog file
+ */
+ private static String getTLogFilename(String connectionTypeLabel, long connectionTimestamp){
+ return TLOG_PREFIX
+ + "_" + connectionTypeLabel
+ + "_" + FileUtils.getTimeStamp(connectionTimestamp)
+ + TLOG_FILENAME_EXT;
+ }
+
+ /**
+ * Returns the uri where the tlog data should be logged.
+ * @param context
+ * @param connectionType
+ * @param connectionTimestamp
+ * @return
+ */
+ public static Uri getTLogLoggingUri(Context context, @ConnectionType.Type int connectionType, long connectionTimestamp){
+ File tlogLoggingFile = new File(getTLogsDirectory(context),
+ getTLogFilename(ConnectionType.getConnectionTypeLabel(connectionType), connectionTimestamp));
+ return Uri.fromFile(tlogLoggingFile);
+ }
+}
diff --git a/Android/src/org/droidplanner/android/utils/collection/HashBiMap.java b/Android/src/org/droidplanner/android/utils/collection/HashBiMap.java
deleted file mode 100644
index 5597cdd465..0000000000
--- a/Android/src/org/droidplanner/android/utils/collection/HashBiMap.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.droidplanner.android.utils.collection;
-
-import java.util.HashMap;
-import java.util.Set;
-
-/**
- * Created by fhuya on 9/15/14.
- */
-public class HashBiMap {
-
- private final HashMap mKeyToValueMap = new HashMap();
- private final HashMap mValueToKeyMap = new HashMap();
-
- public void put(K key, V value) {
- mKeyToValueMap.put(key, value);
- mValueToKeyMap.put(value, key);
- }
-
- public void removeKey(K key) {
- final V value = mKeyToValueMap.get(key);
- if (value != null) {
- mKeyToValueMap.remove(key);
- mValueToKeyMap.remove(value);
- }
- }
-
- public void removeValue(V value) {
- final K key = mValueToKeyMap.get(value);
- if (key != null) {
- mValueToKeyMap.remove(value);
- mKeyToValueMap.remove(key);
- }
- }
-
- public V getValue(K key) {
- return mKeyToValueMap.get(key);
- }
-
- public K getKey(V value) {
- return mValueToKeyMap.get(value);
- }
-
- public void clear() {
- mKeyToValueMap.clear();
- mValueToKeyMap.clear();
- }
-
- public Set keySet() {
- return mKeyToValueMap.keySet();
- }
-
- public Set valueSet() {
- return mValueToKeyMap.keySet();
- }
-}
diff --git a/Android/src/org/droidplanner/android/utils/prefs/DroidPlannerPrefs.java b/Android/src/org/droidplanner/android/utils/prefs/DroidPlannerPrefs.java
index 89abd9d422..a5838880d2 100644
--- a/Android/src/org/droidplanner/android/utils/prefs/DroidPlannerPrefs.java
+++ b/Android/src/org/droidplanner/android/utils/prefs/DroidPlannerPrefs.java
@@ -272,7 +272,7 @@ public boolean isUsageStatisticsEnabled() {
return prefs.getBoolean(PREF_USAGE_STATISTICS, DEFAULT_USAGE_STATISTICS);
}
- public void setConnectionParameterType(int connectionType) {
+ public void setConnectionParameterType(@ConnectionType.Type int connectionType) {
prefs.edit().putString(PREF_CONNECTION_TYPE, String.valueOf(connectionType)).apply();
lbm.sendBroadcast(new Intent(PREF_CONNECTION_TYPE));
}
@@ -280,8 +280,9 @@ public void setConnectionParameterType(int connectionType) {
/**
* @return the selected mavlink connection type.
*/
- public int getConnectionParameterType() {
- return Integer.parseInt(prefs.getString(PREF_CONNECTION_TYPE, DEFAULT_CONNECTION_TYPE));
+ public @ConnectionType.Type int getConnectionParameterType() {
+ @ConnectionType.Type int connectionType = Integer.parseInt(prefs.getString(PREF_CONNECTION_TYPE, DEFAULT_CONNECTION_TYPE));
+ return connectionType;
}
public int getUnitSystemType() {
diff --git a/Android/src/org/droidplanner/android/view/FastScroller.kt b/Android/src/org/droidplanner/android/view/FastScroller.kt
new file mode 100644
index 0000000000..67548345ef
--- /dev/null
+++ b/Android/src/org/droidplanner/android/view/FastScroller.kt
@@ -0,0 +1,196 @@
+package org.droidplanner.android.view
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.support.v7.widget.RecyclerView
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.widget.LinearLayout
+import org.droidplanner.android.R
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class FastScroller(context: Context, attrs: AttributeSet, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr){
+
+ constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
+
+ private companion object {
+ const val HANDLE_ANIMATION_DURATION = 100L
+ const val SCALE_X = "scaleX"
+ const val SCALE_Y = "scaleY"
+ const val ALPHA = "alpha"
+
+ private const val HANDLE_HIDE_DELAY = 1000L
+ private const val TRACK_SNAP_RANGE = 5
+ }
+
+ private val bubble : View
+ private val handle : View
+ private val scrollListener = ScrollListener()
+ private val handleHider = HandleHider()
+
+ private var heightRef : Int = 0
+ private var widthRef : Int = 0
+ private var currentAnimator: AnimatorSet? = null
+ private var recyclerView: RecyclerView? = null
+
+ init {
+ clipChildren = false
+ val inflater = LayoutInflater.from(context)
+ inflater.inflate(if(orientation == HORIZONTAL) R.layout.horizontal_fastscroller else R.layout.vertical_fastscroller, this)
+
+ bubble = findViewById(R.id.fastscroller_bubble);
+ handle = findViewById(R.id.fastscroller_handle);
+ }
+
+ fun setRecyclerView(recyclerView: RecyclerView) {
+ this.recyclerView = recyclerView
+ recyclerView.setOnScrollListener(scrollListener)
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int){
+ super.onSizeChanged(w, h, oldw, oldh)
+ widthRef = w
+ heightRef = h
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ val action = event.action
+ when (action) {
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
+ setPosition(event.x, event.y)
+ currentAnimator?.cancel()
+ handler.removeCallbacks(handleHider)
+ if(handle.visibility == INVISIBLE)
+ showHandle()
+
+ setRecyclerViewPosition(event.x, event.y)
+ return true
+ }
+ MotionEvent.ACTION_UP -> {
+ getHandler().postDelayed(handleHider, HANDLE_HIDE_DELAY)
+ return true
+ }
+ else -> return super.onTouchEvent(event)
+ }
+ }
+
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+ return true
+ }
+
+ private fun setRecyclerViewPosition(x: Float, y: Float){
+ if(recyclerView != null){
+ val itemCount = recyclerView!!.adapter.itemCount
+ if(orientation == HORIZONTAL) {
+ val proportion = if (bubble.x == 0f) 0f
+ else if (bubble.x + bubble.width >= widthRef - TRACK_SNAP_RANGE) 1f
+ else x / widthRef
+
+ val targetPos = getValueInRange(0, itemCount - 1, (proportion * itemCount).toInt())
+ recyclerView?.scrollToPosition(targetPos)
+ }
+ else {
+ val proportion: Float
+ if (bubble.y == 0f) {
+ proportion = 0f
+ } else if (bubble.y + bubble.height >= heightRef - TRACK_SNAP_RANGE) {
+ proportion = 1f
+ } else {
+ proportion = y / heightRef.toFloat()
+ }
+ val targetPos = getValueInRange(0, itemCount - 1, (proportion * itemCount.toFloat()).toInt())
+ recyclerView?.scrollToPosition(targetPos)
+ }
+ }
+ }
+
+ private fun setPosition(x: Float, y: Float){
+ if(orientation == HORIZONTAL) {
+ val position = x / widthRef
+ val bubbleWidth = bubble.width
+ bubble.setX(getValueInRange(0, widthRef - bubbleWidth, ((widthRef - bubbleWidth) * position).toInt()).toFloat())
+ val handleWidth = handle.width
+ handle.setX(getValueInRange(0, widthRef - handleWidth, ((widthRef - handleWidth) * position).toInt()).toFloat())
+ }
+ else {
+ val position = y / heightRef
+ val bubbleHeight = bubble.height
+ bubble.y = getValueInRange(0, heightRef - bubbleHeight, ((heightRef - bubbleHeight) * position).toInt()).toFloat()
+ val handleHeight = handle.height
+ handle.y = getValueInRange(0, heightRef - handleHeight, ((heightRef - handleHeight) * position).toInt()).toFloat()
+ }
+ }
+
+ private fun getValueInRange(min: Int, max: Int, value: Int): Int {
+ val minimum = Math.max(min, value)
+ return Math.min(minimum, max)
+ }
+
+ private fun showHandle(){
+ val animatorSet = AnimatorSet()
+ handle.setPivotX(handle.width.toFloat())
+ handle.setPivotY(handle.height.toFloat())
+ handle.visibility = VISIBLE
+
+ val growerX = ObjectAnimator.ofFloat(handle, SCALE_X, 0f, 1f).setDuration(HANDLE_ANIMATION_DURATION)
+ val growerY = ObjectAnimator.ofFloat(handle, SCALE_Y, 0f, 1f).setDuration(HANDLE_ANIMATION_DURATION)
+ val alpha = ObjectAnimator.ofFloat(handle, ALPHA, 0f, 1f).setDuration(HANDLE_ANIMATION_DURATION)
+ animatorSet.playTogether(growerX, growerY, alpha)
+ animatorSet.start()
+ }
+
+ private fun hideHandle(){
+ currentAnimator = AnimatorSet()
+ handle.setPivotX(handle.width.toFloat())
+ handle.setPivotY(handle.height.toFloat())
+ val shrinkerX = ObjectAnimator.ofFloat(handle, SCALE_X, 1f, 0f).setDuration(HANDLE_ANIMATION_DURATION)
+ val shrinkerY = ObjectAnimator.ofFloat(handle, SCALE_Y, 1f, 0f).setDuration(HANDLE_ANIMATION_DURATION)
+ val alpha = ObjectAnimator.ofFloat(handle, ALPHA, 1f, 0f).setDuration(HANDLE_ANIMATION_DURATION)
+ currentAnimator?.playTogether(shrinkerX, shrinkerY, alpha)
+ currentAnimator?.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ super.onAnimationEnd(animation)
+ handle.visibility = INVISIBLE
+ currentAnimator = null
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ super.onAnimationCancel(animation)
+ handle.visibility = INVISIBLE
+ currentAnimator = null
+ }
+ })
+ currentAnimator?.start()
+ }
+
+ private inner class ScrollListener : RecyclerView.OnScrollListener(){
+ override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
+
+ val firstVisibleView = recyclerView!!.getChildAt(0)
+ val firstVisiblePosition = recyclerView!!.getChildPosition(firstVisibleView)
+ val visibleRange = recyclerView!!.childCount
+ val lastVisiblePosition = firstVisiblePosition + visibleRange
+ val itemCount = recyclerView!!.adapter.itemCount
+ val position = if (firstVisiblePosition == 0) 0
+ else if (lastVisiblePosition == itemCount - 1) itemCount - 1
+ else firstVisiblePosition
+
+ val proportion = position.toFloat() / itemCount.toFloat()
+ setPosition((widthRef * proportion), heightRef * proportion)
+ }
+ }
+
+ private inner class HandleHider : Runnable {
+ override fun run() {
+ hideHandle()
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/view/ScrollingLinearLayoutManager.kt b/Android/src/org/droidplanner/android/view/ScrollingLinearLayoutManager.kt
new file mode 100644
index 0000000000..f3178a62f1
--- /dev/null
+++ b/Android/src/org/droidplanner/android/view/ScrollingLinearLayoutManager.kt
@@ -0,0 +1,55 @@
+package org.droidplanner.android.view
+
+import android.content.Context
+import android.graphics.PointF
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.LinearSmoothScroller
+import android.support.v7.widget.RecyclerView
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+class ScrollingLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean, val duration: Int) :
+ LinearLayoutManager(context, orientation, reverseLayout){
+
+ companion object {
+ const val TARGET_SEEK_SCROLL_DISTANCE_PX = 10000
+ }
+
+ override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State, position: Int){
+ val firstVisibleChild = recyclerView.getChildAt(0)
+ val itemSize = if(orientation == HORIZONTAL) firstVisibleChild.width else firstVisibleChild.height
+ val currentPosition = recyclerView.getChildAdapterPosition(firstVisibleChild)
+ var distanceInPixels = Math.abs((currentPosition - position) * itemSize)
+ if(distanceInPixels == 0){
+ distanceInPixels = Math.abs(if(orientation == HORIZONTAL) firstVisibleChild.x else firstVisibleChild.y).toInt()
+ }
+
+ val smoothScroller = SmoothScroller(recyclerView.context, distanceInPixels.toFloat(), duration)
+ smoothScroller.targetPosition = position
+ startSmoothScroll(smoothScroller)
+ }
+
+ private inner class SmoothScroller(context: Context, val distanceInPixels: Float, duration: Int) :
+ LinearSmoothScroller(context) {
+
+ private val duration: Float
+
+ init {
+ val millisecondsPerPx = calculateSpeedPerPixel(context.getResources().getDisplayMetrics())
+ this.duration = if (distanceInPixels < TARGET_SEEK_SCROLL_DISTANCE_PX)
+ (Math.abs(distanceInPixels) * millisecondsPerPx)
+ else
+ duration.toFloat()
+ }
+
+ override fun computeScrollVectorForPosition(targetPosition: Int): PointF {
+ return this@ScrollingLinearLayoutManager.computeScrollVectorForPosition(targetPosition)
+ }
+
+ override fun calculateTimeForScrolling(dx: Int): Int {
+ val proportion = dx.toFloat() / distanceInPixels
+ return (duration * proportion).toInt()
+ }
+ }
+}
\ No newline at end of file
diff --git a/Android/src/org/droidplanner/android/view/adapterViews/AbstractRecyclerViewFooterAdapter.kt b/Android/src/org/droidplanner/android/view/adapterViews/AbstractRecyclerViewFooterAdapter.kt
new file mode 100644
index 0000000000..cb9f2a461a
--- /dev/null
+++ b/Android/src/org/droidplanner/android/view/adapterViews/AbstractRecyclerViewFooterAdapter.kt
@@ -0,0 +1,147 @@
+package org.droidplanner.android.view.adapterViews
+
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.ViewGroup
+
+/**
+ * @author ne0fhyk (Fredia Huya-Kouadio)
+ */
+abstract class AbstractRecyclerViewFooterAdapter(recyclerView: RecyclerView, onLoadMoreListener: OnLoadMoreListener?) :
+ RecyclerView.Adapter(){
+
+ private companion object {
+ private const val VISIBLE_THRESHOLD = 5
+
+ private const val ITEM_VIEW_TYPE_BASIC = 0
+ private const val ITEM_VIEW_TYPE_FOOTER = 1
+ }
+
+ private val dataSet = mutableListOf()
+
+ private var firstVisibleItem = 0
+ private var visibleItemCount = 0
+ private var totalItemCount = 0
+ private var previousTotal = 0
+ private var loading = true
+
+ private var hasMoreData = true
+
+ init {
+ if(recyclerView.layoutManager is LinearLayoutManager) {
+ val layoutMgr = recyclerView.layoutManager as LinearLayoutManager
+ recyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener(){
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ totalItemCount = layoutMgr.getItemCount()
+ visibleItemCount = layoutMgr.getChildCount()
+ firstVisibleItem = layoutMgr.findFirstVisibleItemPosition()
+
+ if (loading) {
+ if (totalItemCount > previousTotal) {
+ loading = false
+ previousTotal = totalItemCount
+ }
+ }
+ if (hasMoreData && !loading && totalItemCount - visibleItemCount <= firstVisibleItem + VISIBLE_THRESHOLD) {
+ // End has been reached
+
+ addItem(null)
+ onLoadMoreListener?.onLoadMore()
+ loading = true
+ }
+ }
+ })
+ }
+ }
+
+ fun setHasMoreData(hasMore: Boolean){
+ hasMoreData = hasMore
+ if(!hasMore){
+ removeItem(null)
+ }
+ }
+
+ fun getFirstVisibleItem(): Int {
+ return firstVisibleItem
+ }
+
+ fun resetItems(newDataSet: List?) {
+ loading = true
+ firstVisibleItem = 0
+ visibleItemCount = 0
+ totalItemCount = 0
+ previousTotal = 0
+
+ dataSet.clear()
+
+ if(newDataSet != null) {
+ dataSet.addAll(newDataSet)
+ }
+ notifyDataSetChanged()
+ }
+
+ fun addItems(newDataSetItems: List) {
+ removeItem(null)
+
+ val lastPos = dataSet.size
+ dataSet.addAll(newDataSetItems)
+ notifyItemRangeInserted(lastPos, newDataSetItems.size)
+ }
+
+ private fun addItem(item: T?) {
+ if (!dataSet.contains(item)) {
+ dataSet.add(item)
+ notifyItemInserted(dataSet.size - 1)
+ }
+ }
+
+ private fun removeItem(item: T?) {
+ val indexOfItem = dataSet.indexOf(item)
+ if (indexOfItem != -1) {
+ this.dataSet.removeAt(indexOfItem)
+ notifyItemRemoved(indexOfItem)
+ }
+ }
+
+ fun getItem(index: Int): T {
+ return dataSet[index]?: throw IllegalArgumentException("Item with index $index doesn't exist, dataSet is $dataSet")
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (dataSet.get(position) != null) ITEM_VIEW_TYPE_BASIC else ITEM_VIEW_TYPE_FOOTER
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (getItemViewType(position) == ITEM_VIEW_TYPE_BASIC) {
+ onBindBasicItemView(holder, position);
+ } else {
+ onBindFooterView(holder, position);
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ if (viewType == ITEM_VIEW_TYPE_BASIC) {
+ return onCreateBasicItemViewHolder(parent, viewType);
+ } else if (viewType == ITEM_VIEW_TYPE_FOOTER) {
+ return onCreateFooterViewHolder(parent, viewType);
+ } else {
+ throw IllegalStateException("Invalid type, this type ot items " + viewType + " can't be handled");
+ }
+ }
+
+ override fun getItemCount() = dataSet.size
+
+ abstract fun onCreateBasicItemViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
+
+ abstract fun onBindBasicItemView(genericHolder: RecyclerView.ViewHolder, position: Int)
+
+ abstract fun onCreateFooterViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
+
+ abstract fun onBindFooterView(genericHolder: RecyclerView.ViewHolder, position: Int)
+
+}
+
+interface OnLoadMoreListener {
+ fun onLoadMore()
+
+}
diff --git a/Android/src/org/droidplanner/android/view/adapterViews/MissionItemListAdapter.java b/Android/src/org/droidplanner/android/view/adapterViews/MissionItemListAdapter.java
index ef28b2bd89..a6281d0352 100644
--- a/Android/src/org/droidplanner/android/view/adapterViews/MissionItemListAdapter.java
+++ b/Android/src/org/droidplanner/android/view/adapterViews/MissionItemListAdapter.java
@@ -120,7 +120,7 @@ public void onClick(View v) {
final TextView nameView = viewHolder.nameView;
final TextView altitudeView = viewHolder.altitudeView;
- final MissionProxy missionProxy = proxy.getMission();
+ final MissionProxy missionProxy = proxy.getMissionProxy();
final MissionItem missionItem = proxy.getMissionItem();
nameView.setText(String.format("%3d", missionProxy.getOrder(proxy)));
diff --git a/build.gradle b/build.gradle
index 71b3fe90ef..4aa722567c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ buildscript {
okhttp_version = '2.5.0'
android_build_sdk_version = 23
- android_build_tools_version = '23.0.2'
+ android_build_tools_version = '23.0.3'
android_build_target_sdk_version = 22
android_build_min_sdk_version = 14
}
@@ -31,11 +31,11 @@ buildscript {
}
def getMavenUsername(){
- return hasProperty('DROIDPLANNER_MAVEN_USERNAME') ? DROIDPLANNER_MAVEN_USERNAME : ''
+ return hasProperty('COM_O3DR_MAVEN_USERNAME') ? COM_O3DR_MAVEN_USERNAME : ''
}
def getMavenApiKey(){
- return hasProperty('DROIDPLANNER_MAVEN_APIKEY') ? DROIDPLANNER_MAVEN_APIKEY : ''
+ return hasProperty('COM_O3DR_MAVEN_APIKEY') ? COM_O3DR_MAVEN_APIKEY : ''
}
def getMavenRepoUrl(){