大疆V5文件KMZ离线生成

在开发大疆MSDKV5版本的过程中我遇到个比较egg疼的问题,大疆航线只能通过上传kmz航线文件来执行航线任务。那么以前V4版本写的KML是不能直接使用的,问题来了,他们并没有提供相关的转换接口文档一键转换,号称可以通过大疆司空或者把kml导入pilot2里自动转换,我们的需求大多数都是一套执行的,估计没有几家公司会把自己的航线处理后,再人工导入到pilot2里再人工拿出来手动导入自己的项目里上传执行,都这样玩了直接pilot2不香吗,所以我就自己写了转换。

在线版请参考大疆V5文件KMZ在线生成

这个是离线版本

可定制度比较高,需要自己手动拼接自己想要的参数,相对复杂:

流程是从服务端或者本地生成好的KML航线,把文件提取出来后,使用Dom解析提取出对应的坐标点经、纬、高度,以及对应的航点动作、拍照、悬停、录像、兴趣点等,再Dom合成对应的kmz文件,对应的结构文件如下

编辑MSDKV5航线KMZ文件的时候,结构必须按照上面的结构不能错不能省,里面的res可以不要,template.kml和waylines.wpml两个文件必须得有,最早我是把两个文件的航线执行都写出来了,后来发现大疆文档上说:若是pilot2执行KMZ则只需要编写template.kml,若是通过MSDK执行KMZ可以只把航点和航点动过写在waylines.wpml里,再写个无航点和无动作的template.kml文件打包。

官方提供的template.kml如下:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:wpml="http://www.dji.com/wpmz/1.0.2">
<Document></Document>
</kml>

然后是template.kml的编写方式,我这里提供相关的编写方式,代码简单看看就好,下面会给文档的

private int createFolder() {
        if (docElement != null) {
            Element folderElement = docElement.addElement("Folder");

            //模板ID,范围:[0, 65535]
            folderElement.addElement("wpml:templateId").setText(String.valueOf(flightMission.getMissionId()));

            //航线ID 在一条航线中该ID唯一  [0, 65535]
            folderElement.addElement("wpml:waylineId").setText(String.valueOf(flightMission.getMissionId()));

            //全局航线飞行速度
            folderElement.addElement("wpml:autoFlightSpeed").setText(String.valueOf(flightMission.getSpeed()));

            //执行高度模式 该元素仅在waylines.wpml中使用
//            folderElement.addElement("wpml:executeHeightMode").setText("WGS84");
            folderElement.addElement("wpml:executeHeightMode").setText("relativeToStartPoint");

            if (pointList != null && pointList.size() > 0) {
                //航点信息(包括航点经纬度和高度等)
                for (int i = 0; i < pointList.size(); i++) {
                    addPointToPlacemark(folderElement, pointList.get(i), pointList.size(), i);
                }
            }

            return 0;
        } else {
            return -1;
        }
    }
private boolean addPointToPlacemark(Element parentElement, TaskKMLBean missionPoint, int size, int index) {
        if (missionPoint == null || missionPoint.getLatitude() == 0 || missionPoint.getLongitude() == 0) {
        return false;
        } else {
        Element placemarkElement = parentElement.addElement("Placemark");
        Element point = placemarkElement.addElement("Point");
        //航点经纬度<经度,纬度>
        point.addElement("coordinates")
        .setText(missionPoint.getLongitude() + "," + missionPoint.getLatitude());
        //航点序号
        placemarkElement.addElement("wpml:index")
        .setText(String.valueOf(index));
        //航点执行高度
        placemarkElement.addElement("wpml:executeHeight")
        .setText(String.valueOf(missionPoint.getAltitude()));
        //航点飞行速度,当前航点飞向下一个航点的速度
        placemarkElement.addElement("wpml:waypointSpeed")
        .setText(String.valueOf(missionPoint.getSpeed()));
        //偏航角模式参数
        Element waypointHeadingParamElememt = placemarkElement.addElement("wpml:waypointHeadingParam");
        waypointHeadingParamElememt.addElement("wpml:waypointHeadingMode").setText(getHeadMode(flightMission.getHeadingMode()));
        if (flightMission.getHeadingMode() == 3) {//表明为兴趣点
        waypointHeadingParamElememt.addElement("wpml:waypointPoiPoint").setText(
        missionPoint.getPointAction().getLookatlatitude() + ","
        + missionPoint.getPointAction().getLookatlongitude() + ","
        + missionPoint.getPointAction().getLookataltitude()
        );
        }
        //飞行器偏航角度
        waypointHeadingParamElememt.addElement("wpml:waypointHeadingAngle").setText("0");
        waypointHeadingParamElememt.addElement("wpml:waypointHeadingPathMode").setText("followBadArc");
        //航点类型(航点转弯模式
        Element waypointTurnParamElement = placemarkElement.addElement("wpml:waypointTurnParam");
        waypointTurnParamElement.addElement("wpml:waypointTurnMode").setText("toPointAndStopWithDiscontinuityCurvature");
        waypointTurnParamElement.addElement("wpml:waypointTurnDampingDist").setText("0");
        placemarkElement.addElement("wpml:useStraightLine").setText("1");
        //*注:该元素用于规划一系列初始动作,在航线开始前执行。航线中断恢复时,先执行初始动作,再执行航点动作
        Element actionGroupElement = placemarkElement.addElement("wpml:actionGroup");
        //动作组id
        //* 注:在一个kmz文件内该ID唯一。建议从0开始单调连续递增。[0, 65535]
        actionGroupElement.addElement("wpml:actionGroupId").setText(String.valueOf(index));
        //动作组开始生效的航点 [0, 65535]
        actionGroupElement.addElement("wpml:actionGroupStartIndex").setText(String.valueOf(index));
        actionGroupElement.addElement("wpml:actionGroupEndIndex").setText(String.valueOf(index));
        //动作执行模式 sequence:串行执行。即动作组内的动作依次按顺序执行。
        actionGroupElement.addElement("wpml:actionGroupMode").setText("sequence");
        //动作组触发器
        Element actionTriggerElement = actionGroupElement.addElement("wpml:actionTrigger");
        actionTriggerElement.addElement("wpml:actionTriggerType").setText("reachPoint");
        Element seconAactionElement = actionGroupElement.addElement("wpml:action");
        seconAactionElement.addElement("wpml:actionId").setText("0");
        seconAactionElement.addElement("wpml:actionActuatorFunc").setText("gimbalRotate");
        Element secondActionActuatorFuncParamElement = seconAactionElement.addElement("wpml" +
        ":actionActuatorFuncParam");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalHeadingYawBase").setText("aircraft");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalRotateMode").setText("absoluteAngle");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalPitchRotateEnable").setText("1");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalPitchRotateAngle").setText(String.valueOf(missionPoint.getPitch()));
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalRollRotateEnable").setText("0");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalRollRotateAngle").setText("0");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalYawRotateEnable").setText("0");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalYawRotateAngle").setText("0");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalRotateTimeEnable").setText("0");
        secondActionActuatorFuncParamElement.addElement("wpml:gimbalRotateTime").setText("10");
        secondActionActuatorFuncParamElement.addElement("wpml:payloadPositionIndex").setText("0");
        Element firstActionElement = actionGroupElement.addElement("wpml:action");
        firstActionElement.addElement("wpml:actionId").setText("1");
        String firstAction = getActionActuatorFunc("1");
        firstActionElement.addElement("wpml:actionActuatorFunc").setText(firstAction);
        Element firstActionActuatorFuncParamElement = firstActionElement.addElement("wpml:actionActuatorFuncParam");
        if (firstAction.equals("takePhoto")) {
        firstActionActuatorFuncParamElement.addElement("wpml:payloadPositionIndex").setText("0");
        firstActionActuatorFuncParamElement.addElement("wpml:fileSuffix").setText("point" + index);
        } else if (firstAction.equals("startRecord")) {
        firstActionActuatorFuncParamElement.addElement("wpml:payloadPositionIndex").setText("0");
        firstActionActuatorFuncParamElement.addElement("wpml:fileSuffix").setText("point" + index);
        firstActionActuatorFuncParamElement.addElement("wpml:payloadLensIndex").setText("wide,ir,narrow_band");
        firstActionActuatorFuncParamElement.addElement("wpml:useGlobalPayloadLensIndex").setText("0");
        } else if (firstAction.equals("stopRecord")) {
        firstActionActuatorFuncParamElement.addElement("wpml:payloadPositionIndex").setText("0");
        firstActionActuatorFuncParamElement.addElement("wpml:payloadLensIndex").setText("wide,ir,narrow_band");
        } else if (firstAction.equals("hover")) {
        firstActionActuatorFuncParamElement.addElement("wpml:hoverTime").setText("5");
        } else if (firstAction.equals("rotateYaw")) {
        firstActionActuatorFuncParamElement.addElement("wpml:aircraftHeading").setText("180");
        firstActionActuatorFuncParamElement.addElement("wpml:aircraftPathMode").setText("clockwise");
        }

        return true;
        }
        }

这里只是摘取了部分解析方法,更复杂的动作什么的,大家可以根据官网里的格式自己修改链接如下:https://developer.dji.com/doc/cloud-api-tutorial/cn/api-reference/dji-wpml/overview.html

然后再把文件压缩成指定的kmz文件

private void addFolderToZip(File folder, String parentFolder, ZipOutputStream zos) throws Exception {
        String baseFolder = new File(parentFolder).getName(); // 获取基础文件夹名称,例如 "107"
        for (File file : folder.listFiles()) {
            String relativePath = file.getAbsolutePath().substring(parentFolder.length() + 1); // 获取相对于基础文件夹的路径
            if (file.isDirectory()) {
                addFolderToZip(file, parentFolder + "/" + file.getName(), zos);
                continue;
            }
            zos.putNextEntry(new ZipEntry(baseFolder + "/" + relativePath)); // 使用相对路径创建 ZipEntry

            FileInputStream fis = new FileInputStream(file);
            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) > 0) {
                zos.write(buffer, 0, length);
            }
            zos.closeEntry();
            fis.close();
        }
    }

然后可以执行航线任务了,可以查看”大疆V5MSDK航线任务“这篇文档避坑。


已发布

分类

来自

标签:

评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注