package edu.kit.pse.ws2013.routekit.controllers; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.xml.sax.SAXException; import edu.kit.pse.ws2013.routekit.map.StreetMap; import edu.kit.pse.ws2013.routekit.models.CurrentCombinationListener; import edu.kit.pse.ws2013.routekit.models.ProfileMapCombination; import edu.kit.pse.ws2013.routekit.models.ProgressReporter; import edu.kit.pse.ws2013.routekit.models.ProgressReporter.CloseableTask; import edu.kit.pse.ws2013.routekit.profiles.Profile; import edu.kit.pse.ws2013.routekit.util.Dummies; import edu.kit.pse.ws2013.routekit.util.FileUtil; /** * The {@link ProfileMapManager} manages {@link ProfileMapCombination * ProfileMapCombinations}. It loads them from the disk when {@link #init(File) * initialized} and reads the information which one is the current combination. * * @author Lucas Werkmeister */ public class ProfileMapManager { private static final Charset INDEX_FILE_CHARSET = Charset.forName("UTF-8"); private static ProfileMapManager instance = null; private final File root; private ProfileMapCombination current; private final Set<ProfileMapCombination> precalculations; private final Set<CurrentCombinationListener> listeners = new HashSet<>(); private boolean dispatchEvents = true; private boolean eventsDirty = false; private ProfileMapManager(File root) throws IOException { this.root = root; if (!root.isDirectory()) { throw new IllegalArgumentException(root.toString() + " is not a directory!"); } final File indexFile = new File(root, "routeKIT.idx"); Map<String, Set<String>> combinations = new HashMap<>(); Entry<String, String> current = null; try (BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream(indexFile), INDEX_FILE_CHARSET))) { String currentMap = null; String line; while ((line = br.readLine()) != null) { if (line.charAt(0) == '\t') { // a profile if (currentMap == null) { throw new IOException( "No current map when reading profile '" + line.substring(1) + "'!"); } final String profile; final boolean isDefault; if (line.startsWith("\t* ")) { profile = line.substring("\t* ".length()); isDefault = true; } else { profile = line.substring(1); isDefault = false; } combinations.get(currentMap).add(profile); if (isDefault) { Entry<String, String> newCurrent = new AbstractMap.SimpleEntry<>( currentMap, profile); if (current != null) { throw new IOException("Two current combinations – " + current + " and " + newCurrent + "!"); } current = newCurrent; } } else { // a map currentMap = line; combinations.put(currentMap, new HashSet<String>()); } } } if (current == null) { System.err .println("No current combination found, will choose arbitrary one!"); // TODO } final Map<String, Profile> profilesByName = new HashMap<>(); for (Profile p : ProfileManager.getInstance().getProfiles()) { profilesByName.put(p.getName(), p); } final Map<String, StreetMap> mapsByName = new HashMap<>(); for (StreetMap m : MapManager.getInstance().getMaps()) { mapsByName.put(m.getName(), m); } this.precalculations = new HashSet<>(); for (Entry<String, Set<String>> entry : combinations.entrySet()) { String mapName = entry.getKey(); StreetMap map = mapsByName.get(mapName); if (map == null) { System.err.println("Map '" + mapName + "' not found, skipping"); continue; } for (String profileName : entry.getValue()) { Profile profile = profilesByName.get(profileName); if (profile == null) { System.err.println("Profile '" + profileName + "' not found, skipping"); continue; } ProfileMapCombination combination; try { combination = ProfileMapCombination .loadLazily(profile, map, new File(new File(root, mapName), profileName)); } catch (IOException | IllegalArgumentException e) { if (current != null && current.getKey().equals(mapName) && current.getValue().equals(profileName)) { // the current combination may not have been // precalculated combination = new ProfileMapCombination(map, profile); this.current = combination; continue; } else { throw e; } } this.precalculations.add(combination); if (current != null && mapName.equals(current.getKey()) && profileName.equals(current.getValue())) { this.current = combination; } } } if (current == null || this.current == null) { // choose any precalculation if (!this.precalculations.isEmpty()) { this.current = this.precalculations.iterator().next(); } else { // there are no precalculations if (!mapsByName.isEmpty()) { selectProfileAndMap(Profile.defaultCar, mapsByName.values() .iterator().next()); } else { // TODO disallow this // we’ll allow it for now for Dummies } } } } public ProfileMapCombination getCurrentCombination() { return current; } public void addCurrentCombinationListener( CurrentCombinationListener listener) { listeners.add(listener); } public void savePrecalculation(ProfileMapCombination precalculation) { if (!precalculation.isCalculated()) { throw new IllegalArgumentException("Not a precalculation!"); } // 1. save the precalculation try { precalculation.save(new File(new File(root, precalculation .getStreetMap().getName()), precalculation.getProfile() .getName())); } catch (IOException e) { e.printStackTrace(); return; // don’t write an invalid index file } precalculations.add(precalculation); // 2. write a new index file try { rewriteIndex(); } catch (IOException e) { e.printStackTrace(); // don’t return – not critical } // 3. special case if that was the current combination if (precalculation.getProfile().equals(current.getProfile()) && precalculation.getStreetMap().equals(current.getStreetMap())) { // 3.1 remove the old one from combinations if (current != precalculation) { precalculations.remove(current); } // 3.2 update current current = precalculation; dispatchEvent(); } } /** * Sets the current combination to the given one. * <p> * The index file is rewritten, but for performance reasons, the * precalculation (if it’s a precalculation) isn’t saved; if you’re not sure * if the precalculation has been saved, use * {@link #savePrecalculation(ProfileMapCombination)}. * <p> * Note that you’d usually want to use * {@link #selectProfileAndMap(Profile, StreetMap)} instead of this method, * since that method looks for an existing (precalculated) combination. * Especially, you should <i>never</i> call <code> * setCurrentCombination(new ProfileMapCombination(map, profile)); * </code> * * @param combination * The combination. */ public void setCurrentCombination(ProfileMapCombination combination) { if (combination == null) { throw new IllegalArgumentException("combination must not be null!"); } current = combination; try { rewriteIndex(); } catch (IOException e) { e.printStackTrace(); // don’t return – not critical } dispatchEvent(); } private void dispatchEvent() { eventsDirty = true; if (dispatchEvents) { for (CurrentCombinationListener listener : listeners) { listener.currentCombinationChanged(current); } eventsDirty = false; } } /** * Checks if the profile and map of the {@link #getCurrentCombination() * current combination} still exist (in the {@link MapManager} and * {@link ProfileManager}); if they don’t, another profile / map is * selected. */ public void checkIfCurrentStillExists() { Profile newProfile = current.getProfile(); if (!ProfileManager.getInstance().getProfiles().contains(newProfile)) { // profile was deleted, choose another newProfile = ProfileManager.getInstance().getProfiles().iterator() .next(); } StreetMap newMap = current.getStreetMap(); if (!MapManager.getInstance().getMaps().contains(newMap)) { // map was deleted, choose another newMap = MapManager.getInstance().getMaps().iterator().next(); } if (!(newProfile == current.getProfile()) || !(newMap == current.getStreetMap())) { selectProfileAndMap(newProfile, newMap); } } void rewriteIndex() throws IOException { Map<StreetMap, Set<ProfileMapCombination>> combinationsByMap = new HashMap<>(); for (ProfileMapCombination combo : precalculations) { Set<ProfileMapCombination> combos = combinationsByMap.get(combo .getStreetMap()); if (combos == null) { combos = new HashSet<>(); } combos.add(combo); combinationsByMap.put(combo.getStreetMap(), combos); } Set<ProfileMapCombination> currentMapCombos = combinationsByMap .get(current.getStreetMap()); if (currentMapCombos == null) { currentMapCombos = new HashSet<>(); } currentMapCombos.add(current); combinationsByMap.put(current.getStreetMap(), currentMapCombos); for (StreetMap map : MapManager.getInstance().getMaps()) { if (!combinationsByMap.containsKey(map)) { // maps with no precalculations should still be in the index combinationsByMap .put(map, new HashSet<ProfileMapCombination>()); } } try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(new File(root, "routeKIT.idx")), INDEX_FILE_CHARSET))) { for (Entry<StreetMap, Set<ProfileMapCombination>> map : combinationsByMap .entrySet()) { bw.write(map.getKey().getName()); bw.newLine(); for (ProfileMapCombination combo : map.getValue()) { bw.write("\t"); if (combo == current) { bw.write("* "); } bw.write(combo.getProfile().getName()); bw.newLine(); } } } } /** * Searches for a precalculation with the given profile and map. If the is * already pre-calculated, the {@link ProfileMapCombination} is returned; * otherwise, {@code null} is returned. * * @param profile * The profile. * @param map * The map. * @return A {@link ProfileMapCombination} with the results of a * precalculation for the given profile and map if it exists, * otherwise {@code null}. */ public ProfileMapCombination getPrecalculation(Profile profile, StreetMap map) { for (ProfileMapCombination combination : precalculations) { if (combination.getProfile().equals(profile) && combination.getStreetMap().equals(map)) { return combination; } } return null; } /** * Selects the given profile and map. If a precalculation exists, it becomes * the current one and is returned; if it doesn’t, then a new * {@link ProfileMapCombination} is created and selected (but not saved). * <p> * In other words, this behaves like <code> * setCurrentCombination(getPrecalculation(profile, map) else new ProfileMapCombination(map, profile)); * </code> * * @param profile * The profile. * @param map * The map. * @return A {@link ProfileMapCombination combination} of the given profile * and map. */ public ProfileMapCombination selectProfileAndMap(Profile profile, StreetMap map) { ProfileMapCombination combination = getPrecalculation(profile, map); if (combination == null) { combination = new ProfileMapCombination(map, profile); } setCurrentCombination(combination); return combination; } /** * Remove a precalculation from the internal list and optionally delete it * from disk. * * @param precalculation * The precalculation. (This must be an actual precalculation – * i. e. an element of {@link #getPrecalculations()} – and * not just any {@link ProfileMapCombination}.) * @param deleteFromDisk * If {@code true}, delete from disk as well. You usually want to * do this; the only case where you don’t need this is if you’re * deleting a precalculation because you’re deleting its * {@link StreetMap}, in which case the recursive delete of the * Map’s folder will delete all precalculations as well. */ public void deletePrecalculation(ProfileMapCombination precalculation, boolean deleteFromDisk) { if (!precalculations.contains(precalculation) && precalculation != current) { throw new IllegalArgumentException("Unknown precalculation!"); } precalculations.remove(precalculation); if (deleteFromDisk) { try { FileUtil.rmRf(new File(new File(root, precalculation .getStreetMap().getName()), precalculation.getProfile() .getName())); } catch (IOException e1) { e1.printStackTrace(); // don’t return – not critical } } try { rewriteIndex(); } catch (IOException e) { e.printStackTrace(); } } /** * Remove a precalculation from the internal list and delete it from disk. * * @param precalculation * The precalculation. * @see #deletePrecalculation(ProfileMapCombination, boolean) */ public void deletePrecalculation(ProfileMapCombination precalculation) { deletePrecalculation(precalculation, true); } public Set<ProfileMapCombination> getPrecalculations() { return Collections.unmodifiableSet(precalculations); } /** * Pause sending events. The manager will not send * {@link CurrentCombinationListener#currentCombinationChanged(ProfileMapCombination) * events} to its * {@link #addCurrentCombinationListener(CurrentCombinationListener) * registered} listeners until {@link #resumeEvents()} is called. */ public void pauseEvents() { dispatchEvents = false; } /** * Resume sending events. If at least one event should have been sent since * {@link #pauseEvents()}, an event will be sent now. */ public void resumeEvents() { dispatchEvents = true; if (eventsDirty) { dispatchEvent(); } } public static ProfileMapCombination init(File rootDirectory, ProgressReporter pr) throws IOException { boolean mustCreateInstall = false; if (!rootDirectory.exists()) { pr.setSubTasks(new float[] { .0025f, .0025f, .0025f, .99f, .0025f }); initFirstStart(rootDirectory); mustCreateInstall = true; } else if (!rootDirectory.isDirectory()) { throw new IllegalArgumentException(rootDirectory.toString() + " is not a directory!"); } else { pr.setSubTasks(new float[] { .01f, .01f, .01f, .97f }); } if (instance != null) { throw new IllegalStateException("Already initialized!"); } pr.pushTask("Initialisiere ProfileManager"); ProfileManager.init(rootDirectory); pr.nextTask("Initialisiere MapManager"); MapManager.init(rootDirectory); pr.nextTask("Erstelle ProfileMapManager"); instance = new ProfileMapManager(rootDirectory); pr.popTask(); if (mustCreateInstall) { try (CloseableTask task = pr.openTask("Schließe Installation ab")) { Dummies.createInstall(rootDirectory, pr); } catch (SAXException e) { throw new RuntimeException("Couldn’t import initial map!", e); } } pr.pushTask("Lade Vorberechnung"); // un-lazy instance.getCurrentCombination().ensureLoaded(pr); pr.popTask(); return instance.getCurrentCombination(); } /** * create the root directory and add an empty index file * * @throws IOException */ private static void initFirstStart(File rootDirectory) throws IOException { rootDirectory.mkdir(); new File(rootDirectory, "routeKIT.idx").createNewFile(); } public static ProfileMapManager getInstance() { if (instance == null) { throw new IllegalStateException("Not yet initialized!"); } return instance; } }