package org.droidplanner.android.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.FragmentManager;
import android.text.TextUtils;
import android.util.Pair;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.TextView;
import com.o3dr.android.client.utils.FileUtils;
import com.o3dr.services.android.lib.coordinate.LatLong;
import com.o3dr.services.android.lib.drone.attribute.AttributeEvent;
import com.o3dr.services.android.lib.drone.mission.MissionItemType;
import org.beyene.sius.unit.length.LengthUnit;
import org.droidplanner.android.R;
import org.droidplanner.android.activities.interfaces.OnEditorInteraction;
import org.droidplanner.android.dialogs.SupportEditInputDialog;
import org.droidplanner.android.dialogs.openfile.OpenFileDialog;
import org.droidplanner.android.fragments.EditorListFragment;
import org.droidplanner.android.fragments.EditorMapFragment;
import org.droidplanner.android.fragments.account.editor.tool.EditorToolsFragment;
import org.droidplanner.android.fragments.account.editor.tool.EditorToolsFragment.EditorTools;
import org.droidplanner.android.fragments.account.editor.tool.EditorToolsImpl;
import org.droidplanner.android.fragments.helpers.GestureMapFragment;
import org.droidplanner.android.fragments.helpers.GestureMapFragment.OnPathFinishedListener;
import org.droidplanner.android.proxy.mission.MissionProxy;
import org.droidplanner.android.proxy.mission.MissionSelection;
import org.droidplanner.android.proxy.mission.item.MissionItemProxy;
import org.droidplanner.android.proxy.mission.item.fragments.MissionDetailFragment;
import org.droidplanner.android.utils.file.DirectoryPath;
import org.droidplanner.android.utils.file.FileList;
import org.droidplanner.android.utils.file.FileStream;
import org.droidplanner.android.utils.prefs.AutoPanMode;
import org.droidplanner.android.utils.prefs.DroidPlannerPrefs;
import java.io.File;
import java.util.List;
import java.util.Locale;
/**
* This implements the map editor activity. The map editor activity allows the
* user to create and/or modify autonomous missions for the drone.
*/
public class EditorActivity extends DrawerNavigationUI implements OnPathFinishedListener,
EditorToolsFragment.EditorToolListener, MissionDetailFragment.OnMissionDetailListener,
OnEditorInteraction, MissionSelection.OnSelectionUpdateListener, OnClickListener,
OnLongClickListener, SupportEditInputDialog.Listener {
/**
* Used to retrieve the item detail window when the activity is destroyed,
* and recreated.
*/
private static final String ITEM_DETAIL_TAG = "Item Detail Window";
private static final String EXTRA_OPENED_MISSION_FILENAME = "extra_opened_mission_filename";
private static final IntentFilter eventFilter = new IntentFilter();
private static final String MISSION_FILENAME_DIALOG_TAG = "Mission filename";
static {
eventFilter.addAction(MissionProxy.ACTION_MISSION_PROXY_UPDATE);
eventFilter.addAction(AttributeEvent.MISSION_RECEIVED);
eventFilter.addAction(AttributeEvent.PARAMETERS_REFRESH_COMPLETED);
eventFilter.addAction(DroidPlannerPrefs.PREF_VEHICLE_DEFAULT_SPEED);
eventFilter.addAction(AttributeEvent.STATE_CONNECTED);
eventFilter.addAction(AttributeEvent.HEARTBEAT_RESTORED);
}
private final BroadcastReceiver eventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
switch (action) {
case MissionProxy.ACTION_MISSION_PROXY_UPDATE:
if (mAppPrefs.isZoomToFitEnable()) gestureMapFragment.getMapFragment().zoomToFit();
// FALL THROUGH
case AttributeEvent.PARAMETERS_REFRESH_COMPLETED:
case DroidPlannerPrefs.PREF_VEHICLE_DEFAULT_SPEED:
case AttributeEvent.STATE_CONNECTED:
case AttributeEvent.HEARTBEAT_RESTORED:
updateMissionLength();
break;
case AttributeEvent.MISSION_RECEIVED:
final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
if (planningMapFragment != null) {
planningMapFragment.zoomToFit();
}
break;
}
}
};
/**
* Used to provide access and interact with the
* {@link org.droidplanner.android.proxy.mission.MissionProxy} object on the Android
* layer.
*/
private MissionProxy missionProxy;
/*
* View widgets.
*/
private GestureMapFragment gestureMapFragment;
private EditorToolsFragment editorToolsFragment;
private MissionDetailFragment itemDetailFragment;
private FragmentManager fragmentManager;
private TextView infoView;
/**
* If the mission was loaded from a file, the filename is stored here.
*/
private File openedMissionFile;
private FloatingActionButton itemDetailToggle;
private EditorListFragment editorListFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
fragmentManager = getSupportFragmentManager();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_editor);
gestureMapFragment = ((GestureMapFragment) fragmentManager.findFragmentById(R.id.editor_map_fragment));
if (gestureMapFragment == null) {
gestureMapFragment = new GestureMapFragment();
fragmentManager.beginTransaction().add(R.id.editor_map_fragment, gestureMapFragment).commit();
}
editorToolsFragment = (EditorToolsFragment) fragmentManager.findFragmentById(R.id.mission_tools_fragment);
infoView = (TextView) findViewById(R.id.editorInfoWindow);
final FloatingActionButton zoomToFit = (FloatingActionButton) findViewById(R.id.zoom_to_fit_button);
zoomToFit.setVisibility(View.VISIBLE);
zoomToFit.setOnClickListener(this);
final FloatingActionButton mGoToMyLocation = (FloatingActionButton) findViewById(R.id.my_location_button);
mGoToMyLocation.setOnClickListener(this);
mGoToMyLocation.setOnLongClickListener(this);
final FloatingActionButton mGoToDroneLocation = (FloatingActionButton) findViewById(R.id.drone_location_button);
mGoToDroneLocation.setOnClickListener(this);
mGoToDroneLocation.setOnLongClickListener(this);
itemDetailToggle = (FloatingActionButton) findViewById(R.id.toggle_action_drawer);
itemDetailToggle.setOnClickListener(this);
if (savedInstanceState != null) {
String openedMissionFilename = savedInstanceState.getString(EXTRA_OPENED_MISSION_FILENAME);
if(!TextUtils.isEmpty(openedMissionFilename)) {
openedMissionFile = new File(openedMissionFilename);
}
}
// Retrieve the item detail fragment using its tag
itemDetailFragment = (MissionDetailFragment) fragmentManager.findFragmentByTag(ITEM_DETAIL_TAG);
gestureMapFragment.setOnPathFinishedListener(this);
openActionDrawer();
}
@Override
protected void onNewIntent(Intent intent){
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent){
if(intent == null || missionProxy == null)
return;
String action = intent.getAction();
if(TextUtils.isEmpty(action))
return;
switch (action) {
case Intent.ACTION_VIEW:
Uri loadUri = intent.getData();
if (loadUri != null) {
openMissionFile(loadUri);
}
break;
}
}
@Override
protected float getActionDrawerTopMargin() {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, getResources().getDisplayMetrics());
}
/**
* Account for the various ui elements and update the map padding so that it
* remains 'visible'.
*/
private void updateLocationButtonsMargin(boolean isOpened) {
final View actionDrawer = getActionDrawer();
if (actionDrawer == null)
return;
itemDetailToggle.setActivated(isOpened);
}
@Override
public void onApiConnected() {
super.onApiConnected();
missionProxy = dpApp.getMissionProxy();
if (missionProxy != null) {
missionProxy.selection.addSelectionUpdateListener(this);
itemDetailToggle.setVisibility(missionProxy.selection.getSelected().isEmpty() ? View.GONE : View.VISIBLE);
}
handleIntent(getIntent());
updateMissionLength();
getBroadcastManager().registerReceiver(eventReceiver, eventFilter);
}
@Override
public void onApiDisconnected() {
super.onApiDisconnected();
if (missionProxy != null)
missionProxy.selection.removeSelectionUpdateListener(this);
getBroadcastManager().unregisterReceiver(eventReceiver);
}
@Override
public void onClick(View v) {
final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
switch (v.getId()) {
case R.id.toggle_action_drawer:
if (missionProxy == null)
return;
if (itemDetailFragment == null) {
List<MissionItemProxy> selected = missionProxy.selection.getSelected();
showItemDetail(selectMissionDetailType(selected));
} else {
removeItemDetail();
}
break;
case R.id.zoom_to_fit_button:
if (planningMapFragment != null) {
planningMapFragment.zoomToFit();
}
break;
case R.id.drone_location_button:
planningMapFragment.goToDroneLocation();
break;
case R.id.my_location_button:
planningMapFragment.goToMyLocation();
break;
default:
break;
}
}
@Override
public boolean onLongClick(View view) {
final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
switch (view.getId()) {
case R.id.drone_location_button:
planningMapFragment.setAutoPanMode(AutoPanMode.DRONE);
return true;
case R.id.my_location_button:
planningMapFragment.setAutoPanMode(AutoPanMode.USER);
return true;
default:
return false;
}
}
@Override
public void onResume() {
super.onResume();
editorToolsFragment.setToolAndUpdateView(getTool());
setupTool();
}
@Override
protected int getToolbarId() {
return R.id.actionbar_container;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(openedMissionFile != null) {
outState.putString(EXTRA_OPENED_MISSION_FILENAME, openedMissionFile.getAbsolutePath());
}
}
@Override
protected int getNavigationDrawerMenuItemId() {
return R.id.navigation_editor;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu_mission, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_open_mission:
openMissionFile();
return true;
case R.id.menu_save_mission:
saveMissionFile();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void openMissionFile() {
OpenFileDialog missionDialog = new OpenFileDialog() {
@Override
public void onFileSelected(String filepath) {
File missionFile = new File(filepath);
openedMissionFile = missionFile;
openMissionFile(Uri.fromFile(missionFile));
}
};
missionDialog.openDialog(this, DirectoryPath.getWaypointsPath(), FileList.getWaypointFileList());
}
private void openMissionFile(Uri missionUri){
if(missionProxy != null) {
missionProxy.readMissionFromFile(missionUri);
}
}
@Override
public void onOk(String dialogTag, CharSequence input) {
switch (dialogTag) {
case MISSION_FILENAME_DIALOG_TAG:
File saveFile = openedMissionFile == null
? new File(DirectoryPath.getWaypointsPath(), input.toString() + FileList.WAYPOINT_FILENAME_EXT)
: new File(openedMissionFile.getParent(), input.toString() + FileList.WAYPOINT_FILENAME_EXT);
missionProxy.writeMissionToFile(Uri.fromFile(saveFile));
break;
}
}
@Override
public void onCancel(String dialogTag) {
}
private void saveMissionFile() {
final String defaultFilename = openedMissionFile == null
? getWaypointFilename("waypoints")
: FileUtils.getFilenameWithoutExtension(openedMissionFile);
final SupportEditInputDialog dialog = SupportEditInputDialog.newInstance(MISSION_FILENAME_DIALOG_TAG,
getString(R.string.label_enter_filename), defaultFilename, true);
dialog.show(getSupportFragmentManager(), MISSION_FILENAME_DIALOG_TAG);
}
private static String getWaypointFilename(String prefix){
return prefix + "-" + FileStream.getTimeStamp();
}
@Override
public void onBackPressed() {
super.onBackPressed();
gestureMapFragment.getMapFragment().saveCameraPosition();
}
private void updateMissionLength() {
if (missionProxy != null) {
Pair<Double, Double> distanceAndTime = missionProxy.getMissionFlightTime();
LengthUnit convertedMissionLength = unitSystem.getLengthUnitProvider()
.boxBaseValueToTarget(distanceAndTime.first);
double time = distanceAndTime.second;
String infoString = getString(R.string.editor_info_window_distance,
convertedMissionLength.toString()) +
", " +
getString(R.string.editor_info_window_flight_time, time == Double.POSITIVE_INFINITY
? time
: String.format(Locale.US, "%1$02d:%2$02d", ((int) time / 60), ((int) time % 60)));
infoView.setText(infoString);
// Remove detail window if item is removed
if (missionProxy.selection.getSelected().isEmpty() && itemDetailFragment != null) {
removeItemDetail();
}
}
}
@Override
public void onMapClick(LatLong point) {
EditorToolsImpl toolImpl = getToolImpl();
toolImpl.onMapClick(point);
}
public EditorTools getTool() {
return editorToolsFragment.getTool();
}
public EditorToolsImpl getToolImpl() {
return editorToolsFragment.getToolImpl();
}
@Override
public void editorToolChanged(EditorTools tools) {
setupTool();
}
@Override
public void enableGestureDetection(boolean enable) {
if (gestureMapFragment == null)
return;
if (enable)
gestureMapFragment.enableGestureDetection();
else
gestureMapFragment.disableGestureDetection();
}
private void setupTool() {
final EditorToolsImpl toolImpl = getToolImpl();
toolImpl.setup();
editorListFragment.enableDeleteMode(toolImpl.getEditorTools() == EditorTools.TRASH);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
updateLocationButtonsMargin(itemDetailFragment != null);
}
@Override
protected void addToolbarFragment(){
final int toolbarId = getToolbarId();
editorListFragment = (EditorListFragment) fragmentManager.findFragmentById(toolbarId);
if (editorListFragment == null) {
editorListFragment = new EditorListFragment();
fragmentManager.beginTransaction().add(toolbarId, editorListFragment).commit();
}
}
private void showItemDetail(MissionDetailFragment itemDetail) {
if (itemDetailFragment == null) {
addItemDetail(itemDetail);
} else {
switchItemDetail(itemDetail);
}
editorToolsFragment.setToolAndUpdateView(EditorTools.NONE);
}
private void addItemDetail(MissionDetailFragment itemDetail) {
itemDetailFragment = itemDetail;
if (itemDetailFragment == null)
return;
fragmentManager.beginTransaction()
.replace(getActionDrawerId(), itemDetailFragment, ITEM_DETAIL_TAG)
.commit();
updateLocationButtonsMargin(true);
}
public void switchItemDetail(MissionDetailFragment itemDetail) {
removeItemDetail();
addItemDetail(itemDetail);
}
private void removeItemDetail() {
if (itemDetailFragment != null) {
fragmentManager.beginTransaction().remove(itemDetailFragment).commit();
itemDetailFragment = null;
updateLocationButtonsMargin(false);
}
}
@Override
public void onPathFinished(List<LatLong> path) {
final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
List<LatLong> points = planningMapFragment.projectPathIntoMap(path);
EditorToolsImpl toolImpl = getToolImpl();
toolImpl.onPathFinished(points);
}
@Override
public void onDetailDialogDismissed(List<MissionItemProxy> itemList) {
if (missionProxy != null) missionProxy.selection.removeItemsFromSelection(itemList);
}
@Override
public void onWaypointTypeChanged(MissionItemType newType, List<Pair<MissionItemProxy,
List<MissionItemProxy>>> oldNewItemsList) {
missionProxy.replaceAll(oldNewItemsList);
}
private MissionDetailFragment selectMissionDetailType(List<MissionItemProxy> proxies) {
if (proxies == null || proxies.isEmpty())
return null;
MissionItemType referenceType = null;
for (MissionItemProxy proxy : proxies) {
final MissionItemType proxyType = proxy.getMissionItem().getType();
if (referenceType == null) {
referenceType = proxyType;
} else if (referenceType != proxyType
|| MissionDetailFragment.typeWithNoMultiEditSupport.contains(referenceType)) {
//Return a generic mission detail.
return new MissionDetailFragment();
}
}
return MissionDetailFragment.newInstance(referenceType);
}
@Override
public void onItemClick(MissionItemProxy item, boolean zoomToFit) {
if (missionProxy == null) return;
EditorToolsImpl toolImpl = getToolImpl();
toolImpl.onListItemClick(item);
if (zoomToFit) {
zoomToFitSelected();
}
}
@Override
public void zoomToFitSelected() {
final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
List<MissionItemProxy> selected = missionProxy.selection.getSelected();
if (selected.isEmpty()) {
planningMapFragment.zoomToFit();
} else {
planningMapFragment.zoomToFit(MissionProxy.getVisibleCoords(selected));
}
}
@Override
public void onListVisibilityChanged() {
}
@Override
protected boolean enableMissionMenus() {
return true;
}
@Override
public void onSelectionUpdate(List<MissionItemProxy> selected) {
EditorToolsImpl toolImpl = getToolImpl();
toolImpl.onSelectionUpdate(selected);
final boolean isEmpty = selected.isEmpty();
if (isEmpty) {
itemDetailToggle.setVisibility(View.GONE);
removeItemDetail();
} else {
itemDetailToggle.setVisibility(View.VISIBLE);
if (getTool() == EditorTools.SELECTOR)
removeItemDetail();
else {
showItemDetail(selectMissionDetailType(selected));
}
}
}
}