2025-09-08 17:01:50 +08:00
|
|
|
package com.yj.earth.business.controller;
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
|
|
import com.graphhopper.GHRequest;
|
|
|
|
|
import com.graphhopper.GHResponse;
|
|
|
|
|
import com.graphhopper.GraphHopper;
|
|
|
|
|
import com.graphhopper.ResponsePath;
|
|
|
|
|
import com.graphhopper.config.Profile;
|
|
|
|
|
import com.graphhopper.util.shapes.GHPoint;
|
2025-09-16 11:41:45 +08:00
|
|
|
import com.yj.earth.annotation.CheckAuth;
|
2025-09-08 17:01:50 +08:00
|
|
|
import com.yj.earth.business.domain.FileInfo;
|
2025-09-29 13:56:36 +08:00
|
|
|
import com.yj.earth.business.domain.WebSource;
|
2025-09-08 17:01:50 +08:00
|
|
|
import com.yj.earth.business.service.FileInfoService;
|
2025-09-29 13:56:36 +08:00
|
|
|
import com.yj.earth.business.service.WebSourceService;
|
2025-09-08 17:01:50 +08:00
|
|
|
import com.yj.earth.common.config.GraphHopperProperties;
|
|
|
|
|
import com.yj.earth.common.util.ApiResponse;
|
|
|
|
|
import com.yj.earth.model.Point;
|
|
|
|
|
import com.yj.earth.model.RouteRequest;
|
|
|
|
|
import com.yj.earth.model.RouteResponse;
|
|
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
|
|
import io.swagger.v3.oas.annotations.Parameter;
|
|
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
|
|
import javax.annotation.PostConstruct;
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
|
|
import java.io.File;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Data
|
|
|
|
|
@Slf4j
|
|
|
|
|
@Tag(name = "路径规划管理")
|
2025-09-23 16:45:42 +08:00
|
|
|
@CheckAuth
|
2025-09-08 17:01:50 +08:00
|
|
|
@RestController
|
|
|
|
|
@RequestMapping("/graphhopper")
|
|
|
|
|
public class GraphHopperController {
|
2025-09-29 13:56:36 +08:00
|
|
|
@Resource
|
|
|
|
|
private WebSourceService webSourceService;
|
2025-09-08 17:01:50 +08:00
|
|
|
@Resource
|
|
|
|
|
private FileInfoService fileInfoService;
|
|
|
|
|
@Resource
|
|
|
|
|
private GraphHopperProperties graphHopperProperties;
|
|
|
|
|
|
|
|
|
|
// 存储当前可用的 GraphHopper 实例
|
|
|
|
|
private volatile GraphHopper currentHopper;
|
|
|
|
|
|
|
|
|
|
// 状态控制: 线程安全
|
|
|
|
|
private final AtomicBoolean isLoading = new AtomicBoolean(false);
|
|
|
|
|
private final AtomicBoolean isLoaded = new AtomicBoolean(false);
|
|
|
|
|
|
|
|
|
|
@Operation(summary = "加载地图数据")
|
|
|
|
|
@PostMapping("/loadMap")
|
2025-09-16 11:41:45 +08:00
|
|
|
@CheckAuth
|
2025-10-09 11:03:15 +08:00
|
|
|
public ApiResponse loadMap(@Parameter(description = "文件路径") @RequestParam String path) {
|
2025-09-29 13:56:36 +08:00
|
|
|
File osmFile = new File(path);
|
|
|
|
|
if (!osmFile.exists()) {
|
|
|
|
|
return ApiResponse.failure("地图文件不存在: " + path);
|
|
|
|
|
}
|
|
|
|
|
if (!osmFile.isFile() || !osmFile.getName().endsWith(".pbf")) {
|
|
|
|
|
return ApiResponse.failure("仅支持有效的.pbf格式OSM文件");
|
|
|
|
|
}
|
|
|
|
|
// 防止并发加载
|
|
|
|
|
if (isLoading.get()) {
|
|
|
|
|
return ApiResponse.failure("地图正在加载中、请稍后查询状态");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 标记加载状态
|
|
|
|
|
isLoading.set(true);
|
|
|
|
|
isLoaded.set(false);
|
|
|
|
|
|
|
|
|
|
// 异步执行: 删除旧数据 → 创建新实例 → 加载新地图
|
|
|
|
|
new Thread(() -> {
|
|
|
|
|
GraphHopper newHopper = null;
|
|
|
|
|
try {
|
|
|
|
|
// 关键步骤1: 彻底删除旧地图数据目录
|
|
|
|
|
deleteOldGraphDir();
|
|
|
|
|
// 关键步骤2: 创建全新的GraphHopper实例
|
|
|
|
|
newHopper = createNewGraphHopperInstance(path);
|
|
|
|
|
// 关键步骤3: 加载新地图
|
|
|
|
|
newHopper.importOrLoad();
|
|
|
|
|
// 关键步骤4: 加载成功 → 替换当前实例 + 更新状态
|
|
|
|
|
currentHopper = newHopper;
|
|
|
|
|
isLoaded.set(true);
|
|
|
|
|
log.info("地图加载成功");
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 加载失败 → 清理新实例资源
|
|
|
|
|
if (newHopper != null) {
|
|
|
|
|
newHopper.close();
|
|
|
|
|
}
|
|
|
|
|
isLoaded.set(false);
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
log.error("地图加载失败: " + e.getMessage());
|
|
|
|
|
} finally {
|
|
|
|
|
// 无论成功/失败、释放加载锁
|
|
|
|
|
isLoading.set(false);
|
|
|
|
|
}
|
|
|
|
|
}).start();
|
|
|
|
|
return ApiResponse.success(null);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 11:03:15 +08:00
|
|
|
@Operation(summary = "清除地图服务")
|
|
|
|
|
@PostMapping("/clearMap")
|
|
|
|
|
@CheckAuth
|
|
|
|
|
public ApiResponse clearMap() {
|
|
|
|
|
// 防止并发操作(与加载操作互斥)
|
|
|
|
|
if (isLoading.get()) {
|
|
|
|
|
return ApiResponse.failure("地图正在加载中、无法清除、请稍后再试");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 标记正在处理清除操作
|
|
|
|
|
isLoading.set(true);
|
|
|
|
|
try {
|
|
|
|
|
// 1. 关闭当前GraphHopper实例
|
|
|
|
|
if (currentHopper != null) {
|
|
|
|
|
log.info("开始关闭当前地图服务实例");
|
|
|
|
|
currentHopper.close();
|
|
|
|
|
currentHopper = null;
|
|
|
|
|
log.info("地图服务实例已关闭");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 删除地图数据目录
|
|
|
|
|
log.info("开始删除地图数据目录");
|
|
|
|
|
deleteOldGraphDir();
|
|
|
|
|
log.info("地图数据目录已删除");
|
|
|
|
|
|
|
|
|
|
// 3. 重置状态变量
|
|
|
|
|
isLoaded.set(false);
|
|
|
|
|
|
|
|
|
|
return ApiResponse.success("地图服务已成功清除");
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("清除地图服务失败", e);
|
|
|
|
|
return ApiResponse.failure("清除地图服务失败: " + e.getMessage());
|
|
|
|
|
} finally {
|
|
|
|
|
// 释放操作锁
|
|
|
|
|
isLoading.set(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-08 17:01:50 +08:00
|
|
|
@Operation(summary = "路径规划")
|
|
|
|
|
@PostMapping("/route")
|
2025-09-16 11:41:45 +08:00
|
|
|
@CheckAuth
|
2025-09-08 17:01:50 +08:00
|
|
|
public ApiResponse calculateRoute(@RequestBody RouteRequest request) {
|
|
|
|
|
// 校验地图是否加载完成 + 实例是否可用
|
|
|
|
|
if (!isLoaded.get() || currentHopper == null) {
|
|
|
|
|
return ApiResponse.failure("地图未加载完成");
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// 构建路径点列表
|
|
|
|
|
List<GHPoint> ghPoints = new ArrayList<>();
|
|
|
|
|
ghPoints.add(new GHPoint(request.getStartLat(), request.getStartLng())); // 起点
|
|
|
|
|
// 添加途经点
|
|
|
|
|
if (request.getWaypoints() != null && !request.getWaypoints().isEmpty()) {
|
|
|
|
|
for (Point waypoint : request.getWaypoints()) {
|
|
|
|
|
ghPoints.add(new GHPoint(waypoint.getLat(), waypoint.getLng()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ghPoints.add(new GHPoint(request.getEndLat(), request.getEndLng())); // 终点
|
|
|
|
|
|
2025-10-09 11:03:15 +08:00
|
|
|
// 构建请求
|
2025-09-08 17:01:50 +08:00
|
|
|
String targetProfile = request.getProfile() != null ? request.getProfile() : "car";
|
|
|
|
|
GHRequest ghRequest = new GHRequest(ghPoints)
|
|
|
|
|
.setProfile(targetProfile);
|
|
|
|
|
|
|
|
|
|
// 用新实例计算路径
|
|
|
|
|
GHResponse response = currentHopper.route(ghRequest);
|
|
|
|
|
// 处理错误
|
|
|
|
|
if (response.hasErrors()) {
|
2025-09-16 11:41:45 +08:00
|
|
|
// 检查是否有超出范围的错误
|
|
|
|
|
boolean hasOutOfBoundsError = response.getErrors().stream()
|
|
|
|
|
.anyMatch(e -> e instanceof com.graphhopper.util.exceptions.PointOutOfBoundsException);
|
2025-10-09 11:03:15 +08:00
|
|
|
boolean hasPointNotFoundError = response.getErrors().stream()
|
|
|
|
|
.anyMatch(e -> e instanceof com.graphhopper.util.exceptions.PointNotFoundException);
|
2025-09-16 11:41:45 +08:00
|
|
|
if (hasOutOfBoundsError) {
|
|
|
|
|
return ApiResponse.failure("路径超出地图范围");
|
2025-10-09 11:03:15 +08:00
|
|
|
} else if (hasPointNotFoundError) {
|
|
|
|
|
return ApiResponse.failure("未超地图范围但找不到路径点");
|
2025-09-16 11:41:45 +08:00
|
|
|
} else {
|
2025-10-09 11:03:15 +08:00
|
|
|
return ApiResponse.failure("路径计算异常: " + response.getErrors().get(0).getMessage());
|
2025-09-16 11:41:45 +08:00
|
|
|
}
|
2025-09-08 17:01:50 +08:00
|
|
|
}
|
|
|
|
|
// 解析结果
|
|
|
|
|
ResponsePath bestPath = response.getBest();
|
|
|
|
|
List<Point> pathPoints = new ArrayList<>();
|
|
|
|
|
bestPath.getPoints().forEach(ghPoint ->
|
|
|
|
|
pathPoints.add(new Point(ghPoint.getLat(), ghPoint.getLon()))
|
|
|
|
|
);
|
|
|
|
|
// 封装返回
|
|
|
|
|
RouteResponse routeResponse = new RouteResponse(bestPath.getDistance() / 1000, (double) (bestPath.getTime() / 60000), pathPoints);
|
|
|
|
|
return ApiResponse.success(routeResponse);
|
2025-09-16 11:41:45 +08:00
|
|
|
} catch (com.graphhopper.util.exceptions.PointOutOfBoundsException e) {
|
|
|
|
|
// 捕获单点超出范围的异常
|
|
|
|
|
return ApiResponse.failure("路径超出地图范围");
|
2025-09-08 17:01:50 +08:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
return ApiResponse.failure("路径计算异常: " + e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Operation(summary = "获取交通方式")
|
2025-09-16 11:41:45 +08:00
|
|
|
@CheckAuth
|
2025-09-08 17:01:50 +08:00
|
|
|
@PostMapping("/profiles")
|
|
|
|
|
public ApiResponse profiles() {
|
|
|
|
|
return ApiResponse.success(graphHopperProperties.getProfiles());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建全新的 GraphHopper 实例
|
|
|
|
|
*/
|
|
|
|
|
private GraphHopper createNewGraphHopperInstance(String osmFilePath) {
|
|
|
|
|
GraphHopper hopper = new GraphHopper();
|
|
|
|
|
// 配置基础参数
|
|
|
|
|
hopper.setOSMFile(osmFilePath);
|
|
|
|
|
hopper.setGraphHopperLocation(graphHopperProperties.getGraphLocation());
|
|
|
|
|
// 配置交通方式 + 权重策略
|
|
|
|
|
List<Profile> profileList = new ArrayList<>();
|
|
|
|
|
for (String profileName : graphHopperProperties.getProfiles()) {
|
|
|
|
|
Profile profile = new Profile(profileName);
|
|
|
|
|
profile.setVehicle(profileName);
|
|
|
|
|
profile.setWeighting("fastest");
|
|
|
|
|
profileList.add(profile);
|
|
|
|
|
}
|
|
|
|
|
hopper.setProfiles(profileList);
|
|
|
|
|
|
|
|
|
|
return hopper;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 递归删除旧地图数据目录
|
|
|
|
|
*/
|
|
|
|
|
private void deleteOldGraphDir() {
|
|
|
|
|
File graphDir = new File(graphHopperProperties.getGraphLocation());
|
|
|
|
|
if (!graphDir.exists()) {
|
|
|
|
|
log.info("旧地图目录不存在、无需删除: " + graphHopperProperties.getGraphLocation());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 递归删除所有文件和子目录
|
|
|
|
|
File[] files = graphDir.listFiles();
|
|
|
|
|
if (files != null) {
|
|
|
|
|
for (File file : files) {
|
|
|
|
|
if (file.isDirectory()) {
|
|
|
|
|
deleteOldGraphDir(file);
|
|
|
|
|
} else {
|
|
|
|
|
boolean deleted = file.delete();
|
|
|
|
|
log.info("删除旧地图文件: " + file.getAbsolutePath() + " → " + (deleted ? "成功" : "失败"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除空目录
|
|
|
|
|
boolean dirDeleted = graphDir.delete();
|
|
|
|
|
System.out.println("删除旧地图目录: " + graphDir.getAbsolutePath() + " → " + (dirDeleted ? "成功" : "失败"));
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 15:30:14 +08:00
|
|
|
// 重载:递归删除子目录
|
2025-09-08 17:01:50 +08:00
|
|
|
private void deleteOldGraphDir(File subDir) {
|
|
|
|
|
File[] files = subDir.listFiles();
|
|
|
|
|
if (files != null) {
|
|
|
|
|
for (File file : files) {
|
|
|
|
|
if (file.isDirectory()) {
|
|
|
|
|
deleteOldGraphDir(file);
|
|
|
|
|
} else {
|
|
|
|
|
file.delete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
subDir.delete();
|
|
|
|
|
}
|
|
|
|
|
}
|