1.导入航线优化、生成航线kmz优化

2.增加修改返航高度、预计飞行时间(航点航线)接口
This commit is contained in:
sdy 2026-06-23 14:53:16 +08:00
parent 559fdb5d96
commit 64335ed9d6
12 changed files with 445 additions and 10 deletions

View File

@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.collect.Maps;
import com.multictrl.common.utils.DateUtils;
import com.multictrl.common.utils.Spatial4jUtils;
import com.multictrl.common.utils.map.Spatial4jUtils;
import com.multictrl.modules.business.dao.FlightTaskDao;
import com.multictrl.modules.business.entity.FlightTaskEntity;
import com.multictrl.modules.business.entity.MediaFileEntity;

View File

@ -252,4 +252,31 @@ public class Utils {
double result = bytes / Math.pow(1024, exp);
return format(result, 2) + " " + pre + "B";
}
/**
* 将秒转换为分秒
*/
public static String calculateSeconds(long totalSeconds) {
// 处理负数
if (totalSeconds < 0) {
return "-" + calculateSeconds(-totalSeconds);
}
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
StringBuilder sb = new StringBuilder();
if (hours > 0) {
sb.append(hours).append('h');
}
if (minutes > 0) {
sb.append(minutes).append('m');
}
// 当所有部分都为0时至少显示 "0s"
if (seconds > 0 || sb.isEmpty()) {
sb.append(seconds).append('s');
}
return sb.toString();
}
}

View File

@ -68,11 +68,12 @@ public class TemplateUtils {
folder.addElement("wpml:globalUseStraightLine").setText("1");
List<RouteWaypointDTO> waypointList = route.getRouteWaypointList();
for (RouteWaypointDTO waypoint : waypointList) {
for (int i = 0; i < waypointList.size(); i++) {
RouteWaypointDTO waypoint = waypointList.get(i);
Element placemark = folder.addElement("Placemark");
Element point = placemark.addElement("Point");
point.addElement("coordinates").setText(waypoint.getLongitude() + "," + waypoint.getLatitude());
placemark.addElement("wpml:index").setText(waypoint.getWaypointSort() + "");
placemark.addElement("wpml:index").setText(i + "");
placemark.addElement("wpml:ellipsoidHeight").setText(waypoint.getFlightHeight() + "");
placemark.addElement("wpml:height").setText(waypoint.getFlightHeight() + "");
placemark.addElement("wpml:waypointSpeed").setText(waypoint.getFlightSpeed() + "");

View File

@ -59,11 +59,12 @@ public class WaylinesUtils {
//航点信息
List<RouteWaypointDTO> waypointList = route.getRouteWaypointList();
for (RouteWaypointDTO waypoint : waypointList) {
for (int i = 0; i < waypointList.size(); i++) {
RouteWaypointDTO waypoint = waypointList.get(i);
Element placemark = folder.addElement("Placemark");
Element point = placemark.addElement("Point");
point.addElement("coordinates").setText(waypoint.getLongitude() + "," + waypoint.getLatitude());
placemark.addElement("wpml:index").setText(waypoint.getWaypointSort() + "");
placemark.addElement("wpml:index").setText(i + "");
placemark.addElement("wpml:executeHeight").setText(waypoint.getFlightHeight() + "");
placemark.addElement("wpml:waypointSpeed").setText(waypoint.getFlightSpeed() + "");
//添加转弯模式/飞机偏航角模式

View File

@ -0,0 +1,36 @@
package com.multictrl.common.utils.map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.List;
/**
* 经纬度
*
* @author Sdy
* @since 1.0.0 2026/6/23
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GeoPoint {
private double longitude;
private double latitude;
/**
* 构造方法
*
* @param location 经纬度x,y
*/
public GeoPoint(String location) {
List<String> list = Arrays.asList(location.split(","));
this.longitude = Double.parseDouble(list.get(0));
this.latitude = Double.parseDouble(list.get(1));
}
}

View File

@ -0,0 +1,245 @@
package com.multictrl.common.utils.map;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.multictrl.common.exception.RenException;
import java.util.ArrayList;
import java.util.List;
/**
* 地图数据工具
*
* @author Sdy
* @since 1.0.0 2026/6/23
*/
public class GeoUtils {
private static final int EARTH_RADIUS_IN_METERS = 6371000; // 地球半径单位
/**
* 创建wkt点
*
* @param longitude 精度
* @param latitude 维度
* @return 返回WKT格式的点例如 "POINT(120.123456789 30.987654789)"
*/
public static String createPointWkt(Double longitude, Double latitude) {
if (longitude == null || latitude == null) {
return null;
}
// 判断是否是合法的数字
if (Double.isNaN(longitude) || Double.isNaN(latitude) ||
Double.isInfinite(longitude) || Double.isInfinite(latitude)) {
return null;
}
return String.format("POINT(%.9f %.9f)", longitude, latitude);
}
/**
* 解析 WKT 格式的 POINT 字符串并根据指定的精度返回经度和纬度
*
* @param pointWkt WKT 格式的 POINT 字符串例如 "POINT(120.123456789 30.987654789)"
* @return 包含经度和纬度的 GeoPoint 对象
*/
public static GeoPoint parsePointWkt(String pointWkt) {
if (pointWkt == null || !pointWkt.startsWith("POINT(") || !pointWkt.endsWith(")")) {
throw new RenException("经纬度解析错误");
}
// 去掉 "POINT(" ")"然后按空格分割
String content = pointWkt.substring(6, pointWkt.length() - 1);
String[] parts = content.split("\\s+");
if (parts.length != 2) {
throw new RenException("经纬度解析错误");
}
try {
double longitude = Double.parseDouble(parts[0]);
double latitude = Double.parseDouble(parts[1]);
// 根据精度进行四舍五入
longitude = roundToPrecision(longitude, 9);
latitude = roundToPrecision(latitude, 9);
return new GeoPoint(longitude, latitude);
} catch (Exception e) {
throw new RenException("经纬度解析错误", e);
}
}
/**
* 四舍五入到指定的小数位数
*
* @param value 要四舍五入的值
* @param precision 小数点后保留的位数
* @return 四舍五入后的结果
*/
private static double roundToPrecision(double value, int precision) {
double scale = Math.pow(10, precision);
return Math.round(value * scale) / scale;
}
/**
* 使用 Haversine 公式计算两个经纬度之间的距离单位
*
* @param lat1 第一个点的纬度
* @param lon1 第一个点的经度
* @param lat2 第二个点的纬度
* @param lon2 第二个点的经度
* @return 距离
*/
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
double lat1Rad = Math.toRadians(lat1);
double lon1Rad = Math.toRadians(lon1);
double lat2Rad = Math.toRadians(lat2);
double lon2Rad = Math.toRadians(lon2);
double a = Math.pow(Math.sin((lat2Rad - lat1Rad) / 2), 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.pow(Math.sin((lon2Rad - lon1Rad) / 2), 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_IN_METERS * c;
}
/**
* 判断两点之间的距离是否在指定范围内单位
*
* @param lat1 第一个点的纬度
* @param lon1 第一个点的经度
* @param lat2 第二个点的纬度
* @param lon2 第二个点的经度
* @param minDistance 最小距离单位
* @param maxDistance 最大距离单位
* @return 是否在范围内
*/
public static boolean isDistanceInRange(double lat1, double lon1, double lat2, double lon2,
double minDistance, double maxDistance) {
double distance = calculateDistance(lat1, lon1, lat2, lon2);
return distance >= minDistance && distance <= maxDistance;
}
/**
* "经度,纬度;经度,纬度" 格式的字符串转换为闭合的坐标数组用于 GeoJSON Polygon
*
* @param mapping 输入的坐标字符串
* @return 格式化后的 List<List<Double>>每个内层 List [经度, 纬度]
*/
public static List<List<Double>> parseCoordinates(String mapping) {
if (mapping == null || mapping.trim().isEmpty()) {
throw new RenException("面状范围不能为空");
}
String[] points = mapping.split(";");
List<List<Double>> coordinates = new ArrayList<>();
List<Double> firstPoint = null;
for (int i = 0; i < points.length; i++) {
String[] latLon = points[i].split(",");
if (latLon.length != 2) {
throw new RenException("无效坐标对: " + points[i]);
}
double lon = Double.parseDouble(latLon[0].trim());
double lat = Double.parseDouble(latLon[1].trim());
List<Double> point = List.of(lon, lat);
coordinates.add(point);
// 记录第一个点用于闭合
/*if (i == 0) {
firstPoint = point;
}*/
}
// 闭合多边形最后一个点必须等于第一个点
/*if (firstPoint != null && !coordinates.get(coordinates.size() - 1).equals(firstPoint)) {
coordinates.add(firstPoint);
}*/
return coordinates;
}
/**
* 根据经纬度获取高程单位
*
* @param latitude 纬度 40.714728
* @param longitude 经度 -73.998672
* @return 海拔高度
* @throws Exception 如果API请求失败或数据异常
*/
public static double getElevation(double latitude, double longitude) {
// 构建API请求URL使用aster30m数据集
String url = "https://api.opentopodata.org/v1/aster30m?locations=" + latitude + "," + longitude;
//发送HTTP请求
String response = HttpUtil.get(url);
// 解析JSON
JSONObject json = JSONUtil.parseObj(response);
if (!json.containsKey("results") || json.getJSONArray("results").isEmpty()) {
return 0;
}
// 返回第一个结果的高程值
return json.getJSONArray("results").getJSONObject(0).getDouble("elevation");
}
/**
* 计算点数集合总时长
* 包含了上升时间
*/
public static long calculateFlightTime(List<JSONObject> waypoints) {
if (waypoints == null || waypoints.size() < 2) {
return 0;
}
double totalSeconds = 0.0;
final double EARTH_RADIUS = 6371000.0; // 地球半径
final double VERTICAL_SPEED = 2.0; // 垂直调整速度m/s大疆典型值
for (int i = 1; i < waypoints.size(); i++) {
JSONObject prev = waypoints.get(i - 1);
JSONObject curr = waypoints.get(i);
// 获取位置和速度
double lat1 = prev.getDouble("latitude");
double lon1 = prev.getDouble("longitude");
double alt1 = prev.getDouble("altitude");
double lat2 = curr.getDouble("latitude");
double lon2 = curr.getDouble("longitude");
double alt2 = curr.getDouble("altitude");
double horizontalSpeed = prev.getDouble("speed"); // 水平速度m/s
// 1. 计算水平距离地面投影单位
double lat1Rad = Math.toRadians(lat1);
double lon1Rad = Math.toRadians(lon1);
double lat2Rad = Math.toRadians(lat2);
double lon2Rad = Math.toRadians(lon2);
double deltaLat = lat2Rad - lat1Rad;
double deltaLon = lon2Rad - lon1Rad;
double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double groundDistance = EARTH_RADIUS * c;
// 2. 计算垂直高度差绝对值
double heightDiff = Math.abs(alt2 - alt1);
// 3. 计算两段飞行时间
double timeVertical = heightDiff / VERTICAL_SPEED; // 垂直调整时间
double timeHorizontal = groundDistance / horizontalSpeed; // 水平飞行时间
totalSeconds += timeVertical + timeHorizontal;
}
return Math.round(totalSeconds); // 四舍五入为整数秒
}
}

View File

@ -1,4 +1,4 @@
package com.multictrl.common.utils;
package com.multictrl.common.utils.map;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;

View File

@ -11,9 +11,11 @@ import com.multictrl.common.validator.group.AddGroup;
import com.multictrl.common.validator.group.UpdateGroup;
import com.multictrl.modules.business.dto.RouteAiDTO;
import com.multictrl.modules.business.dto.RouteDTO;
import com.multictrl.modules.business.dto.RouteEstimateTimeDTO;
import com.multictrl.modules.business.dto.UploadRouteDTO;
import com.multictrl.modules.business.service.RouteService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
@ -125,13 +127,33 @@ public class RouteController {
@Parameter(name = "dockSn", description = "机库SN"),
@Parameter(name = "accurateImport", description = "是否精准导入"),
@Parameter(name = "globalRthHeight", description = "返航高度"),
@Parameter(name = "file", description = "航线文件")
@Parameter(name = "file", description = "航线文件",
schema = @Schema(type = "string", format = "binary"))
})
@RequiresPermissions("bus:route:upload")
public Result<Object> upload(UploadRouteDTO dto) {
public Result<Object> upload(@Parameter(hidden = true) UploadRouteDTO dto) {
ValidatorUtils.validateEntity(dto);
routeService.upload(dto);
return new Result<>();
}
@PostMapping("/getWaypointEstimateTime")
@Operation(summary = "航点航线预计飞行时间")
public Result<String> getWaypointEstimateTime(@RequestBody RouteEstimateTimeDTO dto) {
ValidatorUtils.validateEntity(dto);
String estimateTime = routeService.getWaypointEstimateTime(dto);
return new Result<String>().ok(estimateTime);
}
@PutMapping("/updateGlobalRthHeight")
@Operation(summary = "修改返航高度")
@LogOperation("修改返航高度")
public Result<Object> updateGlobalRthHeight(@RequestParam Long routeId,
@RequestParam Integer globalRthHeight) {
routeService.updateGlobalRthHeight(routeId, globalRthHeight);
return new Result<>();
}
}

View File

@ -0,0 +1,34 @@
package com.multictrl.modules.business.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 航线预计飞行时长参数
*
* @author Sdy
* @since 1.0.0 2026/6/23
*/
@Data
@Schema(name = "航线预计飞行时长参数")
public class RouteEstimateTimeDTO {
@NotBlank(message = "机库SN不能为空")
@Schema(description = "机库SN")
private String dockSn;
@Schema(description = "全局航线过渡速度")
@NotNull(message = "全局航线过渡速度不能为空")
private Double globalTransitionalSpeed;
@NotBlank(message = "高度模式不能为空")
@Schema(description = "高度模式")
private String heightModel;
@Schema(description = "航线航点信息")
private List<RouteWaypointDTO> waypointList;
}

View File

@ -4,6 +4,7 @@ import com.multictrl.common.page.PageData;
import com.multictrl.common.service.CrudService;
import com.multictrl.modules.business.dto.RouteAiDTO;
import com.multictrl.modules.business.dto.RouteDTO;
import com.multictrl.modules.business.dto.RouteEstimateTimeDTO;
import com.multictrl.modules.business.dto.UploadRouteDTO;
import com.multictrl.modules.business.entity.RouteEntity;
@ -40,4 +41,10 @@ public interface RouteService extends CrudService<RouteEntity, RouteDTO> {
//上传航线
void upload(UploadRouteDTO dto);
//获取航点航线预计飞行时间
String getWaypointEstimateTime(RouteEstimateTimeDTO dto);
//修改返航高度
void updateGlobalRthHeight(Long routeId, Integer globalRthHeight);
}

View File

@ -17,6 +17,7 @@ import com.multictrl.common.page.PageData;
import com.multictrl.common.service.impl.CrudServiceImpl;
import cn.hutool.core.util.StrUtil;
import com.multictrl.common.utils.*;
import com.multictrl.common.utils.map.Spatial4jUtils;
import com.multictrl.modules.business.dao.DockDeviceDao;
import com.multictrl.modules.business.dao.FlightTaskDao;
import com.multictrl.modules.business.dao.ZhimouCallbackDao;

View File

@ -2,8 +2,10 @@ package com.multictrl.modules.business.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.multictrl.common.constant.BusinessConstant;
import com.multictrl.common.exception.ErrorCode;
import com.multictrl.common.exception.ExceptionUtils;
@ -12,6 +14,9 @@ import com.multictrl.common.page.PageData;
import com.multictrl.common.service.impl.CrudServiceImpl;
import com.multictrl.common.utils.ConvertUtils;
import com.multictrl.common.utils.JsonUtils;
import com.multictrl.common.utils.Utils;
import com.multictrl.common.utils.map.GeoUtils;
import com.multictrl.common.utils.map.Spatial4jUtils;
import com.multictrl.common.utils.kmz.KmzUtils;
import com.multictrl.common.utils.kmz.WpmlKmlParser;
import com.multictrl.common.validator.AssertUtils;
@ -25,6 +30,7 @@ import com.multictrl.modules.business.entity.RouteAiEntity;
import com.multictrl.modules.business.entity.RouteEntity;
import com.multictrl.modules.business.entity.RouteWaypointEntity;
import com.multictrl.modules.business.entity.WaypointActionEntity;
import com.multictrl.modules.business.service.DJIBaseService;
import com.multictrl.modules.business.service.MinioService;
import com.multictrl.modules.business.service.RouteService;
import com.multictrl.modules.security.user.SecurityUser;
@ -53,6 +59,7 @@ public class RouteServiceImpl extends CrudServiceImpl<RouteDao, RouteEntity, Rou
private final WaypointActionDao waypointActionDao;
private final MinioService minioService;
private final RouteAiDao routeAiDao;
private final DJIBaseService djiBaseService;
@Override
public QueryWrapper<RouteEntity> getWrapper(Map<String, Object> params) {
@ -236,7 +243,7 @@ public class RouteServiceImpl extends CrudServiceImpl<RouteDao, RouteEntity, Rou
byte[] waylines = kmzInfo.get("waylines.wpml");
WpmlKmlParser.parseWpmlFile(waylines, routeDTO, dto.getAccurateImport());
log.debug("解析结果:{}", JsonUtils.toJsonString(dto));
log.debug("解析结果:{}", JsonUtils.toJsonString(routeDTO));
} catch (Exception e) {
log.error("解析结果失败:{}", ExceptionUtils.getErrorStackTrace(e));
// throw new RenException(ErrorCode.FILE_PARSER_ERROR);
@ -244,7 +251,9 @@ public class RouteServiceImpl extends CrudServiceImpl<RouteDao, RouteEntity, Rou
}
routeDTO.setRouteName(name);
if (dto.getAccurateImport()) {
routeDTO.setGlobalRthHeight(dto.getGlobalRthHeight());
}
String path = dto.getDockSn() + "/" + originalFilename;
try {
minioService.uploadFile(file.getInputStream(), BusinessConstant.ROUTE_KMZ_BUCKET, path);
@ -259,17 +268,69 @@ public class RouteServiceImpl extends CrudServiceImpl<RouteDao, RouteEntity, Rou
//航点动作处理
List<RouteWaypointDTO> routeWaypointList = routeDTO.getRouteWaypointList();
handleWaypointAction(routeWaypointList);
//计算总距离
double total = 0;
for (int i = 0; i < routeWaypointList.size() - 1; i++) {
RouteWaypointDTO p1 = routeWaypointList.get(i);
RouteWaypointDTO p2 = routeWaypointList.get(i + 1);
total += Spatial4jUtils.distance(p1.getLatitude(), p1.getLongitude(), p2.getLatitude(), p2.getLongitude());
}
//计算预计飞行时长
RouteEstimateTimeDTO timeDTO = new RouteEstimateTimeDTO();
timeDTO.setDockSn(dto.getDockSn());
timeDTO.setGlobalTransitionalSpeed(routeDTO.getGlobalTransitionalSpeed());
timeDTO.setHeightModel(routeDTO.getHeightModel());
timeDTO.setWaypointList(routeWaypointList);
String estimateTime = getWaypointEstimateTime(timeDTO);
//保存信息
RouteEntity routeEntity = ConvertUtils.sourceToTarget(routeDTO, currentModelClass());
routeEntity.setDeptId(SecurityUser.getDeptId());
routeEntity.setWaypointNum(routeWaypointList.size());
routeEntity.setTotalDistance(total);
routeEntity.setExpectFlightTime(estimateTime);
baseDao.insert(routeEntity);
//保存航点信息
saveWaypoint(routeEntity.getId(), routeWaypointList);
}
@Override
public String getWaypointEstimateTime(RouteEstimateTimeDTO dto) {
String dockSn = dto.getDockSn();
LatLonDTO dockLocation = djiBaseService.getDockLocation(dockSn);
List<RouteWaypointDTO> waypointList = dto.getWaypointList();
List<JSONObject> waypoints = new ArrayList<>();
JSONObject jsonObject = null;
if (dockLocation != null) {
jsonObject = new JSONObject();
jsonObject.set("longitude", dockLocation.getLongitude());
jsonObject.set("latitude", dockLocation.getLatitude());
jsonObject.set("altitude", dockLocation.getHeight());
jsonObject.set("speed", dto.getGlobalTransitionalSpeed());
waypoints.add(jsonObject);
}
for (RouteWaypointDTO waypointDTO : waypointList) {
JSONObject object = new JSONObject();
object.set("longitude", waypointDTO.getLongitude());
object.set("latitude", waypointDTO.getLatitude());
object.set("altitude", "relativeToStartPoint".equals(dto.getHeightModel()) ?
waypointDTO.getFlightHeight() + (dockLocation != null ? dockLocation.getHeight() : 0) : waypointDTO.getFlightHeight());
object.set("speed", waypointDTO.getFlightSpeed());
waypoints.add(object);
}
long totalTravelTime = GeoUtils.calculateFlightTime(waypoints);
return Utils.calculateSeconds(totalTravelTime);
}
@Override
public void updateGlobalRthHeight(Long routeId, Integer globalRthHeight) {
baseDao.update(new UpdateWrapper<RouteEntity>()
.set("global_rth_height", globalRthHeight)
.eq("id", routeId));
}
//判断航线是否存在
private void checkRouteExist(Long id, String routeName) {
RouteEntity uavRouteEntity = baseDao.selectOne(new QueryWrapper<RouteEntity>()