Skip to content

Commit ca6eb86

Browse files
uugaemiclaude
andcommitted
fix: Mapbox Map Matching → Directions API 전환 및 경로 품질 개선
- Map Matching API(GPS 보정용)에서 Directions API(경유지 라우팅)로 변경 - 도형 윤곽의 웨이포인트를 순서대로 방문하는 방식으로 유사도 98% 달성 - continue_straight=true로 불필요한 U턴 방지 - 49개 웨이포인트 선택 (2청크 분할) - 왕복 구간 제거 로직 제거 (경로 품질 저하 원인) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 13e5497 commit ca6eb86

File tree

1 file changed

+54
-52
lines changed

1 file changed

+54
-52
lines changed

src/main/java/com/artrun/server/service/MapboxMapMatchingService.java

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
public class MapboxMapMatchingService {
2222

2323
private static final GeometryFactory GF = new GeometryFactory(new PrecisionModel(), 4326);
24-
private static final int MAX_COORDINATES_PER_REQUEST = 100;
24+
private static final int MAX_WAYPOINTS_PER_REQUEST = 25;
2525

2626
private final String apiKey;
2727
private final ObjectMapper objectMapper;
@@ -39,85 +39,87 @@ public boolean isAvailable() {
3939
return apiKey != null && !apiKey.isBlank();
4040
}
4141

42-
/**
43-
* 좌표열을 Mapbox Map Matching API로 도로에 매칭하여 경로를 반환한다.
44-
* 입력: 도형 윤곽을 따르는 촘촘한 좌표열 (lat/lng)
45-
* 출력: 도로를 따라가는 LineString
46-
*/
4742
public LineString matchToRoads(Coordinate[] coordinates) {
48-
log.info("Mapbox Map Matching: {} coordinates", coordinates.length);
43+
log.info("Mapbox Directions: {} input coordinates", coordinates.length);
4944

50-
List<Coordinate> allMatched = new ArrayList<>();
45+
Coordinate[] waypoints = selectWaypoints(coordinates);
46+
log.info("Selected {} waypoints", waypoints.length);
5147

52-
// Mapbox는 한 번에 최대 100좌표 → 청크로 분할
53-
for (int start = 0; start < coordinates.length; start += MAX_COORDINATES_PER_REQUEST - 1) {
54-
int end = Math.min(start + MAX_COORDINATES_PER_REQUEST, coordinates.length);
48+
List<Coordinate> allCoords = new ArrayList<>();
49+
50+
for (int start = 0; start < waypoints.length - 1; start += MAX_WAYPOINTS_PER_REQUEST - 1) {
51+
int end = Math.min(start + MAX_WAYPOINTS_PER_REQUEST, waypoints.length);
5552
Coordinate[] chunk = new Coordinate[end - start];
56-
System.arraycopy(coordinates, start, chunk, 0, end - start);
53+
System.arraycopy(waypoints, start, chunk, 0, end - start);
54+
55+
List<Coordinate> segment = callDirectionsApi(chunk);
56+
if (segment.isEmpty()) continue;
5757

58-
List<Coordinate> matched = callMapMatchingApi(chunk);
58+
if (!allCoords.isEmpty()) {
59+
segment = segment.subList(1, segment.size());
60+
}
61+
allCoords.addAll(segment);
62+
}
5963

60-
if (!allMatched.isEmpty() && !matched.isEmpty()) {
61-
matched = matched.subList(1, matched.size());
64+
// 폐곡선: 마지막 → 첫 웨이포인트
65+
if (waypoints.length > 2) {
66+
List<Coordinate> closing = callDirectionsApi(
67+
new Coordinate[]{waypoints[waypoints.length - 1], waypoints[0]});
68+
if (!closing.isEmpty() && !allCoords.isEmpty()) {
69+
allCoords.addAll(closing.subList(1, closing.size()));
6270
}
63-
allMatched.addAll(matched);
6471
}
6572

66-
if (allMatched.size() < 2) {
67-
throw new BusinessException(ErrorCode.ROUTING_FAILED, "Mapbox Map Matching 실패: 매칭된 좌표가 부족합니다.");
73+
if (allCoords.size() < 2) {
74+
throw new BusinessException(ErrorCode.ROUTING_FAILED, "Mapbox 경로 생성 실패");
6875
}
6976

70-
log.info("Mapbox matched: {} -> {} coordinates", coordinates.length, allMatched.size());
71-
return GF.createLineString(allMatched.toArray(new Coordinate[0]));
77+
log.info("Mapbox route: {} coordinates", allCoords.size());
78+
return GF.createLineString(allCoords.toArray(new Coordinate[0]));
7279
}
7380

74-
private List<Coordinate> callMapMatchingApi(Coordinate[] coordinates) {
75-
// coordinates는 JTS 형식 (x=lng, y=lat)
81+
private Coordinate[] selectWaypoints(Coordinate[] coords) {
82+
int maxTotal = MAX_WAYPOINTS_PER_REQUEST * 2 - 1; // 49
83+
if (coords.length <= maxTotal) return coords;
84+
85+
double step = (double) (coords.length - 1) / (maxTotal - 1);
86+
Coordinate[] selected = new Coordinate[maxTotal];
87+
for (int i = 0; i < maxTotal; i++) {
88+
selected[i] = coords[Math.min((int) Math.round(i * step), coords.length - 1)];
89+
}
90+
return selected;
91+
}
92+
93+
private List<Coordinate> callDirectionsApi(Coordinate[] waypoints) {
7694
StringBuilder coordStr = new StringBuilder();
77-
StringBuilder radiuses = new StringBuilder();
78-
for (int i = 0; i < coordinates.length; i++) {
79-
if (i > 0) {
80-
coordStr.append(";");
81-
radiuses.append(";");
82-
}
83-
coordStr.append(coordinates[i].x).append(",").append(coordinates[i].y);
84-
radiuses.append("25"); // 25m 반경 내 도로 매칭
95+
for (int i = 0; i < waypoints.length; i++) {
96+
if (i > 0) coordStr.append(";");
97+
coordStr.append(waypoints[i].x).append(",").append(waypoints[i].y);
8598
}
8699

87-
String url = "https://api.mapbox.com/matching/v5/mapbox/walking/%s?access_token=%s&geometries=geojson&radiuses=%s&overview=full"
88-
.formatted(coordStr, apiKey, radiuses);
100+
String url = "https://api.mapbox.com/directions/v5/mapbox/walking/%s?access_token=%s&geometries=geojson&overview=full&continue_straight=true"
101+
.formatted(coordStr, apiKey);
89102

90103
try {
91-
String response = restClient.get()
92-
.uri(url)
93-
.retrieve()
94-
.body(String.class);
95-
104+
String response = restClient.get().uri(url).retrieve().body(String.class);
96105
JsonNode root = objectMapper.readTree(response);
97-
String code = root.path("code").asText();
98106

99-
if (!"Ok".equals(code)) {
100-
log.warn("Mapbox Map Matching returned: {}", code);
107+
if (!"Ok".equals(root.path("code").asText())) {
108+
log.warn("Mapbox: {}", root.path("message").asText());
101109
return List.of();
102110
}
103111

104-
JsonNode matchings = root.path("matchings");
105-
if (matchings.isEmpty()) {
106-
return List.of();
107-
}
112+
JsonNode routes = root.path("routes");
113+
if (routes.isEmpty()) return List.of();
108114

109-
// 첫 번째 매칭 결과의 geometry 좌표 추출
110-
JsonNode coords = matchings.path(0).path("geometry").path("coordinates");
115+
JsonNode coords = routes.path(0).path("geometry").path("coordinates");
111116
List<Coordinate> result = new ArrayList<>();
112-
for (JsonNode point : coords) {
113-
double lng = point.path(0).asDouble();
114-
double lat = point.path(1).asDouble();
115-
result.add(new Coordinate(lng, lat));
117+
for (JsonNode pt : coords) {
118+
result.add(new Coordinate(pt.path(0).asDouble(), pt.path(1).asDouble()));
116119
}
117-
118120
return result;
119121
} catch (Exception e) {
120-
log.error("Mapbox API call failed: {}", e.getMessage());
122+
log.error("Mapbox API failed: {}", e.getMessage());
121123
return List.of();
122124
}
123125
}

0 commit comments

Comments
 (0)