package edu.vanderbilt.vm.guide.util; import java.util.ArrayList; import java.util.List; import java.util.Set; import org.jgrapht.Graphs; import org.jgrapht.alg.DijkstraShortestPath; import org.jgrapht.alg.KruskalMinimumSpanningTree; import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.graph.SimpleWeightedGraph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.location.Location; import android.util.SparseArray; import com.google.android.gms.maps.model.LatLng; import edu.vanderbilt.vm.guide.container.Agenda; import edu.vanderbilt.vm.guide.container.MapVertex; import edu.vanderbilt.vm.guide.container.Place; import edu.vanderbilt.vm.guide.container.Route; import edu.vanderbilt.vm.guide.db.GuideDBConstants.NodeTable; import edu.vanderbilt.vm.guide.db.GuideDBOpenHelper; /** * This class holds singletons of certain objects we need to share throughout * the application, such as the user's agenda. This is simpler and easier than * using a SQLite database to hold the agenda and allows us to use several * methods to make data transactions with the agenda easier. * * @author nicholasking */ public class GlobalState { // Agenda singleton // private static Agenda sUserAgendaSingleton = new Agenda(); private static Logger logger = LoggerFactory.getLogger("util.GlobalState"); private GlobalState() { throw new AssertionError("Do not instantiate this class."); } public static Agenda getUserAgenda() { return sUserAgendaSingleton; } // End Agenda singleton // Database singleton // private static SQLiteDatabase sReadableDb, sWritableDb; private static GuideDBOpenHelper sHelper; public static SQLiteDatabase getReadableDatabase(Context c) { if (sHelper == null) { sHelper = new GuideDBOpenHelper(c.getApplicationContext()); } if (sReadableDb == null) { sReadableDb = sHelper.getReadableDatabase(); } return sReadableDb; } public static SQLiteDatabase getWritableDatabase(Context c) { if (sHelper == null) { sHelper = new GuideDBOpenHelper(c.getApplicationContext()); } if (sWritableDb == null) { sWritableDb = sHelper.getWritableDatabase(); } return sWritableDb; } // End database singleton // // Graph singleton // private static SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> sGraph; private static SparseArray<MapVertex> sVertexMap = new SparseArray<MapVertex>(); /** * Builds and returns a graph built from the node table. This method could * take a long time to execute the first time it is called since it will * have to build the graph, so consider calling this method on a background * thread the first time. * * @param c The Context to use for accessing the database * @return a SimpleWeightedGraph built from the Node table */ public static SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> getWeightedGraph(Context c) { if (sGraph == null) { sGraph = new SimpleWeightedGraph<MapVertex, DefaultWeightedEdge>( DefaultWeightedEdge.class); SQLiteDatabase db = getReadableDatabase(c); Cursor nodeCursor = db.query(NodeTable.NODE_TABLE_NAME, new String[] { NodeTable.ID_COL, NodeTable.LAT_COL, NodeTable.LON_COL, NodeTable.NEIGHBOR_COL }, null, null, null, null, null); if (nodeCursor != null) { int id_ix = nodeCursor.getColumnIndex(NodeTable.ID_COL); int lat_ix = nodeCursor.getColumnIndex(NodeTable.LAT_COL); int lon_ix = nodeCursor.getColumnIndex(NodeTable.LON_COL); int neighbors_ix = nodeCursor.getColumnIndex(NodeTable.NEIGHBOR_COL); while (nodeCursor.moveToNext()) { double lat = nodeCursor.getDouble(lat_ix); double lon = nodeCursor.getDouble(lon_ix); String neighbors = nodeCursor.getString(neighbors_ix); int id = nodeCursor.getInt(id_ix); // Make an object to hold the values and add it to // a map for making a graph later on MapVertex mv = new MapVertex(); mv.lat = lat; mv.lon = lon; mv.id = id; String[] neighborStrs = neighbors.split(","); mv.neighbors = new int[neighborStrs.length]; for (int i = 0; i < neighborStrs.length; i++) { if (neighborStrs[i].length() > 0) { mv.neighbors[i] = Integer.parseInt(neighborStrs[i]); } } sVertexMap.put(id, mv); } } // Build the graph for (int i = 0; i < sVertexMap.size(); i++) { int mvId = sVertexMap.keyAt(i); MapVertex mv = sVertexMap.get(mvId); /* * Do not add the vertices for places to the graph. These * vertices should only be present in the graph when finding * paths and routes. Having these extra vertices in the graph * results in the algorithms finding paths through buildings, * which is not what we want. */ if (!isPlace(mv)) { addVertexToGraph(sGraph, mv); } } } return sGraph; } private static void addVertexToGraph(SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> graph, MapVertex mv) { graph.addVertex(mv); for (int neighborId : mv.neighbors) { MapVertex mv2 = sVertexMap.get(neighborId); // XXX: Bad hack job! if (mv2 == null) continue; /* * Assuming the MapVertex equals() and hashCode() methods work * correctly, addVertex() will ensure there are no duplicate * vertices in the graph */ graph.addVertex(mv2); try { /* * XXX: Needs debugging, for some reason there are vertices * adjacent to a vertex with id "0" */ LatLng latlng1 = new LatLng(mv.lat, mv.lon); LatLng latlng2 = new LatLng(mv2.lat, mv2.lon); DefaultWeightedEdge e = graph.addEdge(mv, mv2); // e will be null if the edge already existed if (e != null) { graph.setEdgeWeight(e, distanceBetween(latlng1, latlng2)); } } catch (NullPointerException e) { logger.error("mv1 id: {}, mv2 id: {}", mv.id, neighborId); } } } public static boolean isPlace(MapVertex mv) { return mv.id <= GuideConstants.MAX_PLACE_ID; } /** * Returns the MapVertex with the given id in the graph. This will match the * node's id in the nodes.json file. If the graph has not been built yet, * this method will return null. Be sure to call getWeightedGraph() so the * graph will be built before calling this method. * * @param id the id to match * @return the MapVertex with the given id */ public static MapVertex getMapVertexWithId(int id) { if (sGraph == null) { return null; } else { return sVertexMap.get(id); } } public static SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> mst( SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> graph) { KruskalMinimumSpanningTree<MapVertex, DefaultWeightedEdge> mstFinder = new KruskalMinimumSpanningTree<MapVertex, DefaultWeightedEdge>( graph); Set<DefaultWeightedEdge> mstEdges = mstFinder.getEdgeSet(); SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> mst = new SimpleWeightedGraph<MapVertex, DefaultWeightedEdge>( DefaultWeightedEdge.class); for (MapVertex mv : graph.vertexSet()) { mst.addVertex(mv); } for (DefaultWeightedEdge e : mstEdges) { mst.addEdge(graph.getEdgeSource(e), graph.getEdgeTarget(e)); mst.setEdgeWeight(e, graph.getEdgeWeight(e)); } return mst; } public static SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> shortestPath( SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> graph, MapVertex start, MapVertex end) { // Temporarily add the start and end vertices to the graph addVertexToGraph(graph, start); addVertexToGraph(graph, end); // Find the path DijkstraShortestPath<MapVertex, DefaultWeightedEdge> pathSolver = new DijkstraShortestPath<MapVertex, DefaultWeightedEdge>( graph, start, end); SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> path = new SimpleWeightedGraph<MapVertex, DefaultWeightedEdge>( DefaultWeightedEdge.class); List<DefaultWeightedEdge> pathEdges = pathSolver.getPath().getEdgeList(); for (DefaultWeightedEdge e : pathEdges) { MapVertex mv1 = graph.getEdgeSource(e); MapVertex mv2 = graph.getEdgeTarget(e); path.addVertex(mv1); path.addVertex(mv2); path.addEdge(mv1, mv2); } // Remove the start and end vertices graph.removeVertex(start); graph.removeVertex(end); return path; } public static Route findRoute(Location deviceLoc, Agenda agenda, Context c) { logger.trace("Finding route through agenda."); if (agenda == null || agenda.size() == 0) { logger.trace("{}", (agenda == null) ? "Agenda parameter null" : "Agenda size 0"); return null; } // Load the Agenda into a list for route calculation // Find closest place List<Place> unusedPlaceList = new ArrayList<Place>(agenda.size()), orderedPlaceList = new ArrayList<Place>( agenda.size()); Place closestPlace = null; double minDist = Double.POSITIVE_INFINITY, deviceLat = deviceLoc.getLatitude(), deviceLon = deviceLoc .getLongitude(); for (Place place : agenda) { double dist = Geomancer.findDistance(place.getLatitude(), place.getLongitude(), deviceLat, deviceLon); if (dist < minDist) { minDist = dist; closestPlace = place; } unusedPlaceList.add(place); } logger.debug("Closest place to device location: {}", closestPlace.getName()); SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> rtGraph = new SimpleWeightedGraph<MapVertex, DefaultWeightedEdge>( DefaultWeightedEdge.class); int startNodeId = Geomancer.findClosestNodeId(deviceLoc, c); SimpleWeightedGraph<MapVertex, DefaultWeightedEdge> path = shortestPath( getWeightedGraph(c), getMapVertexWithId(startNodeId), getMapVertexWithId(closestPlace.getUniqueId())); // Union the path and the route graph Graphs.addGraph(rtGraph, path); // Update the place lists unusedPlaceList.remove(closestPlace); orderedPlaceList.add(closestPlace); // XXX: This loop needs to be refactored to make this code more DRY while (!unusedPlaceList.isEmpty()) { Place currentPlace = closestPlace; // Find the closest place to the place we most recently added // to the route minDist = Double.POSITIVE_INFINITY; for (Place place : unusedPlaceList) { double dist = Geomancer.findDistance(currentPlace.getLatitude(), currentPlace.getLongitude(), place.getLatitude(), place.getLongitude()); if (dist < minDist) { closestPlace = place; minDist = dist; } } logger.debug("Closest place to {}: {}", currentPlace.getName(), closestPlace.getName()); path = shortestPath(getWeightedGraph(c), getMapVertexWithId(currentPlace.getUniqueId()), getMapVertexWithId(closestPlace.getUniqueId())); // Union the path and the route graph Graphs.addGraph(rtGraph, path); // Update the place lists unusedPlaceList.remove(closestPlace); orderedPlaceList.add(closestPlace); } return new Route(orderedPlaceList, rtGraph); } public static double distanceBetween(LatLng point1, LatLng point2) { return distanceInRadians(point1, point2) * GuideConstants.EARTH_RADIUS; } /** * Modified from LatLngTool.java of simplelatlng project at * https://code.google * .com/p/simplelatlng/source/browse/src/main/java/com/javadocmd * /simplelatlng/LatLngTool.java */ public static double distanceInRadians(LatLng point1, LatLng point2) { double lat1R = Math.toRadians(point1.latitude); double lat2R = Math.toRadians(point2.latitude); double dLatR = Math.abs(lat2R - lat1R); double dLngR = Math.abs(Math.toRadians(point2.longitude - point1.longitude)); double a = Math.sin(dLatR / 2) * Math.sin(dLatR / 2) + Math.cos(lat1R) * Math.cos(lat2R) * Math.sin(dLngR / 2) * Math.sin(dLngR / 2); return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } }