liusuyi
2025-09-06 bb508127e9454e8b39ac7d431765b6de0ac74c35
优化
26 files added
10 files modified
2848 ■■■■■ changed files
ard-admin/src/main/resources/application-dev.yml 30 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/resources/application.yml 4 ●●●●● patch | view | raw | blame | history
ard-ui/package.json 10 ●●●●● patch | view | raw | blame | history
ard-ui/src/main.js 6 ●●●●● patch | view | raw | blame | history
ard-ui/vue.config.js 80 ●●●●● patch | view | raw | blame | history
ard-work/pom.xml 57 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/component/minio/config/MinioConfig.java 44 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/component/minio/utils/MinioUtil.java 210 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/component/websocket/handler/TyMessageHandler.java 6 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/config/GeoserverConfig.java 14 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/config/OssConfiguration.java 92 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/config/WaylinePlaningConfig.java 18 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/controller/UavWayLineController.java 2 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/controller/wayline/UavWaylineController.java 114 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/controller/wayline/terrain/GeoUtils.java 62 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/controller/wayline/terrain/TerrainFollowingPlanner.java 222 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdActionDTO.java 53 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdGimbalDTO.java 27 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdPositionDTO.java 30 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdUavPositionDTO.java 65 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/ArdWaylineDTO.java 77 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/dto/wayline/WaylineFileDTO.java 43 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/entity/UavWayline.java 80 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/domain/enums/WaylineEnum.java 76 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/mapper/UavWaylineMapper.java 71 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/IGeoToolsService.java 16 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/IKmzFileService.java 13 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/IUavWaylineService.java 71 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/impl/GeoToolsServiceImpl.java 158 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/impl/KmzFileServiceImpl.java 517 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/impl/UavWaylineServiceImpl.java 278 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/util/CameraUtil.java 6 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/util/GeoTools.java 98 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/util/Vertex.java 29 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/UavWaylineMapper.xml 161 ●●●●● patch | view | raw | blame | history
pom.xml 8 ●●●●● patch | view | raw | blame | history
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>