Files
yjearth/src/main/java/com/yj/earth/business/controller/GraphHopperController.java
2025-09-23 16:45:42 +08:00

253 lines
9.5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
import com.yj.earth.annotation.CheckAuth;
import com.yj.earth.business.domain.FileInfo;
import com.yj.earth.business.service.FileInfoService;
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 = "路径规划管理")
@CheckAuth
@RestController
@RequestMapping("/graphhopper")
public class GraphHopperController {
@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 = "获取地图列表")
@CheckAuth
@GetMapping("/list")
public ApiResponse list() {
LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FileInfo::getFileSuffix, "pbf");
return ApiResponse.success(fileInfoService.list(queryWrapper));
}
@Operation(summary = "加载地图数据")
@PostMapping("/loadMap")
@CheckAuth
public ApiResponse loadMap(@Parameter(description = "文件ID") @RequestParam String fileId) {
// 参数校验
if (fileId == null) {
return ApiResponse.failure("文件ID不能为空");
}
// 获取并校验OSM文件
String osmFilePath = fileInfoService.getFileAbsolutePath(fileId);
File osmFile = new File(osmFilePath);
if (!osmFile.exists()) {
return ApiResponse.failure("地图文件不存在: " + osmFilePath);
}
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(osmFilePath);
// 关键步骤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);
}
@Operation(summary = "路径规划")
@PostMapping("/route")
@CheckAuth
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())); // 终点
// 构建请求仅指定Profile、无setWeighting
String targetProfile = request.getProfile() != null ? request.getProfile() : "car";
GHRequest ghRequest = new GHRequest(ghPoints)
.setProfile(targetProfile);
// 用新实例计算路径
GHResponse response = currentHopper.route(ghRequest);
// 处理错误
if (response.hasErrors()) {
// 检查是否有超出范围的错误
boolean hasOutOfBoundsError = response.getErrors().stream()
.anyMatch(e -> e instanceof com.graphhopper.util.exceptions.PointOutOfBoundsException);
if (hasOutOfBoundsError) {
// 返回超出范围的特定格式响应
return ApiResponse.failure("路径超出地图范围");
} else {
return ApiResponse.failure("路径计算失败: " + response.getErrors().toString());
}
}
// 解析结果
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);
} catch (com.graphhopper.util.exceptions.PointOutOfBoundsException e) {
// 捕获单点超出范围的异常
return ApiResponse.failure("路径超出地图范围");
} catch (Exception e) {
return ApiResponse.failure("路径计算异常: " + e.getMessage());
}
}
@Operation(summary = "获取交通方式")
@CheckAuth
@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 ? "成功" : "失败"));
}
// 重载: 递归删除子目录
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();
}
}