2121public 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