简介:这份源码资源面向无人机应用开发者、GIS与遥感从业者以及具备一定Java基础的编程爱好者,聚焦大疆KMZ标准航线文件的解析与生成这一具体场景。KMZ基于KML压缩格式,广泛用于航点规划与航线设计,资源通过读取KMZ、提取航点坐标、高度、速度等航线信息,并支持按需生成新的KMZ文件,帮助开发者将航线处理能力快速集成进自有项目。压缩包共171个文件,以115个xml与52个java为主,另有yml、md等少量配置与说明文件,整体约97KB,代码结构清晰、注释详尽,涵盖控制器、航线工具类、坐标工具类及KML元素模型等模块。目前已有1103人学习下载,适合希望理解KML/KMZ结构、掌握XML解析与文件生成思路的读者参考借鉴。1. Java解析大疆KMZ航线文件:从黑匣子到可控数据流拿到一个.kmz文件,解压后里面躺着一个wpmz/template.kml和一个wpmz/waylines.wpml,很多人第一反应是“这不就是个压缩包加 XML 吗”。真动手写 Java 解析时才发现,大疆 KMZ 航线文件不是普通 KML,它有一套自己的命名空间、坐标系约定和动作编码规则,直接拿 DOM4J 硬怼很容易在wpml:waypoint节点上翻车。这个标题要解决的核心问题就一个:把大疆飞行规划软件导出的 KMZ 航线文件,用 Java 代码稳定地读出来、改参数、再写回去,让航线生成从“手工在 App 里点”变成“程序批量产出”。适合做测绘巡检、航线批量化、后端调度平台的开发者,也适合需要把航线数据接入自有系统的团队。下面按“先看懂结构、再解析、再生成、最后避坑”的顺序推一遍。2. KMZ航线文件结构拆解:wpml与kml到底谁说了算2.1 解压后两个核心文件的分工KMZ 本质是 ZIP,用unzip -l看一眼就清楚:unzip -l demo.kmz # Archive: demo.kmz # Length Name # ---------- ---- # 1234 wpmz/template.kml # 5678 wpmz/waylines.wpml # 890 wpmz/res/...template.kml描述的是“航线模板”——全局参数、飞行器型号、负载类型、航线整体高度模式等;waylines.wpml描述的是“实际执行航线”——每个航点的经纬度、高度、速度、机头朝向、云台俯仰、动作序列。两者通过wpml:templateId和wpml:waylineId关联。常见做法是:解析时以waylines.wpml为主,template.kml只取全局配置;生成时两个文件都要写,缺一个大疆 App 就认不出来。注意:不同版本导出的 KMZ 内部路径可能略有差异,但wpmz/目录和这两个文件名是稳定的,不要假设一定有res/目录。2.2 命名空间与坐标系:最容易读错的两个点waylines.wpml根节点通常长这样:<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:wpml="http://www.dji.com/wpmz/1.0.2">所有航线业务字段都带wpml:前缀。用 DOM 解析时如果只按标签名waypoint找,会漏掉带前缀的节点。正确做法是按命名空间 URI 匹配,而不是按前缀字符串匹配——前缀是可以变的,URI 才是身份。坐标系方面,wpml:coordinates里的经纬度是 WGS84 十进制度,顺序是经度,纬度,不是纬度,经度。这个顺序搞反,航线会飞到地球另一边,属于血泪经验级别的坑。2.3 一个航点节点里到底有什么<wpml:waypoint> <wpml:index>0</wpml:index> <wpml:executeHeight>100</wpml:executeHeight> <wpml:waypointSpeed>8</wpml:waypointSpeed> <wpml:waypointHeadingParam> <wpml:waypointHeadingMode>followWayline</wpml:waypointHeadingMode> <wpml:waypointHeadingAngle>0</wpml:waypointHeadingAngle> </wpml:waypointHeadingParam> <wpml:waypointPitchParam> <wpml:waypointPitchMode>fixed</wpml:waypointPitchMode> <wpml:waypointPitchAngle>-90</wpml:waypointPitchAngle> </wpml:waypointPitchParam> <wpml:actionGroup> <wpml:actionGroupId>1</wpml:actionGroupId> <wpml:actionGroupStartIndex>0</wpml:actionGroupStartIndex> <wpml:actionGroupEndIndex>0</wpml:actionGroupEndIndex> <wpml:action> <wpml:actionId>0</wpml:actionId> <wpml:actionActuatorFunc>gimbalRotate</wpml:actionActuatorFunc> <wpml:actionActuatorFuncParam> <wpml:gimbalRotateMode>absoluteAngle</wpml:gimbalRotateMode> <wpml:gimbalPitchRotateAngle>-90</wpml:gimbalPitchRotateAngle> </wpml:actionActuatorFuncParam> </wpml:action> </wpml:actionGroup> <wpml:useGlobalHeight>0</wpml:useGlobalHeight> <wpml:useGlobalSpeed>1</wpml:useGlobalSpeed> <Point> <coordinates>113.123456,23.123456</coordinates> </Point> </wpml:waypoint>这里有几个关键点:useGlobalHeight和useGlobalSpeed决定该航点是继承全局参数还是用局部值;actionGroup里的actionGroupStartIndex和EndIndex表示这组动作在哪些航点之间执行;actionActuatorFunc是动作类型枚举,常见的有gimbalRotate、takePhoto、rotateYaw、hover等。解析时要把这些枚举映射成 Java 枚举,否则后面生成时容易写错字符串。3. 用Java解析KMZ:从ZIP流到航点对象3.1 依赖选型:为什么用JDOM2而不是DOM4J解析带命名空间的 XML,JDOM2 的XPathFactory对命名空间支持更直观,而且 API 比原生 DOM 简洁。Maven 依赖:<dependency> <groupId>org.jdom</groupId> <artifactId>jdom2</artifactId> <version>2.0.6.1</version> </dependency>如果项目已经用了 DOM4J 也不用换,只是 XPath 里要显式注册命名空间前缀。选型理由:JDOM2 在“按命名空间取节点”这个高频操作上代码量最少,出错概率低。3.2 读取KMZ并定位wpml文件import org.jdom2.*; import org.jdom2.input.SAXBuilder; import java.io.*; import java.util.zip.*; public class KmzReader { // 从KMZ字节流中提取指定内部文件内容 public static byte[] extractEntry(byte[] kmzBytes, String entryName) throws IOException { try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(kmzBytes))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.getName().equals(entryName)) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[4096]; int len; while ((len = zis.read(buf)) != -1) { bos.write(buf, 0, len); } return bos.toByteArray(); } } } throw new FileNotFoundException("KMZ中未找到: " + entryName); } // 解析waylines.wpml为JDOM文档 public static Document parseWaylines(byte[] kmzBytes) throws Exception { byte[] wpmlBytes = extractEntry(kmzBytes, "wpmz/waylines.wpml"); SAXBuilder builder = new SAXBuilder(); return builder.build(new ByteArrayInputStream(wpmlBytes)); } }逻辑说明:extractEntry遍历 ZIP 条目,按名称精确匹配。参数entryName必须写全路径wpmz/waylines.wpml,不能只写文件名。parseWaylines把字节流交给 JDOM2 构建 Document。失败时优先看异常是FileNotFoundException(路径不对)还是JDOMException(XML 格式问题)。3.3 按命名空间提取航点列表import org.jdom2.*; import org.jdom2.filter.Filters; import org.jdom2.xpath.XPathExpression; import org.jdom2.xpath.XPathFactory; import java.util.*; public class WaypointParser { private static final Namespace WPML = Namespace.getNamespace("wpml", "http://www.dji.com/wpmz/1.0.2"); public static List<Waypoint> parse(Document doc) { XPathFactory xf = XPathFactory.instance(); // 按命名空间URI匹配,不依赖前缀字符串 XPathExpression<Element> expr = xf.compile( "//wpml:waypoint", Filters.element(), null, WPML); List<Element> nodes = expr.evaluate(doc); List<Waypoint> result = new ArrayList<>(); for (Element node : nodes) { Waypoint wp = new Waypoint(); wp.setIndex(Integer.parseInt( node.getChildText("index", WPML))); wp.setExecuteHeight(Double.parseDouble( node.getChildText("executeHeight", WPML))); wp.setSpeed(Double.parseDouble( node.getChildText("waypointSpeed", WPML))); // 坐标在Point/coordinates下,无wpml前缀 Element point = node.getChild("Point"); String coord = point.getChildText("coordinates"); String[] parts = coord.split(","); wp.setLng(Double.parseDouble(parts[0])); wp.setLat(Double.parseDouble(parts[1])); result.add(wp); } return result; } }逻辑说明:Namespace.getNamespace("wpml", uri)把前缀和 URI 绑定,XPath 里用wpml:引用。getChildText("index", WPML)第二个参数指定命名空间,漏掉就取不到值。Point和coordinates属于 KML 默认命名空间,不带wpml:前缀,所以用getChild("Point")不带命名空间参数。参数executeHeight单位是米,waypointSpeed单位是米每秒。3.4 动作组的解析与枚举映射public enum ActionType { GIMBAL_ROTATE("gimbalRotate"), TAKE_PHOTO("takePhoto"), ROTATE_YAW("rotateYaw"), HOVER("hover"), UNKNOWN(""); private final String code; ActionType(String code) { this.code = code; } public static ActionType fromCode(String code) { for (ActionType t : values()) { if (t.code.equals(code)) return t; } return UNKNOWN; } }解析actionGroup时,先取actionGroupStartIndex和EndIndex确定作用范围,再遍历action子节点,用fromCode把字符串转枚举。遇到UNKNOWN不要抛异常,记录日志跳过即可——大疆固件升级可能新增动作类型,硬抛会导致整个解析失败。4. 用Java生成KMZ航线:模板拼装与ZIP回写4.1 生成waylines.wpml的骨架生成比解析更容易出错,因为要保证命名空间声明、节点顺序、必填字段都正确。先建根节点:import org.jdom2.*; import org.jdom2.output.*; public class WpmlBuilder { private static final Namespace WPML = Namespace.getNamespace("wpml", "http://www.dji.com/wpmz/1.0.2"); private static final Namespace KML = Namespace.getNamespace("http://www.opengis.net/kml/2.2"); public static Document build(List<Waypoint> waypoints) { Element root = new Element("kml", KML); root.addNamespaceDeclaration(WPML); Element doc = new Element("Document", KML); root.addContent(doc); // 全局参数 Element missionConfig = new Element("missionConfig", WPML); missionConfig.addContent(new Element("flyToWaylineMode", WPML) .setText("safely")); missionConfig.addContent(new Element("finishAction", WPML) .setText("goHome")); missionConfig.addContent(new Element("exitOnRCLost", WPML) .setText("executeLostAction")); doc.addContent(missionConfig); // 航点 Element folder = new Element("Folder", KML); for (Waypoint wp : waypoints) { folder.addContent(buildWaypoint(wp)); } doc.addContent(folder); return new Document(root); } }逻辑说明:根节点必须同时声明 KML 默认命名空间和wpml命名空间。missionConfig里的flyToWaylineMode常见值有safely和pointToPoint,finishAction常见goHome、noAction、autoLand。这些字段缺失会导致 App 导入时报“文件格式错误”。4.2 单个航点节点的拼装private static Element buildWaypoint(Waypoint wp) { Element node = new Element("waypoint", WPML); node.addContent(new Element("index", WPML) .setText(String.valueOf(wp.getIndex()))); node.addContent(new Element("executeHeight", WPML) .setText(String.valueOf(wp.getExecuteHeight()))); node.addContent(new Element("waypointSpeed", WPML) .setText(String.valueOf(wp.getSpeed()))); Element heading = new Element("waypointHeadingParam", WPML); heading.addContent(new Element("waypointHeadingMode", WPML) .setText("followWayline")); heading.addContent(new Element("waypointHeadingAngle", WPML) .setText("0")); node.addContent(heading); Element pitch = new Element("waypointPitchParam", WPML); pitch.addContent(new Element("waypointPitchMode", WPML) .setText("fixed")); pitch.addContent(new Element("waypointPitchAngle", WPML) .setText("-90")); node.addContent(pitch); Element point = new Element("Point", KML); point.addContent(new Element("coordinates", KML) .setText(wp.getLng() + "," + wp.getLat())); node.addContent(point); node.addContent(new Element("useGlobalHeight", WPML).setText("0")); node.addContent(new Element("useGlobalSpeed", WPML).setText("1")); return node; }参数说明:waypointHeadingMode可选followWayline、manually、fixed;waypointPitchMode可选fixed、usePointSetting。useGlobalHeight=0表示用executeHeight局部值,useGlobalSpeed=1表示用全局速度。这两个开关和实际值要一致,否则 App 里显示的高度和代码里写的不一样。4.3 打包回KMZimport java.util.zip.*; import org.jdom2.output.XMLOutputter; import org.jdom2.output.Format; public class KmzWriter { public static byte[] write(Document waylines, Document template) throws IOException { XMLOutputter out = new XMLOutputter(Format.getPrettyFormat()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(bos)) { zos.putNextEntry(new ZipEntry("wpmz/template.kml")); zos.write(out.outputString(template).getBytes("UTF-8")); zos.closeEntry(); zos.putNextEntry(new ZipEntry("wpmz/waylines.wpml")); zos.write(out.outputString(waylines).getBytes("UTF-8")); zos.closeEntry(); } return bos.toByteArray(); } }逻辑说明:ZIP 条目名称必须和解析时一致。Format.getPrettyFormat()会加缩进和换行,不影响解析,但文件体积略大;如果追求最小体积可以用Format.getRawFormat()。编码固定 UTF-8,中文路径或备注不会乱码。5. 避坑与排查:KMZ解析生成中最容易翻车的5个点5.1 现象:App导入提示“文件格式错误”,但XML能正常打开原因:template.kml和waylines.wpml的templateId不匹配,或者missionConfig缺少必填字段。解决:用解析代码把两个文件的templateId都打出来对比;对照官方示例补齐flyToWaylineMode、finishAction、exitOnRCLost三个字段。5.2 现象:航点坐标全部偏移到另一个半球原因:经纬度顺序写反。coordinates里是经度,纬度,不是纬度,经度。解决:在Waypoint类里把字段命名成lng和lat,不要用x、y,从命名上杜绝混淆。5.3 现象:解析时getChildText返回 null原因:命名空间没传对。带wpml:前缀的节点必须传WPML命名空间对象,不带前缀的Point、coordinates不能传。解决:统一封装一个getWpmlChildText(Element, String)工具方法,内部固定传WPML,减少手误。5.4 现象:生成的文件在App里航点顺序错乱原因:index字段没有按 0、1、2 递增,或者Folder里航点顺序和index不一致。解决:生成前对waypoints按index排序,写入时严格按排序后顺序添加节点。5.5 现象:动作组不执行原因:actionGroupStartIndex和EndIndex指向的航点索引不存在,或者actionActuatorFunc拼写错误。解决:生成后写一个校验方法,遍历所有actionGroup,检查起止索引是否在航点索引范围内,动作类型是否在枚举表里。6. 进阶技巧:用校验器把航线错误拦在起飞前解析和生成跑通之后,真正省时间的做法是加一层校验。我一般会写一个WaylineValidator,在生成 KMZ 之前跑一遍,把能提前发现的问题全拦下来。核心校验项用表格列一下:校验项规则失败后果航点数量至少 2 个App 无法生成航线索引连续性0 到 N-1 无缺失航点顺序错乱高度范围20m 到 500m超出飞行器限制速度范围1m/s 到 15m/s被固件拒绝动作索引在航点索引范围内动作不执行命名空间根节点含 wpml URI解析失败校验器代码骨架:public class WaylineValidator { public static List<String> validate(List<Waypoint> wps) { List<String> errors = new ArrayList<>(); if (wps.size() < 2) { errors.add("航点数量不足2个"); } for (int i = 0; i < wps.size(); i++) { Waypoint wp = wps.get(i); if (wp.getIndex() != i) { errors.add("索引不连续: 期望" + i + " 实际" + wp.getIndex()); } if (wp.getExecuteHeight() < 20 || wp.getExecuteHeight() > 500) { errors.add("航点" + i + "高度超限: " + wp.getExecuteHeight()); } if (wp.getSpeed() < 1 || wp.getSpeed() > 15) { errors.add("航点" + i + "速度超限: " + wp.getSpeed()); } } return errors; } }这个校验器不依赖任何大疆 SDK,纯 Java 逻辑,跑一次不到一毫秒,但能省掉反复导入 App 试错的几分钟。另一个技巧是:把template.kml做成一个可配置的模板文件放在resources下,生成时只替换templateId和全局高度、速度,不要每次从零拼 XML。这样全局参数改动只需要改模板,不用动 Java 代码。我自己的习惯是:任何一次生成 KMZ 之后,先用解析代码把生成的文件读回来,对比航点数量和关键字段,确认“写进去的”和“读出来的”一致,再交给 App。这个回读校验帮我拦过好几次命名空间写错、坐标顺序写反的问题。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份源码资源面向无人机应用开发者、GIS与遥感从业者以及具备一定Java基础的编程爱好者,聚焦大疆KMZ标准航线文件的解析与生成这一具体场景。KMZ基于KML压缩格式,广泛用于航点规划与航线设计,资源通过读取KMZ、提取航点坐标、高度、速度等航线信息,并支持按需生成新的KMZ文件,帮助开发者将航线处理能力快速集成进自有项目。压缩包共171个文件,以115个xml与52个java为主,另有yml、md等少量配置与说明文件,整体约97KB,代码结构清晰、注释详尽,涵盖控制器、航线工具类、坐标工具类及KML元素模型等模块。目前已有1103人学习下载,适合希望理解KML/KMZ结构、掌握XML解析与文件生成思路的读者参考借鉴。1. Java解析大疆KMZ航线文件:从黑匣子到可控数据流拿到一个.kmz文件,解压后里面躺着一个wpmz/template.kml和一个wpmz/waylines.wpml,很多人第一反应是“这不就是个压缩包加 XML 吗”。真动手写 Java 解析时才发现,大疆 KMZ 航线文件不是普通 KML,它有一套自己的命名空间、坐标系约定和动作编码规则,直接拿 DOM4J 硬怼很容易在wpml:waypoint节点上翻车。这个标题要解决的核心问题就一个:把大疆飞行规划软件导出的 KMZ 航线文件,用 Java 代码稳定地读出来、改参数、再写回去,让航线生成从“手工在 App 里点”变成“程序批量产出”。适合做测绘巡检、航线批量化、后端调度平台的开发者,也适合需要把航线数据接入自有系统的团队。下面按“先看懂结构、再解析、再生成、最后避坑”的顺序推一遍。2. KMZ航线文件结构拆解:wpml与kml到底谁说了算2.1 解压后两个核心文件的分工KMZ 本质是 ZIP,用unzip -l看一眼就清楚:unzip -l demo.kmz # Archive: demo.kmz # Length Name # ---------- ---- # 1234 wpmz/template.kml # 5678 wpmz/waylines.wpml # 890 wpmz/res/...template.kml描述的是“航线模板”——全局参数、飞行器型号、负载类型、航线整体高度模式等;waylines.wpml描述的是“实际执行航线”——每个航点的经纬度、高度、速度、机头朝向、云台俯仰、动作序列。两者通过wpml:templateId和wpml:waylineId关联。常见做法是:解析时以waylines.wpml为主,template.kml只取全局配置;生成时两个文件都要写,缺一个大疆 App 就认不出来。注意:不同版本导出的 KMZ 内部路径可能略有差异,但wpmz/目录和这两个文件名是稳定的,不要假设一定有res/目录。2.2 命名空间与坐标系:最容易读错的两个点waylines.wpml根节点通常长这样:<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:wpml="http://www.dji.com/wpmz/1.0.2">所有航线业务字段都带wpml:前缀。用 DOM 解析时如果只按标签名waypoint找,会漏掉带前缀的节点。正确做法是按命名空间 URI 匹配,而不是按前缀字符串匹配——前缀是可以变的,URI 才是身份。坐标系方面,wpml:coordinates里的经纬度是 WGS84 十进制度,顺序是经度,纬度,不是纬度,经度。这个顺序搞反,航线会飞到地球另一边,属于血泪经验级别的坑。2.3 一个航点节点里到底有什么<wpml:waypoint> <wpml:index>0</wpml:index> <wpml:executeHeight>100</wpml:executeHeight> <wpml:waypointSpeed>8</wpml:waypointSpeed> <wpml:waypointHeadingParam> <wpml:waypointHeadingMode>followWayline</wpml:waypointHeadingMode> <wpml:waypointHeadingAngle>0</wpml:waypointHeadingAngle> </wpml:waypointHeadingParam> <wpml:waypointPitchParam> <wpml:waypointPitchMode>fixed</wpml:waypointPitchMode> <wpml:waypointPitchAngle>-90</wpml:waypointPitchAngle> </wpml:waypointPitchParam> <wpml:actionGroup> <wpml:actionGroupId>1</wpml:actionGroupId> <wpml:actionGroupStartIndex>0</wpml:actionGroupStartIndex> <wpml:actionGroupEndIndex>0</wpml:actionGroupEndIndex> <wpml:action> <wpml:actionId>0</wpml:actionId> <wpml:actionActuatorFunc>gimbalRotate</wpml:actionActuatorFunc> <wpml:actionActuatorFuncParam> <wpml:gimbalRotateMode>absoluteAngle</wpml:gimbalRotateMode> <wpml:gimbalPitchRotateAngle>-90</wpml:gimbalPitchRotateAngle> </wpml:actionActuatorFuncParam> </wpml:action> </wpml:actionGroup> <wpml:useGlobalHeight>0</wpml:useGlobalHeight> <wpml:useGlobalSpeed>1</wpml:useGlobalSpeed> <Point> <coordinates>113.123456,23.123456</coordinates> </Point> </wpml:waypoint>这里有几个关键点:useGlobalHeight和useGlobalSpeed决定该航点是继承全局参数还是用局部值;actionGroup里的actionGroupStartIndex和EndIndex表示这组动作在哪些航点之间执行;actionActuatorFunc是动作类型枚举,常见的有gimbalRotate、takePhoto、rotateYaw、hover等。解析时要把这些枚举映射成 Java 枚举,否则后面生成时容易写错字符串。3. 用Java解析KMZ:从ZIP流到航点对象3.1 依赖选型:为什么用JDOM2而不是DOM4J解析带命名空间的 XML,JDOM2 的XPathFactory对命名空间支持更直观,而且 API 比原生 DOM 简洁。Maven 依赖:<dependency> <groupId>org.jdom</groupId> <artifactId>jdom2</artifactId> <version>2.0.6.1</version> </dependency>如果项目已经用了 DOM4J 也不用换,只是 XPath 里要显式注册命名空间前缀。选型理由:JDOM2 在“按命名空间取节点”这个高频操作上代码量最少,出错概率低。3.2 读取KMZ并定位wpml文件import org.jdom2.*; import org.jdom2.input.SAXBuilder; import java.io.*; import java.util.zip.*; public class KmzReader { // 从KMZ字节流中提取指定内部文件内容 public static byte[] extractEntry(byte[] kmzBytes, String entryName) throws IOException { try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(kmzBytes))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.getName().equals(entryName)) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[4096]; int len; while ((len = zis.read(buf)) != -1) { bos.write(buf, 0, len); } return bos.toByteArray(); } } } throw new FileNotFoundException("KMZ中未找到: " + entryName); } // 解析waylines.wpml为JDOM文档 public static Document parseWaylines(byte[] kmzBytes) throws Exception { byte[] wpmlBytes = extractEntry(kmzBytes, "wpmz/waylines.wpml"); SAXBuilder builder = new SAXBuilder(); return builder.build(new ByteArrayInputStream(wpmlBytes)); } }逻辑说明:extractEntry遍历 ZIP 条目,按名称精确匹配。参数entryName必须写全路径wpmz/waylines.wpml,不能只写文件名。parseWaylines把字节流交给 JDOM2 构建 Document。失败时优先看异常是FileNotFoundException(路径不对)还是JDOMException(XML 格式问题)。3.3 按命名空间提取航点列表import org.jdom2.*; import org.jdom2.filter.Filters; import org.jdom2.xpath.XPathExpression; import org.jdom2.xpath.XPathFactory; import java.util.*; public class WaypointParser { private static final Namespace WPML = Namespace.getNamespace("wpml", "http://www.dji.com/wpmz/1.0.2"); public static List<Waypoint> parse(Document doc) { XPathFactory xf = XPathFactory.instance(); // 按命名空间URI匹配,不依赖前缀字符串 XPathExpression<Element> expr = xf.compile( "//wpml:waypoint", Filters.element(), null, WPML); List<Element> nodes = expr.evaluate(doc); List<Waypoint> result = new ArrayList<>(); for (Element node : nodes) { Waypoint wp = new Waypoint(); wp.setIndex(Integer.parseInt( node.getChildText("index", WPML))); wp.setExecuteHeight(Double.parseDouble( node.getChildText("executeHeight", WPML))); wp.setSpeed(Double.parseDouble( node.getChildText("waypointSpeed", WPML))); // 坐标在Point/coordinates下,无wpml前缀 Element point = node.getChild("Point"); String coord = point.getChildText("coordinates"); String[] parts = coord.split(","); wp.setLng(Double.parseDouble(parts[0])); wp.setLat(Double.parseDouble(parts[1])); result.add(wp); } return result; } }逻辑说明:Namespace.getNamespace("wpml", uri)把前缀和 URI 绑定,XPath 里用wpml:引用。getChildText("index", WPML)第二个参数指定命名空间,漏掉就取不到值。Point和coordinates属于 KML 默认命名空间,不带wpml:前缀,所以用getChild("Point")不带命名空间参数。参数executeHeight单位是米,waypointSpeed单位是米每秒。3.4 动作组的解析与枚举映射public enum ActionType { GIMBAL_ROTATE("gimbalRotate"), TAKE_PHOTO("takePhoto"), ROTATE_YAW("rotateYaw"), HOVER("hover"), UNKNOWN(""); private final String code; ActionType(String code) { this.code = code; } public static ActionType fromCode(String code) { for (ActionType t : values()) { if (t.code.equals(code)) return t; } return UNKNOWN; } }解析actionGroup时,先取actionGroupStartIndex和EndIndex确定作用范围,再遍历action子节点,用fromCode把字符串转枚举。遇到UNKNOWN不要抛异常,记录日志跳过即可——大疆固件升级可能新增动作类型,硬抛会导致整个解析失败。4. 用Java生成KMZ航线:模板拼装与ZIP回写4.1 生成waylines.wpml的骨架生成比解析更容易出错,因为要保证命名空间声明、节点顺序、必填字段都正确。先建根节点:import org.jdom2.*; import org.jdom2.output.*; public class WpmlBuilder { private static final Namespace WPML = Namespace.getNamespace("wpml", "http://www.dji.com/wpmz/1.0.2"); private static final Namespace KML = Namespace.getNamespace("http://www.opengis.net/kml/2.2"); public static Document build(List<Waypoint> waypoints) { Element root = new Element("kml", KML); root.addNamespaceDeclaration(WPML); Element doc = new Element("Document", KML); root.addContent(doc); // 全局参数 Element missionConfig = new Element("missionConfig", WPML); missionConfig.addContent(new Element("flyToWaylineMode", WPML) .setText("safely")); missionConfig.addContent(new Element("finishAction", WPML) .setText("goHome")); missionConfig.addContent(new Element("exitOnRCLost", WPML) .setText("executeLostAction")); doc.addContent(missionConfig); // 航点 Element folder = new Element("Folder", KML); for (Waypoint wp : waypoints) { folder.addContent(buildWaypoint(wp)); } doc.addContent(folder); return new Document(root); } }逻辑说明:根节点必须同时声明 KML 默认命名空间和wpml命名空间。missionConfig里的flyToWaylineMode常见值有safely和pointToPoint,finishAction常见goHome、noAction、autoLand。这些字段缺失会导致 App 导入时报“文件格式错误”。4.2 单个航点节点的拼装private static Element buildWaypoint(Waypoint wp) { Element node = new Element("waypoint", WPML); node.addContent(new Element("index", WPML) .setText(String.valueOf(wp.getIndex()))); node.addContent(new Element("executeHeight", WPML) .setText(String.valueOf(wp.getExecuteHeight()))); node.addContent(new Element("waypointSpeed", WPML) .setText(String.valueOf(wp.getSpeed()))); Element heading = new Element("waypointHeadingParam", WPML); heading.addContent(new Element("waypointHeadingMode", WPML) .setText("followWayline")); heading.addContent(new Element("waypointHeadingAngle", WPML) .setText("0")); node.addContent(heading); Element pitch = new Element("waypointPitchParam", WPML); pitch.addContent(new Element("waypointPitchMode", WPML) .setText("fixed")); pitch.addContent(new Element("waypointPitchAngle", WPML) .setText("-90")); node.addContent(pitch); Element point = new Element("Point", KML); point.addContent(new Element("coordinates", KML) .setText(wp.getLng() + "," + wp.getLat())); node.addContent(point); node.addContent(new Element("useGlobalHeight", WPML).setText("0")); node.addContent(new Element("useGlobalSpeed", WPML).setText("1")); return node; }参数说明:waypointHeadingMode可选followWayline、manually、fixed;waypointPitchMode可选fixed、usePointSetting。useGlobalHeight=0表示用executeHeight局部值,useGlobalSpeed=1表示用全局速度。这两个开关和实际值要一致,否则 App 里显示的高度和代码里写的不一样。4.3 打包回KMZimport java.util.zip.*; import org.jdom2.output.XMLOutputter; import org.jdom2.output.Format; public class KmzWriter { public static byte[] write(Document waylines, Document template) throws IOException { XMLOutputter out = new XMLOutputter(Format.getPrettyFormat()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(bos)) { zos.putNextEntry(new ZipEntry("wpmz/template.kml")); zos.write(out.outputString(template).getBytes("UTF-8")); zos.closeEntry(); zos.putNextEntry(new ZipEntry("wpmz/waylines.wpml")); zos.write(out.outputString(waylines).getBytes("UTF-8")); zos.closeEntry(); } return bos.toByteArray(); } }逻辑说明:ZIP 条目名称必须和解析时一致。Format.getPrettyFormat()会加缩进和换行,不影响解析,但文件体积略大;如果追求最小体积可以用Format.getRawFormat()。编码固定 UTF-8,中文路径或备注不会乱码。5. 避坑与排查:KMZ解析生成中最容易翻车的5个点5.1 现象:App导入提示“文件格式错误”,但XML能正常打开原因:template.kml和waylines.wpml的templateId不匹配,或者missionConfig缺少必填字段。解决:用解析代码把两个文件的templateId都打出来对比;对照官方示例补齐flyToWaylineMode、finishAction、exitOnRCLost三个字段。5.2 现象:航点坐标全部偏移到另一个半球原因:经纬度顺序写反。coordinates里是经度,纬度,不是纬度,经度。解决:在Waypoint类里把字段命名成lng和lat,不要用x、y,从命名上杜绝混淆。5.3 现象:解析时getChildText返回 null原因:命名空间没传对。带wpml:前缀的节点必须传WPML命名空间对象,不带前缀的Point、coordinates不能传。解决:统一封装一个getWpmlChildText(Element, String)工具方法,内部固定传WPML,减少手误。5.4 现象:生成的文件在App里航点顺序错乱原因:index字段没有按 0、1、2 递增,或者Folder里航点顺序和index不一致。解决:生成前对waypoints按index排序,写入时严格按排序后顺序添加节点。5.5 现象:动作组不执行原因:actionGroupStartIndex和EndIndex指向的航点索引不存在,或者actionActuatorFunc拼写错误。解决:生成后写一个校验方法,遍历所有actionGroup,检查起止索引是否在航点索引范围内,动作类型是否在枚举表里。6. 进阶技巧:用校验器把航线错误拦在起飞前解析和生成跑通之后,真正省时间的做法是加一层校验。我一般会写一个WaylineValidator,在生成 KMZ 之前跑一遍,把能提前发现的问题全拦下来。核心校验项用表格列一下:校验项规则失败后果航点数量至少 2 个App 无法生成航线索引连续性0 到 N-1 无缺失航点顺序错乱高度范围20m 到 500m超出飞行器限制速度范围1m/s 到 15m/s被固件拒绝动作索引在航点索引范围内动作不执行命名空间根节点含 wpml URI解析失败校验器代码骨架:public class WaylineValidator { public static List<String> validate(List<Waypoint> wps) { List<String> errors = new ArrayList<>(); if (wps.size() < 2) { errors.add("航点数量不足2个"); } for (int i = 0; i < wps.size(); i++) { Waypoint wp = wps.get(i); if (wp.getIndex() != i) { errors.add("索引不连续: 期望" + i + " 实际" + wp.getIndex()); } if (wp.getExecuteHeight() < 20 || wp.getExecuteHeight() > 500) { errors.add("航点" + i + "高度超限: " + wp.getExecuteHeight()); } if (wp.getSpeed() < 1 || wp.getSpeed() > 15) { errors.add("航点" + i + "速度超限: " + wp.getSpeed()); } } return errors; } }这个校验器不依赖任何大疆 SDK,纯 Java 逻辑,跑一次不到一毫秒,但能省掉反复导入 App 试错的几分钟。另一个技巧是:把template.kml做成一个可配置的模板文件放在resources下,生成时只替换templateId和全局高度、速度,不要每次从零拼 XML。这样全局参数改动只需要改模板,不用动 Java 代码。我自己的习惯是:任何一次生成 KMZ 之后,先用解析代码把生成的文件读回来,对比航点数量和关键字段,确认“写进去的”和“读出来的”一致,再交给 App。这个回读校验帮我拦过好几次命名空间写错、坐标顺序写反的问题。希望帮到你。本文还有配套的精品资源,点击获取