package org.droidplanner.android.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
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.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.analytics.HitBuilders;
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.EditInputDialog;
import org.droidplanner.android.dialogs.openfile.OpenFileDialog;
import org.droidplanner.android.dialogs.openfile.OpenMissionDialog;
import org.droidplanner.android.fragments.EditorListFragment;
import org.droidplanner.android.fragments.EditorMapFragment;
import org.droidplanner.android.fragments.EditorToolsFragment;
import org.droidplanner.android.fragments.EditorToolsFragment.EditorTools;
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.analytics.GAUtils;
import org.droidplanner.android.utils.file.FileStream;
import org.droidplanner.android.utils.file.IO.MissionReader;
import org.droidplanner.android.utils.prefs.AutoPanMode;
import java.util.List;
/**
* 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 {
private static final double DEFAULT_SPEED = 5; //meters per second.
/**
* 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();
static {
eventFilter.addAction(MissionProxy.ACTION_MISSION_PROXY_UPDATE);
eventFilter.addAction(AttributeEvent.MISSION_RECEIVED);
eventFilter.addAction(AttributeEvent.PARAMETERS_REFRESH_ENDED);
}
private final BroadcastReceiver eventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
switch (action) {
case AttributeEvent.PARAMETERS_REFRESH_ENDED:
case MissionProxy.ACTION_MISSION_PROXY_UPDATE:
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 String openedMissionFilename;
private View mLocationButtonsContainer;
private ImageButton itemDetailToggle;
private EditorListFragment editorListFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_editor);
fragmentManager = getSupportFragmentManager();
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.editor_tools_fragment);
editorListFragment = (EditorListFragment) fragmentManager.findFragmentById(R.id.mission_list_fragment);
infoView = (TextView) findViewById(R.id.editorInfoWindow);
mLocationButtonsContainer = findViewById(R.id.location_button_container);
final ImageButton zoomToFit = (ImageButton) findViewById(R.id.zoom_to_fit_button);
zoomToFit.setVisibility(View.VISIBLE);
zoomToFit.setOnClickListener(this);
final ImageButton mGoToMyLocation = (ImageButton) findViewById(R.id.my_location_button);
mGoToMyLocation.setOnClickListener(this);
mGoToMyLocation.setOnLongClickListener(this);
final ImageButton mGoToDroneLocation = (ImageButton) findViewById(R.id.drone_location_button);
mGoToDroneLocation.setOnClickListener(this);
mGoToDroneLocation.setOnLongClickListener(this);
itemDetailToggle = (ImageButton) findViewById(R.id.toggle_action_drawer);
itemDetailToggle.setOnClickListener(this);
if (savedInstanceState != null) {
openedMissionFilename = savedInstanceState.getString(EXTRA_OPENED_MISSION_FILENAME);
}
// Retrieve the item detail fragment using its tag
itemDetailFragment = (MissionDetailFragment) fragmentManager.findFragmentByTag(ITEM_DETAIL_TAG);
gestureMapFragment.setOnPathFinishedListener(this);
openActionDrawer();
}
@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);
// Update the right margin for the my location button
final ViewGroup.MarginLayoutParams marginLp = (ViewGroup.MarginLayoutParams) mLocationButtonsContainer
.getLayoutParams();
final int rightMargin = isOpened ? marginLp.leftMargin + actionDrawer.getWidth() : marginLp.leftMargin;
marginLp.setMargins(marginLp.leftMargin, marginLp.topMargin, rightMargin, marginLp.bottomMargin);
mLocationButtonsContainer.requestLayout();
}
@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);
}
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);
outState.putString(EXTRA_OPENED_MISSION_FILENAME, openedMissionFilename);
}
@Override
protected int getNavigationDrawerEntryId() {
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 OpenMissionDialog() {
@Override
public void waypointFileLoaded(MissionReader reader) {
openedMissionFilename = getSelectedFilename();
missionProxy.readMissionFromFile(reader);
gestureMapFragment.getMapFragment().zoomToFit();
}
};
missionDialog.openDialog(this);
}
private void saveMissionFile() {
final Context context = getApplicationContext();
final String defaultFilename = TextUtils.isEmpty(openedMissionFilename)
? FileStream.getWaypointFilename("waypoints")
: openedMissionFilename;
final EditInputDialog dialog = EditInputDialog.newInstance(context, getString(R.string.label_enter_filename),
defaultFilename, new EditInputDialog.Listener() {
@Override
public void onOk(CharSequence input) {
if (missionProxy.writeMissionToFile(input.toString())) {
Toast.makeText(context, R.string.file_saved_success, Toast.LENGTH_SHORT)
.show();
final HitBuilders.EventBuilder eventBuilder = new HitBuilders.EventBuilder()
.setCategory(GAUtils.Category.MISSION_PLANNING)
.setAction("Mission saved to file")
.setLabel("Mission items count");
GAUtils.sendEvent(eventBuilder);
return;
}
Toast.makeText(context, R.string.file_saved_error, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onCancel() {
}
});
dialog.show(getSupportFragmentManager(), "Mission filename");
}
@Override
public void onBackPressed() {
super.onBackPressed();
gestureMapFragment.getMapFragment().saveCameraPosition();
}
private void updateMissionLength() {
if (missionProxy != null) {
double missionLength = missionProxy.getMissionLength();
LengthUnit convertedMissionLength = unitSystem.getLengthUnitProvider().boxBaseValueToTarget(missionLength);
double speedParameter = dpApp.getDrone().getSpeedParameter() / 100; //cm/s to m/s conversion.
if(speedParameter == 0)
speedParameter = DEFAULT_SPEED;
int time = (int) (missionLength / speedParameter);
String infoString = getString(R.string.editor_info_window_distance, convertedMissionLength.toString())
+ ", " + getString(R.string.editor_info_window_flight_time, time / 60, 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) {
EditorToolsFragment.EditorToolsImpl toolImpl = getToolImpl();
toolImpl.onMapClick(point);
}
public EditorTools getTool() {
return editorToolsFragment.getTool();
}
public EditorToolsFragment.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();
}
@Override
public void skipMarkerClickEvents(boolean skip) {
if (gestureMapFragment == null)
return;
final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
if (planningMapFragment != null)
planningMapFragment.skipMarkerClickEvents(skip);
}
private void setupTool() {
final EditorToolsFragment.EditorToolsImpl toolImpl = getToolImpl();
toolImpl.setup();
editorListFragment.enableDeleteMode(toolImpl.getEditorTools() == EditorTools.TRASH);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
updateLocationButtonsMargin(itemDetailFragment != null);
}
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);
EditorToolsFragment.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;
EditorToolsFragment.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) {
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));
}
}
final EditorMapFragment planningMapFragment = gestureMapFragment.getMapFragment();
if (planningMapFragment != null)
planningMapFragment.postUpdate();
}
}