package edu.kit.pse.ws2013.routekit.controllers;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import javax.swing.JOptionPane;
import javax.xml.stream.XMLStreamException;
import edu.kit.pse.ws2013.routekit.export.GPXExporter;
import edu.kit.pse.ws2013.routekit.export.HTMLExporter;
import edu.kit.pse.ws2013.routekit.history.History;
import edu.kit.pse.ws2013.routekit.history.HistoryEntry;
import edu.kit.pse.ws2013.routekit.map.GraphIndex;
import edu.kit.pse.ws2013.routekit.map.StreetMap;
import edu.kit.pse.ws2013.routekit.mapdisplay.OSMRenderer;
import edu.kit.pse.ws2013.routekit.mapdisplay.TileCache;
import edu.kit.pse.ws2013.routekit.mapdisplay.TileRenderer;
import edu.kit.pse.ws2013.routekit.mapdisplay.TileSource;
import edu.kit.pse.ws2013.routekit.models.ProfileMapCombination;
import edu.kit.pse.ws2013.routekit.models.ProgressReporter;
import edu.kit.pse.ws2013.routekit.models.RouteModel;
import edu.kit.pse.ws2013.routekit.precalculation.PreCalculator;
import edu.kit.pse.ws2013.routekit.profiles.Profile;
import edu.kit.pse.ws2013.routekit.routecalculation.ArcFlagsDijkstra;
import edu.kit.pse.ws2013.routekit.routecalculation.Dijkstra;
import edu.kit.pse.ws2013.routekit.routecalculation.Route;
import edu.kit.pse.ws2013.routekit.routecalculation.RouteCalculator;
import edu.kit.pse.ws2013.routekit.routecalculation.RouteDescription;
import edu.kit.pse.ws2013.routekit.routecalculation.RouteDescriptionGenerator;
import edu.kit.pse.ws2013.routekit.util.Coordinates;
import edu.kit.pse.ws2013.routekit.util.FileUtil;
import edu.kit.pse.ws2013.routekit.util.PointOnEdge;
import edu.kit.pse.ws2013.routekit.views.MainView;
import edu.kit.pse.ws2013.routekit.views.ProgressDialog;
/**
* routeKIT’s main Controller. Manages the program execution and remains alive
* until routeKIT exits.
*/
public class MainController {
private final GPXExporter gpxExporter = new GPXExporter();
private final HTMLExporter htmlExporter = new HTMLExporter();
private static MainController instance;
private final ProfileMapManager profileMapManager;
private RouteModel rm = new RouteModel();
private final History history;
MainView view;
private RouteCalculator rc;
private RouteDescriptionGenerator rdg;
private boolean useOnlineMaps = false;
private TileCache cache = null;
/**
* Creates the controller, initializes the {@link MapManager},
* {@link ProfileManager} and {@link ProfileMapManager} and then creates the
* {@link MainView}.
*
* @param pr
* the starter
*/
private MainController(ProgressReporter pr) {
pr.pushTask("Starte routeKIT");
pr.setSubTasks(new float[] { 0.99f, 0.01f });
instance = this;
pr.pushTask("Lese Index");
try {
ProfileMapManager.init(FileUtil.getRootDir(), pr);
} catch (IOException e) {
// die
history = null;
profileMapManager = null;
return;
}
profileMapManager = ProfileMapManager.getInstance();
profileMapManager.getCurrentCombination().ensureLoaded(pr);
pr.popTask("Lese Index");
profileMapManager.addCurrentCombinationListener(rm);
History _history; // because history is final
try {
_history = History.load(FileUtil.getHistoryFile());
} catch (IOException e) {
_history = new History();
}
history = _history;
rc = new ArcFlagsDijkstra();
rdg = new RouteDescriptionGenerator();
pr.pushTask("Lade Ansicht");
view = new MainView(rm);
pr.popTask();
pr.popTask();
}
/**
* Called when the start point changes (e. g. via user input). Sets the
* new start point in the {@link RouteModel}. If a destination point is
* present, adds an entry to the {@link History} and starts route
* calculation.
*
* @param start
* The coordinates of the new start point.
*/
public void setStartPoint(Coordinates start) {
rm.setStart(start);
checkAndCalculate();
}
/**
* Called when the destination point changes (e. g. via user input).
* Sets the new destination point in the {@link RouteModel}. If a start
* point is present, adds an entry to the {@link History} and starts route
* calculation.
*
* @param destination
* The coordinates of the new destination point.
*/
public void setDestinationPoint(Coordinates destination) {
rm.setDestination(destination);
checkAndCalculate();
}
Thread calculator;
/**
* If a start and destination point are present, adds a history entry and
* starts route calculation.
*/
private void checkAndCalculate() {
final Coordinates start = rm.getStart();
final Coordinates destination = rm.getDestination();
if (start != null && destination != null) {
List<HistoryEntry> entries = history.getEntries();
boolean saveNewEntry = true;
if (!entries.isEmpty()) {
HistoryEntry last = entries.get(entries.size() - 1);
if (last != null && last.getStart().equals(start)
&& last.getDest().equals(destination)) {
saveNewEntry = false;
}
}
if (saveNewEntry) {
history.addEntry(start, destination);
try {
history.save(FileUtil.getHistoryFile());
} catch (IOException e) {
e.printStackTrace();
}
}
rm.setCurrentRoute(null);
calculator = new Thread("Route calc") {
{
setDaemon(true);
}
@Override
public void run() {
rm.startCalculating();
ProfileMapCombination currentCombination = profileMapManager
.getCurrentCombination();
GraphIndex index = currentCombination.getStreetMap()
.getGraph().getIndex(18);
PointOnEdge startPointOnEdge = index
.findNearestPointOnEdge(start);
PointOnEdge destinationPointOnEdge = index
.findNearestPointOnEdge(destination);
if (startPointOnEdge == null
|| destinationPointOnEdge == null) {
rm.setCurrentRoute(null);
rm.setCurrentDescription(null);
return;
}
Route r = rc.calculateRoute(startPointOnEdge,
destinationPointOnEdge, currentCombination);
if (calculator != this) {
return;
}
rm.setCurrentRoute(r);
RouteDescription rd = r == null ? null : rdg
.generateRouteDescription(r);
rm.setCurrentDescription(rd);
}
};
calculator.start();
}
}
/**
* Saves the description of the current route in HTML format into the given
* file. If no current route is available (e. g. because no
* precalculation has been executed), an {@link IllegalStateException} is
* thrown.
*
* @param target
* The file into which the description shall be saved.
*/
public void exportHTML(File target) {
Route route = rm.getCurrentRoute();
if (route == null) {
throw new IllegalStateException("No current route to export!");
}
RouteDescription description = rdg.generateRouteDescription(route);
try {
htmlExporter.exportRouteDescription(description, target);
} catch (IOException e) {
view.textMessage("Beim Export der Routenbeschreibung ist ein Fehler aufgetreten!\n"
+ e.getMessage());
}
}
/**
* Called when the start and destination point change, e. g. by
* selecting a {@link HistoryEntry}. The same actions as for
* {@link #setStartPoint(Coordinates)} and
* {@link #setDestinationPoint(Coordinates)} are executed, but only once.
*
* @param start
* The coordinates of the new start point.
* @param destination
* The coordinates of the new destination point.
*/
public void setStartAndDestinationPoint(Coordinates start,
Coordinates destination) {
rm.setStart(start);
rm.setDestination(destination);
checkAndCalculate();
}
/**
* Calls {@link PreCalculator#doPrecalculation(ProfileMapCombination)} in a
* new worker thread if no precalculation for the current
* {@link ProfileMapCombination} exists.
* <p>
* The given {@link ProgressReporter} should already have the task
* "Precalculating and saving" or something similar pushed onto it. This
* method will then push and pop sub-tasks.
* <p>
* The changes are executed asynchronously in a new worker thread, and after
* all changes have been executed, an additional task is popped off the
* reporter that this method did not push (the task
* "Precalculating and saving", as mentioned earlier). This way, the caller
* may be notified when the changes are done.
*
* @param reporter
* The {@link ProgressReporter} to which progress shall be
* reported.
*/
public void startPrecalculation(final ProgressReporter reporter) {
new Thread("MainController Precalculation Thread") {
{
setDaemon(true);
}
@Override
public void run() {
ProfileMapCombination combination = profileMapManager
.getCurrentCombination();
if (!combination.isCalculated()) {
reporter.setSubTasks(new float[] { .95f, .05f });
reporter.pushTask("Führe Vorberechnung durch für '"
+ combination + "'");
new PreCalculator().doPrecalculation(combination, reporter);
reporter.nextTask("Speichere '" + combination + "'");
profileMapManager.savePrecalculation(combination);
reporter.popTask();
}
reporter.popTask();
};
}.start();
}
/**
* Determines whether OSM tiles or our own tiles shall be used for
* rendering. For OSM tiles, the {@link OSMRenderer} is used; for our own
* tiles, {@link TileRenderer}.
*
* @param useOnlineMaps
* {@code true} to use OSM tiles, {@code false} to use our own
* tiles.
*
*/
public void setUseOnlineMaps(boolean useOnlineMaps) {
this.useOnlineMaps = useOnlineMaps;
}
public void setUseArcFlags(boolean useArcFlags) {
if (useArcFlags) {
rc = new ArcFlagsDijkstra();
} else {
rc = new Dijkstra();
}
// TODO: Direkt neu berechnen oder nicht?
// checkAndCalculate();
}
/**
* Creates a new {@link ProfileManagerController}, which opens the profile
* management dialog.
*/
public void manageProfiles() {
profileMapManager.pauseEvents();
ProfileManagerController c = new ProfileManagerController(view);
Profile selected = c.getSelectedProfile();
ProfileMapCombination current = profileMapManager
.getCurrentCombination();
if (selected != null && !selected.equals(current.getProfile())) {
load(selected, current.getStreetMap());
}
profileMapManager.resumeEvents();
}
MapManagerController mapManagement;
/**
* Creates a new {@link MapManagerController}, which opens the map
* management dialog.
*/
public void manageMaps() {
profileMapManager.pauseEvents();
mapManagement = new MapManagerController(view);
StreetMap selected = mapManagement.getSelectedMap();
ProfileMapCombination current = profileMapManager
.getCurrentCombination();
if (selected != null) {
load(current.getProfile(), selected);
}
profileMapManager.resumeEvents();
}
private void load(Profile profile, StreetMap map) {
ProfileMapCombination newCombination = profileMapManager
.getPrecalculation(profile, map);
if (newCombination == null) {
newCombination = new ProfileMapCombination(map, profile);
}
final ProfileMapCombination theNewCombination = newCombination;
ProgressDialog p = new ProgressDialog(view);
final ProgressReporter reporter = new ProgressReporter();
reporter.addProgressListener(p);
reporter.pushTask("Lade ausgewählte Karte und Vorberechnung");
new Thread("Load map + precalculation") {
{
setDaemon(true);
}
@Override
public void run() {
theNewCombination.ensureLoaded(reporter);
reporter.popTask();
};
}.start();
p.setVisible(true);
profileMapManager.setCurrentCombination(newCombination);
}
/**
* Saves the current {@link Route} in the GPS Exchange Format into the given
* file. If there is no current route, an {@link IllegalStateException} is
* thrown.
*
* @param target
* The file into which the route shall be exported.
*/
public void exportGPX(File target) {
Route route = rm.getCurrentRoute();
if (route == null) {
throw new IllegalStateException("No current route to export!");
}
try {
gpxExporter.exportRoute(route, target);
} catch (FileNotFoundException | XMLStreamException e) {
// TODO the view should display this error
e.printStackTrace();
}
}
/**
* Returns a {@link TileSource} that should be used to render tiles.
*
* @return A {@link TileSource} for rendering tiles.
*/
public TileSource getTileSource() {
if (cache != null) {
cache.stop();
}
if (useOnlineMaps) {
String tileServer = FileUtil.getTileServer();
if (tileServer != null) {
try {
return cache = new TileCache(new OSMRenderer(tileServer));
} catch (IOException e) {
// OSMRenderer couldn’t construct, server not reachable
JOptionPane
.showMessageDialog(
view,
"Der Server für die Onlinekacheln kann nicht verwendet werden.\n"
+ "Bitte überprüfen Sie den Inhalt der Datei tileServer.txt im Ordner "
+ FileUtil.getRootDir()
+ "\noder verwenden sie einen anderen Server,\n"
+ "zum Beispiel http://[abc].tile.openstreetmap.org/.",
"Kachelserver nicht verwendbar",
JOptionPane.ERROR_MESSAGE);
}
} else {
JOptionPane
.showMessageDialog(
view,
"Es wurde noch kein Server für die Onlinekacheln eingestellt.\n"
+ "Bitte legen Sie eine Datei tileServer.txt im Ordner "
+ FileUtil.getRootDir()
+ " an,\n"
+ "zum Beispiel mit dem Inhalt http://[abc].tile.openstreetmap.org/.",
"Kein Kachelserver", JOptionPane.ERROR_MESSAGE);
}
// fallthrough / fallback to own renderer
}
return cache = new TileCache(new TileRenderer(profileMapManager
.getCurrentCombination().getStreetMap().getGraph()));
}
/**
* Gets the history.
* <p>
* (Please don’t modify it.)
*
* @return The history.
*/
public History getHistory() {
return history;
}
public static MainController getInstance() {
if (instance == null) {
throw new Error("hey");
}
return instance;
}
/**
* Main method of the program. Creates the {@link MainController}.
*
* @param args
* Command line arguments (currently unused).
*/
public static void main(String[] args) {
if (args.length != 0) {
new TerminalCLI(args).run();
return;
}
final ProgressDialog pd = new ProgressDialog(null);
new Thread("Upstart") {
{
setDaemon(true);
}
@Override
public void run() {
ProgressReporter pr = new ProgressReporter();
pr.addProgressListener(pd);
instance = new MainController(pr);
}
}.start();
pd.setVisible(true);
instance.view.setVisible(true);
}
}