Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// Lombok
compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.Centralthon.domain.route.algo;
import com.example.Centralthon.domain.route.exception.RouteSegmentMissingException;
import com.example.Centralthon.domain.route.model.*;
import com.example.Centralthon.domain.route.web.dto.LocationRes;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PathStitcher {

public static CombinedPath stitch(List<Integer> order, Map<SegmentKey, PedSegment> segs) {
long distSum = 0, durSum = 0;
List<LocationRes> merged = new ArrayList<>();

for (int t = 0; t < order.size() - 1; t++) {
int i = order.get(t), j = order.get(t + 1);

PedSegment seg = segs.get(SegmentKey.of(i, j));
if (seg == null) throw new RouteSegmentMissingException();

List<LocationRes> p = seg.path();
if (merged.isEmpty()) merged.addAll(p);
else {
if (!p.isEmpty() && !merged.isEmpty()
&& equalsPoint(merged.get(merged.size()-1), p.get(0))) {
merged.addAll(p.subList(1, p.size()));
} else merged.addAll(p);
}
distSum += seg.distance();
durSum += seg.duration();
}
return new CombinedPath(distSum, durSum, merged);
}

// 좌표 중복 여부 확인
private static boolean equalsPoint(LocationRes a, LocationRes b) {
return Double.compare(a.lng(), b.lng()) == 0 && Double.compare(a.lat(), b.lat()) == 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example.Centralthon.domain.route.algo;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class TspSolver {

public static List<Integer> solveOpenTour(double[][] d) {
int k = d.length;
// 초기화 작업
boolean[] used = new boolean[k];
List<Integer> path = new ArrayList<>();
int cur = 0;
used[0] = true;
path.add(0);

// 최근접 이웃 초기 해 구성
for (int step = 1; step < k; step++) {
int best = -1;
double bestD = Double.POSITIVE_INFINITY;
for (int j = 1; j < k; j++) if (!used[j]) {
double cand = d[cur][j];
if (cand < bestD) {
bestD = cand;
best = j;
}
}
used[best] = true;
path.add(best);
cur = best;
}

return twoOpt(path, d);
}

// 교차 제거를 통한 로컬 개선
private static List<Integer> twoOpt(List<Integer> tour, double[][] d) {
boolean improved = true;
int n = tour.size();
while (improved) {
improved = false;
for (int i = 1; i < n - 2; i++) {
for (int k = i + 1; k < n - 1; k++) {
double delta =
- d[tour.get(i - 1)][tour.get(i)]
- d[tour.get(k)][tour.get(k + 1)]
+ d[tour.get(i - 1)][tour.get(k)]
+ d[tour.get(i)][tour.get(k + 1)];
if (delta < -1e-6) {
Collections.reverse(tour.subList(i, k + 1));
improved = true;
}
}
}
}
return tour;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.Centralthon.domain.route.client;

import com.example.Centralthon.domain.route.model.PedSegment;
import com.example.Centralthon.domain.route.port.PedestrianRoutingPort;
import com.example.Centralthon.domain.route.web.dto.LocationRes;
import lombok.RequiredArgsConstructor;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Map;

@Component
@RequiredArgsConstructor
public class TmapPedestrianClient implements PedestrianRoutingPort {
private final WebClient tmapWebClient;
private final TmapPedestrianParser parser;

@Override
public Mono<PedSegment> fetchSegment(LocationRes a, LocationRes b) {
// 1. Tmap 보행자 경로 API 요청 바디 생성
Map<String, Object> body = Map.of(
"startX", String.valueOf(a.lng()),
"startY", String.valueOf(a.lat()),
"endX", String.valueOf(b.lng()),
"endY", String.valueOf(b.lat()),
"reqCoordType", "WGS84GEO",
"resCoordType", "WGS84GEO",
"startName", "S", "endName", "E"
);

// 2. API 호출 → JSON 응답 수신 → parser로 PedSegment 변환
return tmapWebClient.post()
.uri(ub -> ub.path("/tmap/routes/pedestrian").queryParam("version","1").build())
.bodyValue(body)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String,Object>>() {})
.map(parser::parsePedestrian);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.example.Centralthon.domain.route.client;

import com.example.Centralthon.domain.route.model.PedSegment;
import com.example.Centralthon.domain.route.web.dto.LocationRes;
import com.example.Centralthon.global.util.geo.GeoUtils;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Component
public class TmapPedestrianParser {
public PedSegment parsePedestrian(Map<String, Object> pedRes) {
long totalDistance = 0L;
long totalDuration = 0L;
List<LocationRes> path = new ArrayList<>();

List<Map<String, Object>> features =
(List<Map<String, Object>>) pedRes.getOrDefault("features", List.of());

// 1) 총거리/시간 추출
for (Map<String, Object> f : features) {
Map<String, Object> props = (Map<String, Object>) f.get("properties");
if (props == null) continue;

Object d = props.get("totalDistance");
long candDist = asLong(d);
if (candDist > 0) totalDistance = candDist;

Object t = props.get("totalTime");
long candTime = asLong(t);
if (candTime > 0) totalDuration = candTime;
if (totalDistance > 0 && totalDuration > 0) break;
}

// 2) LineString 경로 좌표 이어붙이기 (중복점 제거)
for (Map<String, Object> f : features) {
Map<String, Object> geom = (Map<String, Object>) f.get("geometry");
if (geom == null) continue;
String type = String.valueOf(geom.get("type"));
if (!"LineString".equalsIgnoreCase(type)) continue;

List<List<Number>> coords = (List<List<Number>>) geom.get("coordinates");
if (coords == null) continue;

for (List<Number> xy : coords) {
if (xy.size() < 2) continue;
double x = xy.get(0).doubleValue(); // lon
double y = xy.get(1).doubleValue(); // lat
LocationRes p = new LocationRes(x, y); // (lng, lat)

if (path.isEmpty()) {
path.add(p);
} else {
LocationRes last = path.get(path.size() - 1);
if (Double.compare(last.lng(), p.lng()) != 0 || Double.compare(last.lat(), p.lat()) != 0) {
path.add(p);
}
}
}
}
// 3) 총거리 누락 시 하버사인 합으로 보정
if (totalDistance <= 0 && path.size() >= 2) {
totalDistance = Math.round(GeoUtils.sumHaversineMeters(path));
}

return new PedSegment(totalDistance, totalDuration, path);
}

private static long asLong(Object v) {
if (v == null) return 0L;
if (v instanceof Number n) return n.longValue();
if (v instanceof String s && !s.isBlank()) {
try { return Long.parseLong(s.trim()); } catch (NumberFormatException ignored) {}
}
return 0L;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.Centralthon.domain.route.exception;

import com.example.Centralthon.global.response.code.BaseResponseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum RouteErrorCode implements BaseResponseCode {
ROUTE_SEGMENT_MISSING("ROUTE_500_1", 500, "서버에서 세그먼트가 누락되었습니다."),
ROUTE_NOT_CREATED("ROUTE_500_2",500,"서버에서 보행자 경로를 생성하지 못했습니다.");

private final String code;
private final int httpStatus;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.Centralthon.domain.route.exception;

import com.example.Centralthon.global.exception.BaseException;

public class RouteNotCreatedException extends BaseException {
public RouteNotCreatedException() {
super(RouteErrorCode.ROUTE_NOT_CREATED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.Centralthon.domain.route.exception;

import com.example.Centralthon.global.exception.BaseException;

public class RouteSegmentMissingException extends BaseException {
public RouteSegmentMissingException() {
super(RouteErrorCode.ROUTE_SEGMENT_MISSING);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.Centralthon.domain.route.model;

import com.example.Centralthon.domain.route.web.dto.LocationRes;
import java.util.List;

// 최종 합성 경로 (사용자 출발 → 경유지들 → 도착)
public record CombinedPath(
long distanceMeters,
long durationSeconds,
List<LocationRes> path) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.Centralthon.domain.route.model;
import java.util.Map;

// 보행 경로 행렬 (Pedestrian Matrix)
public record PedMatrix(
double[][] distMatrix, // 비용(거리/시간) 테이블
Map<SegmentKey, PedSegment> segments) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.Centralthon.domain.route.model;

import com.example.Centralthon.domain.route.web.dto.LocationRes;
import java.util.List;

// 보행 경로 세그먼트 (두 지점 A→B 사이)
public record PedSegment(
long distance,
long duration,
List<LocationRes> path // A -> B까지의 보행 경로 좌표 리스트
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.Centralthon.domain.route.model;

// 경로 행렬의 키 (두 노드 간 구간을 고유하게 식별)
public record SegmentKey(
int i,
int j) {
public SegmentKey {
if (i > j) {
int tmp = i;
i = j;
j = tmp;
}
}

public static SegmentKey of(int a, int b) {
return new SegmentKey(a, b);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.Centralthon.domain.route.port;

import com.example.Centralthon.domain.route.model.PedSegment;
import com.example.Centralthon.domain.route.web.dto.LocationRes;
import reactor.core.publisher.Mono;

public interface PedestrianRoutingPort {
Mono<PedSegment> fetchSegment(LocationRes a, LocationRes b);
}
Loading