ard-admin/src/main/resources/application-dev.yml
@@ -37,14 +37,36 @@ # 天宇服务器配置 tianyu: # 天宇http服务器地址 endpoint: 59.110.237.99:8060 endpoint: 192.168.1.97:8060 accesskeyid: wuwuwu accesskeysecret: 9c9f139f63b343b0a9ef42e05ed29b0b # 天宇tcp服务器地址 tcpserver: 59.110.237.99 tcpport: 8125 tcpserver: 192.168.1.97 tcpport: 8126 # 地图资源配置 geoserver: deviceCoordinateType: WGS84 tifPath: H:\map\hlj.tif wayline: #可理解为100米大小占图像的比例 targetRatio: 0.5 #最大航段距离 maxLength: 2000 #最小航段距离 minLength: 1 #随飞随动分段长度 PeerAtLength: 10 #航线KMZ临时存放路径 waylineSavePath: /home/ard/myworkspace/uav_server/mainserver/waylines # MinIO minio: url: http://192.168.1.15:9001 accessKey: admin secretKey: xzx12345 bucketName: kmz #流媒体配置 mediamtx: # 流媒体服务器地址 host: 192.168.1.27 host: 192.168.1.15 ard-admin/src/main/resources/application.yml
@@ -111,13 +111,13 @@ # redis 配置 redis: # 地址 host: 127.0.0.1 host: 192.168.1.15 # 端口,默认为6379 port: 6379 # 数据库索引 database: 0 # 密码 password: xzx12345 password: # 连接超时时间 timeout: 10s lettuce: ard-ui/package.json
@@ -6,10 +6,16 @@ "license": "MIT", "scripts": { "dev": "vue-cli-service serve", "build:prod": "vue-cli-service build", "build:prod": "vue-cli-service build --mode production", "build:stage": "vue-cli-service build --mode staging", "build:analyze": "vue-cli-service build --report", "build:test": "vue-cli-service build --mode test", "preview": "node build/index.js --preview", "lint": "eslint --ext .js,.vue src" "lint": "eslint --ext .js,.vue src", "lint:fix": "eslint --ext .js,.vue src --fix", "clean": "rimraf dist", "serve:prod": "npm run build:prod && npm run preview", "serve:stage": "npm run build:stage && npm run preview" }, "husky": { "hooks": { ard-ui/src/main.js
@@ -37,6 +37,10 @@ import VueMeta from 'vue-meta' // 字典数据组件 import DictData from '@/components/DictData' // 性能监控工具 import performanceMonitor from '@/utils/performance' // 缓存管理器 import cacheManager from '@/utils/cache' // 全局方法挂载 Vue.prototype.getDicts = getDicts @@ -48,6 +52,8 @@ Vue.prototype.selectDictLabels = selectDictLabels Vue.prototype.download = download Vue.prototype.handleTree = handleTree Vue.prototype.performanceMonitor = performanceMonitor Vue.prototype.cacheManager = cacheManager // 全局组件挂载 Vue.component('DictTag', DictTag) ard-ui/vue.config.js
@@ -1,14 +1,13 @@ 'use strict' const path = require('path') const CompressionPlugin = require('compression-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') function resolve(dir) { return path.join(__dirname, dir) } const CompressionPlugin = require('compression-webpack-plugin') const name = process.env.VUE_APP_TITLE || '无人机后台管理系统' // 网页标题 const port = process.env.port || process.env.npm_config_port || 80 // 端口 // vue.config.js 配置说明 @@ -27,12 +26,23 @@ lintOnSave: process.env.NODE_ENV === 'development', // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 productionSourceMap: false, // 转译依赖 transpileDependencies: ['quill'], // webpack-dev-server 相关配置 devServer: { host: '0.0.0.0', port: port, open: true, // 添加热更新配置 hot: true, // 添加客户端日志级别 client: { logging: 'warn', overlay: { errors: true, warnings: false, }, }, proxy: { // detail: https://cli.vuejs.org/config/#devserver-proxy [process.env.VUE_APP_BASE_API]: { @@ -40,34 +50,66 @@ changeOrigin: true, pathRewrite: { ['^' + process.env.VUE_APP_BASE_API]: '' } }, // 添加代理超时配置 timeout: 30000, // 添加代理日志 logLevel: 'debug' } }, disableHostCheck: true // 移除已废弃的配置 // disableHostCheck: true, // 使用新的配置 allowedHosts: 'all' }, css: { loaderOptions: { sass: { sassOptions: { outputStyle: "expanded" } } } }, // 添加CSS提取配置 extract: process.env.NODE_ENV === 'production' }, configureWebpack: { name: name, resolve: { alias: { '@': resolve('src') }, // 添加文件扩展名解析 extensions: ['.js', '.vue', '.json'] }, // 添加性能提示配置 performance: { hints: process.env.NODE_ENV === 'production' ? 'warning' : false, maxEntrypointSize: 512000, maxAssetSize: 512000 }, // 添加优化配置 optimization: { minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: process.env.NODE_ENV === 'production', drop_debugger: process.env.NODE_ENV === 'production' } } }) ] }, plugins: [ // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件 new CompressionPlugin({ cache: false, // 不启用文件缓存 test: /\.(js|css|html|jpe?g|png|gif|svg)?$/i, // 压缩文件格式 test: /\.(js|css|html|jpe?g|png|gif|svg|woff2?|eot|ttf|otf)?$/i, // 压缩文件格式 filename: '[path][base].gz[query]', // 压缩后的文件名 algorithm: 'gzip', // 使用gzip压缩 minRatio: 0.8, // 压缩比例,小于 80% 的文件不会被压缩 deleteOriginalAssets: false // 压缩后删除原文件 deleteOriginalAssets: false, // 压缩后删除原文件 threshold: 10240, // 只有大于该值的资源会被处理 minSize: 0 // 只有大于该值的资源会被处理 }) ], }, @@ -91,6 +133,13 @@ symbolId: 'icon-[name]' }) .end() // 添加图片压缩配置 config.module .rule('images') .use('url-loader') .loader('url-loader') .tap(options => Object.assign(options, { limit: 10240 })) config.when(process.env.NODE_ENV !== 'development', config => { config @@ -116,6 +165,11 @@ test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm priority: 20 // the weight needs to be larger than libs and app or it will be packaged into libs or app }, echarts: { name: 'chunk-echarts', // split echarts into a single package test: /[\\/]node_modules[\\/]_?echarts(.*)/, priority: 15 }, commons: { name: 'chunk-commons', test: resolve('src/components'), // can customize your rules @@ -127,5 +181,15 @@ }) config.optimization.runtimeChunk('single') }) // 添加开发环境优化 config.when(process.env.NODE_ENV === 'development', config => { config.devtool('cheap-module-eval-source-map') }) // 添加生产环境优化 config.when(process.env.NODE_ENV === 'production', config => { config.devtool('source-map') }) } } ard-work/pom.xml
@@ -14,7 +14,10 @@ <description> 主业务入口 </description> <properties> <minio.version>8.3.7</minio.version> <dom4j.version>2.1.3</dom4j.version> </properties> <dependencies> <!-- 通用工具--> @@ -69,6 +72,56 @@ <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> <!--geo--> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-geotiff</artifactId> <version>28.2</version> </dependency> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-main</artifactId> <version>28.2</version> </dependency> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-referencing</artifactId> <version>28.2</version> </dependency> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-epsg-hsql</artifactId> <version>28.2</version> </dependency> <!--minio--> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>${minio.version}</version> </dependency> <!--DOM4j--> <dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>${dom4j.version}</version> </dependency> <!--okhttp3--> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </dependency> </dependencies> <repositories> <repository> <id>osgeo</id> <name>OSGeo Release Repository</name> <url>https://repo.osgeo.org/repository/release/</url> <snapshots> <enabled>false</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> </project> ard-work/src/main/java/com/ard/tianyu/component/minio/config/MinioConfig.java
New file @@ -0,0 +1,44 @@ package com.ard.tianyu.component.minio.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.minio.MinioClient; /** * Minio 配置信息 * * @author ruoyi */ @Data @Configuration @ConfigurationProperties(prefix = "minio") public class MinioConfig { /** * 服务地址 */ private String url; /** * 用户名 */ private String accessKey; /** * 密码 */ private String secretKey; /** * 存储桶名称 */ private String bucketName; @Bean public MinioClient getMinioClient() { return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build(); } } ard-work/src/main/java/com/ard/tianyu/component/minio/utils/MinioUtil.java
New file @@ -0,0 +1,210 @@ package com.ard.tianyu.component.minio.utils; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import com.ard.common.utils.ServletUtils; import com.ard.common.utils.spring.SpringUtils; import io.minio.*; import org.springframework.web.multipart.MultipartFile; import io.minio.http.Method; /** * Minio 文件存储工具类 * * @author ruoyi */ public class MinioUtil { /** * 判断桶是否存在 * * @param bucketName 桶名称 * @return 如果桶存在返回 true,否则返回 false * @throws IOException */ public static boolean bucketExists(String bucketName) throws IOException { MinioClient minioClient = SpringUtils.getBean(MinioClient.class); try { return minioClient.bucketExists(io.minio.BucketExistsArgs.builder().bucket(bucketName).build()); } catch (Exception e) { throw new IOException("Error checking if bucket exists", e); } } // 创建桶 public static void createBucket(String bucketName) throws IOException { MinioClient minioClient = SpringUtils.getBean(MinioClient.class); try { minioClient.makeBucket(io.minio.MakeBucketArgs.builder().bucket(bucketName).build()); setBucketPolicy(bucketName); } catch (Exception e) { throw new IOException("Error creating bucket", e); } } /** * 设置桶策略 * 刘苏义 * 2023/9/20 10:01:39 */ public static boolean setBucketPolicy(String bucket) throws Exception { MinioClient minioClient = SpringUtils.getBean(MinioClient.class); try { //设置策略 String sb = "{\"Version\":\"2012-10-17\"," + "\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":" + "{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"," + "\"s3:GetBucketLocation\"],\"Resource\":[\"arn:aws:s3:::" + bucket + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\"],\"Resource\":[\"arn:aws:s3:::" + bucket + "/*\"]}]}"; SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs.builder() .bucket(bucket) .config(sb) .build(); minioClient.setBucketPolicy(setBucketPolicyArgs); return true; } catch (Exception ex) { throw new Exception("设置桶策略异常:" + ex.getMessage()); } } /** * 上传文件 * * @param bucketName 桶名称 * @param fileName * @throws IOException */ public static String uploadFile(String bucketName, String fileName, MultipartFile multipartFile) throws IOException { String url = ""; MinioClient minioClient = SpringUtils.getBean(MinioClient.class); try (InputStream inputStream = multipartFile.getInputStream()) { minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(inputStream, multipartFile.getSize(), -1).contentType(multipartFile.getContentType()).build()); url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(fileName).method(Method.GET).build()); url = url.substring(0, url.indexOf('?')); return ServletUtils.urlDecode(url); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } /** * 上传本地文件 * * @param bucketName 桶名称 * @param fileName 存储到 Minio 的文件名 * @param localFilePath 本地文件绝对路径 * @return 访问 URL * @throws IOException */ public static String uploadLocalFile(String bucketName, String fileName, String localFilePath) throws IOException { MinioClient minioClient = SpringUtils.getBean(MinioClient.class); try (InputStream inputStream = new FileInputStream(localFilePath)) { File file = new File(localFilePath); minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(inputStream, file.length(), -1) .contentType("application/octet-stream") // 可根据需求自行修改 .build() ); String url = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(fileName) .method(Method.GET) .build() ); return ServletUtils.urlDecode(url.substring(0, url.indexOf('?'))); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } public static String uploadInputStream(String bucketName, String fileName, InputStream inputStream, long size, String contentType) throws IOException { MinioClient minioClient = SpringUtils.getBean(MinioClient.class); if (!bucketExists(bucketName)) { createBucket(bucketName); } try { minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(inputStream, size, -1) .contentType(contentType) .build() ); String url = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(fileName) .method(Method.GET) .build() ); url = url.substring(0, url.indexOf('?')); return ServletUtils.urlDecode(url); } catch (Exception e) { throw new IOException(e.getMessage(), e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException ignore) { } } } } /** * 判断文件是否存在 */ public static boolean exists(String bucketName, String fileName) throws IOException { MinioClient minioClient = SpringUtils.getBean(MinioClient.class); if (!bucketExists(bucketName)) { createBucket(bucketName); } try { minioClient.statObject( io.minio.StatObjectArgs.builder() .bucket(bucketName) .object(fileName) .build() ); return true; } catch (io.minio.errors.ErrorResponseException e) { if ("NoSuchKey".equals(e.errorResponse().code()) || "NoSuchObject".equals(e.errorResponse().code())) { return false; } throw new IOException(e.getMessage(), e); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } /** * 删除文件 */ public static void delete(String bucketName, String fileName) throws IOException { MinioClient minioClient = SpringUtils.getBean(MinioClient.class); try { minioClient.removeObject( io.minio.RemoveObjectArgs.builder() .bucket(bucketName) .object(fileName) .build() ); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } } ard-work/src/main/java/com/ard/tianyu/component/websocket/handler/TyMessageHandler.java
@@ -122,7 +122,7 @@ Boolean subscribe = uavStateMessage.getSubscribe(); if (subscribe) { // 执行订阅操作 uavStateManager.subscribe(boxSn, userId, 2); uavStateManager.subscribe(boxSn, userId, 1); } else { // 执行退订操作 uavStateManager.unsubscribe(boxSn, userId); @@ -153,7 +153,7 @@ Boolean subscribe = uavTelemetryMessage.getSubscribe(); if (subscribe) { // 执行订阅操作 uavTelemetryManager.subscribe(boxSn, userId, 2); uavTelemetryManager.subscribe(boxSn, userId, 1); } else { // 执行退订操作 uavTelemetryManager.unsubscribe(boxSn, userId); @@ -239,7 +239,7 @@ Boolean subscribe = uavControlMessage.getSubscribe(); if (subscribe) { // 执行订阅操作 uavAllManager.subscribe(boxSn, userId, 2); uavAllManager.subscribe(boxSn, userId, 1); } else { // 执行退订操作 uavAllManager.unsubscribe(boxSn, userId); ard-work/src/main/java/com/ard/tianyu/config/GeoserverConfig.java
New file @@ -0,0 +1,14 @@ package com.ard.tianyu.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Data @Configuration @ConfigurationProperties(prefix = "geoserver") public class GeoserverConfig { private String deviceCoordinateType; private String tifPath;//用于仿地飞行的地形文件 } ard-work/src/main/java/com/ard/tianyu/config/OssConfiguration.java
New file @@ -0,0 +1,92 @@ package com.ard.tianyu.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @author sean * @version 0.2 * @date 2021/12/9 */ @ConfigurationProperties(prefix = "oss") @Component public class OssConfiguration { /** * @see com.dji.ard.component.oss.model.enums.OssTypeEnum */ public static String provider; /** * Whether to use the object storage service. */ public static boolean enable; /** * The protocol needs to be included at the beginning of the address. */ public static String endpoint; public static String accessKey; public static String secretKey; public static String region; public static Long expire; public static String roleSessionName; public static String roleArn; public static String bucket; public static String objectDirPrefix; public void setProvider(String provider) { OssConfiguration.provider = provider; } public void setEnable(boolean enable) { OssConfiguration.enable = enable; } public void setEndpoint(String endpoint) { OssConfiguration.endpoint = endpoint; } public void setAccessKey(String accessKey) { OssConfiguration.accessKey = accessKey; } public void setSecretKey(String secretKey) { OssConfiguration.secretKey = secretKey; } public void setRegion(String region) { OssConfiguration.region = region; } public void setExpire(Long expire) { OssConfiguration.expire = expire; } public void setRoleSessionName(String roleSessionName) { OssConfiguration.roleSessionName = roleSessionName; } public void setRoleArn(String roleArn) { OssConfiguration.roleArn = roleArn; } public void setBucket(String bucket) { OssConfiguration.bucket = bucket; } public void setObjectDirPrefix(String objectDirPrefix) { OssConfiguration.objectDirPrefix = objectDirPrefix; } } ard-work/src/main/java/com/ard/tianyu/config/WaylinePlaningConfig.java
New file @@ -0,0 +1,18 @@ package com.ard.tianyu.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Data @Configuration @ConfigurationProperties(prefix = "wayline") public class WaylinePlaningConfig { private Float targetRatio; private Integer maxLength; private Integer minLength; private Integer PeerAtLength; private String waylineSavePath; } ard-work/src/main/java/com/ard/tianyu/controller/UavWayLineController.java
@@ -19,7 +19,7 @@ * @author liusuyi * @date 2024年10月21日13:52 **/ @Api(tags = "无人机航线飞行接口") @Api(tags = "无人机航线操作接口") @RequestMapping("/uav/wayline") @RestController public class UavWayLineController { ard-work/src/main/java/com/ard/tianyu/controller/wayline/UavWaylineController.java
New file @@ -0,0 +1,114 @@ package com.ard.tianyu.controller.wayline; import com.ard.tianyu.domain.dto.wayline.ArdWaylineDTO; import com.ard.tianyu.domain.entity.UavWayline; import com.ard.tianyu.service.IUavWaylineService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import java.util.List; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import com.ard.common.annotation.Log; import com.ard.common.core.controller.BaseController; import com.ard.common.core.domain.AjaxResult; import com.ard.common.enums.BusinessType; import com.ard.common.utils.poi.ExcelUtil; import com.ard.common.core.page.TableDataInfo; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; /** * 航线Controller * * @author ruoyi * @date 2025-06-27 */ @Api(tags = "无人机航线管理接口") @RestController @RequestMapping("/uav/manage/wayline") public class UavWaylineController extends BaseController { @Resource private IUavWaylineService uavWaylineService; /** * 查询航线列表 */ @ApiOperation("查询航线列表") @PreAuthorize("@ss.hasPermi('uav:wayline:list')") @GetMapping("/list") public TableDataInfo list(UavWayline uavWayline) { startPage(); List<UavWayline> list = uavWaylineService.selectUavWaylineList(uavWayline); return getDataTable(list); } /** * 导出航线列表 */ @PreAuthorize("@ss.hasPermi('uav:wayline:export')") @Log(title = "航线", businessType = BusinessType.EXPORT) @PostMapping("/export") public void export(HttpServletResponse response, UavWayline uavWayline) { List<UavWayline> list = uavWaylineService.selectUavWaylineList(uavWayline); ExcelUtil<UavWayline> util = new ExcelUtil<UavWayline>(UavWayline.class); util.exportExcel(response, list, "航线数据"); } /** * 获取航线详细信息 */ @ApiOperation("获取航线详细信息") @PreAuthorize("@ss.hasPermi('uav:wayline:query')") @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") String id) { return success(uavWaylineService.selectUavWaylineById(id)); } /** * 新增航线 */ @ApiOperation("新增航线") @PreAuthorize("@ss.hasPermi('uav:wayline:add')") @Log(title = "航线", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody ArdWaylineDTO ardWaylineDTO) { if (!uavWaylineService.checkWaylineNameUnique(ardWaylineDTO)) { return error("新增航线'" + ardWaylineDTO.getWaylineName() + "'失败,航线已存在"); } return toAjax(uavWaylineService.insertUavWayline(ardWaylineDTO)); } /** * 修改航线 */ @ApiOperation("修改航线") @PreAuthorize("@ss.hasPermi('uav:wayline:edit')") @Log(title = "航线", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody ArdWaylineDTO ardWaylineDTO) { if (!uavWaylineService.checkWaylineNameUnique(ardWaylineDTO)) { return error("修改航线'" + ardWaylineDTO.getWaylineName() + "'失败,航线已存在"); } return toAjax(uavWaylineService.updateUavWayline(ardWaylineDTO)); } /** * 删除航线 */ @ApiOperation("删除航线") @PreAuthorize("@ss.hasPermi('uav:wayline:remove')") @Log(title = "航线", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") public AjaxResult remove(@PathVariable String[] ids) { return toAjax(uavWaylineService.deleteUavWaylineByIds(ids)); } } ard-work/src/main/java/com/ard/tianyu/controller/wayline/terrain/GeoUtils.java
New file @@ -0,0 +1,62 @@ package com.ard.tianyu.controller.wayline.terrain; import com.ard.tianyu.config.GeoserverConfig; import lombok.extern.slf4j.Slf4j; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.processing.Operations; import org.geotools.gce.geotiff.GeoTiffReader; import org.geotools.geometry.DirectPosition2D; import org.geotools.referencing.CRS; import org.opengis.geometry.DirectPosition; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.File; import java.io.IOException; @Slf4j @Component public class GeoUtils { @Resource private GeoserverConfig geoserverConfig; private GridCoverage2D coverage; /** * 加载地形文件 * * @author lsy * @date 2025/6/18 */ @PostConstruct public void initTif() { try { File file = new File(geoserverConfig.getTifPath()); GeoTiffReader reader = new GeoTiffReader(file); GridCoverage2D rawCoverage = reader.read(null); CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:4326"); coverage = (GridCoverage2D) Operations.DEFAULT.resample(rawCoverage, targetCRS); } catch (Exception e) { log.error("加载tif地形文件异常:{}", e.getMessage()); } } /** * 获取地形高度 * * @param lon 经度 * @param lat 纬度 * @return 海拔高度 * @author lsy * @date 2025/6/18 */ public double getHeight(double lon, double lat) throws IOException { DirectPosition pos = new DirectPosition2D(null, lat, lon); double[] result = coverage.evaluate(pos, new double[1]); System.out.println("Elevation at (" + lon + ", " + lat + "): " + result[0]); return result[0]; } } ard-work/src/main/java/com/ard/tianyu/controller/wayline/terrain/TerrainFollowingPlanner.java
New file @@ -0,0 +1,222 @@ package com.ard.tianyu.controller.wayline.terrain; import com.ard.tianyu.domain.dto.wayline.ArdActionDTO; import com.ard.tianyu.domain.dto.wayline.ArdPositionDTO; import com.ard.tianyu.util.GeoTools; import com.ard.tianyu.util.Vertex; import com.ard.tianyu.domain.dto.wayline.ArdUavPositionDTO; import com.ard.tianyu.domain.dto.wayline.ArdWaylineDTO; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * 仿地飞行规划器 * * @author lsy * @date 2025/6/18 */ @Component public class TerrainFollowingPlanner { @Resource private GeoUtils geoUtils; private final double intervalMeter = 100.0; // 插值距离 private final double smoothThreshold = 3.0; // 平滑阈值 private static final ObjectMapper mapper = new ObjectMapper(); // 算法主函数 public List<ArdUavPositionDTO> interpolateWithTerrain(ArdWaylineDTO ardWaylineDTO) throws Exception { // 0. 验证源航线数据 validateWaylineData(ardWaylineDTO); // 1. 先进行纯几何插值(不考虑高度) List<ArdUavPositionDTO> interpolatedPoints = pureInterpolation(ardWaylineDTO.getUavPositions()); // 2. 然后统一处理地形高度 return adjustAltitudeForTerrain(interpolatedPoints, ardWaylineDTO.getGlobalFlyHeight()); } // 第一阶段:纯几何插值 private List<ArdUavPositionDTO> pureInterpolation(List<ArdUavPositionDTO> originalPoints) { List<ArdUavPositionDTO> result = new ArrayList<>(); for (int i = 0; i < originalPoints.size() - 1; i++) { ArdUavPositionDTO start = originalPoints.get(i); ArdUavPositionDTO end = originalPoints.get(i + 1); // 添加原始起点 result.add(deepCopy(start, ArdUavPositionDTO.class)); // 插值中间点 result.addAll(interpolateBetween(start, end)); } // 添加最后一个点 result.add(deepCopy(originalPoints.get(originalPoints.size() - 1), ArdUavPositionDTO.class)); return result; } // 第二阶段:地形高度调整 private List<ArdUavPositionDTO> adjustAltitudeForTerrain(List<ArdUavPositionDTO> points, double flyHeight) throws IOException { double homeTerrainHeight = 0; // 记录起飞点地形高度作为基准海拔 double lastFlightAltitude = flyHeight; // 记录上一个飞行高度 for (int i = 0; i < points.size(); i++) { ArdUavPositionDTO point = points.get(i); double currentTerrainHeight = geoUtils.getHeight( point.getUavPosition().getLongitude(), point.getUavPosition().getLatitude() ); if (i == 0) { homeTerrainHeight = currentTerrainHeight; } // 计算相对于起飞点的地形高度差 double terrainHeightDiff = currentTerrainHeight - homeTerrainHeight; // 理论目标高度 = 设定高度 + 地形高度变化量 double targetAltitude = flyHeight + terrainHeightDiff; // 平滑处理:如果高度变化不大,沿用上一个飞行高度 if (i > 0 && Math.abs(targetAltitude - lastFlightAltitude) < smoothThreshold) { targetAltitude = lastFlightAltitude; } // 设置高度 point.getUavPosition().setAltitude(targetAltitude); updateActionAltitude(point, currentTerrainHeight); lastFlightAltitude = targetAltitude; // 更新最后飞行高度 } renumberWaypoints(points); return points; } // 辅助方法:两点间插值 private List<ArdUavPositionDTO> interpolateBetween(ArdUavPositionDTO start, ArdUavPositionDTO end) { List<ArdUavPositionDTO> interpolated = new ArrayList<>(); // 计算两点间的距离 Vertex startV=new Vertex( start.getUavPosition().getLatitude(), start.getUavPosition().getLongitude()); Vertex endV=new Vertex( end.getUavPosition().getLatitude(), end.getUavPosition().getLongitude()); double distance = GeoTools.DistanceSphere(startV,endV); // 如果距离小于插值间隔,则直接返回 if (distance <= intervalMeter) return interpolated; // 计算插值步数 int steps = (int) (distance / intervalMeter); for (int i = 1; i < steps; i++) { double ratio = i * intervalMeter / distance; double lat = interpolate( start.getUavPosition().getLatitude(), end.getUavPosition().getLatitude(), ratio ); double lon = interpolate( start.getUavPosition().getLongitude(), end.getUavPosition().getLongitude(), ratio ); ArdUavPositionDTO newPoint = deepCopy(start, ArdUavPositionDTO.class); newPoint.setType(1); newPoint.getUavPosition().setLatitude(lat); newPoint.getUavPosition().setLongitude(lon); interpolated.add(newPoint); } return interpolated; } /** * 更新航点动作中的地面高度 * * @param point 航点对象 * @param terrainHeight 当前地形高度 */ private void updateActionAltitude(ArdUavPositionDTO point, double terrainHeight) { if (point.getActionGroup() == null || point.getActionGroup().isEmpty()) { return; } for (ArdActionDTO action : point.getActionGroup()) { // 如果已有地面位置,则更新高度 if (action.getGroundPosition() != null) { action.getGroundPosition().setLongitude(point.getUavPosition().getLongitude()); action.getGroundPosition().setLatitude(point.getUavPosition().getLatitude()); action.getGroundPosition().setAltitude(terrainHeight); } // 如果没有地面位置,则创建新的地面位置 else { action.setGroundPosition(ArdPositionDTO.builder() .longitude(point.getUavPosition().getLongitude()) .latitude(point.getUavPosition().getLatitude()) .altitude(terrainHeight) .build()); } } } /** * 重新编号航点和动作 * * @param points 航点列表 */ private void renumberWaypoints(List<ArdUavPositionDTO> points) { // 航点重新编号 for (int i = 0; i < points.size(); i++) { ArdUavPositionDTO point = points.get(i); point.setIndex(i + 1); // 通常从1开始编号 // 动作重新编号 if (point.getActionGroup() != null && !point.getActionGroup().isEmpty()) { int actionIndex = 1; for (ArdActionDTO action : point.getActionGroup()) { action.setActionIndex(actionIndex++); } } } } /** * 校验航线数据有效性 * * @param ardWaylineDTO 航线数据 * @throws IllegalArgumentException 如果数据无效 */ private void validateWaylineData(ArdWaylineDTO ardWaylineDTO) { if (ardWaylineDTO == null) { throw new IllegalArgumentException("航线数据不能为null"); } if (ardWaylineDTO.getUavPositions() == null || ardWaylineDTO.getUavPositions().size() < 2) { throw new IllegalArgumentException("航点数量必须大于等于2"); } if (ardWaylineDTO.getGlobalFlyHeight() <= 0) { throw new IllegalArgumentException("全局飞行高度必须大于0"); } } // 通用深拷贝 public static <T> T deepCopy(T original, Class<T> clazz) { if (original == null) { return null; } try { byte[] bytes = mapper.writeValueAsBytes(original); return mapper.readValue(bytes, clazz); } catch (Exception e) { e.printStackTrace(); return null; } } // 线性插值 public double interpolate(double start, double end, double ratio) { return start + (end - start) * ratio; } } ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdActionDTO.java
New file @@ -0,0 +1,53 @@ package com.ard.tianyu.domain.dto.wayline; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @Builder @AllArgsConstructor @NoArgsConstructor @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) public class ArdActionDTO { @JsonProperty("actionIndex") public Integer actionIndex; @JsonProperty("actionType") public Integer actionType; @JsonProperty("actionParameter") public Integer actionParameter; @JsonProperty("waitTime") public Integer waitTime; @JsonProperty("gimbalMode") @Builder.Default public Integer gimbalMode = 0; @JsonProperty("actionGimbal") public ArdGimbalDTO actionGimbal; @JsonProperty("zoomFactor") public Integer zoomFactor; @Builder.Default @JsonProperty("fieldAngle") public Float fieldAngle = 66.6F; @JsonProperty("groundPosition") public ArdPositionDTO groundPosition; //地面视场范围 @JsonProperty("groundViewRange") private List<ArdPositionDTO> groundViewRange; } ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdGimbalDTO.java
New file @@ -0,0 +1,27 @@ package com.ard.tianyu.domain.dto.wayline; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @AllArgsConstructor @NoArgsConstructor @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) public class ArdGimbalDTO { @JsonProperty("pitch") public Double pitch; @JsonProperty("yaw") public Double yaw; @JsonProperty("roll") public Double roll; } ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdPositionDTO.java
New file @@ -0,0 +1,30 @@ package com.ard.tianyu.domain.dto.wayline; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class ArdPositionDTO { /** * 经度 */ private Double longitude; /** * 纬度 */ private Double latitude; /** * 高度 */ @Builder.Default private Double altitude = 0.0; } ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdUavPositionDTO.java
New file @@ -0,0 +1,65 @@ package com.ard.tianyu.domain.dto.wayline; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class ArdUavPositionDTO { /** * 索引 */ private Integer Index; /** * 位置类型 * 0-用户输入 * 1-算法生成 */ @Builder.Default private Integer Type = 0; /** * 飞行路径模式 * 1-直线飞行(过点且到点停) * 2-协调转弯(提前转弯不过点) * 3-曲线飞行(到点停) * 4-曲线飞行(到点不停) */ @JsonProperty("flightPathMode") @Builder.Default private Integer flightPathMode = 1; /** * 偏航模式 */ @JsonProperty("headingMode") @Builder.Default private Integer headingMode = 0; /** * 点飞行速度 */ @JsonProperty("pointFlySpeed") private Float pointFlySpeed; /** * 无人机位置 */ @JsonProperty("uavPosition") private ArdPositionDTO uavPosition; /** * 动作组 */ @JsonProperty("actionGroup") private List<ArdActionDTO> actionGroup; } ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdWaylineDTO.java
New file @@ -0,0 +1,77 @@ package com.ard.tianyu.domain.dto.wayline; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @Builder @AllArgsConstructor @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class ArdWaylineDTO { /** * 航线id */ public String waylineId; /** * 航线名称 */ public String waylineName; /** * 航线类型 */ public Integer waylineType; /** * 无人机型号 */ private String uavModel; /** * 负载型号 */ private String payloadModel; /** * 航线飞行速度 */ public Float globalFlySpeed; /** * 航线飞行高度 */ public Float globalFlyHeight; /** * 航线长度 */ public Double waylineLength; /** * 用户航点数量 */ public Integer waylineUserPointCount; /** * 航点总数量 */ public Integer waylineAllPointCount; /** * 航线飞行时间 */ public Double waylineBaseFlyTime; /** * 航线航点集合 */ public List<ArdUavPositionDTO> uavPositions; } ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/WaylineFileDTO.java
New file @@ -0,0 +1,43 @@ package com.ard.tianyu.domain.dto.wayline; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; /** * @author sean * @version 0.3 * @date 2021/12/22 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class WaylineFileDTO { private String name; @JsonProperty("id") private String waylineId; private String droneModelKey; private String sign; private List<String> payloadModelKeys; private Boolean favorited; private List<Integer> templateTypes; private String objectKey; @JsonProperty("user_name") private String username; private Long updateTime; } ard-work/src/main/java/com/ard/tianyu/domain/entity/UavWayline.java
New file @@ -0,0 +1,80 @@ package com.ard.tianyu.domain.entity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import com.ard.common.annotation.Excel; import com.ard.common.core.domain.BaseEntity; /** * 航线对象 uav_wayline * * @author ruoyi * @date 2025-06-27 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class UavWayline extends BaseEntity { private static final long serialVersionUID = 1L; /** 主键 */ private String id; /** 航线名称 */ @Excel(name = "航线名称") private String name; /** 航线类型 */ @Excel(name = "航线类型") private Integer type; /** 无人机型号 */ @Excel(name = "无人机型号") private String uavModel; /** 负载型号 */ @Excel(name = "负载型号") private String payloadModel; /** 全局速度 */ @Excel(name = "全局速度") private Float globalSpeed; /** 全局高度 */ @Excel(name = "全局高度") private Float globalHeight; /** 用户航点数量 */ @Excel(name = "用户航点数量") private Integer userWaypointCount; /** 规划航点数量 */ @Excel(name = "规划航点数量") private Integer planWaypointCount; /** 原始航线数据 */ @Excel(name = "航线数据") private byte[] srcWaylineData; /** 规划航线数据 */ @Excel(name = "航线数据") private byte[] planWaylineData; /** kmz地址 */ @Excel(name = "kmz地址") private String kmzUrl; /** 用户ID */ @Excel(name = "用户ID") private Long userId; /** 部门ID */ @Excel(name = "部门ID") private Long deptId; } ard-work/src/main/java/com/ard/tianyu/domain/enums/WaylineEnum.java
New file @@ -0,0 +1,76 @@ package com.ard.tianyu.domain.enums; public enum WaylineEnum { //航向 coordinateTurn(1,"coordinateTurn"), toPointAndStopWithDiscontinuityCurvature(2,"toPointAndStopWithDiscontinuityCurvature"), toPointAndStopWithContinuityCurvature(3,"toPointAndStopWithContinuityCurvature"), toPointAndPassWithContinuityCurvature(4,"toPointAndPassWithContinuityCurvature"), UNKNOWN(-1, "unknown"); private int val; private String desc; WaylineEnum(int val, String desc) { this.val = val; this.desc = desc; } public int getVal() { return val; } public static String getDesc(int val) { if (coordinateTurn.val == val) { return coordinateTurn.desc; } if (toPointAndStopWithDiscontinuityCurvature.val == val) { return toPointAndStopWithDiscontinuityCurvature.desc; } if (toPointAndStopWithContinuityCurvature.val == val) { return toPointAndStopWithContinuityCurvature.desc; } if (toPointAndPassWithContinuityCurvature.val == val) { return toPointAndPassWithContinuityCurvature.desc; } return UNKNOWN.desc; } public static int getVal(String desc) { if (coordinateTurn.desc.equals(desc)) { return coordinateTurn.val; } if (toPointAndStopWithDiscontinuityCurvature.desc.equals(desc)) { return toPointAndStopWithDiscontinuityCurvature.val; } if (toPointAndStopWithContinuityCurvature.desc.equals(desc)) { return toPointAndStopWithContinuityCurvature.val; } if (toPointAndPassWithContinuityCurvature.desc.equals(desc)) { return toPointAndPassWithContinuityCurvature.val; } return UNKNOWN.val; } } ard-work/src/main/java/com/ard/tianyu/mapper/UavWaylineMapper.java
New file @@ -0,0 +1,71 @@ package com.ard.tianyu.mapper; import com.ard.common.core.domain.entity.SysUser; import com.ard.tianyu.domain.entity.UavWayline; import java.util.List; /** * 航线Mapper接口 * * @author ruoyi * @date 2025-06-27 */ public interface UavWaylineMapper { /** * 查询航线 * * @param id 航线主键 * @return 航线 */ public UavWayline selectUavWaylineById(String id); /** * 查询航线列表 * * @param uavWayline 航线 * @return 航线集合 */ public List<UavWayline> selectUavWaylineList(UavWayline uavWayline); /** * 新增航线 * * @param uavWayline 航线 * @return 结果 */ public int insertUavWayline(UavWayline uavWayline); /** * 修改航线 * * @param uavWayline 航线 * @return 结果 */ public int updateUavWayline(UavWayline uavWayline); /** * 删除航线 * * @param id 航线主键 * @return 结果 */ public int deleteUavWaylineById(String id); /** * 批量删除航线 * * @param ids 需要删除的数据主键集合 * @return 结果 */ public int deleteUavWaylineByIds(String[] ids); /** * 校验航线是否唯一 * * @param name 航线名称 * @return 结果 */ public UavWayline checkUavWaylineNameUnique(String name); } ard-work/src/main/java/com/ard/tianyu/service/IGeoToolsService.java
New file @@ -0,0 +1,16 @@ package com.ard.tianyu.service; import com.ard.tianyu.domain.dto.wayline.ArdPositionDTO; import com.ard.tianyu.util.Vertex; import java.util.List; public interface IGeoToolsService { Vertex gcj02ToTargetCoordinate(Vertex gcjVertex); Vertex targetCoordinateToGcj02(Vertex targetVertex); List<ArdPositionDTO> calConvexHull(List<ArdPositionDTO> polygonPoints); } ard-work/src/main/java/com/ard/tianyu/service/IKmzFileService.java
New file @@ -0,0 +1,13 @@ package com.ard.tianyu.service; import com.ard.tianyu.domain.dto.wayline.ArdWaylineDTO; /** * @author sean * @version 0.3 * @date 2021/12/22 */ public interface IKmzFileService { void outputKmz(ArdWaylineDTO ardWaylineDTO); } ard-work/src/main/java/com/ard/tianyu/service/IUavWaylineService.java
New file @@ -0,0 +1,71 @@ package com.ard.tianyu.service; import java.util.List; import com.ard.tianyu.domain.dto.wayline.ArdWaylineDTO; import com.ard.tianyu.domain.entity.UavWayline; /** * 航线Service接口 * * @author ruoyi * @date 2025-06-27 */ public interface IUavWaylineService { /** * 查询航线 * * @param id 航线主键 * @return 航线 */ public ArdWaylineDTO selectUavWaylineById(String id); /** * 查询航线列表 * * @param uavWayline 航线 * @return 航线集合 */ public List<UavWayline> selectUavWaylineList(UavWayline uavWayline); /** * 新增航线 * * @param ardWaylineDTO 航线 * @return 结果 */ public int insertUavWayline(ArdWaylineDTO ardWaylineDTO); /** * 修改航线 * * @param ardWaylineDTO 航线 * @return 结果 */ public int updateUavWayline(ArdWaylineDTO ardWaylineDTO); /** * 批量删除航线 * * @param ids 需要删除的航线主键集合 * @return 结果 */ public int deleteUavWaylineByIds(String[] ids); /** * 删除航线信息 * * @param id 航线主键 * @return 结果 */ public int deleteUavWaylineById(String id); /** * 校验航线是否唯一 * * @param ardWaylineDTO 航线信息 * @return 结果 */ public boolean checkWaylineNameUnique(ArdWaylineDTO ardWaylineDTO); } ard-work/src/main/java/com/ard/tianyu/service/impl/GeoToolsServiceImpl.java
New file @@ -0,0 +1,158 @@ package com.ard.tianyu.service.impl; import com.ard.tianyu.config.GeoserverConfig; import com.ard.tianyu.domain.dto.wayline.ArdPositionDTO; import com.ard.tianyu.service.IGeoToolsService; import com.ard.tianyu.util.GeoTools; import com.ard.tianyu.util.Vertex; import lombok.extern.slf4j.Slf4j; import org.geotools.data.DefaultTransaction; import org.geotools.data.Transaction; import org.geotools.data.collection.ListFeatureCollection; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.simple.SimpleFeatureStore; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geometry.jts.JTSFactoryFinder; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.locationtech.jts.geom.*; import org.locationtech.jts.operation.buffer.BufferParameters; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @Service @Slf4j public class GeoToolsServiceImpl implements IGeoToolsService { @Autowired private GeoserverConfig geoserverConfig; private static Double distance = 1500 * 180 / Math.PI / 6371000; private static GeometryFactory geoFactory = new GeometryFactory(); //坐标类型转换 public Vertex gcj02ToTargetCoordinate(Vertex gcjVertex){ Vertex newVertex = new Vertex(0,0); if(geoserverConfig.getDeviceCoordinateType().equals("WGS84")){ newVertex = GeoTools.GCJ02toWGS84(new Vertex(gcjVertex.x, gcjVertex.y)); }else { newVertex = new Vertex(gcjVertex.x, gcjVertex.y); } return newVertex; } public Vertex targetCoordinateToGcj02(Vertex targetVertex){ Vertex newVertex = new Vertex(0,0); if(geoserverConfig.getDeviceCoordinateType().equals("WGS84")){ newVertex = GeoTools.WGS84toGCJ02(new Vertex(targetVertex.x, targetVertex.y)); }else { newVertex = new Vertex(targetVertex.x, targetVertex.y); } return newVertex; } public List<ArdPositionDTO> calConvexHull(List<ArdPositionDTO> polygonPoints){ //缓冲距离 List<ArdPositionDTO> convexHullPoints = new ArrayList<>(); try{ //用户定义区域 Integer pCount = polygonPoints.size(); //首位最后一个点 Coordinate[] uPoints = new Coordinate[pCount+1]; int i = 0; for(i=0;i<pCount;i++){ uPoints[i] = new Coordinate(polygonPoints.get(i).getLongitude(),polygonPoints.get(i).getLatitude()); } //将第一个点重新加入到最后,形成闭环 uPoints[i] = new Coordinate(polygonPoints.get(0).getLongitude(),polygonPoints.get(0).getLatitude()); //多边形点的个数必须大于等于3 if(uPoints.length >= 3){ //生成集合要素 Geometry oneGeometry = geoFactory.createPolygon(uPoints); //做凸包 Geometry geoConvexHull = oneGeometry.convexHull(); Coordinate[] points = geoConvexHull.getCoordinates(); for (Coordinate coordinate : points) { convexHullPoints.add(ArdPositionDTO.builder().longitude(coordinate.getX()) .latitude(coordinate.getY()).build()); } } }catch (Exception e){ e.printStackTrace(); } return convexHullPoints; } private double meterToDegree(double threshold){ return threshold * 180 / Math.PI / 6371000; } public void deleteFile(File filePath) { //判断路径是否存在 if(filePath.exists()) { //判断是否是目录 if (filePath.isDirectory()) { //将目录逐层拆分成数组元素 File[] files = filePath.listFiles(); for (File f : files) { //递归调用,由最底层往上逐层删除 deleteFile(f); } } //如果是文件,直接删除 filePath.delete(); } } public void zipShapeFile(String shpPath){ try{ File shpFile = new File(shpPath); String shpRoot = shpFile.getParentFile().getPath(), _shpName = shpFile.getName(), shpName = _shpName.substring(0, _shpName.lastIndexOf(".")); String zipPath = shpRoot+File.separator+shpName+".zip"; File zipFile = new File(zipPath); InputStream input = null; ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile)); // zip的名称为 zipOut.setComment(shpName); String[] shpFiles = new String[]{ shpRoot+File.separator+shpName+".dbf", shpRoot+File.separator+shpName+".prj", shpRoot+File.separator+shpName+".shp", shpRoot+File.separator+shpName+".shx", }; for(int i=0;i<shpFiles.length;i++){ File _file = new File(shpFiles[i]); input = new FileInputStream(_file); zipOut.putNextEntry(new ZipEntry(_file.getName())); int temp = 0; while ((temp = input.read()) != -1) { zipOut.write(temp); } input.close(); } zipOut.close(); }catch (Exception e){ e.printStackTrace(); } } } ard-work/src/main/java/com/ard/tianyu/service/impl/KmzFileServiceImpl.java
New file @@ -0,0 +1,517 @@ package com.ard.tianyu.service.impl; import com.ard.tianyu.component.minio.config.MinioConfig; import com.ard.tianyu.component.minio.utils.MinioUtil; import com.ard.tianyu.config.WaylinePlaningConfig; import com.ard.tianyu.domain.dto.wayline.*; import com.ard.tianyu.domain.entity.UavWayline; import com.ard.tianyu.domain.enums.WaylineEnum; import com.ard.tianyu.mapper.UavWaylineMapper; import com.ard.tianyu.service.IGeoToolsService; import com.ard.tianyu.service.IKmzFileService; import com.ard.tianyu.util.Vertex; import lombok.extern.slf4j.Slf4j; import org.dom4j.*; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.io.*; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * @author liusuyi * @version 3.0 * @date 2025/06/27 */ @Service @Slf4j @Transactional public class KmzFileServiceImpl implements IKmzFileService { @Resource private MinioConfig minioConfig; @Resource private IGeoToolsService geoToolsService; @Resource private WaylinePlaningConfig waylinePlaningConfig; @Resource private UavWaylineMapper uavWaylineMapper; private Document initWayline(Boolean isTemplate) { //创建Document对象 Document document = DocumentHelper.createDocument(); document.setXMLEncoding("UTF-8"); // 定义命名空间 String KML_NAMESPACE = "http://www.opengis.net/kml/2.2"; String WPML_NAMESPACE = "http://www.dji.com/wpmz/1.0.6"; //创建kml节点 QName kmlQName = QName.get("kml", Namespace.get("", KML_NAMESPACE)); Element kml = document.addElement(kmlQName); //添加命名空间 kml.addNamespace("wpml", WPML_NAMESPACE); //生成Document节点 Element wpml = kml.addElement("Document", KML_NAMESPACE); //是否为模板 if (isTemplate) { // Step 1: Implement File Creation Information //wpml.addElement("wpml:author").addText(user); wpml.addElement("wpml:createTime").addText( String.valueOf(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli())); wpml.addElement("wpml:updateTime").addText( String.valueOf(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli())); } //返回 return document; } //产生kmz总体过程 private void gntTemplateKml(ArdWaylineDTO ardWaylineDTO, String savePath) { //初始化文件头 Document reader = this.initWayline(true); //Document元素 Element document = reader.getRootElement().element("Document"); //添加missionConfig信息 this.addMissionConfig(document, ardWaylineDTO); //添加folder信息 Element folder = this.addFolder(document, ardWaylineDTO); //2设置航点参数 List<ArdUavPositionDTO> uavPositions = ardWaylineDTO.getUavPositions(); //循环添加placemark Integer actionGroupId = 0; for (int i = 0; i < uavPositions.size(); i++) { ArdUavPositionDTO onePosition = uavPositions.get(i); this.addPlaceMark(folder, i, actionGroupId, onePosition); if (onePosition.getActionGroup().size() > 0) { actionGroupId += 1; } } //打印输出结果 //log.info(reader.asXML()); //输出 String tmpKml = savePath + File.separator + "wpmz" + File.separator + "template.kml"; this.outputXml(tmpKml, reader); } public void gntWaylineWpml(ArdWaylineDTO ardWaylineDTO, String savePath) { //初始化文件头 Document reader = this.initWayline(false); //Document元素 Element document = reader.getRootElement().element("Document"); //添加missionConfig信息 this.addMissionConfig(document, ardWaylineDTO); //添加folder信息 Element folder = this.addFolderWayline(document, ardWaylineDTO); //2设置航点参数 //循环添加placemark Integer actionGroupId = 0; for (int i = 0; i < ardWaylineDTO.getUavPositions().size(); i++) { ArdUavPositionDTO onePosition = ardWaylineDTO.getUavPositions().get(i); this.addPlaceMarkWayline(folder, i, actionGroupId, onePosition, ardWaylineDTO); if (onePosition.getActionGroup().size() > 0) { actionGroupId += 1; } } //打印输出结果 //log.info(reader.asXML()); //输出 String waylineWpml = savePath + File.separator + "wpmz" + File.separator + "waylines.wpml"; this.outputXml(waylineWpml, reader); } //missionConfig信息 private void addMissionConfig(Element document, ArdWaylineDTO ardWaylineDTO) { //创建wpml:missionConfig元素 Element missionConfig = document.addElement("wpml:missionConfig"); //创建子元素 missionConfig.addElement("wpml:flyToWaylineMode").addText("safely"); missionConfig.addElement("wpml:finishAction").addText("goHome"); missionConfig.addElement("wpml:exitOnRCLost").addText("goContinue"); missionConfig.addElement("wpml:executeRCLostAction").addText("goBack"); missionConfig.addElement("wpml:takeOffSecurityHeight").addText("30"); //与第一个点重合 ArdPositionDTO firstPoint = ardWaylineDTO.getUavPositions().get(0).getUavPosition(); String refPoint = String.format("%s,%s,%s", firstPoint.getLatitude(), firstPoint.getLongitude() , firstPoint.getAltitude()); missionConfig.addElement("wpml:takeOffRefPoint").addText(refPoint); missionConfig.addElement("wpml:globalTransitionalSpeed").addText(String.valueOf(ardWaylineDTO.getGlobalFlySpeed())); missionConfig.addElement("wpml:globalRTHHeight").addText(String.valueOf(ardWaylineDTO.getGlobalFlyHeight())); //添加droneInfo Element droneInfo = missionConfig.addElement("wpml:droneInfo"); droneInfo.addElement("wpml:droneEnumValue").addText("89"); droneInfo.addElement("wpml:droneSubEnumValue").addText("1"); //添加payloadInfo Element payloadInfo = missionConfig.addElement("wpml:payloadInfo"); payloadInfo.addElement("wpml:payloadEnumValue").addText("43"); payloadInfo.addElement("wpml:payloadSubEnumValue").addText("0"); payloadInfo.addElement("wpml:payloadPositionIndex").addText("0"); } //folder信息 private Element addFolder(Element document, ArdWaylineDTO ardWaylineDTO) { //全局信息 Element folder = document.addElement("Folder"); folder.addElement("wpml:templateType").addText("waypoint"); folder.addElement("wpml:templateId").addText("0"); //坐标系统 Element waylineCoordinateSysParam = folder.addElement("wpml:waylineCoordinateSysParam"); waylineCoordinateSysParam.addElement("wpml:coordinateMode").addText("WGS84"); waylineCoordinateSysParam.addElement("wpml:heightMode").addText("relativeToStartPoint"); // folder.addElement("wpml:autoFlightSpeed").addText(String.valueOf(ardWaylineDTO.getGlobalFlySpeed())); folder.addElement("wpml:globalWaypointTurnMode").addText("toPointAndStopWithDiscontinuityCurvature"); folder.addElement("wpml:globalUseStraightLine").addText("1"); //全局航向角模式 Element globalWaypointHeadingParam = folder.addElement("wpml:globalWaypointHeadingParam"); globalWaypointHeadingParam.addElement("wpml:waypointHeadingMode").addText("followWayline"); globalWaypointHeadingParam.addElement("wpml:waypointHeadingYawPathMode").addText("followBadArc"); // folder.addElement("wpml:gimbalPitchMode").addText("manual");//manual , usePointSetting folder.addElement("wpml:globalHeight").addText(String.valueOf(ardWaylineDTO.getGlobalFlyHeight())); // return folder; } private Element addFolderWayline(Element document, ArdWaylineDTO ardWaylineDTO) { //全局信息 Element folder = document.addElement("Folder"); folder.addElement("wpml:templateId").addText("0"); folder.addElement("wpml:executeHeightMode").addText("relativeToStartPoint"); folder.addElement("wpml:waylineId").addText("0"); folder.addElement("wpml:autoFlightSpeed").addText(String.valueOf(ardWaylineDTO.getGlobalFlySpeed())); return folder; } //placemark信息 private void addPlaceMark(Element folder, Integer index, Integer actionGroupId, ArdUavPositionDTO onePostion) { //添加placemark Element placemark = folder.addElement("Placemark"); //转换坐标 //航点坐标位置转换 Double tLng = 0.0; Double tLat = 0.0; //转换坐标,将目标坐标转化为高德坐标 if (onePostion.getUavPosition().getLongitude() != null && onePostion.getUavPosition().getLatitude() != null) { Vertex newVertex = geoToolsService.gcj02ToTargetCoordinate( new Vertex(onePostion.getUavPosition().getLongitude(), onePostion.getUavPosition().getLatitude())); tLng = newVertex.x; tLat = newVertex.y; } Element point = placemark.addElement("Point"); point.addElement("coordinates").addText(tLng + "," + tLat); //添加一般元素 placemark.addElement("wpml:index").addText(String.valueOf(index)); placemark.addElement("wpml:ellipsoidHeight").addText(String.valueOf(onePostion.getUavPosition().getAltitude())); placemark.addElement("wpml:height").addText(String.valueOf(onePostion.getUavPosition().getAltitude())); //全局信息 placemark.addElement("wpml:useGlobalHeight").addText("1"); placemark.addElement("wpml:useGlobalSpeed").addText("1"); placemark.addElement("wpml:useGlobalHeadingParam").addText("1"); placemark.addElement("wpml:useGlobalTurnParam").addText("1"); placemark.addElement("wpml:useStraightLine").addText("1"); //添加actionGroup if (onePostion.getActionGroup().size() > 0) { Element actionGroup = placemark.addElement("wpml:actionGroup"); //添加动作组基础信息 actionGroup.addElement("wpml:actionGroupId").addText(String.valueOf(actionGroupId)); actionGroup.addElement("wpml:actionGroupStartIndex").addText(String.valueOf(index)); actionGroup.addElement("wpml:actionGroupEndIndex").addText(String.valueOf(index)); actionGroup.addElement("wpml:actionGroupMode").addText("sequence"); Element actionTrigger = actionGroup.addElement("wpml:actionTrigger"); actionTrigger.addElement("wpml:actionTriggerType").addText("reachPoint"); //定义action Integer actionId = 0; for (int j = 0; j < onePostion.getActionGroup().size(); j++) { //逐个添加action Integer actionCount = this.addAction(actionGroup, actionId, onePostion.getActionGroup().get(j)); actionId += actionCount; } } } //placemark信息 private void addPlaceMarkWayline(Element folder, Integer index, Integer actionGroupId, ArdUavPositionDTO onePostion, ArdWaylineDTO ardWaylineDTO) { //添加placemark Element placemark = folder.addElement("Placemark"); //转换坐标 //航点坐标位置转换 Double tLng = 0.0; Double tLat = 0.0; //转换坐标,将目标坐标转化为高德坐标 if (onePostion.getUavPosition().getLongitude() != null && onePostion.getUavPosition().getLatitude() != null) { Vertex newVertex = geoToolsService.gcj02ToTargetCoordinate( new Vertex(onePostion.getUavPosition().getLongitude(), onePostion.getUavPosition().getLatitude())); tLng = newVertex.x; tLat = newVertex.y; } Element point = placemark.addElement("Point"); point.addElement("coordinates").addText(tLng + "," + tLat); //添加一般元素 placemark.addElement("wpml:index").addText(String.valueOf(index)); placemark.addElement("wpml:executeHeight").addText(String.valueOf(onePostion.getUavPosition().getAltitude())); placemark.addElement("wpml:waypointSpeed").addText(String.valueOf(ardWaylineDTO.getGlobalFlySpeed())); Element waypointHeadingParam = placemark.addElement("wpml:waypointHeadingParam"); waypointHeadingParam.addElement("wpml:waypointHeadingMode").addText("followWayline"); Element waypointTurnParam = placemark.addElement("wpml:waypointTurnParam"); waypointTurnParam.addElement("wpml:waypointTurnMode").addText(WaylineEnum.getDesc(onePostion.getFlightPathMode())); waypointTurnParam.addElement("wpml:waypointTurnDampingDist").addText("0"); //添加actionGroup if (onePostion.getActionGroup().size() > 0) { Element actionGroup = placemark.addElement("wpml:actionGroup"); //添加动作组基础信息 actionGroup.addElement("wpml:actionGroupId").addText(String.valueOf(actionGroupId)); actionGroup.addElement("wpml:actionGroupStartIndex").addText(String.valueOf(index)); actionGroup.addElement("wpml:actionGroupEndIndex").addText(String.valueOf(index)); actionGroup.addElement("wpml:actionGroupMode").addText("sequence"); Element actionTrigger = actionGroup.addElement("wpml:actionTrigger"); actionTrigger.addElement("wpml:actionTriggerType").addText("reachPoint"); //定义action Integer actionId = 0; for (int j = 0; j < onePostion.getActionGroup().size(); j++) { //逐个添加action Integer actionCount = this.addAction(actionGroup, actionId, onePostion.getActionGroup().get(j)); actionId += actionCount; } } } //action private Integer addAction(Element actionGroup, Integer index, ArdActionDTO actionDTO) { Integer actionCount = 2; //变焦动作 Element actionGimbal = actionGroup.addElement("wpml:action"); actionGimbal.addElement("wpml:actionId").addText(String.valueOf(index)); actionGimbal.addElement("wpml:actionActuatorFunc").addText("gimbalRotate"); Element param = actionGimbal.addElement("wpml:actionActuatorFuncParam"); param.addElement("wpml:payloadPositionIndex").addText(String.valueOf(0)); param.addElement("wpml:gimbalHeadingYawBase").addText("north"); param.addElement("wpml:gimbalRotateMode").addText("absoluteAngle"); param.addElement("wpml:gimbalPitchRotateEnable").addText(String.valueOf(1)); param.addElement("wpml:gimbalPitchRotateAngle").addText(String.valueOf(actionDTO.getActionGimbal().getPitch())); param.addElement("wpml:gimbalRollRotateEnable").addText(String.valueOf(0)); param.addElement("wpml:gimbalRollRotateAngle").addText(String.valueOf(0)); param.addElement("wpml:gimbalYawRotateEnable").addText(String.valueOf(1)); param.addElement("wpml:gimbalYawRotateAngle").addText(String.valueOf(actionDTO.getActionGimbal().getYaw())); param.addElement("wpml:gimbalRotateTimeEnable").addText(String.valueOf(0)); param.addElement("wpml:gimbalRotateTime").addText(String.valueOf(0)); //动作类型 switch (actionDTO.actionType) { case 0://拍照 Element action0 = actionGroup.addElement("wpml:action"); action0.addElement("wpml:actionId").addText(String.valueOf(index + 1)); action0.addElement("wpml:actionActuatorFunc").addText("takePhoto"); Element param0 = action0.addElement("wpml:actionActuatorFuncParam"); param0.addElement("wpml:fileSuffix").addText("point"); param0.addElement("wpml:payloadPositionIndex").addText(String.valueOf(0)); break; case 1://录像,拆解为3个动作 //添加开始录像动作 Element action11 = actionGroup.addElement("wpml:action"); action11.addElement("wpml:actionId").addText(String.valueOf(index + 1)); action11.addElement("wpml:actionActuatorFunc").addText("startRecord"); Element param11 = action11.addElement("wpml:actionActuatorFuncParam"); param11.addElement("wpml:fileSuffix").addText("point"); param11.addElement("wpml:payloadPositionIndex").addText(String.valueOf(0)); //添加新的悬停动作 Element action12 = actionGroup.addElement("wpml:action"); action12.addElement("wpml:actionId").addText(String.valueOf(index + 2)); action12.addElement("wpml:actionActuatorFunc").addText("hover"); Element param12 = action12.addElement("wpml:actionActuatorFuncParam"); param12.addElement("wpml:hoverTime").addText(String.valueOf(actionDTO.getActionParameter())); //添加结束动作 Element action13 = actionGroup.addElement("wpml:action"); action13.addElement("wpml:actionId").addText(String.valueOf(index + 3)); action13.addElement("wpml:actionActuatorFunc").addText("stopRecord"); Element param13 = action13.addElement("wpml:actionActuatorFuncParam"); param13.addElement("wpml:payloadPositionIndex").addText(String.valueOf(0)); actionCount = 5; break; case 2://悬停 ,协议是3,前端发送为2 Element action2 = actionGroup.addElement("wpml:action"); action2.addElement("wpml:actionId").addText(String.valueOf(index + 1)); action2.addElement("wpml:actionActuatorFunc").addText("hover"); Element param2 = action2.addElement("wpml:actionActuatorFuncParam"); param2.addElement("wpml:hoverTime").addText(String.valueOf(actionDTO.getWaitTime())); break; default: actionCount = 0; log.info("未知的动作类型{}!", actionDTO.actionType); break; } return actionCount; } //输出模板kml private void outputXml(String vpath, Document doc) { try { OutputFormat format = OutputFormat.createPrettyPrint(); format.setEncoding("UTF-8"); //FileWriter改为FileOutputStream解决乱码问题 XMLWriter xmlWriter = new XMLWriter(new FileOutputStream(vpath), format); xmlWriter.write(doc); xmlWriter.close(); } catch (Exception e) { log.error("输出航线文件失败:{}", vpath); e.printStackTrace(); } } //创建路径 private String createSavePath(String waylineName) { //创建航线名称路径 String pathStr = waylinePlaningConfig.getWaylineSavePath() + File.separator + waylineName; File savePath = new File(pathStr); if (!savePath.exists()) { savePath.mkdirs(); } else { deleteFile(savePath); savePath.mkdirs(); } //创建wpmz File wpmz = new File(pathStr + File.separator + "wpmz"); wpmz.mkdir(); //创建res File res = new File(pathStr + File.separator + "wpmz" + File.separator + "res"); res.mkdir(); return pathStr; } private void deleteFile(File filePath) { //判断路径是否存在 if (filePath.exists()) { //判断是否是目录 if (filePath.isDirectory()) { //将目录逐层拆分成数组元素 File[] files = filePath.listFiles(); for (File f : files) { //递归调用,由最底层往上逐层删除 deleteFile(f); } } //如果是文件,直接删除 filePath.delete(); } } //输出MKZ过程 @Override @Async public void outputKmz(ArdWaylineDTO ardWaylineDTO) { try { //创建目录 String savePath = this.createSavePath(ardWaylineDTO.getWaylineName()); //输出模板kml this.gntTemplateKml(ardWaylineDTO, savePath); //输出航线wpml this.gntWaylineWpml(ardWaylineDTO, savePath); //压缩输出成kmz String fileName = ardWaylineDTO.getWaylineName() + ".kmz"; String kmzPath = waylinePlaningConfig.getWaylineSavePath() + File.separator + fileName; //输出kmz文件,是否成功 Boolean isSuccess = this.toKmz(kmzPath, savePath + File.separator + "wpmz", true); if (isSuccess) { //上传kmz文件到对象数据库 InputStream inputStream = new FileInputStream(kmzPath); //构建记录 String objectKey = fileName; //若已经存在,则先删除,再添加 if (MinioUtil.exists(minioConfig.getBucketName(), objectKey)) MinioUtil.delete(minioConfig.getBucketName(), objectKey); String kmzUrl = MinioUtil.uploadInputStream(minioConfig.getBucketName(), objectKey, inputStream, inputStream.available(), "application/octet-stream"); // 更新航线kmz地址 UavWayline uavWayline = UavWayline.builder() .id(ardWaylineDTO.waylineId) .kmzUrl(kmzUrl) .build(); uavWaylineMapper.updateUavWayline(uavWayline); } } catch (Exception e) { e.printStackTrace(); log.info("生成kmz文件失败!"); } } private Boolean toKmz(String zipFileName, String sourceFileName, boolean KeepDirStructure) { Boolean result = true; long start = System.currentTimeMillis();//开始 ZipOutputStream zos = null; try { FileOutputStream fileOutputStream = new FileOutputStream(zipFileName); zos = new ZipOutputStream(fileOutputStream); File sourceFile = new File(sourceFileName); compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure); long end = System.currentTimeMillis();//结束 System.out.println("压缩完成,耗时:" + (end - start) + " 毫秒"); } catch (Exception e) { result = false; e.printStackTrace(); } finally { if (zos != null) { try { zos.close(); } catch (IOException e) { e.getStackTrace(); } } } return result; } private void compress(File sourceFile, ZipOutputStream zos, String name, boolean KeepDirStructure) throws Exception { byte[] buf = new byte[2 * 1024]; if (sourceFile.isFile()) { // 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字 zos.putNextEntry(new ZipEntry(name)); // copy文件到zip输出流中 int len; FileInputStream in = new FileInputStream(sourceFile); while ((len = in.read(buf)) != -1) { zos.write(buf, 0, len); } // Complete the entry zos.closeEntry(); in.close(); } else { File[] listFiles = sourceFile.listFiles(); if (listFiles == null || listFiles.length == 0) { // 需要保留原来的文件结构时,需要对空文件夹进行处理 if (KeepDirStructure) { // 空文件夹的处理 zos.putNextEntry(new ZipEntry(name + "/")); // 没有文件,不需要文件的copy zos.closeEntry(); } } else { for (File file : listFiles) { // 判断是否需要保留原来的文件结构 if (KeepDirStructure) { // 注意:file.getName()前面需要带上父文件夹的名字加一斜杠, // 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了 compress(file, zos, name + "/" + file.getName(), KeepDirStructure); } else { compress(file, zos, file.getName(), KeepDirStructure); } } } } } } ard-work/src/main/java/com/ard/tianyu/service/impl/UavWaylineServiceImpl.java
New file @@ -0,0 +1,278 @@ package com.ard.tianyu.service.impl; import com.ard.common.constant.UserConstants; import com.ard.common.core.domain.entity.SysUser; import com.ard.common.utils.DateUtils; import com.ard.common.utils.StringUtils; import com.ard.common.utils.uuid.IdUtils; import com.ard.tianyu.controller.wayline.terrain.TerrainFollowingPlanner; import com.ard.tianyu.domain.dto.wayline.ArdUavPositionDTO; import com.ard.tianyu.domain.dto.wayline.ArdWaylineDTO; import com.ard.tianyu.domain.entity.UavWayline; import com.ard.tianyu.mapper.UavWaylineMapper; import com.ard.tianyu.service.IUavWaylineService; import com.ard.tianyu.service.IKmzFileService; import com.ard.tianyu.util.GeoTools; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.IOException; import java.util.List; import java.util.Optional; /** * 航线Service业务层处理 * * @author ruoyi * @date 2025-06-27 */ @Slf4j @Service public class UavWaylineServiceImpl implements IUavWaylineService { ObjectMapper mapper = new ObjectMapper(); @Resource private UavWaylineMapper uavWaylineMapper; @Resource private IKmzFileService waylineFileService; @Resource private TerrainFollowingPlanner terrainFollowingPlanner; /** * 查询航线 * * @param id 航线主键 * @return 航线 */ @Override public ArdWaylineDTO selectUavWaylineById(String id) { UavWayline uavWayline = uavWaylineMapper.selectUavWaylineById(id); return EntityConverterTowaylineDTO(uavWayline); } /** * 查询航线列表 * * @param uavWayline 航线 * @return 航线 */ @Override public List<UavWayline> selectUavWaylineList(UavWayline uavWayline) { return uavWaylineMapper.selectUavWaylineList(uavWayline); } /** * 新增航线 * * @param ardWaylineDTO 航线 * @return 结果 */ @Override public int insertUavWayline(ArdWaylineDTO ardWaylineDTO) { UavWayline uavWayline = waylineDTOConverterToEntity(ardWaylineDTO); uavWayline.setCreateTime(DateUtils.getNowDate()); int row = uavWaylineMapper.insertUavWayline(uavWayline); if (row > 0) { waylineFileService.outputKmz(ardWaylineDTO); } return row; } /** * 修改航线 * * @param ardWaylineDTO 航线 * @return 结果 */ @Override public int updateUavWayline(ArdWaylineDTO ardWaylineDTO) { UavWayline uavWayline = waylineDTOConverterToEntity(ardWaylineDTO); uavWayline.setUpdateTime(DateUtils.getNowDate()); int row = uavWaylineMapper.updateUavWayline(uavWayline); if (row > 0) { waylineFileService.outputKmz(ardWaylineDTO); } return row; } /** * 批量删除航线 * * @param ids 需要删除的航线主键 * @return 结果 */ @Override public int deleteUavWaylineByIds(String[] ids) { return uavWaylineMapper.deleteUavWaylineByIds(ids); } /** * 删除航线信息 * * @param id 航线主键 * @return 结果 */ @Override public int deleteUavWaylineById(String id) { return uavWaylineMapper.deleteUavWaylineById(id); } /** * 校验航线是否唯一 * * @param ardWaylineDTO 航线信息 * @return 结果 */ @Override public boolean checkWaylineNameUnique(ArdWaylineDTO ardWaylineDTO) { String waylineId = ardWaylineDTO.getWaylineId(); UavWayline info = uavWaylineMapper.checkUavWaylineNameUnique(ardWaylineDTO.getWaylineName()); if (StringUtils.isNotNull(info) && !info.getId().equals(waylineId)) { return UserConstants.NOT_UNIQUE; } return UserConstants.UNIQUE; } // DTO转换为实体类 private UavWayline waylineDTOConverterToEntity(ArdWaylineDTO ardWaylineDTO) { UavWayline uavWayline = UavWayline.builder() .id(Optional.ofNullable(ardWaylineDTO.getWaylineId()).orElse(IdUtils.simpleUUID())) .name(ardWaylineDTO.getWaylineName()) .type(ardWaylineDTO.getWaylineType()) .uavModel(ardWaylineDTO.getUavModel()) .payloadModel(ardWaylineDTO.getPayloadModel()) .userWaypointCount(ardWaylineDTO.waylineUserPointCount) .globalSpeed(ardWaylineDTO.getGlobalFlySpeed()) .globalHeight(ardWaylineDTO.getGlobalFlyHeight()) .build(); generateWaylineBytes(ardWaylineDTO, uavWayline); return uavWayline; } // 实体类转换DTO private ArdWaylineDTO EntityConverterTowaylineDTO(UavWayline uavWayline) { try { ArdWaylineDTO ardWaylineDTO = ArdWaylineDTO.builder() .waylineId(uavWayline.getId()) .waylineName(uavWayline.getName()) .waylineType(uavWayline.getType()) .uavModel(uavWayline.getUavModel()) .payloadModel(uavWayline.getPayloadModel()) .waylineUserPointCount(uavWayline.getUserWaypointCount()) .waylineAllPointCount(uavWayline.getPlanWaypointCount()) .globalFlySpeed(uavWayline.getGlobalSpeed()) .globalFlyHeight(uavWayline.getGlobalHeight()) .build(); byte[] planWaylineBytes = uavWayline.getPlanWaylineData(); ardWaylineDTO = mapper.readValue(planWaylineBytes, ArdWaylineDTO.class); //计算长度 double length = calculateWaylineLength(ardWaylineDTO); ardWaylineDTO.setWaylineLength(length); //计算飞行时间 double flyTime = calculateFlyTime(ardWaylineDTO); ardWaylineDTO.setWaylineBaseFlyTime(flyTime); return ardWaylineDTO; } catch (IOException e) { throw new RuntimeException("恢复航线对象错误!" + e.getMessage()); } } // 生成航线字节数组 private void generateWaylineBytes(ArdWaylineDTO ardWaylineDTO, UavWayline uavWayline) { try { ardWaylineDTO.setWaylineId(uavWayline.getId()); //二进制航线编码,保存原始航线数据 byte[] waylineBytes = mapper.writeValueAsBytes(ardWaylineDTO); uavWayline.setSrcWaylineData(waylineBytes); if (ardWaylineDTO.getWaylineType() == 1) { log.info("面向逐点的航线规划!"); byte[] newWaylineBytes = mapper.writeValueAsBytes(ardWaylineDTO); uavWayline.setPlanWaylineData(newWaylineBytes); uavWayline.setPlanWaypointCount(ardWaylineDTO.getWaylineUserPointCount()); } else if (ardWaylineDTO.getWaylineType() == 3) { log.info("面向仿地的航线规划!"); //仿地飞行算法 ArdWaylineDTO newWayline = terrainFollowingWayline(ardWaylineDTO); byte[] newWaylineBytes = mapper.writeValueAsBytes(newWayline); uavWayline.setPlanWaylineData(newWaylineBytes); uavWayline.setPlanWaypointCount(newWayline.getWaylineUserPointCount()); } else { log.error("航线设置错误:{}", ardWaylineDTO.getWaylineType()); throw new RuntimeException("航线类型错误" + ardWaylineDTO.getWaylineType()); } } catch (Exception e) { log.error("航线规划失败:{}", e.getMessage()); throw new RuntimeException("航线规划失败!" + e.getMessage()); } } //调用仿地航线算法 private ArdWaylineDTO terrainFollowingWayline(ArdWaylineDTO ardWaylineDTO) throws Exception { ArdWaylineDTO newWaylineDTO = deepCopyWayline(ardWaylineDTO); //使用仿地飞行规划器 List<ArdUavPositionDTO> ardUavPositionDTOS = terrainFollowingPlanner.interpolateWithTerrain(ardWaylineDTO); newWaylineDTO.setWaylineAllPointCount(ardUavPositionDTOS.size());//总航点数量 long userPointCount = ardUavPositionDTOS.stream() .filter(p -> p.getType() == 0) .count(); newWaylineDTO.setWaylineUserPointCount((int) userPointCount);//用户航点数量 newWaylineDTO.setUavPositions(ardUavPositionDTOS); // 规划成功,返回结果 return newWaylineDTO; } private ArdWaylineDTO deepCopyWayline(ArdWaylineDTO ardWaylineDTO) { ArdWaylineDTO newWaylineDTO = null; try { byte[] waylineBytes = mapper.writeValueAsBytes(ardWaylineDTO); newWaylineDTO = mapper.readValue(waylineBytes, ArdWaylineDTO.class); } catch (JsonProcessingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return newWaylineDTO; } //计算航线长度 public double calculateWaylineLength( ArdWaylineDTO ardWaylineDTO) { List<ArdUavPositionDTO> uavPositions = ardWaylineDTO.getUavPositions(); if (uavPositions == null || uavPositions.size() < 2) { return 0.0; } double totalLength = 0.0; for (int i = 1; i < uavPositions.size(); i++) { ArdUavPositionDTO prev = uavPositions.get(i - 1); ArdUavPositionDTO curr = uavPositions.get(i); totalLength += GeoTools.calculateDistance( prev.getUavPosition().getLatitude(), prev.getUavPosition().getLongitude(), curr.getUavPosition().getLatitude(), curr.getUavPosition().getLongitude() ); } return totalLength; } //计算飞行时间 public double calculateFlyTime(ArdWaylineDTO ardWaylineDTO) { double totalLength = calculateWaylineLength(ardWaylineDTO); // 单位:米 Float globalSpeed = ardWaylineDTO.getGlobalFlySpeed(); // 单位:米/秒 if (globalSpeed == null || globalSpeed <= 0) { throw new IllegalArgumentException("全局速度无效,必须大于0"); } return totalLength / globalSpeed; // 返回时间,单位:秒 } } ard-work/src/main/java/com/ard/tianyu/util/CameraUtil.java
@@ -66,10 +66,4 @@ } return zoomFactor; } public static void main(String[] args) { Float fieldAngleFromZoom = getFieldAngleFromZoom(1, 3F); System.out.println(fieldAngleFromZoom); } } ard-work/src/main/java/com/ard/tianyu/util/GeoTools.java
New file @@ -0,0 +1,98 @@ package com.ard.tianyu.util; public class GeoTools { public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) { final double R = 6371000; // 地球半径,单位:米 double radLat1 = Math.toRadians(lat1); double radLat2 = Math.toRadians(lat2); double deltaLat = radLat2 - radLat1; double deltaLon = Math.toRadians(lon2 - lon1); double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } // 经纬度坐标球面距离计算 public static double DistanceSphere(Vertex v1, Vertex v2) { double radLat1 = Math.toRadians(v1.y); double radLat2 = Math.toRadians(v2.y); double a = Math.abs(radLat1 - radLat2); double b = Math.abs(Math.toRadians(v1.x) - Math.toRadians(v2.x)); double h = Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2); double earthRadius = 6371000; return 2 * earthRadius * Math.asin(Math.sqrt(h)); } /* * 高德坐标系转wgs84 */ public static Vertex GCJ02toWGS84(Vertex v_gcj) { Vertex temp = WGS84toGCJ02(v_gcj); double lng_wgs = v_gcj.x * 2 - temp.x; double lat_wgs = v_gcj.y * 2 - temp.y; return new Vertex(lng_wgs, lat_wgs); } /* * wgs84转高德坐标系 */ public static Vertex WGS84toGCJ02(Vertex v_wgs) { // 一些常数 double a = 6378245.0; // 长半轴 double ee = 0.00669342162296594323; // 扁率 double lng = v_wgs.x; double lat = v_wgs.y; //判断是否在国内 if (OutOfChina(v_wgs)) return v_wgs; double dlat = transformlat(lng - 105.0, lat - 35.0); double dlng = transformlng(lng - 105.0, lat - 35.0); double radlat = lat / 180.0 * Math.PI; double magic = Math.sin(radlat); magic = 1 - ee * magic * magic; double sqrtmagic = Math.sqrt(magic); dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * Math.PI); dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * Math.PI); return new Vertex(lng + dlng, lat + dlat); } /* * 判断点(经纬度)是否在国内,不在国内不做偏移 */ public static boolean OutOfChina(Vertex v) { if (v.x < 72.004 || v.x > 137.8347) return true; if (v.y < 0.8293 || v.y > 55.8271) return true; return false; } public static double transformlat(double lng, double lat) { double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lat * Math.PI) + 40.0 * Math.sin(lat / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(lat / 12.0 * Math.PI) + 320 * Math.sin(lat * Math.PI / 30.0)) * 2.0 / 3.0; return ret; } public static double transformlng(double lng, double lat) { double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lng * Math.PI) + 40.0 * Math.sin(lng / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(lng / 12.0 * Math.PI) + 300.0 * Math.sin(lng / 30.0 * Math.PI)) * 2.0 / 3.0; return ret; } } ard-work/src/main/java/com/ard/tianyu/util/Vertex.java
New file @@ -0,0 +1,29 @@ package com.ard.tianyu.util; public class Vertex { public double x; public double y; public Vertex() { } public Vertex(double x, double y) { this.x = x; this.y = y; } // 判断这个点是不是和另外一个点重合(允许极小的误差,算作同一个点 public boolean IsSame(Vertex anotherV) { double almostZero = 0.00000001; if ((Math.abs(x - anotherV.x) < almostZero) && (Math.abs(y - anotherV.y) < almostZero)) return true; else return false; } // 直线距离 public double Distance(Vertex anotherV) { return Math.sqrt(Math.pow(x - anotherV.x, 2) + Math.pow(y - anotherV.y, 2)); } } ard-work/src/main/resources/mapper/UavWaylineMapper.xml
New file @@ -0,0 +1,161 @@ <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ard.tianyu.mapper.UavWaylineMapper"> <resultMap type="UavWayline" id="UavWaylineResult"> <result property="id" column="id" /> <result property="name" column="name" /> <result property="type" column="type" /> <result property="uavModel" column="uav_model" /> <result property="payloadModel" column="payload_model" /> <result property="globalSpeed" column="global_speed" /> <result property="globalHeight" column="global_height" /> <result property="userWaypointCount" column="user_waypoint_count" /> <result property="planWaypointCount" column="plan_waypoint_count" /> <result property="srcWaylineData" column="src_wayline_data" /> <result property="planWaylineData" column="plan_wayline_data" /> <result property="kmzUrl" column="kmz_url" /> <result property="userId" column="user_id" /> <result property="deptId" column="dept_id" /> <result property="createBy" column="create_by" /> <result property="createTime" column="create_time" /> <result property="updateBy" column="update_by" /> <result property="updateTime" column="update_time" /> <result property="remark" column="remark" /> </resultMap> <sql id="selectUavWaylineVo"> select id, name, type, uav_model, payload_model, global_speed, global_height, user_waypoint_count, plan_waypoint_count, src_wayline_data, plan_wayline_data, kmz_url, user_id, dept_id, create_by, create_time, update_by, update_time, remark from uav_wayline </sql> <select id="selectUavWaylineList" parameterType="UavWayline" resultMap="UavWaylineResult"> <include refid="selectUavWaylineVo"/> <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if> <if test="type != null and type != ''"> and type = #{type}</if> <if test="uavModel != null and uavModel != ''"> and uav_model = #{uavModel}</if> <if test="payloadModel != null and payloadModel != ''"> and payload_model = #{payloadModel}</if> <if test="globalSpeed != null "> and global_speed = #{globalSpeed}</if> <if test="globalHeight != null "> and global_height = #{globalHeight}</if> <if test="userWaypointCount != null "> and user_waypoint_count = #{userWaypointCount}</if> <if test="planWaypointCount != null "> and plan_waypoint_count = #{planWaypointCount}</if> <if test="srcWaylineData != null"> and src_wayline_data =#{srcWaylineData}</if> <if test="planWaylineData != null"> and plan_wayline_data = #{planWaylineData}</if> <if test="kmzUrl != null and kmzUrl != ''"> and kmz_url = #{kmzUrl}</if> <if test="userId != null "> and user_id = #{userId}</if> <if test="deptId != null "> and dept_id = #{deptId}</if> </where> </select> <select id="selectUavWaylineById" parameterType="String" resultMap="UavWaylineResult"> <include refid="selectUavWaylineVo"/> where id = #{id} </select> <insert id="insertUavWayline" parameterType="UavWayline"> insert into uav_wayline <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null">id,</if> <if test="name != null">name,</if> <if test="type != null">type,</if> <if test="uavModel != null">uav_model,</if> <if test="payloadModel != null">payload_model,</if> <if test="globalSpeed != null">global_speed,</if> <if test="globalHeight != null">global_height,</if> <if test="userWaypointCount != null">user_waypoint_count,</if> <if test="planWaypointCount != null">plan_waypoint_count,</if> <if test="srcWaylineData != null">src_wayline_data,</if> <if test="planWaylineData != null">plan_wayline_data,</if> <if test="kmzUrl != null">kmz_url,</if> <if test="userId != null">user_id,</if> <if test="deptId != null">dept_id,</if> <if test="createBy != null">create_by,</if> <if test="createTime != null">create_time,</if> <if test="updateBy != null">update_by,</if> <if test="updateTime != null">update_time,</if> <if test="remark != null">remark,</if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="id != null">#{id},</if> <if test="name != null">#{name},</if> <if test="type != null">#{type},</if> <if test="uavModel != null">#{uavModel},</if> <if test="payloadModel != null">#{payloadModel},</if> <if test="globalSpeed != null">#{globalSpeed},</if> <if test="globalHeight != null">#{globalHeight},</if> <if test="userWaypointCount != null">#{userWaypointCount},</if> <if test="planWaypointCount != null">#{planWaypointCount},</if> <if test="srcWaylineData != null">#{srcWaylineData},</if> <if test="planWaylineData != null">#{planWaylineData},</if> <if test="kmzUrl != null">#{kmzUrl},</if> <if test="userId != null">#{userId},</if> <if test="deptId != null">#{deptId},</if> <if test="createBy != null">#{createBy},</if> <if test="createTime != null">#{createTime},</if> <if test="updateBy != null">#{updateBy},</if> <if test="updateTime != null">#{updateTime},</if> <if test="remark != null">#{remark},</if> </trim> </insert> <update id="updateUavWayline" parameterType="UavWayline"> update uav_wayline <trim prefix="SET" suffixOverrides=","> <if test="name != null">name = #{name},</if> <if test="type != null">type = #{type},</if> <if test="uavModel != null">uav_model = #{uavModel},</if> <if test="payloadModel != null">payload_model = #{payloadModel},</if> <if test="globalSpeed != null">global_speed = #{globalSpeed},</if> <if test="globalHeight != null">global_height = #{globalHeight},</if> <if test="userWaypointCount != null "> user_waypoint_count = #{userWaypointCount}</if> <if test="planWaypointCount != null "> plan_waypoint_count = #{planWaypointCount}</if> <if test="srcWaylineData != null "> src_wayline_data =#{srcWaylineData}</if> <if test="planWaylineData != null "> plan_wayline_data = #{planWaylineData}</if> <if test="kmzUrl != null">kmz_url = #{kmzUrl},</if> <if test="userId != null">user_id = #{userId},</if> <if test="deptId != null">dept_id = #{deptId},</if> <if test="createBy != null">create_by = #{createBy},</if> <if test="createTime != null">create_time = #{createTime},</if> <if test="updateBy != null">update_by = #{updateBy},</if> <if test="updateTime != null">update_time = #{updateTime},</if> <if test="remark != null">remark = #{remark},</if> </trim> where id = #{id} </update> <delete id="deleteUavWaylineById" parameterType="String"> delete from uav_wayline where id = #{id} </delete> <delete id="deleteUavWaylineByIds" parameterType="String"> delete from uav_wayline where id in <foreach item="id" collection="array" open="(" separator="," close=")"> #{id} </foreach> </delete> <select id="checkUavWaylineNameUnique" parameterType="String" resultMap="UavWaylineResult"> select id, name from uav_wayline where name = #{name} limit 1 </select> </mapper> pom.xml
@@ -31,6 +31,7 @@ <velocity.version>2.3</velocity.version> <jwt.version>0.9.1</jwt.version> <knife4j.version>3.0.3</knife4j.version> <okhttp.version>4.8.1</okhttp.version> </properties> <!-- 依赖声明 --> @@ -182,7 +183,12 @@ <artifactId>ard-common</artifactId> <version>${ard.version}</version> </dependency> <!-- okhttp3--> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> </dependencies> </dependencyManagement>