java jts 针对shp含洞多边形进行三角剖分切分成可行区域

前言

java jts 提供了Delaunay三角剖分的相关方法,但是该方法不考虑含洞的多边形的。虽然 jts 的 ConformingDelaunayTriangulationBuilder 类可以通过提供线段约束的方式防止切割到洞内,但是仅支持最多99条线段,虽然可以通过重写破除99条线段的约束,但是线段多了性能会变得很差。且在网上没找到相关的解决方案。

本文主要是基于 jts 实现对含洞多边形进行三角剖分,并且可以不切割到洞内的方法。

三角剖分是生成寻路用的 navmesh 的步骤之一

原生工具切割效果展示

首先是初始shp文件,就是一系列的多边形障碍物,三角剖分时不能切割到障碍物内部。

将上面的shp取反后就是一个含洞的多边形,洞内就是上面的多边形障碍物。

切分后的效果如下图所示

放大局部后如下图所示,可以看出切割的时候是不考虑多边形障碍物的,会切割到多边形内部。

实现代码

pom.xml依赖

 <dependency><groupId>org.geotools</groupId><artifactId>gt-geotiff</artifactId><version>21.1</version>
</dependency>
<dependency><groupId>org.geotools</groupId><artifactId>gt-shapefile</artifactId><version>22.1</version>
</dependency>
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.19</version>
</dependency>

其他工具

QGIS: 用于打开 shp 文件可视化看效果

代码

代码目前测试暂未发现还有什么问题,但不确保一点问题都没。

public class ShpUtils {public static final GeometryFactory GEOMETRY_FACTORY = JTSFactoryFinder.getGeometryFactory();public static void main(String[] args) throws Exception {List<Polygon> resultTriangles = genTriangle("D:\\shp\\test.shp", false, Lists.newArrayList());//将剖分的结果写入shp文件,可以用于看剖分效果writeMultiPolygonShp(GEOMETRY_FACTORY.createMultiPolygon(resultTriangles.toArray(new Polygon[resultTriangles.size()])), "D:\\shp\\test_triangle.shp");}/*** 三角剖分** @param shpPath     shp文件地址* @param isNegation  是否取反后再剖分* @param coordinates 外边框(地图的边界范围),可以为空* @throws Exception*/public static List<Polygon> genTriangle(String shpPath, boolean isNegation, List<Coordinate> coordinates) throws Exception {SimpleFeatureSource featureSource = getFeatureSourceByPath(shpPath);List<MultiPolygon> multiPolygons = getPolygons(featureSource);Geometry triangles;if (!isNegation) {if (CollectionUtil.isEmpty(coordinates)) {//如果没传入外边框,则不设置外边框coordinates = Lists.newArrayList();//取shp外接矩形的外边框的话剖分结果应该是和取反后再三角剖分的效果是一样的,取反默认就是shp文件外接矩形对应的外边框
//            ReferencedEnvelope bounds = featureSource.getBounds();
//            List<Coordinate> coordinates = boundToCoordinate(bounds);}triangles = genTriangle(coordinates, (List) multiPolygons);} else {//取反后三角剖分Geometry negationShp = negationShp(featureSource, coordinates);triangles = genTriangle(Lists.newArrayList(), Lists.newArrayList(negationShp));}//最终剖分得到的三角形List<Polygon> resultTriangles = Lists.newArrayList();List<Polygon> polygons = multiPolygonToPolygon(multiPolygons);for (int i = 0; i < triangles.getNumGeometries(); i++) {//剖分后的三角形Polygon triangle = (Polygon) triangles.getGeometryN(i);List<Polygon> disposePolygons = disposeTriangle(triangle, polygons, 0);resultTriangles.addAll(disposePolygons);//注释掉上面两行使用下面这行即为原生的剖分效果
//            resultTriangles.add(triangle);}return resultTriangles;}/*** 对剖分的三角形存在下面两中情况的进行处理* 1.剖分的三角形可能在障碍物内部* 2.剖分的三角形可能和障碍物有交集,但没被完全包含** @param triangle* @param polygons* @param start* @return*/private static List<Polygon> disposeTriangle(Polygon triangle, List<Polygon> polygons, int start) {List<Polygon> resultTriangles = Lists.newArrayList();for (int i = start; i < polygons.size(); i++) {Polygon polygon = polygons.get(i);if (polygon.contains(triangle)) {//当剖分的三角形完全在障碍物多边形内部,则忽略掉该三角形,在障碍物内部的三角形是不可能为可行区域的return resultTriangles;} else if (polygon.intersects(triangle)) {//当剖分的三角形和障碍物多边形有交集,则相交部分必为不可行区域//取出当前三角形相对于障碍物多边形没有交集的部分,没有交集的部分可能为可行区域,需要对没有交集的部分进行处理Geometry difference = triangle.difference(polygon);if (difference instanceof MultiPolygon) {//障碍物多边形可能将三角形截断成多个多边形,需要对截断后的多个多边形分别进行处理for (int j = 0; j < difference.getNumGeometries(); j++) {//只需要对之后的障碍物多边形校验是否相交resultTriangles.addAll(disposeTriangle((Polygon) difference.getGeometryN(j), polygons, i + 1));}} else if (difference instanceof Polygon) {//如果没有交集的部分依然只是一个多边形,则检测该多边形与后续障碍物多边形是否有交集triangle = (Polygon) difference;} else if (difference instanceof GeometryCollection) {//同 MultiPolygonfor (int k = 0; k < difference.getNumGeometries(); k++) {Geometry geometryN = difference.getGeometryN(k);if (geometryN instanceof Polygon) {resultTriangles.addAll(disposeTriangle((Polygon) geometryN, polygons, i + 1));} else {//TODO 有可能得到的结果是非多边形,目前测试可能得到一条线段,线段应该不需要处理,不确定是否有其他情况System.out.println(MessageFormat.format("不可解析图形#1,{0}", geometryN));}}} else {//TODO 目前测试可能得到一条线段,不予处理System.out.println(MessageFormat.format("不可解析图形#2,{0}", difference));}}}//确保返回的几何图形都为三角形if (triangle.getCoordinates().length == 4) {resultTriangles.add(triangle);} else {//对于非三角形,还需要进行一次原生工具的三角剖分,剖分成多个三角形Geometry geometry = genTriangle(Lists.newArrayList(), Lists.newArrayList(triangle));for (int i = 0; i < geometry.getNumGeometries(); i++) {resultTriangles.add((Polygon) geometry.getGeometryN(i));}}//下方代码是判断是否为凸多边形,返回的结果集集合图形不一定是三角形,但不会是凹多边形// navmesh寻路只要不是凹多边形就可以使用
//        if (isConvex(triangle)) {
//            resultTriangles.add(triangle);
//        } else {
//            //如果是被障碍物多边形截断后的形状,有可能是凹多边形,对于凹多边形,还需要进行一次原生工具的三角剖分
//            Geometry geometry = genTriangle(Lists.newArrayList(), Lists.newArrayList(triangle));
//            for (int i = 0; i < geometry.getNumGeometries(); i++) {
//                resultTriangles.add((Polygon) geometry.getGeometryN(i));
//            }
//        }return resultTriangles;}/*** 多边形是否为凸多边形* 本方法代码是chatgpt生成的,目前测试好像可用** @param polygon 多边形* @return*/public static boolean isConvex(Polygon polygon) {Coordinate[] coords = polygon.getExteriorRing().getCoordinates();if (coords.length < 4) {//之所以是四个是因为第一个点和第四个点是一个样的,所以四个点则为三角形return false; // 少于4个顶点的多边形不可能是凸多边形}boolean direction = getTurnDirection(coords[0], coords[1], coords[2]);for (int i = 1; i < coords.length - 2; i++) {boolean currentDirection = getTurnDirection(coords[i], coords[i + 1], coords[i + 2]);if (currentDirection != direction) {return false;}}return true;}/*** chatgpt 生成代码** @param a* @param b* @param c* @return*/private static boolean getTurnDirection(Coordinate a, Coordinate b, Coordinate c) {double crossProduct = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);return crossProduct > 0; // 返回true表示逆时针转,false表示顺时针转}/*** 将 MultiPolygon 转成 Polygon 集合** @param multiPolygons* @return*/public static List<Polygon> multiPolygonToPolygon(List<MultiPolygon> multiPolygons) {List<Polygon> polygonList = Lists.newArrayList();for (MultiPolygon polygon : multiPolygons) {for (int i = 0; i < polygon.getNumGeometries(); i++) {polygonList.add((Polygon) polygon.getGeometryN(i));}}return polygonList;}/*** 使用jts 原生工具进行三角剖分** @param bounds   外边框的四个坐标点(地图边界范围),可以取所有多边形的最大外边框,也可以自己设置* @param polygons 所有障碍物多边形* @return*/public static Geometry genTriangle(List<Coordinate> bounds, List<Geometry> polygons) {//获取外边框及所有障碍物多边形点位的集合Set<Coordinate> coordinates = Sets.newHashSet();for (Geometry polygon : polygons) {coordinates.addAll(Arrays.asList(polygon.getCoordinates()));}coordinates.addAll(bounds);//对所有点调用 jts 原生工具进行三角剖分DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder();builder.setSites(coordinates);return builder.getTriangles(GEOMETRY_FACTORY);}/*** 获取外边框的四个坐标点** @param bounds* @return*/public static List<Coordinate> boundToCoordinate(ReferencedEnvelope bounds) {List<Coordinate> coordinates = Lists.newArrayList();coordinates.add(new Coordinate(bounds.getMinX(), bounds.getMinY()));coordinates.add(new Coordinate(bounds.getMinX(), bounds.getMaxY()));coordinates.add(new Coordinate(bounds.getMaxX(), bounds.getMaxY()));coordinates.add(new Coordinate(bounds.getMaxX(), bounds.getMinY()));return coordinates;}/*** 加载shp文件** @param shpPath shp文件地址* @return* @throws Exception*/public static SimpleFeatureSource getFeatureSourceByPath(String shpPath) throws Exception {ShapefileDataStore store = new ShapefileDataStore(new URL("file:/" + shpPath));String typeName = store.getTypeNames()[0];return store.getFeatureSource(typeName);}/*** 获取shp文件内的所有多边形** @param featureSource* @return* @throws IOException*/private static List<MultiPolygon> getPolygons(SimpleFeatureSource featureSource) throws IOException {List<MultiPolygon> polygons = Lists.newArrayList();SimpleFeatureIterator features = featureSource.getFeatures().features();while (features.hasNext()) {SimpleFeature feature = features.next();Geometry geometry = (Geometry) feature.getDefaultGeometry();if (geometry instanceof MultiPolygon) {MultiPolygon multiPolygon = (MultiPolygon) geometry;polygons.add(multiPolygon);}}return polygons;}/*** 取反* 比如如果一个shp 含有多个多边形障碍物,可以用本方法生成一个多边形含有多个洞,洞表示原来的障碍物多边形** @param featureSource* @param coordinates   外边框(地图边界范围)* @return 取反后的几何图形* @throws IOException*/public static Geometry negationShp(SimpleFeatureSource featureSource, List<Coordinate> coordinates) throws IOException {if (CollectionUtil.isEmpty(coordinates)) {//如果没有自定义外边框,则取shp文件的外接矩形作为外边框ReferencedEnvelope bounds = featureSource.getBounds();coordinates = boundToCoordinate(bounds);}//考虑是否判断最后一个点和第一个点一样时不添加新点coordinates.add(coordinates.get(0));Geometry geometryNegation = GEOMETRY_FACTORY.createPolygon(coordinates.toArray(new Coordinate[0]));SimpleFeatureIterator features = featureSource.getFeatures().features();while (features.hasNext()) {SimpleFeature feature = features.next();Geometry geometry = (Geometry) feature.getDefaultGeometry();geometryNegation = geometryNegation.difference(geometry);}return geometryNegation;}/*** 将几何图形写入shp文件** @param geometry  几何图形(可以是一个集合GeometryCollection)* @param writePath 写入的路径位置* @throws Exception*/public static void writeMultiPolygonShp(Geometry geometry, String writePath) throws Exception {ShapefileDataStore ds = new ShapefileDataStore(new URL("file:/" + writePath));//设置编码ds.setCharset(Charset.forName("UTF-8"));//定义图形信息和属性信息SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();tb.setCRS(DefaultGeographicCRS.WGS84);tb.setName("shapefile");tb.add("the_geom", MultiPolygon.class);tb.add("num", Integer.class);ds.createSchema(tb.buildFeatureType());//设置WriterFeatureWriter<SimpleFeatureType, SimpleFeature> writer = ds.getFeatureWriter(ds.getTypeNames()[0], Transaction.AUTO_COMMIT);//写入文件信息for (int i = 0; i < geometry.getNumGeometries(); i++) {SimpleFeature feature = writer.next();Geometry triangle = geometry.getGeometryN(i);feature.setAttribute("the_geom", triangle);feature.setAttribute("num", i);}writer.write();writer.close();ds.dispose();}
}

效果

上面代码切割的效果如下图所示

上面代码有个取反后再进行三角剖分的代码,效果如下

与原生工具对比如下,不会切割到障碍物内部了。

扩展

求共边

上面生成的三角形数量还是比较多的, navmesh 不要求一定要是三角形,可以是凸多边形,不可以是凹多边形,所以可以基于上面的三角形集合根据共边合并成凸多边形,从而减少寻路的三角形数量。从而提升寻路性能。

耳切法

网上是有说耳切法进行三角剖分是可以切割有洞的多边形的,有兴趣的可以自己试试

寻路

GitHub - jzyong/GameAI4j: Game AI for java.NavMesh、A*、BehaviorTree、FSM

上面地址提供了如何基于 navmesh 寻路的相关代码

核心类

PolygonMeshWindow 多边形寻路的可视化 ui 界面启动类, loadMap 方法会加载 navmesh 文件初始化PolygonNavMesh
PolygonNavMesh
多边形寻路的类,将多边形转为改结构即可用 findPath 进行寻路

TriangleNavMeshWindow 三角形寻路的可视化 ui 界面启动类
TriangleNavMesh 用于三角形寻路的类

三角形寻路没用过,可以考虑直接用多边形寻路。

navmesh文件结构介绍

只需要将本文生成的三角形转换成上面库可以解析的navmesh文件,即可用上面的库进行寻路

navmesh的结构如下图所示

主要就是介绍下 pathTriangles pathVertices

pathVertices s是所有多边形的点集数组,如下图,表示的是多边形所有顶点。

pathTriangles 多边形的所有标识,存的值表示的是 pathVertices 中的数组下标对应的点。具有以下特性

1.每三个点表示一个三角形,比如第1-3个值 0,1,2 表示  pathVertices[0]、pathVertices[1]、pathVertices[2] 组成一个三角形,第4-6个指 0、2、3 又是一个三角形。
2.当连续的三角形有两个下标是一样的,表示是共边三角形,可以组成一个凸多边形。所以生成navmesh的时候需要注意不然让连续的三角形有两个下标一样的却不能合成凸多边形。比如前面 0、1、20、2、3有相同的点0、2,所以就可以组成一个0、1、2、3的四边形,以此类推可以合并成五边形、六边形。

另一种思路

上面的方法是将 shp 转换的三角形或者多边形转成 navmesh 文件,但是这样得按照navmesh的格式规则,其实可以自己去看  P olygonNavMesh 类的代码,直接将三角形和多边形转成 PolygonNavMesh 需要的数据结构。这个也不会很难,无非就修改一些 navmesh的解析逻辑

recast

java 版的 recastnavigation ,提供类似unity 3d 的寻路,可视化的三角剖分、体素化等相关功能。我也没去研究过,里面可能会有三角剖分、求共边的相关工具。链接地址如下

GitHub - recast4j/recast4j: Java Port of Recast & Detour navigation mesh toolset

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/49179.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java——————接口(interface) <详解>

1.1 接口的概念 在现实生活中&#xff0c;接口的例子比比皆是&#xff0c;比如&#xff1a;笔记本电脑上的USB接口&#xff0c;电源插座等。 电脑的USB口上&#xff0c;可以插&#xff1a;U盘、鼠标、键盘...所有符合USB协议的设备 电源插座插孔上&#xff0c;可以插&#xff…

OS Copilot初体验的感受与心得

本文介绍体验操作系统智能助手OS Copilot后&#xff0c;个人的一些收获、体验等。 最近&#xff0c;抽空体验了阿里云的操作系统智能助手OS Copilot&#xff0c;在这里记录一下心得与收获。总体观之&#xff0c;从个人角度来说&#xff0c;感觉这个OS Copilot确实抓住了不少开发…

EasyMedia转码rtsp视频流flv格式,hls格式,H5页面播放flv流视频

EasyMedia转码rtsp视频流flv格式&#xff0c;hls格式 H5页面播放flv流视频 文章最后有源码地址 解决海康视频播放视频流&#xff0c;先转码后自定义页面播放flv视频流 先看效果&#xff0c;1&#xff0c;EasyMedia自带的页面&#xff0c;这个页面二次开发改动页面比较麻烦 …

张高兴的 MicroPython 入门指南:(三)使用串口通信

目录 什么是串口使用方法使用板载串口相互通信 硬件需求电路代码使用板载的 USB 串口参考 什么是串口 串口是串行接口的简称&#xff0c;这是一个非常大的概念&#xff0c;在嵌入式中串口通常指 UART(Universal Asynchronous Receiver/Transmitter&#xff0c;通用异步收发器)。…

初识c++(string和模拟实现string)

一、标准库中的string类 string类的文档介绍&#xff1a;cplusplus.com/reference/string/string/?kwstring 1、auto和范围for auto&#xff1a; 在早期C/C中auto的含义是&#xff1a;使用auto修饰的变量&#xff0c;是具有自动存储器的局部变量&#xff0c;后来这个 不重…

Cannot perform upm operation: connect ETIMEDOUT 34.36.199.114:443 [NotFound]

版本&#xff1a;Unity 2018 Windows 问题&#xff1a;打开 Package Manager&#xff0c;加载报错 尝试解决&#xff1a; 删除项目文件里的Packages下的mainfest.json文件&#xff0c;然后重新打开项目&#xff08;X&#xff09;重新登录 Unity 账号&#xff08;X&#xff09…

Kotlin 协程 — 基础

Kotlin 协程 — 基础 协程已经存在一段时间了&#xff0c;关于它的各种文章也很多。但我发现想要了解它还比较费时&#xff0c;所以我花了一段时间才真正理解了协程的基础知识以及它的工作原理。因此&#xff0c;我想分享一些我理解到的内容。 什么是协程&#xff1f; 协程代表…

vue项目实战速查记录

1.图片下载到本地 2.本地静态文件访问 3.元素大小相同,相互覆盖 1.图片下载到本地 实现原理:创建a标签,利用a标签下载属性. download(){const link document.createElement(a);link.href "图片地址";link.setAttribute(download, name);document.body.ap…

读论文《Hi-Net: Hybrid-fusion Network for Multi-modalMR Image Synthesis》

论文题目&#xff1a;Hi-Net:用于多模态磁共振图像合成的混合融合网络 论文地址&#xff1a;arxiv 项目地址&#xff1a;github 原项目可能在训练的时候汇报version的错&#xff0c;这是因为生成器和辨别器的优化有些逻辑错误&#xff0c;会改的话多加一个生成操作可以解决&…

React 学习——条件渲染、遍历循环、事件绑定

React特点&#xff1a; 声明式的设计高效&#xff0c;采用虚拟DOM来实现DOM的渲染&#xff0c;最大限度减少DOM的操作灵活&#xff0c;跟其他库灵活搭配使用JSX&#xff0c;俗称JS里面写HTML&#xff0c;JavaScript语法的扩展组件化&#xff0c;模块化&#xff0c;代码容易复用…

pdf的下载,后端返回工作流,前端进行转换

前端将后端返回的工作流进行转换 项目中接触到了pdf的下载和预览的功能&#xff0c;记录一下~ 这里pdf的下载和预览的接口&#xff0c;后端返回的数据结构与其他的接口返回的数据结构有点不同&#xff0c;是直接返回的工作流&#xff0c;在控制台接口的响应预览内容大致是这样…

初学MySQl简单sql语句(1)

目录 SQL语句介绍&#xff1a; DDL创建数据库&#xff1a; char和varchar比较 数值类型 数据库存储引擎 数据库存储引擎——InnoDB 数据库存储引擎——MyISAM 数据库存储引擎-MyISAM 和InnoDB区别 修改和删除数据库表 数据库设计三大范式 一、什么是范式 二、约束作…

css实战案例1:顶部搜索

代码样式&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8" /><title></title></head><body><div class"search_box"><!-- 搜索框--><div class"search">搜索…

【Linux】网络基础_2

文章目录 十、网络基础2. IP地址和MAC地址3. 端口号端口号和进程ID 4. 网络字节序 未完待续 十、网络基础 2. IP地址和MAC地址 IP协议有两个版本&#xff0c;IPv4和IPv6&#xff0c; 用的比较多的都是IPv4。IP地址是在IP协议中&#xff0c;用来标识网络中不同主机的地址&…

如何发现快速发现分析生产问题SQL

Performance Schema介绍 Performance Schema提供了有关MySQL服务器内部运行的操作上的底层指标。为了解释清楚Performance Schema的工作机制&#xff0c;先介绍两个概念。 第一个概念是程序插桩&#xff08;instrument&#xff09;。程序插桩在MySQL代码中插入探测代码&#xf…

基本聚集函数和case的应用

文章目录 1.基本聚集函数(1)基本聚集函数的介绍(2)使用基本聚集函数的简单例子&#xff08;1&#xff09;查询最大年龄&#xff0c;最小年龄年龄和平均年龄<1>最大年龄<2>最小年龄<3>平均年龄 (2&#xff09;配合上where语句&#xff0c;查询女士的平均年龄(…

JAVA笔记十四

十四、集合 1.集合概述 (1)集合是存储其它对象的特殊对象&#xff0c;可以将集合当作一个容器 (2)集合的相关接口和类位于java.util包中 (3)集合中的接口和类是一个整体、一个体系 2.集合接口 接口定义了一组抽象方法&#xff0c;实现该接口的类需要实现这些抽象方法&…

Docker核心技术:Docker原理之Cgroups

云原生学习路线导航页&#xff08;持续更新中&#xff09; 本文是 Docker核心技术 系列文章&#xff1a;Docker原理之Cgroups&#xff0c;其他文章快捷链接如下&#xff1a; 应用架构演进容器技术要解决哪些问题Docker的基本使用Docker是如何实现的 Docker核心技术&#xff1a;…

C++初学者指南-5.标准库(第一部分)--标准库最小/最大算法

C初学者指南-5.标准库(第一部分)–标准库min/max算法 文章目录 C初学者指南-5.标准库(第一部分)--标准库min/max算法minmaxminmaxclamp (C17)min_elementmax_elementminmax_element相关内容 C标准库算法是一块新领域&#xff1f;⇒简短介绍 min min(a, b) → a 如果 a < b则…

Linux_实现UDP网络通信

目录 1、实现服务器的逻辑 1.1 socket 1.2 bind 1.3 recvfrom 1.4 sendto 1.5 服务器代码 2、实现客户端的逻辑 2.1 客户端代码 3、实现通信 结语 前言&#xff1a; 在Linux下&#xff0c;实现传输层协议为UDP的套接字进行网络通信&#xff0c;网络层协议为IPv4&am…