Skip to content

Commit f545aa8

Browse files
authored
✨ feat : 최적 경로 API 구현
✨ feat : 최적 경로 API 구현
2 parents 1dac874 + 659709e commit f545aa8

24 files changed

+683
-0
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2929

3030
implementation 'org.springframework.boot:spring-boot-starter-web'
31+
implementation 'org.springframework.boot:spring-boot-starter-webflux'
3132

3233
// Lombok
3334
compileOnly 'org.projectlombok:lombok'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.example.Centralthon.domain.route.algo;
2+
import com.example.Centralthon.domain.route.exception.RouteSegmentMissingException;
3+
import com.example.Centralthon.domain.route.model.*;
4+
import com.example.Centralthon.domain.route.web.dto.LocationRes;
5+
import lombok.AccessLevel;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
13+
public class PathStitcher {
14+
15+
public static CombinedPath stitch(List<Integer> order, Map<SegmentKey, PedSegment> segs) {
16+
long distSum = 0, durSum = 0;
17+
List<LocationRes> merged = new ArrayList<>();
18+
19+
for (int t = 0; t < order.size() - 1; t++) {
20+
int i = order.get(t), j = order.get(t + 1);
21+
22+
PedSegment seg = segs.get(SegmentKey.of(i, j));
23+
if (seg == null) throw new RouteSegmentMissingException();
24+
25+
List<LocationRes> p = seg.path();
26+
if (merged.isEmpty()) merged.addAll(p);
27+
else {
28+
if (!p.isEmpty() && !merged.isEmpty()
29+
&& equalsPoint(merged.get(merged.size()-1), p.get(0))) {
30+
merged.addAll(p.subList(1, p.size()));
31+
} else merged.addAll(p);
32+
}
33+
distSum += seg.distance();
34+
durSum += seg.duration();
35+
}
36+
return new CombinedPath(distSum, durSum, merged);
37+
}
38+
39+
// 좌표 중복 여부 확인
40+
private static boolean equalsPoint(LocationRes a, LocationRes b) {
41+
return Double.compare(a.lng(), b.lng()) == 0 && Double.compare(a.lat(), b.lat()) == 0;
42+
}
43+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.example.Centralthon.domain.route.algo;
2+
import lombok.AccessLevel;
3+
import lombok.NoArgsConstructor;
4+
5+
import java.util.ArrayList;
6+
import java.util.Collections;
7+
import java.util.List;
8+
9+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
10+
public final class TspSolver {
11+
12+
public static List<Integer> solveOpenTour(double[][] d) {
13+
int k = d.length;
14+
// 초기화 작업
15+
boolean[] used = new boolean[k];
16+
List<Integer> path = new ArrayList<>();
17+
int cur = 0;
18+
used[0] = true;
19+
path.add(0);
20+
21+
// 최근접 이웃 초기 해 구성
22+
for (int step = 1; step < k; step++) {
23+
int best = -1;
24+
double bestD = Double.POSITIVE_INFINITY;
25+
for (int j = 1; j < k; j++) if (!used[j]) {
26+
double cand = d[cur][j];
27+
if (cand < bestD) {
28+
bestD = cand;
29+
best = j;
30+
}
31+
}
32+
used[best] = true;
33+
path.add(best);
34+
cur = best;
35+
}
36+
37+
return twoOpt(path, d);
38+
}
39+
40+
// 교차 제거를 통한 로컬 개선
41+
private static List<Integer> twoOpt(List<Integer> tour, double[][] d) {
42+
boolean improved = true;
43+
int n = tour.size();
44+
while (improved) {
45+
improved = false;
46+
for (int i = 1; i < n - 2; i++) {
47+
for (int k = i + 1; k < n - 1; k++) {
48+
double delta =
49+
- d[tour.get(i - 1)][tour.get(i)]
50+
- d[tour.get(k)][tour.get(k + 1)]
51+
+ d[tour.get(i - 1)][tour.get(k)]
52+
+ d[tour.get(i)][tour.get(k + 1)];
53+
if (delta < -1e-6) {
54+
Collections.reverse(tour.subList(i, k + 1));
55+
improved = true;
56+
}
57+
}
58+
}
59+
}
60+
return tour;
61+
}
62+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.example.Centralthon.domain.route.client;
2+
3+
import com.example.Centralthon.domain.route.model.PedSegment;
4+
import com.example.Centralthon.domain.route.port.PedestrianRoutingPort;
5+
import com.example.Centralthon.domain.route.web.dto.LocationRes;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.core.ParameterizedTypeReference;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.web.reactive.function.client.WebClient;
10+
import reactor.core.publisher.Mono;
11+
12+
import java.util.Map;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
public class TmapPedestrianClient implements PedestrianRoutingPort {
17+
private final WebClient tmapWebClient;
18+
private final TmapPedestrianParser parser;
19+
20+
@Override
21+
public Mono<PedSegment> fetchSegment(LocationRes a, LocationRes b) {
22+
// 1. Tmap 보행자 경로 API 요청 바디 생성
23+
Map<String, Object> body = Map.of(
24+
"startX", String.valueOf(a.lng()),
25+
"startY", String.valueOf(a.lat()),
26+
"endX", String.valueOf(b.lng()),
27+
"endY", String.valueOf(b.lat()),
28+
"reqCoordType", "WGS84GEO",
29+
"resCoordType", "WGS84GEO",
30+
"startName", "S", "endName", "E"
31+
);
32+
33+
// 2. API 호출 → JSON 응답 수신 → parser로 PedSegment 변환
34+
return tmapWebClient.post()
35+
.uri(ub -> ub.path("/tmap/routes/pedestrian").queryParam("version","1").build())
36+
.bodyValue(body)
37+
.retrieve()
38+
.bodyToMono(new ParameterizedTypeReference<Map<String,Object>>() {})
39+
.map(parser::parsePedestrian);
40+
}
41+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.example.Centralthon.domain.route.client;
2+
3+
import com.example.Centralthon.domain.route.model.PedSegment;
4+
import com.example.Centralthon.domain.route.web.dto.LocationRes;
5+
import com.example.Centralthon.global.util.geo.GeoUtils;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
@Component
13+
public class TmapPedestrianParser {
14+
public PedSegment parsePedestrian(Map<String, Object> pedRes) {
15+
long totalDistance = 0L;
16+
long totalDuration = 0L;
17+
List<LocationRes> path = new ArrayList<>();
18+
19+
List<Map<String, Object>> features =
20+
(List<Map<String, Object>>) pedRes.getOrDefault("features", List.of());
21+
22+
// 1) 총거리/시간 추출
23+
for (Map<String, Object> f : features) {
24+
Map<String, Object> props = (Map<String, Object>) f.get("properties");
25+
if (props == null) continue;
26+
27+
Object d = props.get("totalDistance");
28+
long candDist = asLong(d);
29+
if (candDist > 0) totalDistance = candDist;
30+
31+
Object t = props.get("totalTime");
32+
long candTime = asLong(t);
33+
if (candTime > 0) totalDuration = candTime;
34+
if (totalDistance > 0 && totalDuration > 0) break;
35+
}
36+
37+
// 2) LineString 경로 좌표 이어붙이기 (중복점 제거)
38+
for (Map<String, Object> f : features) {
39+
Map<String, Object> geom = (Map<String, Object>) f.get("geometry");
40+
if (geom == null) continue;
41+
String type = String.valueOf(geom.get("type"));
42+
if (!"LineString".equalsIgnoreCase(type)) continue;
43+
44+
List<List<Number>> coords = (List<List<Number>>) geom.get("coordinates");
45+
if (coords == null) continue;
46+
47+
for (List<Number> xy : coords) {
48+
if (xy.size() < 2) continue;
49+
double x = xy.get(0).doubleValue(); // lon
50+
double y = xy.get(1).doubleValue(); // lat
51+
LocationRes p = new LocationRes(x, y); // (lng, lat)
52+
53+
if (path.isEmpty()) {
54+
path.add(p);
55+
} else {
56+
LocationRes last = path.get(path.size() - 1);
57+
if (Double.compare(last.lng(), p.lng()) != 0 || Double.compare(last.lat(), p.lat()) != 0) {
58+
path.add(p);
59+
}
60+
}
61+
}
62+
}
63+
// 3) 총거리 누락 시 하버사인 합으로 보정
64+
if (totalDistance <= 0 && path.size() >= 2) {
65+
totalDistance = Math.round(GeoUtils.sumHaversineMeters(path));
66+
}
67+
68+
return new PedSegment(totalDistance, totalDuration, path);
69+
}
70+
71+
private static long asLong(Object v) {
72+
if (v == null) return 0L;
73+
if (v instanceof Number n) return n.longValue();
74+
if (v instanceof String s && !s.isBlank()) {
75+
try { return Long.parseLong(s.trim()); } catch (NumberFormatException ignored) {}
76+
}
77+
return 0L;
78+
}
79+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.example.Centralthon.domain.route.exception;
2+
3+
import com.example.Centralthon.global.response.code.BaseResponseCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public enum RouteErrorCode implements BaseResponseCode {
10+
ROUTE_SEGMENT_MISSING("ROUTE_500_1", 500, "서버에서 세그먼트가 누락되었습니다."),
11+
ROUTE_NOT_CREATED("ROUTE_500_2",500,"서버에서 보행자 경로를 생성하지 못했습니다.");
12+
13+
private final String code;
14+
private final int httpStatus;
15+
private final String message;
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.Centralthon.domain.route.exception;
2+
3+
import com.example.Centralthon.global.exception.BaseException;
4+
5+
public class RouteNotCreatedException extends BaseException {
6+
public RouteNotCreatedException() {
7+
super(RouteErrorCode.ROUTE_NOT_CREATED);
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.Centralthon.domain.route.exception;
2+
3+
import com.example.Centralthon.global.exception.BaseException;
4+
5+
public class RouteSegmentMissingException extends BaseException {
6+
public RouteSegmentMissingException() {
7+
super(RouteErrorCode.ROUTE_SEGMENT_MISSING);
8+
}
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.Centralthon.domain.route.model;
2+
3+
import com.example.Centralthon.domain.route.web.dto.LocationRes;
4+
import java.util.List;
5+
6+
// 최종 합성 경로 (사용자 출발 → 경유지들 → 도착)
7+
public record CombinedPath(
8+
long distanceMeters,
9+
long durationSeconds,
10+
List<LocationRes> path) {
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.example.Centralthon.domain.route.model;
2+
import java.util.Map;
3+
4+
// 보행 경로 행렬 (Pedestrian Matrix)
5+
public record PedMatrix(
6+
double[][] distMatrix, // 비용(거리/시간) 테이블
7+
Map<SegmentKey, PedSegment> segments) {
8+
}

0 commit comments

Comments
 (0)