Java 生成与解析大疆无人机 KMZ 航线文件

2024/11/19 dji

完整的 Demo 源码可以在该 GitHub 仓库 (opens new window) 中获取,源码实现了 KMZ 文件生成和解析功能,生成的 KMZ 文件可以直接导入到 Pilot2 或机场中使用。如果该项目对你有帮助的话,欢迎点个 star 支持!

# 实现效果

  • KMZ 文件结构。 kmz-01.gif

  • template.kml 文件内容。 kmz-02.png

# 航线文件介绍

KMZ 航线文件本质上是一个 ZIP 格式的压缩文件,一个标准的 KMZ 文件结构如下。

—— wpmz
  |—— res  
  |—— template.kml
  |—— waylines.wpml 
  • res:资源文件夹,用来存储航线所需的辅助资源文件(如图片、数据等)。
  • template.kml:模板文件,定义了航线的业务属性,便于用户快速编辑和调整。
  • waylines.wpml:执行文件,包含了无人机执行航线任务时的具体执行细节。

template.kml、waylines.wpml 和 res 资源文件夹都是航线文件格式标准的一部分,我们可以根据使用场景灵活调整文件结构。如果需要将 KMZ 航线文件导入到 Pilot2 中使用,可以只生成 template.kml 文件,Pilot2 可以根据 template 文件生成 waylines 文件。除特殊使用场景外(精准复拍前,准备参考目标照片等),可以不生成 res 文件夹。

# 实现思路

大疆开发者平台上云 API 文档中详细介绍了航线文件格式标准 (opens new window)以及文件中具体的元素、名称以及取值和释义。本文主要介绍如何生成和解析航点飞行模板类型的 KMZ 航线文件,具体的实现思路如下:

  • KMZ 文件生成:首先生成一个 wpmz 文件夹,然后在 wpmz 文件夹下,生成 template.kml 文件和 waylines.wpml 文件,然后将 wpmz 文件夹压缩成 .zip 格式,最后修改 .zip 文件后缀为 .kmz。
  • KMZ 文件解析:上传 KMZ 文件,对 KMZ 文件解压缩,获取到 template.kml 和 waylines.wpml 文件,然后分别解析 template.kml 文件和 waylines.wpml 文件。

template.kml 和 waylines.wpml 文件都是 XML 格式,而且文件中包含多个元素标签,每个元素标签又有不同的属性,航线文件内容比较灵活,因此可以使用 Java 类库 XStream 来方便的将 Java 对象和 XML 文档相互转换,航线文件中每个元素标签可以对应一个 Java Bean,通过操作这些 Java Bean 来生成和解析 KMZ 航线文件。

# 具体实现

由于代码量较大,这里主要贴一下关键代码。

# 引入 maven 依赖

<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.20</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.26.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10

# 使用 XStream 注解的 Java 类。

import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
import lombok.Data;

@Data
@XStreamAlias("kml")
public class KmlInfo {

    @XStreamAsAttribute
    @XStreamAlias("xmlns")
    private String xmlns = "http://www.opengis.net/kml/2.2";

    @XStreamAsAttribute
    @XStreamAlias("xmlns:wpml")
    private String wpml = "http://www.dji.com/wpmz/1.0.4";

    @XStreamAlias("Document")
    private KmlDocument document;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 生成 KMZ 文件。

/**
 * 生成kmz文件
 */
public static String buildKmz(String fileName, KmlParams kmlParams) {
    KmlInfo kmlInfo = buildKml(kmlParams);
    KmlInfo wpmlInfo = buildWpml(kmlParams);
    return buildKmz(fileName, kmlInfo, wpmlInfo);
}

public static String buildKmz(String fileName, KmlInfo kmlInfo, KmlInfo wpmlInfo) {
    XStream xStream = new XStream(new DomDriver());
    xStream.processAnnotations(KmlInfo.class);
    xStream.addImplicitCollection(KmlActionGroup.class, "action");

    String kml = XML_HEADER + xStream.toXML(kmlInfo);
    String wpml = XML_HEADER + xStream.toXML(wpmlInfo);

    try (FileOutputStream fileOutputStream = new FileOutputStream(LOCAL_KMZ_FILE_PATH + fileName + ".kmz");
         ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream)) {
        zipOutputStream.setLevel(0); // 0 表示不压缩,存储方式

        // 创建 wpmz 目录中的 template.kml 文件条目
        ZipEntry kmlEntry = new ZipEntry("wpmz/template.kml");
        zipOutputStream.putNextEntry(kmlEntry);
        // 将内容写入 ZIP 条目
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(kml.getBytes(StandardCharsets.UTF_8))) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) >= 0) {
                zipOutputStream.write(buffer, 0, length);
            }
        }
        zipOutputStream.closeEntry(); // 关闭条目

        // 创建 wpmz 目录中的 waylines.wpml 文件条目
        ZipEntry wpmlEntry = new ZipEntry("wpmz/waylines.wpml");
        zipOutputStream.putNextEntry(wpmlEntry);
        // 将内容写入 ZIP 条目
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(wpml.getBytes(StandardCharsets.UTF_8))) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) >= 0) {
                zipOutputStream.write(buffer, 0, length);
            }
        }
        zipOutputStream.closeEntry(); // 关闭条目

    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    return LOCAL_KMZ_FILE_PATH + fileName + ".kmz";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 解析 KMZ 文件。

/**
 * 解析kmz文件
 * @param file
 */
@GetMapping("/parseKmz")
public void test(@RequestParam("file") MultipartFile file) {
    try (ArchiveInputStream archiveInputStream = new ZipArchiveInputStream(file.getInputStream())) {
        ArchiveEntry entry;
        while (!Objects.isNull(entry = archiveInputStream.getNextEntry())) {
            String name = entry.getName();
            if (name.toLowerCase().endsWith(".kml")) {
                KmlInfo kmlInfo = RouteFileUtils.parseKml(archiveInputStream);
                System.out.println("kml = " + kmlInfo);
            } else if (name.toLowerCase().endsWith(".wpml")) {
                KmlInfo kmlInfo = RouteFileUtils.parseKml(archiveInputStream);
                System.out.println("wpml = " + kmlInfo);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * kml文件解析
 *
 * @param inputStream
 * @return
 */
public static KmlInfo parseKml(InputStream inputStream) {
    XStream xStream = new XStream();
    xStream.allowTypes(new Class[]{KmlInfo.class, KmlAction.class, KmlWayLineCoordinateSysParam.class, KmlPoint.class});
    xStream.alias("kml", KmlInfo.class);
    xStream.processAnnotations(KmlInfo.class);
    xStream.autodetectAnnotations(true);
    xStream.addImplicitCollection(KmlActionGroup.class, "action");
    return (KmlInfo) xStream.fromXML(inputStream);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38