大疆文档(7)-Android教程-地图视图和航点App

本节全篇为大疆 Mobile SDK 安卓教程 部分,ios教程参见 IOS教程 .

地图视图和航点应用程序

在本教程中,您将学习如何实现 DJIWaypoint Mission 功能并熟悉MissionControl的用法。此外,您还将了解如何使用DJI Assistant 2 Simulator测试Waypoint Mission API。让我们开始吧!

您可以从此 Github Page 下载教程的最终示例项目。

Note:在本教程中,我们将使用 Mavic Pro 进行测试,使用Android Studio 2.1.1 开发演示应用程序(我的版本是3.3.2),并使用 Gaode Map API 进行导航。

准备

下载SDK并安装Android开发环境

最新的 Android SDK: https://developer.dji.com/mobile-sdk/downloads.

Android Studio : http://developer.android.com/sdk/index.html.

应用激活与在中国绑定飞机

对于在中国使用的DJI SDK移动应用程序,需要激活应用程序并将飞机绑定到用户的DJI帐户。

如果未激活应用程序,未绑定飞机(如果需要)或使用旧版SDK(<4.1),则将禁用所有 相机实时流 ,并且飞行将限制为直径100米和高度为30米的区域,以确保飞机保持在视线范围内。

要了解如何实现此功能,请查看本教程 Application Activation and Aircraft Binding.

实现应用程序的UI

我们可以使用地图视图去显示航点,并且当航点任务被执行的时候显示飞机的飞行路线。在这里,我们以 Gaode Map (高德地图) 为例。

配置 AMAP API Key

1. 创建项目

  • 创建名为 GSDemo 的新项目
  • 包名 com.dji.GSDemo.GaodeMap
  • 最低版本 API 19: Android 4.4 (KitKat)
  • 选择 "Empty Activity" 然后其他默认

2. 生成 SHA-1 Key

我们可以使用 Android Studio 轻松生成SHA-1密钥。点击Android Studio右侧的 Gradle 选项卡。选择项目并导航到 Tasks - > android - > signingReport

signingReport

然后双击 signingReport 并检查 Android Studio 的控制台区域,您可以轻松找到 SHA-1 密钥:

SHA1

3. 申请 AMAP Key

现在,让我们去 AMAP Developer Platform 申请AMAP Key。如果您是第一次访问这个网站,请先注册。然后使用您的amap帐户登录并按右上角的 "+创建新应用" 按钮。输入您的应用程序名称,然后按 "创建" 继续。您将在此处看到以下屏幕截图:

createApplication

接下来,单击 "GSDemo" 应用程序右上角的 "添加新Key" 按钮。根据需要输入信息,对于 "发布版安全码:SHA1" 和 "调试版安全码SHA1" 字段,请输入我们刚刚在上述步骤中生成的SHA-1密钥。

createAMAPKey

Note: "Package" 应与Android项目的Package名称相同。

按 "提交" 后你可以得到你的 AMAP Key :

AMAPKey

4. 添加 AMAP Key

打开 AndroidManifest.xml 文件,添加以下元素作为元素的子元素, 并在 value 属性中替换您的AMAP Key 为 "YOUR _ AMAP_KEY" ,如下所示:

<!-- 启用高德地图服务 -->
<meta-data
    android:name="com.amap.api.v2.apikey"
    android:value="YOUR_AMAP_KEY" />

这会将key "com.amap.api.v2.apikey" 设置为你的AMAP键的值。

接下来,通过在 "AndroidManifest.xml" 文件中添加 <uses-permission> 元素作为 <manifest> 元素的子元素来指定应用程序所需的权限。

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />

更多的权限问题请参阅 http://lbs.amap.com/api/android-sdk/guide/create-project/dev-attention .

导入AMAP JAR包

让我们去 http://lbs.amap.com/api/android-sdk/down/ 下载最新版本的 2D地图SDK 和 搜索功能SDK ,如下所示:

amapSDK

完成下载后,复制这两个 SDK jar 文件到 Android Studio 项目的 libs 文件夹:app-> libs

(目前版本为一个合成的sdk jar包,打开 "Project Structure" 如下方式导入即可,记得勾选remove 原包,否则会报重复资源异常)

然后右键单击项目导航栏中的 app 文件夹,选择 "Open Module Settings" 打开 "Project Structure" 窗口。

openModuleSettings

接下来,点击窗口左上角的 "+" 按钮,选择 "Import .JAR/.ARR Package", 然后点击 "Next" 按钮。此外,在 "Create New Module" 窗口的 "File name" 字段中选择amap包,如下所示:

selectLibsPackage

然后按 "Finish" 按钮以导入 AMap_2DMap JAR 包:

createNewModule

再次重复此过程以导入 AMap_Search JAR 包。直到 Gradle 同步完成。如果一切顺利,当您打开 "build.gradle(Module: app)" 文件时,您应该会看到 "dependencies" 中包含的两个 AMap JAR 包(Android Studio 3.3.2中没有):

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.3.0'
    compile project(':AMap_2DMap_V2.8.1_20160202')
    compile project(':AMap_Search_V3.2.1_20160308')
}

导入 Maven 依赖

您可以查看 Integrate SDK into Application 教程中,以了解如何导入Android SDK Maven依赖项。

构建 MainActivity 布局

实现 MainActivity 布局

打开 activity_main.xml 布局文件,并使用以下代码替换:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#FFFFFF"
    tools:context="com.dji.GSDemo.GaodeMap.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/ConnectStatusTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="GSDemo"
            android:gravity="center"
            android:textColor="#000000"
            android:textSize="21sp"
            />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/locate"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Locate"
            android:layout_weight="1"/>
        <Button
            android:id="@+id/add"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Add"
            android:layout_weight="1"/>
        <Button
            android:id="@+id/clear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Clear"
            android:layout_weight="1"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/config"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Config"
            android:layout_weight="0.9"/>
        <Button
            android:id="@+id/upload"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Upload"
            android:layout_weight="0.9"/>
        <Button
            android:id="@+id/start"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Start"
            android:layout_weight="1"/>
        <Button
            android:id="@+id/stop"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Stop"
            android:layout_weight="1"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <com.amap.api.maps2d.MapView
            android:id="@+id/map"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>

</LinearLayout>

在 xml 文件中,我们实现以下UI:

  1. 创建一个 LinearLayout 用以显示带有 "GSDemo" 标题的TextView并将其置于顶部。
  2. 创建两行按钮: "LOCATE", "ADD", "CLEAR", "CONFIG", "UPLOAD", "START" and "STOP",将它们水平放置。
  3. 最后,我们创建一个 MapView Fragment 并将其放在底部。

接下来,从本教程的 Github sample project 中把 "aircraft.png" and "ic_launcher.png" 图片复制到 res 文件夹中的 drawable 文件夹中。

然后打开 AndroidManifest.xml 文件并为 ".MainActivity" activity 元素添加几个属性,如下所示:

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize"
    android:label="@string/app_name"
    android:screenOrientation="landscape"
    android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

现在,如果您检查 "activity_main.xml" 文件,您应该会看到MainActivity的预览屏幕截图,如下所示:

MainActivity

最后,让我们创建一个名为 "dialog_waypointsetting.xml" 的新xml文件,然后用这个项目 Github sample project 中的相同文件替换代码,因为内容太多,我们不会在这里显示它们。

这个xml文件将帮助设置一个TextView用以输入 "Altitude" 并创建三个 RadioButton 组用以选择 Speed, Action After Finished and Heading

现在,如果您检查 dialog_waypointsetting.xml 文件,您可以看到航点配置弹框的预览屏幕截图,如下所示:

MainActivity

使用 MainActivity 类

让我们回到 MainActivity.java 类,用以下内容替换,记得按照Android Studio的建议导入相关类:

public class MainActivity extends FragmentActivity implements View.OnClickListener, OnMapClickListener {

    protected static final String TAG = "MainActivity";
    
    private MapView mapView;
    private AMap aMap;
    
    private Button locate, add, clear;
    private Button config, upload, start, stop;

    @Override
    protected void onResume(){
        super.onResume();
    }

    @Override
    protected void onPause(){
        super.onPause();
    }

    @Override
    protected void onDestroy(){
        super.onDestroy();
    }

    /**
     * @Description : RETURN BTN RESPONSE FUNCTION
     */
    public void onReturn(View view){
        Log.d(TAG, "onReturn");
        this.finish();
    }

    private void initUI() {
        locate = (Button) findViewById(R.id.locate);
        add = (Button) findViewById(R.id.add);
        clear = (Button) findViewById(R.id.clear);
        config = (Button) findViewById(R.id.config);
        upload = (Button) findViewById(R.id.upload);
        start = (Button) findViewById(R.id.start);
        stop = (Button) findViewById(R.id.stop);

        locate.setOnClickListener(this);
        add.setOnClickListener(this);
        clear.setOnClickListener(this);
        config.setOnClickListener(this);
        upload.setOnClickListener(this);
        start.setOnClickListener(this);
        stop.setOnClickListener(this);
    }

    private void initMapView() {

        if (aMap == null) {
            aMap = mapView.getMap();
            aMap.setOnMapClickListener(this);// add the listener for click for amap object
        }

        LatLng shenzhen = new LatLng(22.5362, 113.9454);
        aMap.addMarker(new MarkerOptions().position(shenzhen).title("Marker in Shenzhen"));
        aMap.moveCamera(CameraUpdateFactory.newLatLng(shenzhen));
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_main);

        mapView = (MapView) findViewById(R.id.map);
        mapView.onCreate(savedInstanceState);

        initMapView();
        initUI();

    }

    private void showSettingDialog(){
        LinearLayout wayPointSettings = (LinearLayout)getLayoutInflater().inflate(R.layout.dialog_waypointsetting, null);

        final TextView wpAltitude_TV = (TextView) wayPointSettings.findViewById(R.id.altitude);
        RadioGroup speed_RG = (RadioGroup) wayPointSettings.findViewById(R.id.speed);
        RadioGroup actionAfterFinished_RG = (RadioGroup) wayPointSettings.findViewById(R.id.actionAfterFinished);
        RadioGroup heading_RG = (RadioGroup) wayPointSettings.findViewById(R.id.heading);

        speed_RG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener(){

            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                // TODO Auto-generated method stub
                Log.d(TAG, "Select Speed finish");
            }
        });

        actionAfterFinished_RG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                // TODO Auto-generated method stub
                Log.d(TAG, "Select action action");
            }
        });

        heading_RG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {

            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                // TODO Auto-generated method stub
                Log.d(TAG, "Select heading finish");
            }
        });

        new AlertDialog.Builder(this)
                .setTitle("")
                .setView(wayPointSettings)
                .setPositiveButton("Finish",new DialogInterface.OnClickListener(){
                    public void onClick(DialogInterface dialog, int id) {
                    }
                })
                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.cancel();
                    }
                })
                .create()
                .show();
    }
    
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
        switch (v.getId()) {
            case R.id.config:{
                showSettingDialog();
                break;
            }
            default:
                break;
        }
    }

    @Override
    public void onMapClick(LatLng point) {
    }
}

在上面显示的代码中,我们实现了以下功能:

1. 为 UI 创建 MapView 和 AMap 变量以及7个 Button 成员变量。然后创建 initUI() 方法来初始化7个 Button 变量并实现它们的 setOnClickListener 方法并将 "this" 作为参数传递。

2.onCreate() 方法中,我们在运行时请求几个权限,以确保当编译和目标SDK版本高于22时(如Android Marshmallow 6.0设备和API 23),SDK可以正常工作。

3. 然后调用 initUI() 方法初始化UI。然后调用 initMapView() 方法创建 MapView 并在此处添加标记为shenzhen。因此,当加载Gaode地图时,您将在中国深圳上面看到一个蓝针标记。

4. 实现 showSettingDialog 方法来显示 Waypoint Configuration 对话框,并重写 onClick() 方法,当按下 Config 按钮的时候显示配置对话框。

实现航点任务

在高德地图上定位飞机

在我们实施航点任务功能之前,我们应该在Gaode地图上显示飞机的位置,并尝试自动放大以查看飞机的周围区域。

让我们打开 MainActivity.java 文件并首先声明以下变量:

private double droneLocationLat = 181, droneLocationLng = 181;
private Marker droneMarker = null;
private FlightController mFlightController;

然后,由于我们需要检测产品连接状态,我们应该在 onCreate() 方法中注册一个 BroadcastReceiver 并重写 onReceive() 方法,如下所示:

@Override
protected void onDestroy(){
    super.onDestroy();
    unregisterReceiver(mReceiver);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    IntentFilter filter = new IntentFilter();
    filter.addAction(DJIDemoApplication.FLAG_CONNECTION_CHANGE);
    registerReceiver(mReceiver, filter);

    mapView = (MapView) findViewById(R.id.map);
    mapView.onCreate(savedInstanceState);

    initMapView();
    initUI();
}
    
protected BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            onProductConnectionChange();
        }
    };

当DJI产品连接状态发生变化时,onReceive() 方法将被调用,我们可以用它来更新我们飞机的位置。

接下来,让我们实现 initFlightController() 方法并在 onProductConnectionChange() 方法中调用它:

private void onProductConnectionChange()
{
    initFlightController();
}

 private void initFlightController() {

        BaseProduct product = DJIDemoApplication.getProductInstance();
        if (product != null && product.isConnected()) {
            if (product instanceof Aircraft) {
                mFlightController = ((Aircraft) product).getFlightController();
            }
        }

        if (mFlightController != null) {
            mFlightController.setStateCallback(
                    new FlightControllerState.Callback() {
                @Override
                public void onUpdate(FlightControllerState djiFlightControllerCurrentState) {
                    droneLocationLat = djiFlightControllerCurrentState.getAircraftLocation().getLatitude();
                    droneLocationLng = djiFlightControllerCurrentState.getAircraftLocation().getLongitude();
                    updateDroneLocation();

                }
            });
        }
    }

在上面的代码中,我们首先使用 BaseProduct 对象的 isConnected() 方法检查产品连接状态。然后初始化 mFlightController 变量并重写 onUpdate() 方法以调用 updateDroneLocation 方法。通过使用该onUpdate()方法,您可以从参数中获取飞行控制器的当前状态。

此外,让我们实现 updateDroneLocation() 方法并通过 onClick() 方法的 locate 按钮点击操作调用它:

public static boolean checkGpsCoordination(double latitude, double longitude) {
        return (latitude > -90 && latitude < 90 && longitude > -180 && longitude < 180) && (latitude != 0f && longitude != 0f);
}
    
private void updateDroneLocation(){

    LatLng pos = new LatLng(droneLocationLat, droneLocationLng);
    //Create MarkerOptions object
    final MarkerOptions markerOptions = new MarkerOptions();
    markerOptions.position(pos);
    markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.aircraft));

    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            if (droneMarker != null) {
                droneMarker.remove();
            }

            if (checkGpsCoordination(droneLocationLat, droneLocationLng)) {
                droneMarker = aMap.addMarker(markerOptions);
            }
        }
    });
}

@Override
public void onClick(View v) {
    // TODO Auto-generated method stub
    switch (v.getId()) {
        case R.id.locate:{
            updateDroneLocation();
            cameraUpdate();
            break;
        }
        case R.id.config:{
            showSettingDialog();
            break;
        }
        default:
            break;
    }
}

updateDroneLocation() 方法中,我们在Gaode地图上添加无人机位置标记。

最后,让我们实现 camearUpdate() 方法去移动相机并将地图放大到无人机位置的:

private void cameraUpdate(){
    LatLng pos = new LatLng(droneLocationLat, droneLocationLng);
    float zoomlevel = (float) 18.0;
    CameraUpdate cu = CameraUpdateFactory.newLatLngZoom(pos, zoomlevel);
    aMap.moveCamera(cu);
}

在继续之前,您可以看一下 Using DJI Assistant 2 Simulator 的基本用法。

现在,让我们通过Micro USB线将飞机连接到运行 DJI Assistant 2 的电脑,然后打开飞机和遥控器的电源。按下 DJI Assistant 2 的 Simulator 按钮,随意输入您当前位置的纬度和经度数据到模拟器中。

simulatorPreview

接下来,构建并运行项目并将其安装在Android设备中,然后用USB线将其连接到遥控器。

Start Simulating 按钮。如果您现在看应用程序,地图上会显示一架小红色飞机。如果找不到飞机,请按 "LOCATE" 按钮飞机就会放大到地图中心。你可以看看这个gif动画:

locateAircraft

添加Waypoint标记

由于您现在可以在Gaode地图上清楚地看到飞机,您可以在地图上添加 Marker 以显示航点任务的航点。我们继续声明 mMarkers 变量:

private boolean isAdd = false;
private final Map<Integer, Marker> mMarkers = new ConcurrentHashMap<Integer, Marker>();

然后,实现 onMapClick() and markWaypoint() 方法如下所示:

private void setResultToToast(final String string){
    MainActivity.this.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(MainActivity.this, string, Toast.LENGTH_SHORT).show();
        }
    });
}
    
@Override
public void onMapClick(LatLng point) {
    if (isAdd == true){
        markWaypoint(point);       
    }else{
        setResultToToast("Cannot add waypoint");
    }
}
    
private void markWaypoint(LatLng point){
    //Create MarkerOptions object
    MarkerOptions markerOptions = new MarkerOptions();
    markerOptions.position(point);
  markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE));
    Marker marker = aMap.addMarker(markerOptions);
    mMarkers.put(mMarkers.size(), marker);
}

这里,当用户点击地图时 onMapClick() 方法将被调用。当用户点击地图的不同位置时,我们将创建一个 MarkerOptions 对象并为其分配 "LatLng" 对象,然后通过传递 markerOptions 参数调用aMap的 addMarker() 方法,在Gaode地图上去添加一个航点标记。

最后,让我们实现 onClick() and enableDisableAdd() 方法来实现 ADDCLEAR 操作,如下所示:

 @Override
 public void onClick(View v) {
    switch (v.getId()) {
        case R.id.add:{
            enableDisableAdd();
            break;
        }
        case R.id.clear:{
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    aMap.clear();
                }
                
            });
            updateDroneLocation();
            break;
        }
        case R.id.config:{
            showSettingDialog();
            break;
        }
        default:
            break;
    }
}
    
private void enableDisableAdd(){
   if (isAdd == false) {
      isAdd = true;
      add.setText("Exit");
   }else{
      isAdd = false;
      add.setText("Add");
   }
 }

现在,让我们尝试在Android设备上构建和运行您的应用程序,并尝试在Gaode地图上添加航点。如果一切顺利,你应该看到以下gif动画:

addWaypointsAni

实现 Waypoint 任务

配置 Waypoint 任务

在我们上传航点任务之前,我们应该为用户提供一种配置它的方法,比如设置飞行高度,速度,飞机头等。所以让我们首先声明几个变量,如下所示:

private float altitude = 100.0f;
private float mSpeed = 10.0f;

private List<Waypoint> waypointList = new ArrayList<>();

public static WaypointMission.Builder waypointMissionBuilder;
private WaypointMissionOperator instance;
private WaypointMissionFinishedAction mFinishedAction = WaypointMissionFinishedAction.NO_ACTION;
private WaypointMissionHeadingMode mHeadingMode = WaypointMissionHeadingMode.AUTO;

这里我们声明了 altitude, mSpeed, mFinishedActionmHeadingMode 变量并使用默认值初始化。此外,我们声明 WaypointMission.BuilderWaypointMissionOperator 变量用于设置任务。

接下来,将 showSettingDialog() 方法的代码替换为以下内容:

private void showSettingDialog(){
    LinearLayout wayPointSettings = (LinearLayout)getLayoutInflater().inflate(R.layout.dialog_waypointsetting, null);

    final TextView wpAltitude_TV = (TextView) wayPointSettings.findViewById(R.id.altitude);
    RadioGroup speed_RG = (RadioGroup) wayPointSettings.findViewById(R.id.speed);
    RadioGroup actionAfterFinished_RG = (RadioGroup) wayPointSettings.findViewById(R.id.actionAfterFinished);
    RadioGroup heading_RG = (RadioGroup) wayPointSettings.findViewById(R.id.heading);

    speed_RG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener(){

        @Override
        public void onCheckedChanged(RadioGroup group, int checkedId) {
            if (checkedId == R.id.lowSpeed){
                mSpeed = 3.0f;
            } else if (checkedId == R.id.MidSpeed){
                mSpeed = 5.0f;
            } else if (checkedId == R.id.HighSpeed){
                mSpeed = 10.0f;
            }
        }

    });

    actionAfterFinished_RG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {

        @Override
        public void onCheckedChanged(RadioGroup group, int checkedId) {
            Log.d(TAG, "Select finish action");
            if (checkedId == R.id.finishNone){
                mFinishedAction = WaypointMissionFinishedAction.NO_ACTION;
            } else if (checkedId == R.id.finishGoHome){
                mFinishedAction = WaypointMissionFinishedAction.GO_HOME;
            } else if (checkedId == R.id.finishAutoLanding){
                mFinishedAction = WaypointMissionFinishedAction.AUTO_LAND;
            } else if (checkedId == R.id.finishToFirst){
                mFinishedAction = WaypointMissionFinishedAction.GO_FIRST_WAYPOINT;
            }
        }
    });

    heading_RG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {

        @Override
        public void onCheckedChanged(RadioGroup group, int checkedId) {
            Log.d(TAG, "Select heading");

            if (checkedId == R.id.headingNext) {
                mHeadingMode = WaypointMissionHeadingMode.AUTO;
            } else if (checkedId == R.id.headingInitDirec) {
                mHeadingMode = WaypointMissionHeadingMode.USING_INITIAL_DIRECTION;
            } else if (checkedId == R.id.headingRC) {
                mHeadingMode = WaypointMissionHeadingMode.CONTROL_BY_REMOTE_CONTROLLER;
            } else if (checkedId == R.id.headingWP) {
                mHeadingMode = WaypointMissionHeadingMode.USING_WAYPOINT_HEADING;
            }
        }
    });

    new AlertDialog.Builder(this)
            .setTitle("")
            .setView(wayPointSettings)
            .setPositiveButton("Finish",new DialogInterface.OnClickListener(){
                public void onClick(DialogInterface dialog, int id) {

                    String altitudeString = wpAltitude_TV.getText().toString();
                    altitude = Integer.parseInt(nulltoIntegerDefault(altitudeString));
                    Log.e(TAG,"altitude "+altitude);
                    Log.e(TAG,"speed "+mSpeed);
                    Log.e(TAG, "mFinishedAction "+mFinishedAction);
                    Log.e(TAG, "mHeadingMode "+mHeadingMode);
                    configWayPointMission();
                }

            })
            .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    dialog.cancel();
                }

            })
            .create()
            .show();
}

String nulltoIntegerDefault(String value){
    if(!isIntValue(value)) value="0";
    return value;
}

boolean isIntValue(String val)
{
    try {
        val=val.replace(" ","");
        Integer.parseInt(val);
    } catch (Exception e) {return false;}
    return true;
}

在这里,我们实现了 "RadioGroup" 类的 setOnCheckedChangeListener() 方法,并根据用户选择的项,将不同的值传递给 mSpeed, mFinishedActionmHeadingMode 变量。

对于 DJIWaypointMission 的完成动作,我们在这里提供了几个枚举值:

  • AUTO_LAND

    飞机将在最后一个航路点自动降落。

  • CONTINUE_UNTIL_END

    如果用户在执行任务时试图沿飞行路径拉回飞机,飞机将朝之前的航点移动并将继续这样做,直到没有更多的航点返回或用户已停止尝试将飞机移回。

  • GO_FIRST_WAYPOINT

    飞机将返回其第一个航点并悬停在原位。

  • GO_HOME

    任务完成后,飞机将返回home点。

  • NO_ACTION

    完成任务后不会采取进一步行动。

对于 DJIWaypointMission 的机头模式,我们在这里提供这些枚举值:

  • AUTO

    飞机的机头始终是飞行方向。

  • CONTROL_BY_REMOTE_CONTROLLER

    飞机的机头将由遥控器控制。

  • TOWARD_POINT_OF_INTEREST

    飞机的机头总是朝向兴趣点。

  • USING_INITIAL_DIRECTION

    飞机的机头将设置为初始起飞航向。

  • USING_WAYPOINT_HEADING

    在航点之间移动时,飞机的机头将设置为前一航点的机头方向。

现在,让我们继续实现 getWaypointMissionOperator()configWayPointMission() 方法,如下所示:

public WaypointMissionOperator getWaypointMissionOperator() {
    if (instance == null) {
        instance = DJISDKManager.getInstance().getMissionControl().getWaypointMissionOperator();
    }
    return instance;
}

private void configWayPointMission(){

    if (waypointMissionBuilder == null){

        waypointMissionBuilder = new WaypointMission.Builder().finishedAction(mFinishedAction)
                                                              .headingMode(mHeadingMode)
                                                              .autoFlightSpeed(mSpeed)
                                                              .maxFlightSpeed(mSpeed)
                                                              .flightPathMode(WaypointMissionFlightPathMode.NORMAL);

    }else
    {
        waypointMissionBuilder.finishedAction(mFinishedAction)
                .headingMode(mHeadingMode)
                .autoFlightSpeed(mSpeed)
                .maxFlightSpeed(mSpeed)
                .flightPathMode(WaypointMissionFlightPathMode.NORMAL);

    }

    if (waypointMissionBuilder.getWaypointList().size() > 0){

        for (int i=0; i< waypointMissionBuilder.getWaypointList().size(); i++){
            waypointMissionBuilder.getWaypointList().get(i).altitude = altitude;
        }

        setResultToToast("Set Waypoint attitude successfully");
    }

    DJIError error = getWaypointMissionOperator().loadMission(waypointMissionBuilder.build());
    if (error == null) {
        setResultToToast("loadWaypoint succeeded");
    } else {
        setResultToToast("loadWaypoint failed " + error.getDescription());
    }

}

在上面的代码中,我们首先在 getWaypointMissionOperator() 方法中获取 WaypointMissionOperator 实例,然后在 configWayPointMission() 方法中,我们检查 waypointMissionBuilder 是否为null并在 WaypointMission.Builder 中设置它的 finishedAction, headingMode, autoFlightSpeed, maxFlightSpeedflightPathMode 变量 。然后我们在 waypointMissionBuilder 的 waypointsList 中使用for循环在设置每个 DJIWaypoint 的高度。接下来,我们调用 WaypointMissionOperator 的 loadMission() 方法并将 waypointMissionBuilder.build() 作为参数传递,将航点任务加载到operator。

上传 Waypoint 任务

现在,让我们创建以下方法来设置 WaypointMissionOperatorListener

@Override
protected void onCreate(Bundle savedInstanceState) {
   ...
   addListener();
   
}

@Override
protected void onDestroy(){
    ...
    removeListener();
}

//Add Listener for WaypointMissionOperator
private void addListener() {
    if (getWaypointMissionOperator() != null) {
        getWaypointMissionOperator().addListener(eventNotificationListener);
    }
}

private void removeListener() {
    if (getWaypointMissionOperator() != null) {
        getWaypointMissionOperator().removeListener(eventNotificationListener);
    }
}

private WaypointMissionOperatorListener eventNotificationListener = new WaypointMissionOperatorListener() {
    @Override
    public void onDownloadUpdate(WaypointMissionDownloadEvent downloadEvent) {

    }

    @Override
    public void onUploadUpdate(WaypointMissionUploadEvent uploadEvent) {

    }

    @Override
    public void onExecutionUpdate(WaypointMissionExecutionEvent executionEvent) {

    }

    @Override
    public void onExecutionStart() {

    }

    @Override
    public void onExecutionFinish(@Nullable final DJIError error) {
        setResultToToast("Execution finished: " + (error == null ? "Success!" : error.getDescription()));
    }
};

在上面的代码中,我们调用了 WaypointMissionOperator 的 addListener()removeListener() 方法来添加和删除 WaypointMissionOperatorListener ,然后在 onCreate() 方法的底部调用了addListener() 方法,并在 onDestroy() 方法中调用了 removeListener() 方法。

接下来,初始化 WaypointMissionOperatorListener 实例并实现它的 onExecutionFinish() 方法以显示消息,以在任务执行完成时通知用户。

此外,让我们设置 WaypointMission.Builder 的waypointList,当用户在地图上点击时, 在onMapClick() 方法中添加一个航点,并实现 uploadWayPointMission() 方法,以便上传任务给操作者,如下所示:

@Override
public void onMapClick(LatLng point) {
    if (isAdd == true){
        markWaypoint(point);
        Waypoint mWaypoint = new Waypoint(point.latitude, point.longitude, altitude);
        //Add Waypoints to Waypoint arraylist;
        if (waypointMissionBuilder != null) {
            waypointList.add(mWaypoint);
            waypointMissionBuilder.waypointList(waypointList).waypointCount(waypointList.size());
        }else
        {
            waypointMissionBuilder = new WaypointMission.Builder();
            waypointList.add(mWaypoint);
            waypointMissionBuilder.waypointList(waypointList).waypointCount(waypointList.size());
        }
    }else{
        setResultToToast("Cannot Add Waypoint");
    }
}

private void uploadWayPointMission(){

    getWaypointMissionOperator().uploadMission(new CommonCallbacks.CompletionCallback() {
        @Override
        public void onResult(DJIError error) {
            if (error == null) {
                setResultToToast("Mission upload successfully!");
            } else {
                setResultToToast("Mission upload failed, error: " + error.getDescription() + " retrying...");
                getWaypointMissionOperator().retryUploadMission(null);
            }
        }
    });

}

最后,让我们在onClick()方法中添加 R.id.upload 实例 :

Lastly, let's add the R.id.upload case checking in the onClick() method:

case R.id.upload:{
    uploadWayPointMission();
    break;
}

启动和停止任务

一旦任务完成上传,我们可以调用 WaypointMissionOperator 的 startMission()stopMission() 方法来实现启动和停止任务的功能,如下图所示:

private void startWaypointMission(){

    getWaypointMissionOperator().startMission(new CommonCallbacks.CompletionCallback() {
        @Override
        public void onResult(DJIError error) {
            setResultToToast("Mission Start: " + (error == null ? "Successfully" : error.getDescription()));
        }
    });

}

private void stopWaypointMission(){

    getWaypointMissionOperator().stopMission(new CommonCallbacks.CompletionCallback() {
        @Override
        public void onResult(DJIError error) {
            setResultToToast("Mission Stop: " + (error == null ? "Successfully" : error.getDescription()));
        }
    });

}

最后,让我们改进 onClick() 方法以改进 clear 按钮操作,并实现 startstop 按钮操作:

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.locate:{
            updateDroneLocation();
            cameraUpdate(); // Locate the drone's place
            break;
        }
        case R.id.add:{
            enableDisableAdd();
            break;
        }
        case R.id.clear: {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    aMap.clear();
                }

            });
            waypointList.clear();
            waypointMissionBuilder.waypointList(waypointList);
            updateDroneLocation();
            break;
        }
        case R.id.config:{
            showSettingDialog();
            break;
        }
        case R.id.upload:{
            uploadWayPointMission();
            break;
        }
        case R.id.start:{
            startWaypointMission();
            break;
        }
        case R.id.stop:{
            stopWaypointMission();
            break;
        }
        default:
            break;
    }
}

使用DJI Assistant 2模拟器测试Waypoint任务

在本教程中你已经走了很长的路,现在是时候测试整个应用程序了。

Important: 确保飞机的电池电量超过10%,否则航点任务可能会失败!

构建并运行项目以将应用程序安装到Android设备中。之后,请通过Micro USB线将飞机连接到运行DJI Assistant 2 Simulator的PC或Mac。然后,按顺序打开遥控器和飞机的电源。

接下来,按下DJI Assistant 2中的 Simulator 按钮,随意输入当前位置的纬度和经度数据到模拟器中。

simulatorPreview

然后使用USB线将Android设备连接到遥控器并运行应用程序。在PC或Mac上返回DJI Assistant 2 Simulator,然后按 Start Simulating 按钮。在您的应用程序中,地图上会出现一个小红色飞机,如果您按下 LOCATE 按钮,地图将放大您所在的区域并使飞机居中:

locateAircraft

接下来,按 Add 按钮,然后在地图上点击你想要添加的航点,如下所示:

addWaypointsAni

一旦按下 CONFIG 按钮,将会出现 Waypoint Configuration 对话框。根据需要修改设置,然后按 Finish 按钮。然后按 UPLOAD 按钮上传任务。

如果上传任务成功,请按 START 按钮开始执行航点任务。

prepareMission

现在您应该看到飞机朝着你之前在地图上设置的航点移动,如下所示:

startMission

同时,你可以看到Mavic Pro起飞并开始在 DJI Assistant 2 Simulator 中飞行。

flyingInSimulator

当航点任务结束时,一个 "Execution finished: Success!" 的消息将出现,Mavic Pro将开始返航!

此外,遥控器将开始发出哔哔声。现在让我们来看看 DJI Assistant 2 Simulator:

landing

Mavic Pro最终会返回,降落,遥控器发出的哔哔声将停止。该应用程序将恢复正常状态。如果按 CLEAR 按钮,你之前设置的所有航点都会被清除。在执行任务期间,如果您想要停止 DJIWaypoint 任务,可以按 STOP 按钮。

摘要

在本教程中,您已经学习如何设置和使用 DJI Assistant 2 Simulator 来测试您的航点任务应用程序,将您的飞机固件升级到开发人员版本,使用 DJI Mobile SDK 创建简单的地图视图,修改地图视图注释,使用来自 DJI Assistant 2 Simulator 的 GPS 数据在地图视图上显示飞机。接下来,您学习了如何使用 WaypointMission.Builder 配置航点任务设置,如何在 WaypointMission.Builder 中创建和设置 waypointList 。此外,您还学习了如何使用 WaypointMissionOperator 去 upload, startstop 任务。

恭喜!现在您已经完成了演示项目,您可以在您学到的东西的基础上开始构建您自己的航点任务应用程序。您可以改进添加航点的方法(例如在地图上绘制线条并自动生成航点),使用航点的属性(例如机头等),以及添加更多功能。为了制作一个很酷的航点任务应用程序,你还有很长的路要走。祝你好运,希望你喜欢这个教程!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容