diff --git a/README.md b/README.md index d6299154c..b7b7c8acb 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,46 @@ # 지하철 노선도 경로 조회 미션 -- 등록된 지하철 노선도에서 경로를 조회하는 기능을 구현한다. -
+등록된 지하철 노선도에서 최단 거리/최소 시간으로 경로를 조회하는 프로그램 -## 🚀 기능 요구사항 +## 기능 목록 -> 프리코스 3주차 미션에서 사용한 코드를 참고해도 무관하다. + - [x] 프로그램 시작 시 역, 노선, 구간 정보를 초기 설정한다. -### 초기 설정 -- 프로그램 시작 시 역, 노선, 구간 정보를 초기 설정 해야 한다. -- 거리와 소요 시간은 양의 정수이며 단위는 km와 분을 의미한다. -- 아래의 사전 등록 정보로 반드시 초기 설정을 한다. + - [x] 메인 메뉴를 출력하고 사용자의 선택을 입력을 받는다. -``` - 1. 지하철역으로 교대역, 강남역, 역삼역, 남부터미널역, 양재역, 양재시민의숲역, 매봉역이 등록되어 있다. - 2. 지하철 노선으로 2호선, 3호선, 신분당선이 등록되어 있다. - 3. 노선에 역이 아래와 같이 등록되어 있다.(왼쪽 끝이 상행 종점) - - 2호선: 교대역 - ( 2km / 3분 ) - 강남역 - ( 2km / 3분 ) - 역삼역 - - 3호선: 교대역 - ( 3km / 2분 ) - 남부터미널역 - ( 6km / 5분 ) - 양재역 - ( 1km / 1분 ) - 매봉역 - - 신분당선: 강남역 - ( 2km / 8분 ) - 양재역 - ( 10km / 3분 ) - 양재시민의숲역 - ``` - -### 경로 조회 기능 -- 출발역과 도착역을 입력받아 경로를 조회한다. -- 경로 조회 시 총 거리, 총 소요 시간도 함께 출력한다. -- 경로 조회 기준은 `최단 거리` `최소 시간`이 있다. - -### 예외 처리 -- 경로 조회 시 출발역과 도착역이 같으면 에러를 출력한다. -- 경로 조회 시 출발역과 도착역이 연결되어 있지 않으면 에러를 출력한다. -- 그 외 정상적으로 프로그램이 수행되지 않은 경우 에러를 출력한다. + - [x] 메인 메뉴를 출력한다. + - [x] 사용자의 입력을 받는다. + - [x] 1 을 입력받으면 경로 조회 화면으로 전환한다. + - [x] Q 를 입력받으면 프로그램을 종료한다. + - [x] [예외 처리] 메뉴에 없는 값을 입력받으면 에러를 출력한다. -
+ - [x] 경로를 조회할 기준을 출력하고 사용자의 선택을 입력받는다. -## ✍🏻 입출력 요구사항 -- `프로그래밍 실행 결과 예시`와 동일하게 입출력을 구현한다. -- 기대하는 출력 결과는 `[INFO]`를 붙여서 출력한다. 출력값의 형식은 예시와 동일하게 한다. -- 에러 발생 시 `[ERROR]`를 붙여서 출력한다. 에러의 문구는 자유롭게 작성한다. + - [x] 경로를 조회할 기준 메뉴를 출력한다. + - [x] 1 을 입력받으면 최단 거리를 기준으로 경로를 조회한다. + - [x] 2 를 입력받으면 최소 시간을 기준으로 경로를 조회한다. + - [x] B 를 입력받으면 메인 메뉴로 돌아간다. + - [x] [예외 처리] 메뉴에 없는 값을 입력받으면 에러를 출력한다. -### 💻 프로그래밍 실행 결과 예시 -#### 경로 조회 -``` -## 메인 화면 -1. 경로 조회 -Q. 종료 + - [x] 경로를 조회하는 기능에서는 출발역과 도착역을 설정한다. + + - [x] 출발역을 입력받는다. + - [x] 도착역을 입력받는다. + - [x] [예외 처리] 등록되지 않은 역을 입력받으면 에러를 출력한다. + - [x] [예외 처리] 입력받은 출발역과 도착역이 같으면 에러를 출력한다. -## 원하는 기능을 선택하세요. -1 + - [x] 출발역에서 도착역까지 가는 최단 거리 경로를 구한다. -## 경로 기준 -1. 최단 거리 -2. 최소 시간 -B. 돌아가기 + - [x] [예외 처리] 출발 역과 도착역이 연결되어 있지 않으면 에러를 출력한다. -## 원하는 기능을 선택하세요. -1 + - [x] 출발역에서 도착역까지 가는 최단 시간 경로를 구한다. -## 출발역을 입력하세요. -교대역 + - [x] [예외 처리] 출발 역과 도착역이 연결되어 있지 않으면 에러를 출력한다. -## 도착역을 입력하세요. -양재역 + - [x] 출발역에서 도착역 까지 가는 경로를 포함하여 총 거리와 총 소요 시간을 아래와 같이 출력한다. -## 조회 결과 +``` +##조회 결과 [INFO] --- [INFO] 총 거리: 6km [INFO] 총 소요 시간: 14분 @@ -70,184 +48,14 @@ B. 돌아가기 [INFO] 교대역 [INFO] 강남역 [INFO] 양재역 - -## 메인 화면 -1. 경로 조회 -Q. 종료 - -... -``` - -#### 에러 출력 예시 - -``` -## 메인 화면 -1. 경로 조회 -Q. 종료 - -## 원하는 기능을 선택하세요. -1 - -## 경로 기준 -1. 최단 거리 -2. 최소 시간 -B. 돌아가기 - -## 원하는 기능을 선택하세요. -1 - -## 출발역을 입력하세요. -강남역 - -## 도착역을 입력하세요. -강남역 - -[ERROR] 출발역과 도착역이 동일합니다. - -## 경로 기준 -1. 최단 거리 -2. 최소 시간 -B. 돌아가기 - -## 원하는 기능을 선택하세요. - -... - -``` - -
- -## 🎱 프로그래밍 요구사항 -- 자바 코드 컨벤션을 지키면서 프로그래밍한다. - - 기본적으로 [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)을 원칙으로 한다. - - 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다. -- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. - - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. - - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. -- 3항 연산자를 쓰지 않는다. -- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. - - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. -- else 예약어를 쓰지 않는다. - - 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. - - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. -- 프로그래밍 요구사항에서 별도로 변경 불가 안내가 없는 경우 파일 수정과 패키지 이동을 자유롭게 할 수 있다. -- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 `[ERROR]` 로 시작해야 한다. - -### 프로그래밍 요구사항 - Application -- Application 클래스를 활용해 구현해야 한다. -- Application의 패키지 구조는 변경하지 않는다. -- Application 클래스에 있는 Scanner를 사용하고 별도의 Scanner 객체를 만들지 않는다. -```java -public class Application { - public static void main(String[] args) { - final Scanner scanner = new Scanner(System.in); - ... - } -} ``` -### 프로그래밍 요구사항 - Station, Line -- Station, Line 클래스를 활용하여 지하철역과 노선을 구현해야 한다. -- 제공하는 각 클래스의 기본 생성자를 추가할 수 없다. -- 필드(인스턴스 변수)인 name의 접근 제어자 private을 변경할 수 없다. -- 가능하면 setter 메소드(ex. setXXX)를 추가하지 않고 구현한다. - -```java -public class Station { - private String name; - - public Station(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - // 추가 기능 구현 -} +## 요구사항 -``` - -### 프로그래밍 요구사항 - StationRepository, LineRepository -- Station과 Line의 상태를 저장할 수 있는 StationRepository, LineRepository를 제공한다. -- 필요 시 StationRepository, LineRepository 이 외 추가로 Repository를 만들 수 있다. -- 추가로 생성되는 객체에 대해서 XXXRepository 네이밍으로 저장 클래스를 추가한다. -- 객체들의 상태를 관리하기 위해서 XXXRepository 클래스를 활용해 저장 로직을 구현해야 한다. -- 작성된 메서드는 수정할 수 없고, 필요에 따라 메서드를 자유롭게 추가할 수 있다. - -```java -public class StationRepository { - private static final List stations = new ArrayList<>(); - - public static List stations() { - return Collections.unmodifiableList(stations); - } - - public static void addStation(Station station) { - stations.add(station); - } - - public static boolean deleteStation(String name) { - return stations.removeIf(station -> Objects.equals(station.getName(), name)); - } - - public static void deleteAll() { - stations.clear(); - } -} -``` - -
- -## ❗️힌트 -### 최단 경로 라이브러리 -- jgrapht 라이브러리를 활용하면 간편하게 최단거리를 조회할 수 있음 -- Dijkstra 알고리즘을 반드시 이해할 필요는 없고 미션에 적용할 정도로만 이해하면 됨 -- JGraphtTest 클래스의 테스트를 활용하여 미션에 필요한 라이브러리의 기능을 학습할 수 있음 -- 정점(vertex)과 간선(edge), 그리고 가중치 개념을 이용 - - 정점: 지하철역 - - 간선: 지하철역 연결정보 - - 가중치: 거리 or 소요 시간 -- 최단 거리 기준 조회 시 가중치를 거리로 설정 - -```java -@Test -public void getDijkstraShortestPath() { - WeightedMultigraph graph - = new WeightedMultigraph(DefaultWeightedEdge.class); - graph.addVertex("v1"); - graph.addVertex("v2"); - graph.addVertex("v3"); - graph.setEdgeWeight(graph.addEdge("v1", "v2"), 2); - graph.setEdgeWeight(graph.addEdge("v2", "v3"), 2); - graph.setEdgeWeight(graph.addEdge("v1", "v3"), 100); - - DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); - List shortestPath = dijkstraShortestPath.getPath("v3", "v1").getVertexList(); - - assertThat(shortestPath.size()).isEqualTo(3); -} -``` - -#### 테스트 설명 - - - -- 역 사이의 거리를 고려하지 않는 경우 V1->V3 경로가 최단 경로 -- 역 사이의 거리를 고려할 경우 V1->V3 경로의 거리는 100km, V1->V2->V3 경로의 거리는 4km이므로 최단 경로는 V1->V2->V3 - -
- -## 📈 진행 요구사항 -- 미션은 [java-subway-path-precourse 저장소](https://github.com/woowacourse/java-subway-path-precourse) 를 fork/clone해 시작한다. -- 기능을 구현하기 전에 java-subway-path-precourse/docs/README.md 파일에 구현할 기능 목록을 정리해 추가한다. -- git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다. - - [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남긴다. -- [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 절차를 따라 미션을 제출한다. - - [프리코스 과제 FAQ](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse/faq) 문서를 참고하여 진행할 수 있다. -
- -## 📝 License - -This project is [MIT](https://github.com/woowacourse/java-subway-path-precourse/blob/master/LICENSE.md) licensed. + - 거리는 km, 시간은 분 단위로 계산한다. + + - 기대하는 출력 결과는 [INFO]를 붙인다. + + - 에러 출력은 [ERROR]를 붙인다. + + - jgrapht 라이브러리를 활용한다. \ No newline at end of file diff --git a/src/main/java/subway/Application.java b/src/main/java/subway/Application.java index 0bcf786cc..e2e9fce0b 100644 --- a/src/main/java/subway/Application.java +++ b/src/main/java/subway/Application.java @@ -1,10 +1,16 @@ package subway; +import subway.controller.MainController; + import java.util.Scanner; public class Application { public static void main(String[] args) { final Scanner scanner = new Scanner(System.in); - // TODO: 프로그램 구현 + + InitialSetting.settingInitialSubways(); + + MainController mainController = MainController.getInstance(); + mainController.run(scanner); } } diff --git a/src/main/java/subway/InitialSetting.java b/src/main/java/subway/InitialSetting.java new file mode 100644 index 000000000..196128780 --- /dev/null +++ b/src/main/java/subway/InitialSetting.java @@ -0,0 +1,89 @@ +package subway; + +import subway.domain.Line; +import subway.domain.RequiredResources; +import subway.domain.Section; +import subway.domain.Station; +import subway.repository.LineRepository; +import subway.repository.SectionRepository; +import subway.repository.StationRepository; + +public class InitialSetting { + private static final String GYODAE = "교대역"; + private static final String GANGNAM = "강남역"; + private static final String YUCKSAM = "역삼역"; + private static final String NAMBUTERMINAL = "남부터미널역"; + private static final String YANGGAE = "양재역"; + private static final String YANGGAEFORREST = "양재시민의숲역"; + private static final String MAEBONG = "매봉역"; + private static final String LINE_TWO = "2호선"; + private static final String LINE_THREE = "3호선"; + private static final String LINE_SINBUNDANG = "신분당선"; + private static final String[] LINE_TWO_STATIONS = {"교대역", "강남역", "역삼역"}; + private static final String[] LINE_THREE_STATIONS = {"교대역", "남부터미널역", "양재역", "매봉역"}; + private static final String[] LINE_SINBUNDANG_STATIONS = {"강남역", "양재역", "양재시민의숲역"}; + + public static void settingInitialSubways() { + deleteAllRepository(); + + settingInitialStations(); + + settingInitialLines(LINE_TWO, LINE_TWO_STATIONS); + settingInitialLines(LINE_THREE, LINE_THREE_STATIONS); + settingInitialLines(LINE_SINBUNDANG, LINE_SINBUNDANG_STATIONS); + + settingInitialSection(); + } + + private static void deleteAllRepository() { + StationRepository.deleteAll(); + LineRepository.deleteAll(); + SectionRepository.deleteAll(); + } + + private static void settingInitialStations() { + StationRepository.addStation(new Station(GYODAE)); + StationRepository.addStation(new Station(GANGNAM)); + StationRepository.addStation(new Station(YUCKSAM)); + StationRepository.addStation(new Station(NAMBUTERMINAL)); + StationRepository.addStation(new Station(YANGGAE)); + StationRepository.addStation(new Station(YANGGAEFORREST)); + StationRepository.addStation(new Station(MAEBONG)); + } + + private static void settingInitialLines(String lineName, String[] stationNames) { + Line line = new Line(lineName); + addStationInLine(line, stationNames); + LineRepository.addLine(line); + } + + private static void addStationInLine(Line line, String[] stationNames) { + for (String stationName : stationNames) { + line.addLineStation(new Station(stationName)); + } + } + + private static void settingInitialSection() { + Section section = new Section(new Station(GYODAE), new Station(GANGNAM)); + RequiredResources requiredResources = new RequiredResources(2, 3); + SectionRepository.addSection(section, requiredResources); + section = new Section(new Station(GANGNAM), new Station(YUCKSAM)); + requiredResources = new RequiredResources(2, 3); + SectionRepository.addSection(section, requiredResources); + section = new Section(new Station(GYODAE), new Station(NAMBUTERMINAL)); + requiredResources = new RequiredResources(3, 2); + SectionRepository.addSection(section, requiredResources); + section = new Section(new Station(NAMBUTERMINAL), new Station(YANGGAE)); + requiredResources = new RequiredResources(6, 5); + SectionRepository.addSection(section, requiredResources); + section = new Section(new Station(YANGGAE), new Station(MAEBONG)); + requiredResources = new RequiredResources(1, 1); + SectionRepository.addSection(section, requiredResources); + section = new Section(new Station(GANGNAM), new Station(YANGGAE)); + requiredResources = new RequiredResources(2, 8); + SectionRepository.addSection(section, requiredResources); + section = new Section(new Station(YANGGAE), new Station(YANGGAEFORREST)); + requiredResources = new RequiredResources(10, 3); + SectionRepository.addSection(section, requiredResources); + } +} diff --git a/src/main/java/subway/controller/Controller.java b/src/main/java/subway/controller/Controller.java new file mode 100644 index 000000000..9ceb375b9 --- /dev/null +++ b/src/main/java/subway/controller/Controller.java @@ -0,0 +1,7 @@ +package subway.controller; + +import java.util.Scanner; + +public interface Controller { + String mappingMenu(Scanner scanner); +} diff --git a/src/main/java/subway/controller/MainController.java b/src/main/java/subway/controller/MainController.java new file mode 100644 index 000000000..bcec3d531 --- /dev/null +++ b/src/main/java/subway/controller/MainController.java @@ -0,0 +1,45 @@ +package subway.controller; + +import subway.menus.MainMenu; +import subway.views.mainviews.MainInputView; +import subway.views.mainviews.MainOutputView; + +import java.util.Scanner; + +public class MainController implements Controller{ + private static final String END_CODE = "Q"; + private static final MainController MAIN_CONTROLLER = new MainController(); + + private MainController() { + } + + public static MainController getInstance() { + return MAIN_CONTROLLER; + } + + public void run(Scanner scanner) { + String selectedOption; + do { + selectedOption = mappingMenu(scanner); + } while (!selectedOption.equals(END_CODE)); + } + + public String mappingMenu(Scanner scanner) { + try { + MainOutputView.printMainMenu(); + String selectedOption = MainInputView.inputMainOption(scanner); + branchBySelectedOption(selectedOption, scanner); + return selectedOption; + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + return mappingMenu(scanner); + } + } + + private void branchBySelectedOption(String selectedOption, Scanner scanner) { + if (selectedOption.equals(END_CODE)) { + return; + } + MainMenu.execute(selectedOption, scanner); + } +} diff --git a/src/main/java/subway/controller/RouteController.java b/src/main/java/subway/controller/RouteController.java new file mode 100644 index 000000000..35d3c59f5 --- /dev/null +++ b/src/main/java/subway/controller/RouteController.java @@ -0,0 +1,38 @@ +package subway.controller; + +import subway.menus.RouteMenu; +import subway.views.routeviews.RouteInputView; +import subway.views.routeviews.RouteOutputView; + +import java.util.Scanner; + +public class RouteController implements Controller{ + private static final String GO_BACK_CODE = "B"; + private static final RouteController ROUTE_CONTROLLER = new RouteController(); + + private RouteController() { + } + + public static RouteController getInstance() { + return ROUTE_CONTROLLER; + } + + public String mappingMenu(Scanner scanner) { + try { + RouteOutputView.printRouteMenu(); + String selectedOption = RouteInputView.inputRouteOption(scanner); + branchBySelectedOption(selectedOption, scanner); + return selectedOption; + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + return mappingMenu(scanner); + } + } + + private void branchBySelectedOption(String selectedOption, Scanner scanner) { + if (selectedOption.equals(GO_BACK_CODE)) { + return; + } + RouteMenu.execute(selectedOption, scanner); + } +} diff --git a/src/main/java/subway/domain/Distance.java b/src/main/java/subway/domain/Distance.java new file mode 100644 index 000000000..848b34566 --- /dev/null +++ b/src/main/java/subway/domain/Distance.java @@ -0,0 +1,13 @@ +package subway.domain; + +public class Distance { + private int distance; + + public Distance(int distance) { + this.distance = distance; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/subway/domain/DistanceMap.java b/src/main/java/subway/domain/DistanceMap.java new file mode 100644 index 000000000..d035535e2 --- /dev/null +++ b/src/main/java/subway/domain/DistanceMap.java @@ -0,0 +1,22 @@ +package subway.domain; + +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.WeightedMultigraph; +import subway.repository.SectionRepository; + +import java.util.List; +import java.util.Map; + +public class DistanceMap implements SubwayGraph{ + public void addWeight() { + Map sections = SectionRepository.sections(); + sections.forEach((key, value) + -> subwayGraph.setEdgeWeight(subwayGraph.addEdge(key.getFirstStation(), key.getSecondStation()), value.getDistanceResource().getDistance())); + } + + public List getShortestRoute(WeightedMultigraph graph, Station first, Station second) { + DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); + isConnectedMap(dijkstraShortestPath.getPath(first, second)); + return dijkstraShortestPath.getPath(first, second).getVertexList(); + } +} diff --git a/src/main/java/subway/domain/Line.java b/src/main/java/subway/domain/Line.java index f4d738d5a..c816c1751 100644 --- a/src/main/java/subway/domain/Line.java +++ b/src/main/java/subway/domain/Line.java @@ -1,15 +1,40 @@ package subway.domain; public class Line { - private String name; + private final String name; + private final LineStations stations; public Line(String name) { this.name = name; + this.stations = new LineStations(); } public String getName() { return name; } - // 추가 기능 구현 + public void addLineStation(Station station) { + stations.addLineStation(station); + } + + @Override + public boolean equals(Object object) { + if (getClass() != object.getClass()) { + return false; + } + boolean isEqualObject = false; + Line line = (Line) object; + if (getName().equals(line.getName())) { + isEqualObject = true; + } + return isEqualObject; + } + + @Override + public int hashCode() { + final int prime = 31; + int hashCode = 1; + hashCode = prime * hashCode + getName().hashCode(); + return hashCode; + } } diff --git a/src/main/java/subway/domain/LineStations.java b/src/main/java/subway/domain/LineStations.java new file mode 100644 index 000000000..78e073213 --- /dev/null +++ b/src/main/java/subway/domain/LineStations.java @@ -0,0 +1,23 @@ +package subway.domain; + +import java.util.ArrayList; +import java.util.List; + +public class LineStations { + private final List stations; + + public LineStations() { + this.stations = new ArrayList<>(); + } + + public void addLineStation(Station station) { + stations.add(station); + } + + public Boolean isContain(Station station) { + if (stations.contains(station)) { + return true; + } + return false; + } +} diff --git a/src/main/java/subway/domain/RequiredResources.java b/src/main/java/subway/domain/RequiredResources.java new file mode 100644 index 000000000..d1dc835b8 --- /dev/null +++ b/src/main/java/subway/domain/RequiredResources.java @@ -0,0 +1,19 @@ +package subway.domain; + +public class RequiredResources { + private final Distance distance; + private final Time time; + + public RequiredResources(int distance, int time) { + this.distance = new Distance(distance); + this.time = new Time(time); + } + + public Distance getDistanceResource() { + return distance; + } + + public Time getTimeResource() { + return time; + } +} diff --git a/src/main/java/subway/domain/Section.java b/src/main/java/subway/domain/Section.java new file mode 100644 index 000000000..92874ef74 --- /dev/null +++ b/src/main/java/subway/domain/Section.java @@ -0,0 +1,42 @@ +package subway.domain; + +public class Section { + private final Station firstStation; + private final Station secondStation; + + public Section(Station firstStation, Station secondStation) { + this.firstStation = firstStation; + this.secondStation = secondStation; + } + + public Station getFirstStation() { + return firstStation; + } + + public Station getSecondStation() { + return secondStation; + } + + @Override + public boolean equals(Object object) { + if (getClass() != object.getClass()) { + return false; + } + boolean isEqualObject = false; + Section section = (Section) object; + if (firstStation.getName().equals(section.firstStation.getName()) + && secondStation.getName().equals(section.secondStation.getName())) { + isEqualObject = true; + } + return isEqualObject; + } + + @Override + public int hashCode() { + final int prime = 31; + int hashCode = 1; + hashCode = prime * hashCode + firstStation.getName().hashCode() + + secondStation.getName().hashCode(); + return hashCode; + } +} diff --git a/src/main/java/subway/domain/Station.java b/src/main/java/subway/domain/Station.java index bdb142590..dba8be823 100644 --- a/src/main/java/subway/domain/Station.java +++ b/src/main/java/subway/domain/Station.java @@ -11,5 +11,24 @@ public String getName() { return name; } - // 추가 기능 구현 + @Override + public boolean equals(Object object) { + if (getClass() != object.getClass()) { + return false; + } + boolean isEqualObject = false; + Station station = (Station) object; + if (getName().equals(station.getName())) { + isEqualObject = true; + } + return isEqualObject; + } + + @Override + public int hashCode() { + final int prime = 31; + int hashCode = 1; + hashCode = prime * hashCode + getName().hashCode(); + return hashCode; + } } diff --git a/src/main/java/subway/domain/SubwayGraph.java b/src/main/java/subway/domain/SubwayGraph.java new file mode 100644 index 000000000..3b37980a3 --- /dev/null +++ b/src/main/java/subway/domain/SubwayGraph.java @@ -0,0 +1,33 @@ +package subway.domain; + +import org.jgrapht.GraphPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; +import subway.repository.StationRepository; + +import java.util.List; + +public interface SubwayGraph { + String NOT_CONNECTED_STATION_MESSAGE = "\n[ERROR] 두 역은 연결되지 않았습니다.\n"; + + WeightedMultigraph subwayGraph + = new WeightedMultigraph(DefaultWeightedEdge.class); + + default void addStationVertex() { + StationRepository.stations() + .forEach(subwayGraph::addVertex); + } + + default WeightedMultigraph getGraph() { + return subwayGraph; + } + + default void isConnectedMap(GraphPath shortestPath) { + if (shortestPath == null) { + throw new IllegalArgumentException(NOT_CONNECTED_STATION_MESSAGE); + } + } + + void addWeight(); + List getShortestRoute(WeightedMultigraph graph, Station firstStation, Station secondStation); +} diff --git a/src/main/java/subway/domain/Time.java b/src/main/java/subway/domain/Time.java new file mode 100644 index 000000000..2d847f344 --- /dev/null +++ b/src/main/java/subway/domain/Time.java @@ -0,0 +1,13 @@ +package subway.domain; + +public class Time { + private int time; + + public Time(int time) { + this.time = time; + } + + public int getTime() { + return time; + } +} diff --git a/src/main/java/subway/domain/TimeMap.java b/src/main/java/subway/domain/TimeMap.java new file mode 100644 index 000000000..4af5a65b5 --- /dev/null +++ b/src/main/java/subway/domain/TimeMap.java @@ -0,0 +1,22 @@ +package subway.domain; + +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.WeightedMultigraph; +import subway.repository.SectionRepository; + +import java.util.List; +import java.util.Map; + +public class TimeMap implements SubwayGraph{ + public void addWeight() { + Map sections = SectionRepository.sections(); + sections.forEach((key, value) + -> subwayGraph.setEdgeWeight(subwayGraph.addEdge(key.getFirstStation(), key.getSecondStation()), value.getTimeResource().getTime())); + } + + public List getShortestRoute(WeightedMultigraph graph, Station first, Station second) { + DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); + isConnectedMap(dijkstraShortestPath.getPath(first, second)); + return dijkstraShortestPath.getPath(first, second).getVertexList(); + } +} diff --git a/src/main/java/subway/menus/MainMenu.java b/src/main/java/subway/menus/MainMenu.java new file mode 100644 index 000000000..3e985732a --- /dev/null +++ b/src/main/java/subway/menus/MainMenu.java @@ -0,0 +1,38 @@ +package subway.menus; + +import subway.controller.RouteController; +import subway.views.OutputConstant; + +import java.util.Arrays; +import java.util.Scanner; +import java.util.function.Supplier; + +public enum MainMenu { + ROUTE_LOOKUP("1", "경로 조회", RouteController::getInstance), + EXIT_PROGRAM("Q", "종료", () -> null); + + private final String option; + private final String description; + private final Supplier routeController; + + MainMenu(String option, String description, Supplier routeController) { + this.option = option; + this.description = description; + this.routeController = routeController; + } + + public static void execute(String selectedOption, Scanner scanner) { + Arrays.stream(values()) + .filter(mainMenu -> mainMenu.option.equals(selectedOption)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(OutputConstant.NOT_EXIST_OPTION_ERROR)) + .routeController + .get() + .mappingMenu(scanner); + } + + @Override + public String toString() { + return option + OutputConstant.OPTION_SEPARATOR + description; + } +} diff --git a/src/main/java/subway/menus/RouteMenu.java b/src/main/java/subway/menus/RouteMenu.java new file mode 100644 index 000000000..3f9062ca3 --- /dev/null +++ b/src/main/java/subway/menus/RouteMenu.java @@ -0,0 +1,41 @@ +package subway.menus; + +import subway.service.DistanceRouteService; +import subway.service.RouteService; +import subway.service.TimeRouteService; +import subway.views.OutputConstant; + +import java.util.Arrays; +import java.util.Scanner; +import java.util.function.Supplier; + +public enum RouteMenu { + SHORTEST_DISTANCE("1", "최단 거리", DistanceRouteService::getInstance), + SHORTEST_TIME("2", "최소 시간", TimeRouteService::getInstance), + GO_BACK_TO_MAIN_MENU("B", "돌아가기", () -> null); + + private final String option; + private final String description; + private final Supplier routeService; + + RouteMenu(String option, String description, Supplier routeService) { + this.option = option; + this.description = description; + this.routeService = routeService; + } + + public static void execute(String selectedOption, Scanner scanner) { + Arrays.stream(values()) + .filter(routeMenu -> routeMenu.option.equals(selectedOption)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(OutputConstant.NOT_EXIST_OPTION_ERROR)) + .routeService + .get() + .routingService(scanner); + } + + @Override + public String toString() { + return option + OutputConstant.OPTION_SEPARATOR + description; + } +} diff --git a/src/main/java/subway/domain/LineRepository.java b/src/main/java/subway/repository/LineRepository.java similarity index 91% rename from src/main/java/subway/domain/LineRepository.java rename to src/main/java/subway/repository/LineRepository.java index 2c4a723c9..84101c3e0 100644 --- a/src/main/java/subway/domain/LineRepository.java +++ b/src/main/java/subway/repository/LineRepository.java @@ -1,4 +1,6 @@ -package subway.domain; +package subway.repository; + +import subway.domain.Line; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/subway/repository/SectionRepository.java b/src/main/java/subway/repository/SectionRepository.java new file mode 100644 index 000000000..0b967961b --- /dev/null +++ b/src/main/java/subway/repository/SectionRepository.java @@ -0,0 +1,24 @@ +package subway.repository; + +import subway.domain.RequiredResources; +import subway.domain.Section; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class SectionRepository { + private static Map sections = new HashMap<>(); + + public static Map sections() { + return Collections.unmodifiableMap(sections); + } + + public static void addSection(Section section, RequiredResources requiredResources) { + sections.put(section, requiredResources); + } + + public static void deleteAll() { + sections.clear(); + } +} diff --git a/src/main/java/subway/domain/StationRepository.java b/src/main/java/subway/repository/StationRepository.java similarity index 91% rename from src/main/java/subway/domain/StationRepository.java rename to src/main/java/subway/repository/StationRepository.java index 8ed9d103f..5fbadba4a 100644 --- a/src/main/java/subway/domain/StationRepository.java +++ b/src/main/java/subway/repository/StationRepository.java @@ -1,4 +1,6 @@ -package subway.domain; +package subway.repository; + +import subway.domain.Station; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/subway/service/DistanceRouteService.java b/src/main/java/subway/service/DistanceRouteService.java new file mode 100644 index 000000000..3a73e5619 --- /dev/null +++ b/src/main/java/subway/service/DistanceRouteService.java @@ -0,0 +1,34 @@ +package subway.service; + +import subway.domain.DistanceMap; +import subway.domain.Station; + +import java.util.List; +import java.util.Scanner; + +public class DistanceRouteService implements RouteService { + private static final DistanceRouteService distanceRouteService = new DistanceRouteService(); + + private DistanceRouteService() { + } + + public static DistanceRouteService getInstance() { + return distanceRouteService; + } + + @Override + public void routingService(Scanner scanner) { + try { + Station startStation = inputStartStations(scanner); + Station endStation = inputEndStation(scanner); + isSameName(startStation, endStation); + DistanceMap distanceMap = new DistanceMap(); + distanceMap.addStationVertex(); + distanceMap.addWeight(); + List stationList = distanceMap.getShortestRoute(distanceMap.getGraph(), startStation, endStation); + calculateTotalDistanceAndTime(stationList); + } catch (IllegalArgumentException e) { + goToMenu(e, scanner); + } + } +} diff --git a/src/main/java/subway/service/RouteService.java b/src/main/java/subway/service/RouteService.java new file mode 100644 index 000000000..f3e40cbac --- /dev/null +++ b/src/main/java/subway/service/RouteService.java @@ -0,0 +1,73 @@ +package subway.service; + +import subway.controller.RouteController; +import subway.domain.Section; +import subway.domain.Station; +import subway.repository.SectionRepository; +import subway.repository.StationRepository; +import subway.views.routeviews.RouteInputView; +import subway.views.routeviews.RouteOutputView; + +import java.util.List; +import java.util.Scanner; + +public interface RouteService { + String NOT_EXIST_STATION_ERROR = "\n[ERROR] 존재하지 않는 역입니다.\n"; + String SAME_STATION_ERROR = "\n[ERROR] 같은 역은 입력할 수 없습니다.\n"; + + void routingService(Scanner scanner); + + default Station inputStartStations(Scanner scanner) { + Station startStation = new Station(RouteInputView.inputStartStation(scanner)); + isExistStation(startStation); + return startStation; + } + + default Station inputEndStation(Scanner scanner) { + Station endStation = new Station(RouteInputView.inputEndStation(scanner)); + isExistStation(endStation); + return endStation; + } + + default void goToMenu(IllegalArgumentException e, Scanner scanner) { + System.out.println(e.getMessage()); + RouteController routeController = RouteController.getInstance(); + routeController.mappingMenu(scanner); + } + + static void isExistStation(Station station) { + if (!StationRepository.stations().contains(station)) { + throw new IllegalArgumentException(NOT_EXIST_STATION_ERROR); + } + } + + default void isSameName(Station startStation, Station endStation) { + if (startStation.getName().equals(endStation.getName())) { + throw new IllegalArgumentException(SAME_STATION_ERROR); + } + } + + default void calculateTotalDistanceAndTime(List shortestRouteStationList) { + int totalDistance = calculateTotalDistance(shortestRouteStationList); + int totalTime = calculateTotalTime(shortestRouteStationList); + RouteOutputView.printRoutedMapWithResources(shortestRouteStationList, totalDistance, totalTime); + } + + default int calculateTotalDistance(List shortestRouteStationList) { + int totalDistance = 0; + for (int i = 0; i < shortestRouteStationList.size()-1 ; i++) { + Section section = new Section(shortestRouteStationList.get(i), shortestRouteStationList.get(i + 1)); + totalDistance += SectionRepository.sections().get(section).getDistanceResource().getDistance(); + } + return totalDistance; + } + + default int calculateTotalTime(List shortestRouteStationList) { + int totalTime = 0; + for (int i = 0; i < shortestRouteStationList.size()-1 ; i++) { + Section section = new Section(shortestRouteStationList.get(i), shortestRouteStationList.get(i + 1)); + totalTime += SectionRepository.sections().get(section).getTimeResource().getTime(); + } + return totalTime; + } +} diff --git a/src/main/java/subway/service/TimeRouteService.java b/src/main/java/subway/service/TimeRouteService.java new file mode 100644 index 000000000..142d0e582 --- /dev/null +++ b/src/main/java/subway/service/TimeRouteService.java @@ -0,0 +1,34 @@ +package subway.service; + +import subway.domain.Station; +import subway.domain.TimeMap; + +import java.util.List; +import java.util.Scanner; + +public class TimeRouteService implements RouteService { + private static final TimeRouteService timeRouteService = new TimeRouteService(); + + private TimeRouteService() { + } + + public static TimeRouteService getInstance() { + return timeRouteService; + } + + @Override + public void routingService(Scanner scanner) { + try { + Station startStation = inputStartStations(scanner); + Station endStation = inputEndStation(scanner); + isSameName(startStation, endStation); + TimeMap timeMap = new TimeMap(); + timeMap.addStationVertex(); + timeMap.addWeight(); + List stationList = timeMap.getShortestRoute(timeMap.getGraph(), startStation, endStation); + calculateTotalDistanceAndTime(stationList); + } catch (IllegalArgumentException e) { + goToMenu(e, scanner); + } + } +} diff --git a/src/main/java/subway/views/OutputConstant.java b/src/main/java/subway/views/OutputConstant.java new file mode 100644 index 000000000..860da1a41 --- /dev/null +++ b/src/main/java/subway/views/OutputConstant.java @@ -0,0 +1,9 @@ +package subway.views; + +public class OutputConstant { + public static final String OPTION_SEPARATOR = ". "; + public static final String NOT_EXIST_OPTION_ERROR = "[ERROR] 존재하지 않는 옵션입니다."; + + private OutputConstant() { + } +} diff --git a/src/main/java/subway/views/OutputView.java b/src/main/java/subway/views/OutputView.java new file mode 100644 index 000000000..1bfdedda3 --- /dev/null +++ b/src/main/java/subway/views/OutputView.java @@ -0,0 +1,10 @@ +package subway.views; + +public interface OutputView { + String LINE_WRAP = "\n"; + String SELECT_FEATURE_MESSAGE = "## 원하는 기능을 선택하세요."; + + static void printSelectOptionMessage() { + System.out.println(LINE_WRAP + SELECT_FEATURE_MESSAGE); + } +} diff --git a/src/main/java/subway/views/mainviews/MainInputView.java b/src/main/java/subway/views/mainviews/MainInputView.java new file mode 100644 index 000000000..d13353b40 --- /dev/null +++ b/src/main/java/subway/views/mainviews/MainInputView.java @@ -0,0 +1,15 @@ +package subway.views.mainviews; + +import subway.views.OutputView; + +import java.util.Scanner; + +public class MainInputView { + private MainInputView() { + } + + public static String inputMainOption(Scanner scanner) { + OutputView.printSelectOptionMessage(); + return scanner.nextLine(); + } +} diff --git a/src/main/java/subway/views/mainviews/MainOutputView.java b/src/main/java/subway/views/mainviews/MainOutputView.java new file mode 100644 index 000000000..c96975dec --- /dev/null +++ b/src/main/java/subway/views/mainviews/MainOutputView.java @@ -0,0 +1,19 @@ +package subway.views.mainviews; + +import subway.menus.MainMenu; +import subway.views.OutputView; + +import java.util.Arrays; + +public class MainOutputView implements OutputView { + private static final String MAIN_SCREEN_MESSAGE = "## 메인 화면"; + + private MainOutputView() { + } + + public static void printMainMenu() { + System.out.println(MAIN_SCREEN_MESSAGE); + Arrays.stream(MainMenu.values()) + .forEach(System.out::println); + } +} diff --git a/src/main/java/subway/views/routeviews/RouteInputView.java b/src/main/java/subway/views/routeviews/RouteInputView.java new file mode 100644 index 000000000..a056ba892 --- /dev/null +++ b/src/main/java/subway/views/routeviews/RouteInputView.java @@ -0,0 +1,25 @@ +package subway.views.routeviews; + +import subway.views.OutputView; + +import java.util.Scanner; + +public class RouteInputView { + private RouteInputView() { + } + + public static String inputRouteOption(Scanner scanner) { + OutputView.printSelectOptionMessage(); + return scanner.nextLine(); + } + + public static String inputStartStation(Scanner scanner) { + RouteOutputView.printStartStationMessage(); + return scanner.nextLine(); + } + + public static String inputEndStation(Scanner scanner) { + RouteOutputView.printEndStationMessage(); + return scanner.nextLine(); + } +} diff --git a/src/main/java/subway/views/routeviews/RouteOutputView.java b/src/main/java/subway/views/routeviews/RouteOutputView.java new file mode 100644 index 000000000..b245c3629 --- /dev/null +++ b/src/main/java/subway/views/routeviews/RouteOutputView.java @@ -0,0 +1,54 @@ +package subway.views.routeviews; + +import subway.domain.Station; +import subway.menus.RouteMenu; +import subway.views.OutputView; + +import java.util.Arrays; +import java.util.List; + +public class RouteOutputView implements OutputView { + private static final String ROUTE_SCREEN_MESSAGE = "## 경로 기준"; + private static final String INPUT_START_STATION_MESSAGE = "## 출발역을 입력하세요."; + private static final String INPUT_END_STATION_MESSAGE = "## 도착역을 입력하세요."; + private static final String ROUTE_RESULT_MESSAGE = "## 조회 결과"; + private static final String INFORMATION = "[INFO] "; + private static final String SEPARATOR = "---"; + private static final String TOTAL_DISTANCE_MESSAGE = "총 거리: "; + private static final String TOTAL_TIME_MESSAGE = "총 소요 시간: "; + private static final String DISTANCE_UNIT = "km"; + private static final String TIME_UNIT = "분"; + + private RouteOutputView() { + } + + public static void printRouteMenu() { + System.out.println(LINE_WRAP + ROUTE_SCREEN_MESSAGE); + Arrays.stream(RouteMenu.values()) + .forEach(System.out::println); + } + + public static void printStartStationMessage() { + System.out.println(LINE_WRAP + INPUT_START_STATION_MESSAGE); + } + + public static void printEndStationMessage() { + System.out.println(LINE_WRAP + INPUT_END_STATION_MESSAGE); + } + + public static void printRoutedMapWithResources(List stationList, int distance, int time) { + System.out.println(LINE_WRAP + ROUTE_RESULT_MESSAGE); + System.out.println(INFORMATION + SEPARATOR); + System.out.println(INFORMATION + TOTAL_DISTANCE_MESSAGE + distance + DISTANCE_UNIT); + System.out.println(INFORMATION + TOTAL_TIME_MESSAGE + time + TIME_UNIT); + System.out.println(INFORMATION + SEPARATOR); + printRoute(stationList); + System.out.println(); + } + + private static void printRoute(List stationList) { + stationList.stream() + .map(Station::getName) + .forEach(stationName -> System.out.println(INFORMATION + stationName)); + } +}