From 73b9d157bc9309a4e142e8e5fafb8b82d006f303 Mon Sep 17 00:00:00 2001 From: Brunetto Marie Date: Wed, 22 Mar 2023 08:02:30 +0100 Subject: [PATCH] Ajout fichiers originaux --- README.md | 62 ++ be-graphes-algos/pom.xml | 25 + .../graphs/algorithm/AbstractAlgorithm.java | 87 ++ .../graphs/algorithm/AbstractInputData.java | 101 ++ .../graphs/algorithm/AbstractSolution.java | 88 ++ .../graphs/algorithm/AlgorithmFactory.java | 128 +++ .../insa/graphs/algorithm/ArcInspector.java | 43 + .../graphs/algorithm/ArcInspectorFactory.java | 189 ++++ .../carpooling/CarPoolingAlgorithm.java | 24 + .../algorithm/carpooling/CarPoolingData.java | 13 + .../carpooling/CarPoolingGraphicObserver.java | 5 + .../carpooling/CarPoolingObserver.java | 5 + .../carpooling/CarPoolingSolution.java | 11 + .../carpooling/CarPoolingTextObserver.java | 5 + .../packageswitch/PackageSwitchAlgorithm.java | 29 + .../packageswitch/PackageSwitchData.java | 13 + .../PackageSwitchGraphicObserver.java | 5 + .../packageswitch/PackageSwitchObserver.java | 5 + .../packageswitch/PackageSwitchSolution.java | 11 + .../PackageSwitchTextObserver.java | 5 + .../shortestpath/AStarAlgorithm.java | 9 + .../shortestpath/BellmanFordAlgorithm.java | 100 ++ .../shortestpath/DijkstraAlgorithm.java | 17 + .../shortestpath/ShortestPathAlgorithm.java | 69 ++ .../shortestpath/ShortestPathData.java | 47 + .../shortestpath/ShortestPathObserver.java | 37 + .../shortestpath/ShortestPathSolution.java | 74 ++ .../ShortestPathTextObserver.java | 37 + .../graphs/algorithm/utils/BinaryHeap.java | 205 +++++ .../algorithm/utils/BinaryHeapFormatter.java | 198 ++++ .../algorithm/utils/BinarySearchTree.java | 64 ++ .../utils/ElementNotFoundException.java | 32 + .../utils/EmptyPriorityQueueException.java | 16 + .../graphs/algorithm/utils/PriorityQueue.java | 81 ++ .../WeaklyConnectedComponentObserver.java | 30 + .../WeaklyConnectedComponentTextObserver.java | 36 + .../WeaklyConnectedComponentsAlgorithm.java | 159 ++++ .../WeaklyConnectedComponentsData.java | 20 + .../WeaklyConnectedComponentsSolution.java | 57 ++ .../algorithm/utils/BinaryHeapTest.java | 15 + .../algorithm/utils/BinarySearchTreeTest.java | 15 + .../algorithm/utils/PriorityQueueTest.java | 320 +++++++ be-graphes-gui/pom.xml | 87 ++ .../org/insa/graphs/gui/AlgorithmPanel.java | 381 ++++++++ .../graphs/gui/BlockingActionFactory.java | 56 ++ .../graphs/gui/DrawingChangeListener.java | 24 + .../insa/graphs/gui/GraphChangeListener.java | 14 + .../graphs/gui/GraphReaderProgressBar.java | 123 +++ .../java/org/insa/graphs/gui/MainWindow.java | 862 ++++++++++++++++++ .../org/insa/graphs/gui/NodesInputPanel.java | 424 +++++++++ .../java/org/insa/graphs/gui/PathsPanel.java | 361 ++++++++ .../org/insa/graphs/gui/RunningAction.java | 33 + .../org/insa/graphs/gui/SolutionPanel.java | 289 ++++++ .../org/insa/graphs/gui/StreamCapturer.java | 54 ++ .../org/insa/graphs/gui/ThreadWrapper.java | 62 ++ .../graphs/gui/drawing/BasicGraphPalette.java | 82 ++ .../drawing/BlackAndWhiteGraphPalette.java | 19 + .../org/insa/graphs/gui/drawing/Drawing.java | 157 ++++ .../gui/drawing/DrawingClickListener.java | 14 + .../insa/graphs/gui/drawing/GraphPalette.java | 23 + .../gui/drawing/MercatorProjection.java | 116 +++ .../gui/drawing/PlateCarreProjection.java | 68 ++ .../insa/graphs/gui/drawing/Projection.java | 51 ++ .../gui/drawing/components/BasicDrawing.java | 747 +++++++++++++++ .../drawing/components/MapViewDrawing.java | 521 +++++++++++ .../drawing/components/MapZoomControls.java | 209 +++++ .../components/ZoomAndPanListener.java | 177 ++++ .../drawing/overlays/MarkerAutoScaling.java | 73 ++ .../gui/drawing/overlays/MarkerOverlay.java | 19 + .../gui/drawing/overlays/MarkerUtils.java | 97 ++ .../graphs/gui/drawing/overlays/Overlay.java | 42 + .../gui/drawing/overlays/PaintUtils.java | 65 ++ .../gui/drawing/overlays/PathOverlay.java | 5 + .../gui/drawing/overlays/PointSetOverlay.java | 70 ++ .../drawing/overlays/PolylineAutoScaling.java | 92 ++ .../ShortestPathGraphicObserver.java | 42 + ...aklyConnectedComponentGraphicObserver.java | 42 + .../org/insa/graphs/gui/simple/Launch.java | 73 ++ .../org/insa/graphs/gui/utils/ColorUtils.java | 21 + .../org/insa/graphs/gui/utils/FileUtils.java | 151 +++ .../src/main/resources/delete-icon.png | Bin 0 -> 2118 bytes .../src/main/resources/marker_mask.bin | Bin 0 -> 35144 bytes .../src/main/resources/save-icon.png | Bin 0 -> 3054 bytes be-graphes-gui/src/main/resources/zoomIn.png | Bin 0 -> 3536 bytes be-graphes-gui/src/main/resources/zoomOut.png | Bin 0 -> 3273 bytes be-graphes-model/pom.xml | 17 + .../insa/graphs/model/AccessRestrictions.java | 236 +++++ .../main/java/org/insa/graphs/model/Arc.java | 70 ++ .../org/insa/graphs/model/ArcBackward.java | 55 ++ .../org/insa/graphs/model/ArcForward.java | 68 ++ .../java/org/insa/graphs/model/Graph.java | 131 +++ .../insa/graphs/model/GraphStatistics.java | 204 +++++ .../main/java/org/insa/graphs/model/Node.java | 159 ++++ .../main/java/org/insa/graphs/model/Path.java | 248 +++++ .../java/org/insa/graphs/model/Point.java | 80 ++ .../insa/graphs/model/RoadInformation.java | 126 +++ .../graphs/model/io/BadFormatException.java | 33 + .../model/io/BadMagicNumberException.java | 46 + .../graphs/model/io/BadVersionException.java | 42 + .../graphs/model/io/BinaryGraphReader.java | 323 +++++++ .../graphs/model/io/BinaryPathReader.java | 76 ++ .../graphs/model/io/BinaryPathWriter.java | 54 ++ .../insa/graphs/model/io/BinaryReader.java | 121 +++ .../insa/graphs/model/io/BinaryWriter.java | 42 + .../org/insa/graphs/model/io/GraphReader.java | 39 + .../graphs/model/io/GraphReaderObserver.java | 69 ++ .../graphs/model/io/MapMismatchException.java | 45 + .../org/insa/graphs/model/io/PathReader.java | 34 + .../org/insa/graphs/model/io/PathWriter.java | 31 + .../org/insa/graphes/model/GraphTest.java | 112 +++ .../java/org/insa/graphes/model/NodeTest.java | 92 ++ .../java/org/insa/graphes/model/PathTest.java | 243 +++++ pom.xml | 48 + 113 files changed, 11082 insertions(+) create mode 100644 README.md create mode 100644 be-graphes-algos/pom.xml create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractInputData.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractSolution.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AlgorithmFactory.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspector.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspectorFactory.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingData.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingGraphicObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingSolution.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingTextObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchData.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchGraphicObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchSolution.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchTextObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/AStarAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/BellmanFordAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/DijkstraAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathData.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathSolution.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathTextObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeap.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeapFormatter.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinarySearchTree.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/ElementNotFoundException.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/EmptyPriorityQueueException.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/PriorityQueue.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentTextObserver.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsAlgorithm.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsData.java create mode 100644 be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsSolution.java create mode 100644 be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinaryHeapTest.java create mode 100644 be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinarySearchTreeTest.java create mode 100644 be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/PriorityQueueTest.java create mode 100644 be-graphes-gui/pom.xml create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/AlgorithmPanel.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/BlockingActionFactory.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/DrawingChangeListener.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphChangeListener.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphReaderProgressBar.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/MainWindow.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/NodesInputPanel.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/PathsPanel.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/RunningAction.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/SolutionPanel.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/StreamCapturer.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/ThreadWrapper.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BasicGraphPalette.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BlackAndWhiteGraphPalette.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Drawing.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/DrawingClickListener.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/GraphPalette.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/MercatorProjection.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/PlateCarreProjection.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Projection.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/BasicDrawing.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapViewDrawing.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapZoomControls.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/ZoomAndPanListener.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerAutoScaling.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerOverlay.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerUtils.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/Overlay.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PaintUtils.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PathOverlay.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PointSetOverlay.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PolylineAutoScaling.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/ShortestPathGraphicObserver.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/WeaklyConnectedComponentGraphicObserver.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/simple/Launch.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/ColorUtils.java create mode 100644 be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/FileUtils.java create mode 100644 be-graphes-gui/src/main/resources/delete-icon.png create mode 100644 be-graphes-gui/src/main/resources/marker_mask.bin create mode 100644 be-graphes-gui/src/main/resources/save-icon.png create mode 100644 be-graphes-gui/src/main/resources/zoomIn.png create mode 100644 be-graphes-gui/src/main/resources/zoomOut.png create mode 100644 be-graphes-model/pom.xml create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/AccessRestrictions.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/Arc.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/ArcBackward.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/ArcForward.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/Graph.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/GraphStatistics.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/Node.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/Path.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/Point.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/RoadInformation.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BadFormatException.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BadMagicNumberException.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BadVersionException.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryGraphReader.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathReader.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathWriter.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryReader.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryWriter.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReader.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReaderObserver.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/MapMismatchException.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/PathReader.java create mode 100644 be-graphes-model/src/main/java/org/insa/graphs/model/io/PathWriter.java create mode 100644 be-graphes-model/src/test/java/org/insa/graphes/model/GraphTest.java create mode 100644 be-graphes-model/src/test/java/org/insa/graphes/model/NodeTest.java create mode 100644 be-graphes-model/src/test/java/org/insa/graphes/model/PathTest.java create mode 100644 pom.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..b24c5a0 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Graph & Algorithm project — INSA Toulouse + +## How to start? + +You will not be able to use this repository to save your work, you need to copy / import / fork it to +your favorite Git platform. + +### Importing to [Github](https://github.com), [Bitbucket](https://bitbucket.org) or [Gitlab](https://gitlab.com): + +You first need to register and then log in to the platform you want. The steps to import the project are detailed below: + +#### Github + +1. In the upper-right corner of any page, click the **"+"** icon, then click **Import repository**, or go to [https://github.com/new/import](https://github.com/new/import). +2. Paste the following URL in the first input: + [https://gitea.typename.fr/INSA/be-graphes.git](https://gitea.typename.fr/INSA/be-graphes.git) +3. Choose the name you want for the repository. +4. Click *Begin import*. +5. Wait for completion... Done! + +#### Bitbucket + +1. On the left panel of any page, click the **"+"** icon, then **Repository**, and then **Import**, or directly go to [https://bitbucket.org/repo/import](https://bitbucket.org/repo/import). +2. Paste the following URL in the first input (select Git as source if not already selected): + [https://gitea.typename.fr/INSA/be-graphes.git](https://gitea.typename.fr/INSA/be-graphes.git) +3. Choose the name you want for repository, and select Git as the *Version control system*. +4. Click *Import repository*. +5. Wait for completion... Done! + +#### Gitlab + +1. In the upper-right corner of any page, click the **"+"** icon, then **New project**, or directly go to [https://gitlab.com/projects/new](https://gitlab.com/projects/new). +2. Select the **Import project** tab, and then click **Repo by URL** (right-most option). +3. Paste the following URL in the first input: + [https://gitea.typename.fr/INSA/be-graphes.git](https://gitea.typename.fr/INSA/be-graphes.git) +4. Choose the name you want for the repository. +5. Click *Create project*. +6. Wait for completion... Done! + +### Importing to another repository provider *[not recommended]*: + +1. Create a new **empty** repository (no README, no LICENSE) on your provider. Let's assume the URL of your repository is `$URL_REPOSITORY`. +2. Clone this repository somewhere: + + ```bash + git clone https://gitea.typename.fr/INSA/be-graphes.git + ``` + +3. Go inside the newly cloned repository and update the **remote**: + + ```bash + cd be-graphes + git remote set-url origin $URL_REPOSITORY + ``` + +4. Push to your repository: + + ```bash + push -u origin master + ``` + +Another way is to do a bare clone and then mirror it to your repository: [https://help.github.com/articles/importing-a-git-repository-using-the-command-line/](https://help.github.com/articles/importing-a-git-repository-using-the-command-line/) \ No newline at end of file diff --git a/be-graphes-algos/pom.xml b/be-graphes-algos/pom.xml new file mode 100644 index 0000000..d66075a --- /dev/null +++ b/be-graphes-algos/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + org.insa.graphs + be-graphes-all + 0.0.1-SNAPSHOT + + + be-graphes-algos + be-graphes-algos + + + + org.insa.graphs + be-graphes-model + ${project.version} + + + + diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractAlgorithm.java new file mode 100644 index 0000000..10fe8c4 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractAlgorithm.java @@ -0,0 +1,87 @@ +package org.insa.graphs.algorithm; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; + +/** + * Base class for algorithm classes. + * + * @param Observer type for the algorithm. + */ +public abstract class AbstractAlgorithm { + + // Input data for the algorithm + protected final AbstractInputData data; + + // List of observers for the algorithm + protected final ArrayList observers; + + /** + * Create a new algorithm with an empty list of observers. + * + * @param data Input data for the algorithm. + */ + protected AbstractAlgorithm(AbstractInputData data) { + this.data = data; + this.observers = new ArrayList(); + } + + /** + * Create a new algorithm with the given list of observers. + * + * @param data Input data for the algorithm. + * @param observers Initial list of observers for the algorithm. + */ + protected AbstractAlgorithm(AbstractInputData data, ArrayList observers) { + this.data = data; + this.observers = observers; + } + + /** + * Add an observer to this algorithm. + * + * @param observer Observer to add to this algorithm. + */ + public void addObserver(Observer observer) { + observers.add(observer); + } + + /** + * @return The list of observers for this algorithm. + */ + public ArrayList getObservers() { + return observers; + } + + /** + * @return Input for this algorithm. + */ + public AbstractInputData getInputData() { + return data; + } + + /** + * Run the algorithm and return the solution. + * + * This methods internally time the call to doRun() and update the result of the + * call with the computed solving time. + * + * @return The solution found by the algorithm (may not be a feasible solution). + */ + public AbstractSolution run() { + Instant start = Instant.now(); + AbstractSolution solution = this.doRun(); + solution.setSolvingTime(Duration.between(start, Instant.now())); + return solution; + } + + /** + * Abstract method that should be implemented by child class. + * + * @return The solution found, must not be null (use an infeasible or unknown + * status if necessary). + */ + protected abstract AbstractSolution doRun(); + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractInputData.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractInputData.java new file mode 100644 index 0000000..3ef1ee0 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractInputData.java @@ -0,0 +1,101 @@ +package org.insa.graphs.algorithm; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +//import org.insa.graphs.model.GraphStatistics; + +/** + * Base class for algorithm input data classes. This class contains the basic + * data that are required by most graph algorithms, i.e. a graph, a mode (time / + * length) and a filter for the arc. + * + */ +public abstract class AbstractInputData { + + /** + * Enum specifying the top mode of the algorithms. + * + * @see ArcInspector + */ + public enum Mode { + TIME, LENGTH + } + + // Graph + private final Graph graph; + + // Arc filter. + protected final ArcInspector arcInspector; + + /** + * Create a new AbstractInputData instance for the given graph, mode and filter. + * + * @param graph Graph for this input data. + * @param arcInspector Arc inspector for this input data. + */ + protected AbstractInputData(Graph graph, ArcInspector arcInspector) { + this.graph = graph; + this.arcInspector = arcInspector; + } + + /** + * @return Graph associated with this input. + */ + public Graph getGraph() { + return graph; + } + + /** + * Retrieve the cost associated with the given arc according to the underlying + * arc inspector. + * + * @param arc Arc for which cost should be retrieved. + * + * @return Cost for the given arc. + * + * @see ArcInspector + */ + public double getCost(Arc arc) { + return this.arcInspector.getCost(arc); + } + + /** + * @return Mode associated with this input data. + * + * @see Mode + */ + public Mode getMode() { + return this.arcInspector.getMode(); + } + + /** + * Retrieve the maximum speed associated with this input data, or + * {@link GraphStatistics#NO_MAXIMUM_SPEED} if none is associated. The maximum + * speed associated with input data is different from the maximum speed + * associated with graph (accessible via {@link Graph#getGraphInformation()}). + * + * @return The maximum speed for this inspector, or + * {@link GraphStatistics#NO_MAXIMUM_SPEED} if none is set. + */ + + /* Apparently, getMaximumSpeed is never initialized in arcInspectors. + * => Do not use. + public int getMaximumSpeed() { + return this.arcInspector.getMaximumSpeed(); + } + */ + + /** + * Check if the given arc is allowed for the filter corresponding to this input. + * + * @param arc Arc to check. + * + * @return true if the given arc is allowed. + * + * @see ArcInspector + */ + public boolean isAllowed(Arc arc) { + return this.arcInspector.isAllowed(arc); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractSolution.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractSolution.java new file mode 100644 index 0000000..0d271eb --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AbstractSolution.java @@ -0,0 +1,88 @@ +package org.insa.graphs.algorithm; + +import java.time.Duration; + +/** + * Base class for solution classes returned by the algorithm. This class + * contains the basic information that any solution should have: status of the + * solution (unknown, infeasible, etc.), solving time and the original input + * data. + */ +public abstract class AbstractSolution { + + /** + * Possible status for a solution. + * + */ + public enum Status { + UNKNOWN, INFEASIBLE, FEASIBLE, OPTIMAL, + }; + + // Status of the solution. + private final Status status; + + // Solving time for the solution. + private Duration solvingTime; + + // Original input of the solution. + private final AbstractInputData data; + + /** + * Create a new abstract solution with unknown status. + * + * @param data + */ + protected AbstractSolution(AbstractInputData data) { + this.data = data; + this.solvingTime = Duration.ZERO; + this.status = Status.UNKNOWN; + } + + /** + * + * @param data + * @param status + */ + protected AbstractSolution(AbstractInputData data, Status status) { + this.data = data; + this.status = status; + } + + /** + * @return Original input for this solution. + */ + public AbstractInputData getInputData() { + return data; + } + + /** + * @return Status of this solution. + */ + public Status getStatus() { + return status; + } + + /** + * @return Solving time of this solution. + */ + public Duration getSolvingTime() { + return solvingTime; + } + + /** + * Set the solving time of this solution. + * + * @param solvingTime Solving time for the solution. + */ + protected void setSolvingTime(Duration solvingTime) { + this.solvingTime = solvingTime; + } + + /** + * @return true if the solution is feasible or optimal. + */ + public boolean isFeasible() { + return status == Status.FEASIBLE || status == Status.OPTIMAL; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AlgorithmFactory.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AlgorithmFactory.java new file mode 100644 index 0000000..4dde531 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/AlgorithmFactory.java @@ -0,0 +1,128 @@ +package org.insa.graphs.algorithm; + +import java.lang.reflect.Constructor; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.insa.graphs.algorithm.shortestpath.AStarAlgorithm; +import org.insa.graphs.algorithm.shortestpath.BellmanFordAlgorithm; +import org.insa.graphs.algorithm.shortestpath.DijkstraAlgorithm; +import org.insa.graphs.algorithm.shortestpath.ShortestPathAlgorithm; +import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentsAlgorithm; + +/** + * Factory class used to register and retrieve algorithms based on their common + * ancestor and name. + * + */ +public class AlgorithmFactory { + + // Map between algorithm names and class. + private final static Map>, Map>>> ALGORITHMS = new IdentityHashMap<>(); + + static { + // Register weakly-connected components algorithm: + registerAlgorithm(WeaklyConnectedComponentsAlgorithm.class, "WCC basic", + WeaklyConnectedComponentsAlgorithm.class); + + // Register shortest path algorithm: + registerAlgorithm(ShortestPathAlgorithm.class, "Bellman-Ford", BellmanFordAlgorithm.class); + registerAlgorithm(ShortestPathAlgorithm.class, "Dijkstra", DijkstraAlgorithm.class); + registerAlgorithm(ShortestPathAlgorithm.class, "A*", AStarAlgorithm.class); + + // Register your algorithms here: + // registerAlgorithm(CarPoolingAlgorithm.class, "My Awesome Algorithm", + // MyCarPoolingAlgorithm.class); + } + + /** + * Register the given algorithm class with the given name as a child class of + * the given base algorithm. + * + * @param baseAlgorithm Base algorithm class that corresponds to the newly + * registered algorithm class (e.g., generic algorithm + * class for the problem). + * @param name Name for the registered algorithm class. + * @param algoClass Algorithm class to register. + */ + public static void registerAlgorithm(Class> baseAlgorithm, + String name, Class> algoClass) { + if (!ALGORITHMS.containsKey(baseAlgorithm)) { + ALGORITHMS.put(baseAlgorithm, new LinkedHashMap<>()); + } + ALGORITHMS.get(baseAlgorithm).put(name, algoClass); + } + + /** + * Create an instance of the given algorithm class using the given input data. + * Assuming algorithm correspond to a class "Algorithm", this function returns + * an object equivalent to `new Algorithm(data)`. + * + * @param algorithm Class of the algorithm to create. + * @param data Input data for the algorithm. + * + * @return A new instance of the given algorithm class using the given data. + * + * @throws Exception if something wrong happens when constructing the object, + * i.e. the given input data does not correspond to the given + * algorithm and/or no constructor that takes a single + * parameter of type (data.getClass()) exists. + */ + public static AbstractAlgorithm createAlgorithm( + Class> algorithm, AbstractInputData data) + throws Exception { + // Retrieve the set of constructors for the given algorithm class. + Constructor[] constructors = algorithm.getDeclaredConstructors(); + + // Within this set, find the constructor that can be called with "data" (only). + AbstractAlgorithm constructed = null; + for (Constructor c: constructors) { + Class[] params = c.getParameterTypes(); + if (params.length == 1 && params[0].isAssignableFrom(data.getClass())) { + c.setAccessible(true); + constructed = (AbstractAlgorithm) c.newInstance(new Object[] { data }); + break; + } + } + return constructed; + } + + /** + * Return the algorithm class corresponding to the given base algorithm class + * and name. The algorithm must have been previously registered using + * registerAlgorithm. + * + * @param baseAlgorithm Base algorithm class for the algorithm to retrieve. + * @param name Name of the algorithm to retrieve. + * + * @return Class corresponding to the given name. + * + * @see #registerAlgorithm + */ + public static Class> getAlgorithmClass( + Class> baseAlgorithm, String name) { + return ALGORITHMS.get(baseAlgorithm).get(name); + } + + /** + * Return the list of names corresponding to the registered algorithm classes + * for the given base algorithm class. + * + * @param baseAlgorithm Base algorithm class for the algorithm class names to + * retrieve. + * + * @return Names of the currently registered algorithms. + * + * @see #registerAlgorithm + */ + public static Set getAlgorithmNames( + Class> baseAlgorithm) { + if (!ALGORITHMS.containsKey(baseAlgorithm)) { + return new TreeSet<>(); + } + return ALGORITHMS.get(baseAlgorithm).keySet(); + } +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspector.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspector.java new file mode 100644 index 0000000..57010bd --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspector.java @@ -0,0 +1,43 @@ +package org.insa.graphs.algorithm; + +import org.insa.graphs.algorithm.AbstractInputData.Mode; +import org.insa.graphs.model.Arc; +//import org.insa.graphs.model.GraphStatistics; + +/** + * This class can be used to indicate to an algorithm which arcs can be used and + * the costs of the usable arcs.. + * + */ +public interface ArcInspector { + + /** + * Check if the given arc can be used (is allowed). + * + * @param arc Arc to check. + * + * @return true if the given arc is allowed. + */ + public boolean isAllowed(Arc arc); + + /** + * Find the cost of the given arc. + * + * @param arc Arc for which the cost should be returned. + * + * @return Cost of the arc. + */ + public double getCost(Arc arc); + + /** + * @return The maximum speed for this inspector, or + * {@link GraphStatistics#NO_MAXIMUM_SPEED} if none is set. + */ + /* public int getMaximumSpeed(); */ + + /** + * @return Mode for this arc inspector. + */ + public Mode getMode(); + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspectorFactory.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspectorFactory.java new file mode 100644 index 0000000..ab42c7d --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/ArcInspectorFactory.java @@ -0,0 +1,189 @@ +package org.insa.graphs.algorithm; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import org.insa.graphs.algorithm.AbstractInputData.Mode; +import org.insa.graphs.model.Arc; +//import org.insa.graphs.model.GraphStatistics; +import org.insa.graphs.model.AccessRestrictions.AccessMode; +import org.insa.graphs.model.AccessRestrictions.AccessRestriction; + +public class ArcInspectorFactory { + + /** + * @return List of all arc filters in this factory. + */ + public static List getAllFilters() { + List filters = new ArrayList<>(); + + // Common filters: + + // No filter (all arcs allowed): + filters.add(new ArcInspector() { + @Override + public boolean isAllowed(Arc arc) { + return true; + } + + @Override + public double getCost(Arc arc) { + return arc.getLength(); + } + + /* + @Override + public int getMaximumSpeed() { + return GraphStatistics.NO_MAXIMUM_SPEED; + } + */ + + @Override + public Mode getMode() { + return Mode.LENGTH; + } + + @Override + public String toString() { + return "Shortest path, all roads allowed"; + } + }); + + // Only road allowed for cars and length: + filters.add(new ArcInspector() { + @Override + public boolean isAllowed(Arc arc) { + return arc.getRoadInformation().getAccessRestrictions() + .isAllowedForAny(AccessMode.MOTORCAR, EnumSet.complementOf(EnumSet + .of(AccessRestriction.FORBIDDEN, AccessRestriction.PRIVATE))); + } + + @Override + public double getCost(Arc arc) { + return arc.getLength(); + } + + /* + @Override + public int getMaximumSpeed() { + return GraphStatistics.NO_MAXIMUM_SPEED; + } + */ + + @Override + public Mode getMode() { + return Mode.LENGTH; + } + + @Override + public String toString() { + return "Shortest path, only roads open for cars"; + } + }); + + // Only road allowed for cars and time: + + filters.add(new ArcInspector() { + @Override + public boolean isAllowed(Arc arc) { + return true; + } + + @Override + public double getCost(Arc arc) { + return arc.getMinimumTravelTime(); + } + + /* + @Override + public int getMaximumSpeed() { + return GraphStatistics.NO_MAXIMUM_SPEED; + } + */ + + @Override + public Mode getMode() { + return Mode.TIME; + } + + @Override + public String toString() { + return "Fastest path, all roads allowed"; + } + }); + + filters.add(new ArcInspector() { + @Override + public boolean isAllowed(Arc arc) { + return arc.getRoadInformation().getAccessRestrictions() + .isAllowedForAny(AccessMode.MOTORCAR, EnumSet.complementOf(EnumSet + .of(AccessRestriction.FORBIDDEN, AccessRestriction.PRIVATE))); + } + + @Override + public double getCost(Arc arc) { + return arc.getMinimumTravelTime(); + } + + /* + @Override + public int getMaximumSpeed() { + return GraphStatistics.NO_MAXIMUM_SPEED; + } + */ + + @Override + public Mode getMode() { + return Mode.TIME; + } + + @Override + public String toString() { + return "Fastest path, only roads open for cars"; + } + }); + + // Non-private roads for pedestrian and bicycle: + filters.add(new ArcInspector() { + + static final int maxPedestrianSpeed = 5 ; + + @Override + public boolean isAllowed(Arc arc) { + return arc.getRoadInformation().getAccessRestrictions() + .isAllowedForAny(AccessMode.FOOT, EnumSet.complementOf(EnumSet + .of(AccessRestriction.FORBIDDEN, AccessRestriction.PRIVATE))); + } + + @Override + public double getCost(Arc arc) { + return arc.getTravelTime( + Math.min(maxPedestrianSpeed, arc.getRoadInformation().getMaximumSpeed())); + } + + @Override + public String toString() { + return "Fastest path for pedestrian"; + } + + /* + @Override + public int getMaximumSpeed() { + return 5; + } + */ + + @Override + public Mode getMode() { + return Mode.TIME; + } + }); + + // Add your own filters here (do not forget to implement toString() + // to get an understandable output!): + + return filters; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingAlgorithm.java new file mode 100644 index 0000000..a895e38 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingAlgorithm.java @@ -0,0 +1,24 @@ +package org.insa.graphs.algorithm.carpooling; + +import org.insa.graphs.algorithm.AbstractAlgorithm; + +public abstract class CarPoolingAlgorithm extends AbstractAlgorithm { + + protected CarPoolingAlgorithm(CarPoolingData data) { + super(data); + } + + @Override + public CarPoolingSolution run() { + return (CarPoolingSolution) super.run(); + } + + @Override + protected abstract CarPoolingSolution doRun(); + + @Override + public CarPoolingData getInputData() { + return (CarPoolingData) super.getInputData(); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingData.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingData.java new file mode 100644 index 0000000..d281f72 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingData.java @@ -0,0 +1,13 @@ +package org.insa.graphs.algorithm.carpooling; + +import org.insa.graphs.algorithm.AbstractInputData; +import org.insa.graphs.algorithm.ArcInspector; +import org.insa.graphs.model.Graph; + +public class CarPoolingData extends AbstractInputData { + + protected CarPoolingData(Graph graph, ArcInspector arcFilter) { + super(graph, arcFilter); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingGraphicObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingGraphicObserver.java new file mode 100644 index 0000000..eaffac6 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingGraphicObserver.java @@ -0,0 +1,5 @@ +package org.insa.graphs.algorithm.carpooling; + +public class CarPoolingGraphicObserver implements CarPoolingObserver { + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingObserver.java new file mode 100644 index 0000000..7726e92 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingObserver.java @@ -0,0 +1,5 @@ +package org.insa.graphs.algorithm.carpooling; + +public interface CarPoolingObserver { + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingSolution.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingSolution.java new file mode 100644 index 0000000..701c47b --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingSolution.java @@ -0,0 +1,11 @@ +package org.insa.graphs.algorithm.carpooling; + +import org.insa.graphs.algorithm.AbstractSolution; + +public class CarPoolingSolution extends AbstractSolution { + + protected CarPoolingSolution(CarPoolingData data, Status status) { + super(data, status); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingTextObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingTextObserver.java new file mode 100644 index 0000000..0327a95 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/carpooling/CarPoolingTextObserver.java @@ -0,0 +1,5 @@ +package org.insa.graphs.algorithm.carpooling; + +public class CarPoolingTextObserver implements CarPoolingObserver { + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchAlgorithm.java new file mode 100644 index 0000000..191e927 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchAlgorithm.java @@ -0,0 +1,29 @@ +package org.insa.graphs.algorithm.packageswitch; + +import org.insa.graphs.algorithm.AbstractAlgorithm; + +public abstract class PackageSwitchAlgorithm extends AbstractAlgorithm { + + /** + * Create a new PackageSwitchAlgorithm with the given data. + * + * @param data + */ + protected PackageSwitchAlgorithm(PackageSwitchData data) { + super(data); + } + + @Override + public PackageSwitchSolution run() { + return (PackageSwitchSolution) super.run(); + } + + @Override + protected abstract PackageSwitchSolution doRun(); + + @Override + public PackageSwitchData getInputData() { + return (PackageSwitchData) super.getInputData(); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchData.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchData.java new file mode 100644 index 0000000..cc9e6d5 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchData.java @@ -0,0 +1,13 @@ +package org.insa.graphs.algorithm.packageswitch; + +import org.insa.graphs.algorithm.AbstractInputData; +import org.insa.graphs.algorithm.ArcInspector; +import org.insa.graphs.model.Graph; + +public class PackageSwitchData extends AbstractInputData { + + protected PackageSwitchData(Graph graph, ArcInspector arcFilter) { + super(graph, arcFilter); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchGraphicObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchGraphicObserver.java new file mode 100644 index 0000000..6b4af91 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchGraphicObserver.java @@ -0,0 +1,5 @@ +package org.insa.graphs.algorithm.packageswitch; + +public class PackageSwitchGraphicObserver implements PackageSwitchObserver { + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchObserver.java new file mode 100644 index 0000000..c7313b7 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchObserver.java @@ -0,0 +1,5 @@ +package org.insa.graphs.algorithm.packageswitch; + +public interface PackageSwitchObserver { + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchSolution.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchSolution.java new file mode 100644 index 0000000..93562bb --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchSolution.java @@ -0,0 +1,11 @@ +package org.insa.graphs.algorithm.packageswitch; + +import org.insa.graphs.algorithm.AbstractSolution; + +public class PackageSwitchSolution extends AbstractSolution { + + protected PackageSwitchSolution(PackageSwitchData data, Status status) { + super(data, status); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchTextObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchTextObserver.java new file mode 100644 index 0000000..d0225d5 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/packageswitch/PackageSwitchTextObserver.java @@ -0,0 +1,5 @@ +package org.insa.graphs.algorithm.packageswitch; + +public class PackageSwitchTextObserver implements PackageSwitchObserver { + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/AStarAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/AStarAlgorithm.java new file mode 100644 index 0000000..fd172f0 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/AStarAlgorithm.java @@ -0,0 +1,9 @@ +package org.insa.graphs.algorithm.shortestpath; + +public class AStarAlgorithm extends DijkstraAlgorithm { + + public AStarAlgorithm(ShortestPathData data) { + super(data); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/BellmanFordAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/BellmanFordAlgorithm.java new file mode 100644 index 0000000..42986aa --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/BellmanFordAlgorithm.java @@ -0,0 +1,100 @@ +package org.insa.graphs.algorithm.shortestpath; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +import org.insa.graphs.algorithm.AbstractSolution.Status; +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.Path; + +public class BellmanFordAlgorithm extends ShortestPathAlgorithm { + + public BellmanFordAlgorithm(ShortestPathData data) { + super(data); + } + + @Override + protected ShortestPathSolution doRun() { + + // Retrieve the graph. + ShortestPathData data = getInputData(); + Graph graph = data.getGraph(); + + final int nbNodes = graph.size(); + + // Initialize array of distances. + double[] distances = new double[nbNodes]; + Arrays.fill(distances, Double.POSITIVE_INFINITY); + distances[data.getOrigin().getId()] = 0; + + // Notify observers about the first event (origin processed). + notifyOriginProcessed(data.getOrigin()); + + // Initialize array of predecessors. + Arc[] predecessorArcs = new Arc[nbNodes]; + + // Actual algorithm, we will assume the graph does not contain negative + // cycle... + boolean found = false; + for (int i = 0; !found && i < nbNodes; ++i) { + found = true; + for (Node node: graph.getNodes()) { + for (Arc arc: node.getSuccessors()) { + + // Small test to check allowed roads... + if (!data.isAllowed(arc)) { + continue; + } + + // Retrieve weight of the arc. + double w = data.getCost(arc); + double oldDistance = distances[arc.getDestination().getId()]; + double newDistance = distances[node.getId()] + w; + + if (Double.isInfinite(oldDistance) && Double.isFinite(newDistance)) { + notifyNodeReached(arc.getDestination()); + } + + // Check if new distances would be better, if so update... + if (newDistance < oldDistance) { + found = false; + distances[arc.getDestination().getId()] = distances[node.getId()] + w; + predecessorArcs[arc.getDestination().getId()] = arc; + } + } + } + } + + ShortestPathSolution solution = null; + + // Destination has no predecessor, the solution is infeasible... + if (predecessorArcs[data.getDestination().getId()] == null) { + solution = new ShortestPathSolution(data, Status.INFEASIBLE); + } + else { + + // The destination has been found, notify the observers. + notifyDestinationReached(data.getDestination()); + + // Create the path from the array of predecessors... + ArrayList arcs = new ArrayList<>(); + Arc arc = predecessorArcs[data.getDestination().getId()]; + while (arc != null) { + arcs.add(arc); + arc = predecessorArcs[arc.getOrigin().getId()]; + } + + // Reverse the path... + Collections.reverse(arcs); + + // Create the final solution. + solution = new ShortestPathSolution(data, Status.OPTIMAL, new Path(graph, arcs)); + } + + return solution; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/DijkstraAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/DijkstraAlgorithm.java new file mode 100644 index 0000000..bacb8e3 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/DijkstraAlgorithm.java @@ -0,0 +1,17 @@ +package org.insa.graphs.algorithm.shortestpath; + +public class DijkstraAlgorithm extends ShortestPathAlgorithm { + + public DijkstraAlgorithm(ShortestPathData data) { + super(data); + } + + @Override + protected ShortestPathSolution doRun() { + final ShortestPathData data = getInputData(); + ShortestPathSolution solution = null; + // TODO: + return solution; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathAlgorithm.java new file mode 100644 index 0000000..005e40c --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathAlgorithm.java @@ -0,0 +1,69 @@ +package org.insa.graphs.algorithm.shortestpath; + +import org.insa.graphs.algorithm.AbstractAlgorithm; +import org.insa.graphs.model.Node; + +public abstract class ShortestPathAlgorithm extends AbstractAlgorithm { + + protected ShortestPathAlgorithm(ShortestPathData data) { + super(data); + } + + @Override + public ShortestPathSolution run() { + return (ShortestPathSolution) super.run(); + } + + @Override + protected abstract ShortestPathSolution doRun(); + + @Override + public ShortestPathData getInputData() { + return (ShortestPathData) super.getInputData(); + } + + /** + * Notify all observers that the origin has been processed. + * + * @param node Origin. + */ + public void notifyOriginProcessed(Node node) { + for (ShortestPathObserver obs: getObservers()) { + obs.notifyOriginProcessed(node); + } + } + + /** + * Notify all observers that a node has been reached for the first time. + * + * @param node Node that has been reached. + */ + public void notifyNodeReached(Node node) { + for (ShortestPathObserver obs: getObservers()) { + obs.notifyNodeReached(node); + } + } + + /** + * Notify all observers that a node has been marked, i.e. its final value has + * been set. + * + * @param node Node that has been marked. + */ + public void notifyNodeMarked(Node node) { + for (ShortestPathObserver obs: getObservers()) { + obs.notifyNodeMarked(node); + } + } + + /** + * Notify all observers that the destination has been reached. + * + * @param node Destination. + */ + public void notifyDestinationReached(Node node) { + for (ShortestPathObserver obs: getObservers()) { + obs.notifyDestinationReached(node); + } + } +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathData.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathData.java new file mode 100644 index 0000000..7b3d014 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathData.java @@ -0,0 +1,47 @@ +package org.insa.graphs.algorithm.shortestpath; + +import org.insa.graphs.algorithm.AbstractInputData; +import org.insa.graphs.algorithm.ArcInspector; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; + +public class ShortestPathData extends AbstractInputData { + + // Origin and destination nodes. + private final Node origin, destination; + + /** + * Construct a new instance of ShortestPathInputData with the given parameters. + * + * @param graph Graph in which the path should be looked for. + * @param origin Origin node of the path. + * @param destination Destination node of the path. + * @param arcInspector Filter for arcs (used to allow only a specific set of + * arcs in the graph to be used). + */ + public ShortestPathData(Graph graph, Node origin, Node destination, ArcInspector arcInspector) { + super(graph, arcInspector); + this.origin = origin; + this.destination = destination; + } + + /** + * @return Origin node for the path. + */ + public Node getOrigin() { + return origin; + } + + /** + * @return Destination node for the path. + */ + public Node getDestination() { + return destination; + } + + @Override + public String toString() { + return "Shortest-path from #" + origin.getId() + " to #" + destination.getId() + " [" + + this.arcInspector.toString().toLowerCase() + "]"; + } +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathObserver.java new file mode 100644 index 0000000..d56d2dc --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathObserver.java @@ -0,0 +1,37 @@ +package org.insa.graphs.algorithm.shortestpath; + +import org.insa.graphs.model.Node; + +public interface ShortestPathObserver { + + /** + * Notify the observer that the origin has been processed. + * + * @param node Origin. + */ + public void notifyOriginProcessed(Node node); + + /** + * Notify the observer that a node has been reached for the first + * time. + * + * @param node Node that has been reached. + */ + public void notifyNodeReached(Node node); + + /** + * Notify the observer that a node has been marked, i.e. its final + * value has been set. + * + * @param node Node that has been marked. + */ + public void notifyNodeMarked(Node node); + + /** + * Notify the observer that the destination has been reached. + * + * @param node Destination. + */ + public void notifyDestinationReached(Node node); + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathSolution.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathSolution.java new file mode 100644 index 0000000..9122ac4 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathSolution.java @@ -0,0 +1,74 @@ +package org.insa.graphs.algorithm.shortestpath; + +import org.insa.graphs.algorithm.AbstractInputData.Mode; +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Path; +import org.insa.graphs.algorithm.AbstractSolution; + +public class ShortestPathSolution extends AbstractSolution { + + // Optimal solution. + private final Path path; + + /** + * Create a new infeasible shortest-path solution for the given input and + * status. + * + * @param data Original input data for this solution. + * @param status Status of the solution (UNKNOWN / INFEASIBLE). + */ + public ShortestPathSolution(ShortestPathData data, Status status) { + super(data, status); + this.path = null; + } + + /** + * Create a new shortest-path solution. + * + * @param data Original input data for this solution. + * @param status Status of the solution (FEASIBLE / OPTIMAL). + * @param path Path corresponding to the solution. + */ + public ShortestPathSolution(ShortestPathData data, Status status, Path path) { + super(data, status); + this.path = path; + } + + @Override + public ShortestPathData getInputData() { + return (ShortestPathData) super.getInputData(); + } + + /** + * @return The path of this solution, if any. + */ + public Path getPath() { + return path; + } + + @Override + public String toString() { + String info = null; + if (!isFeasible()) { + info = String.format("No path found from node #%d to node #%d", + getInputData().getOrigin().getId(), getInputData().getDestination().getId()); + } + else { + double cost = 0; + for (Arc arc: getPath().getArcs()) { + cost += getInputData().getCost(arc); + } + info = String.format("Found a path from node #%d to node #%d", + getInputData().getOrigin().getId(), getInputData().getDestination().getId()); + if (getInputData().getMode() == Mode.LENGTH) { + info = String.format("%s, %.4f kilometers", info, cost / 1000.0); + } + else { + info = String.format("%s, %.4f minutes", info, cost / 60.0); + } + } + info += " in " + getSolvingTime().getSeconds() + " seconds."; + return info; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathTextObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathTextObserver.java new file mode 100644 index 0000000..e74cc0a --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/shortestpath/ShortestPathTextObserver.java @@ -0,0 +1,37 @@ +package org.insa.graphs.algorithm.shortestpath; + +import java.io.PrintStream; + +import org.insa.graphs.model.Node; + +public class ShortestPathTextObserver implements ShortestPathObserver { + + private final PrintStream stream; + + public ShortestPathTextObserver(PrintStream stream) { + this.stream = stream; + } + + @Override + public void notifyOriginProcessed(Node node) { + // TODO Auto-generated method stub + + } + + @Override + public void notifyNodeReached(Node node) { + stream.println("Node " + node.getId() + " reached."); + } + + @Override + public void notifyNodeMarked(Node node) { + stream.println("Node " + node.getId() + " marked."); + } + + @Override + public void notifyDestinationReached(Node node) { + // TODO Auto-generated method stub + + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeap.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeap.java new file mode 100644 index 0000000..2c1a239 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeap.java @@ -0,0 +1,205 @@ +package org.insa.graphs.algorithm.utils; + +import java.util.ArrayList; + +/** + * Implements a binary heap containing elements of type E. + * + * Note that all comparisons are based on the compareTo method, hence E must + * implement Comparable + * + * @author Mark Allen Weiss + * @author DLB + */ +public class BinaryHeap> implements PriorityQueue { + + // Number of elements in heap. + private int currentSize; + + // The heap array. + protected final ArrayList array; + + /** + * Construct a new empty binary heap. + */ + public BinaryHeap() { + this.currentSize = 0; + this.array = new ArrayList(); + } + + /** + * Construct a copy of the given heap. + * + * @param heap Binary heap to copy. + */ + public BinaryHeap(BinaryHeap heap) { + this.currentSize = heap.currentSize; + this.array = new ArrayList(heap.array); + } + + /** + * Set an element at the given index. + * + * @param index Index at which the element should be set. + * @param value Element to set. + */ + private void arraySet(int index, E value) { + if (index == this.array.size()) { + this.array.add(value); + } + else { + this.array.set(index, value); + } + } + + /** + * @return Index of the parent of the given index. + */ + protected int indexParent(int index) { + return (index - 1) / 2; + } + + /** + * @return Index of the left child of the given index. + */ + protected int indexLeft(int index) { + return index * 2 + 1; + } + + /** + * Internal method to percolate up in the heap. + * + * @param index Index at which the percolate begins. + */ + private void percolateUp(int index) { + E x = this.array.get(index); + + for (; index > 0 + && x.compareTo(this.array.get(indexParent(index))) < 0; index = indexParent( + index)) { + E moving_val = this.array.get(indexParent(index)); + this.arraySet(index, moving_val); + } + + this.arraySet(index, x); + } + + /** + * Internal method to percolate down in the heap. + * + * @param index Index at which the percolate begins. + */ + private void percolateDown(int index) { + int ileft = indexLeft(index); + int iright = ileft + 1; + + if (ileft < this.currentSize) { + E current = this.array.get(index); + E left = this.array.get(ileft); + boolean hasRight = iright < this.currentSize; + E right = (hasRight) ? this.array.get(iright) : null; + + if (!hasRight || left.compareTo(right) < 0) { + // Left is smaller + if (left.compareTo(current) < 0) { + this.arraySet(index, left); + this.arraySet(ileft, current); + this.percolateDown(ileft); + } + } + else { + // Right is smaller + if (right.compareTo(current) < 0) { + this.arraySet(index, right); + this.arraySet(iright, current); + this.percolateDown(iright); + } + } + } + } + + @Override + public boolean isEmpty() { + return this.currentSize == 0; + } + + @Override + public int size() { + return this.currentSize; + } + + @Override + public void insert(E x) { + int index = this.currentSize++; + this.arraySet(index, x); + this.percolateUp(index); + } + + @Override + public void remove(E x) throws ElementNotFoundException { + // TODO: + } + + @Override + public E findMin() throws EmptyPriorityQueueException { + if (isEmpty()) + throw new EmptyPriorityQueueException(); + return this.array.get(0); + } + + @Override + public E deleteMin() throws EmptyPriorityQueueException { + E minItem = findMin(); + E lastItem = this.array.get(--this.currentSize); + this.arraySet(0, lastItem); + this.percolateDown(0); + return minItem; + } + + /** + * Creates a multi-lines string representing a sorted view of this binary heap. + * + * @return a string containing a sorted view this binary heap. + */ + public String toStringSorted() { + return BinaryHeapFormatter.toStringSorted(this, -1); + } + + /** + * Creates a multi-lines string representing a sorted view of this binary heap. + * + * @param maxElement Maximum number of elements to display. or {@code -1} to + * display all the elements. + * + * @return a string containing a sorted view this binary heap. + */ + public String toStringSorted(int maxElement) { + return BinaryHeapFormatter.toStringSorted(this, maxElement); + } + + /** + * Creates a multi-lines string representing a tree view of this binary heap. + * + * @return a string containing a tree view of this binary heap. + */ + public String toStringTree() { + return BinaryHeapFormatter.toStringTree(this, Integer.MAX_VALUE); + } + + /** + * Creates a multi-lines string representing a tree view of this binary heap. + * + * @param maxDepth Maximum depth of the tree to display. + * + * @return a string containing a tree view of this binary heap. + */ + public String toStringTree(int maxDepth) { + return BinaryHeapFormatter.toStringTree(this, maxDepth); + } + + @Override + public String toString() { + return BinaryHeapFormatter.toStringTree(this, 8); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeapFormatter.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeapFormatter.java new file mode 100644 index 0000000..861e1c3 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinaryHeapFormatter.java @@ -0,0 +1,198 @@ +package org.insa.graphs.algorithm.utils; + +import java.util.ArrayList; + +public class BinaryHeapFormatter { + + /** + * This class is used by {@link #toStringTree}, and simply contains three string + * accumulating. This is an immutable class. + * + */ + private static class Context { + + // Output text: + public final String acu; + + // Margin to get back exactly under the current position: + public final String margin; + + // Last margin used for the last child of a node. The markers are different: + public final String lastmargin; + + /** + * Creaet a new {@code Context}. + * + * @param acu The accumulated string. + * @param margin The current margin. + * @param lastMargin The last margin used. + */ + public Context(String acu, String margin, String lastMargin) { + this.acu = acu; + this.margin = margin; + this.lastmargin = lastMargin; + } + + /** + * Creates a new context by appending newlines to this context. + * + * @param n Number of newlines to append. + * + * @return a new context with {@code n} newlines appended. + */ + public Context appendNewlines(int n) { + if (n <= 0) { + return this; + } + else { + return (new Context(this.acu + "\n" + this.margin, this.margin, this.lastmargin) + .appendNewlines(n - 1)); + } + } + + /** + * Creates a new context by appending the given string to this context. + * + * @param count Number of spaces to add to the margin, or {@code null} to use + * the length of the string. + * @param text String to append. + * + * @return a new context with {@code text} appended. + */ + public Context appendText(Integer count, String text) { + int cnt = (count == null) ? text.length() : count; + final String spaces = new String(new char[cnt]).replace('\0', ' '); + return new Context(this.acu + text, this.margin + spaces, this.lastmargin + spaces); + } + + /** + * Creates a new context by appending a branch to this context. + * + * @param n Number of spaces to add to the margin, or {@code null} to use + * the length of the string. + * @param label Name of the branch. + * + * @return a new context with the branch appended. + */ + public Context appendBranch(Integer count, String label) { + final Context ctxt = this.appendText(count, label); + + if (count == null) { + return new Context(ctxt.acu + "_", ctxt.margin + "|", ctxt.margin + " "); + } + else { + return new Context(ctxt.acu, ctxt.margin + "|", ctxt.margin + " ") + .appendNewlines(1); + + } + } + } + + /* + * Input : ready to write the current node at the current context position. + * Output : the last character of acu is the last character of the current node. + */ + protected static > Context toStringLoop(BinaryHeap heap, + Context ctxt, int node, int max_depth) { + + if (max_depth < 0) { + return ctxt.appendText(null, "..."); + } + else { + E nodeval = heap.array.get(node); + String nodevals = nodeval.toString(); + + ArrayList childs = new ArrayList(); + // Add childs + int index_left = heap.indexLeft(node); + int index_right = index_left + 1; + + if (index_left < heap.size()) { + childs.add(index_left); + } + if (index_right < heap.size()) { + childs.add(index_right); + } + + Context ctxt2 = childs.isEmpty() ? ctxt.appendText(null, nodevals) + : ctxt.appendBranch(1, nodevals); + + for (int ch = 0; ch < childs.size(); ch++) { + boolean is_last = (ch == childs.size() - 1); + int child = childs.get(ch); + + if (is_last) { + Context ctxt3 = new Context(ctxt2.acu, ctxt2.lastmargin, ctxt2.lastmargin); + ctxt2 = new Context(toStringLoop(heap, ctxt3.appendText(null, "___"), child, + max_depth - 1).acu, ctxt2.margin, ctxt2.lastmargin); + } + else { + ctxt2 = new Context(toStringLoop(heap, ctxt2.appendText(null, "___"), child, + max_depth - 1).acu, ctxt2.margin, ctxt2.lastmargin).appendNewlines(2); + } + } + + return ctxt2; + } + } + + /** + * Creates a multi-lines string representing a tree view of the given binary + * heap. + * + * @param heap The binary heap to display. + * @param maxDepth Maximum depth of the tree to display. + * + * @return a string containing a tree view of the given binary heap. + */ + public static > String toStringTree(BinaryHeap heap, int maxDepth) { + final Context init_context = new Context(" ", " ", " "); + final Context result = toStringLoop(heap, init_context, 0, maxDepth); + return result.acu; + } + + /** + * Creates a multi-lines string representing a sorted view of the given binary + * heap. + * + * @param heap The binary heap to display. + * @param max_elements Maximum number of elements to display. or {@code -1} to + * display all the elements. + * + * @return a string containing a sorted view the given binary heap. + */ + public static > String toStringSorted(BinaryHeap heap, + int max_elements) { + String result = ""; + final BinaryHeap copy = new BinaryHeap(heap); + + final String truncate; + if (max_elements < 0 || max_elements >= heap.size()) { + truncate = ""; + } + else { + truncate = ", only " + max_elements + " elements are shown"; + } + + result += "======== Sorted HEAP (size = " + heap.size() + truncate + ") ========\n\n"; + + while (!copy.isEmpty() && max_elements-- != 0) { + result += copy.deleteMin() + "\n"; + } + + result += "\n-------- End of heap --------"; + + return result; + } + + public static void main(String[] args) { + final BinaryHeap heap = new BinaryHeap(); + + for (int i = 0; i < 12; i++) { + heap.insert(i); + } + + System.out.println(heap.toStringSorted(-1)); + System.out.println(heap.toStringTree(6)); + } +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinarySearchTree.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinarySearchTree.java new file mode 100644 index 0000000..32dfbb1 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/BinarySearchTree.java @@ -0,0 +1,64 @@ +package org.insa.graphs.algorithm.utils; + +import java.util.SortedSet; +import java.util.TreeSet; + +public class BinarySearchTree> implements PriorityQueue { + + // Underlying implementation + private final SortedSet sortedSet; + + /** + * Create a new empty binary search tree. + */ + public BinarySearchTree() { + this.sortedSet = new TreeSet<>(); + } + + /** + * Create a copy of the given binary search tree. + * + * @param bst Binary search tree to copy. + */ + public BinarySearchTree(BinarySearchTree bst) { + this.sortedSet = new TreeSet<>(bst.sortedSet); + } + + @Override + public boolean isEmpty() { + return sortedSet.isEmpty(); + } + + @Override + public int size() { + return sortedSet.size(); + } + + @Override + public void insert(E x) { + sortedSet.add(x); + } + + @Override + public void remove(E x) throws ElementNotFoundException { + if (!sortedSet.remove(x)) { + throw new ElementNotFoundException(x); + } + } + + @Override + public E findMin() throws EmptyPriorityQueueException { + if (isEmpty()) { + throw new EmptyPriorityQueueException(); + } + return sortedSet.first(); + } + + @Override + public E deleteMin() throws EmptyPriorityQueueException { + E min = findMin(); + remove(min); + return min; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/ElementNotFoundException.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/ElementNotFoundException.java new file mode 100644 index 0000000..3fb2729 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/ElementNotFoundException.java @@ -0,0 +1,32 @@ +package org.insa.graphs.algorithm.utils; + +public class ElementNotFoundException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + // Element not found + private final Object element; + + /** + * @param element Element that was not found. + */ + public ElementNotFoundException(Object element) { + this.element = element; + } + + /** + * @return The element that was not found. + */ + public Object getElement() { + return this.element; + } + + @Override + public String toString() { + return "element not found: " + element; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/EmptyPriorityQueueException.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/EmptyPriorityQueueException.java new file mode 100644 index 0000000..bf09c5d --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/EmptyPriorityQueueException.java @@ -0,0 +1,16 @@ +package org.insa.graphs.algorithm.utils; + +public class EmptyPriorityQueueException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * + */ + public EmptyPriorityQueueException() { + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/PriorityQueue.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/PriorityQueue.java new file mode 100644 index 0000000..104f79e --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/utils/PriorityQueue.java @@ -0,0 +1,81 @@ +package org.insa.graphs.algorithm.utils; + +/** + * Interface representing a basic priority queue. + * + * Implementation should enforce the required complexity of each method. + * + */ +public interface PriorityQueue> { + + /** + * Check if the priority queue is empty. + * + *

+ * Complexity: O(1) + *

+ * + * @return true if the queue is empty, false otherwise. + */ + public boolean isEmpty(); + + /** + * Get the number of elements in this queue. + * + *

+ * Complexity: O(1) + *

+ * + * @return Current size (number of elements) of this queue. + */ + public int size(); + + /** + * Insert the given element into the queue. + * + *

+ * Complexity: O(log n) + *

+ * + * @param x Item to insert. + */ + public void insert(E x); + + /** + * Remove the given element from the priority queue. + * + *

+ * Complexity: O(log n) + *

+ * + * @param x Item to remove. + */ + public void remove(E x) throws ElementNotFoundException; + + /** + * Retrieve (but not remove) the smallest item in the queue. + * + *

+ * Complexity: O(1) + *

+ * + * @return The smallest item in the queue. + * + * @throws EmptyPriorityQueueException if this queue is empty. + */ + public E findMin() throws EmptyPriorityQueueException; + + /** + * Remove and return the smallest item from the priority queue. + * + *

+ * Complexity: O(log n) + *

+ * + * @return The smallest item in the queue. + * + * @throws EmptyPriorityQueueException if this queue is empty. + */ + public E deleteMin() throws EmptyPriorityQueueException; + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentObserver.java new file mode 100644 index 0000000..e7b0a5a --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentObserver.java @@ -0,0 +1,30 @@ +package org.insa.graphs.algorithm.weakconnectivity; + +import java.util.ArrayList; + +import org.insa.graphs.model.Node; + +public interface WeaklyConnectedComponentObserver { + + /** + * Notify that the algorithm is entering a new component. + * + * @param curNode Starting node for the component. + */ + public void notifyStartComponent(Node curNode); + + /** + * Notify that a new node has been found for the current component. + * + * @param node New node found for the current component. + */ + public void notifyNewNodeInComponent(Node node); + + /** + * Notify that the algorithm has computed a new component. + * + * @param nodes List of nodes in the component. + */ + public void notifyEndComponent(ArrayList nodes); + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentTextObserver.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentTextObserver.java new file mode 100644 index 0000000..1a725c7 --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentTextObserver.java @@ -0,0 +1,36 @@ +package org.insa.graphs.algorithm.weakconnectivity; + +import java.io.PrintStream; +import java.util.ArrayList; + +import org.insa.graphs.model.Node; + +public class WeaklyConnectedComponentTextObserver implements WeaklyConnectedComponentObserver { + + // Number of the current component. + private int numComponent = 1; + + // Output stream + PrintStream stream; + + public WeaklyConnectedComponentTextObserver(PrintStream stream) { + this.stream = stream; + } + + @Override + public void notifyStartComponent(Node curNode) { + stream.print("Entering component #" + numComponent + " from node #" + curNode.getId() + "... "); + } + + @Override + public void notifyNewNodeInComponent(Node node) { + } + + @Override + public void notifyEndComponent(ArrayList nodes) { + stream.println(nodes.size() + " nodes found."); + stream.flush(); + numComponent += 1; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsAlgorithm.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsAlgorithm.java new file mode 100644 index 0000000..42462bb --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsAlgorithm.java @@ -0,0 +1,159 @@ +package org.insa.graphs.algorithm.weakconnectivity; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; + +import org.insa.graphs.algorithm.AbstractAlgorithm; +import org.insa.graphs.algorithm.AbstractSolution.Status; +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; + +public class WeaklyConnectedComponentsAlgorithm + extends AbstractAlgorithm { + + /** + * @param data Input data for this algorithm. + */ + public WeaklyConnectedComponentsAlgorithm(WeaklyConnectedComponentsData data) { + super(data); + } + + @Override + public WeaklyConnectedComponentsSolution run() { + return (WeaklyConnectedComponentsSolution) super.run(); + } + + @Override + public WeaklyConnectedComponentsData getInputData() { + return (WeaklyConnectedComponentsData) super.getInputData(); + } + + /** + * Notify all observers that the algorithm is entering a new component. + * + * @param curNode Starting node for the component. + */ + protected void notifyStartComponent(Node curNode) { + for (WeaklyConnectedComponentObserver obs: getObservers()) { + obs.notifyStartComponent(curNode); + } + } + + /** + * Notify all observers that a new node has been found for the current + * component. + * + * @param node New node found for the current component. + */ + protected void notifyNewNodeInComponent(Node node) { + for (WeaklyConnectedComponentObserver obs: getObservers()) { + obs.notifyNewNodeInComponent(node); + } + } + + /** + * Notify all observers that the algorithm has computed a new component. + * + * @param nodes List of nodes in the component. + */ + protected void notifyEndComponent(ArrayList nodes) { + for (WeaklyConnectedComponentObserver obs: getObservers()) { + obs.notifyEndComponent(nodes); + } + } + + /** + * @return An adjacency list for the undirected graph equivalent to the stored + * graph. + */ + protected ArrayList> createUndirectedGraph() { + int nNodes = getInputData().getGraph().size(); + ArrayList> res = new ArrayList>(nNodes); + for (int i = 0; i < nNodes; ++i) { + res.add(new HashSet()); + } + + for (Node node: getInputData().getGraph().getNodes()) { + for (Arc arc: node.getSuccessors()) { + res.get(node.getId()).add(arc.getDestination().getId()); + if (arc.getRoadInformation().isOneWay()) { + res.get(arc.getDestination().getId()).add(node.getId()); + } + } + } + + return res; + } + + /** + * Apply a breadth first search algorithm on the given undirected graph + * (adjacency list), starting at node cur, and marking nodes in marked. + * + * @param marked + * @param cur + * + * @return + */ + protected ArrayList bfs(ArrayList> ugraph, boolean[] marked, int cur) { + Graph graph = getInputData().getGraph(); + ArrayList component = new ArrayList(); + + // Using a queue because we are doing a BFS + Queue queue = new LinkedList(); + + // Notify observers about the current component. + notifyStartComponent(graph.get(cur)); + + // Add original node and loop until the queue is empty. + queue.add(cur); + marked[cur] = true; + while (!queue.isEmpty()) { + Node node = graph.get(queue.remove()); + component.add(node); + + // Notify observers + notifyNewNodeInComponent(node); + + for (Integer destId: ugraph.get(node.getId())) { + Node dest = graph.get(destId); + if (!marked[dest.getId()]) { + queue.add(destId); + marked[destId] = true; + } + } + } + + notifyEndComponent(component); + + return component; + } + + @Override + protected WeaklyConnectedComponentsSolution doRun() { + + Graph graph = getInputData().getGraph(); + ArrayList> ugraph = createUndirectedGraph(); + boolean[] marked = new boolean[graph.size()]; + Arrays.fill(marked, false); + + ArrayList> components = new ArrayList>(); + + // perform algorithm + int cur = 0; + while (cur < marked.length) { + // Apply BFS + components.add(this.bfs(ugraph, marked, cur)); + + // Find next non-marked + for (; cur < marked.length && marked[cur]; ++cur) + ; + } + + return new WeaklyConnectedComponentsSolution(getInputData(), Status.OPTIMAL, components); + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsData.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsData.java new file mode 100644 index 0000000..553a40b --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsData.java @@ -0,0 +1,20 @@ +package org.insa.graphs.algorithm.weakconnectivity; + +import org.insa.graphs.algorithm.AbstractInputData; +import org.insa.graphs.model.Graph; + +public class WeaklyConnectedComponentsData extends AbstractInputData { + + /** + * @param graph Graph for which components should be retrieved. + */ + public WeaklyConnectedComponentsData(Graph graph) { + super(graph, null); + } + + @Override + public String toString() { + return "Weakly-connected components from #0."; + } + +} diff --git a/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsSolution.java b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsSolution.java new file mode 100644 index 0000000..a0b2d4a --- /dev/null +++ b/be-graphes-algos/src/main/java/org/insa/graphs/algorithm/weakconnectivity/WeaklyConnectedComponentsSolution.java @@ -0,0 +1,57 @@ +package org.insa.graphs.algorithm.weakconnectivity; + +import java.util.ArrayList; + +import org.insa.graphs.algorithm.AbstractSolution; +import org.insa.graphs.model.Node; + +public class WeaklyConnectedComponentsSolution extends AbstractSolution { + + // Components + private ArrayList> components; + + protected WeaklyConnectedComponentsSolution(WeaklyConnectedComponentsData data) { + super(data); + } + + protected WeaklyConnectedComponentsSolution(WeaklyConnectedComponentsData data, Status status, + ArrayList> components) { + super(data, status); + this.components = components; + } + + @Override + public WeaklyConnectedComponentsData getInputData() { + return (WeaklyConnectedComponentsData) super.getInputData(); + } + + /** + * @return Components of the solution, if any. + */ + public ArrayList> getComponents() { + return components; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + int nIsolated = 0; + int nGt10 = 0; + for (ArrayList component: components) { + if (component.size() == 1) { + nIsolated += 1; + } + else if (component.size() > 10) { + nGt10 += 1; + } + } + return "Found " + components.size() + " components (" + nGt10 + " with more than 10 nodes, " + + nIsolated + " isolated nodes) in " + getSolvingTime().getSeconds() + " seconds."; + + } + +} diff --git a/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinaryHeapTest.java b/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinaryHeapTest.java new file mode 100644 index 0000000..df91636 --- /dev/null +++ b/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinaryHeapTest.java @@ -0,0 +1,15 @@ +package org.insa.graphs.algorithm.utils; + +public class BinaryHeapTest extends PriorityQueueTest { + + @Override + public PriorityQueue createQueue() { + return new BinaryHeap<>(); + } + + @Override + public PriorityQueue createQueue(PriorityQueue queue) { + return new BinaryHeap<>((BinaryHeap) queue); + } + +} diff --git a/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinarySearchTreeTest.java b/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinarySearchTreeTest.java new file mode 100644 index 0000000..803846d --- /dev/null +++ b/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/BinarySearchTreeTest.java @@ -0,0 +1,15 @@ +package org.insa.graphs.algorithm.utils; + +public class BinarySearchTreeTest extends PriorityQueueTest { + + @Override + public PriorityQueue createQueue() { + return new BinarySearchTree<>(); + } + + @Override + public PriorityQueue createQueue(PriorityQueue queue) { + return new BinarySearchTree<>((BinarySearchTree) queue); + } + +} diff --git a/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/PriorityQueueTest.java b/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/PriorityQueueTest.java new file mode 100644 index 0000000..c796598 --- /dev/null +++ b/be-graphes-algos/src/test/java/org/insa/graphs/algorithm/utils/PriorityQueueTest.java @@ -0,0 +1,320 @@ +package org.insa.graphs.algorithm.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.IntStream; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public abstract class PriorityQueueTest { + + /** + * Needs to be implemented by child class to actually provide priority queue + * implementation. + * + * @return A new instance of a PriorityQueue implementation. + */ + public abstract PriorityQueue createQueue(); + + /** + * Needs to be implemented by child class to actually provide priority queue + * implementation. + * + * @param queue Queue to copy. + * + * @return Copy of the given queue. + */ + public abstract PriorityQueue createQueue(PriorityQueue queue); + + protected static class MutableInteger implements Comparable { + + // Actual value + private int value; + + public MutableInteger(int value) { + this.value = value; + } + + /** + * @return The integer value stored inside this MutableInteger. + */ + public int get() { + return this.value; + } + + /** + * Update the integer value stored inside this MutableInteger. + * + * @param value New value to set. + */ + public void set(int value) { + this.value = value; + } + + @Override + public int compareTo(MutableInteger other) { + return Integer.compare(this.value, other.value); + } + + @Override + public String toString() { + return Integer.toString(get()); + } + + }; + + protected static class TestParameters> { + + // Data to insert + public final E[] data; + public final int[] deleteOrder; + + public TestParameters(E[] data, int[] deleteOrder) { + this.data = data; + this.deleteOrder = deleteOrder; + } + + }; + + /** + * Set of parameters. + * + */ + @Parameters + public static Collection data() { + Collection objects = new ArrayList<>(); + + // Empty queue + objects.add(new TestParameters<>(new MutableInteger[0], new int[0])); + + // Queue with 50 elements from 0 to 49, inserted in order and deleted in order. + objects.add(new TestParameters<>( + IntStream.range(0, 50).mapToObj(MutableInteger::new).toArray(MutableInteger[]::new), + IntStream.range(0, 50).toArray())); + + // Queue with 20 elements from 0 to 19, inserted in order, deleted in the given + // order. + objects.add(new TestParameters<>( + IntStream.range(0, 20).mapToObj(MutableInteger::new).toArray(MutableInteger[]::new), + new int[] { 12, 17, 18, 19, 4, 5, 3, 2, 0, 9, 10, 16, 8, 14, 13, 15, 7, 6, 1, + 11 })); + + // Queue with 7 elements. + objects.add( + new TestParameters<>( + Arrays.stream(new int[] { 8, 1, 6, 3, 4, 5, 9 }) + .mapToObj(MutableInteger::new).toArray(MutableInteger[]::new), + new int[] { 6, 5, 0, 1, 4, 2, 3 })); + + // Queue with 7 elements. + objects.add( + new TestParameters<>( + Arrays.stream(new int[] { 1, 7, 4, 8, 9, 6, 5 }) + .mapToObj(MutableInteger::new).toArray(MutableInteger[]::new), + new int[] { 2, 0, 1, 3, 4, 5, 6 })); + + // Queue with 13 elements. + objects.add(new TestParameters<>( + Arrays.stream(new int[] { 1, 7, 2, 8, 9, 3, 4, 10, 11, 12, 13, 5, 6 }) + .mapToObj(MutableInteger::new).toArray(MutableInteger[]::new), + new int[] { 3, 4, 0, 2, 5, 6, 1, 7, 8, 9, 10, 11, 12 })); + + return objects; + } + + @Parameter + public TestParameters parameters; + + // Actual queue. + private PriorityQueue queue; + + @Before + public void init() { + // Create the range queue + this.queue = createQueue(); + + for (MutableInteger v: parameters.data) { + this.queue.insert(v); + } + } + + @Test + public void testIsEmpty() { + assertEquals(parameters.data.length == 0, this.queue.isEmpty()); + } + + @Test + public void testSize() { + assertEquals(parameters.data.length, this.queue.size()); + } + + @Test + public void testInsert() { + PriorityQueue queue = createQueue(); + int size = 0; + for (MutableInteger x: parameters.data) { + queue.insert(x); + assertEquals(++size, queue.size()); + } + assertEquals(parameters.data.length, queue.size()); + MutableInteger[] range = Arrays.copyOf(parameters.data, parameters.data.length); + Arrays.sort(range); + + for (MutableInteger mi: range) { + assertEquals(mi.get(), queue.deleteMin().value); + assertEquals(--size, queue.size()); + } + } + + @Test(expected = EmptyPriorityQueueException.class) + public void testEmptyFindMin() { + Assume.assumeTrue(queue.isEmpty()); + queue.findMin(); + } + + @Test + public void testFindMin() { + Assume.assumeFalse(queue.isEmpty()); + assertEquals(Collections.min(Arrays.asList(parameters.data)).get(), queue.findMin().get()); + } + + @Test(expected = EmptyPriorityQueueException.class) + public void testEmptyDeleteMin() { + Assume.assumeTrue(queue.isEmpty()); + queue.deleteMin(); + } + + @Test + public void testDeleteMin() { + int size = parameters.data.length; + assertEquals(size, queue.size()); + MutableInteger[] range = Arrays.copyOf(parameters.data, parameters.data.length); + Arrays.sort(range); + for (MutableInteger x: range) { + assertEquals(x, queue.deleteMin()); + size -= 1; + assertEquals(size, queue.size()); + } + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + } + + @Test(expected = ElementNotFoundException.class) + public void testRemoveEmpty() { + Assume.assumeTrue(queue.isEmpty()); + queue.remove(new MutableInteger(0)); + } + + @Test + public void testRemoveNotFound() { + Assume.assumeFalse(queue.isEmpty()); + List data = Arrays.asList(parameters.data); + MutableInteger min = new MutableInteger(Collections.min(data).get() - 1), + max = new MutableInteger(Collections.max(data).get() + 1); + try { + queue.remove(min); + fail("Expected exception " + ElementNotFoundException.class.getName()); + } + catch (ElementNotFoundException e) { + assertEquals(min, e.getElement()); + } + try { + queue.remove(max); + fail("Expected exception " + ElementNotFoundException.class.getName()); + } + catch (ElementNotFoundException e) { + assertEquals(max, e.getElement()); + } + } + + @Test + public void testDeleteThenRemove() { + Assume.assumeFalse(queue.isEmpty()); + while (!queue.isEmpty()) { + MutableInteger min = queue.deleteMin(); + try { + queue.remove(min); + fail("Expected exception " + ElementNotFoundException.class.getName()); + } + catch (ElementNotFoundException e) { + assertEquals(min, e.getElement()); + } + } + } + + @Test + public void testRemoveTwice() { + Assume.assumeFalse(queue.isEmpty()); + for (MutableInteger data: parameters.data) { + PriorityQueue copyQueue = this.createQueue(this.queue); + copyQueue.remove(data); + try { + copyQueue.remove(data); + fail("Expected exception " + ElementNotFoundException.class.getName()); + } + catch (ElementNotFoundException e) { + assertEquals(data, e.getElement()); + } + } + } + + @Test + public void testRemove() { + int size1 = queue.size(); + for (int i = 0; i < parameters.deleteOrder.length; ++i) { + // Remove from structure + queue.remove(parameters.data[parameters.deleteOrder[i]]); + + // Copy the remaining elements + PriorityQueue copyTree = createQueue(queue); + + // Retrieve all remaining elements in both structures + ArrayList remains_in = new ArrayList<>(), + remains_cp = new ArrayList<>(); + for (int j = i + 1; j < parameters.deleteOrder.length; ++j) { + remains_in.add(parameters.data[parameters.deleteOrder[j]]); + remains_cp.add(copyTree.deleteMin()); + } + + Collections.sort(remains_in); + + // Check that the copy is now empty, and that both list contains all + // elements. + assertTrue(copyTree.isEmpty()); + assertEquals(remains_in, remains_cp); + + // Check that the size of the original tree is correct. + assertEquals(--size1, queue.size()); + } + assertTrue(queue.isEmpty()); + } + + @Test + public void testRemoveThenAdd() { + Assume.assumeFalse(queue.isEmpty()); + int min = Collections.min(Arrays.asList(parameters.data)).get(); + for (MutableInteger mi: parameters.data) { + queue.remove(mi); + assertEquals(parameters.data.length - 1, queue.size()); + mi.set(--min); + queue.insert(mi); + assertEquals(parameters.data.length, queue.size()); + assertEquals(min, queue.findMin().get()); + } + } + +} diff --git a/be-graphes-gui/pom.xml b/be-graphes-gui/pom.xml new file mode 100644 index 0000000..8efa922 --- /dev/null +++ b/be-graphes-gui/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.insa.graphs + be-graphes-all + 0.0.1-SNAPSHOT + + + + 0.13.0 + + + be-graphes-gui + be-graphes-gui + + + + + jitpack.io + https://jitpack.io + + + + + + + org.insa.graphs + be-graphes-model + ${project.version} + + + + org.insa.graphs + be-graphes-algos + ${project.version} + + + + + net.sf.kxml + kxml2 + 2.3.0 + + + + + org.mapsforge + mapsforge-themes + ${mapsforge.version} + + + + + org.mapsforge + mapsforge-map + ${mapsforge.version} + + + + + org.mapsforge + mapsforge-map-awt + ${mapsforge.version} + + + + + org.mapsforge + mapsforge-themes + ${mapsforge.version} + + + + + org.mapsforge + mapsforge-map-reader + ${mapsforge.version} + + + + + + diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/AlgorithmPanel.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/AlgorithmPanel.java new file mode 100644 index 0000000..75da459 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/AlgorithmPanel.java @@ -0,0 +1,381 @@ +package org.insa.graphs.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import org.insa.graphs.algorithm.AbstractAlgorithm; +import org.insa.graphs.algorithm.AlgorithmFactory; +import org.insa.graphs.algorithm.ArcInspector; +import org.insa.graphs.algorithm.ArcInspectorFactory; +import org.insa.graphs.gui.NodesInputPanel.InputChangedEvent; +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.utils.ColorUtils; +import org.insa.graphs.model.Node; + +public class AlgorithmPanel extends JPanel implements DrawingChangeListener { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public class StartActionEvent extends ActionEvent { + + /** + * + */ + private static final long serialVersionUID = 4090710269781229078L; + + protected static final String START_EVENT_COMMAND = "allInputFilled"; + + protected static final int START_EVENT_ID = 0x1; + + private final List nodes; + private final Class> algoClass; + + private final ArcInspector arcFilter; + + private final boolean graphicVisualization; + private final boolean textualVisualization; + + public StartActionEvent(Class> algoClass, List nodes, + ArcInspector arcFilter, boolean graphicVisualization, + boolean textualVisualization) { + super(AlgorithmPanel.this, START_EVENT_ID, START_EVENT_COMMAND); + this.nodes = nodes; + this.algoClass = algoClass; + this.graphicVisualization = graphicVisualization; + this.textualVisualization = textualVisualization; + this.arcFilter = arcFilter; + } + + /** + * @return Nodes associated with this event. + */ + public List getNodes() { + return this.nodes; + } + + /** + * @return Arc filter associated with this event. + */ + public ArcInspector getArcFilter() { + return this.arcFilter; + } + + /** + * @return Algorithm class associated with this event. + */ + public Class> getAlgorithmClass() { + return this.algoClass; + } + + /** + * @return true if graphic visualization is enabled. + */ + public boolean isGraphicVisualizationEnabled() { + return this.graphicVisualization; + } + + /** + * @return true if textual visualization is enabled. + */ + public boolean isTextualVisualizationEnabled() { + return this.textualVisualization; + } + + }; + + // Input panels for node. + protected NodesInputPanel nodesInputPanel; + + // Solution + protected SolutionPanel solutionPanel; + + // Component that can be enabled/disabled. + private ArrayList components = new ArrayList<>(); + + // Graphic / Text checkbox observer + private final JCheckBox graphicObserverCheckbox, textualObserverCheckbox; + + private JButton startAlgoButton; + + // Start listeners + List startActionListeners = new ArrayList<>(); + + /** + * Create a new AlgorithmPanel with the given parameters. + * + * @param parent Parent component for this panel. Only use for centering + * dialogs. + * @param baseAlgorithm Base algorithm for this algorithm panel. + * @param title Title of the panel. + * @param nodeNames Names of the input nodes. + * @param enableArcFilterSelection true to enable + * {@link ArcInspector} selection. + * + * @see ArcInspectorFactory + */ + public AlgorithmPanel(Component parent, Class> baseAlgorithm, + String title, String[] nodeNames, boolean enableArcFilterSelection) { + super(); + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + + setBorder(new EmptyBorder(15, 15, 15, 15)); + + // Set title. + add(createTitleLabel(title)); + add(Box.createVerticalStrut(8)); + + // Add algorithm selection + JComboBox algoSelect = createAlgoritmSelectComboBox(baseAlgorithm); + if (algoSelect.getItemCount() > 1) { + add(algoSelect); + components.add(algoSelect); + } + + // Add inputs for node. + this.nodesInputPanel = createNodesInputPanel(nodeNames); + add(this.nodesInputPanel); + components.add(this.nodesInputPanel); + + JComboBox arcFilterSelect = new JComboBox<>( + ArcInspectorFactory.getAllFilters().toArray(new ArcInspector[0])); + arcFilterSelect.setBackground(Color.WHITE); + + // Add mode selection + JPanel modeAndObserverPanel = new JPanel(); + modeAndObserverPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + modeAndObserverPanel.setLayout(new GridBagLayout()); + + graphicObserverCheckbox = new JCheckBox("Graphic"); + graphicObserverCheckbox.setSelected(true); + textualObserverCheckbox = new JCheckBox("Textual"); + + GridBagConstraints c = new GridBagConstraints(); + + c.fill = GridBagConstraints.HORIZONTAL; + + c.gridy = 2; + c.gridx = 0; + c.weightx = 0; + modeAndObserverPanel.add(new JLabel("Visualization: "), c); + c.gridx = 1; + c.weightx = 1; + modeAndObserverPanel.add(graphicObserverCheckbox, c); + c.gridx = 2; + c.weightx = 1; + modeAndObserverPanel.add(textualObserverCheckbox, c); + + if (enableArcFilterSelection) { + c.gridy = 1; + c.gridx = 0; + c.weightx = 0; + modeAndObserverPanel.add(new JLabel("Mode: "), c); + c.gridx = 1; + c.gridwidth = 2; + c.weightx = 1; + modeAndObserverPanel.add(arcFilterSelect, c); + } + + components.add(arcFilterSelect); + components.add(textualObserverCheckbox); + + add(modeAndObserverPanel); + + solutionPanel = new SolutionPanel(parent); + solutionPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + solutionPanel.setVisible(false); + add(Box.createVerticalStrut(10)); + add(solutionPanel); + + // Bottom panel + JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.LINE_AXIS)); + + startAlgoButton = new JButton("Start"); + startAlgoButton.setEnabled(false); + startAlgoButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + for (ActionListener lis: startActionListeners) { + lis.actionPerformed(new StartActionEvent( + AlgorithmFactory.getAlgorithmClass(baseAlgorithm, + (String) algoSelect.getSelectedItem()), + nodesInputPanel.getNodeForInputs(), + (ArcInspector) arcFilterSelect.getSelectedItem(), + graphicObserverCheckbox.isSelected(), + textualObserverCheckbox.isSelected())); + } + } + }); + + JButton hideButton = new JButton("Hide"); + hideButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + nodesInputPanel.setEnabled(false); + setVisible(false); + } + }); + + bottomPanel.add(startAlgoButton); + bottomPanel.add(Box.createHorizontalGlue()); + bottomPanel.add(hideButton); + + components.add(hideButton); + + bottomPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + add(Box.createVerticalStrut(8)); + add(bottomPanel); + + nodesInputPanel.addInputChangedListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + InputChangedEvent evt = (InputChangedEvent) e; + startAlgoButton.setEnabled(allNotNull(evt.getNodes())); + } + }); + + addComponentListener(new ComponentAdapter() { + + @Override + public void componentShown(ComponentEvent e) { + setEnabled(true); + nodesInputPanel.setVisible(true); + } + + @Override + public void componentHidden(ComponentEvent e) { + setEnabled(false); + nodesInputPanel.setVisible(false); + } + + }); + + setEnabled(false); + } + + /** + * Create the title JLabel for this panel. + * + * @param title Title for the label. + * + * @return A new JLabel containing the given title with proper font. + */ + protected JLabel createTitleLabel(String title) { + JLabel titleLabel = new JLabel(title); + titleLabel.setBackground(Color.RED); + titleLabel.setHorizontalAlignment(JLabel.LEFT); + titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + Font font = titleLabel.getFont(); + font = font.deriveFont(Font.BOLD, 18); + titleLabel.setFont(font); + return titleLabel; + } + + /** + * Create the combo box for the algorithm selection. + * + * @param baseAlgorithm Base algorithm for which the select box should be + * created. + * + * @return A new JComboBox containing algorithms for the given base algorithm. + * + * @see AlgorithmFactory + */ + protected JComboBox createAlgoritmSelectComboBox( + Class> baseAlgorithm) { + JComboBox algoSelect = new JComboBox<>( + AlgorithmFactory.getAlgorithmNames(baseAlgorithm).toArray(new String[0])); + algoSelect.setBackground(Color.WHITE); + algoSelect.setAlignmentX(Component.LEFT_ALIGNMENT); + return algoSelect; + + } + + /** + * Create a node input panel with the given node input names. + * + * @param nodeNames Field names for the inputs to create. + * + * @return A new NodesInputPanel containing inputs for the given names. + */ + protected NodesInputPanel createNodesInputPanel(String[] nodeNames) { + NodesInputPanel panel = new NodesInputPanel(); + panel.setAlignmentX(Component.LEFT_ALIGNMENT); + for (int i = 0; i < nodeNames.length; ++i) { + panel.addTextField(nodeNames[i] + ": ", ColorUtils.getColor(i)); + } + panel.setEnabled(false); + return panel; + } + + /** + * Check if the given list of nodes does not contain any null + * value. + * + * @param nodes List of {@link Node} to check. + * + * @return true if the list does not contain any null + * value, false otherwise. + */ + protected boolean allNotNull(List nodes) { + boolean allNotNull = true; + for (Node node: nodes) { + allNotNull = allNotNull && node != null; + } + return allNotNull; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + nodesInputPanel.setEnabled(enabled); + solutionPanel.setEnabled(enabled); + for (JComponent component: components) { + component.setEnabled(enabled); + } + graphicObserverCheckbox.setEnabled(enabled); + enabled = enabled && allNotNull(this.nodesInputPanel.getNodeForInputs()); + startAlgoButton.setEnabled(enabled); + } + + /** + * Add a new start action listener to this class. + * + * @param listener Listener to add. + */ + public void addStartActionListener(ActionListener listener) { + this.startActionListeners.add(listener); + } + + @Override + public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) { + } + + @Override + public void onRedrawRequest() { + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/BlockingActionFactory.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/BlockingActionFactory.java new file mode 100644 index 0000000..439c8cf --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/BlockingActionFactory.java @@ -0,0 +1,56 @@ +package org.insa.graphs.gui; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; + +import javax.swing.JOptionPane; + +public class BlockingActionFactory { + + // List of running actions. + private ArrayList actions = new ArrayList<>(); + + // Parent component. + private Component parentComponent; + + public BlockingActionFactory(Component parentComponent) { + this.parentComponent = parentComponent; + } + + public void addAction(RunningAction action) { + actions.add(action); + } + + public ActionListener createBlockingAction(ActionListener listener) { + return new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + boolean accepted = true; + + // Check if actions... + for (int i = 0; i < actions.size() && accepted; ++i) { + RunningAction action = actions.get(i); + // If action is running, ask user... + if (action.isRunning()) { + if (JOptionPane.showConfirmDialog(parentComponent, "Action {" + + action.getInformation() + + "} is running, do you want to stop it?") == JOptionPane.OK_OPTION) { + action.interrupt(); + } + else { + accepted = false; + } + } + } + + // If action is accepted, run it... + if (accepted) { + listener.actionPerformed(e); + } + } + }; + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/DrawingChangeListener.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/DrawingChangeListener.java new file mode 100644 index 0000000..40d6bfd --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/DrawingChangeListener.java @@ -0,0 +1,24 @@ +package org.insa.graphs.gui; + +import org.insa.graphs.gui.drawing.Drawing; + +public interface DrawingChangeListener { + + /** + * Event fired when a new drawing is loaded. + * + * @param oldDrawing Old drawing, may be null if no drawing exits prior to this + * one. + * @param newDrawing New drawing. + */ + public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing); + + /** + * Event fired when a redraw request is emitted - This is typically emitted + * after a onDrawingLoaded event, but not always, and request that elements are + * drawn again on the new drawing. + * + */ + public void onRedrawRequest(); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphChangeListener.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphChangeListener.java new file mode 100644 index 0000000..d438b7d --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphChangeListener.java @@ -0,0 +1,14 @@ +package org.insa.graphs.gui; + +import org.insa.graphs.model.Graph; + +public interface GraphChangeListener { + + /** + * Event fire when a new graph has been loaded. + * + * @param graph The new graph. + */ + public void newGraphLoaded(Graph graph); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphReaderProgressBar.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphReaderProgressBar.java new file mode 100644 index 0000000..8827a40 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/GraphReaderProgressBar.java @@ -0,0 +1,123 @@ +package org.insa.graphs.gui; + +import java.awt.Component; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.border.EmptyBorder; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.RoadInformation; +import org.insa.graphs.model.io.GraphReaderObserver; + +/** + * One-time use GraphReaderObserver that display progress in three different + * JProgressBar. + * + * @author Mikael + * + */ +public class GraphReaderProgressBar extends JDialog implements GraphReaderObserver { + + /** + * + */ + private static final long serialVersionUID = -1; + + // Index... + private static final int NODE = 0, DESC = 1, ARC = 2; + + // Progress bar + private final JProgressBar[] progressBars = new JProgressBar[3]; + + // Current element read, and modulo. + private int[] counters = new int[]{ 0, 0, 0 }; + private int[] modulos = new int[3]; + + public GraphReaderProgressBar(JFrame owner) { + super(owner); + this.setVisible(false); + setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + + final String[] infos = { "nodes", "road informations", "arcs" }; + + JPanel pane = new JPanel(); + pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS)); + pane.setBorder(new EmptyBorder(15, 15, 15, 15)); + pane.add(Box.createVerticalGlue()); + + for (int i = 0; i < 3; ++i) { + JLabel label = new JLabel("Reading " + infos[i] + "... "); + label.setAlignmentX(Component.LEFT_ALIGNMENT); + progressBars[i] = new JProgressBar(); + progressBars[i].setAlignmentX(Component.LEFT_ALIGNMENT); + pane.add(label); + pane.add(progressBars[i]); + } + + pane.add(Box.createVerticalGlue()); + setContentPane(pane); + pack(); + } + + @Override + public void notifyStartReading(String mapId) { + setTitle("Reading graph " + mapId + "... "); + setVisible(true); + } + + @Override + public void notifyEndReading() { + setVisible(false); + dispose(); + } + + protected void initProgressBar(int index, int max) { + progressBars[index].setMaximum(max); + modulos[index] = Math.max(max / 100, 1); + } + + protected void incCounter(int index) { + counters[index] += 1; + if (counters[index] % modulos[index] == 0) { + progressBars[index].setValue(counters[index]); + } + } + + @Override + public void notifyStartReadingNodes(int nNodes) { + initProgressBar(NODE, nNodes); + } + + @Override + public void notifyNewNodeRead(Node node) { + incCounter(NODE); + } + + @Override + public void notifyStartReadingDescriptors(int nDesc) { + initProgressBar(DESC, nDesc); + } + + @Override + public void notifyNewDescriptorRead(RoadInformation desc) { + incCounter(DESC); + } + + @Override + public void notifyStartReadingArcs(int nArcs) { + initProgressBar(ARC, nArcs); + } + + @Override + public void notifyNewArcRead(Arc arc) { + incCounter(ARC); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/MainWindow.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/MainWindow.java new file mode 100644 index 0000000..41ff400 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/MainWindow.java @@ -0,0 +1,862 @@ +package org.insa.graphs.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.KeyStroke; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; + +import org.insa.graphs.algorithm.AbstractSolution; +import org.insa.graphs.algorithm.AlgorithmFactory; +import org.insa.graphs.algorithm.carpooling.CarPoolingAlgorithm; +import org.insa.graphs.algorithm.packageswitch.PackageSwitchAlgorithm; +import org.insa.graphs.algorithm.shortestpath.ShortestPathAlgorithm; +import org.insa.graphs.algorithm.shortestpath.ShortestPathData; +import org.insa.graphs.algorithm.shortestpath.ShortestPathSolution; +import org.insa.graphs.algorithm.shortestpath.ShortestPathTextObserver; +import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentTextObserver; +import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentsAlgorithm; +import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentsData; +import org.insa.graphs.gui.AlgorithmPanel.StartActionEvent; +import org.insa.graphs.gui.drawing.BasicGraphPalette; +import org.insa.graphs.gui.drawing.BlackAndWhiteGraphPalette; +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.GraphPalette; +import org.insa.graphs.gui.drawing.components.BasicDrawing; +import org.insa.graphs.gui.drawing.components.MapViewDrawing; +import org.insa.graphs.gui.observers.ShortestPathGraphicObserver; +import org.insa.graphs.gui.observers.WeaklyConnectedComponentGraphicObserver; +import org.insa.graphs.gui.utils.FileUtils; +import org.insa.graphs.gui.utils.FileUtils.FolderType; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Path; +import org.insa.graphs.model.io.BinaryGraphReader; +import org.insa.graphs.model.io.BinaryPathReader; +import org.insa.graphs.model.io.GraphReader; +import org.insa.graphs.model.io.MapMismatchException; + +public class MainWindow extends JFrame { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * + */ + private static final String WINDOW_TITLE = "BE Graphes INSA"; + + /** + * + */ + private static final int THREAD_TIMER_DELAY = 1000; // in milliseconds + + // Current graph. + protected Graph graph; + + // Path to the last opened graph file. + private String graphFilePath; + + // Drawing and click adapter. + protected Drawing drawing; + private final MapViewDrawing mapViewDrawing; + private final BasicDrawing basicDrawing; + + private final GraphPalette basicPalette, blackAndWhitePalette; + private GraphPalette currentPalette; + + // Main panel. + private final JSplitPane mainPanel; + + // Algorithm panels + private final List algoPanels = new ArrayList<>(); + private final AlgorithmPanel wccPanel, spPanel, cpPanel, psPanel; + + // Path panel + private final PathsPanel pathPanel; + + // List of items that cannot be used without a graph + private final ArrayList graphLockItems = new ArrayList(); + + // Label containing the map ID of the current graph. + private JLabel graphInfoPanel; + + // Thread information + private Timer threadTimer; + private JPanel threadPanel; + + // Log stream and print stream + private StreamCapturer logStream; + + private PrintStream printStream; + + // Current running thread + private ThreadWrapper currentThread; + + // Factory + private BlockingActionFactory baf; + + // Observers + private List drawingChangeListeners = new ArrayList<>(); + private List graphChangeListeneres = new ArrayList<>(); + + public MainWindow() { + super(WINDOW_TITLE); + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + setLayout(new BorderLayout()); + + setMinimumSize(new Dimension(800, 600)); + + // Create drawing and action listeners... + this.basicDrawing = new BasicDrawing(); + this.mapViewDrawing = new MapViewDrawing(); + this.drawing = basicDrawing; + + // Createa palettes + this.basicPalette = new BasicGraphPalette(); + this.blackAndWhitePalette = new BlackAndWhiteGraphPalette(); + this.currentPalette = this.basicPalette; + + wccPanel = new AlgorithmPanel(this, WeaklyConnectedComponentsAlgorithm.class, + "Weakly-Connected Components", new String[] {}, false); + wccPanel.addStartActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + StartActionEvent evt = (StartActionEvent) e; + WeaklyConnectedComponentsData data = new WeaklyConnectedComponentsData(graph); + + WeaklyConnectedComponentsAlgorithm wccAlgorithm = null; + try { + wccAlgorithm = (WeaklyConnectedComponentsAlgorithm) AlgorithmFactory + .createAlgorithm(evt.getAlgorithmClass(), data); + } + catch (Exception e1) { + JOptionPane.showMessageDialog(MainWindow.this, + "An error occurred while creating the specified algorithm.", + "Internal error: Algorithm instantiation failure", + JOptionPane.ERROR_MESSAGE); + e1.printStackTrace(); + return; + } + + wccPanel.setEnabled(false); + + if (evt.isGraphicVisualizationEnabled()) { + wccAlgorithm.addObserver(new WeaklyConnectedComponentGraphicObserver(drawing)); + } + if (evt.isTextualVisualizationEnabled()) { + wccAlgorithm.addObserver(new WeaklyConnectedComponentTextObserver(printStream)); + } + + // We love Java... + final WeaklyConnectedComponentsAlgorithm copyAlgorithm = wccAlgorithm; + launchThread(new Runnable() { + @Override + public void run() { + AbstractSolution solution = copyAlgorithm.run(); + wccPanel.solutionPanel.addSolution(solution, false); + wccPanel.solutionPanel.setVisible(true); + wccPanel.setEnabled(true); + } + }); + } + }); + + spPanel = new AlgorithmPanel(this, ShortestPathAlgorithm.class, "Shortest-Path", + new String[] { "Origin", "Destination" }, true); + spPanel.addStartActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + StartActionEvent evt = (StartActionEvent) e; + ShortestPathData data = new ShortestPathData(graph, evt.getNodes().get(0), + evt.getNodes().get(1), evt.getArcFilter()); + + ShortestPathAlgorithm spAlgorithm = null; + try { + spAlgorithm = (ShortestPathAlgorithm) AlgorithmFactory + .createAlgorithm(evt.getAlgorithmClass(), data); + } + catch (Exception e1) { + JOptionPane.showMessageDialog(MainWindow.this, + "An error occurred while creating the specified algorithm.", + "Internal error: Algorithm instantiation failure", + JOptionPane.ERROR_MESSAGE); + e1.printStackTrace(); + return; + } + + spPanel.setEnabled(false); + + if (evt.isGraphicVisualizationEnabled()) { + spAlgorithm.addObserver(new ShortestPathGraphicObserver(drawing)); + } + if (evt.isTextualVisualizationEnabled()) { + spAlgorithm.addObserver(new ShortestPathTextObserver(printStream)); + } + + final ShortestPathAlgorithm copyAlgorithm = spAlgorithm; + launchThread(new Runnable() { + @Override + public void run() { + // Run the algorithm. + ShortestPathSolution solution = copyAlgorithm.run(); + // Add the solution to the solution panel (but do not display + // overlay). + spPanel.solutionPanel.addSolution(solution, false); + // If the solution is feasible, add the path to the path panel. + if (solution.isFeasible()) { + pathPanel.addPath(solution.getPath()); + } + // Show the solution panel and enable the shortest-path panel. + spPanel.solutionPanel.setVisible(true); + spPanel.setEnabled(true); + } + }); + } + }); + + cpPanel = new AlgorithmPanel(this, CarPoolingAlgorithm.class, "Car-Pooling", new String[] { + "Origin Car", "Origin Pedestrian", "Destination Car", "Destination Pedestrian" }, + true); + + psPanel = new AlgorithmPanel(this, PackageSwitchAlgorithm.class, "Car-Pooling", + new String[] { "Oribin A", "Origin B", "Destination A", "Destination B" }, true); + + // add algorithm panels + algoPanels.add(wccPanel); + algoPanels.add(spPanel); + algoPanels.add(cpPanel); + algoPanels.add(psPanel); + + this.pathPanel = new PathsPanel(this); + + // Add click listeners to both drawing. + + for (AlgorithmPanel panel: algoPanels) { + this.basicDrawing.addDrawingClickListener(panel.nodesInputPanel); + this.mapViewDrawing.addDrawingClickListener(panel.nodesInputPanel); + this.graphChangeListeneres.add(panel.nodesInputPanel); + this.graphChangeListeneres.add(panel.solutionPanel); + this.drawingChangeListeners.add(panel.nodesInputPanel); + this.drawingChangeListeners.add(panel.solutionPanel); + this.drawingChangeListeners.add(panel); + } + + this.graphChangeListeneres.add(pathPanel); + this.drawingChangeListeners.add(pathPanel); + + // Create action factory. + this.currentThread = new ThreadWrapper(this); + this.baf = new BlockingActionFactory(this); + this.baf.addAction(currentThread); + + // Click adapter + ActionListener openMapActionListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JFileChooser chooser = FileUtils.createFileChooser(FolderType.Map); + if (chooser.showOpenDialog(MainWindow.this) == JFileChooser.APPROVE_OPTION) { + graphFilePath = chooser.getSelectedFile().getAbsolutePath(); + + // Note: Don't use a try-resources block since loadGraph is asynchronous. + final DataInputStream stream; + try { + stream = new DataInputStream(new BufferedInputStream( + new FileInputStream(chooser.getSelectedFile()))); + } + catch (IOException e1) { + JOptionPane.showMessageDialog(MainWindow.this, + "Cannot open the selected file."); + return; + } + loadGraph(new BinaryGraphReader(stream)); + } + } + }; + + setJMenuBar(createMenuBar(openMapActionListener)); + + // Initial panel to show "Open Map... " + JPanel openPanel = new JPanel(); + openPanel.setLayout(new BoxLayout(openPanel, BoxLayout.PAGE_AXIS)); + JButton openButton = new JButton("Open Map... "); + openButton.setAlignmentX(Component.CENTER_ALIGNMENT); + openButton.addActionListener(openMapActionListener); + openButton.setFocusPainted(false); + openPanel.add(Box.createVerticalGlue()); + openPanel.add(openButton); + openPanel.add(Box.createVerticalGlue()); + + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + int confirmed = JOptionPane.showConfirmDialog(MainWindow.this, + "Are you sure you want to close the application?", "Exit Confirmation", + JOptionPane.YES_NO_OPTION); + + if (confirmed == JOptionPane.YES_OPTION) { + dispose(); + System.exit(0); + } + } + }); + + // Create graph area + mainPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + + JTextArea infoPanel = new JTextArea(); + infoPanel.setMinimumSize(new Dimension(200, 50)); + infoPanel.setBackground(Color.WHITE); + infoPanel.setLineWrap(true); + infoPanel.setEditable(false); + this.logStream = new StreamCapturer(infoPanel); + this.printStream = new PrintStream(this.logStream); + + JPanel rightComponent = new JPanel(); + rightComponent.setLayout(new GridBagLayout()); + + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.fill = GridBagConstraints.HORIZONTAL; + rightComponent.add(pathPanel, c); + + c.gridy = 1; + for (AlgorithmPanel panel: algoPanels) { + panel.setVisible(false); + rightComponent.add(panel, c); + } + + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 2; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + c.gridheight = GridBagConstraints.REMAINDER; + rightComponent.add(new JScrollPane(infoPanel), c); + + mainPanel.setResizeWeight(0.8); + mainPanel.setDividerSize(5); + + mainPanel.setBackground(Color.WHITE); + mainPanel.setLeftComponent(openPanel); + mainPanel.setRightComponent(rightComponent); + this.add(mainPanel, BorderLayout.CENTER); + + // Top Panel + this.add(createStatusBar(), BorderLayout.SOUTH); + + // Notify everythin + notifyDrawingLoaded(null, drawing); + } + + /** + * @param runnable + * @param canInterrupt + */ + private void launchThread(Runnable runnable, boolean canInterrupt) { + if (canInterrupt) { + currentThread.setThread(new Thread(new Runnable() { + @Override + public void run() { + threadTimer.restart(); + threadPanel.setVisible(true); + runnable.run(); + clearCurrentThread(); + } + })); + } + else { + currentThread.setThread(new Thread(runnable)); + } + currentThread.startThread(); + } + + private void launchThread(Runnable runnable) { + launchThread(runnable, true); + } + + protected void clearCurrentThread() { + threadTimer.stop(); + threadPanel.setVisible(false); + currentThread.setThread(null); + if (spPanel.isVisible()) { + spPanel.setEnabled(true); + } + } + + /** + * Notify all listeners that a new graph has been loaded. + */ + private void notifyNewGraphLoaded() { + for (GraphChangeListener listener: graphChangeListeneres) { + listener.newGraphLoaded(graph); + } + } + + /** + * Notify all listeners that a new drawing has been set up. + * + * @param oldDrawing + * @param newDrawing + */ + private void notifyDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) { + for (DrawingChangeListener listener: drawingChangeListeners) { + listener.onDrawingLoaded(oldDrawing, newDrawing); + } + } + + /** + * Notify all listeners that a redraw request is emitted. + */ + private void notifyRedrawRequest() { + for (DrawingChangeListener listener: drawingChangeListeners) { + listener.onRedrawRequest(); + } + } + + /** + * Draw the stored graph on the drawing. + */ + private void drawGraph(Class newClass, GraphPalette palette) { + + // Save old divider location + int oldLocation = mainPanel.getDividerLocation(); + + // Set drawing if not set + if (!(mainPanel.getLeftComponent() instanceof Drawing)) { + mainPanel.setLeftComponent((Component) this.drawing); + mainPanel.setDividerLocation(oldLocation); + // Need to re-validate or the drawing will not have the + // correct size prior to drawing, which can cause issue. + this.revalidate(); + } + + boolean isNewGraph = newClass == null; + boolean isMapView = (isNewGraph && drawing == mapViewDrawing) + || (!isNewGraph && newClass.equals(MapViewDrawing.class)); + + // We need to draw MapView, we have to check if the file exists. + File mfile = null; + if (isMapView) { + String mfpath = graphFilePath.substring(0, graphFilePath.lastIndexOf(".map")) + + ".mapfg"; + mfile = new File(mfpath); + if (!mfile.exists()) { + if (JOptionPane.showConfirmDialog(this, + "The associated mapsforge (.mapfg) file has not been found, do you want to specify it manually?", + "File not found", + JOptionPane.YES_NO_CANCEL_OPTION) == JOptionPane.YES_OPTION) { + JFileChooser chooser = new JFileChooser(mfile.getParentFile()); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + mfile = chooser.getSelectedFile(); + } + else { + mfile = null; + } + } + else { + mfile = null; + } + } + } + + Runnable runnable = null; + + if (isMapView && mfile != null) { + final File mfileFinal = mfile; + // It is a mapview drawing and the file was found, so: + // 1. We create the drawing if necessary. + if (drawing != mapViewDrawing) { + drawing.clear(); + drawing = mapViewDrawing; + mainPanel.setLeftComponent(mapViewDrawing); + mainPanel.setDividerLocation(oldLocation); + notifyDrawingLoaded(basicDrawing, mapViewDrawing); + drawing.clear(); + isNewGraph = true; + mainPanel.revalidate(); + } + if (isNewGraph) { + drawing.clear(); + runnable = new Runnable() { + public void run() { + ((MapViewDrawing) drawing).drawGraph(mfileFinal); + notifyRedrawRequest(); + } + }; + } + + } + else if (!isMapView || (isMapView && mfile == null && isNewGraph)) { + if (drawing == mapViewDrawing) { + mapViewDrawing.clear(); + drawing = basicDrawing; + mainPanel.setLeftComponent(basicDrawing); + mainPanel.setDividerLocation(oldLocation); + notifyDrawingLoaded(mapViewDrawing, basicDrawing); + isNewGraph = true; + } + if (isNewGraph || palette != this.currentPalette) { + this.currentPalette = palette; + drawing.clear(); + runnable = new Runnable() { + public void run() { + drawing.drawGraph(graph, palette); + notifyRedrawRequest(); + } + }; + } + } + + if (runnable != null) { + launchThread(runnable, false); + } + else { + drawing.clearOverlays(); + notifyRedrawRequest(); + } + + } + + /** + * @param newClass + */ + private void drawGraph(Class newClass) { + drawGraph(newClass, new BasicGraphPalette()); + } + + /** + * + */ + private void drawGraph() { + drawGraph(null, this.currentPalette); + } + + private void loadGraph(GraphReader reader) { + launchThread(new Runnable() { + @Override + public void run() { + GraphReaderProgressBar progressBar = new GraphReaderProgressBar(MainWindow.this); + progressBar.setLocationRelativeTo(mainPanel.getLeftComponent()); + reader.addObserver(progressBar); + try { + graph = reader.read(); + reader.close(); + } + catch (Exception exception) { + progressBar.setVisible(false); + progressBar.dispose(); + progressBar = null; + JOptionPane.showMessageDialog(MainWindow.this, + "

Unable to read graph from the selected file:

" + + exception.getMessage() + "

"); + exception.printStackTrace(System.out); + return; + } + + // In case of.... + progressBar.setVisible(false); + progressBar.dispose(); + progressBar = null; + + String info = graph.getMapId(); + if (graph.getMapName() != null && !graph.getMapName().isEmpty()) { + // The \u200e character is the left-to-right mark, we need to avoid issue with + // name that are right-to-left (e.g. arabic names). + info += " - " + graph.getMapName() + "\u200e"; + } + info += ", " + graph.size() + " nodes, " + graph.getGraphInformation().getArcCount() + + " arcs."; + graphInfoPanel.setText(info); + + drawGraph(); + + notifyNewGraphLoaded(); + + for (JMenuItem item: graphLockItems) { + item.setEnabled(true); + } + } + }, false); + } + + /** + * Show and enable the given AlgorithmPanel (and hide all others). + * + * @param algorithmPanel + */ + private void enableAlgorithmPanel(AlgorithmPanel algorithmPanel) { + int dividerLocation = mainPanel.getDividerLocation(); + for (AlgorithmPanel panel: algoPanels) { + panel.setVisible(panel == algorithmPanel); + } + mainPanel.setDividerLocation(dividerLocation); + } + + private JMenuBar createMenuBar(ActionListener openMapActionListener) { + + // Open Map item... + JMenuItem openMapItem = new JMenuItem("Open Map... ", KeyEvent.VK_O); + openMapItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.ALT_MASK)); + openMapItem.addActionListener(baf.createBlockingAction(openMapActionListener)); + + // Open Path item... + JMenuItem openPathItem = new JMenuItem("Open Path... ", KeyEvent.VK_P); + openPathItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.ALT_MASK)); + openPathItem.addActionListener(baf.createBlockingAction(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + JFileChooser chooser = FileUtils.createFileChooser(FolderType.PathInput); + if (chooser.showOpenDialog(MainWindow.this) == JFileChooser.APPROVE_OPTION) { + try (BinaryPathReader reader = new BinaryPathReader(new DataInputStream(new BufferedInputStream( + new FileInputStream(chooser.getSelectedFile()))))){ + Path path = reader.readPath(graph); + pathPanel.addPath(path); + } + catch (MapMismatchException exception) { + JOptionPane.showMessageDialog(MainWindow.this, + "The selected file does not contain a path for the current graph."); + } + catch (IOException e1) { + JOptionPane.showMessageDialog(MainWindow.this, + "Cannot open the selected file."); + } + catch (Exception exception) { + JOptionPane.showMessageDialog(MainWindow.this, + "Unable to read path from the selected file."); + } + } + } + })); + graphLockItems.add(openPathItem); + + // Close item + JMenuItem closeItem = new JMenuItem("Quit", KeyEvent.VK_Q); + closeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.ALT_MASK)); + closeItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + MainWindow.this.dispatchEvent( + new WindowEvent(MainWindow.this, WindowEvent.WINDOW_CLOSING)); + } + }); + + // Build the first menu. + JMenu fileMenu = new JMenu("File"); + fileMenu.add(openMapItem); + fileMenu.add(openPathItem); + fileMenu.addSeparator(); + fileMenu.add(closeItem); + + // Second menu + JMenuItem drawGraphItem = new JMenuItem("Redraw", KeyEvent.VK_R); + drawGraphItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, ActionEvent.ALT_MASK)); + drawGraphItem.addActionListener(baf.createBlockingAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + drawGraph(BasicDrawing.class, basicPalette); + } + })); + graphLockItems.add(drawGraphItem); + JMenuItem drawGraphBWItem = new JMenuItem("Redraw (B&W)", KeyEvent.VK_B); + drawGraphBWItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, ActionEvent.ALT_MASK)); + drawGraphBWItem.addActionListener(baf.createBlockingAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + drawGraph(BasicDrawing.class, blackAndWhitePalette); + } + })); + graphLockItems.add(drawGraphBWItem); + JMenuItem drawGraphMapsforgeItem = new JMenuItem("Redraw (Map)", KeyEvent.VK_M); + drawGraphMapsforgeItem + .setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, ActionEvent.ALT_MASK)); + drawGraphMapsforgeItem.addActionListener(baf.createBlockingAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + drawGraph(MapViewDrawing.class); + } + })); + graphLockItems.add(drawGraphMapsforgeItem); + + JMenu graphMenu = new JMenu("Graph"); + graphMenu.add(drawGraphItem); + graphMenu.add(drawGraphBWItem); + graphMenu.addSeparator(); + graphMenu.add(drawGraphMapsforgeItem); + + // Algo menu + JMenu algoMenu = new JMenu("Algorithms"); + + // Weakly connected components + JMenuItem wccItem = new JMenuItem("Weakly Connected Components"); + wccItem.addActionListener(baf.createBlockingAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + enableAlgorithmPanel(wccPanel); + } + })); + + // Shortest path + JMenuItem spItem = new JMenuItem("Shortest-Path"); + spItem.addActionListener(baf.createBlockingAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + enableAlgorithmPanel(spPanel); + } + })); + + // Car pooling + JMenuItem cpItem = new JMenuItem("Car Pooling"); + cpItem.addActionListener(baf.createBlockingAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + enableAlgorithmPanel(cpPanel); + } + })); + + // Car pooling + JMenuItem psItem = new JMenuItem("Package Switch"); + psItem.addActionListener(baf.createBlockingAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + enableAlgorithmPanel(psPanel); + } + })); + + graphLockItems.add(wccItem); + graphLockItems.add(spItem); + graphLockItems.add(cpItem); + graphLockItems.add(psItem); + + algoMenu.add(wccItem); + algoMenu.addSeparator(); + algoMenu.add(spItem); + algoMenu.add(cpItem); + algoMenu.add(psItem); + + // Create the menu bar. + JMenuBar menuBar = new JMenuBar(); + + menuBar.add(fileMenu); + menuBar.add(graphMenu); + menuBar.add(algoMenu); + + for (JMenuItem item: graphLockItems) { + item.setEnabled(false); + } + + return menuBar; + } + + private JPanel createStatusBar() { + // create the status bar panel and shove it down the bottom of the frame + JPanel statusPanel = new JPanel(); + statusPanel.setBorder( + new CompoundBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.GRAY), + new EmptyBorder(0, 15, 0, 15))); + statusPanel.setPreferredSize(new Dimension(getWidth(), 38)); + statusPanel.setLayout(new BorderLayout()); + + graphInfoPanel = new JLabel(); + graphInfoPanel.setHorizontalAlignment(SwingConstants.LEFT); + statusPanel.add(graphInfoPanel, BorderLayout.WEST); + + JLabel threadInfo = new JLabel("Thread running... "); + JLabel threadTimerLabel = new JLabel("00:00:00"); + JButton threadButton = new JButton("Stop"); + threadButton.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + if (currentThread.isRunning()) { + int confirmed = JOptionPane.showConfirmDialog(MainWindow.this, + "Are you sure you want to kill the running thread?", + "Kill Confirmation", JOptionPane.YES_NO_OPTION); + if (confirmed == JOptionPane.YES_OPTION) { + currentThread.interrupt(); + } + } + } + }); + + threadTimer = new Timer(THREAD_TIMER_DELAY, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + long seconds = currentThread.getDuration().getSeconds(); + threadTimerLabel.setText(String.format("%02d:%02d:%02d", seconds / 3600, + seconds / 60 % 60, seconds % 60)); + } + }); + threadTimer.setInitialDelay(0); + + threadPanel = new JPanel(); + threadPanel.add(threadInfo); + threadPanel.add(threadTimerLabel); + threadPanel.add(threadButton); + threadPanel.setVisible(false); + statusPanel.add(threadPanel, BorderLayout.EAST); + + return statusPanel; + } + + public static void main(final String[] args) { + + // Try to set system look and feel. + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception e) { + } + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + MainWindow w = new MainWindow(); + w.setExtendedState(JFrame.MAXIMIZED_BOTH); + w.setVisible(true); + } + }); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/NodesInputPanel.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/NodesInputPanel.java new file mode 100644 index 0000000..0c41fe7 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/NodesInputPanel.java @@ -0,0 +1,424 @@ +package org.insa.graphs.gui; + +import java.awt.Color; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.Drawing.AlphaMode; +import org.insa.graphs.gui.drawing.DrawingClickListener; +import org.insa.graphs.gui.drawing.overlays.MarkerOverlay; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.Point; + +public class NodesInputPanel extends JPanel + implements DrawingClickListener, DrawingChangeListener, GraphChangeListener { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private static final Color DEFAULT_MARKER_COLOR = Color.BLUE; + + /** + * Utility class that can be used to find a node from coordinates in a "fast" + * way. + * + */ + private static class NodeFinder { + + // Graph associated with this node finder. + private Graph graph; + + /** + * @param graph + */ + public NodeFinder(Graph graph) { + this.graph = graph; + } + + /** + * @param point + * + * @return the closest node to the given point, or null if no node is "close + * enough". + */ + public Node findClosestNode(Point point) { + Node minNode = null; + double minDis = Double.POSITIVE_INFINITY; + for (Node node: graph.getNodes()) { + double dlon = point.getLongitude() - node.getPoint().getLongitude(); + double dlat = point.getLatitude() - node.getPoint().getLatitude(); + double dis = dlon * dlon + dlat * dlat; // No need to square + if (dis < minDis) { + minNode = node; + minDis = dis; + } + } + return minNode; + } + + } + + /** + * Event data send when a node input has changed. + * + */ + public class InputChangedEvent extends ActionEvent { + + /** + * + */ + private static final long serialVersionUID = 3440024811352247731L; + + protected static final String ALL_INPUT_FILLED_EVENT_COMMAND = "allInputFilled"; + + protected static final int ALL_INPUT_FILLED_EVENT_ID = 0x1; + + // List of nodes + List nodes; + + public InputChangedEvent(List nodes2) { + super(NodesInputPanel.this, ALL_INPUT_FILLED_EVENT_ID, ALL_INPUT_FILLED_EVENT_COMMAND); + this.nodes = nodes2; + } + + List getNodes() { + return Collections.unmodifiableList(nodes); + } + + }; + + // Node inputs and markers. + private final ArrayList nodeInputs = new ArrayList<>(); + private final Map markerTrackers = new IdentityHashMap(); + + // Component that can be enabled/disabled. + private ArrayList components = new ArrayList<>(); + private int inputToFillIndex; + + // ActionListener called when all inputs are filled. + private ArrayList inputChangeListeners = new ArrayList<>(); + + // Drawing and graph + private Drawing drawing; + private Graph graph; + private NodeFinder nodeFinder; + + /** + * Create a new NodesInputPanel. + * + */ + public NodesInputPanel() { + super(new GridBagLayout()); + initInputToFill(); + } + + /** + * Add an InputChanged listener to this panel. This listener will be notified by + * a {@link InputChangedEvent} each time an input in this panel change (click, + * clear, manual input). + * + * @param listener Listener to add. + * + * @see InputChangedEvent + */ + public void addInputChangedListener(ActionListener listener) { + inputChangeListeners.add(listener); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + for (JTextField input: nodeInputs) { + MarkerOverlay marker = markerTrackers.getOrDefault(input, null); + if (marker != null) { + marker.setVisible(visible && !input.getText().trim().isEmpty()); + } + } + } + + @Override + public void setEnabled(boolean enabled) { + for (JComponent component: components) { + component.setEnabled(enabled); + } + super.setEnabled(enabled); + if (enabled) { + // Enable: Check if there is an input to fill, otherwize find the next one. + if (getInputToFill() == null) { + nextInputToFill(); + } + } + else { + // Disable, next input to fill = -1. + this.inputToFillIndex = -1; + } + } + + public void clear() { + for (JTextField field: nodeInputs) { + field.setText(""); + markerTrackers.put(field, null); + } + initInputToFill(); + } + + public void addTextField(String label) { + addTextField(label, DEFAULT_MARKER_COLOR); + } + + public void addTextField(String label, Color markerColor) { + + GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets(3, 3, 3, 3); + + JLabel jLabel = new JLabel(label); + jLabel.setFont(jLabel.getFont().deriveFont(~Font.BOLD)); + JTextField textField = new JTextField(); + jLabel.setLabelFor(textField); + + c.gridx = 0; + c.gridy = nodeInputs.size(); + c.weightx = 0; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(jLabel, c); + + c.gridx = 1; + c.gridy = nodeInputs.size(); + c.weightx = 1; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(textField, c); + + JButton clearButton = new JButton("Clear"); + c.gridx = 2; + c.gridy = nodeInputs.size(); + c.weightx = 0; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(clearButton, c); + + JButton clickButton = new JButton("Click"); + c.gridx = 3; + c.gridy = nodeInputs.size(); + c.weightx = 0; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(clickButton, c); + + // Did not find something easier that this... ? + textField.getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void changedUpdate(DocumentEvent e) { + insertUpdate(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + insertUpdate(e); + } + + @Override + public void insertUpdate(DocumentEvent e) { + + // Draw marker if possible + Node curnode = getNodeForInput(textField); + MarkerOverlay tracker = markerTrackers.getOrDefault(textField, null); + if (curnode != null) { + if (tracker == null) { + tracker = drawing.drawMarker(curnode.getPoint(), markerColor, Color.BLACK, + AlphaMode.TRANSPARENT); + markerTrackers.put(textField, tracker); + } + else { + tracker.moveTo(curnode.getPoint()); + } + tracker.setVisible(true); + } + else if (tracker != null) { + tracker.setVisible(false); + if (getInputToFill() == null) { + nextInputToFill(); + } + } + + // Create array of nodes + List nodes = getNodeForInputs(); + + // Trigger change event. + for (ActionListener lis: inputChangeListeners) { + lis.actionPerformed(new InputChangedEvent(nodes)); + } + } + }); + + clearButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + textField.setText(""); + setInputToFill(textField); + } + }); + + clickButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setInputToFill(textField); + } + }); + + nodeInputs.add(textField); + components.add(textField); + components.add(clearButton); + components.add(clickButton); + } + + /** + * @return Current graph associated with this input panel. + */ + protected Graph getGraph() { + return this.graph; + } + + /** + * @return The node for the given text field, or null if the content of the text + * field is invalid. + */ + protected Node getNodeForInput(JTextField textfield) { + try { + Node node = graph.get(Integer.valueOf(textfield.getText().trim())); + return node; + } + catch (IllegalArgumentException | IndexOutOfBoundsException ex) { + return null; + } + } + + /** + * @return List of nodes associated with the input. Some nodes may be null if + * their associated input is invalid. + */ + public List getNodeForInputs() { + List nodes = new ArrayList<>(nodeInputs.size()); + for (JTextField input: nodeInputs) { + nodes.add(getNodeForInput(input)); + } + return nodes; + } + + /** + * Get the next input that should be filled by a click, or null if none should + * be filled. + * + * @return + */ + protected JTextField getInputToFill() { + if (inputToFillIndex < 0 || inputToFillIndex >= nodeInputs.size()) { + return null; + } + return nodeInputs.get(inputToFillIndex); + } + + /** + * Initialize the next input to fill. + */ + protected void initInputToFill() { + inputToFillIndex = 0; + } + + /** + * Set the next input to fill to the given text field. + * + * @param input + */ + protected void setInputToFill(JTextField input) { + inputToFillIndex = nodeInputs.indexOf(input); + } + + /** + * Find the next input to fill, if any. + */ + protected void nextInputToFill() { + boolean found = false; + if (inputToFillIndex == -1) { + inputToFillIndex = 0; + } + for (int i = 0; i < nodeInputs.size() && !found; ++i) { + int nextIndex = (i + inputToFillIndex) % nodeInputs.size(); + JTextField input = nodeInputs.get(nextIndex); + if (input.getText().trim().isEmpty()) { + inputToFillIndex = nextIndex; + found = true; + } + } + if (!found) { + inputToFillIndex = -1; + } + } + + @Override + public void mouseClicked(Point point) { + JTextField input = getInputToFill(); + if (input != null) { + Node node = nodeFinder.findClosestNode(point); + input.setText(String.valueOf(node.getId())); + nextInputToFill(); + } + } + + @Override + public void newGraphLoaded(Graph graph) { + if (graph != this.graph) { + this.clear(); + this.graph = graph; + + nodeFinder = new NodeFinder(graph); + + // Disable if previously disabled... + setEnabled(this.isEnabled()); + } + } + + @Override + public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) { + if (newDrawing != drawing) { + this.drawing = newDrawing; + } + } + + @Override + public void onRedrawRequest() { + for (JTextField input: nodeInputs) { + MarkerOverlay tracker = markerTrackers.getOrDefault(input, null); + if (tracker != null) { + MarkerOverlay newMarker = this.drawing.drawMarker(tracker.getPoint(), + tracker.getColor(), Color.BLACK, AlphaMode.TRANSPARENT); + markerTrackers.put(input, newMarker); + newMarker.setVisible(tracker.isVisible()); + tracker.delete(); + } + } + } +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/PathsPanel.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/PathsPanel.java new file mode 100644 index 0000000..29ea418 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/PathsPanel.java @@ -0,0 +1,361 @@ +package org.insa.graphs.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.overlays.PathOverlay; +import org.insa.graphs.gui.utils.ColorUtils; +import org.insa.graphs.gui.utils.FileUtils; +import org.insa.graphs.gui.utils.FileUtils.FolderType; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Path; +import org.insa.graphs.model.io.BinaryPathWriter; + +public class PathsPanel extends JPanel implements DrawingChangeListener, GraphChangeListener { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private class PathPanel extends JPanel { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Simple icon that represents a unicolor rectangle. + * + */ + protected class ColorIcon implements Icon { + + private Color color; + private int width, height; + + public ColorIcon(Color color, int width, int height) { + this.width = width; + this.height = height; + this.color = color; + } + + public void setColor(Color color) { + this.color = color; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + g.setColor(this.color); + g.fillRect(x, y, getIconWidth(), getIconHeight()); + } + + @Override + public int getIconWidth() { + return this.width; + } + + @Override + public int getIconHeight() { + return this.height; + } + } + + // Solution + private final Path path; + + // Path Overlay (not final due to redraw) + private PathOverlay overlay; + + // Color button + private final JButton colorButton; + + /** + * Create a new bundle with the given path and create a new overlay + * corresponding to the path. + * + * @param path Path for this bundle, must not be null. + * + * @throws IOException If a resource was not found. + * + */ + public PathPanel(Path path, Color color) throws IOException { + super(); + setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); + setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, Color.LIGHT_GRAY), + new EmptyBorder(5, 0, 5, 0))); + this.path = path; + this.overlay = drawing.drawPath(this.path, color); + + JCheckBox checkbox = new JCheckBox(); + checkbox.setSelected(true); + checkbox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + overlay.setVisible(checkbox.isSelected()); + } + }); + + JLabel infoPanel = new JLabel(); + String info = ""; + + // Display length + float length = path.getLength(); + if (length < 2000) { + info += String.format("Length = %.1f meters", length); + } + else { + info += String.format("Length = %.3f kilometers", length / 1000.); + } + + // Display time + info += ", Duration="; + double time = path.getMinimumTravelTime(); + int hours = (int) (time / 3600); + int minutes = (int) (time / 60) % 60; + int seconds = ((int) time) % 60; + if (hours > 0) { + info += String.format("%d hours, ", hours); + } + if (minutes > 0) { + info += String.format("%d minutes, ", minutes); + } + info += String.format("%d seconds.", seconds); + infoPanel.setText("" + toString() + "
" + info + ""); + infoPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + checkbox.setSelected(!checkbox.isSelected()); + overlay.setVisible(checkbox.isSelected()); + } + }); + + Dimension size = new Dimension(24, 24); + + ColorIcon icon = new ColorIcon(overlay.getColor(), 14, 14); + colorButton = new JButton(icon); + colorButton.setFocusable(false); + colorButton.setFocusPainted(false); + colorButton.setMinimumSize(size); + colorButton.setPreferredSize(size); + colorButton.setMaximumSize(size); + + colorButton.setToolTipText("Pick a color"); + + colorButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final Color originalColor = overlay.getColor(); + JColorChooser chooser = new JColorChooser(overlay.getColor()); + chooser.getSelectionModel().addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + icon.setColor(chooser.getSelectionModel().getSelectedColor()); + colorButton.repaint(); + overlay.setColor(chooser.getSelectionModel().getSelectedColor()); + overlay.redraw(); + } + }); + + JColorChooser.createDialog(getTopLevelAncestor(), "Pick a new color", true, + chooser, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + icon.setColor(chooser.getSelectionModel().getSelectedColor()); + colorButton.repaint(); + overlay.setColor( + chooser.getSelectionModel().getSelectedColor()); + overlay.redraw(); + } + }, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + icon.setColor(originalColor); + colorButton.repaint(); + overlay.setColor(originalColor); + overlay.redraw(); + } + }).setVisible(true); + ; + + } + }); + + Image saveImg = ImageIO.read(getClass().getResourceAsStream("/save-icon.png")) + .getScaledInstance(14, 14, java.awt.Image.SCALE_SMOOTH); + JButton saveButton = new JButton(new ImageIcon(saveImg)); + saveButton.setFocusPainted(false); + saveButton.setFocusable(false); + saveButton.setMinimumSize(size); + saveButton.setPreferredSize(size); + saveButton.setMaximumSize(size); + saveButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String filepath = String.format("path_%s_%d_%d.path", + path.getGraph().getMapId().toLowerCase().replaceAll("[^a-z0-9_]", ""), + path.getOrigin().getId(), path.getDestination().getId()); + JFileChooser chooser = FileUtils.createFileChooser(FolderType.PathOutput, + filepath); + + if (chooser + .showSaveDialog(getTopLevelAncestor()) == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + try (BinaryPathWriter writer = new BinaryPathWriter(new DataOutputStream( + new BufferedOutputStream(new FileOutputStream(file))))) { + writer.writePath(path); + } + catch (IOException e1) { + JOptionPane.showMessageDialog(getTopLevelAncestor(), + "Unable to write path to the selected file."); + e1.printStackTrace(); + } + } + } + }); + + Image newimg = ImageIO.read(getClass().getResourceAsStream("/delete-icon.png")) + .getScaledInstance(14, 14, java.awt.Image.SCALE_SMOOTH); + JButton deleteButton = new JButton(new ImageIcon(newimg)); + deleteButton.setFocusPainted(false); + deleteButton.setFocusable(false); + deleteButton.setMinimumSize(size); + deleteButton.setPreferredSize(size); + deleteButton.setMaximumSize(size); + deleteButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + overlay.delete(); + PathsPanel.this.removePath(PathPanel.this); + } + }); + + add(checkbox); + add(Box.createHorizontalStrut(5)); + add(infoPanel); + add(Box.createHorizontalGlue()); + add(colorButton); + add(saveButton); + add(deleteButton); + + } + + /** + * Re-draw the current overlay (if any) on the new drawing. + * + */ + public void updateOverlay() { + PathOverlay oldOverlay = this.overlay; + this.overlay = drawing.drawPath(path); + this.overlay.setColor(oldOverlay.getColor()); + ((ColorIcon) this.colorButton.getIcon()).setColor(this.overlay.getColor()); + this.colorButton.repaint(); + this.overlay.setVisible(oldOverlay.isVisible()); + oldOverlay.delete(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + public String toString() { + return "Path from #" + path.getOrigin().getId() + " to #" + + path.getDestination().getId(); + } + + } + + // Solution + private Drawing drawing; + + public PathsPanel(Component parent) { + super(); + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + setBorder(new EmptyBorder(15, 15, 15, 15)); + + // Default hidden + this.setVisible(false); + + } + + public void addPath(Path path) { + try { + this.add(new PathPanel(path, ColorUtils.getColor(this.getComponentCount()))); + this.setVisible(true); + this.revalidate(); + this.repaint(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + protected void removePath(PathPanel panel) { + PathsPanel.this.remove(panel); + PathsPanel.this.revalidate(); + PathsPanel.this.repaint(); + if (this.getComponentCount() == 0) { + this.setVisible(false); + } + } + + @Override + public void newGraphLoaded(Graph graph) { + for (Component c: this.getComponents()) { + if (c instanceof PathPanel) { + ((PathPanel) c).overlay.delete(); + } + } + this.removeAll(); + this.setVisible(false); + } + + @Override + public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) { + if (newDrawing != drawing) { + drawing = newDrawing; + } + } + + @Override + public void onRedrawRequest() { + for (Component c: this.getComponents()) { + if (c instanceof PathPanel) { + ((PathPanel) c).updateOverlay(); + } + } + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/RunningAction.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/RunningAction.java new file mode 100644 index 0000000..8cca834 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/RunningAction.java @@ -0,0 +1,33 @@ +package org.insa.graphs.gui; + +import java.time.Duration; +import java.time.Instant; + +public interface RunningAction { + + /** + * @return true if this action is running. + */ + public boolean isRunning(); + + /** + * Interrupt this action. + */ + public void interrupt(); + + /** + * @return Starting time of this action. + */ + public Instant getStartingTime(); + + /** + * @return Current duration of this action. + */ + public Duration getDuration(); + + /** + * @return Information for this action. + */ + public String getInformation(); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/SolutionPanel.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/SolutionPanel.java new file mode 100644 index 0000000..e96bcef --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/SolutionPanel.java @@ -0,0 +1,289 @@ +package org.insa.graphs.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; + +import org.insa.graphs.algorithm.AbstractInputData; +import org.insa.graphs.algorithm.AbstractSolution; +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.overlays.PathOverlay; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Path; + +public class SolutionPanel extends JPanel implements DrawingChangeListener, GraphChangeListener { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private class SolutionBundle { + + // Solution + private final AbstractSolution solution; + + // Path Overlay (not final due to redraw) + private List overlays = new ArrayList<>(); + + /** + * Create a new bundle with the given solution and create a new overlay + * corresponding to the solution (if the solution is feasible). + * + * @param solution Solution for this bundle, must not be null. + * + */ + public SolutionBundle(AbstractSolution solution, boolean createOverlays) { + this.solution = solution; + if (createOverlays) { + this.overlays = createOverlaysFromSolution(); + } + } + + /** + * @return Solution associated with this bundle. + */ + public AbstractSolution getSolution() { + return this.solution; + } + + /** + * @return Data assocaited with this bundle. + */ + public AbstractInputData getData() { + return this.solution.getInputData(); + } + + /** + * @return Overlays associated with this bundle, or null. + */ + public List getOverlays() { + return this.overlays; + } + + /** + * @return true if this bundle has overlays. + */ + public boolean hasOverlays() { + return !this.overlays.isEmpty(); + } + + /** + * Re-draw the current overlay (if any) on the new drawing. + * + */ + public void updateOverlays() { + if (this.overlays.isEmpty()) { + return; // This bundle has no overlay. + } + List oldOverlays = this.overlays; + this.overlays = createOverlaysFromSolution(); + for (int i = 0; i < oldOverlays.size(); ++i) { + oldOverlays.get(i).delete(); + } + } + + private List createOverlaysFromSolution() { + List overlays = new ArrayList<>(); + if (solution.isFeasible()) { + Method[] methods = this.solution.getClass().getDeclaredMethods(); + for (Method method: methods) { + if (method.getReturnType().equals(Path.class) + && method.getParameterCount() == 0) { + try { + Path path = (Path) method.invoke(this.solution); + overlays.add(drawing.drawPath(path)); + } + catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + // This has been check before, so should never happen... + e.printStackTrace(); + } + } + } + } + return overlays; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + public String toString() { + return getData().toString(); + } + + } + + // Solution + private Drawing drawing; + + // Solution selector + private final JComboBox solutionSelect; + + private final JLabel informationPanel; + + // Current bundle + private SolutionBundle currentBundle = null; + + public SolutionPanel(Component parent) { + super(); + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + setBorder(new CompoundBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, Color.LIGHT_GRAY), + new EmptyBorder(10, 0, 10, 0))); + + solutionSelect = new JComboBox<>(); + solutionSelect.setBackground(Color.WHITE); + solutionSelect.setAlignmentX(Component.LEFT_ALIGNMENT); + add(solutionSelect); + + informationPanel = new JLabel(); + informationPanel.setOpaque(true); + informationPanel.setFocusable(false); + // informationPanel.setAlignmentX(JLabel.LEFT_ALIGNMENT); + informationPanel.setHorizontalAlignment(JLabel.LEFT); + + add(Box.createVerticalStrut(8)); + add(informationPanel); + + JButton clearButton = new JButton("Hide"); + clearButton.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + for (PathOverlay overlay: currentBundle.getOverlays()) { + if (overlay.isVisible()) { + overlay.setVisible(false); + clearButton.setText("Show"); + } + else { + overlay.setVisible(true); + clearButton.setText("Hide"); + } + } + } + }); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS)); + buttonPanel.add(Box.createHorizontalGlue()); + buttonPanel.add(clearButton); + buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + add(Box.createVerticalStrut(4)); + add(buttonPanel); + + solutionSelect.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + + if (currentBundle != null) { + for (PathOverlay overlay: currentBundle.getOverlays()) { + overlay.setVisible(false); + } + } + + SolutionBundle bundle = (SolutionBundle) solutionSelect.getSelectedItem(); + + if (bundle != null) { + + updateInformationLabel(bundle); + buttonPanel + .setVisible(bundle.getSolution().isFeasible() && bundle.hasOverlays()); + clearButton.setText(bundle.getSolution().isFeasible() ? "Hide" : "Show"); + + for (PathOverlay overlay: bundle.getOverlays()) { + overlay.setVisible(true); + } + } + + currentBundle = bundle; + } + }); + + } + + public void addSolution(AbstractSolution solution) { + addSolution(solution, true); + } + + /** + * Add the given solution to the panel. + * + * @param solution the solution to add to the panel + * @param createOverlays Whether or not overlay should be created for this + * solution. + */ + public void addSolution(AbstractSolution solution, boolean createOverlays) { + SolutionBundle bundle = new SolutionBundle(solution, createOverlays); + solutionSelect.addItem(bundle); + solutionSelect.setSelectedItem(bundle); + } + + protected void updateInformationLabel(SolutionBundle bundle) { + informationPanel.setText(bundle.getSolution().toString()); + revalidate(); + repaint(); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + solutionSelect.setEnabled(enabled); + + if (enabled) { + // Trigger event + solutionSelect.setSelectedItem(currentBundle); + } + else { + SolutionBundle bundle = (SolutionBundle) this.solutionSelect.getSelectedItem(); + if (bundle != null) { + for (PathOverlay overlay: bundle.getOverlays()) { + overlay.setVisible(false); + } + } + } + } + + @Override + public void newGraphLoaded(Graph graph) { + for (int i = 0; i < this.solutionSelect.getItemCount(); ++i) { + for (PathOverlay overlay: this.solutionSelect.getItemAt(i).getOverlays()) { + overlay.delete(); + } + } + this.solutionSelect.removeAllItems(); + this.currentBundle = null; + this.setVisible(false); + } + + @Override + public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) { + if (newDrawing != drawing) { + drawing = newDrawing; + } + } + + @Override + public void onRedrawRequest() { + for (int i = 0; i < this.solutionSelect.getItemCount(); ++i) { + this.solutionSelect.getItemAt(i).updateOverlays(); + } + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/StreamCapturer.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/StreamCapturer.java new file mode 100644 index 0000000..0a7d010 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/StreamCapturer.java @@ -0,0 +1,54 @@ +package org.insa.graphs.gui; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.swing.JTextArea; + +public class StreamCapturer extends OutputStream { + + private StringBuilder buffer; + private String prefix = null; + private JTextArea output; + + /** + * @param output Output JTextArea to which this stream should print. + * @param prefix Prefix to add to each line of this output. + */ + public StreamCapturer(JTextArea output, String prefix) { + this.prefix = prefix; + buffer = new StringBuilder(128); + this.output = output; + } + + /** + * Create a new StreamCapturer without prefix. + * + * @param output Output JTextArea to which this stream should print. + */ + public StreamCapturer(JTextArea output) { + this(output, null); + } + + @Override + public void write(int b) throws IOException { + char c = (char) b; + String value = Character.toString(c); + buffer.append(value); + if (value.equals("\n")) { + output.append(getPrefix() + buffer.toString()); + output.setCaretPosition(output.getText().length()); + buffer.delete(0, buffer.length()); + } + } + + /** + * @return Formatted prefix, or empty string if no prefix is set. + */ + public String getPrefix() { + if (this.prefix == null) { + return ""; + } + return "[" + prefix + "] "; + } +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/ThreadWrapper.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/ThreadWrapper.java new file mode 100644 index 0000000..e5e9cfc --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/ThreadWrapper.java @@ -0,0 +1,62 @@ +package org.insa.graphs.gui; + +import java.time.Duration; +import java.time.Instant; + +public class ThreadWrapper implements RunningAction { + + // Thread hold by this wrapper. + private Thread thread; + + // Starting time of the thread. + Instant startingTime; + + // MainWindow + private MainWindow mainWindow; + + public ThreadWrapper(MainWindow mainWindow) { + this.thread = null; + this.mainWindow = mainWindow; + } + + public void setThread(Thread thread) { + this.thread = thread; + } + + public void startThread() { + this.startingTime = Instant.now(); + this.thread.start(); + } + + public Thread getThread() { + return this.thread; + } + + @Override + public boolean isRunning() { + return thread != null && thread.isAlive(); + } + + @SuppressWarnings("deprecation") + @Override + public void interrupt() { + thread.stop(); + this.mainWindow.clearCurrentThread(); + } + + @Override + public Instant getStartingTime() { + return startingTime; + } + + @Override + public Duration getDuration() { + return Duration.between(getStartingTime(), Instant.now()); + } + + @Override + public String getInformation() { + return getClass().getName(); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BasicGraphPalette.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BasicGraphPalette.java new file mode 100644 index 0000000..7c7d26e --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BasicGraphPalette.java @@ -0,0 +1,82 @@ +package org.insa.graphs.gui.drawing; + +import java.awt.Color; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.RoadInformation.RoadType; + +public class BasicGraphPalette implements GraphPalette { + + // Color types for arc. + private static final Color MOTORWAY_COLOR = Color.RED; + private static final Color BIG_ROAD_COLOR = new Color(255, 105, 0); + private static final Color SMALL_ROAD_COLOR = new Color(255, 200, 0); + private static final Color COASTLINE_COLOR = Color.BLUE; + + @Override + public Color getColorForArc(Arc arc) { + RoadType type = arc.getRoadInformation().getType(); + switch (type) { + case MOTORWAY: + return MOTORWAY_COLOR; + case TRUNK: + case PRIMARY: + case SECONDARY: + case MOTORWAY_LINK: + case TRUNK_LINK: + case PRIMARY_LINK: + return BIG_ROAD_COLOR; + case SECONDARY_LINK: + case TERTIARY: + case RESIDENTIAL: + case UNCLASSIFIED: + case LIVING_STREET: + case SERVICE: + case ROUNDABOUT: + case PEDESTRIAN: + case CYCLEWAY: + case TRACK: + return SMALL_ROAD_COLOR; + case COASTLINE: + return COASTLINE_COLOR; + } + + return Color.BLACK; + } + + @Override + public int getWidthForArc(Arc arc) { + RoadType type = arc.getRoadInformation().getType(); + int width = 1; + switch (type) { + case MOTORWAY: + width = 2; + break; + case TRUNK: + case PRIMARY: + case SECONDARY: + case MOTORWAY_LINK: + case TRUNK_LINK: + case PRIMARY_LINK: + width = 1; + break; + case SECONDARY_LINK: + case TERTIARY: + case RESIDENTIAL: + case UNCLASSIFIED: + case LIVING_STREET: + case SERVICE: + case ROUNDABOUT: + case PEDESTRIAN: + case CYCLEWAY: + case TRACK: + width = 1; + break; + case COASTLINE: + width = 4; + break; + } + return width; + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BlackAndWhiteGraphPalette.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BlackAndWhiteGraphPalette.java new file mode 100644 index 0000000..3141227 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/BlackAndWhiteGraphPalette.java @@ -0,0 +1,19 @@ +package org.insa.graphs.gui.drawing; + +import java.awt.Color; + +import org.insa.graphs.model.Arc; + +public class BlackAndWhiteGraphPalette extends BasicGraphPalette { + + // Road colors (index + private final static Color[] ROAD_COLOR_FROM_WIDTH = { null, new Color(140, 140, 140), + new Color(80, 80, 80), new Color(40, 40, 40), new Color(30, 30, 30) }; + + @Override + public Color getColorForArc(Arc arc) { + int width = getWidthForArc(arc); + return ROAD_COLOR_FROM_WIDTH[width]; + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Drawing.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Drawing.java new file mode 100644 index 0000000..ee2a018 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Drawing.java @@ -0,0 +1,157 @@ +package org.insa.graphs.gui.drawing; + +import java.awt.Color; + +import org.insa.graphs.gui.drawing.overlays.MarkerOverlay; +import org.insa.graphs.gui.drawing.overlays.PathOverlay; +import org.insa.graphs.gui.drawing.overlays.PointSetOverlay; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Path; +import org.insa.graphs.model.Point; + +public interface Drawing { + + /** + * Available fill mode for the creation of markers, see the documentation of + * each value for more details. + */ + enum AlphaMode { + + /** + * Do not use the original transparency of the inner part to fill it. + */ + OPAQUE, + + /** + * Use the original transparency of the inner part to fill it. + */ + TRANSPARENT + } + + /** + * Add a listener to click to this drawing. + * + * @param listener DrawingClickListener to add to this Drawing. + */ + public void addDrawingClickListener(DrawingClickListener listener); + + /** + * Remove the given listener from the drawing. + * + * @param listener DrawingClickListener to remove from this Drawing. + */ + public void removeDrawingClickListener(DrawingClickListener listener); + + /** + * Clear the drawing (overlays and underlying graph/map). + */ + public void clear(); + + /** + * Remove overlays from the drawing (do not remove the underlying graph/map). + */ + public void clearOverlays(); + + /** + * Draw a marker at the given position using the given colors and according to + * the given mode. + * + * @param point Position of the marker to draw. + * @param outer Color for the outer part of the marker to draw. + * @param inner Color for the inner part of the marker to draw. + * @param mode Mode for filling the inner par of the marker. + * + * @return A MarkerOverlay instance representing the newly drawn marker. + */ + public MarkerOverlay drawMarker(Point point, Color outer, Color inner, AlphaMode mode); + + /** + * Create a new PointSetOverlay that can be used to add overlay points to this + * drawing. + * + * PointSetOverlay are heavy memory resources, do not use one for each point! + * + * @return A new PointSetOverlay for this drawing. + */ + public PointSetOverlay createPointSetOverlay(); + + /** + * Create a new PointSetOverlay with the given initial width and color that can + * be used to add overlay points to this drawing. + * + * PointSetOverlay are heavy memory resources, do not use one for each point! + * + * @param width Initial width of points in the overlay. + * @param color Initial width of points in the overlay. + * + * @return A new PointSetOverlay for this drawing. + */ + public PointSetOverlay createPointSetOverlay(int width, Color color); + + /** + * Draw the given graph using the given palette. + * + * @param graph Graph to draw. + * @param palette Palette to use to draw the graph. + * + * @see BasicGraphPalette + * @see BlackAndWhiteGraphPalette + */ + public void drawGraph(Graph graph, GraphPalette palette); + + /** + * Draw the given graph using a default palette specific to the implementation. + * + * @param graph Graph to draw. + */ + public void drawGraph(Graph graph); + + /** + * Draw a path using the given color. + * + * @param path Path to draw. + * @param color Color of the path to draw. + * @param markers true to show origin and destination markers. + * + * @return A PathOverlay instance representing the newly drawn path. + */ + public PathOverlay drawPath(Path path, Color color, boolean markers); + + /** + * Draw a path with both origin and destination markers using the given color. + * + * @param path Path to draw. + * @param color Color of the path to draw. + * + * @return A PathOverlay instance representing the newly drawn path. + * + * @see Drawing#drawPath(Path, Color, boolean) + */ + public PathOverlay drawPath(Path path, Color color); + + /** + * Draw a path using a default color specific to the implementation + * + * @param path Path to draw. + * @param markers true to show origin and destination markers. + * + * @return A PathOverlay instance representing the newly drawn path. + * + * @see Drawing#drawPath(Path, Color, boolean) + */ + public PathOverlay drawPath(Path path, boolean markers); + + /** + * Draw a path with both origin and destination markers using a default color + * specific to the implementation + * + * + * @param path Path to draw. + * + * @return A PathOverlay instance representing the newly drawn path. + * + * @see Drawing#drawPath(Path, Color, boolean) + */ + public PathOverlay drawPath(Path path); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/DrawingClickListener.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/DrawingClickListener.java new file mode 100644 index 0000000..fddfd9a --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/DrawingClickListener.java @@ -0,0 +1,14 @@ +package org.insa.graphs.gui.drawing; + +import org.insa.graphs.model.Point; + +public interface DrawingClickListener { + + /** + * Event triggered when a click is made on the map. + * + * @param point Position (on the map) of the mouse click. + */ + public void mouseClicked(Point point); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/GraphPalette.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/GraphPalette.java new file mode 100644 index 0000000..00af58d --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/GraphPalette.java @@ -0,0 +1,23 @@ +package org.insa.graphs.gui.drawing; + +import java.awt.Color; + +import org.insa.graphs.model.Arc; + +public interface GraphPalette { + + /** + * @param arc Arc for which color should be retrieved. + * + * @return Color associated with the given arc. + */ + public Color getColorForArc(Arc arc); + + /** + * @param arc Arc for which width should be retrieved. + * + * @return Width associated with the given arc. + */ + public int getWidthForArc(Arc arc); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/MercatorProjection.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/MercatorProjection.java new file mode 100644 index 0000000..374c20d --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/MercatorProjection.java @@ -0,0 +1,116 @@ +package org.insa.graphs.gui.drawing; + +import java.awt.Dimension; + +import org.insa.graphs.model.GraphStatistics.BoundingBox; + +public class MercatorProjection implements Projection { + + public static final double MAX_LATITUDE = 82; + + public static final double MIN_LATITUDE = -MAX_LATITUDE; + + // From Wikipedia... for the above max/min latitude. + private static final double IMAGE_WIDTH = 2058, IMAGE_HEIGHT = 1746; + + private static final double MAX_LATITUDE_PROJ = projectY(MAX_LATITUDE); + private static final double MIN_LATITUDE_PROJ = projectY(MIN_LATITUDE); + + // Bounding box + private final float minLatitude, minLongitude, maxLatitude, maxLongitude; + + // Projection of min and max latitude. + private final double minLatitudeProj, maxLatitudeProj; + + // Dimension of the image + private final double width, height; + + /** + * Create a new MercatorProjection corresponding to the given BoundingBox and + * maxSize. + * + * @param boundingBox Box for this projection. + * @param maxSize Maximum size of any side (width / height) of the image to + * which this projection should draw. + */ + public MercatorProjection(BoundingBox boundingBox, int maxSize) { + // Find minimum/maximum longitude and latitude. + this.minLongitude = boundingBox.getTopLeftPoint().getLongitude(); + this.maxLongitude = boundingBox.getBottomRightPoint().getLongitude(); + this.minLatitude = boundingBox.getBottomRightPoint().getLatitude(); + this.maxLatitude = boundingBox.getTopLeftPoint().getLatitude(); + + // Compute projection + this.minLatitudeProj = projectY(this.minLatitude); + this.maxLatitudeProj = projectY(this.maxLatitude); + + Dimension imageDimension = computeImageSize(maxSize); + this.width = imageDimension.getWidth(); + this.height = imageDimension.getHeight(); + } + + /** + * Compute the projection (without scaling) of the given latitude. + * + * @param latitude Latitude to project. + * + * @return Projection of the given latitude (without scaling). + */ + private static double projectY(double latitude) { + double sinLatitude = Math.sin(latitude * Math.PI / 180.0); + return Math.log((1 + sinLatitude) / (1 - sinLatitude)) / 2; + } + + /** + * Compute the dimension required for drawing a projection of the given box on + * an image, ensuring that none of the side of image is greater than maxSize. + * + * @param maxSize Maximum side of any side of the image. + * + * @return Dimension corresponding to the preferred size for the image. + */ + protected Dimension computeImageSize(int maxSize) { + double propWidth = (maxLongitude - minLongitude) * IMAGE_WIDTH / 360.0; + double propHeight = (this.maxLatitudeProj - this.minLatitudeProj) + / (MAX_LATITUDE_PROJ - MIN_LATITUDE_PROJ) * IMAGE_HEIGHT; + + return propWidth < propHeight + ? new Dimension((int) (maxSize * propWidth / propHeight), maxSize) + : new Dimension(maxSize, (int) (maxSize * propHeight / propWidth)); + } + + @Override + public double getImageWidth() { + return this.width; + } + + @Override + public double getImageHeight() { + return this.height; + } + + @Override + public int latitudeToPixelY(float latitude) { + return (int) ((this.maxLatitudeProj - projectY(latitude)) + / (this.maxLatitudeProj - this.minLatitudeProj) * this.height); + } + + @Override + public int longitudeToPixelX(float longitude) { + return (int) (width * (longitude - minLongitude) / (maxLongitude - minLongitude)); + } + + @Override + public float pixelYToLatitude(double py) { + float y = (float) (this.maxLatitudeProj + - (py / this.height) * (this.maxLatitudeProj - this.minLatitudeProj)); + return (float) (180 * (2 * Math.atan(Math.exp(y)) - Math.PI / 2) / Math.PI); + } + + @Override + public float pixelXToLongitude(double px) { + return (float) ((px / this.width) * (this.maxLongitude - this.minLongitude) + + this.minLongitude); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/PlateCarreProjection.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/PlateCarreProjection.java new file mode 100644 index 0000000..0b15490 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/PlateCarreProjection.java @@ -0,0 +1,68 @@ +package org.insa.graphs.gui.drawing; + +import org.insa.graphs.model.GraphStatistics.BoundingBox; + +public class PlateCarreProjection implements Projection { + + // Bounding box + private final float minLatitude, minLongitude, maxLatitude, maxLongitude; + + // Dimension of the image + private final double width, height; + + /** + * Create a new PlateCarreProjection corresponding to the given BoundingBox and + * maxSize. + * + * @param boundingBox Box for this projection. + * @param maxSize Maximum size of any side (width / height) of the image to + * which this projection should draw. + */ + public PlateCarreProjection(BoundingBox boundingBox, int maxSize) { + // Find minimum/maximum longitude and latitude. + this.minLongitude = boundingBox.getTopLeftPoint().getLongitude(); + this.maxLongitude = boundingBox.getBottomRightPoint().getLongitude(); + this.minLatitude = boundingBox.getBottomRightPoint().getLatitude(); + this.maxLatitude = boundingBox.getTopLeftPoint().getLatitude(); + + float diffLon = maxLongitude - minLongitude, diffLat = maxLatitude - minLatitude; + + this.width = diffLon < diffLat ? (int) (maxSize * diffLon / diffLat) : maxSize; + this.height = diffLon < diffLat ? maxSize : (int) (maxSize * diffLat / diffLon); + } + + @Override + public double getImageWidth() { + return this.width; + } + + @Override + public double getImageHeight() { + return this.height; + } + + @Override + public int latitudeToPixelY(float latitude) { + return (int) (this.height * (this.maxLatitude - latitude) + / (this.maxLatitude - this.minLatitude)); + } + + @Override + public int longitudeToPixelX(float longitude) { + return (int) (this.width * (longitude - this.minLongitude) + / (this.maxLongitude - this.minLongitude)); + } + + @Override + public float pixelYToLatitude(double py) { + return (float) (this.maxLatitude + - py / this.height * (this.maxLatitude - this.minLatitude)); + } + + @Override + public float pixelXToLongitude(double px) { + return (float) (px / this.width * (this.maxLongitude - this.minLongitude) + + this.minLongitude); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Projection.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Projection.java new file mode 100644 index 0000000..f05dd9c --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/Projection.java @@ -0,0 +1,51 @@ +package org.insa.graphs.gui.drawing; + +public interface Projection { + + /** + * @return Image width for this projection to work properly. + */ + public double getImageWidth(); + + /** + * @return Image weight for this projection to work properly. + */ + public double getImageHeight(); + + /** + * Project the given latitude on the image. + * + * @param latitude Latitude to project. + * + * @return Projected position of the latitude on the image. + */ + public int latitudeToPixelY(float latitude); + + /** + * Project the given longitude on the image. + * + * @param longitude Longitude to project. + * + * @return Projected position of the longitude on the image. + */ + public int longitudeToPixelX(float longitude); + + /** + * Retrieve the latitude associated to the given projected point. + * + * @param py Projected y-position for which latitude should be retrieved. + * + * @return The original latitude of the point. + */ + public float pixelYToLatitude(double py); + + /** + * Retrieve the longitude associated to the given projected point. + * + * @param px Projected x-position for which longitude should be retrieved. + * + * @return The original longitude of the point. + */ + public float pixelXToLongitude(double px); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/BasicDrawing.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/BasicDrawing.java new file mode 100644 index 0000000..6d51a05 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/BasicDrawing.java @@ -0,0 +1,747 @@ +package org.insa.graphs.gui.drawing.components; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.swing.JPanel; + +import org.insa.graphs.gui.drawing.BasicGraphPalette; +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.DrawingClickListener; +import org.insa.graphs.gui.drawing.GraphPalette; +import org.insa.graphs.gui.drawing.MercatorProjection; +import org.insa.graphs.gui.drawing.PlateCarreProjection; +import org.insa.graphs.gui.drawing.Projection; +import org.insa.graphs.gui.drawing.overlays.MarkerOverlay; +import org.insa.graphs.gui.drawing.overlays.MarkerUtils; +import org.insa.graphs.gui.drawing.overlays.Overlay; +import org.insa.graphs.gui.drawing.overlays.PathOverlay; +import org.insa.graphs.gui.drawing.overlays.PointSetOverlay; +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.Path; +import org.insa.graphs.model.Point; +import org.insa.graphs.model.GraphStatistics.BoundingBox; + +/** + * + */ +public class BasicDrawing extends JPanel implements Drawing { + + /** + * + */ + private static final long serialVersionUID = 96779785877771827L; + + private abstract class BasicOverlay implements Overlay { + + // Visible? + protected boolean visible; + + // Color + protected Color color; + + public BasicOverlay(Color color) { + this.visible = true; + this.color = color; + } + + /** + * @return The Z level of this overlay (>= 1). + */ + public abstract int getZLevel(); + + @Override + public void setColor(Color color) { + this.color = color; + } + + @Override + public Color getColor() { + return this.color; + } + + @Override + public void setVisible(boolean visible) { + this.visible = visible; + BasicDrawing.this.repaint(); + } + + @Override + public boolean isVisible() { + return this.visible; + } + + @Override + public void delete() { + BasicDrawing.this.overlays.remove(this); + } + + /** + * Draw the given overlay. + */ + public void draw(Graphics2D g) { + if (this.visible) { + drawImpl(g); + } + } + + public abstract void drawImpl(Graphics2D g); + + public void redraw() { + BasicDrawing.this.repaint(); + } + + }; + + private class BasicMarkerOverlay extends BasicOverlay implements MarkerOverlay { + + // Marker width and height + public static final int MARKER_WIDTH = 30, MARKER_HEIGHT = 60; + + // Point of the marker. + private Point point; + + // Image to draw + private Image image; + + // Inner color and fill mode. + private Color innerColor; + private final AlphaMode alphaMode; + + public BasicMarkerOverlay(Point point, Color color, Color inner, AlphaMode alphaMode) { + super(color); + this.point = point; + this.image = MarkerUtils.getMarkerForColor(color, inner, alphaMode); + this.innerColor = inner; + this.alphaMode = alphaMode; + } + + public int getZLevel() { + return 3; + } + + @Override + public Point getPoint() { + return point; + } + + @Override + public void setColor(Color color) { + this.innerColor = this.innerColor.equals(this.color) ? color : innerColor; + super.setColor(color); + this.image = MarkerUtils.getMarkerForColor(color, this.innerColor, alphaMode); + } + + @Override + public void moveTo(Point point) { + this.point = point; + BasicDrawing.this.repaint(); + } + + @Override + public void drawImpl(Graphics2D graphics) { + + int px = projection.longitudeToPixelX(getPoint().getLongitude()); + int py = projection.latitudeToPixelY(getPoint().getLatitude()); + + graphics.drawImage(this.image, px - MARKER_WIDTH / 2, py - MARKER_HEIGHT, MARKER_WIDTH, + MARKER_HEIGHT, BasicDrawing.this); + } + + }; + + private class BasicPathOverlay extends BasicOverlay implements PathOverlay { + + // List of points + private final List points; + + // Origin / Destination markers. + private BasicMarkerOverlay origin, destination; + + public BasicPathOverlay(List points, Color color, BasicMarkerOverlay origin, + BasicMarkerOverlay destination) { + super(color); + this.points = points; + this.origin = origin; + this.destination = destination; + this.color = color; + } + + public int getZLevel() { + return 2; + } + + @Override + public void setColor(Color color) { + super.setColor(color); + this.origin.setColor(color); + this.destination.setColor(color); + } + + @Override + public void drawImpl(Graphics2D graphics) { + + if (!points.isEmpty()) { + + graphics.setStroke(new BasicStroke(2)); + graphics.setColor(getColor()); + + Iterator itPoint = points.iterator(); + Point prev = itPoint.next(); + + while (itPoint.hasNext()) { + Point curr = itPoint.next(); + + int x1 = projection.longitudeToPixelX(prev.getLongitude()); + int x2 = projection.longitudeToPixelX(curr.getLongitude()); + int y1 = projection.latitudeToPixelY(prev.getLatitude()); + int y2 = projection.latitudeToPixelY(curr.getLatitude()); + + graphics.drawLine(x1, y1, x2, y2); + + prev = curr; + } + + } + + if (this.origin != null) { + this.origin.draw(graphics); + } + if (this.destination != null) { + this.destination.draw(graphics); + } + } + + }; + + private class BasicPointSetOverlay extends BasicOverlay implements PointSetOverlay { + + // Default point width + private static final int DEFAULT_POINT_WIDTH = 5; + + // Image for path / points + private final BufferedImage image; + private final Graphics2D graphics; + + private int width = DEFAULT_POINT_WIDTH; + + public BasicPointSetOverlay() { + super(Color.BLACK); + this.image = new BufferedImage(BasicDrawing.this.width, BasicDrawing.this.height, + BufferedImage.TYPE_4BYTE_ABGR); + this.graphics = image.createGraphics(); + this.graphics.setBackground(new Color(0, 0, 0, 0)); + } + + public int getZLevel() { + return 1; + } + + @Override + public void setColor(Color color) { + super.setColor(color); + this.graphics.setColor(color); + } + + @Override + public void setWidth(int width) { + this.width = Math.max(2, width); + } + + @Override + public void setWidthAndColor(int width, Color color) { + setWidth(width); + setColor(color); + } + + @Override + public void addPoint(Point point) { + int x = projection.longitudeToPixelX(point.getLongitude()) - this.width / 2; + int y = projection.latitudeToPixelY(point.getLatitude()) - this.width / 2; + this.graphics.fillOval(x, y, this.width, this.width); + BasicDrawing.this.repaint(); + } + + @Override + public void addPoint(Point point, int width) { + setWidth(width); + addPoint(point); + } + + @Override + public void addPoint(Point point, Color color) { + setColor(color); + addPoint(point); + } + + @Override + public void addPoint(Point point, int width, Color color) { + setWidth(width); + setColor(color); + addPoint(point); + } + + @Override + public void drawImpl(Graphics2D g) { + g.drawImage(this.image, 0, 0, BasicDrawing.this); + } + + } + + /** + * Class encapsulating a set of overlays. + * + */ + private class BasicOverlays { + + // List of overlays. + private ArrayList> overlays = new ArrayList<>(); + + public synchronized void draw(Graphics2D g) { + // Clear overlays. + for (ArrayList arr: this.overlays) { + for (BasicOverlay overlay: arr) { + overlay.draw(g); + } + } + } + + public synchronized void remove(BasicOverlay overlay) { + overlays.get(overlay.getZLevel() - 1).remove(overlay); + BasicDrawing.this.repaint(); + } + + public void clear() { + clear(true); + } + + public void clear(boolean repaint) { + // Clear overlays. + for (ArrayList arr: this.overlays) { + arr.clear(); + } + // Repaint if requested. + if (repaint) { + BasicDrawing.this.repaint(); + } + } + + public BasicOverlay add(BasicOverlay marker) { + return add(marker, true); + } + + public synchronized BasicOverlay add(BasicOverlay overlay, boolean repaint) { + + // Check if we have a level for this... + for (int i = overlays.size(); i < overlay.getZLevel(); ++i) { + overlays.add(new ArrayList<>()); + } + + // Add overlay to the given list. + overlays.get(overlay.getZLevel() - 1).add(overlay); + + // Repaint if requested. + if (repaint) { + BasicDrawing.this.repaint(); + } + + return overlay; + } + + }; + + // Default path color. + public static final Color DEFAULT_PATH_COLOR = new Color(66, 134, 244); + + // Default palette. + public static final GraphPalette DEFAULT_PALETTE = new BasicGraphPalette(); + + // Maximum width for the drawing (in pixels). + private static final int MAXIMUM_DRAWING_WIDTH = 2000; + + private Projection projection; + + // Width and height of the image + private int width, height; + + // Zoom controls + private MapZoomControls zoomControls; + private ZoomAndPanListener zoomAndPanListener; + + // + private Image graphImage = null; + private Graphics2D graphGraphics = null; + + // List of image for markers + private BasicOverlays overlays = new BasicOverlays(); + + // Mapping DrawingClickListener -> MouseEventListener + private List drawingClickListeners = new ArrayList<>(); + + /** + * Create a new BasicDrawing. + * + */ + public BasicDrawing() { + setLayout(null); + this.setBackground(new Color(245, 245, 245)); + this.zoomAndPanListener = new ZoomAndPanListener(this, + ZoomAndPanListener.DEFAULT_MIN_ZOOM_LEVEL, 20, 1.2); + + // Try... + try { + this.zoomControls = new MapZoomControls(this, 0, + ZoomAndPanListener.DEFAULT_MIN_ZOOM_LEVEL, 20); + this.zoomControls.addZoomInListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomAndPanListener.zoomIn(); + } + }); + this.zoomControls.addZoomOutListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomAndPanListener.zoomOut(); + } + }); + } + catch (IOException e) { + e.printStackTrace(); + } + + this.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent evt) { + if (zoomControls.contains(evt.getPoint())) { + return; + } + Point lonlat = null; + try { + lonlat = getLongitudeLatitude(evt); + } + catch (NoninvertibleTransformException e) { + return; + } + for (DrawingClickListener listener: drawingClickListeners) { + listener.mouseClicked(lonlat); + } + } + + }); + } + + @Override + public void paintComponent(Graphics g1) { + super.paintComponent(g1); + Graphics2D g = (Graphics2D) g1; + AffineTransform sTransform = g.getTransform(); + g.setColor(this.getBackground()); + g.fillRect(0, 0, getWidth(), getHeight()); + g.setTransform(zoomAndPanListener.getCoordTransform()); + + if (graphImage != null) { + // Draw graph + g.drawImage(graphImage, 0, 0, this); + } + + // Draw markers + this.overlays.draw(g); + + g.setTransform(sTransform); + if (this.zoomControls != null) { + this.zoomControls.setZoomLevel(this.zoomAndPanListener.getZoomLevel()); + this.zoomControls.draw(g, getWidth() - this.zoomControls.getWidth() - 20, + this.getHeight() - this.zoomControls.getHeight() - 10, this); + } + + } + + /* + * (non-Javadoc) + * + * @see org.insa.graphics.drawing.Drawing#clear() + */ + @Override + public void clear() { + if (this.graphGraphics != null) { + this.graphGraphics.clearRect(0, 0, this.width, this.height); + } + this.overlays.clear(false); + this.repaint(); + } + + /* + * (non-Javadoc) + * + * @see org.insa.graphics.drawing.Drawing#clearOverlays() + */ + @Override + public void clearOverlays() { + this.overlays.clear(); + } + + /** + * @return The current ZoomAndPanListener associated with this drawing. + */ + public ZoomAndPanListener getZoomAndPanListener() { + return this.zoomAndPanListener; + } + + /** + * Return the longitude and latitude corresponding to the given position of the + * MouseEvent. + * + * @param event MouseEvent from which longitude/latitude should be retrieved. + * + * @return Point representing the projection of the MouseEvent position in the + * graph/map. + * + * @throws NoninvertibleTransformException if the actual transformation is + * invalid. + */ + protected Point getLongitudeLatitude(MouseEvent event) throws NoninvertibleTransformException { + // Get the point using the inverse transform of the Zoom/Pan object, this gives + // us + // a point within the drawing box (between [0, 0] and [width, height]). + Point2D ptDst = this.zoomAndPanListener.getCoordTransform() + .inverseTransform(event.getPoint(), null); + + // Inverse the "projection" on x/y to get longitude and latitude. + return new Point(projection.pixelXToLongitude(ptDst.getX()), + projection.pixelYToLatitude(ptDst.getY())); + } + + /* + * (non-Javadoc) + * + * @see + * org.insa.graphics.drawing.Drawing#addDrawingClickListener(org.insa.graphics. + * drawing.DrawingClickListener) + */ + @Override + public void addDrawingClickListener(DrawingClickListener listener) { + this.drawingClickListeners.add(listener); + } + + /* + * (non-Javadoc) + * + * @see org.insa.graphics.drawing.Drawing#removeDrawingClickListener(org.insa. + * graphics.drawing.DrawingClickListener) + */ + @Override + public void removeDrawingClickListener(DrawingClickListener listener) { + this.drawingClickListeners.remove(listener); + } + + public BasicMarkerOverlay createMarker(Point point, Color outer, Color inner, AlphaMode mode) { + return new BasicMarkerOverlay(point, outer, inner, mode); + } + + @Override + public MarkerOverlay drawMarker(Point point, Color outer, Color inner, AlphaMode mode) { + return (MarkerOverlay) this.overlays.add(createMarker(point, outer, inner, mode)); + } + + @Override + public PointSetOverlay createPointSetOverlay() { + return (PointSetOverlay) this.overlays.add(new BasicPointSetOverlay(), false); + } + + @Override + public PointSetOverlay createPointSetOverlay(int width, Color color) { + PointSetOverlay ps = createPointSetOverlay(); + ps.setWidthAndColor(width, color); + return ps; + } + + /** + * Draw the given arc. + * + * @param arc Arc to draw. + * @param palette Palette to use to retrieve color and width for arc, or null to + * use current settings. + */ + protected void drawArc(Arc arc, GraphPalette palette, boolean repaint) { + List pts = arc.getPoints(); + if (!pts.isEmpty()) { + if (palette != null) { + this.graphGraphics.setColor(palette.getColorForArc(arc)); + this.graphGraphics.setStroke(new BasicStroke(palette.getWidthForArc(arc))); + } + Iterator it1 = pts.iterator(); + Point prev = it1.next(); + while (it1.hasNext()) { + Point curr = it1.next(); + + int x1 = projection.longitudeToPixelX(prev.getLongitude()); + int x2 = projection.longitudeToPixelX(curr.getLongitude()); + int y1 = projection.latitudeToPixelY(prev.getLatitude()); + int y2 = projection.latitudeToPixelY(curr.getLatitude()); + + graphGraphics.drawLine(x1, y1, x2, y2); + prev = curr; + } + } + if (repaint) { + this.repaint(); + } + } + + /** + * Initialize the drawing for the given graph. + * + * @param graph + */ + protected void initialize(Graph graph) { + + // Clear everything. + this.clear(); + + BoundingBox box = graph.getGraphInformation().getBoundingBox(); + + // Find minimum/maximum longitude and latitude. + float minLon = box.getTopLeftPoint().getLongitude(), + maxLon = box.getBottomRightPoint().getLongitude(), + minLat = box.getBottomRightPoint().getLatitude(), + maxLat = box.getTopLeftPoint().getLatitude(); + + // Add a little delta to avoid drawing on the edge... + float diffLon = maxLon - minLon, diffLat = maxLat - minLat; + float deltaLon = 0.01f * diffLon, deltaLat = 0.01f * diffLat; + + // Create the projection and retrieve width and height for the box. + BoundingBox extendedBox = box.extend(deltaLon, deltaLat, deltaLon, deltaLat); + + // Special projection for non-realistic maps... + if (graph.getMapId().startsWith("0x")) { + projection = new PlateCarreProjection(extendedBox, MAXIMUM_DRAWING_WIDTH / 4); + } + else { + projection = new MercatorProjection(extendedBox, MAXIMUM_DRAWING_WIDTH); + } + this.width = (int) projection.getImageWidth(); + this.height = (int) projection.getImageHeight(); + + // Create the image + BufferedImage img = new BufferedImage(this.width, this.height, + BufferedImage.TYPE_3BYTE_BGR); + this.graphImage = img; + this.graphGraphics = img.createGraphics(); + this.graphGraphics.setBackground(this.getBackground()); + this.graphGraphics.clearRect(0, 0, this.width, this.height); + + // Set the zoom and pan listener + + double scale = 1 / Math.max(this.width / (double) this.getWidth(), + this.height / (double) this.getHeight()); + + this.zoomAndPanListener.setCoordTransform(this.graphGraphics.getTransform()); + this.zoomAndPanListener.getCoordTransform().translate( + (this.getWidth() - this.width * scale) / 2, + (this.getHeight() - this.height * scale) / 2); + this.zoomAndPanListener.getCoordTransform().scale(scale, scale); + this.zoomAndPanListener.setZoomLevel(0); + this.zoomControls.setZoomLevel(0); + + // Repaint + this.repaint(); + } + + @Override + public void drawGraph(Graph graph, GraphPalette palette) { + int repaintModulo = Math.max(1, graph.size() / 100); + + // Initialize the buffered image + + this.initialize(graph); + + // Remove zoom and pan listener + this.removeMouseListener(zoomAndPanListener); + this.removeMouseMotionListener(zoomAndPanListener); + this.removeMouseWheelListener(zoomAndPanListener); + + for (Node node: graph.getNodes()) { + for (Arc arc: node.getSuccessors()) { + // Draw arcs only if there are one-way arcs or if origin is lower than + // destination, avoid drawing two-ways arc twice. + if (arc.getRoadInformation().isOneWay() + || arc.getOrigin().compareTo(arc.getDestination()) < 0) { + drawArc(arc, palette, false); + } + } + if (node.getId() % repaintModulo == 0) { + this.repaint(); + } + } + this.repaint(); + + // Re-add zoom and pan listener + this.addMouseListener(zoomAndPanListener); + this.addMouseMotionListener(zoomAndPanListener); + this.addMouseWheelListener(zoomAndPanListener); + } + + @Override + public void drawGraph(Graph graph) { + drawGraph(graph, DEFAULT_PALETTE); + } + + @Override + public PathOverlay drawPath(Path path, Color color, boolean markers) { + List points = new ArrayList(); + if (!path.isEmpty()) { + points.add(path.getOrigin().getPoint()); + for (Arc arc: path.getArcs()) { + Iterator itPoint = arc.getPoints().iterator(); + // Discard origin each time + itPoint.next(); + while (itPoint.hasNext()) { + points.add(itPoint.next()); + } + } + } + BasicMarkerOverlay origin = null, destination = null; + if (markers && !path.isEmpty()) { + origin = createMarker(path.getOrigin().getPoint(), color, color, AlphaMode.TRANSPARENT); + destination = createMarker(path.getDestination().getPoint(), color, color, + AlphaMode.TRANSPARENT); + } + return (PathOverlay) this.overlays + .add(new BasicPathOverlay(points, color, origin, destination)); + } + + @Override + public PathOverlay drawPath(Path path, Color color) { + return drawPath(path, color, true); + } + + @Override + public PathOverlay drawPath(Path path) { + return drawPath(path, DEFAULT_PATH_COLOR); + } + + @Override + public PathOverlay drawPath(Path path, boolean markers) { + return drawPath(path, DEFAULT_PATH_COLOR, markers); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapViewDrawing.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapViewDrawing.java new file mode 100644 index 0000000..604a6db --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapViewDrawing.java @@ -0,0 +1,521 @@ +package org.insa.graphs.gui.drawing.components; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.DrawingClickListener; +import org.insa.graphs.gui.drawing.GraphPalette; +import org.insa.graphs.gui.drawing.overlays.MarkerAutoScaling; +import org.insa.graphs.gui.drawing.overlays.MarkerOverlay; +import org.insa.graphs.gui.drawing.overlays.MarkerUtils; +import org.insa.graphs.gui.drawing.overlays.Overlay; +import org.insa.graphs.gui.drawing.overlays.PathOverlay; +import org.insa.graphs.gui.drawing.overlays.PointSetOverlay; +import org.insa.graphs.gui.drawing.overlays.PolylineAutoScaling; +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Path; +import org.insa.graphs.model.Point; +import org.mapsforge.core.graphics.GraphicFactory; +import org.mapsforge.core.model.BoundingBox; +import org.mapsforge.core.model.LatLong; +import org.mapsforge.core.model.MapPosition; +import org.mapsforge.core.util.LatLongUtils; +import org.mapsforge.core.util.Parameters; +import org.mapsforge.map.awt.graphics.AwtGraphicFactory; +import org.mapsforge.map.awt.util.AwtUtil; +import org.mapsforge.map.awt.view.MapView; +import org.mapsforge.map.datastore.MapDataStore; +import org.mapsforge.map.layer.Layer; +import org.mapsforge.map.layer.Layers; +import org.mapsforge.map.layer.cache.TileCache; +import org.mapsforge.map.layer.hills.HillsRenderConfig; +import org.mapsforge.map.layer.overlay.Marker; +import org.mapsforge.map.layer.overlay.Polygon; +import org.mapsforge.map.layer.renderer.TileRendererLayer; +import org.mapsforge.map.model.DisplayModel; +import org.mapsforge.map.model.IMapViewPosition; +import org.mapsforge.map.model.Model; +import org.mapsforge.map.reader.MapFile; +import org.mapsforge.map.rendertheme.InternalRenderTheme; + +/** + * + */ +public class MapViewDrawing extends MapView implements Drawing { + + /** + * + */ + private static final long serialVersionUID = 8606967833704938092L; + + /** + * Base Overlay for MapViewDrawing overlays. + * + */ + private abstract class MapViewOverlay implements Overlay { + + // Marker associated. + protected Layer[] layers; + + // Current color + protected Color color; + + public MapViewOverlay(Layer[] layers, Color color) { + this.layers = layers; + for (Layer layer: this.layers) { + MapViewDrawing.this.getLayerManager().getLayers().add(layer); + } + this.color = color; + } + + @Override + public void setColor(Color color) { + this.color = color; + } + + @Override + public Color getColor() { + return this.color; + } + + @Override + public void setVisible(boolean visible) { + for (Layer layer: layers) { + layer.setVisible(visible); + } + } + + @Override + public boolean isVisible() { + if (this.layers.length == 0) { + return true; + } + return this.layers[0].isVisible(); + } + + @Override + public void delete() { + Layers mlayers = MapViewDrawing.this.getLayerManager().getLayers(); + for (Layer layer: layers) { + mlayers.remove(layer); + } + } + + @Override + public void redraw() { + MapViewDrawing.this.getLayerManager().redrawLayers(); + } + }; + + /** + * MarkerOverlay for MapViewDrawing. + * + */ + private class MapViewMarkerOverlay extends MapViewOverlay implements MarkerOverlay { + + private final AlphaMode alphaMode; + private Color innerColor; + + public MapViewMarkerOverlay(Marker marker, Color outer, Color innerColor, + AlphaMode alphaMode) { + super(new Layer[] { marker }, outer); + this.innerColor = innerColor; + this.alphaMode = alphaMode; + } + + @Override + public Point getPoint() { + Marker marker = (Marker) super.layers[0]; + return new Point((float) marker.getLatLong().getLongitude(), + (float) marker.getLatLong().getLatitude()); + } + + @Override + public void setColor(Color outer) { + this.innerColor = this.innerColor.equals(this.color) ? outer : this.innerColor; + super.setColor(color); + MarkerAutoScaling marker = (MarkerAutoScaling) super.layers[0]; + marker.setImage(MarkerUtils.getMarkerForColor(color, this.innerColor, this.alphaMode)); + } + + @Override + public void moveTo(Point point) { + MarkerAutoScaling marker = (MarkerAutoScaling) this.layers[0]; + this.delete(); + marker = new MarkerAutoScaling(convertPoint(point), marker.getImage()); + this.layers[0] = marker; + MapViewDrawing.this.getLayerManager().getLayers().add(marker); + } + + }; + + /** + * PathOverlay for MapViewDrawing. + * + */ + private class MapViewPathOverlay extends MapViewOverlay implements PathOverlay { + + public MapViewPathOverlay(PolylineAutoScaling path, MarkerAutoScaling origin, + MarkerAutoScaling destination) { + super(new Layer[] { path, origin, destination }, path.getColor()); + } + + public MapViewPathOverlay(PolylineAutoScaling path) { + super(new Layer[] { path }, path.getColor()); + } + + @Override + public void setColor(Color color) { + super.setColor(color); + ((PolylineAutoScaling) this.layers[0]).setColor(color); + ((MarkerAutoScaling) this.layers[1]) + .setImage(MarkerUtils.getMarkerForColor(color, color, AlphaMode.TRANSPARENT)); + ((MarkerAutoScaling) this.layers[2]) + .setImage(MarkerUtils.getMarkerForColor(color, color, AlphaMode.TRANSPARENT)); + } + + } + + /** + * PointSetOverlay for MapViewDrawing - Not currently implemented. + * + */ + private class MapViewPointSetOverlay extends MapViewOverlay implements PointSetOverlay { + + private List points = new ArrayList<>(); + private final Polygon polygon; + + private List convexHull(List p) { + if (p.isEmpty()) { + return new ArrayList<>(); + } + p.sort((p1, p2) -> Float.compare(p1.getLongitude(), p2.getLongitude())); + List h = new ArrayList<>(); + + // lower hull + for (Point pt: p) { + while (h.size() >= 2 && !ccw(h.get(h.size() - 2), h.get(h.size() - 1), pt)) { + h.remove(h.size() - 1); + } + h.add(pt); + } + + // upper hull + int t = h.size() + 1; + for (int i = p.size() - 1; i >= 0; i--) { + Point pt = p.get(i); + while (h.size() >= t && !ccw(h.get(h.size() - 2), h.get(h.size() - 1), pt)) { + h.remove(h.size() - 1); + } + h.add(pt); + } + + h.remove(h.size() - 1); + return h; + } + + // ccw returns true if the three points make a counter-clockwise turn + private boolean ccw(Point a, Point b, Point c) { + return ((b.getLongitude() - a.getLongitude()) + * (c.getLatitude() - a.getLatitude())) > ((b.getLatitude() - a.getLatitude()) + * (c.getLongitude() - a.getLongitude())); + } + + public MapViewPointSetOverlay() { + super(new Layer[] { new Polygon(GRAPHIC_FACTORY.createPaint(), null, GRAPHIC_FACTORY) }, + Color.BLACK); + polygon = (Polygon) this.layers[0]; + } + + @Override + public void setColor(Color color) { + super.setColor(color); + polygon.getPaintFill().setColor(GRAPHIC_FACTORY.createColor(100, color.getRed(), + color.getGreen(), color.getBlue())); + } + + @Override + public void setWidth(int width) { + } + + @Override + public void setWidthAndColor(int width, Color color) { + setWidth(width); + setColor(color); + } + + @Override + public void addPoint(Point point) { + points.add(point); + this.points = convexHull(points); + polygon.setPoints(this.points.stream().map(MapViewDrawing.this::convertPoint) + .collect(Collectors.toList())); + polygon.requestRedraw(); + } + + @Override + public void addPoint(Point point, int width) { + setWidth(width); + addPoint(point); + } + + @Override + public void addPoint(Point point, Color color) { + setColor(color); + addPoint(point); + } + + @Override + public void addPoint(Point point, int width, Color color) { + setWidth(width); + setColor(color); + addPoint(point); + } + + }; + + // Default path color. + public static final Color DEFAULT_PATH_COLOR = new Color(66, 134, 244); + + // Graphic factory. + private static final GraphicFactory GRAPHIC_FACTORY = AwtGraphicFactory.INSTANCE; + + // Default tile size. + private static final int DEFAULT_TILE_SIZE = 512; + + // List of listeners. + private ArrayList drawingClickListeners = new ArrayList<>(); + + // Tile size + private int tileSize; + + // Zoom controls + private MapZoomControls zoomControls; + + public MapViewDrawing() { + super(); + Parameters.NUMBER_OF_THREADS = 2; + Parameters.SQUARE_FRAME_BUFFER = false; + + getMapScaleBar().setVisible(true); + DisplayModel model = getModel().displayModel; + this.tileSize = DEFAULT_TILE_SIZE; + model.setFixedTileSize(this.tileSize); + + this.setZoomLevelMin((byte) 0); + this.setZoomLevelMax((byte) 20); + + // Try... + try { + this.zoomControls = new MapZoomControls(this, 0, 0, 20); + this.zoomControls.addZoomInListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + getModel().mapViewPosition.zoomIn(); + } + }); + this.zoomControls.addZoomOutListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + getModel().mapViewPosition.zoomOut(); + } + }); + } + catch (IOException e) { + e.printStackTrace(); + } + } + + /* + * (non-Javadoc) + * + * @see org.mapsforge.map.awt.view.MapView#paint(java.awt.Graphics) + */ + @Override + public void paint(Graphics graphics) { + super.paint(graphics); + if (this.zoomControls != null) { + this.zoomControls.setZoomLevel(this.getModel().mapViewPosition.getZoomLevel()); + this.zoomControls.draw((Graphics2D) graphics, + getWidth() - this.zoomControls.getWidth() - 20, + this.getHeight() - this.zoomControls.getHeight() - 10, this); + } + + } + + /* + * (non-Javadoc) + * + * @see org.insa.graphics.drawing.Drawing#clear() + */ + @Override + public void clear() { + getLayerManager().getLayers().clear(); + repaint(); + } + + /* + * (non-Javadoc) + * + * @see org.insa.graphics.drawing.Drawing#clearOverlays() + */ + @Override + public void clearOverlays() { + Layers layers = getLayerManager().getLayers(); + for (Layer layer: layers) { + if (layer instanceof PolylineAutoScaling || layer instanceof MarkerAutoScaling) { + getLayerManager().getLayers().remove(layer, false); + } + } + repaint(); + } + + protected LatLong convertPoint(Point point) { + return new LatLong(point.getLatitude(), point.getLongitude()); + } + + private TileRendererLayer createTileRendererLayer(TileCache tileCache, + MapDataStore mapDataStore, IMapViewPosition mapViewPosition, + HillsRenderConfig hillsRenderConfig) { + TileRendererLayer tileRendererLayer = new TileRendererLayer(tileCache, mapDataStore, + mapViewPosition, false, true, false, GRAPHIC_FACTORY, hillsRenderConfig) { + @Override + public boolean onTap(LatLong tapLatLong, org.mapsforge.core.model.Point layerXY, + org.mapsforge.core.model.Point tapXY) { + if (zoomControls.contains(new java.awt.Point((int) tapXY.x, (int) tapXY.y))) { + return false; + } + Point pt = new Point((float) tapLatLong.getLongitude(), + (float) tapLatLong.getLatitude()); + for (DrawingClickListener listener: MapViewDrawing.this.drawingClickListeners) { + listener.mouseClicked(pt); + } + return true; + } + }; + tileRendererLayer.setXmlRenderTheme(InternalRenderTheme.DEFAULT); + return tileRendererLayer; + } + + @Override + public void addDrawingClickListener(DrawingClickListener listener) { + this.drawingClickListeners.add(listener); + } + + @Override + public void removeDrawingClickListener(DrawingClickListener listener) { + this.drawingClickListeners.remove(listener); + } + + protected MarkerAutoScaling createMarker(Point point, Color outer, Color inner, + AlphaMode mode) { + Image image = MarkerUtils.getMarkerForColor(outer, inner, mode); + return new MarkerAutoScaling(convertPoint(point), image); + } + + @Override + public MarkerOverlay drawMarker(Point point, Color outer, Color inner, AlphaMode mode) { + return new MapViewMarkerOverlay(createMarker(point, outer, inner, mode), outer, inner, + mode); + } + + @Override + public PointSetOverlay createPointSetOverlay() { + return new MapViewPointSetOverlay(); + } + + @Override + public PointSetOverlay createPointSetOverlay(int width, Color color) { + PointSetOverlay ps = new MapViewPointSetOverlay(); + ps.setWidthAndColor(width, color); + return ps; + } + + public void drawGraph(File file) { + + // Tile cache + TileCache tileCache = AwtUtil.createTileCache(tileSize, + getModel().frameBufferModel.getOverdrawFactor(), 1024, + new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString())); + + // Layers + Layers layers = getLayerManager().getLayers(); + + MapDataStore mapDataStore = new MapFile(file); + TileRendererLayer tileRendererLayer = createTileRendererLayer(tileCache, mapDataStore, + getModel().mapViewPosition, null); + layers.add(tileRendererLayer); + BoundingBox boundingBox = mapDataStore.boundingBox(); + + final Model model = getModel(); + if (model.mapViewPosition.getZoomLevel() == 0 + || !boundingBox.contains(model.mapViewPosition.getCenter())) { + byte zoomLevel = LatLongUtils.zoomForBounds(model.mapViewDimension.getDimension(), + boundingBox, model.displayModel.getTileSize()); + model.mapViewPosition + .setMapPosition(new MapPosition(boundingBox.getCenterPoint(), zoomLevel)); + zoomControls.setZoomLevel(zoomLevel); + } + + } + + @Override + public void drawGraph(Graph graph, GraphPalette palette) { + throw new RuntimeException("Not implemented, use drawGraph(File)."); + } + + @Override + public void drawGraph(Graph graph) { + throw new RuntimeException("Not implemented, use drawGraph(File)."); + } + + @Override + public PathOverlay drawPath(Path path, Color color, boolean markers) { + PolylineAutoScaling line = new PolylineAutoScaling(1, color); + ArrayList points = new ArrayList<>(path.getArcs().size() * 4); + for (Arc arc: path.getArcs()) { + points.addAll(arc.getPoints()); + } + line.addAll(points); + PathOverlay overlay = null; + if (markers) { + MarkerAutoScaling origin = createMarker(path.getOrigin().getPoint(), color, color, + AlphaMode.TRANSPARENT), + destination = createMarker(path.getDestination().getPoint(), color, color, + AlphaMode.TRANSPARENT); + overlay = new MapViewPathOverlay(line, origin, destination); + } + else { + overlay = new MapViewPathOverlay(line); + } + return overlay; + } + + @Override + public PathOverlay drawPath(Path path, Color color) { + return drawPath(path, color, true); + } + + @Override + public PathOverlay drawPath(Path path) { + return drawPath(path, DEFAULT_PATH_COLOR, true); + } + + @Override + public PathOverlay drawPath(Path path, boolean markers) { + return drawPath(path, DEFAULT_PATH_COLOR, markers); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapZoomControls.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapZoomControls.java new file mode 100644 index 0000000..0db6628 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/MapZoomControls.java @@ -0,0 +1,209 @@ +package org.insa.graphs.gui.drawing.components; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.ImageObserver; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +public class MapZoomControls { + + // Default ID for action events + private static final int ZOOM_IN_ACTION_ID = 0x1; + private static final String ZOOM_IN_ACTION_NAME = "ZoomIn"; + + private static final int ZOOM_OUT_ACTION_ID = 0x2; + private static final String ZOOM_OUT_ACTION_NAME = "ZoomOut"; + + // Height + private static final int DEFAULT_HEIGHT = 20; + + // Default spacing between mark + private static final int DEFAULT_SPACING = 4; + + // Zoom ticks ratio from height (not the current one) + private static final double ZOOM_TICK_HEIGHT_RATIO = 0.3; + + // Zoom ticks color + private static final Color ZOOM_TICK_COLOR = Color.GRAY; + + // Current zoom ticks ratio from height + private static final double CURRENT_ZOOM_TICK_HEIGHT_RATIO = 0.8; + + // Zoom ticks color + private static final Color CURRENT_ZOOM_TICK_COLOR = new Color(25, 25, 25); + + // Use half mark or not + private boolean halfMark = true; + + private int currentLevel = 0; + private final int minLevel, maxLevel; + + // Zoom in/out image and their rectangles. + private final Image zoomIn, zoomOut; + private final Rectangle zoomInRect = new Rectangle(0, 0, 0, 0), + zoomOutRect = new Rectangle(0, 0, 0, 0); + + // List of listeners + private final List zoomInListeners = new ArrayList<>(); + private final List zoomOutListeners = new ArrayList<>(); + + public MapZoomControls(Component component, final int defaultZoom, final int minZoom, + final int maxZoom) throws IOException { + + zoomIn = ImageIO.read(getClass().getResourceAsStream("/zoomIn.png")) + .getScaledInstance(DEFAULT_HEIGHT, DEFAULT_HEIGHT, Image.SCALE_SMOOTH); + zoomOut = ImageIO.read(getClass().getResourceAsStream("/zoomOut.png")) + .getScaledInstance(DEFAULT_HEIGHT, DEFAULT_HEIGHT, Image.SCALE_SMOOTH); + + this.currentLevel = defaultZoom; + this.minLevel = minZoom; + this.maxLevel = maxZoom; + + component.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + if (zoomInRect.contains(e.getPoint()) || zoomOutRect.contains(e.getPoint())) { + component.setCursor(new Cursor(Cursor.HAND_CURSOR)); + } + else { + component.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + } + } + }); + + component.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (zoomInRect.contains(e.getPoint()) && currentLevel < maxLevel) { + currentLevel += 1; + for (ActionListener al: zoomInListeners) { + al.actionPerformed( + new ActionEvent(this, ZOOM_IN_ACTION_ID, ZOOM_IN_ACTION_NAME)); + } + } + else if (zoomOutRect.contains(e.getPoint()) && currentLevel > minLevel) { + currentLevel -= 1; + for (ActionListener al: zoomOutListeners) { + al.actionPerformed( + new ActionEvent(this, ZOOM_OUT_ACTION_ID, ZOOM_OUT_ACTION_NAME)); + } + } + component.repaint(); + } + }); + } + + /** + * Add a zoom-in listener. + * + * @param listener Zoom-in listener to add to this MapZoomControls instance. + */ + public void addZoomInListener(ActionListener listener) { + this.zoomInListeners.add(listener); + } + + /** + * Add a zoom-out listener. + * + * @param listener Zoom-out listener to add to this MapZoomControls instance. + */ + public void addZoomOutListener(ActionListener listener) { + this.zoomOutListeners.add(listener); + } + + /** + * @return the current zoom level. + */ + public int getZoomLevel() { + return this.currentLevel; + } + + /** + * Set the current zoom level without requesting a redraw. + * + * @param level Zoom level to set. + */ + public void setZoomLevel(int level) { + this.currentLevel = level; + } + + /** + * @return Height of this "component" when drawn. + */ + public int getHeight() { + return DEFAULT_HEIGHT; + } + + /** + * @return Width of this "component" when drawn. + */ + public int getWidth() { + return DEFAULT_HEIGHT + 2 + (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1 + 2 + + DEFAULT_HEIGHT; + } + + /** + * Check if a point is contained inside an element of this zoom controls, useful + * to avoid spurious click listeners. + * + * @param point Point to check. + * + * @return true if the given point correspond to an element of this zoom + * controls. + */ + public boolean contains(Point point) { + return zoomInRect.contains(point) || zoomOutRect.contains(point); + } + + protected void draw(Graphics2D g, int xoffset, int yoffset, ImageObserver observer) { + + int height = getHeight(); + + // Draw icon + g.drawImage(zoomOut, xoffset, yoffset, observer); + zoomOutRect.setBounds(xoffset, yoffset, DEFAULT_HEIGHT, DEFAULT_HEIGHT); + + g.setStroke(new BasicStroke(1)); + + // Draw ticks + xoffset += DEFAULT_HEIGHT + 2; + g.setColor(ZOOM_TICK_COLOR); + g.drawLine(xoffset, yoffset + height / 2, + xoffset + (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1, + yoffset + height / 2); + for (int i = 0; i <= (this.maxLevel - this.minLevel); i += halfMark ? 2 : 1) { + g.drawLine(xoffset + i * DEFAULT_SPACING, + yoffset + (int) (height * (1 - ZOOM_TICK_HEIGHT_RATIO) / 2), + xoffset + i * DEFAULT_SPACING, + yoffset + (int) (height * (1 + ZOOM_TICK_HEIGHT_RATIO) / 2)); + } + + // Draw current ticks + g.setColor(CURRENT_ZOOM_TICK_COLOR); + g.drawLine(xoffset + (currentLevel - this.minLevel) * DEFAULT_SPACING, + yoffset + (int) (height * (1 - CURRENT_ZOOM_TICK_HEIGHT_RATIO) / 2), + xoffset + (currentLevel - this.minLevel) * DEFAULT_SPACING, + yoffset + (int) (height * (1 + CURRENT_ZOOM_TICK_HEIGHT_RATIO) / 2)); + + xoffset += (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1 + 2; + + g.drawImage(zoomIn, xoffset, yoffset, observer); + zoomInRect.setBounds(xoffset, yoffset, DEFAULT_HEIGHT, DEFAULT_HEIGHT); + + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/ZoomAndPanListener.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/ZoomAndPanListener.java new file mode 100644 index 0000000..fc43671 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/components/ZoomAndPanListener.java @@ -0,0 +1,177 @@ +package org.insa.graphs.gui.drawing.components; + +import java.awt.Component; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; + +public class ZoomAndPanListener implements MouseListener, MouseMotionListener, MouseWheelListener { + public static final int DEFAULT_MIN_ZOOM_LEVEL = -20; + public static final int DEFAULT_MAX_ZOOM_LEVEL = 10; + public static final double DEFAULT_ZOOM_MULTIPLICATION_FACTOR = 1.2; + + private Component targetComponent; + + private int zoomLevel = 0; + private int minZoomLevel = DEFAULT_MIN_ZOOM_LEVEL; + private int maxZoomLevel = DEFAULT_MAX_ZOOM_LEVEL; + private double zoomMultiplicationFactor = DEFAULT_ZOOM_MULTIPLICATION_FACTOR; + + private Point dragStartScreen; + private Point dragEndScreen; + private AffineTransform coordTransform = new AffineTransform(); + + public ZoomAndPanListener(Component targetComponent) { + this.targetComponent = targetComponent; + } + + public ZoomAndPanListener(Component targetComponent, int minZoomLevel, int maxZoomLevel, + double zoomMultiplicationFactor) { + this.targetComponent = targetComponent; + this.minZoomLevel = minZoomLevel; + this.maxZoomLevel = maxZoomLevel; + this.zoomMultiplicationFactor = zoomMultiplicationFactor; + } + + public void translate(double dx, double dy) { + coordTransform.translate(dx, dy); + targetComponent.repaint(); + } + + public void mouseClicked(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + dragStartScreen = e.getPoint(); + dragEndScreen = null; + } + + public void mouseReleased(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mouseMoved(MouseEvent e) { + } + + public void mouseDragged(MouseEvent e) { + moveCamera(e); + } + + public void mouseWheelMoved(MouseWheelEvent e) { + zoomCamera(e); + } + + private void moveCamera(MouseEvent e) { + try { + dragEndScreen = e.getPoint(); + Point2D.Float dragStart = transformPoint(dragStartScreen); + Point2D.Float dragEnd = transformPoint(dragEndScreen); + double dx = dragEnd.getX() - dragStart.getX(); + double dy = dragEnd.getY() - dragStart.getY(); + coordTransform.translate(dx, dy); + dragStartScreen = dragEndScreen; + dragEndScreen = null; + targetComponent.repaint(); + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } + + private void zoomCamera(MouseWheelEvent e) { + try { + int wheelRotation = e.getWheelRotation(); + Point p = e.getPoint(); + if (wheelRotation < 0) { + if (zoomLevel < maxZoomLevel) { + zoomLevel++; + Point2D p1 = transformPoint(p); + coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + } + else { + if (zoomLevel > minZoomLevel) { + zoomLevel--; + Point2D p1 = transformPoint(p); + coordTransform.scale(1 / zoomMultiplicationFactor, + 1 / zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + } + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } + + private Point2D.Float transformPoint(Point p1) throws NoninvertibleTransformException { + + AffineTransform inverse = coordTransform.createInverse(); + + Point2D.Float p2 = new Point2D.Float(); + inverse.transform(p1, p2); + return p2; + } + + public int getZoomLevel() { + return zoomLevel; + } + + public void setZoomLevel(int zoomLevel) { + this.zoomLevel = zoomLevel; + } + + public void zoomIn() { + try { + Point p = new Point(targetComponent.getWidth() / 2, targetComponent.getHeight() / 2); + zoomLevel++; + Point2D p1 = transformPoint(p); + coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } + + public void zoomOut() { + try { + Point p = new Point(targetComponent.getWidth() / 2, targetComponent.getHeight() / 2); + zoomLevel--; + Point2D p1 = transformPoint(p); + coordTransform.scale(1 / zoomMultiplicationFactor, 1 / zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } + + public AffineTransform getCoordTransform() { + return coordTransform; + } + + public void setCoordTransform(AffineTransform coordTransform) { + this.coordTransform = coordTransform; + } +} \ No newline at end of file diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerAutoScaling.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerAutoScaling.java new file mode 100644 index 0000000..6d6b097 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerAutoScaling.java @@ -0,0 +1,73 @@ +package org.insa.graphs.gui.drawing.overlays; + +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; + +import org.mapsforge.core.graphics.Canvas; +import org.mapsforge.core.model.BoundingBox; +import org.mapsforge.core.model.LatLong; +import org.mapsforge.core.model.Point; +import org.mapsforge.map.awt.graphics.AwtBitmap; +import org.mapsforge.map.layer.overlay.Marker; + +/** + * Class extending the default Mapsforge's {@link Marker} with auto-scaling. + * + * Mapsforge's Markers do not scale with zoom level, this class aims at + * correcting this. Internally, this image stores an {@link Image} instance and + * scale it when a redraw is requested. + * + * @see MarkerUtils#getMarkerForColor(java.awt.Color, java.awt.Color, + * org.insa.graphics.drawing.Drawing.AlphaMode) + * @see PaintUtils#getStrokeWidth(int, byte) + */ +public class MarkerAutoScaling extends Marker { + + // Original image. + private Image image; + + /** + * Create a new MarkerAutoScaling at the given position with the given image. + * + * @param latLong Initial position of the marker. + * @param image Image for this marker. + */ + public MarkerAutoScaling(LatLong latLong, Image image) { + super(latLong, null, 0, 0); + this.image = image; + } + + /** + * Set a new image for this marker overlay + * + * @param image New image to set. + */ + public void setImage(Image image) { + this.image = image; + } + + /** + * @return Current image (marker) of this overlay. + */ + public Image getImage() { + return image; + } + + @Override + public synchronized void draw(BoundingBox boundingBox, byte zoomLevel, Canvas canvas, + Point topLeftPoint) { + int width = (int) PaintUtils.getStrokeWidth(8, zoomLevel), + height = (int) PaintUtils.getStrokeWidth(16, zoomLevel); + BufferedImage bfd = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); + Graphics2D g = bfd.createGraphics(); + g.drawImage( + this.image.getScaledInstance(bfd.getWidth(), bfd.getHeight(), Image.SCALE_SMOOTH), + 0, 0, null); + setBitmap(new AwtBitmap(bfd)); + + setVerticalOffset(-height / 2); + super.draw(boundingBox, zoomLevel, canvas, topLeftPoint); + g.dispose(); + } +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerOverlay.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerOverlay.java new file mode 100644 index 0000000..75b01b5 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerOverlay.java @@ -0,0 +1,19 @@ +package org.insa.graphs.gui.drawing.overlays; + +import org.insa.graphs.model.Point; + +public interface MarkerOverlay extends Overlay { + + /** + * @return The current position of this marker. + */ + public Point getPoint(); + + /** + * Move this marker to the specified location. + * + * @param point New position for the marker. + */ + public void moveTo(Point point); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerUtils.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerUtils.java new file mode 100644 index 0000000..523d5d9 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/MarkerUtils.java @@ -0,0 +1,97 @@ +package org.insa.graphs.gui.drawing.overlays; + +import java.awt.Color; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.DataInputStream; + +import org.insa.graphs.gui.drawing.Drawing.AlphaMode; + +public class MarkerUtils { + + /** + * Create an image to represent a marker using the given color for the outer and + * inner part, and the given mode for the inner part. + * + * @param outer Outer color of the marker. + * @param inner Inner color of the marker. + * @param mode Mode to use to fill the inner part of the marker. + * + * @return An image representing a marker. + */ + public static Image getMarkerForColor(Color outer, Color inner, AlphaMode mode) { + // create image + int[][] mask = readMarkerMask(); + BufferedImage image = new BufferedImage(mask[0].length, mask.length, + BufferedImage.TYPE_4BYTE_ABGR); + + // Color[] map = getColorMapping(color); + int outerRGB = outer.getRGB() & 0x00ffffff; + for (int i = 0; i < image.getHeight(); ++i) { + for (int j = 0; j < image.getWidth(); ++j) { + + // If we are in the "inner" part of the marker... + if (i >= MIN_Y_CENTER && i < MAX_Y_CENTER && j >= MIN_X_CENTER && j < MAX_X_CENTER + && mask[i][j] != MAXIMUM_INNER_MASK_VALUE) { + // Don't ask... https://stackoverflow.com/a/29321264/2666289 + // Basically, this compute a "middle" color between outer and inner depending on + // the current mask value. + double t = 1 - (mask[i][j] - MINIMUM_INNER_MASK_VALUE) + / (double) (MAXIMUM_INNER_MASK_VALUE - MINIMUM_INNER_MASK_VALUE); + int r = (int) Math.sqrt((1 - t) * outer.getRed() * outer.getRed() + + t * inner.getRed() * inner.getRed()); + int g = (int) Math.sqrt((1 - t) * outer.getGreen() * outer.getGreen() + + t * inner.getGreen() * inner.getGreen()); + int b = (int) Math.sqrt((1 - t) * outer.getBlue() * outer.getBlue() + + t * inner.getBlue() * inner.getBlue()); + int a = mode == AlphaMode.OPAQUE ? MAXIMUM_INNER_MASK_VALUE : mask[i][j]; + image.setRGB(j, i, (a << 24) | (r << 16) | (g << 8) | b); + } + // Otherwize, just fill with the outer color and set the alpha value properly. + else { + image.setRGB(j, i, outerRGB | (mask[i][j] << 24)); + } + } + } + + return image; + } + + // Mask cache + private static int[][] MASK_CACHE = null; + + // Hand-made... These defines the "center" of the marker, that can be filled + // with a different color. + private static final int MIN_X_CENTER = 40, MAX_X_CENTER = 101, MIN_Y_CENTER = 40, + MAX_Y_CENTER = 100; + private static final int MINIMUM_INNER_MASK_VALUE = 116, MAXIMUM_INNER_MASK_VALUE = 249; + + /** + * @return Retrieve the mask from the mask file or from the cache. + */ + private static int[][] readMarkerMask() { + if (MASK_CACHE == null) { + try { + DataInputStream dis = new DataInputStream( + MarkerUtils.class.getResourceAsStream("/marker_mask.bin")); + + int nrows = dis.readInt(); + int ncols = dis.readInt(); + + MASK_CACHE = new int[nrows][ncols]; + for (int i = 0; i < nrows; ++i) { + for (int j = 0; j < ncols; ++j) { + MASK_CACHE[i][j] = dis.readUnsignedByte(); + } + } + dis.close(); + } + catch (Exception e) { + e.printStackTrace(); + MASK_CACHE = null; + } + } + return MASK_CACHE; + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/Overlay.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/Overlay.java new file mode 100644 index 0000000..8815c57 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/Overlay.java @@ -0,0 +1,42 @@ +package org.insa.graphs.gui.drawing.overlays; + +import java.awt.Color; + +public interface Overlay { + + /** + * Set the color of this overlay. + * + * @param color New color for the overlay. + */ + public void setColor(Color color); + + /** + * @return The current color of this overlay. + */ + public Color getColor(); + + /** + * Show or hide this marker - A marker should be visible when created. + * + * @param visible true to show the marker, false to hide. + */ + public void setVisible(boolean visible); + + /** + * @return true if this overlay is visible. + */ + public boolean isVisible(); + + /** + * Delete this marker. + */ + public void delete(); + + /** + * Request a redraw of this overlay - This can start a full redraw of the inner + * drawing. + */ + public void redraw(); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PaintUtils.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PaintUtils.java new file mode 100644 index 0000000..a2c4929 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PaintUtils.java @@ -0,0 +1,65 @@ +package org.insa.graphs.gui.drawing.overlays; + +import java.awt.Color; + +import org.mapsforge.core.graphics.GraphicFactory; +import org.mapsforge.map.awt.graphics.AwtGraphicFactory; +import org.mapsforge.map.layer.overlay.Marker; +import org.mapsforge.map.layer.overlay.Polyline; + +public class PaintUtils { + + // Graphic factory. + private static final GraphicFactory GRAPHIC_FACTORY = AwtGraphicFactory.INSTANCE; + + /** + * Convert the given AWT color to a mapsforge compatible color. + * + * @param color AWT color to convert. + * + * @return Integer value representing a corresponding mapsforge color. + */ + public static int convertColor(Color color) { + return GRAPHIC_FACTORY.createColor(color.getAlpha(), color.getRed(), color.getGreen(), + color.getBlue()); + } + + /** + * Convert the given mapsforge color to an AWT Color. + * + * @param color Integer value representing a mapsforge color. + * + * @return AWT color corresponding to the given value. + */ + public static Color convertColor(int color) { + return new Color(color, true); + } + + /** + * Compute an updated value for the given width at the given zoom level. This + * function can be used to automatically scale {@link Polyline} or + * {@link Marker} when zooming (which is not done by default in Mapsforge). + * + * @param width Original width to convert. + * @param zoomLevel Zoom level for which the width should be computed. + * + * @return Actual width at the given zoom level. + */ + public static float getStrokeWidth(int width, byte zoomLevel) { + float mul = 1; + if (zoomLevel < 6) { + mul = 1; + } + else if (zoomLevel < 10) { + mul += (zoomLevel - 5) * 0.5; + } + else if (zoomLevel < 13) { + mul = 3.5f; + } + else { + mul += 2 * (zoomLevel - 8) / 3; + } + return width * mul; + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PathOverlay.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PathOverlay.java new file mode 100644 index 0000000..9622967 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PathOverlay.java @@ -0,0 +1,5 @@ +package org.insa.graphs.gui.drawing.overlays; + +public interface PathOverlay extends Overlay { + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PointSetOverlay.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PointSetOverlay.java new file mode 100644 index 0000000..ec105a6 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PointSetOverlay.java @@ -0,0 +1,70 @@ +package org.insa.graphs.gui.drawing.overlays; + +import java.awt.Color; + +import org.insa.graphs.model.Point; + +public interface PointSetOverlay extends Overlay { + + /** + * Set the width of this overlay for future addPoint(). + * + * @param width New default width for this overlay. + */ + public void setWidth(int width); + + /** + * Set color and width for this overlay for future addPoint(). + * + * @param width New default width for this overlay. + * @param color New default color for this overlay. + */ + public void setWidthAndColor(int width, Color color); + + /** + * Add a new point using the current width and color. + * + * @param point Position of the point to add. + * + * @see #setWidth(int) + * @see #setColor(Color) + */ + public void addPoint(Point point); + + /** + * Set the current width and then add a new point. + * + * @param point Position of the point to add. + * @param width New default width for this overlay. + * + * @see #setWidth(int) + * @see PointSetOverlay#addPoint(Point) + */ + public void addPoint(Point point, int width); + + /** + * Set the current color and then add a new point. + * + * @param point Position of the point to add. + * @param color New default color for this overlay. + * + * @see #setColor(Color) + * @see PointSetOverlay#addPoint(Point) + */ + public void addPoint(Point point, Color color); + + /** + * Add a new point at the given location, with the given color and width, and + * update the current width and color. + * + * @param point Position of the point to add. + * @param width New default width for this overlay. + * @param color New default color for this overlay. + * + * @see #setWidth(int) + * @see #setColor(Color) + * @see PointSetOverlay#addPoint(Point) + */ + public void addPoint(Point point, int width, Color color); + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PolylineAutoScaling.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PolylineAutoScaling.java new file mode 100644 index 0000000..e18d0ae --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/drawing/overlays/PolylineAutoScaling.java @@ -0,0 +1,92 @@ +package org.insa.graphs.gui.drawing.overlays; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collection; + +import org.insa.graphs.model.Point; +import org.mapsforge.core.graphics.Canvas; +import org.mapsforge.core.graphics.GraphicFactory; +import org.mapsforge.core.graphics.Style; +import org.mapsforge.core.model.BoundingBox; +import org.mapsforge.core.model.LatLong; +import org.mapsforge.map.awt.graphics.AwtGraphicFactory; +import org.mapsforge.map.layer.overlay.Polyline; + +/** + * Class extending the default Mapsforge's {@link Polyline} with auto-scaling. + * + * Mapsforge's Polylines do not scale with zoom level, this class aims at + * correcting this. When a redraw is requested, the width of the line is + * recomputed for the current zoom level. + * + * @see PaintUtils#getStrokeWidth(int, byte) + */ +public class PolylineAutoScaling extends Polyline { + + // Graphic factory. + private static final GraphicFactory GRAPHIC_FACTORY = AwtGraphicFactory.INSTANCE; + + // Original width of the polyline. + private final int width; + + /** + * Create a new PolylineAutoScaling with the given width and color. + * + * @param width Original width of the line (independent of the zoom level). + * @param color Color of the line. + * + * @see PaintUtils#getStrokeWidth(int, byte) + */ + public PolylineAutoScaling(int width, Color color) { + super(GRAPHIC_FACTORY.createPaint(), GRAPHIC_FACTORY); + getPaintStroke().setColor(PaintUtils.convertColor(color)); + getPaintStroke().setStyle(Style.STROKE); + this.width = width; + } + + /** + * Set the color for this polyline. + * + * @param color New color for this polyline. + */ + public void setColor(Color color) { + getPaintStroke().setColor(PaintUtils.convertColor(color)); + } + + /** + * @return Color of this polyline. + */ + public Color getColor() { + return PaintUtils.convertColor(getPaintStroke().getColor()); + } + + /** + * @param point Point to add to this line. + */ + public void add(Point point) { + getLatLongs().add(new LatLong(point.getLatitude(), point.getLongitude())); + } + + /** + * @param points Points to add to this line. + */ + public void addAll(Collection points) { + ArrayList latlongs = new ArrayList<>(points.size()); + for (Point point: points) { + latlongs.add(new LatLong(point.getLatitude(), point.getLongitude())); + } + getLatLongs().addAll(latlongs); + } + + @Override + public synchronized void draw(BoundingBox boundingBox, byte zoomLevel, Canvas canvas, + org.mapsforge.core.model.Point topLeftPoint) { + + // Update paint stroke with width for level + this.getPaintStroke().setStrokeWidth(PaintUtils.getStrokeWidth(width, zoomLevel)); + + super.draw(boundingBox, zoomLevel, canvas, topLeftPoint); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/ShortestPathGraphicObserver.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/ShortestPathGraphicObserver.java new file mode 100644 index 0000000..12963b4 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/ShortestPathGraphicObserver.java @@ -0,0 +1,42 @@ +package org.insa.graphs.gui.observers; + +import java.awt.Color; + +import org.insa.graphs.algorithm.shortestpath.ShortestPathObserver; +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.overlays.PointSetOverlay; +import org.insa.graphs.model.Node; + +public class ShortestPathGraphicObserver implements ShortestPathObserver { + + // Drawing and Graph drawing + protected Drawing drawing; + protected PointSetOverlay psOverlay1, psOverlay2; + + public ShortestPathGraphicObserver(Drawing drawing) { + this.drawing = drawing; + psOverlay1 = drawing.createPointSetOverlay(1, Color.CYAN); + psOverlay2 = drawing.createPointSetOverlay(1, Color.BLUE); + } + + @Override + public void notifyOriginProcessed(Node node) { + // drawing.drawMarker(node.getPoint(), Color.RED); + } + + @Override + public void notifyNodeReached(Node node) { + psOverlay1.addPoint(node.getPoint()); + } + + @Override + public void notifyNodeMarked(Node node) { + psOverlay2.addPoint(node.getPoint()); + } + + @Override + public void notifyDestinationReached(Node node) { + // drawing.drawMarker(node.getPoint(), Color.RED); + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/WeaklyConnectedComponentGraphicObserver.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/WeaklyConnectedComponentGraphicObserver.java new file mode 100644 index 0000000..426e3ff --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/observers/WeaklyConnectedComponentGraphicObserver.java @@ -0,0 +1,42 @@ +package org.insa.graphs.gui.observers; + +import java.awt.Color; +import java.util.ArrayList; + +import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentObserver; +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.overlays.PointSetOverlay; +import org.insa.graphs.model.Node; + +public class WeaklyConnectedComponentGraphicObserver implements WeaklyConnectedComponentObserver { + + private static final Color[] COLORS = { Color.BLUE, Color.ORANGE, Color.GREEN, Color.YELLOW, + Color.RED }; + + // Drawing + Graph drawing + private PointSetOverlay grPoints; + + // Current index color + private int cindex = 0; + + public WeaklyConnectedComponentGraphicObserver(Drawing drawing) { + this.grPoints = drawing.createPointSetOverlay(); + this.grPoints.setWidth(1); + } + + @Override + public void notifyStartComponent(Node curNode) { + this.grPoints.setColor(COLORS[cindex]); + } + + @Override + public void notifyNewNodeInComponent(Node node) { + this.grPoints.addPoint(node.getPoint()); + } + + @Override + public void notifyEndComponent(ArrayList nodes) { + cindex = (cindex + 1) % COLORS.length; + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/simple/Launch.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/simple/Launch.java new file mode 100644 index 0000000..7be58b3 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/simple/Launch.java @@ -0,0 +1,73 @@ +package org.insa.graphs.gui.simple; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.FileInputStream; + +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import org.insa.graphs.gui.drawing.Drawing; +import org.insa.graphs.gui.drawing.components.BasicDrawing; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Path; +import org.insa.graphs.model.io.BinaryGraphReader; +import org.insa.graphs.model.io.GraphReader; +import org.insa.graphs.model.io.PathReader; + +public class Launch { + + /** + * Create a new Drawing inside a JFrame an return it. + * + * @return The created drawing. + * + * @throws Exception if something wrong happens when creating the graph. + */ + public static Drawing createDrawing() throws Exception { + BasicDrawing basicDrawing = new BasicDrawing(); + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + JFrame frame = new JFrame("BE Graphes - Launch"); + frame.setLayout(new BorderLayout()); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + frame.setSize(new Dimension(800, 600)); + frame.setContentPane(basicDrawing); + frame.validate(); + } + }); + return basicDrawing; + } + + public static void main(String[] args) throws Exception { + + // Visit these directory to see the list of available files on Commetud. + final String mapName = "/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Maps/insa.mapgr"; + final String pathName = "/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Paths/path_fr31insa_rangueil_r2.path"; + + // Create a graph reader. + final GraphReader reader = new BinaryGraphReader( + new DataInputStream(new BufferedInputStream(new FileInputStream(mapName)))); + + // TODO: Read the graph. + final Graph graph = null; + + // Create the drawing: + final Drawing drawing = createDrawing(); + + // TODO: Draw the graph on the drawing. + + // TODO: Create a PathReader. + final PathReader pathReader = null; + + // TODO: Read the path. + final Path path = null; + + // TODO: Draw the path. + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/ColorUtils.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/ColorUtils.java new file mode 100644 index 0000000..1676699 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/ColorUtils.java @@ -0,0 +1,21 @@ +package org.insa.graphs.gui.utils; + +import java.awt.Color; + +public class ColorUtils { + + private static final Color[] COLORS = { // List of available colors + new Color(57, 172, 115), // Forest (Green) + new Color(246, 67, 63), // Red + new Color(110, 56, 172), // Purple + new Color(53, 191, 179), // Cyan + new Color(219, 136, 48), // Orange / Brown + new Color(110, 110, 110), // Gray + new Color(56, 104, 172) // Blue + }; + + public static Color getColor(int i) { + return COLORS[i % COLORS.length]; + } + +} diff --git a/be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/FileUtils.java b/be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/FileUtils.java new file mode 100644 index 0000000..301cfc9 --- /dev/null +++ b/be-graphes-gui/src/main/java/org/insa/graphs/gui/utils/FileUtils.java @@ -0,0 +1,151 @@ +package org.insa.graphs.gui.utils; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.EnumMap; +import java.util.Map; +import java.util.prefs.Preferences; + +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; + +public class FileUtils { + + // Preferences + private static Preferences preferences = Preferences.userRoot().node(FileUtils.class.getName()); + + /** + * Type of folder with associated preferred folder and path filters. + * + */ + public enum FolderType { + + /** + * Folder type for graph files input (*.mapgr). + */ + Map, + + /** + * Folder type for path inputs (*.path). + */ + PathInput, + + /** + * Folder type for path outputs (*.path). + */ + PathOutput + } + + private static class PreferencesEntry { + public String key; + public String value; + + public PreferencesEntry(String key, String value) { + this.key = key; + this.value = value; + } + } + + // Map folder type -> PreferencesEntry + private static final Map folderToEntry = new EnumMap<>( + FolderType.class); + + // Map folder type -> File Filter + private static final Map folderToFilter = new EnumMap<>( + FolderType.class); + + static { + // Populate folderToEntry + folderToEntry.put(FolderType.Map, new PreferencesEntry("DefaultMapFolder", + "/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Maps")); + folderToEntry.put(FolderType.PathInput, new PreferencesEntry("DefaultPathInputFolder", + "/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Paths")); + folderToEntry.put(FolderType.PathOutput, + new PreferencesEntry("DefaultPathOutputsFolder", "paths")); + + // Populate folderToFilter + folderToFilter.put(FolderType.Map, new FileNameExtensionFilter("Graph files", "mapgr")); + folderToFilter.put(FolderType.PathInput, new FileNameExtensionFilter("Path files", "path")); + folderToFilter.put(FolderType.PathOutput, + new FileNameExtensionFilter("Path files", "path")); + } + + /** + * @param folderType Type of folder to retrieve. + * + * @return A File instance pointing to the preferred folder for the given type. + * + * @see FolderType + */ + public static File getPreferredFolder(FolderType folderType) { + PreferencesEntry entry = folderToEntry.get(folderType); + File folder = new File(preferences.get(entry.key, entry.value)); + if (!folder.exists()) { + folder = new File(System.getProperty("user.dir")); + } + return folder; + } + + /** + * @param folderType Type of folder to update. + * @param newPreferredFolder New preferred folder. + */ + public static void updatePreferredFolder(FolderType folderType, File newPreferredFolder) { + PreferencesEntry entry = folderToEntry.get(folderType); + preferences.put(entry.key, newPreferredFolder.getAbsolutePath()); + } + + /** + * @param folderType Type of folder for which the filter should be retrieved. + * + * @return A FileFilter corresponding to input graph files. + */ + public static FileFilter getFileFilter(FolderType folderType) { + return folderToFilter.get(folderType); + } + + /** + * @param folderType Type of folder for which a file chooser should be created. + * @param defaultFileName Default file name to show, or null to not show any + * file. + * + * @return A new JFileChooser pointing to the preferred folder for the given + * folderType, with the given default file selected (if given). + */ + public static JFileChooser createFileChooser(FolderType folderType, String defaultFileName) { + JFileChooser chooser = new JFileChooser(); + chooser.setCurrentDirectory(getPreferredFolder(folderType)); + if (defaultFileName != null) { + chooser.setSelectedFile(new File(chooser.getCurrentDirectory().getAbsolutePath() + + File.separator + defaultFileName)); + } + chooser.setFileFilter(getFileFilter(folderType)); + chooser.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (e.getActionCommand().equals(JFileChooser.APPROVE_SELECTION)) { + if (chooser.getSelectedFile().exists()) { + updatePreferredFolder(folderType, + chooser.getSelectedFile().getParentFile()); + } + } + } + }); + return chooser; + } + + /** + * @param folderType Type of folder for which a file chooser should be created. + * + * @return A new JFileChooser pointing to the preferred folder for the given + * folderType. + * + * @see #createFileChooser(FolderType, String) + */ + public static JFileChooser createFileChooser(FolderType folderType) { + return createFileChooser(folderType, null); + } + +} diff --git a/be-graphes-gui/src/main/resources/delete-icon.png b/be-graphes-gui/src/main/resources/delete-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b84f63557a0db23042a8679b028d6c198c78a0fc GIT binary patch literal 2118 zcmV-M2)Xx(P)CMLIng*pn%0eVe*ibCHVCF>^$-^6Yn#;>@Fw(RDO<;zuDUfu(v z-8>Aizyw=N18B?133TP>clDK&sJn|7tDd5wQ4Ila7+`@3HW*_P0K%OG1y}kC3sp~g zy6Q+xRecK*&ScRaQ#{(}soq0G3 zQ>KL1(nG8C;yQr6si`Z<MS5Z+^LqH2nwDG{p!~!eKbyD5u-LSQc%XrYO=v$q`ptT4kKm0yFKL}G;lsQcahuX*~8vDqJb8g&YpGvFv1FRRR0hn&G+R<9oJK6=Eyq~ zKxcJWcXD$LA()$IFB)hy8T?>@2{ssEjme+F2$SwHF>02nG)Y}B)R{m4uy7;4%CFMK zA%wsh*0P7aXqY@;fCVPlV2tU_5C+@j^Yw#(-3#EHp=h94hj3YkU}~Nfft@} z^-vU`cl>^Jm?>)LIzut-6VeF9wewm#0JQPID=wc31GYsPI;5vrPoohQ8fYDo=F!F@ zt{;nUkeiP!GNpr-)gQpAVU-;KT4=8GdP~N{_X33QwoL1QW%UPe(pA|3pn(?6kEt(4 z)(Y@jGPV8E0L`3mS?pmi8fcBlZ-zD{CC%C8^WEm&yvAj*hrMWw*{{Z`CQlAEWMtf~ z*LP|^4Y63u9`;sGn>IEBsLIYRxgZZR{+vBKJkeu;0Qc%cD%P@xy=b5{rUE!Dz-Qz^ z#vh?j_$XJ&pRC6N0awKy_M(9nn(-ch-MmcgR(5vyI3JZpnCNhdy=b6?W-LO8dh?A; zUV!-=ACU&QSC6F7KnqQ@@rcU+Dzmam8facVw@mM_eqK=JP}O#m=5<;NO|gL>mvhq7_0^ny(k2-vx|m9pu*7vK;HERJGJj zX=t~(3dl;$0@OCuvQ0P)W(}Nu)qXcOas`hoBvG=4@mmu0$OQV zj&)Es@f8m;jLqA{8rHIhy=a&`V1NZC*kFuF00^Jd&0{QIpL4tFP<8-c%DVLq&6`4C z4|{b0MlV=c1KUZLAq=_s6LaRM9x=dhk8XaSLjl7P{etH2yU)}hXbO1(g*jNKA%sYd@2O|ogoXN|P z>dwjv8|?P``OG<1h17nj&jsW)nciyMd?bYiT48dgiP`gBUuNXKL=KG{R6VO+UlXqWep@}vgc$rvWg}F5|Q_;iDK;R+Q0c=iA zE?A$Oe0`gwU#3m>I25K#=2HQD#W49uPXgL_RN!ggffZ)h*Ci)kqZii!P-{G%@z18D zbn=f5$`IGWcG-rX2-sRf^U(?c54_3^JQU2ODJfl#c|3uLeh5D9@ua>WA?)z^`b~CI z`g$+TifDRM|yB%VtQ_6t*}>r3TrEK(-R|u=}v$7%B1cXn#pbK9BRikO(O*A43PI>{0-ZrDEam`)JoGH~@5)60 zM{oE!u^~U1JEmm__kEm8v1-TR_upLTvO|kW^5-1H8K19c>?qz#o;uOfUw@|iC}XI@4QM^(woGiLqRT@zuB zp_C!cH<9Ao2Z;MA5ac>3aOTn#+1*JlFq(f1fO@oJ_1RCT$OZxxn4=wd-q znn6>)?JSHBUaCKX;|n|ARN09wDO{us8Y{cY8YfgYcP(emCT!2y%yOwF$E0#=6nn=Z z=;{q>;jhy+EX|M8e--5HM(>*Z=_$$-Xm|1q3ytA-W^&iHDC|JkYc~0;E{MnnwADTy z8pFSDRYHua*#?7_(b7yRV{T}Sura&^z1c^n&_fq&{^+SA(u%%?D(y z*BeeE(YuD_`*GbC{1`cQk?o`ioJ1nIjRaJCYzWXgE*0!u5QCFQTtE&(WL<}o#LaTx za99*hA~Af3k|EgDO=dT~q0*?&#Ni|o&rtmceABhEhbTB??RN~G-*GYx4;5t|BhFCm z$G}M>`cdYj+PreMS5eFxcSj4cOyuH-9fxrW$G!{=9W1}D&ojsT~Npe^?i3G+x zst>9Cn8CD`!Q;tb`vRtas}9{k^bp!>lZ?3QHD>N|=OF?QJP}7> z#(aWlVGpOo@jz+Nq$2Jb#I&%fX#}4A4sjG_%npnVc^c7Q!B>+A#`RUa!eDV35qTSf z!i>9(AhEoJh#Uc)r4xqdr19q!LSq8S`q~l~33X-?(cX7|HicB&^&_Hvsgq9-cn-S7 zQJ68iG4s18W8!$!GD4L%@eE_ECu75i@tr(3sffFtBNV>s@Bju+PC|fw#wcXfflG+m zh0_1T*+OIfff!EV0H;46k5ek*?|TUIfUZ9)7ICL{2~tMM})Sgi`@t6OgQK>#94px$dvkq_$3dUZ*mDIJ9;@G-$0{x(HM=X&LNd--v5cuqo3|? zBI`=ewXAo}ji(1Q<~8uBP|0(x^nc_fc74H?xuW|NGp05HsieA^Vf2l^WZqWs%u&_a zQ_3Dixr&wOW~Eh&b2pNfMY+?-Yx6}ZH2RJJ>qhoBs4;_bNS8q%rL~30Z07p)Om^zU zntp@@+cuh+X|@dkX@nAIqN%%pXf>^RbX3(}=r(2MRkLrB`Yc&2Gedta_-sm#;gVRh z^asF^nyyv0P;JD@AlPUDX*%v9rX`ND`TvB{Jtv;d_cEP^e<<}F$ouvqnIi$^EtL)H z`|0Qn5in7 zE-yjl5t}`V((zVuSmlut@z>t+k2+t8zEgyfeIT{-3bDE(Qg)Fz?ORtZkdj*ww=uHi zev0QNc9tT-0)8AQ-?VUp<$S?U97f0s76z+d_ooOI5BTJL7=0wV8iO)x9JO-Ca6gZX zLGav3oAuop)j0!=q4lr;U5lnygwiz@b%x)INID_n*|u8HK1Luyz&-`i!eDg_pvjFgeucy=La5Tj(;Z1Mh~S7 zhb-TF4zX~EsHl3>D(W861*{qu&FlCT4N@(H#Yt0vy$+hdFd-{_$vTU!Ra$AR~Xq`n8dz35^-UGD-G zA$TWtDF6@NV6Zj@*pqAOCOqtJe|LGVO`17Nx?TrZWAIuMge6%zINc1k{ohBVuZ|rB z#349pt#Db&i+2XqO_Km>%Rt{9%*kf5Dc>>MnRqi0%WGBU)*m|%`W1v}nr&pJ6majP z-1?!lFMvu@SjozFyu79SeJZcADk>jqR(^$57o+1q zYs*0Sy~K`#mOPWq@A)N_47n}e2|?wyBvH3ytF7Xq9nRYb+{g%rN#FRuxUE5$s9VHY z$qB1A>z1p0SPFf@wO+Bny6rzBe97~sI%Q-~!b8h-&XifJ!JbCr)bBtCtHqak@Z!YRcsdm#nk zOnpSmiVqrrYt1a!Dbe$!upiWfodv`6dnGs*Bd^RfVY;r1S%IPCxHBs>Ok%g!k`KX8 za@a+h78>O%q4&)cU9U(V4F^FO3n@wWx5ynC*cDQDiteE|MY^C{?s)_m+U;!vhV9mM zSwT|f{1Sv5;#|&XTzL{QQJ3r&97-7=_r<$p zEfth;3J`uHx)fbrqO^UdQj|tICZsO=G>i!TXB9KAKs=w@dLT{c1XUn}Ihl;vY`+yh zo0~ZlaNa%E>s4VlK3#*twhm#@EHF{Z?DvAT#mC(H7zpoJ1gH|5s%rAea-lLD8TWO? z{G9vLQWD`y)K)rH*_T>9kS2Z7gO#tW-%RW1S}n2_CO51jra;Hu8yYy%)Xo9Lca^oE4?B z+2AWDp?B69^;@AX&`|iz%C#8XjawjLB}ND~(ogFGL}J&SDX46tzOwW??sulXv{F71 zqnE*p0b?=M|53ca*qSuXsp>bg29}}UjEp(PNtj#Eav1+26VIX-MPPp$c3+aW`^EV+ jYy6R2OshJqa%Xq<_lu8MA@TT~ztq>47WnUL0d)TY)hWRr literal 0 HcmV?d00001 diff --git a/be-graphes-gui/src/main/resources/save-icon.png b/be-graphes-gui/src/main/resources/save-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..41add8704b80805a4da65726ffb0344001f01cff GIT binary patch literal 3054 zcmcgucTm$?7XBrW2ZUZ91jPUXhGr6iLIjjv0vH5ER7#{tks5l7Qj{X01c_3`P*ffY z*g%lbi*$5}LZnG(QbeVf&HHnAW_M=)*_nIhJKuNiH)rO~xp!_d-rAHO3WEXwz;AA5 zbO8WB>=XoWbFyJ0%4L}ioSynt`T$T(=G`Me*c|R^cEJh&A{7CElmGxbY>6}v03k>K zSabmZEENF$49Ksy(P0bAMtH8@Fu#*zlQ-Q$plvD4|Y>ilv zwrtxsnpIrD$8RZrp`LkbYejNHQkJ7VkxCZa_)B!Z$4Zd(b=-kW4&K3N*YI=m8?M-g z2>`-mR+q(pt6?vz$y8qIf?dL+?~_+Kbj?`^_~C_LujC@OkH?X{Q`1ptxqm!8R{?(c z!uq&|bLC@&Ra86g;A49Jt$esVogZ=}Snz|ET2_INb!}P{RLie2`$lVcvcE)ZIFHn= zEMJwa#{x5we=gZ8OQi=l&|&@|8&bu=<1G`s=TSSIkR&Ld3Bzvv*d6hMgwKhIIonH5 ziFFkSS=Hhmg3oAmEfPX2vmd+9x^gWJx`hp0|7{>Z35RyAIWN*wWW=1yMhtTX&?<2^ zq(ErjjH(EH<)++;E!HsPnjL4nmKsImQI8q|^G#A7rY596(s!MnWduBdTs^)%n)KemPv_ zTvBjKn6hJA9k)u%(>MqX(lyuzUC0z7BzF27`a&&tqhZ6X6F(Vtbh*sqs1z%?5WQ|I z0yJalNR!R_UkVp1?-CaW9+Q62o%gpgW_gNuo^C5Fw_H4+KCu_cauCc&<$FG4pt8FD zgs3clm@$Ffa0?YW-DHg>@9Q4&ohLb=^Cu@>23ue=<)rl0UPYts-fC*;?43H1ph}v` zmG5O$#vqMOibmdNe&H*Au>Bnk!u*8QS^-SU*FFFfLj~#|c!K9WDjhlIS)!unv^T_x zkFY_J6l4j^Qh^>6h~f%9(aDKV=lQ=B@`(x6>LUtFCKV53zTFZerW}14s0DI96jP9| za2Mep?=hQr6dh~GG*!JvOgESDy1$+|idtv<;^oV&a+Z_q=lvl%i54^)RkP8d&xZ2^ zG!ffHI`;!bCZ!g<6|y{|({jis)56Kg$va;s@F!-DzF(}jId=NC%$uprmU5ky_cogv z^uUfN>P0@1NIJUzX7-9FN$u3$*RB`sA=xVs2Fx7sa=M{@r3kFNkX9iHdoo!v_J`vD z;OV>7INUAuhQD5-Sia`MjrPDqM_!kcK+MeBme+8je?L(rnt0SK#jIRNSW4Zzd=SSC zwq&V$pyOc+Msl?H#)fVa0duA4NaMk)9BGBq1gx%FDD-fsyOkqWB=*Biftpd^Vnd59 zC6xs?*FOIPdNB*p1%2^QyX1$Vi@-C5Rvh$2MroF#U{a|ng;yOxx6riTuR{<@uLWp> zhAp#~LR~Dgqcjzj=*N}YuHP|%WYY{j@B}=*{7p#sSyDc*nqn*M8 zPbQP^qzWCvTF;nHZBEr`yQXed25*!p&YKwODd?T{FyWmOQCM#1Kg(#dSumnO0IeQn z`X$a}q1!+sr>&S3qXhpyO`z;wTwVR#Flw`9N4o*TFa4)tl-JBlqH-eqouQd5W>Eco zZl{aPGA6vjq$HHP`#jIU2yBEI?|ox-doZ^yQJs!n$+muUV#W0V5n9J{mGnagQD88n z(;dDb79{){c;;#NPV0Ddz?}~8<%Y@`zL`HXu94+gc?^nhaQsm64dDt9zQV5VK2TEzd{d*xzdp?tF0vvc3ZP= z`~zQE8+=SgJvgJWl0YWPZ6}+WZ@%EFm`s}VR3-Txh!;H{Ouqx@62ENJtR%NOQ>KLf z+x@PBR8Us%(}CBfL^>zNf(xgFB@E@~Bw01uqZ)LiC|#wMDbPgr0d@_-i?p6sMS=Xt zG@&bjPL89aK)~(l9}WV-8p`2C-fyhxi$G5uWNfs5RgeE(+ogggRYUSqXz+T;Zd6qm zvw|$GKU#~rgzKBue|T@@&arF$BA<4{t?QkVXSC1D&)y_ghEQ;$swhfr|Cm;Uk5=om zk2rgE;g3I`cG|2nh)L@IK*^qe5qJ`k>G8J#&ctl&66#rNb&_PP1F zX076FxLr8Y{^I(d93ZAzjQ(b!Z$>Gh3rG0$zt|GZ=3Kak3E=rM`F6tJ zl0YuL(`G@H57kkw)uGQ$WCem$AsE~ofqQ5?SSg^ED3c_V7sbgi%_Ax%0XM(LivWue zV_(JbCZ|sa7H{?yb2OYNAPb{T1_67z_lv`$BS%<07i{|afZ8hcKjZ#pNQ^rh)xtDn+ zJO*C=abz)sS3AmR^G4r$SJ#!M^#xTAVO!d7%|H6|AAatA|D8(=-8VYaWn^M&$p2|y z&r5P@u$>1xUtAEz=%|jqb13-1shZ_gs4C6=+A0u@FB9DeM0c!fkUJXyG!mtyhSX3) zVeHT-EPH93Rzae$NF+{sA?9BUH~rnbJR<(H0qbVd%r;2<-NDx1gBV5#atFe~!qmO| ee1csG0q*MlL7sUlIxzMoz}(o{sLH_Q_CEn<0#pD1 literal 0 HcmV?d00001 diff --git a/be-graphes-gui/src/main/resources/zoomIn.png b/be-graphes-gui/src/main/resources/zoomIn.png new file mode 100644 index 0000000000000000000000000000000000000000..52e80bd51f3722274b4eb34eba924c25b5c12177 GIT binary patch literal 3536 zcmV;>4KMPEP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D4P{A0K~#8N?VV|? zWknIkpF!C)42A#-!ebL;F*7C#Q2{5oBSHcaaRD?2G%@m3SptecSTsJ+xZyG&vMLjZ z;0FN*2%AerWic-3#Nfs@prWD>`2Q~V4)@)6m#$vU>C@*_Qkj>zeY&guRo$mgb$9jL zTzRyyRGJsRQT##ir^FYEe<1#uc%68?_`l+(`^?|`AHPwCvOL4H)InX;N!=Y~hQKQ& z&-=s+;x*zw_l^3D{*B2v3w7VzZ;Li*yQ7K`Sf=*gCB8uX3-ObY4Q+86+C~OsK_+DD zC;itHck~Xz*Fifo#Z#tjOF^AOid7`E$hgXKeIIPJztGjt(6e5qO8nUn731 zfrcM-2y{Rfbn0jZ0*CAOTMIDyq)`Fg(6OTt1m0P=;YS?;M>x3ig1NaB{o!%e*yuY_-ru&m{N`Fsuvi51(>vg-YzZg?F_%f@xcUailt4Z*+K?}CwTJ@F`?gw z`pn<_U!6gv!H5AyU{w??mo~)MU5CZca{p9YzZ733K0|zbzjwV({}R4^xp*rvkvo{b z`9FT63_X!&&g|DgUDO#Wm+SLj1!hIiF%^LKxZ1cud|Kal+a*AbI&lozpe@>THK%XG} zyeFdKPpGahiFeCg@iFotGqV3XA|7BnpDPwSOR(QXRQ3|p^{QNzHORH6%A$*=pyEww26S~E41e^fOvmhowF(P~aQ9UP%UzmkfX@q!DUq^He zbp$Z4eIm@v^o|J1{9JXsHVx)k){3s^9O?vMU+dSw)e)5Vis~pX^y%Ps=#1{6PO#dc ztt3SVP=p9i-2J0TBx_csU=X?nabDFbvsXUE_(aZ1oap)50W3nJiP)qP+FPxJ1UZ<& z?p2AT&1wWpz{a;@VH0eVlr~ZAtc>{z$;>sI&zGb~`BfeoNW(VRC>bpT8lNTCj;133 zMj@Zo=cfXg_mpqXXOH|2sRS#qUa$%|3-RXIm~hI)H`4d8o-HAy8ynME^?@1K`E)vL z74>8in-+Zp;qzwYEKR%LCn5lLzMqKLEDA9@^%df{C=#^05-{}bd{Mr(uJ;t+YwfmM zRPDji#|f}q5WUs2;a};w+>UP_F=FDQy(XITZm|Pc{IvFhZP}> zR`4uF|JxYdeyabAK7)>=519HKq+to#iw^TJJ|4=FZqD@08G3P=H`fE4VCypqU`sRk z^!~MKCFk}0k2nIzA(0&87GMj;RwuxgCVGoUBM5bXsg-IAvl+t@%ZTBS+cB+T<%~f^n*- zqC^J(du(9owBMNoHYrN}ZmjH__ z@dd-c6Il44(Tmf@l(z>;Txpn#79x4mQw& z(E)C3lI8odmh1qih)pb=c+vK8Zo#lTd}`+a*&%>UEH>Qm3C}X=UyTn3Z8NF~y3$7g ztA5zL7wYCXk|i%c@0yp@WWGenG7lhvxD%CdBbH_8;1i;WRJsFT z6YIv&{dwpEMhCdEiPo3QZEyfm<61g%?@0Mc#)rtg+eB)E17I7A1N1iVS`QsG^|VhL z9Kd>t=<7Iub^l)zzW~tl^~T;ovWBLS)yc8FIa^g#*}PUtwo}Ak5HAGs=k@$*qXRsq z@?VV5J^`1{i9ashJ+Vh&8_O$zwDi6Ea$O%HhMfAk)K{ww>l(MK{B@V(WDn_MQ*AcI zHkLIVY;RghTVSY!v_6~JrSRqx&_!1B&WISJ&G%T@4F%JpLL z5y@*3xXu84%FjT~R7IS@{26FXTb?Qq#M0}1EpmcSM>amyc?E*3 zq2QF)n{sKRWlh_qr{!4Y$I>&!UIXg^*j^|&x5c1+B@<`!b2MBGnYjJz#R+;pRUU7| zt+yDI<8tsulIz`Kjx&?f2mVF?qv#Kf;4tC+c0ij0d6E6AViHrWNM!gywPzW1>@5dd zH-xb1pozpcx1|R+aW&9-S!ZuUNVg?yykq2?A!J+!jKut*zXc@1}1;sBIAHV+J( zq$qU+aLs@|Y}>I(qPjG3i#tG5<^Z=FJMP3Zadab+2wFygE>Wb@;FGM7b^RaPPD~Vu zlwGRW=Q}_XMu6`cJ1h<`HC1evEV?-kz^A+Q4xyRJq8SBz(#4ayJa;N>63Bi7%j=r5 zGtqV^Im3v^wx?k>(!XVe>;Hy3C!+At! zD)G{&l}6Bhqw;J`%Z<3#(AqgEZ@M%JrAxauZae8@xo4-hV>UBl{ocCk2jhEP9?7vK zA5Y8q;z-M$xg2vLpYCZBZ0SWlqd?AA$C5zdaR(4{?ERdXu$=lCbY8FCKkydor_-R( zoG*`llaza4iW4vaj(udytM?5(w3+_?wS(i18?l3PIx&0nc@^Y*eLNc|*h9Zo%&O*J z#6J?}y12E<(*~xQ|0FNnOFvk@IR_5Ygp_hGm42}kHH7x~;pac$X?$K6*e(ijkFL)s zfa9!;B0)PVVfeP>aUQl&)Vq&)2XRuy#|a+RbIuW{cG`KY)+EeY?}<6K+B`Ovz5xiF z=6-RG*RjXOC{}&k!)(35b+~h4t0ZK-S!`ISdnHlI6mv-wuAx#o%5Ob;K6o$M?j2ix zV&m4$qoYp1QM8W6Bft`Gi1D#a^7V_o)rfTu`mhu5eu_3bEy$CWOP6&4Js+FwQxzT@ zEB2WX4xS-D&OucP+kkf1q-jakH4gjUQSU10yg(=56OoJ-ZKo3ifbXZ?z!9)TEwnn& z2~dFDL{smyuSK);jglsY>>(i|DbHeq+K6w>J0d7Srm|Go$7%y-O2gb;V7=dGoTJ>S zdZK^tTLIfz5AcZ>>dCCtb3Xt{qe~@b=q%~_tm=*8OG>?Masi4@JKsp*sA4Q%mN-Eq zfdh+Ap&f764w0ZLvXE+K?WNcYM}h94N8NMV*VAL(^4=p|36K5p39ElX{u z3$>?yCOw^X@BXwAf>$jVXHDBmy4xapJb)17Ai?5(S!@2BaHOMS^s?+fba^d+DcClG zn3-Dk-?;!VD07qQ{;-(IXD-AB6Ji>v?mA){cArvL6$( zZ;)5n-Qv_vJghf?F)%BN2=^{j=me7-i8TmD3>IIwUkB!Il0(M`(1bcL7!@5FCm_Sc0!+ZB6~r!FHZ8{dMs@`{w{l8bf-*RL;G;M=)h1vHaB1A3DXf^CG8wB=L&K# zrbPy1CW?m+9R(sl9Q;u6MdE2Edo2b^Q5M;d5m_4Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D3|>h@K~#8N?VV|; zWyKN44QSk=Ga#cTL1)}`Mx7^^2vNa(i^QM^YSckcGy;J`_4~>e7=5l;0o~BC&U`Bz|_TRw(EWo4_#LLht#qhHnA52>PqPUh69M8b;-$;gki3$CF zG{OAM|M6ShGY%MmRjX*Jv?0b`6}bP_Sbq`UEkd(&%O#R<4K4ZdoKaGss57gf#V#2*=i65#! zColuMM$-ie4gaC~zFB;PIJUuEK;Kl2ej~n3{7FJR!LV^e9Gs~$#a;iP`aMp(AR|;Z zmMlDG9!aD#SgxCqzC%UXNWVT783>-g*NJy&00`s`+$8=_WDLL*Y_le&#g`%~{-yf5 zOuS{*S`QX(JN@_7;+V0Hm(?H1E?LrMlIo3!#^){q#HJIgfxIWgdK|u~zBbPd6z=?3 z#{aYW+B-J|Bgn(2<@1po0Vl|2Cg5d^sVUUMnJP~guMq+4T)QB^bEY^(z=_D=hIK2- z+x`;~jQ^ba_*vb`R5<=GG}d1u;P#~Ys5SHGt_Vt8sXkg4`gCw6KI{0h37t8yK`nG) zpzutCzpX{`yaqj4;)G^CVT($MT=@{sC5qR^7d>Ai9H}qXLT(5!6TB0Ecl7rwcIFU$@^OHVb=|gZdY4vH5ZF1fVY4V-Tlabj za8;Pzu%sbetp0>b--o(r%<&BTx+~ZnbAk?G9oq0x(LDDpG}5pwKk}AH1SZ9ZV%4hNUOaL9tPQs#tx~Txl^}oP^V z>P6sjqs_4KdV@qJG12Nx%j(s=j4lL@_plqbo<)}!J7Q1#{9=OI5um!sDx>YN^)#Xa zhIOY(3H;P<`$+-rFggIX#^#em4w`kP>F|8?@4O_G%eOqfu{AcIBx?uMuc>`wV&~2N zJpg1|TlZ>U^GO;vde8#}{Ol0mAPdVZFfD!DdhzeGQ@;qrwsVUEOn$<%#{3WC#X)IC z#a2lWuztp4^IqB>8#ajOxq1)Jf2v{?$_s&?NSrSWcEsk3P=TseuEwVwpAd!eLx9gZ zYk{uaP=jKJ76+iYSgl`Ty#Q#I$H#& zxMHy-w(p&iX}O5~YK8JdfKqf8TlVq2FOl^*+9fAr zrILkJyHrK6u?;qIm7Qo{77oBh2j5B zx#1CDDLC1C1XwOe^%6*WS!&R8>wnJ#Q-Ie?prtxxs?R`d{D6JI;24#!}j&LVGbF({YiV2+Au`?~-;sX6E}6|q!rae&@(ur(nBBc&v~_-U~v zy~(0<)or=s-xuE^9!m+F#|_j=2(hMwagcHqM6M!7t~H&B`pvHbFpXi|-PKD8187SW zt|Wz4WKH8RB7GmqEotFKq%&=9{Q{Uw3rC-_F(gx~8pL9=j^+JkT5X4M+1ihz%vmUsaZ0J!W;wue zkaK5zZl3Y$eG-&3nm;4SecQKe~jXJ7?pKE~&o6txF>h?7d}D-`G6r!Fzq2De~` z4_Nu)JT2WjHXs}@U`-yKstAg)6|K-<1i&W5_}Hd*2t-Qa&C+^6(Nrhk`NWq+q3H;K ziSK(B__CXN`>H@?Te0sH#dQ=++^Z^K8!!%7fJq3Z$5gE+$ps+-^Layg^R5#Z_X~`dVT0O;Z_OtnI02Q)x>SZ9U9HvG2F
    FZ#(#29TwSwA8=!T9yt9mmX;1%_g&FQUtzJ$ENPF0TO%VdF! z8a)f0&@JS>sedh4FRW3QfkTt17|RJz^d_-$5@T*)Opym2&?Sc9=>v?xI!nSUV>}M} zV3GHJxwxuRN@9YF%*bxd-5=&CV4K~YKNB>LNO>%x#{&p~r|ZPK#I#VNzL4?y37KQv z_4M<&xG^WT@@BdsvJ>>dd{F%!C+6id7eW?fLbjMj@An0k>*k`YMGl``My>!7IlEa* z<;@he_ebf;n2e1Kp;5eHIy>#=86|9;zaN`mI-CjEXBy*v@fl+39Hejv`lN5hU@XRT zbylBtJoc8=X}FkUFI8tT0w1nGQZR{?G0NIfKbO=r4!opDGcRW>OLMx$yb0!S{*T{i zLtC!lTKb?bYY`?_SqHTPtFf2{$e}MnpW}(|8W+%syL0jhWmPy}3FdG9FEl3aGXAJG ztmn5nG|n_i6l)X^UHctOz@`&KIhymtF+!qEcN8!H3oz*%@tnK6!?OXpgF)x%AR(}_ zLyqvMbVcVvGY~km<&J>i4=Yw5jR1A*m>wKR@tCPHU9DKqwLz}U!u zEXagxg(^m1YmGbsF?0&-epa!PK00000NkvXX Hu0mjf*OWk2 literal 0 HcmV?d00001 diff --git a/be-graphes-model/pom.xml b/be-graphes-model/pom.xml new file mode 100644 index 0000000..6f62a74 --- /dev/null +++ b/be-graphes-model/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + org.insa.graphs + be-graphes-all + 0.0.1-SNAPSHOT + + + be-graphes-model + be-graphes-model + + diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/AccessRestrictions.java b/be-graphes-model/src/main/java/org/insa/graphs/model/AccessRestrictions.java new file mode 100644 index 0000000..78d8689 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/AccessRestrictions.java @@ -0,0 +1,236 @@ +package org.insa.graphs.model; + +import java.util.EnumMap; +import java.util.EnumSet; + +/** + *

    + * Class containing access restrictions for roads/arcs. + *

    + * + *

    + * This class maps transport modes to their restriction and provide interface + * based on EnumSet to query restrictions. + *

    + * + *

    + * To each transport is associated at most one restriction per road (no + * restriction corresponds to {@link AccessRestriction#UNKNOWN} but a road can + * have different restrictions for different modes. + *

    + * + */ +public class AccessRestrictions { + + /** + * Enumeration representing the available transport modes. + * + * @see OpenStreetMap + * reference for access modes. + */ + public enum AccessMode { + + /** + * Access mode corresponding to pedestrians. + */ + FOOT, + + /** + * Access mode corresponding to bicycles (non-motorized). + */ + BICYCLE, + + /** + * Access mode corresponding to small motorcycles (limited speed). + */ + SMALL_MOTORCYCLE, + + /** + * Access mode corresponding to agricultural vehicles. + */ + AGRICULTURAL, + + /** + * Access mode corresponding to motorcycles. + */ + MOTORCYCLE, + + /** + * Access mode corresponding to motorcars. + */ + MOTORCAR, + + /** + * Access mode corresponding to heavy transportation vehicles. + */ + HEAVY_GOODS, + + /** + * Access mode corresponding to public transport vehicles. + */ + PUBLIC_TRANSPORT; + + /** + * {@code EnumSet} containing all possible transport modes. + * + * + */ + public static final EnumSet ALL = EnumSet.allOf(AccessMode.class); + + /** + * {@code EnumSet} containing all vehicle transport modes. + * + */ + public static final EnumSet VEHICLE = EnumSet.range(AccessMode.BICYCLE, + AccessMode.PUBLIC_TRANSPORT); + + /** + * {@code EnumSet} containing all motorized vehicle transport modes. + * + */ + public static final EnumSet MOTOR_VEHICLE = EnumSet + .range(AccessMode.SMALL_MOTORCYCLE, AccessMode.PUBLIC_TRANSPORT); + } + + /** + * Possible restrictions for the roads/arcs. + * + * @see OpenStreetMap + * reference for access restrictions. + */ + public enum AccessRestriction { + + /** + * + */ + ALLOWED, + + /** + * + */ + FORBIDDEN, + + /** + * + */ + PRIVATE, + + /** + * + */ + DESTINATION, + + /** + * + */ + DELIVERY, + + /** + * + */ + CUSTOMERS, + + /** + * + */ + FORESTRY, + + /** + * + */ + UNKNOWN; + + /** + * {@code EnumSet} corresponding to restrictions that are not totally private. + * + */ + public static final EnumSet ALLOWED_FOR_SOMETHING = EnumSet.of( + AccessRestriction.ALLOWED, AccessRestriction.DESTINATION, + AccessRestriction.DESTINATION, AccessRestriction.DELIVERY, + AccessRestriction.CUSTOMERS, AccessRestriction.FORESTRY); + + } + + // Map mode -> restriction + private final EnumMap restrictions; + + /** + * Create new AccessRestrictions instances with unknown restrictions. + */ + public AccessRestrictions() { + this.restrictions = new EnumMap<>(AccessMode.class); + for (AccessMode mode: AccessMode.values()) { + this.restrictions.put(mode, AccessRestriction.UNKNOWN); + } + } + + /** + * Create a new AccessRestrictions instances with the given restrictions. + * + * @param restrictions Map of restrictions for this instance of + * AccessRestrictions. + */ + public AccessRestrictions(EnumMap restrictions) { + this.restrictions = restrictions; + } + + /** + * Retrieve the restriction corresponding to the given mode. + * + * @param mode Mode for which the restriction should be retrieved. + * + * @return Restriction for the given mode. + */ + public AccessRestriction getRestrictionFor(AccessMode mode) { + return restrictions.getOrDefault(mode, AccessRestriction.UNKNOWN); + } + + /** + * Check if the restriction associated with the given mode is one of the given + * restrictions. + * + * @param mode Mode for which to check the restrictions. + * @param restrictions List of queried restrictions for the mode. + * + * @return {@code true} if the restriction of the given mode is one of the given + * restrictions. + */ + public boolean isAllowedForAny(AccessMode mode, EnumSet restrictions) { + return restrictions.contains(getRestrictionFor(mode)); + } + + /** + * Check if the restriction for the given mode corresponds to the given + * restriction. + * + * @param mode Mode for which the restriction should be checked. + * @param restriction Restriction to check against. + * + * @return {@code true} if the restriction of the given mode corresponds to the + * given restriction. + */ + public boolean isAllowedFor(AccessMode mode, AccessRestriction restriction) { + return getRestrictionFor(mode).equals(restriction); + } + + /** + * Check if the restriction associated to each given mode is one of the + * restrictions. The restriction may not be the same for all modes. + * + * @param modes Modes for which restrictions should be checked. + * @param restrictions Set of wanted restrictions for the modes. + * + * @return {@code true} if all the given modes are allowed for any of the given + * restrictions. + */ + public boolean areAllAllowedForAny(EnumSet modes, + EnumSet restrictions) { + boolean allowed = true; + for (AccessMode mode: modes) { + allowed = allowed && isAllowedForAny(mode, restrictions); + } + return allowed; + } +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/Arc.java b/be-graphes-model/src/main/java/org/insa/graphs/model/Arc.java new file mode 100644 index 0000000..2bbed68 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/Arc.java @@ -0,0 +1,70 @@ +package org.insa.graphs.model; + +import java.util.List; + +/** + *

    + * Interface representing an arc in the graph. {@code Arc} is an interface and + * not a class to allow us to represent two-ways roads in a memory efficient + * manner (without having to duplicate attributes). + *

    + * + *

    + * Arc should never be created manually but always using the + * {@link Node#linkNodes(Node, Node, float, RoadInformation, java.util.ArrayList)} + * method to ensure proper instantiation of the {@link ArcForward} and + * {@link ArcBackward} classes. + *

    + * + */ +public abstract class Arc { + + /** + * @return Origin node of this arc. + */ + public abstract Node getOrigin(); + + /** + * @return Destination node of this arc. + */ + public abstract Node getDestination(); + + /** + * @return Length of this arc, in meters. + */ + public abstract float getLength(); + + /** + * Compute the time required to travel this arc if moving at the given speed. + * + * @param speed Speed to compute the travel time. + * + * @return Time (in seconds) required to travel this arc at the given speed (in + * kilometers-per-hour). + */ + public double getTravelTime(double speed) { + return getLength() * 3600.0 / (speed * 1000.0); + } + + /** + * Compute and return the minimum time required to travel this arc, or the time + * required to travel this arc at the maximum speed allowed. + * + * @return Minimum time required to travel this arc, in seconds. + * + * @see Arc#getTravelTime(double) + */ + public double getMinimumTravelTime() { + return getTravelTime(getRoadInformation().getMaximumSpeed()); + } + + /** + * @return Road information for this arc. + */ + public abstract RoadInformation getRoadInformation(); + + /** + * @return Points representing segments of this arc. + */ + public abstract List getPoints(); +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/ArcBackward.java b/be-graphes-model/src/main/java/org/insa/graphs/model/ArcBackward.java new file mode 100644 index 0000000..051fc34 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/ArcBackward.java @@ -0,0 +1,55 @@ +package org.insa.graphs.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Implementation of Arc that represents a "backward" arc in a graph, i.e., an + * arc that is the reverse of another one. This arc only holds a reference to + * the original arc. + * + */ +class ArcBackward extends Arc { + + // Original arc + private final Arc originalArc; + + /** + * Create a new backward arc which corresponds to the reverse arc of the given + * arc. + * + * @param originalArc Original forwarc arc corresponding to this backward arc. + */ + protected ArcBackward(Arc originalArc) { + this.originalArc = originalArc; + } + + @Override + public Node getOrigin() { + return this.originalArc.getDestination(); + } + + @Override + public Node getDestination() { + return this.originalArc.getOrigin(); + } + + @Override + public float getLength() { + return this.originalArc.getLength(); + } + + @Override + public RoadInformation getRoadInformation() { + return this.originalArc.getRoadInformation(); + } + + @Override + public List getPoints() { + List pts = new ArrayList<>(this.originalArc.getPoints()); + Collections.reverse(pts); + return pts; + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/ArcForward.java b/be-graphes-model/src/main/java/org/insa/graphs/model/ArcForward.java new file mode 100644 index 0000000..df5871e --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/ArcForward.java @@ -0,0 +1,68 @@ +package org.insa.graphs.model; + +import java.util.Collections; +import java.util.List; + +/** + * Implementation of Arc that represents a "forward" arc in a graph, this is the + * arc implementation that stores data relative to the arc. + * + */ +class ArcForward extends Arc { + + // Destination node. + private final Node origin, destination; + + // Length of the road (in meters). + private final float length; + + // Road information. + private final RoadInformation info; + + // Segments. + private final List points; + + /** + * Create a new ArcForward with the given attributes. + * + * @param origin Origin of this arc. + * @param dest Destination of this arc. + * @param length Length of this arc (in meters). + * @param roadInformation Road information for this arc. + * @param points Points representing this arc. + */ + protected ArcForward(Node origin, Node dest, float length, RoadInformation roadInformation, + List points) { + this.origin = origin; + this.destination = dest; + this.length = length; + this.info = roadInformation; + this.points = points; + } + + @Override + public Node getOrigin() { + return origin; + } + + @Override + public Node getDestination() { + return destination; + } + + @Override + public float getLength() { + return length; + } + + @Override + public RoadInformation getRoadInformation() { + return info; + } + + @Override + public List getPoints() { + return Collections.unmodifiableList(points); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/Graph.java b/be-graphes-model/src/main/java/org/insa/graphs/model/Graph.java new file mode 100644 index 0000000..42e3ecc --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/Graph.java @@ -0,0 +1,131 @@ +package org.insa.graphs.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + *

    + * Main graph class. + *

    + * + *

    + * This class acts as a object-oriented adjacency list for a graph, i.e., + * it holds a list of nodes and each node holds a list of its successors. + *

    + * + */ +public final class Graph { + + // Map identifier. + private final String mapId; + + // Map name + private final String mapName; + + // Nodes of the graph. + private final List nodes; + + // Graph information of this graph. + private final GraphStatistics graphStatistics; + + /** + * Create a new graph with the given ID, name, nodes and information. + * + * @param mapId ID of the map corresponding to this graph. + * @param mapName Name of the map corresponding to this graph. + * @param nodes List of nodes for this graph. + * @param graphStatistics Information for this graph. + */ + public Graph(String mapId, String mapName, List nodes, GraphStatistics graphStatistics) { + this.mapId = mapId; + this.mapName = mapName; + this.nodes = Collections.unmodifiableList(nodes); + this.graphStatistics = graphStatistics; + } + + /** + * @return The GraphStatistics instance associated with this graph. + */ + public GraphStatistics getGraphInformation() { + return this.graphStatistics; + } + + /** + * Fetch the node with the given ID. + * + * Complexity: O(1). + * + * @param id ID of the node to fetch. + * + * @return Node with the given ID. + */ + public Node get(int id) { + return this.nodes.get(id); + } + + /** + * @return Number of nodes in this graph. + */ + public int size() { + return this.nodes.size(); + } + + /** + * @return List of nodes in this graph (unmodifiable). + * + * @see Collections#unmodifiableList(List) + */ + public List getNodes() { + return this.nodes; + } + + /** + * @return ID of the map associated with this graph. + */ + public String getMapId() { + return mapId; + } + + /** + * @return Name of the map associated with this graph. + */ + public String getMapName() { + return mapName; + } + + /** + * @return Transpose graph of this graph. + */ + public Graph transpose() { + ArrayList trNodes = new ArrayList<>(nodes.size()); + for (Node node: nodes) { + trNodes.add(new Node(node.getId(), node.getPoint())); + } + for (Node node: nodes) { + Node orig = trNodes.get(node.getId()); + for (Arc arc: node.getSuccessors()) { + if (arc.getRoadInformation().isOneWay()) { + Node dest = trNodes.get(arc.getDestination().getId()); + dest.addSuccessor(new ArcBackward(new ArcForward(orig, dest, arc.getLength(), + arc.getRoadInformation(), arc.getPoints()))); + } + else if (arc instanceof ArcForward) { + Node dest = trNodes.get(arc.getDestination().getId()); + Arc newArc = new ArcForward(orig, dest, arc.getLength(), + arc.getRoadInformation(), arc.getPoints()); + dest.addSuccessor(new ArcBackward(newArc)); + orig.addSuccessor(newArc); + } + } + } + return new Graph("R/" + mapId, mapName, trNodes, graphStatistics); + } + + @Override + public String toString() { + return String.format("%s[id=%s, name=%s, #nodes=%d]", getClass().getCanonicalName(), + getMapId(), getMapName(), size()); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/GraphStatistics.java b/be-graphes-model/src/main/java/org/insa/graphs/model/GraphStatistics.java new file mode 100644 index 0000000..658492a --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/GraphStatistics.java @@ -0,0 +1,204 @@ +package org.insa.graphs.model; + +/** + *

    + * Utility class that stores some statistics of graphs that are not easy to + * access. + *

    + * + *

    + * This class is used to provide constant ({@code O(1)}) access to information + * in graph that do not change, and that usually require linear complexity to + * compute. + *

    + * + */ +public class GraphStatistics { + + /** + * Special value used to indicate that the graph has no maximum speed limit + * (some roads are not limited). + * + */ + public static final int NO_MAXIMUM_SPEED = -1; + + /** + * Class representing a bounding box for a graph (a rectangle that contains all + * nodes in the graph). + * + */ + public static class BoundingBox { + + private final Point topLeft, bottomRight; + + /** + * Create a new BoundingBox represented by the given top-left and bottom-right + * points. + * + * @param topLeft Top left corner of the bounding box. + * @param bottomRight Bottom right corner of the bounding box. + */ + public BoundingBox(Point topLeft, Point bottomRight) { + this.topLeft = topLeft; + this.bottomRight = bottomRight; + } + + /** + * @return Bottom-right point of this bounding box. + */ + public Point getBottomRightPoint() { + return bottomRight; + } + + /** + * @return Top-left point of this bounding box. + */ + public Point getTopLeftPoint() { + return topLeft; + } + + /** + * Create a new bounding box by extending the current one according to the given + * value for each side. + * + * @param left Extra size to add to the left of the box. + * @param top Extra size to add to the top of the box. + * @param right Extra size to add to the right of the box. + * @param bottom Extra size to add to the bottom of the box. + * + * @return New bounding box corresponding to an extension of the current one. + */ + public BoundingBox extend(float left, float top, float right, float bottom) { + return new BoundingBox( + new Point(this.topLeft.getLongitude() - left, this.topLeft.getLatitude() + top), + new Point(this.bottomRight.getLongitude() + right, + this.bottomRight.getLatitude() - bottom)); + } + + /** + * Create a new bounding box by extending the current one according by the given + * value on each side. + * + * @param size Extra size to add to each side of this box. + * + * @return New bounding box corresponding to an extension of the current one. + */ + public BoundingBox extend(float size) { + return this.extend(size, size, size, size); + } + + /** + * @param point Point to check + * + * @return true if this box contains the given point. + */ + public boolean contains(Point point) { + return this.bottomRight.getLatitude() <= point.getLatitude() + && this.topLeft.getLatitude() >= point.getLatitude() + && this.topLeft.getLongitude() <= point.getLongitude() + && this.bottomRight.getLongitude() >= point.getLongitude(); + } + + /** + * @param other Box to intersect. + * + * @return true if this box contains the given box. + */ + public boolean contains(BoundingBox other) { + return this.contains(other.bottomRight) && this.contains(other.topLeft); + } + + @Override + public String toString() { + return "BoundingBox(topLeft=" + this.topLeft + ", bottomRight=" + this.bottomRight + + ")"; + } + + } + + // Bounding box for this graph. + private final BoundingBox boundingBox; + + // Number of roads + private final int nbRoadOneWay, nbRoadTwoWays; + + // Maximum speed on this graph (in kmph). + private final int maximumSpeed; + + // Maximum length of any arc on this graph. + private final float maximumLength; + + /** + * Create a new GraphStatistics instance with the given value. + * + * @param boundingBox Bounding-box for the graph. + * @param nbRoadOneWay Number of one-way roads in the graph. + * @param nbRoadTwoWays Number of two-ways roads in the graph. + * @param maximumSpeed Maximum speed of any road of the graph. You can use + * {@link #NO_MAXIMUM_SPEED} to indicate that the graph has no maximum + * speed limit. + * @param maximumLength Maximum length of any arc of the graph. + */ + public GraphStatistics(BoundingBox boundingBox, int nbRoadOneWay, int nbRoadTwoWays, + int maximumSpeed, float maximumLength) { + this.boundingBox = boundingBox; + this.nbRoadOneWay = nbRoadOneWay; + this.nbRoadTwoWays = nbRoadTwoWays; + this.maximumLength = maximumLength; + this.maximumSpeed = maximumSpeed; + } + + /** + * @return The bounding box for this graph. + */ + public BoundingBox getBoundingBox() { + return this.boundingBox; + } + + /** + * @return Amount of one-way roads in this graph. + */ + public int getOneWayRoadCount() { + return this.nbRoadOneWay; + } + + /** + * @return Amount of two-ways roads in this graph. + */ + public int getTwoWaysRoadCount() { + return this.nbRoadTwoWays; + } + + /** + * @return Number of arcs in this graph. + * + * @see #getOneWayRoadCount() + * @see #getTwoWaysRoadCount() + */ + public int getArcCount() { + return getOneWayRoadCount() + 2 * getTwoWaysRoadCount(); + } + + /** + * @return true if this graph has a maximum speed limit, false otherwise. + */ + public boolean hasMaximumSpeed() { + return this.maximumLength != NO_MAXIMUM_SPEED; + } + + /** + * @return Maximum speed of any arc in the graph, or {@link #NO_MAXIMUM_SPEED} + * if some roads have no speed limitation. + */ + public int getMaximumSpeed() { + return this.maximumSpeed; + } + + /** + * @return Maximum length of any arc in the graph. + */ + public float getMaximumLength() { + return this.maximumLength; + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/Node.java b/be-graphes-model/src/main/java/org/insa/graphs/model/Node.java new file mode 100644 index 0000000..37f20b0 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/Node.java @@ -0,0 +1,159 @@ +package org.insa.graphs.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + *

    + * Class representing a Node in a {@link Graph}. + *

    + * + *

    + * This class holds information regarding nodes in the graph together with the + * successors associated to the nodes. + *

    + * + *

    + * Nodes are comparable based on their ID. + *

    + * + */ +public final class Node implements Comparable { + + /** + *

    + * Link the two given nodes with one or two arcs (depending on roadInformation), + * with the given attributes. + *

    + * + *

    + * If {@code roadInformation.isOneWay()} is {@code true}, only a forward arc is + * created (origin to destination) and added to origin. Otherwise, a + * corresponding backward arc is created and add to destination. + *

    + * + * @param origin Origin of the arc. + * @param destination Destination of the arc. + * @param length Length of the arc. + * @param roadInformation Information corresponding to the arc. + * @param points Points for the arc. + * + * @return The newly created forward arc (origin to destination). + */ + public static Arc linkNodes(Node origin, Node destination, float length, + RoadInformation roadInformation, ArrayList points) { + Arc arc = null; + if (roadInformation.isOneWay()) { + arc = new ArcForward(origin, destination, length, roadInformation, points); + origin.addSuccessor(arc); + } + else { + Arc d2o; + if (origin.getId() < destination.getId()) { + arc = new ArcForward(origin, destination, length, roadInformation, points); + d2o = new ArcBackward(arc); + } + else { + Collections.reverse(points); + d2o = new ArcForward(destination, origin, length, roadInformation, points); + arc = new ArcBackward(d2o); + } + origin.addSuccessor(arc); + destination.addSuccessor(d2o); + } + return arc; + } + + // ID of the node. + private final int id; + + // Point of this graph. + private final Point point; + + // Successors. + private final ArrayList successors; + + /** + * Create a new Node with the given ID corresponding to the given Point with an + * empty list of successors. + * + * @param id ID of the node. + * @param point Position of the node. + */ + public Node(int id, Point point) { + this.id = id; + this.point = point; + this.successors = new ArrayList(); + } + + /** + * Add a successor to this node. + * + * @param arc Arc to the successor. + */ + protected void addSuccessor(Arc arc) { + successors.add(arc); + } + + /** + * @return ID of this node. + */ + public int getId() { + return id; + } + + /** + * @return Number of successors of this node. + */ + public int getNumberOfSuccessors() { + return this.successors.size(); + } + + /** + * @return true if this node has at least one successor. + */ + public boolean hasSuccessors() { + return !this.successors.isEmpty(); + } + + /** + * @return List of successors of this node (unmodifiable list). + * + * @see Collections#unmodifiableList(List) + */ + public List getSuccessors() { + return Collections.unmodifiableList(this.successors); + } + + /** + * @return Location of this node. + */ + public Point getPoint() { + return point; + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (other instanceof Node) { + return getId() == ((Node) other).getId(); + } + return false; + } + + /** + * Compare the ID of this node with the ID of the given node. + * + * @param other Node to compare this node with. + * + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(Node other) { + return Integer.compare(getId(), other.getId()); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/Path.java b/be-graphes-model/src/main/java/org/insa/graphs/model/Path.java new file mode 100644 index 0000000..6ebdb73 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/Path.java @@ -0,0 +1,248 @@ +package org.insa.graphs.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + *

    + * Class representing a path between nodes in a graph. + *

    + * + *

    + * A path is represented as a list of {@link Arc} with an origin and not a list + * of {@link Node} due to the multi-graph nature (multiple arcs between two + * nodes) of the considered graphs. + *

    + * + */ +public class Path { + + /** + * Create a new path that goes through the given list of nodes (in order), + * choosing the fastest route if multiple are available. + * + * @param graph Graph containing the nodes in the list. + * @param nodes List of nodes to build the path. + * + * @return A path that goes through the given list of nodes. + * + * @throws IllegalArgumentException If the list of nodes is not valid, i.e. two + * consecutive nodes in the list are not connected in the graph. + * + * @deprecated Need to be implemented. + */ + public static Path createFastestPathFromNodes(Graph graph, List nodes) + throws IllegalArgumentException { + List arcs = new ArrayList(); + // TODO: + return new Path(graph, arcs); + } + + /** + * Create a new path that goes through the given list of nodes (in order), + * choosing the shortest route if multiple are available. + * + * @param graph Graph containing the nodes in the list. + * @param nodes List of nodes to build the path. + * + * @return A path that goes through the given list of nodes. + * + * @throws IllegalArgumentException If the list of nodes is not valid, i.e. two + * consecutive nodes in the list are not connected in the graph. + * + * @deprecated Need to be implemented. + */ + public static Path createShortestPathFromNodes(Graph graph, List nodes) + throws IllegalArgumentException { + List arcs = new ArrayList(); + // TODO: + return new Path(graph, arcs); + } + + /** + * Concatenate the given paths. + * + * @param paths Array of paths to concatenate. + * + * @return Concatenated path. + * + * @throws IllegalArgumentException if the paths cannot be concatenated (IDs of + * map do not match, or the end of a path is not the beginning of the + * next). + */ + public static Path concatenate(Path... paths) throws IllegalArgumentException { + if (paths.length == 0) { + throw new IllegalArgumentException("Cannot concatenate an empty list of paths."); + } + final String mapId = paths[0].getGraph().getMapId(); + for (int i = 1; i < paths.length; ++i) { + if (!paths[i].getGraph().getMapId().equals(mapId)) { + throw new IllegalArgumentException( + "Cannot concatenate paths from different graphs."); + } + } + ArrayList arcs = new ArrayList<>(); + for (Path path: paths) { + arcs.addAll(path.getArcs()); + } + Path path = new Path(paths[0].getGraph(), arcs); + if (!path.isValid()) { + throw new IllegalArgumentException( + "Cannot concatenate paths that do not form a single path."); + } + return path; + } + + // Graph containing this path. + private final Graph graph; + + // Origin of the path + private final Node origin; + + // List of arcs in this path. + private final List arcs; + + /** + * Create an empty path corresponding to the given graph. + * + * @param graph Graph containing the path. + */ + public Path(Graph graph) { + this.graph = graph; + this.origin = null; + this.arcs = new ArrayList<>(); + } + + /** + * Create a new path containing a single node. + * + * @param graph Graph containing the path. + * @param node Single node of the path. + */ + public Path(Graph graph, Node node) { + this.graph = graph; + this.origin = node; + this.arcs = new ArrayList<>(); + } + + /** + * Create a new path with the given list of arcs. + * + * @param graph Graph containing the path. + * @param arcs Arcs to construct the path. + */ + public Path(Graph graph, List arcs) { + this.graph = graph; + this.arcs = arcs; + this.origin = arcs.size() > 0 ? arcs.get(0).getOrigin() : null; + } + + /** + * @return Graph containing the path. + */ + public Graph getGraph() { + return graph; + } + + /** + * @return First node of the path. + */ + public Node getOrigin() { + return origin; + } + + /** + * @return Last node of the path. + */ + public Node getDestination() { + return arcs.get(arcs.size() - 1).getDestination(); + } + + /** + * @return List of arcs in the path. + */ + public List getArcs() { + return Collections.unmodifiableList(arcs); + } + + /** + * Check if this path is empty (it does not contain any node). + * + * @return true if this path is empty, false otherwise. + */ + public boolean isEmpty() { + return this.origin == null; + } + + /** + * Get the number of nodes in this path. + * + * @return Number of nodes in this path. + */ + public int size() { + return isEmpty() ? 0 : 1 + this.arcs.size(); + } + + /** + * Check if this path is valid. + * + * A path is valid if any of the following is true: + *
      + *
    • it is empty;
    • + *
    • it contains a single node (without arcs);
    • + *
    • the first arc has for origin the origin of the path and, for two + * consecutive arcs, the destination of the first one is the origin of the + * second one.
    • + *
    + * + * @return true if the path is valid, false otherwise. + * + * @deprecated Need to be implemented. + */ + public boolean isValid() { + // TODO: + return false; + } + + /** + * Compute the length of this path (in meters). + * + * @return Total length of the path (in meters). + * + * @deprecated Need to be implemented. + */ + public float getLength() { + // TODO: + return 0; + } + + /** + * Compute the time required to travel this path if moving at the given speed. + * + * @param speed Speed to compute the travel time. + * + * @return Time (in seconds) required to travel this path at the given speed (in + * kilometers-per-hour). + * + * @deprecated Need to be implemented. + */ + public double getTravelTime(double speed) { + // TODO: + return 0; + } + + /** + * Compute the time to travel this path if moving at the maximum allowed speed + * on every arc. + * + * @return Minimum travel time to travel this path (in seconds). + * + * @deprecated Need to be implemented. + */ + public double getMinimumTravelTime() { + // TODO: + return 0; + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/Point.java b/be-graphes-model/src/main/java/org/insa/graphs/model/Point.java new file mode 100644 index 0000000..bbb6770 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/Point.java @@ -0,0 +1,80 @@ +package org.insa.graphs.model; + +/** + * Class representing a point (position) on Earth. + * + */ +public final class Point { + + /** + * Approximated Earth radius (in meters). + */ + public static final double EARTH_RADIUS = 6378137.0; + + /** + * Compute the distance in meters between the two given points. + * + * @param p1 First point. + * @param p2 second point. + * + * @return Distance between the two given points (in meters). + */ + public static double distance(Point p1, Point p2) { + double sinLat = Math.sin(Math.toRadians(p1.getLatitude())) + * Math.sin(Math.toRadians(p2.getLatitude())); + double cosLat = Math.cos(Math.toRadians(p1.getLatitude())) + * Math.cos(Math.toRadians(p2.getLatitude())); + double cosLong = Math.cos(Math.toRadians(p2.getLongitude() - p1.getLongitude())); + + double koef = sinLat + cosLat * cosLong ; + + if (koef >= 1.0) { koef = 1.0 ; } + if (koef <= -1.0) { koef = -1.0 ; } + + return (EARTH_RADIUS * Math.acos(koef)) ; + } + + // Longitude and latitude of the point. + private final float longitude, latitude; + + /** + * Create a new point corresponding to the given (longitude, latitude) position. + * + * @param longitude Longitude of the point (in degrees). + * @param latitude Latitude of the point (in degrees). + */ + public Point(float longitude, float latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + /** + * @return Longitude of this point (in degrees). + */ + public float getLongitude() { + return longitude; + } + + /** + * @return Latitude of this point (in degrees). + */ + public float getLatitude() { + return latitude; + } + + /** + * Compute the distance from this point to the given point + * + * @param target Target point to compute distance to. + * + * @return Distance between this point and the target point, in meters. + */ + public double distanceTo(Point target) { + return distance(this, target); + } + + @Override + public String toString() { + return String.format("Point(%f, %f)", getLongitude(), getLatitude()); + } +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/RoadInformation.java b/be-graphes-model/src/main/java/org/insa/graphs/model/RoadInformation.java new file mode 100644 index 0000000..f5fa39c --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/RoadInformation.java @@ -0,0 +1,126 @@ +package org.insa.graphs.model; + +/** + *

    + * Class containing information for road that may be shared by multiple arcs. + *

    + * + *

    + * Sharing information between arcs reduces memory footprints of the program (a + * long road is often split into multiple arcs at each intersection). + *

    + * + */ +public class RoadInformation { + + /** + * Enumeration for road types. + * + * @see OpenStreetMap + * reference for road types. + */ + public enum RoadType { + MOTORWAY, + TRUNK, + PRIMARY, + SECONDARY, + MOTORWAY_LINK, + TRUNK_LINK, + PRIMARY_LINK, + SECONDARY_LINK, + TERTIARY, + TRACK, + RESIDENTIAL, + UNCLASSIFIED, + LIVING_STREET, + SERVICE, + ROUNDABOUT, + PEDESTRIAN, + CYCLEWAY, + COASTLINE + } + + // Type of the road (see above). + private final RoadType type; + + // Access information + private final AccessRestrictions access; + + // One way road? + private final boolean oneway; + + // Max speed in kilometers per hour. + private final int maxSpeed; + + // Name of the road. + private final String name; + + /** + * Create a new RoadInformation instance containing the given parameters. + * + * @param roadType Type of the road (see {@link RoadType}). + * @param access Access restrictions for the road (see + * {@link AccessRestrictions}). + * @param isOneWay true if this road is a one way road, false otherwise. + * @param maxSpeed Maximum speed for the road (in kilometers-per-hour). + * @param name Name of the road. + */ + public RoadInformation(RoadType roadType, AccessRestrictions access, boolean isOneWay, + int maxSpeed, String name) { + this.type = roadType; + this.access = access; + this.oneway = isOneWay; + this.maxSpeed = maxSpeed; + this.name = name; + } + + /** + * @return Access restrictions for this road. + */ + public AccessRestrictions getAccessRestrictions() { + return this.access; + } + + /** + * @return Type of the road. + */ + public RoadType getType() { + return type; + } + + /** + * @return true if the road is a one-way road. + */ + public boolean isOneWay() { + return oneway; + } + + /** + * @return Maximum speed for this road (in kilometers-per-hour). + */ + public int getMaximumSpeed() { + return maxSpeed; + } + + /** + * @return Name of the road. + */ + public String getName() { + return name; + } + + @Override + public String toString() { + String typeAsString = "road"; + if (getType() == RoadType.COASTLINE) { + typeAsString = "coast"; + } + if (getType() == RoadType.MOTORWAY) { + typeAsString = "highway"; + } + return typeAsString + " : " + getName() + " " + (isOneWay() ? " (oneway) " : "") + maxSpeed + + " km/h (max.)"; + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadFormatException.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadFormatException.java new file mode 100644 index 0000000..184debe --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadFormatException.java @@ -0,0 +1,33 @@ +package org.insa.graphs.model.io; + +import java.io.IOException; + +/** + * Exception thrown when a format-error is detected when reading a graph (e.g., + * non-matching check bytes). + * + */ +public class BadFormatException extends IOException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * + */ + public BadFormatException() { + super(); + } + + /** + * Create a new format exception with the given message. + * + * @param message Message for the exception. + */ + public BadFormatException(String message) { + super(message); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadMagicNumberException.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadMagicNumberException.java new file mode 100644 index 0000000..31d9f42 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadMagicNumberException.java @@ -0,0 +1,46 @@ +package org.insa.graphs.model.io; + +/** + * Exception thrown when there is a mismatch between expected and actual magic + * number. + * + */ +public class BadMagicNumberException extends BadFormatException { + + /** + * + */ + private static final long serialVersionUID = -2176603967548838864L; + + // Actual and expected magic numbers. + private int actualNumber, expectedNumber; + + /** + * Create a new BadMagicNumberException with the given expected and actual magic + * number. + * + * @param actualNumber Actual magic number (read from a file). + * @param expectedNumber Expected magic number. + */ + public BadMagicNumberException(int actualNumber, int expectedNumber) { + super(String.format("Magic number mismatch, expected %#X, got %#X.", expectedNumber, + actualNumber)); + this.actualNumber = actualNumber; + this.expectedNumber = expectedNumber; + } + + /** + * @return The actual magic number. + */ + public int getActualMagicNumber() { + return actualNumber; + } + + /** + * @return The expected magic number. + */ + public int getExpectedMagicNumber() { + return expectedNumber; + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadVersionException.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadVersionException.java new file mode 100644 index 0000000..892f534 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BadVersionException.java @@ -0,0 +1,42 @@ +package org.insa.graphs.model.io; + +/** + * Exception thrown when the version of the file is not at least the expected + * one. + * + */ +public class BadVersionException extends BadFormatException { + + /** + * + */ + private static final long serialVersionUID = 7776317018302386042L; + + // Actual and expected version.. + private int actualVersion, expectedVersion; + + /** + * + * @param actualVersion Actual version of the file. + * @param expectedVersion Expected version of the file. + */ + public BadVersionException(int actualVersion, int expectedVersion) { + super(); + this.actualVersion = actualVersion; + this.expectedVersion = expectedVersion; + } + + /** + * @return Actual version of the file. + */ + public int getActualVersion() { + return actualVersion; + } + + /** + * @return Expected (minimal) version of the file. + */ + public int getExpectedVersion() { + return expectedVersion; + } +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryGraphReader.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryGraphReader.java new file mode 100644 index 0000000..720f54f --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryGraphReader.java @@ -0,0 +1,323 @@ +package org.insa.graphs.model.io; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; + +import org.insa.graphs.model.AccessRestrictions; +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.GraphStatistics; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.Point; +import org.insa.graphs.model.RoadInformation; +import org.insa.graphs.model.AccessRestrictions.AccessMode; +import org.insa.graphs.model.AccessRestrictions.AccessRestriction; +import org.insa.graphs.model.GraphStatistics.BoundingBox; +import org.insa.graphs.model.RoadInformation.RoadType; + +/** + * Implementation of {@link GraphReader} to read graph in binary format. + * + */ +public class BinaryGraphReader extends BinaryReader implements GraphReader { + + // Map version and magic number targeted for this reader. + private static final int VERSION = 5; + private static final int MAGIC_NUMBER = 0x208BC3B3; + + // Length of the map id field (in bytes) + protected static final int MAP_ID_FIELD_LENGTH = 32; + + // List of observers + protected List observers = new ArrayList<>(); + + /** + * Parse the given long value into a new instance of AccessRestrictions. + * + * @param access The value to parse. + * + * @return New instance of access restrictions parsed from the given value. + */ + protected static AccessRestrictions toAccessInformation(final long access) { + + // See the following for more information: + // https://github.com/Holt59/OSM2Graph/blob/master/src/main/org/laas/osm2graph/model/AccessData.java + + // The order of values inside this array is VERY IMPORTANT: For allRestrictions, + // the order correspond to the 4 bits value (i.e. FORBIDDEN is 0 or PRIVATE is + // 2) - UKNOWN is not included because value above 6 (FORESTRY) are all + // considered unknown. + final AccessRestriction[] allRestrictions = new AccessRestriction[] { + AccessRestriction.FORBIDDEN, AccessRestriction.ALLOWED, AccessRestriction.PRIVATE, + AccessRestriction.DESTINATION, AccessRestriction.DELIVERY, + AccessRestriction.CUSTOMERS, AccessRestriction.FORESTRY }; + + // The order of values inside this array is VERY IMPORTANT: The order is such + // that each 4-bits group of the long value is processed in the correct order, + // i.e. FOOT is processed first (4 lowest bits), and so on. + final AccessMode[] allModes = new AccessMode[] { AccessMode.FOOT, null, AccessMode.BICYCLE, + AccessMode.SMALL_MOTORCYCLE, AccessMode.AGRICULTURAL, AccessMode.MOTORCYCLE, + AccessMode.MOTORCAR, AccessMode.HEAVY_GOODS, null, AccessMode.PUBLIC_TRANSPORT }; + + // fill maps... + EnumMap restrictions = new EnumMap<>(AccessMode.class); + long copyAccess = access; + for (AccessMode mode: allModes) { + if (mode == null) { + continue; // filling cells + } + int value = (int) (copyAccess & 0xf); + if (value < allRestrictions.length) { + restrictions.put(mode, allRestrictions[value]); + } + else { + restrictions.put(mode, AccessRestriction.UNKNOWN); + } + copyAccess = copyAccess >> 4; + } + + return new AccessRestrictions(restrictions); + } + + /** + * Convert a character to its corresponding road type. + * + * @param ch Character to convert. + * + * @return Road type corresponding to the given character. + * + */ + protected static RoadType toRoadType(char ch) { + switch (ch) { + case 'a': + return RoadType.MOTORWAY; + case 'b': + return RoadType.TRUNK; + case 'c': + return RoadType.PRIMARY; + case 'd': + return RoadType.SECONDARY; + case 'e': + return RoadType.MOTORWAY_LINK; + case 'f': + return RoadType.TRUNK_LINK; + case 'g': + return RoadType.PRIMARY_LINK; + case 'h': + return RoadType.SECONDARY_LINK; + case 'i': + return RoadType.TERTIARY; + case 'j': + return RoadType.RESIDENTIAL; + case 'k': + case 'l': + return RoadType.UNCLASSIFIED; + case 'm': + return RoadType.LIVING_STREET; + case 'n': + return RoadType.SERVICE; + case 'o': + return RoadType.ROUNDABOUT; + case 'p': + return RoadType.PEDESTRIAN; + case 'r': + return RoadType.CYCLEWAY; + case 's': + return RoadType.TRACK; + case 'z': + return RoadType.COASTLINE; + } + return RoadType.UNCLASSIFIED; + } + + /** + * Create a new BinaryGraphReader that read from the given input stream. + * + * @param dis Input stream to read from. + */ + public BinaryGraphReader(DataInputStream dis) { + super(MAGIC_NUMBER, VERSION, dis); + } + + @Override + public void addObserver(GraphReaderObserver observer) { + observers.add(observer); + } + + @Override + public Graph read() throws IOException { + + // Read and check magic number and file version. + checkMagicNumberOrThrow(dis.readInt()); + checkVersionOrThrow(dis.readInt()); + + // Read map id. + String mapId; + String mapName = ""; + + if (getCurrentVersion() < 6) { + mapId = "0x" + Integer.toHexString(dis.readInt()); + } + else { + mapId = readFixedLengthString(MAP_ID_FIELD_LENGTH, "UTF-8"); + mapName = dis.readUTF(); + } + + observers.forEach((observer) -> observer.notifyStartReading(mapId)); + + // Number of descriptors and nodes. + int nbDesc = dis.readInt(); + int nbNodes = dis.readInt(); + + // Number of successors for each nodes. + int[] nbSuccessors = new int[nbNodes]; + int nbTotalSuccessors = 0; + + // Construct an array list with initial capacity of nbNodes. + ArrayList nodes = new ArrayList(nbNodes); + + // Read nodes. + float minLongitude = Float.POSITIVE_INFINITY, minLatitude = Float.POSITIVE_INFINITY, + maxLongitude = Float.NEGATIVE_INFINITY, maxLatitude = Float.NEGATIVE_INFINITY; + observers.forEach((observer) -> observer.notifyStartReadingNodes(nbNodes)); + for (int node = 0; node < nbNodes; ++node) { + // Read longitude / latitude. + float longitude = ((float) dis.readInt()) / 1E6f; + float latitude = ((float) dis.readInt()) / 1E6f; + + // Update minimum / maximum. + minLongitude = Math.min(longitude, minLongitude); + minLatitude = Math.min(latitude, minLatitude); + maxLongitude = Math.max(longitude, maxLongitude); + maxLatitude = Math.max(latitude, maxLatitude); + + // Update information. + nbSuccessors[node] = dis.readUnsignedByte(); + nbTotalSuccessors += nbSuccessors[node]; + + // Create node. + final Node aNode = new Node(node, new Point(longitude, latitude)); + nodes.add(aNode); + observers.forEach((observer) -> observer.notifyNewNodeRead(aNode)); + } + + // Check format. + checkByteOrThrow(255); + + // Read descriptors. + RoadInformation[] descs = new RoadInformation[nbDesc]; + + // Read + observers.forEach((observer) -> observer.notifyStartReadingDescriptors(nbDesc)); + int maxSpeed = 0; + for (int descr = 0; descr < nbDesc; ++descr) { + final RoadInformation roadinf = readRoadInformation(); + descs[descr] = roadinf; + observers.forEach((observer) -> observer.notifyNewDescriptorRead(roadinf)); + + // Update max speed + maxSpeed = Math.max(roadinf.getMaximumSpeed(), maxSpeed); + } + + // Check format. + checkByteOrThrow(254); + + // Read successors and convert to arcs. + float maxLength = 0; + final int copyNbTotalSuccesors = nbTotalSuccessors; // Stupid Java... + int nbOneWayRoad = 0; + observers.forEach((observer) -> observer.notifyStartReadingArcs(copyNbTotalSuccesors)); + for (int node = 0; node < nbNodes; ++node) { + for (int succ = 0; succ < nbSuccessors[node]; ++succ) { + + // Read target node number. + int destNode = this.read24bits(); + + // Read information number. + int descrNum = this.read24bits(); + + // Length of the arc. + float length; + if (getCurrentVersion() < 8) { + length = dis.readUnsignedShort(); + } + else { + length = dis.readInt() / 1000.0f; + } + maxLength = Math.max(length, maxLength); + + length = Math.max(length, (float) Point.distance(nodes.get(node).getPoint(), + nodes.get(destNode).getPoint())); + + // Number of segments. + int nbSegments = dis.readUnsignedShort(); + + // Chain of points corresponding to the segments. + ArrayList points = new ArrayList(nbSegments + 2); + points.add(nodes.get(node).getPoint()); + + for (int seg = 0; seg < nbSegments; ++seg) { + Point lastPoint = points.get(points.size() - 1); + + float dlon = (dis.readShort()) / 2.0e5f; + float dlat = (dis.readShort()) / 2.0e5f; + + points.add(new Point(lastPoint.getLongitude() + dlon, + lastPoint.getLatitude() + dlat)); + } + + points.add(nodes.get(destNode).getPoint()); + + RoadInformation info = descs[descrNum]; + Node orig = nodes.get(node); + Node dest = nodes.get(destNode); + + // Add successor to initial arc. + Arc arc = Node.linkNodes(orig, dest, length, info, points); + if (info.isOneWay()) { + nbOneWayRoad++; + } + observers.forEach((observer) -> observer.notifyNewArcRead(arc)); + } + } + + // Check format. + checkByteOrThrow(253); + + observers.forEach((observer) -> observer.notifyEndReading()); + + this.dis.close(); + + return new Graph(mapId, mapName, nodes, + new GraphStatistics( + new BoundingBox(new Point(minLongitude, maxLatitude), + new Point(maxLongitude, minLatitude)), + nbOneWayRoad, nbTotalSuccessors - nbOneWayRoad, maxSpeed, maxLength)); + } + + /** + * Read the next road information from the stream. + * + * @return The next RoadInformation in the stream. + * + * @throws IOException if an error occurs while reading from the stream. + */ + private RoadInformation readRoadInformation() throws IOException { + char type = (char) dis.readUnsignedByte(); + int x = dis.readUnsignedByte(); + AccessRestrictions access = new AccessRestrictions(); + if (getCurrentVersion() >= 7) { + access = toAccessInformation(dis.readLong()); + } + else if (getCurrentVersion() >= 6) { + // TODO: Try to create something... + dis.readUnsignedShort(); + } + return new RoadInformation(toRoadType(type), access, (x & 0x80) > 0, (x & 0x7F) * 5, + dis.readUTF()); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathReader.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathReader.java new file mode 100644 index 0000000..f3351bd --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathReader.java @@ -0,0 +1,76 @@ +package org.insa.graphs.model.io; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; + +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.Path; + +/** + * Implementation of {@link PathReader} to read paths in binary format. + * + */ +public class BinaryPathReader extends BinaryReader implements PathReader { + + // Map version and magic number targeted for this reader. + protected static final int VERSION = 1; + protected static final int MAGIC_NUMBER = 0xdecafe; + + /** + * Create a new BinaryPathReader that read from the given input stream. + * + * @param dis Input stream to read from. + */ + public BinaryPathReader(DataInputStream dis) { + super(MAGIC_NUMBER, VERSION, dis); + } + + @Override + public Path readPath(Graph graph) throws IOException { + + // Read and check magic number and version. + checkMagicNumberOrThrow(dis.readInt()); + checkVersionOrThrow(dis.readInt()); + + // Read map ID and check against graph. + String mapId = readFixedLengthString(BinaryGraphReader.MAP_ID_FIELD_LENGTH, "UTF-8"); + + if (!mapId.equals(graph.getMapId())) { + throw new MapMismatchException(mapId, graph.getMapId()); + } + + // Number of nodes in the path (without first and last). + int nbNodes = dis.readInt(); + + // Skip (duplicate) first and last node + readNode(graph); + readNode(graph); + + // Read intermediate nodes: + ArrayList nodes = new ArrayList(); + for (int i = 0; i < nbNodes; ++i) { + nodes.add(readNode(graph)); + } + + this.dis.close(); + + return Path.createFastestPathFromNodes(graph, nodes); + } + + /** + * Read a node from the input stream and returns it. + * + * @param graph Graph containing the nodes. + * + * @return The next node in the input stream. + * + * @throws IOException if something occurs while reading the note. + * @throws IndexOutOfBoundsException if the node is not in the graph. + */ + protected Node readNode(Graph graph) throws IOException { + return graph.get(dis.readInt()); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathWriter.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathWriter.java new file mode 100644 index 0000000..1eb778c --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryPathWriter.java @@ -0,0 +1,54 @@ +package org.insa.graphs.model.io; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Path; + +/** + * Implementation of {@link PathWriter} to write paths in binary format. + * + */ +public class BinaryPathWriter extends BinaryWriter implements PathWriter { + + /** + * Create a new BinaryPathWriter that writes to the given output stream. + * + * @param dos Output stream to write to. + */ + public BinaryPathWriter(DataOutputStream dos) { + super(dos); + } + + @Override + public void writePath(Path path) throws IOException { + + // Write magic number and version. + dos.writeInt(BinaryPathReader.MAGIC_NUMBER); + dos.writeInt(BinaryPathReader.VERSION); + + // Write map id. + byte[] bytes = Arrays.copyOf(path.getGraph().getMapId().getBytes("UTF-8"), + BinaryGraphReader.MAP_ID_FIELD_LENGTH); + dos.write(bytes); + + // Write number of arcs + dos.writeInt(path.getArcs().size() + 1); + + // Write origin / destination. + dos.writeInt(path.getOrigin().getId()); + dos.writeInt(path.getDestination().getId()); + + // Write nodes. + dos.writeInt(path.getOrigin().getId()); + for (Arc arc: path.getArcs()) { + dos.writeInt(arc.getDestination().getId()); + } + + dos.flush(); + dos.close(); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryReader.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryReader.java new file mode 100644 index 0000000..f0bd8d8 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryReader.java @@ -0,0 +1,121 @@ +package org.insa.graphs.model.io; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.IOException; + +/** + * Base class for writing binary file. + * + */ +public abstract class BinaryReader implements Closeable { + + // Map version and magic number targeted for this reader. + private final int minVersion; + private int curVersion; + private final int magicNumber; + + // InputStream + protected final DataInputStream dis; + + /** + * Create a new BinaryReader that reads from the given stream and that expected + * the given magic number and at least the given minimum version. + * + * @param magicNumber Magic number of files to be read. + * @param minVersion Minimum version of files to be read. + * @param dis Input stream from which to read. + */ + protected BinaryReader(int magicNumber, int minVersion, DataInputStream dis) { + this.magicNumber = magicNumber; + this.minVersion = minVersion; + this.dis = dis; + } + + @Override + public void close() throws IOException { + this.dis.close(); + } + + /** + * Check if the given version is greater than the minimum version, and update + * the current version if it is. + * + * @param version Version to check. + * + * @throws BadVersionException if the given version is not greater than the + * minimum version. + */ + protected void checkVersionOrThrow(int version) throws BadVersionException { + if (version < this.minVersion) { + throw new BadVersionException(version, this.minVersion); + } + this.curVersion = version; + } + + /** + * @return The current version. + */ + protected int getCurrentVersion() { + return this.curVersion; + } + + /** + * Check if the given number matches the expected magic number. + * + * @param magicNumber The magic number to check. + * + * @throws BadMagicNumberException If the two magic numbers are not equal. + */ + protected void checkMagicNumberOrThrow(int magicNumber) throws BadMagicNumberException { + if (this.magicNumber != magicNumber) { + throw new BadMagicNumberException(magicNumber, this.magicNumber); + } + } + + /** + * Check if the next byte in the input stream correspond to the given byte. + * + * This function consumes the next byte in the input stream. + * + * @param b Byte to check. + * + * @throws IOException if an error occurs while reading the byte. + * @throws BadFormatException if the byte read is not the expected one. + */ + protected void checkByteOrThrow(int b) throws IOException { + if (dis.readUnsignedByte() != b) { + throw new BadFormatException(); + } + } + + /** + * Read a byte array of fixed length from the input and convert it to a string + * using the given charset, removing any trailing '\0'. + * + * @param length Number of bytes to read. + * @param charset Charset to use to convert the bytes into a string. + * + * @return The convert string. + * + * @throws IOException if an error occurs while reading or converting. + */ + protected String readFixedLengthString(int length, String charset) throws IOException { + byte[] bytes = new byte[length]; + this.dis.read(bytes); + return new String(bytes, "UTF-8").trim(); + } + + /** + * Read 24 bits in BigEndian order from the stream and return the corresponding + * integer value. + * + * @return Integer value read from the next 24 bits of the stream. + * + * @throws IOException if an error occurs while reading from the stream. + */ + protected int read24bits() throws IOException { + int x = dis.readUnsignedShort(); + return (x << 8) | dis.readUnsignedByte(); + } +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryWriter.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryWriter.java new file mode 100644 index 0000000..1f04763 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/BinaryWriter.java @@ -0,0 +1,42 @@ +package org.insa.graphs.model.io; + +import java.io.Closeable; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Base class for writing binary file. + * + */ +public abstract class BinaryWriter implements Closeable { + + // Output stream. + protected final DataOutputStream dos; + + /** + * Create a new BinaryWriter that writes to the given output stream. + * + * @param dos Stream to write to. + */ + protected BinaryWriter(DataOutputStream dos) { + this.dos = dos; + } + + @Override + public void close() throws IOException { + this.dos.close(); + } + + /** + * Write a 24-bits integer in BigEndian order to the output stream. + * + * @param value Value to write. + * + * @throws IOException if an error occurs while writing to the stream. + */ + protected void write24bits(int value) throws IOException { + dos.writeShort(value >> 8); + dos.writeByte(value & 0xff); + } + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReader.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReader.java new file mode 100644 index 0000000..d99b6cf --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReader.java @@ -0,0 +1,39 @@ +package org.insa.graphs.model.io; + +import java.io.Closeable; +import java.io.IOException; + +import org.insa.graphs.model.Graph; + +/** + * Base interface for classes that can read graph. + * + */ +public interface GraphReader extends Closeable { + + /** + * Add a new observer to this graph reader. + * + * @param observer Observer to add. + */ + public void addObserver(GraphReaderObserver observer); + + /** + * Read a graph an returns it. + * + * @return The graph read. + * + * @throws IOException if an exception occurs while reading the graph. + * + */ + public Graph read() throws IOException; + + /** + * Close this graph reader. + * + * @throws IOException if an exception occurs while closing the reader. + * + */ + public void close() throws IOException; + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReaderObserver.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReaderObserver.java new file mode 100644 index 0000000..4f25835 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/GraphReaderObserver.java @@ -0,0 +1,69 @@ +package org.insa.graphs.model.io; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.RoadInformation; + +/** + * Base interface that should be implemented by classes that want to observe the + * reading of a graph by a {@link GraphReader}. + * + */ +public interface GraphReaderObserver { + + /** + * Notify observer about information on the graph, this method is always the + * first called + * + * @param mapId ID of the graph. + */ + public void notifyStartReading(String mapId); + + /** + * Notify that the graph has been fully read. + */ + public void notifyEndReading(); + + /** + * Notify that the reader is starting to read node. + * + * @param nNodes Number of nodes to read. + */ + public void notifyStartReadingNodes(int nNodes); + + /** + * Notify that a new nodes has been read. + * + * @param node read. + */ + public void notifyNewNodeRead(Node node); + + /** + * Notify that the reader is starting to read descriptor/road informations. + * + * @param nDesc Number of descriptors to read. + */ + public void notifyStartReadingDescriptors(int nDesc); + + /** + * Notify that a new descriptor has been read. + * + * @param desc Descriptor read. + */ + public void notifyNewDescriptorRead(RoadInformation desc); + + /** + * Notify that the reader is starting to read arcs. + * + * @param nArcs Number of arcs to read (!= number of arcs in the graph). + */ + public void notifyStartReadingArcs(int nArcs); + + /** + * Notify that a new arc has been read. + * + * @param arc Arc read. + */ + public void notifyNewArcRead(Arc arc); + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/MapMismatchException.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/MapMismatchException.java new file mode 100644 index 0000000..fb2de3b --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/MapMismatchException.java @@ -0,0 +1,45 @@ +package org.insa.graphs.model.io; + +import java.io.IOException; + +/** + * Exception thrown when there is mismatch between the expected map ID and the + * actual map ID when reading a graph. + * + */ +public class MapMismatchException extends IOException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + // Actual and expected map ID. + private String actualMapId, expectedMapId; + + /** + * Create a new MapMismatchException with the given IDs. + * + * @param actualMapId Actual map ID found when reading the path. + * @param expectedMapId Expected map ID from the graph. + */ + public MapMismatchException(String actualMapId, String expectedMapId) { + super(); + this.actualMapId = actualMapId; + this.expectedMapId = expectedMapId; + } + + /** + * @return Actual ID of the map (read from the path). + */ + public String getActualMapId() { + return actualMapId; + } + + /** + * @return Expected ID of the map. + */ + public String getExpectedMapId() { + return expectedMapId; + } +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/PathReader.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/PathReader.java new file mode 100644 index 0000000..b08747c --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/PathReader.java @@ -0,0 +1,34 @@ +package org.insa.graphs.model.io; + +import java.io.Closeable; +import java.io.IOException; + +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Path; + +/** + * Base interface that should be implemented by class used to read paths. + * + */ +public interface PathReader extends Closeable { + + /** + * Read a path of the given graph and returns it. + * + * @param graph Graph of the path. + * + * @return Path read. + * + * @throws IOException When an error occurs while reading the path. + */ + public Path readPath(Graph graph) throws IOException; + + /** + * Close this graph reader. + * + * @throws IOException if an exception occurs while closing the reader. + * + */ + public void close() throws IOException; + +} diff --git a/be-graphes-model/src/main/java/org/insa/graphs/model/io/PathWriter.java b/be-graphes-model/src/main/java/org/insa/graphs/model/io/PathWriter.java new file mode 100644 index 0000000..cb901c6 --- /dev/null +++ b/be-graphes-model/src/main/java/org/insa/graphs/model/io/PathWriter.java @@ -0,0 +1,31 @@ +package org.insa.graphs.model.io; + +import java.io.Closeable; +import java.io.IOException; + +import org.insa.graphs.model.Path; + +/** + * Base interface that should be implemented by class used to write paths. + * + */ +public interface PathWriter extends Closeable { + + /** + * Write the given path. + * + * @param path Path to write. + * + * @throws IOException When an error occurs while writing the path. + */ + public void writePath(Path path) throws IOException; + + /** + * Close this graph reader. + * + * @throws IOException if an exception occurs while closing the reader. + * + */ + public void close() throws IOException; + +} diff --git a/be-graphes-model/src/test/java/org/insa/graphes/model/GraphTest.java b/be-graphes-model/src/test/java/org/insa/graphes/model/GraphTest.java new file mode 100644 index 0000000..f9333b9 --- /dev/null +++ b/be-graphes-model/src/test/java/org/insa/graphes/model/GraphTest.java @@ -0,0 +1,112 @@ +package org.insa.graphes.model; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.RoadInformation; +import org.insa.graphs.model.RoadInformation.RoadType; +import org.junit.BeforeClass; +import org.junit.Test; + +public class GraphTest { + + // Small graph use for tests + private static Graph graph; + + // List of nodes + private static Node[] nodes; + + @BeforeClass + public static void initAll() throws IOException { + + // Create nodes + nodes = new Node[5]; + for (int i = 0; i < nodes.length; ++i) { + nodes[i] = new Node(i, null); + } + + Node.linkNodes(nodes[0], nodes[1], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[0], nodes[2], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[0], nodes[4], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[1], nodes[2], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[2], nodes[3], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[2], nodes[3], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[2], nodes[3], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[3], nodes[0], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[3], nodes[4], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[4], nodes[0], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + + graph = new Graph("ID", "", Arrays.asList(nodes), null); + + } + + /** + * @return List of arcs between a and b. + */ + private List getArcsBetween(Node a, Node b) { + List arcs = new ArrayList<>(); + for (Arc arc: a.getSuccessors()) { + if (arc.getDestination().equals(b)) { + arcs.add(arc); + } + } + return arcs; + } + + @Test + public void testTranspose() { + Graph transpose = graph.transpose(); + + // Basic asserts... + assertEquals("R/" + graph.getMapId(), transpose.getMapId()); + assertEquals(graph.size(), transpose.size()); + + final int expNbSucc[] = { 4, 2, 2, 4, 2 }; + for (int i = 0; i < expNbSucc.length; ++i) { + assertEquals(expNbSucc[i], transpose.get(i).getNumberOfSuccessors()); + } + + assertEquals(1, getArcsBetween(transpose.get(0), transpose.get(1)).size()); + assertEquals(1, getArcsBetween(transpose.get(0), transpose.get(2)).size()); + assertEquals(1, getArcsBetween(transpose.get(0), transpose.get(3)).size()); + assertEquals(1, getArcsBetween(transpose.get(0), transpose.get(4)).size()); + assertEquals(1, getArcsBetween(transpose.get(1), transpose.get(0)).size()); + assertEquals(1, getArcsBetween(transpose.get(1), transpose.get(2)).size()); + assertEquals(0, getArcsBetween(transpose.get(1), transpose.get(3)).size()); + assertEquals(0, getArcsBetween(transpose.get(1), transpose.get(4)).size()); + assertEquals(1, getArcsBetween(transpose.get(2), transpose.get(0)).size()); + assertEquals(1, getArcsBetween(transpose.get(2), transpose.get(1)).size()); + assertEquals(0, getArcsBetween(transpose.get(2), transpose.get(3)).size()); + assertEquals(0, getArcsBetween(transpose.get(2), transpose.get(4)).size()); + assertEquals(1, getArcsBetween(transpose.get(3), transpose.get(0)).size()); + assertEquals(0, getArcsBetween(transpose.get(3), transpose.get(1)).size()); + assertEquals(3, getArcsBetween(transpose.get(3), transpose.get(2)).size()); + assertEquals(0, getArcsBetween(transpose.get(3), transpose.get(4)).size()); + assertEquals(1, getArcsBetween(transpose.get(4), transpose.get(0)).size()); + assertEquals(0, getArcsBetween(transpose.get(4), transpose.get(1)).size()); + assertEquals(0, getArcsBetween(transpose.get(4), transpose.get(2)).size()); + assertEquals(1, getArcsBetween(transpose.get(4), transpose.get(3)).size()); + + } +} diff --git a/be-graphes-model/src/test/java/org/insa/graphes/model/NodeTest.java b/be-graphes-model/src/test/java/org/insa/graphes/model/NodeTest.java new file mode 100644 index 0000000..057c5d5 --- /dev/null +++ b/be-graphes-model/src/test/java/org/insa/graphes/model/NodeTest.java @@ -0,0 +1,92 @@ +package org.insa.graphes.model; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.ArrayList; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.RoadInformation; +import org.insa.graphs.model.RoadInformation.RoadType; +import org.junit.Before; +import org.junit.Test; + +public class NodeTest { + + // List of nodes + private Node[] nodes; + + @Before + public void initAll() throws IOException { + + // Create nodes + nodes = new Node[6]; + for (int i = 0; i < nodes.length; ++i) { + nodes[i] = new Node(i, null); + } + + Node.linkNodes(nodes[0], nodes[1], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[0], nodes[2], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[0], nodes[4], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[1], nodes[2], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[2], nodes[3], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[2], nodes[3], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[2], nodes[3], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[3], nodes[0], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, false, 1, null), + new ArrayList<>()); + Node.linkNodes(nodes[3], nodes[4], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + Node.linkNodes(nodes[4], nodes[0], 0, + new RoadInformation(RoadType.UNCLASSIFIED, null, true, 1, null), new ArrayList<>()); + + } + + /** + * @return The first arc between from a to b, or null. + */ + private Arc getFirstArcBetween(Node a, Node b) { + for (Arc arc: a.getSuccessors()) { + if (arc.getDestination().equals(b)) { + return arc; + } + } + return null; + } + + @Test + public void testGetNumberOfSuccessors() { + final int[] expNbSucc = { 4, 2, 5, 2, 1, 0 }; + assertEquals(expNbSucc.length, nodes.length); + for (int i = 0; i < expNbSucc.length; ++i) { + assertEquals(expNbSucc[i], nodes[i].getNumberOfSuccessors()); + } + } + + @Test + public void testHasSuccessors() { + final int[] expNbSucc = { 4, 2, 5, 2, 1, 0 }; + assertEquals(expNbSucc.length, nodes.length); + for (int i = 0; i < expNbSucc.length; ++i) { + assertEquals(expNbSucc[i] != 0, nodes[i].hasSuccessors()); + } + } + + @Test + public void testLinkNodes() { + assertEquals(getFirstArcBetween(nodes[0], nodes[1]).getRoadInformation(), + getFirstArcBetween(nodes[1], nodes[0]).getRoadInformation()); + } + +} diff --git a/be-graphes-model/src/test/java/org/insa/graphes/model/PathTest.java b/be-graphes-model/src/test/java/org/insa/graphes/model/PathTest.java new file mode 100644 index 0000000..fbef0f3 --- /dev/null +++ b/be-graphes-model/src/test/java/org/insa/graphes/model/PathTest.java @@ -0,0 +1,243 @@ +package org.insa.graphes.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +import org.insa.graphs.model.Arc; +import org.insa.graphs.model.Graph; +import org.insa.graphs.model.Node; +import org.insa.graphs.model.Path; +import org.insa.graphs.model.RoadInformation; +import org.insa.graphs.model.RoadInformation.RoadType; +import org.junit.BeforeClass; +import org.junit.Test; + +public class PathTest { + + // Small graph use for tests + private static Graph graph; + + // List of nodes + private static Node[] nodes; + + // List of arcs in the graph, a2b is the arc from node A (0) to B (1). + @SuppressWarnings("unused") + private static Arc a2b, a2c, a2e, b2c, c2d_1, c2d_2, c2d_3, c2a, d2a, d2e, e2d; + + // Some paths... + private static Path emptyPath, singleNodePath, shortPath, longPath, loopPath, longLoopPath, + invalidPath; + + @BeforeClass + public static void initAll() throws IOException { + + // 10 and 20 meters per seconds + RoadInformation speed10 = new RoadInformation(RoadType.MOTORWAY, null, true, 36, ""), + speed20 = new RoadInformation(RoadType.MOTORWAY, null, true, 72, ""); + + // Create nodes + nodes = new Node[5]; + for (int i = 0; i < nodes.length; ++i) { + nodes[i] = new Node(i, null); + } + + // Add arcs... + a2b = Node.linkNodes(nodes[0], nodes[1], 10, speed10, null); + a2c = Node.linkNodes(nodes[0], nodes[2], 15, speed10, null); + a2e = Node.linkNodes(nodes[0], nodes[4], 15, speed20, null); + b2c = Node.linkNodes(nodes[1], nodes[2], 10, speed10, null); + c2d_1 = Node.linkNodes(nodes[2], nodes[3], 20, speed10, null); + c2d_2 = Node.linkNodes(nodes[2], nodes[3], 10, speed10, null); + c2d_3 = Node.linkNodes(nodes[2], nodes[3], 15, speed20, null); + d2a = Node.linkNodes(nodes[3], nodes[0], 15, speed10, null); + d2e = Node.linkNodes(nodes[3], nodes[4], 22.8f, speed20, null); + e2d = Node.linkNodes(nodes[4], nodes[0], 10, speed10, null); + + graph = new Graph("ID", "", Arrays.asList(nodes), null); + + emptyPath = new Path(graph, new ArrayList()); + singleNodePath = new Path(graph, nodes[1]); + shortPath = new Path(graph, Arrays.asList(new Arc[] { a2b, b2c, c2d_1 })); + longPath = new Path(graph, Arrays.asList(new Arc[] { a2b, b2c, c2d_1, d2e })); + loopPath = new Path(graph, Arrays.asList(new Arc[] { a2b, b2c, c2d_1, d2a })); + longLoopPath = new Path(graph, + Arrays.asList(new Arc[] { a2b, b2c, c2d_1, d2a, a2c, c2d_3, d2a, a2b, b2c })); + invalidPath = new Path(graph, Arrays.asList(new Arc[] { a2b, c2d_1, d2e })); + + } + + @Test + public void testConstructor() { + assertEquals(graph, emptyPath.getGraph()); + assertEquals(graph, singleNodePath.getGraph()); + assertEquals(graph, shortPath.getGraph()); + assertEquals(graph, longPath.getGraph()); + assertEquals(graph, loopPath.getGraph()); + assertEquals(graph, longLoopPath.getGraph()); + assertEquals(graph, invalidPath.getGraph()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testImmutability() { + emptyPath.getArcs().add(a2b); + } + + @Test + public void testIsEmpty() { + assertTrue(emptyPath.isEmpty()); + + assertFalse(singleNodePath.isEmpty()); + assertFalse(shortPath.isEmpty()); + assertFalse(longPath.isEmpty()); + assertFalse(loopPath.isEmpty()); + assertFalse(longLoopPath.isEmpty()); + assertFalse(invalidPath.isEmpty()); + } + + @Test + public void testSize() { + assertEquals(0, emptyPath.size()); + assertEquals(1, singleNodePath.size()); + assertEquals(4, shortPath.size()); + assertEquals(5, longPath.size()); + assertEquals(5, loopPath.size()); + assertEquals(10, longLoopPath.size()); + } + + @Test + public void testIsValid() { + assertTrue(emptyPath.isValid()); + assertTrue(singleNodePath.isValid()); + assertTrue(shortPath.isValid()); + assertTrue(longPath.isValid()); + assertTrue(loopPath.isValid()); + assertTrue(longLoopPath.isValid()); + + assertFalse(invalidPath.isValid()); + } + + @Test + public void testGetLength() { + assertEquals(0, emptyPath.getLength(), 1e-6); + assertEquals(0, singleNodePath.getLength(), 1e-6); + assertEquals(40, shortPath.getLength(), 1e-6); + assertEquals(62.8, longPath.getLength(), 1e-6); + assertEquals(55, loopPath.getLength(), 1e-6); + assertEquals(120, longLoopPath.getLength(), 1e-6); + } + + @Test + public void testGetTravelTime() { + // Note: 18 km/h = 5m/s + assertEquals(0, emptyPath.getTravelTime(18), 1e-6); + assertEquals(0, singleNodePath.getTravelTime(18), 1e-6); + assertEquals(8, shortPath.getTravelTime(18), 1e-6); + assertEquals(12.56, longPath.getTravelTime(18), 1e-6); + assertEquals(11, loopPath.getTravelTime(18), 1e-6); + assertEquals(24, longLoopPath.getTravelTime(18), 1e-6); + + // Note: 28.8 km/h = 8m/s + assertEquals(0, emptyPath.getTravelTime(28.8), 1e-6); + assertEquals(0, singleNodePath.getTravelTime(28.8), 1e-6); + assertEquals(5, shortPath.getTravelTime(28.8), 1e-6); + assertEquals(7.85, longPath.getTravelTime(28.8), 1e-6); + assertEquals(6.875, loopPath.getTravelTime(28.8), 1e-6); + assertEquals(15, longLoopPath.getTravelTime(28.8), 1e-6); + } + + @Test + public void testGetMinimumTravelTime() { + assertEquals(0, emptyPath.getMinimumTravelTime(), 1e-4); + assertEquals(0, singleNodePath.getLength(), 1e-4); + assertEquals(4, shortPath.getMinimumTravelTime(), 1e-4); + assertEquals(5.14, longPath.getMinimumTravelTime(), 1e-4); + assertEquals(5.5, loopPath.getMinimumTravelTime(), 1e-4); + assertEquals(11.25, longLoopPath.getMinimumTravelTime(), 1e-4); + } + + @Test + public void testCreateFastestPathFromNodes() { + Path path; + Arc[] expected; + + // Simple construction + path = Path.createFastestPathFromNodes(graph, + Arrays.asList(new Node[] { nodes[0], nodes[1], nodes[2] })); + expected = new Arc[] { a2b, b2c }; + assertEquals(expected.length, path.getArcs().size()); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i], path.getArcs().get(i)); + } + + // Not so simple construction + path = Path.createFastestPathFromNodes(graph, + Arrays.asList(new Node[] { nodes[0], nodes[1], nodes[2], nodes[3] })); + expected = new Arc[] { a2b, b2c, c2d_3 }; + assertEquals(expected.length, path.getArcs().size()); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i], path.getArcs().get(i)); + } + + // Trap construction! + path = Path.createFastestPathFromNodes(graph, Arrays.asList(new Node[] { nodes[1] })); + assertEquals(nodes[1], path.getOrigin()); + assertEquals(0, path.getArcs().size()); + + // Trap construction - The return! + path = Path.createFastestPathFromNodes(graph, Arrays.asList(new Node[0])); + assertEquals(null, path.getOrigin()); + assertEquals(0, path.getArcs().size()); + assertTrue(path.isEmpty()); + } + + @Test + public void testCreateShortestPathFromNodes() { + Path path; + Arc[] expected; + + // Simple construction + path = Path.createShortestPathFromNodes(graph, + Arrays.asList(new Node[] { nodes[0], nodes[1], nodes[2] })); + expected = new Arc[] { a2b, b2c }; + assertEquals(expected.length, path.getArcs().size()); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i], path.getArcs().get(i)); + } + + // Not so simple construction + path = Path.createShortestPathFromNodes(graph, + Arrays.asList(new Node[] { nodes[0], nodes[1], nodes[2], nodes[3] })); + expected = new Arc[] { a2b, b2c, c2d_2 }; + assertEquals(expected.length, path.getArcs().size()); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i], path.getArcs().get(i)); + } + + // Trap construction! + path = Path.createShortestPathFromNodes(graph, Arrays.asList(new Node[] { nodes[1] })); + assertEquals(nodes[1], path.getOrigin()); + assertEquals(0, path.getArcs().size()); + + // Trap construction - The return! + path = Path.createShortestPathFromNodes(graph, Arrays.asList(new Node[0])); + assertEquals(null, path.getOrigin()); + assertEquals(0, path.getArcs().size()); + assertTrue(path.isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateFastestPathFromNodesException() { + Path.createFastestPathFromNodes(graph, Arrays.asList(new Node[] { nodes[1], nodes[0] })); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateShortestPathFromNodesException() { + Path.createShortestPathFromNodes(graph, Arrays.asList(new Node[] { nodes[1], nodes[0] })); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c275f19 --- /dev/null +++ b/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.insa.graphs + be-graphes-all + 0.0.1-SNAPSHOT + pom + + be-graphes-all + + + 1.8 + ${jdk.version} + ${jdk.version} + UTF-8 + + + + + + maven-compiler-plugin + 3.1 + + ${jdk.version} + ${jdk.version} + + + + + + + + junit + junit + 4.12 + test + + + + + be-graphes-model + be-graphes-algos + be-graphes-gui + +