diff --git a/README.md b/README.md index d6299154c..87382a7c6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,21 @@
+## ✅ 구현할 기능 목록 +- [x] 지하철 노선도 경로 조회 인터페이스 + - [입출력 요구사항](#-입출력-요구사항)의 양식에 맞추어 출력을 한다. + - 존재하지 않는 기능의 입력이면 에러를 출력한다. +- [x] 구간 모델 구현 + - 구간에는 노선의 이름, 역 사이의 거리와 시간이 필요하다. +- [x] 초기 설정 구현 + - [기능 요구사항](#-기능-요구사항)에따라 초기 설정을 하도록 구현한다. +- [x] 최단 경로 기능의 입력 구현 + - 존재하지 않은 역의 입력인 경우 예외 처리 + - 출발역과 도착역이 같은 경우 예외 처리 +- [x] 최단 경로 찾기 기능 구현 + - 출발역과 도착역이 연결되어 있지 않으면 예외 처리 +- [ ] 총 이동거리 및 소요 시간 출력 + ## 🚀 기능 요구사항 > 프리코스 3주차 미션에서 사용한 코드를 참고해도 무관하다. diff --git a/src/main/java/subway/Application.java b/src/main/java/subway/Application.java index 0bcf786cc..ed1178f04 100644 --- a/src/main/java/subway/Application.java +++ b/src/main/java/subway/Application.java @@ -1,10 +1,15 @@ package subway; +import java.io.PrintStream; import java.util.Scanner; +import subway.controller.MainController; public class Application { public static void main(String[] args) { final Scanner scanner = new Scanner(System.in); - // TODO: 프로그램 구현 + final PrintStream printStream = new PrintStream(System.out); + InitialSetup.apply(); + final MainController mainController = new MainController(scanner, printStream); + mainController.run(); } } diff --git a/src/main/java/subway/Error.java b/src/main/java/subway/Error.java new file mode 100644 index 000000000..b9b8dba71 --- /dev/null +++ b/src/main/java/subway/Error.java @@ -0,0 +1,19 @@ +package subway; + +public enum Error { + OK("오류 없음"), + INVALID_MENU("존재하지 않는 기능입니다."), + SAME_STATIONS("출발역과 도착역이 동일합니다."), + STATION_NOT_EXISTS("존재하지 않는 역입니다."), + STATION_NOT_CONNECTED("역이 연결되어 있지 않습니다."); + + private final String message; + + private Error(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/subway/InitialSetup.java b/src/main/java/subway/InitialSetup.java new file mode 100644 index 000000000..372892fd5 --- /dev/null +++ b/src/main/java/subway/InitialSetup.java @@ -0,0 +1,66 @@ +package subway; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import subway.domain.Line; +import subway.domain.LineRepository; +import subway.domain.Station; +import subway.domain.StationRepository; + +public class InitialSetup { + private static final String[] STATIONS = + {"교대역", "강남역", "역삼역", "남부터미널역", "양재역", "양재시민의숲역", "매봉역"}; + private static final LinkedHashMap> STATIONS_OF_LINE = + new LinkedHashMap>(); + private static final LinkedHashMap> DISTANCES_OF_LINE = + new LinkedHashMap>(); + private static final LinkedHashMap> TIMES_OF_LINE = + new LinkedHashMap>(); + + static { + STATIONS_OF_LINE.put("2호선", Arrays.asList("교대역", "강남역", "역삼역")); + STATIONS_OF_LINE.put("3호선", Arrays.asList("교대역", "남부터미널역", "양재역", "매봉역")); + STATIONS_OF_LINE.put("신분당선", Arrays.asList("강남역", "양재역", "양재시민의숲역")); + + DISTANCES_OF_LINE.put("2호선", Arrays.asList(2, 2)); + DISTANCES_OF_LINE.put("3호선", Arrays.asList(3, 6, 1)); + DISTANCES_OF_LINE.put("신분당선", Arrays.asList(2, 10)); + + TIMES_OF_LINE.put("2호선", Arrays.asList(3, 3)); + TIMES_OF_LINE.put("3호선", Arrays.asList(2, 5, 1)); + TIMES_OF_LINE.put("신분당선", Arrays.asList(8, 3)); + } + + public static void apply() { + applyStations(); + applyLines(); + } + + private static void applyStations() { + for (String stationName : STATIONS) { + StationRepository.addStation(new Station(stationName)); + } + } + + private static void applyLines() { + for (String lineName : STATIONS_OF_LINE.keySet()) { + Line currentLine = new Line(lineName); + LineRepository.addLine(currentLine); + applyStationsOfLine(currentLine, STATIONS_OF_LINE.get(lineName)); + } + } + + private static void applyStationsOfLine(Line line, List stationNames) { + List distances = DISTANCES_OF_LINE.get(line.getName()); + List times = TIMES_OF_LINE.get(line.getName()); + for (int index = 0; index < stationNames.size(); index++) { + Station currentStation = StationRepository.getStationbyName(stationNames.get(index)); + if (index == 0) { + line.registerFirstStation(currentStation); + continue; + } + line.pushSections(currentStation, distances.get(index - 1), times.get(index - 1)); + } + } +} diff --git a/src/main/java/subway/Scene.java b/src/main/java/subway/Scene.java new file mode 100644 index 000000000..eb385c372 --- /dev/null +++ b/src/main/java/subway/Scene.java @@ -0,0 +1,48 @@ +package subway; + +import java.io.PrintStream; +import java.util.Scanner; +import java.util.Stack; +import subway.controller.MainViewController; +import subway.controller.ViewController; + +public class Scene { + private final Scanner scanner; + private final PrintStream printStream; + private final Stack controllers = new Stack(); + + public Scene(Scanner scanner, PrintStream printStream) { + this.scanner = scanner; + this.printStream = printStream; + controllers.add(new MainViewController(scanner, printStream)); + } + + public void runCurrentView() { + ViewController viewController = controllers.peek(); + viewController.run(this); + } + + public void goView(ViewController controller) { + controllers.push(controller); + } + + public void back() { + controllers.pop(); + } + + public void exit() { + controllers.clear(); + } + + public boolean isExit() { + return controllers.empty(); + } + + public Scanner getScanner() { + return scanner; + } + + public PrintStream getPrinstream() { + return printStream; + } +} diff --git a/src/main/java/subway/controller/MainController.java b/src/main/java/subway/controller/MainController.java new file mode 100644 index 000000000..2619f6481 --- /dev/null +++ b/src/main/java/subway/controller/MainController.java @@ -0,0 +1,19 @@ +package subway.controller; + +import java.io.PrintStream; +import java.util.Scanner; +import subway.Scene; + +public class MainController { + Scene scene; + + public MainController(Scanner scanner, PrintStream printStream) { + scene = new Scene(scanner, printStream); + } + + public void run() { + while (!scene.isExit()) { + scene.runCurrentView(); + } + } +} diff --git a/src/main/java/subway/controller/MainViewController.java b/src/main/java/subway/controller/MainViewController.java new file mode 100644 index 000000000..7b1ae0cfa --- /dev/null +++ b/src/main/java/subway/controller/MainViewController.java @@ -0,0 +1,35 @@ +package subway.controller; + +import java.io.PrintStream; +import java.util.Scanner; +import java.util.function.BiConsumer; +import subway.Error; +import subway.Scene; +import subway.menu.MainMenu; +import subway.view.MainView; +import subway.view.View; + +public class MainViewController extends ViewController { + + public MainViewController(Scanner scanner, PrintStream printStream) { + view = new MainView(scanner, printStream); + } + + @Override + public BiConsumer selectMenu() { + String input = view.requestMenu(); + BiConsumer result = MainMenu.getAction(input); + if (result == null) { + view.printError(Error.INVALID_MENU); + } + return result; + } + + public static void goSectionView(Scene scene, View view) { + scene.goView(new SectionViewController(scene.getScanner(), scene.getPrinstream())); + } + + public static void exit(Scene scene, View view) { + scene.exit(); + } +} diff --git a/src/main/java/subway/controller/SectionViewController.java b/src/main/java/subway/controller/SectionViewController.java new file mode 100644 index 000000000..1b4b3bf73 --- /dev/null +++ b/src/main/java/subway/controller/SectionViewController.java @@ -0,0 +1,101 @@ +package subway.controller; + +import java.io.PrintStream; +import java.util.List; +import java.util.Scanner; +import java.util.function.BiConsumer; +import subway.Error; +import subway.Scene; +import subway.domain.PathFinder; +import subway.domain.Station; +import subway.domain.StationRepository; +import subway.menu.SectionMenu; +import subway.view.SectionView; +import subway.view.View; + +public class SectionViewController extends ViewController { + + public SectionViewController(Scanner scanner, PrintStream printStream) { + view = new SectionView(scanner, printStream); + } + + @Override + public BiConsumer selectMenu() { + String input = view.requestMenu(); + BiConsumer result = SectionMenu.getAction(input); + if (result == null) { + view.printError(Error.INVALID_MENU); + } + return result; + } + + public static void findMinDistance(Scene scene, View view) { + String departureInput = view.requestDepartureStation(); + String arrivalInput = view.requestArrivalStation(); + Error error = isValidStations(departureInput, arrivalInput); + if (error != Error.OK) { + view.printError(error); + return; + } + List path = findMinDistancePath(departureInput, arrivalInput, view); + if (path == null) { + return; + } + view.printPath(path); + scene.back(); + } + + private static List findMinDistancePath(String departureInput, String arrivalInput, + View view) { + Station departureStation = StationRepository.getStationbyName(departureInput); + Station arrivalStation = StationRepository.getStationbyName(arrivalInput); + List path = PathFinder.findMinDistancePath(departureStation, arrivalStation); + if (path == null) { + view.printError(Error.STATION_NOT_CONNECTED); + } + return path; + } + + public static void findMinTime(Scene scene, View view) { + String departureInput = view.requestDepartureStation(); + String arrivalInput = view.requestArrivalStation(); + Error error = isValidStations(departureInput, arrivalInput); + if (error != Error.OK) { + view.printError(error); + return; + } + List path = findMinTimePath(departureInput, arrivalInput, view); + if (path == null) { + return; + } + view.printPath(path); + scene.back(); + } + + private static List findMinTimePath(String departureInput, String arrivalInput, + View view) { + Station departureStation = StationRepository.getStationbyName(departureInput); + Station arrivalStation = StationRepository.getStationbyName(arrivalInput); + List path = PathFinder.findMinTimePath(departureStation, arrivalStation); + if (path == null) { + view.printError(Error.STATION_NOT_CONNECTED); + } + return path; + } + + private static Error isValidStations(String departureInput, String arrivalInput) { + if (departureInput.equals(arrivalInput)) { + return Error.SAME_STATIONS; + } + Station departureStation = StationRepository.getStationbyName(departureInput); + Station arrivalStation = StationRepository.getStationbyName(arrivalInput); + if ((departureStation == null) || (arrivalStation == null)) { + return Error.STATION_NOT_EXISTS; + } + return Error.OK; + } + + public static void back(Scene scene, View view) { + scene.back(); + } +} diff --git a/src/main/java/subway/controller/ViewController.java b/src/main/java/subway/controller/ViewController.java new file mode 100644 index 000000000..6bd90aba1 --- /dev/null +++ b/src/main/java/subway/controller/ViewController.java @@ -0,0 +1,20 @@ +package subway.controller; + +import java.util.function.BiConsumer; +import subway.Scene; +import subway.view.View; + +public abstract class ViewController { + protected View view; + + public ViewController() {} + + abstract public BiConsumer selectMenu(); + + public void run(Scene scene) { + BiConsumer action = selectMenu(); + if (action != null) { + action.accept(scene, view);; + } + } +} diff --git a/src/main/java/subway/domain/Line.java b/src/main/java/subway/domain/Line.java index f4d738d5a..e8e391ca4 100644 --- a/src/main/java/subway/domain/Line.java +++ b/src/main/java/subway/domain/Line.java @@ -1,7 +1,15 @@ package subway.domain; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class Line { private String name; + private List stations = new ArrayList(); + private List distances = new ArrayList(); // 0번째와 1번째 station들의 관계는 0번째 인덱스에 + // 존재한다. + private List times = new ArrayList(); public Line(String name) { this.name = name; @@ -11,5 +19,27 @@ public String getName() { return name; } - // 추가 기능 구현 + public List getStations() { + return Collections.unmodifiableList(stations); + } + + public List getDistances() { + return Collections.unmodifiableList(distances); + } + + public List getTimes() { + return Collections.unmodifiableList(times); + } + + public void registerFirstStation(Station station) { + if (stations.isEmpty()) { + stations.add(station); + } + } + + public void pushSections(Station station, int distance, int time) { + stations.add(station); + distances.add(distance); + times.add(time); + } } diff --git a/src/main/java/subway/domain/PathFinder.java b/src/main/java/subway/domain/PathFinder.java new file mode 100644 index 000000000..2fefe2205 --- /dev/null +++ b/src/main/java/subway/domain/PathFinder.java @@ -0,0 +1,91 @@ +package subway.domain; + +import java.util.List; +import java.util.stream.Collectors; +import org.jgrapht.GraphPath; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; + +public class PathFinder { + + public static List findMinDistancePath(Station departure, Station arrival) { + WeightedMultigraph graph = initializeGraphForDistance(); + DijkstraShortestPath dijkstraShortesPath = + new DijkstraShortestPath(graph); + GraphPath path = + dijkstraShortesPath.getPath(departure.getName(), arrival.getName()); + if (path == null) { + return null; + } + return path.getVertexList(); + } + + public static List findMinTimePath(Station departure, Station arrival) { + WeightedMultigraph graph = initializeGraphForTime(); + DijkstraShortestPath dijkstraShortesPath = + new DijkstraShortestPath(graph); + GraphPath path = + dijkstraShortesPath.getPath(departure.getName(), arrival.getName()); + if (path == null) { + return null; + } + return path.getVertexList(); + } + + private static WeightedMultigraph initializeGraphForDistance() { + WeightedMultigraph graph = + new WeightedMultigraph(DefaultWeightedEdge.class); + List stationNames = StationRepository.stations().stream() + .map(station -> station.getName()).collect(Collectors.toList()); + List lines = LineRepository.lines(); + initializeStations(stationNames, graph); + initializeLinesForDistance(lines, graph); + return graph; + } + + private static WeightedMultigraph initializeGraphForTime() { + WeightedMultigraph graph = + new WeightedMultigraph(DefaultWeightedEdge.class); + List stationNames = StationRepository.stations().stream() + .map(station -> station.getName()).collect(Collectors.toList()); + List lines = LineRepository.lines(); + initializeStations(stationNames, graph); + initializeLinesForTime(lines, graph); + return graph; + } + + private static void initializeStations(List stationNames, + WeightedMultigraph graph) { + for (String stationName : stationNames) { + graph.addVertex(stationName); + } + } + + private static void initializeLinesForDistance(List lines, + WeightedMultigraph graph) { + for (Line line : lines) { + List stations = line.getStations(); + List distances = line.getDistances(); + for (int index = 0; index < distances.size(); index++) { + String firstStation = stations.get(index).getName(); + String secondStation = stations.get(index + 1).getName(); + graph.setEdgeWeight(graph.addEdge(firstStation, secondStation), + distances.get(index)); + } + } + } + + private static void initializeLinesForTime(List lines, + WeightedMultigraph graph) { + for (Line line : lines) { + List stations = line.getStations(); + List times = line.getTimes(); + for (int index = 0; index < times.size(); index++) { + String firstStation = stations.get(index).getName(); + String secondStation = stations.get(index + 1).getName(); + graph.setEdgeWeight(graph.addEdge(firstStation, secondStation), times.get(index)); + } + } + } +} diff --git a/src/main/java/subway/domain/StationRepository.java b/src/main/java/subway/domain/StationRepository.java index 8ed9d103f..19e3f2563 100644 --- a/src/main/java/subway/domain/StationRepository.java +++ b/src/main/java/subway/domain/StationRepository.java @@ -23,4 +23,9 @@ public static boolean deleteStation(String name) { public static void deleteAll() { stations.clear(); } + + public static Station getStationbyName(String name) { + return stations.stream().filter(station -> name.equals(station.getName())).findFirst() + .orElse(null); + } } diff --git a/src/main/java/subway/menu/MainMenu.java b/src/main/java/subway/menu/MainMenu.java new file mode 100644 index 000000000..874ccb2b2 --- /dev/null +++ b/src/main/java/subway/menu/MainMenu.java @@ -0,0 +1,39 @@ +package subway.menu; + +import java.util.Arrays; +import java.util.function.BiConsumer; +import subway.Scene; +import subway.controller.MainViewController; +import subway.view.View; + +public enum MainMenu { + GO_SECTION_VIEW("1", "경로 조회", MainViewController::goSectionView), + EXIT("Q", "종료", MainViewController::exit); + + private String key; + private String message; + private BiConsumer action; + + private MainMenu(String key, String message, BiConsumer action) { + this.key = key; + this.message = message; + this.action = action; + } + + public static BiConsumer getAction(String input) { + MainMenu selectedMenu = Arrays.asList(MainMenu.values()).stream() + .filter(menu -> input.equals(menu.key)).findFirst().orElse(null); + if (selectedMenu == null) { + return null; + } + return selectedMenu.action; + } + + public String getKey() { + return key; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/subway/menu/SectionMenu.java b/src/main/java/subway/menu/SectionMenu.java new file mode 100644 index 000000000..359011c41 --- /dev/null +++ b/src/main/java/subway/menu/SectionMenu.java @@ -0,0 +1,40 @@ +package subway.menu; + +import java.util.Arrays; +import java.util.function.BiConsumer; +import subway.Scene; +import subway.controller.SectionViewController; +import subway.view.View; + +public enum SectionMenu { + MIN_DISTANCE("1", "최단 거리", SectionViewController::findMinDistance), + MIN_TIME("2", "최소 시간", SectionViewController::findMinTime), + BACK("B", "돌아가기", SectionViewController::back); + + private String key; + private String message; + private BiConsumer action; + + private SectionMenu(String key, String message, BiConsumer action) { + this.key = key; + this.message = message; + this.action = action; + } + + public static BiConsumer getAction(String input) { + SectionMenu selectedMenu = Arrays.asList(SectionMenu.values()).stream() + .filter(menu -> input.equals(menu.key)).findFirst().orElse(null); + if (selectedMenu == null) { + return null; + } + return selectedMenu.action; + } + + public String getKey() { + return key; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/subway/view/MainView.java b/src/main/java/subway/view/MainView.java new file mode 100644 index 000000000..26815847d --- /dev/null +++ b/src/main/java/subway/view/MainView.java @@ -0,0 +1,27 @@ +package subway.view; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Scanner; +import subway.menu.MainMenu; + +public class MainView extends View { + private static final String TITLE = "메인 화면"; + + public MainView(Scanner scanner, PrintStream printStream) { + super(scanner, printStream); + } + + @Override + void printTitle() { + printStream.printf(TITLE_FORM, TITLE); + } + + @Override + void printMenuList() { + Arrays.asList(MainMenu.values()).stream() + .forEach(menu -> printStream.printf(MENU_FORM, menu.getKey(), menu.getMessage())); + printStream.println(); + } + +} diff --git a/src/main/java/subway/view/SectionView.java b/src/main/java/subway/view/SectionView.java new file mode 100644 index 000000000..7afa83835 --- /dev/null +++ b/src/main/java/subway/view/SectionView.java @@ -0,0 +1,26 @@ +package subway.view; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Scanner; +import subway.menu.SectionMenu; + +public class SectionView extends View { + private static final String TITLE = "경로 기준"; + + public SectionView(Scanner scanner, PrintStream printStream) { + super(scanner, printStream); + } + + @Override + void printTitle() { + printStream.printf(TITLE_FORM, TITLE); + } + + @Override + void printMenuList() { + Arrays.asList(SectionMenu.values()).stream() + .forEach(menu -> printStream.printf(MENU_FORM, menu.getKey(), menu.getMessage())); + printStream.println(); + } +} diff --git a/src/main/java/subway/view/View.java b/src/main/java/subway/view/View.java new file mode 100644 index 000000000..f0e7b2d00 --- /dev/null +++ b/src/main/java/subway/view/View.java @@ -0,0 +1,71 @@ +package subway.view; + +import java.io.PrintStream; +import java.util.List; +import java.util.Scanner; +import subway.Error; + +public abstract class View { + private static final String DEPARTURE_STATION_MESSAGE = "출발역을 입력하세요."; + private static final String ARRIVAL_STATION_MESSAGE = "출발역을 입력하세요."; + private static final String MENU_SELECTION_MESSAGE = "원하는 기능을 선택하세요."; + private static final String BORDER = "---\n"; + private static final String TOTAL_DISTANCE = "총 거리: "; + private static final String TOTAL_TIME = "총 소요 시간: "; + private static final String RESULT_TITLE = "조회 결과"; + private static final String INFO_FORM = "[INFO] %s\n"; + private static final String ERROR_FORM = "[ERROR] %s\n"; + protected static final String TITLE_FORM = "## %s\n"; + protected static final String MENU_FORM = "%s. %s\n"; + protected final Scanner scanner; + protected final PrintStream printStream; + + protected View(Scanner scanner, PrintStream printStream) { + this.scanner = scanner; + this.printStream = printStream; + } + + abstract void printTitle(); + + abstract void printMenuList(); + + public String requestDepartureStation() { + printStream.printf(TITLE_FORM, DEPARTURE_STATION_MESSAGE); + String input = scanner.nextLine(); + printStream.println(); + return input; + } + + public String requestArrivalStation() { + printStream.printf(TITLE_FORM, ARRIVAL_STATION_MESSAGE); + String input = scanner.nextLine(); + printStream.println(); + return input; + } + + public String requestMenu() { + printTitle(); + printMenuList(); + printStream.printf(TITLE_FORM, MENU_SELECTION_MESSAGE); + String input = scanner.nextLine(); + printStream.println(); + return input; + } + + public void printPath(List names) { + printStream.printf(TITLE_FORM, RESULT_TITLE); + printStream.print(BORDER); + printStream.printf(INFO_FORM, TOTAL_DISTANCE); + printStream.printf(INFO_FORM, TOTAL_TIME); + printStream.print(BORDER); + for (String name : names) { + printStream.printf(INFO_FORM, name); + } + printStream.println(); + } + + public void printError(Error error) { + printStream.printf(ERROR_FORM, error.getMessage()); + printStream.println(); + } +}