/* Copyright (C) 2014,2015 Björn Stelter, Hagen Sparka * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> */ package de.hu_berlin.informatik.spws2014.mapever.navigation; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PointF; import android.os.Bundle; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.Toast; import java.io.FileNotFoundException; import java.util.HashSet; import de.hu_berlin.informatik.spws2014.ImagePositionLocator.Point2D; import de.hu_berlin.informatik.spws2014.mapever.MapEverApp; import de.hu_berlin.informatik.spws2014.mapever.R; import de.hu_berlin.informatik.spws2014.mapever.largeimageview.LargeImageView; public class MapView extends LargeImageView { // ////// KEYS FÜR ZU SPEICHERNDE DATEN IM SAVEDINSTANCESTATE-BUNDLE // Keys für unakzeptierten Referenzpunkt oder Löschkandidaten private static final String SAVEDUNACCEPTEDPOS = "savedUnacceptedRefPointPosition"; private static final String SAVEDTODELETEPOS = "savedToDeleteRefPointPosition"; // Konstante, die Resource ID der Testkarte angibt private static final int TESTMAP_RESOURCE = R.drawable.debug_testmap; // ////// NAVIGATION ACTIVITY CONTEXT private Navigation navigation; // ////// MAP VIEW UND DATEN // Wurde das Bild bereits geladen? private boolean isMapLoaded = false; // ////// MARKER FÜR DIE USERPOSITION // Marker für die Position des Users private LocationIcon locationIcon; // ////// BEHANDLUNG VON REFERENZPUNKTEN // Liste der gesetzten Referenzpunkte private HashSet<ReferencePointIcon> refPointIcons = new HashSet<ReferencePointIcon>(); // neu erstellter, aber unbestätigter Referenzpunkt private ReferencePointIcon unacceptedRefPointIcon = null; // Referenzpunkt, der zum Löschen ausgewählt wurde private ReferencePointIcon toDeleteRefPointIcon = null; // //////////////////////////////////////////////////////////////////////// // //////////// CONSTRUCTORS AND INITIALIZATION // //////////////////////////////////////////////////////////////////////// public MapView(Context context) { super(context); init(); } public MapView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MapView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } /** * Initialisiert das MapView-Objekt (wird vom Konstruktor aufgerufen). */ private void init() { // Speichere Activity Context // (Der try-catch-Block ist nur notwendig, damit der Layouteditor von Eclipse nicht meckert... -.-) try { navigation = (Navigation) getContext(); } catch (Exception e) { } } @Override protected Parcelable onSaveInstanceState() { // LargeImageView gibt uns ein Bundle, in dem z.B. die Pan-Daten stecken. Verwende dieses als Basis. Bundle bundle = (Bundle) super.onSaveInstanceState(); // Falls ein neuer Referenzpunkt erstellt werden sollte, speichere seine Position (als Point2D). // (Timestamp nicht, der ist sowieso 0, solange der Punkt noch nicht akzeptiert wurde.) bundle.putSerializable(SAVEDUNACCEPTEDPOS, unacceptedRefPointIcon != null ? unacceptedRefPointIcon.getPosition() : null); // Falls ein Referenzpunkt gelöscht werden sollte, speichere seine Position (als Point2D). bundle.putSerializable(SAVEDTODELETEPOS, toDeleteRefPointIcon != null ? toDeleteRefPointIcon.getPosition() : null); return bundle; } @Override protected void onRestoreInstanceState(Parcelable state) { Bundle bundle = (Bundle) state; // Falls ein neuer Referenzpunkt erstellt werden sollte, stelle diesen wieder her. Point2D unacceptedPos = (Point2D) bundle.getSerializable(SAVEDUNACCEPTEDPOS); if (unacceptedPos != null) { // Referenzpunkt erstellen unacceptedRefPointIcon = null; createUnacceptedReferencePoint(unacceptedPos); } // Falls ein Referenzpunkt gelöscht werden sollte, stelle die Auswahl wieder her. Point2D deleteCandidatePos = (Point2D) bundle.getSerializable(SAVEDTODELETEPOS); if (deleteCandidatePos != null) { // Finde den zu löschenden Referenzpunkt for (ReferencePointIcon refPoint : refPointIcons) { if (refPoint.getPosition().equals(deleteCandidatePos)) { // dieser Punkt war der Löschkandidat registerAsDeletionCandidate(refPoint); break; } } if (toDeleteRefPointIcon == null) { Log.w("MapView/onRestoreInstanceState", "Tried to restore delete candidate but didn't find it: " + deleteCandidatePos); } } // Im Bundle stecken noch Informationen von LargeImageView, z.B. Pan-Daten. Reiche das Bundle also weiter. super.onRestoreInstanceState(bundle); // Darstellung aktualisieren update(); } // //////////////////////////////////////////////////////////////////////// // //////////// USER INPUT HANDLING // //////////////////////////////////////////////////////////////////////// /** * Behandlung von Touchevents. */ // Lint-Warnung "MapView overrides onTouchEvent but not performClick", obwohl sich onTouchEvent[_clickDetection]() // um Aufruf von performClick() kümmern. @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { // Dadurch, dass wir Klicks separat behandeln, können wir hier problemlos in den meisten Zuständen // das Panning und Zooming ausführen. Betrachte also nur Fälle, in denen wir KEIN Panning und Zooming // (oder zusätzlich etwas anderes) wollen. // Sind wir in einem Hilfezustand? if (navigation.state.isHelpState()) { // Hier kein Pan/Zoom, aber Klicks sollen die Hilfe beenden. onTouchEvent_clickDetection(event); return true; } // Führe Defaulthandler aus (der Klicks, Pan und Zoom behandelt) return super.onTouchEvent(event); } @Override public boolean onClickPosition(float clickX, float clickY) { // Fallunterscheidung nach aktuellem Zustand NavigationStates navState = navigation.state; if (navState == NavigationStates.MARK_REFPOINT || navState == NavigationStates.ACCEPT_REFPOINT) { // ZUSTAND: Referenzpunkt setzen ODER akzeptieren. // Klicken bewirkt das Erstellen eines neuen unakzeptierten Referenzpunktes an der geklickten Position return onClickPosition_setRefPoint(clickX, clickY); } else if (navState == NavigationStates.DELETE_REFPOINT) { // ZUSTAND: Löschen eines ausgewählten Referenzpunktes // Klicken bricht Löschaktion ab. navigation.refPointDeleteBack(null); return true; } else if (navState.isHelpState()) { // ZUSTAND: Zustand X mit Schnellhilfe // Klicken bewirkt Ende der Schnellhilfe navigation.endQuickHelp(); return true; } // Kein Klickevent behandelt return false; } /** * Behandlung des "Referenzpunkt setzen"-Modus: * Erstelle einen neuen (unakzeptierten) Referenzpunkt an der angeklickten Stelle. */ private boolean onClickPosition_setRefPoint(float clickX, float clickY) { // Bildposition berechnen, die angeklickt wurde PointF imagePos = screenToImagePosition(clickX, clickY); int xCoord = (int) imagePos.x; int yCoord = (int) imagePos.y; // Erstelle neuen unakzeptierten Referenzpunkt an dieser Stelle // (Sanitycheck der Koordinaten passiert dort) createUnacceptedReferencePoint(new Point2D(xCoord, yCoord)); // Falls unakzeptierter Referenzpunkt erfolgreich gesetzt wurde, wechsel Zustand if (unacceptedRefPointIcon != null) { // nächster Zustand: Bestätigen des gesetzten Referenzpunkts navigation.changeState(NavigationStates.ACCEPT_REFPOINT); } return true; } @Override public void update() { // Super-Methode aufrufen (wichtig) super.update(); if (isMapLoaded) { // Locationmarker aktualisieren updateLocationIcon(); } } @Override public void onTouchPanZoomChange() { // Panning/Zooming wurde durch Touch-Event verändert. // aufhören, den Standort zu fokusieren if (navigation.isPositionTracked()) { navigation.stopTrackingPosition(); } } // //////////////////////////////////////////////////////////////////////// // //////////// DARSTELLUNG // //////////////////////////////////////////////////////////////////////// /** * Aktualisiert die Position des LocationIcons. */ public void updateLocationIcon() { if (locationIcon == null) return; if (navigation.isUserPositionKnown()) { // Wenn die aktuelle Benutzerposition bekannt ist, zeige Position an. // (Extra prüfen, ob es bereits sichtbar ist, bevor wir setVisibility aufrufen, ist etwas unnütz.) locationIcon.show(); // Aktuelle Userposition abfragen Point2D pos = navigation.getUserPosition(); // Locationmarker updaten locationIcon.setPosition(pos); } else { // Locationmarker verstecken, bis wieder neue Koordinaten bekannt sind locationIcon.hide(); } } // //////////////////////////////////////////////////////////////////////// // //////////// LADEN/VERWALTUNG VON BILD UND USERPOSITION // //////////////////////////////////////////////////////////////////////// /** * Lade Bild der Karte in die View. * * @param mapID ID des Karte, gleichzeitig der Dateiname des Bildes (oder 0 für Testkarte) * @throws FileNotFoundException */ public void loadMap(long mapID) throws FileNotFoundException { // Karte laden, während bereits Karte geladen ist, macht keinen Sinn. if (isMapLoaded) return; // Bild in die View laden mittels LargeImageView-Funktionalität if (mapID != 0) { // absoluten Pfad der Bilddatei ermitteln String filename = MapEverApp.getAbsoluteFilePath(String.valueOf(mapID)); try { // Bild der LargeImageView auf diese Datei setzen setImageFilename(filename); } catch (FileNotFoundException e) { Log.e("MapView/loadMap", "Konnte InputStream zu " + mapID + " nicht öffnen!"); e.printStackTrace(); // Exception an Navigation weiterreichen throw e; } } if (mapID == 0) { // Ohne Parameter oder im Fehlerfall wird Testkarte angezeigt setImageResource(TESTMAP_RESOURCE); } Log.d("MapView/loadMap", "Loading map #" + mapID + (mapID == 0 ? " [test_karte]" : "") + " (image size " + getImageWidth() + "x" + getImageHeight() + ")"); isMapLoaded = true; // Erstelle LocationIcon und verstecke es zunächst. // (Erst anzeigen, sobald erste Koordinaten von der Lokalisierung eingetroffen sind.) locationIcon = new LocationIcon(this); locationIcon.hide(); // Darstellung aktualisieren update(); } /** * Zentriert die Ansicht auf die aktuelle Benutzerposition. */ public void centerCurrentLocation() { // Benutzerposition abfragen Point2D location = navigation.getUserPosition(); // Pan-Zentrum auf die Position verschieben (ruft update() auf). setPanCenter(location.x, location.y); } // //////////////////////////////////////////////////////////////////////// // //////////// REFERENZPUNKTE // //////////////////////////////////////////////////////////////////////// // ////// LADEN / VERWALTEN /** * Gibt die Anzahl aktuell gesetzter Referenzpunkte zurück. */ public int countReferencePoints() { return refPointIcons.size(); } /** * Erstellt ein ReferencePointIcon zu einem geladenen Referenzpunkt. * * @param pos Position des Referenzpunktes als Point2D * @param time Zeitstempel des Punktes */ public void createLoadedReferencePoint(Point2D pos, long time) { // Erstelle Referenzpunkt-Icon (repräsentiert zugleich den Referenzpunkt selbst) ReferencePointIcon newRefPointIcon = new ReferencePointIcon(this, pos, time, true); // Füge Referenzpunkt in Liste ein refPointIcons.add(newRefPointIcon); // Wir registrieren den Punkt NICHT beim LDM, weil wir davon ausgehen, dass wir // ihn von dort bekommen haben... // Darstellung aktualisieren update(); } // ////// ERSTELLEN / AKZEPTIEREN /** * Erstellt einen neuen unakzeptierten Referenzpunkt an der angegebenen Bildposition. * * @param pos Position des Referenzpunktes im Bild als Point2D */ public void createUnacceptedReferencePoint(Point2D pos) { // Prüfe die Position auf Sinnhaftigkeit (vermeide Referenzpunkte außerhalb der Bildgrenzen) if (pos.x < 0 || pos.y < 0 || pos.x >= getImageWidth() || pos.y >= getImageHeight()) { // Fehlermeldung als Toast ausgeben Toast.makeText(getContext(), getContext().getString(R.string.navigation_toast_refpoint_out_of_boundaries), Toast.LENGTH_SHORT).show(); return; } // Falls der Nutzer bereits vorher einen Referenzpunkt gesetzt und noch nicht bestätigt hat... if (unacceptedRefPointIcon != null) { // -> ... cancelt das Setzen eines neuen Referenzpunkts den alten Punkt. cancelReferencePoint(); } // Erstelle Referenzpunkt-Icon (repräsentiert zugleich den Referenzpunkt selbst) // (Wir übergeben 0 als Zeit, weil der Timestamp erst beim Akzeptieren feststeht.) unacceptedRefPointIcon = new ReferencePointIcon(this, pos, 0, false); // Darstellung aktualisieren update(); } /** * Der noch unbestätigte Referenzpunkt unacceptedRefPointIcon wurde von der GUI bestätigt. */ public void acceptReferencePoint() { // Nichts tun, falls kein unbestätigter Referenzpunkt vorhanden (sollte nicht passieren) if (unacceptedRefPointIcon == null) return; // Aktuellen Timestamp setzen, weil die GPS-Koordinaten jünger als der unakzeptierte Punkt sein könnten. unacceptedRefPointIcon.setTimestamp(SystemClock.elapsedRealtime()); // Referenzpunkt bei Lokalisierung eintragen boolean registerSuccessful = navigation.registerReferencePoint(unacceptedRefPointIcon.getPosition(), unacceptedRefPointIcon.getTimestamp()); if (registerSuccessful) { // Füge Referenzpunkt in Liste ein refPointIcons.add(unacceptedRefPointIcon); // Beginne das Fading des RefPunkts unacceptedRefPointIcon.fadeOut(); } else { // Referenzpunkt konnte nicht registriert werden -> ungültige/unsinnige Koordinaten? // Fehlermeldung anzeigen und Referenzpunkt löschen. Log.w("MapView/acceptReferencePoint", "addMarker for point " + unacceptedRefPointIcon.getPosition() + " at time " + unacceptedRefPointIcon.getTimestamp() + " returned false"); Toast.makeText(getContext(), getContext().getString(R.string.navigation_toast_refpoint_already_set_for_this_position), Toast.LENGTH_SHORT).show(); // unakzeptierten Punkt verwerfen cancelReferencePoint(); } // Referenz auf den Referenzpunkt freigeben unacceptedRefPointIcon = null; } /** * Der noch unbestätigte Referenzpunkt soll verworfen werden. */ public void cancelReferencePoint() { // Nichts tun, falls kein unbestätigter Referenzpunkt vorhanden (sollte nicht passieren) if (unacceptedRefPointIcon == null) return; // unakzeptierten Referenzpunkt aus der Anzeige löschen unacceptedRefPointIcon.detach(); // Referenz auf den Referenzpunkt freigeben (Objekt wird dem überlassen) unacceptedRefPointIcon = null; } // ////// LÖSCHEN /** * Wird aufgerufen, wenn ein Referenzpunkt angeklickt wird, und setzt (wenn im richtigen Zustand) diesen Punkt * als Kandidat zum Löschen. */ public boolean registerAsDeletionCandidate(ReferencePointIcon deletionCandidate) { // Prüfe, ob wir im RUNNING-Zustand sind oder bereits einen anderen Löschkandidaten ausgewählt haben. if (navigation.state != NavigationStates.RUNNING && navigation.state != NavigationStates.DELETE_REFPOINT) { // Wenn nicht, tu nichts! Die aufrufende onClick-Funktion soll das Event als nicht behandelt weitergeben. return false; } // der Zustand ist nun "Referenzpunkt löschen" navigation.changeState(NavigationStates.DELETE_REFPOINT); // Wenn bereits einer zum Löschen ausgewählt ist, ändere die Auswahl und lass den alten wieder ausblenden if (toDeleteRefPointIcon != null) { toDeleteRefPointIcon.fadeOut(); } // Setze diesen Referenzpunkt als Löschkandidaten toDeleteRefPointIcon = deletionCandidate; // Blende Referenzpunkt zur Visualisierung ein. toDeleteRefPointIcon.fadeIn(); // return true: Löschkandidat wurde ausgewählt (aufrufende onClick-Funktion gibt ebenfalls true zurück) return true; } /** * Der ausgewählte Referenzpunkt soll gelöscht werden. */ public void deleteReferencePoint() { // Nichts tun, falls kein Löschkandidat vorhanden (sollte nicht passieren) if (toDeleteRefPointIcon == null) return; // Referenzpunkt aus der Lokalisierung löschen. navigation.unregisterReferencePoint(toDeleteRefPointIcon.getPosition()); // Entferne Referenzpunkt aus der Liste und von der Darstellung refPointIcons.remove(toDeleteRefPointIcon); toDeleteRefPointIcon.detach(); // Referenz auf den Referenzpunkt freigeben (Objekt wird dem überlassen) toDeleteRefPointIcon = null; } /** * Der ausgewählte Referenzpunkt soll nicht gelöscht werden. */ public void dontDeleteReferencePoint() { // Nichts tun, falls kein Löschkandidat vorhanden (sollte nicht passieren) if (toDeleteRefPointIcon == null) return; // Der Punkt wird wieder ausgeblendet und toDeleteRefPointView freigegeben toDeleteRefPointIcon.fadeOut(); toDeleteRefPointIcon = null; } }