package com.xiaopeng.map.utils; import com.google.flatbuffers.FlatBufferBuilder; import com.xiaopeng.map.dto.AreaInput; import com.xiaopeng.map.dto.MapAreaDataDto; import com.xiaopeng.map.dto.OddAreaDto; import com.xiaopeng.map.dto.OddPointDto; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; /** * 基于坐标生成 XNGP 区域相关数据文件的工具类,包括: * - MapAreaData proto 源数据(map_area_data.proto) * - RTree flatbuffer 索引数据(map_area_rtree.fbs) */ public class XngpAreaRTreeFbsUtil { /** * 简单经纬度点结构。 */ public static class LonLat { private final double lon; private final double lat; public LonLat(double lon, double lat) { this.lon = lon; this.lat = lat; } public double getLon() { return lon; } public double getLat() { return lat; } } /** * 将形如 \"lon_lat,lon_lat,...\" 的字符串解析为坐标点列表。 * 例如:104.06249_30.58322,104.06244_30.58049, ... */ public static List parseGcl(String gcl) { if (gcl == null || gcl.isEmpty()) { return java.util.Collections.emptyList(); } String[] pairs = gcl.split(","); java.util.List result = new java.util.ArrayList<>(pairs.length); for (String pair : pairs) { if (pair == null || pair.isEmpty()) { continue; } String[] parts = pair.split("_"); if (parts.length != 2) { continue; } double lon = Double.parseDouble(parts[0]); double lat = Double.parseDouble(parts[1]); result.add(new LonLat(lon, lat)); } return result; } /** * 基于单个 GCL 字符串构造 MapAreaData,并写入指定文件。 * * @param gcl GCL 字符串:形如 "lon_lat,lon_lat,..." * @param oddId 区域 ID,对应 OddArea.odd_id * @param isBlacklist 是否黑名单区域,对应 OddArea.is_blacklist * @param version MapAreaData 的版本号,对应 MapAreaData.version * @param outFile 输出的 MapAreaData 二进制文件路径 * @return 写入是否成功 */ public static boolean buildMapAreaDataFromGcl(String gcl, int oddId, boolean isBlacklist, int version, String outFile) { List points = parseGcl(gcl); if (points == null || points.isEmpty()) { return false; } // 构造 OddAreaDto OddAreaDto oddArea = OddAreaDto.builder() .oddId(oddId) .isBlacklist(isBlacklist) .build(); for (LonLat p : points) { oddArea.getOddPoints().add(OddPointDto.builder() .latitude(p.getLat()) .longitude(p.getLon()) .build()); } // 构造 MapAreaDataDto MapAreaDataDto data = MapAreaDataDto.builder() .version(version) .oddAreas(java.util.Collections.singletonList(oddArea)) .build(); try (FileOutputStream fos = new FileOutputStream(outFile)) { MapAreaDataProtoWriter.writeTo(data, fos); } catch (IOException e) { return false; } return true; } /** * 从一组多边形坐标点生成 RTree fbs 文件(单区域版本)。 * 等价于 C++ 流程:单个 OddArea → PbRTreeCreater → PbRTreeToFlatBuffer。 * Node.type 由 R-Tree 算法自动赋值(1=LEAF),不再由调用方指定。 * * @param points 多边形顶点(经度、纬度),顺序与原始区域一致 * @param id 对应 proto 里的 odd_id * @param isBlacklist 是否黑名单区域 * @param outFile 输出的 fbs 二进制文件路径 * @return 生成是否成功 */ public static boolean buildRTreeFromPoints(List points, int id, boolean isBlacklist, String outFile) { java.util.List leafNodes = new java.util.ArrayList<>(); PbRTreeBuilder.RTreeNode leaf = buildLeafNode(points, id, isBlacklist); if (leaf == null) { return false; } leafNodes.add(leaf); return writeRTreeFbs(leafNodes, outFile); } /** * 从坐标点生成 RTree fbs 字节数组(内存版,供上传等场景使用)。 * * @param points 多边形顶点 * @param areaId 区域 odd_id * @param isBlacklist 是否黑名单 * @return fbs 二进制,失败返回 null */ public static byte[] buildRTreeBytesFromPoints(List points, int areaId, boolean isBlacklist) { java.util.List leafNodes = new java.util.ArrayList<>(); PbRTreeBuilder.RTreeNode leaf = buildLeafNode(points, areaId, isBlacklist); if (leaf == null) { return null; } leafNodes.add(leaf); return buildRTreeBytes(leafNodes); } /** * 从多个区域(AreaInput)生成 RTree fbs 字节数组。 * * @param areas 区域列表,每个 AreaInput 含 points、areaId、isBlacklist * @return fbs 二进制,失败返回 null */ public static byte[] buildRTreeBytesFromMultiAreas(List areas) { if (areas == null || areas.isEmpty()) { return null; } java.util.List leafNodes = new java.util.ArrayList<>(); for (AreaInput area : areas) { PbRTreeBuilder.RTreeNode leaf = buildLeafNode( area.getPoints(), area.getAreaId(), area.isBlacklist() ); if (leaf != null) { leafNodes.add(leaf); } } if (leafNodes.isEmpty()) { return null; } return buildRTreeBytes(leafNodes); } /** * 从多个区域的坐标点列表生成 RTree fbs 文件(多区域版本,与 C++ 完全对齐)。 * * @param areaPoints 每个区域对应的坐标点列表 * @param ids 每个区域的 odd_id * @param isBlacklists 每个区域是否黑名单 * @param outFile 输出的 fbs 二进制文件路径 * @return 生成是否成功 */ public static boolean buildRTreeFromMultiAreas(java.util.List> areaPoints, java.util.List ids, java.util.List isBlacklists, String outFile) { if (areaPoints == null || areaPoints.isEmpty()) { return false; } java.util.List leafNodes = new java.util.ArrayList<>(); for (int i = 0; i < areaPoints.size(); i++) { PbRTreeBuilder.RTreeNode leaf = buildLeafNode( areaPoints.get(i), ids.get(i), isBlacklists.get(i) ); if (leaf != null) { leafNodes.add(leaf); } } return writeRTreeFbs(leafNodes, outFile); } private static PbRTreeBuilder.RTreeNode buildLeafNode(List points, int id, boolean isBlacklist) { if (points == null || points.isEmpty()) { return null; } float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; float maxX = -Float.MAX_VALUE, maxY = -Float.MAX_VALUE; java.util.List ptList = new java.util.ArrayList<>(points.size()); for (LonLat p : points) { float x = (float) p.getLon(); // lon → x float y = (float) p.getLat(); // lat → y ptList.add(new float[]{x, y}); minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); } PbRTreeBuilder.RTreeNode leaf = new PbRTreeBuilder.RTreeNode(); leaf.minX = minX; leaf.minY = minY; leaf.maxX = maxX; leaf.maxY = maxY; leaf.points = ptList; leaf.id = id; leaf.isBlacklist = isBlacklist; return leaf; } private static byte[] buildRTreeBytes(java.util.List leafNodes) { java.util.List rtreeNodes = PbRTreeBuilder.createRTree(leafNodes); FlatBufferBuilder builder = new FlatBufferBuilder(1024); int rtreeOffset = PbRTreeBuilder.writeFlatBuffer(builder, rtreeNodes); builder.finish(rtreeOffset); ByteBuffer buffer = builder.dataBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); return bytes; } private static boolean writeRTreeFbs(java.util.List leafNodes, String outFile) { byte[] bytes = buildRTreeBytes(leafNodes); try (FileOutputStream fos = new FileOutputStream(outFile)) { fos.write(bytes); } catch (IOException e) { return false; } return true; } /** * 简单命令行测试入口。 * 示例:构造一个矩形区域的四个顶点,生成 RTree fbs 文件到指定目录。 */ public static void main(String[] args) { // 示例 GCL 字符串:lon_lat,lon_lat,... String gcl; if (args != null && args.length > 0) { // 如果通过命令行传入,则优先使用参数中的 GCL 字符串 gcl = args[0]; } else { // 默认示例,来源于用户提供的坐标 gcl = "104.06249657558965_30.583221674855963," + "104.06244328689024_30.58049965528362," + "104.06640444191817_30.580606702069797," + "104.06496563619123_30.58420035984882," + "104.06249657558965_30.583221674855963"; } List pts = parseGcl(gcl); int id = 1; boolean isBlacklist = false; String outFile; if (args != null && args.length > 1) { // 如果有第二个参数,则视为输出文件路径 outFile = args[1]; } else { outFile = "xngp_area_data_test.bin"; } boolean ok = buildRTreeFromPoints(pts, id, isBlacklist, outFile); System.out.println("buildRTreeFromPoints result=" + ok + ", outFile=" + outFile); } }