Java tutorial
/* Copyright 2012 Nick Malleson This file is part of RepastCity. RepastCity 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. RepastCity 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 RepastCity. If not, see <http://www.gnu.org/licenses/>. */ package foodsimulationmodel.pathmapping; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.Map; import java.util.Vector; import org.apache.commons.lang.ArrayUtils; import org.geotools.referencing.GeodeticCalculator; import cern.colt.Arrays; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.operation.distance.DistanceOp; import repast.simphony.space.gis.Geography; import repast.simphony.space.graph.RepastEdge; import repast.simphony.space.graph.ShortestPath; import foodsimulationmodel.agents.IAgent; import foodsimulationmodel.environment.ContextManager; import foodsimulationmodel.environment.GlobalVars; import foodsimulationmodel.exceptions.RoutingException; /** * Create routes around a GIS road network. The <code>setRoute</code> function actually finds the route and can be * overridden by subclasses to create different types of Route. See the method documentation for details of how routes * are calculated. * * <p> * A "unit of travel" is the distance that an agent can cover in one iteration (one square on a grid environment or the * distance covered at walking speed in an iteration on a GIS environment). This will change depending on the type of * transport the agent is using. E.g. if they are in a car they will be able to travel faster, similarly if they are * travelling along a transort route they will cover more ground. * </p> * * @author Nick Malleson */ public class Route implements Cacheable { private static Logger LOGGER = Logger.getLogger(Route.class.getName()); static { // Route.routeCache = new Hashtable<CachedRoute, CachedRoute>(); } private IAgent agent; private Coordinate destination; private IAgent destinationAgent; /* * The route consists of a list of coordinates which describe how to get to the destination. Each coordinate might * have an attached 'speed' which acts as a multiplier and is used to indicate whether or not the agent is * travelling along a transport route (i.e. if a coordinate has an attached speed of '2' the agent will be able to * get to the next coordinate twice as fast as they would do if they were walking). The current position incicate * where in the lists of coords the agent is up to. Other attribute information about the route can be included as * separate arrays with indices that match those of the 'route' array below. */ private int currentPosition; private List<Coordinate> routeX; private List<Double> routeSpeedsX; /* * This maps route coordinates to their containing Road, used so that when travelling we know which road/community * the agent is on. private */ private List<Road> roadsX; // Record which function has added each coord, useful for debugging private List<String> routeDescriptionX; /* * Cache every coordinate which forms a road so that Route.onRoad() is quicker. Also save the Road(s) they are part * of, useful for the agent's awareness space (see getRoadFromCoordCache()). */ private static volatile Map<Coordinate, List<Road>> coordCache; /* * Cache the nearest road Coordinate to every building for efficiency (agents usually/always need to get from the * centroids of houses to/from the nearest road). */ private static volatile NearestRoadCoordCache nearestRoadCoordCache; /* * Store which road every building is closest to. This is used to efficiently add buildings to the agent's awareness * space */ private static volatile BuildingsOnRoadCache buildingsOnRoadCache; // To stop threads competing for the cache: private static Object buildingsOnRoadCacheLock = new Object(); /* * Store a route once it has been created, might be used later (note that the same object acts as key and value). */ // TODO Re-think route caching, would be better to cache the whole Route object // private static volatile Map<CachedRoute, CachedRoute> routeCache; // /** Store a route distance once it has been created */ // private static volatile Map<CachedRouteDistance, Double> routeDistanceCache; /* * Keep a record of the last community and road passed so that the same buildings/communities aren't added to the * cognitive map multiple times (the agent could spend a number of iterations on the same road or community). */ private Road previousRoad; private Area previousArea; /** * Creates a new Route object. * * @param agent * The agent which this Route will control. * * @param destination * The agent's destination. * * @param destinationAgent * The agent they're heading to. * */ public Route(IAgent agent, Coordinate destination, IAgent destinationAgent) { this.destination = destination; this.agent = agent; this.destinationAgent = destinationAgent; } /** * Find a route from the origin to the destination. A route is a list of Coordinates which describe the route to a * destination restricted to a road network. The algorithm consists of three major parts: * <ol> * <li>Find out if the agent is on a road already, if not then move to the nearest road segment</li> * <li>Get from the current location (probably mid-point on a road) to the nearest junction</li> * <li>Travel to the junction which is closest to our destination (using Dijkstra's shortest path)</li> * <li>Get from the final junction to the road which is nearest to the destination * <li> * <li>Move from the road to the destination</li> * </ol> * * @throws Exception */ protected void setRoute() throws Exception { long time = System.nanoTime(); // this.routeX = new ArrayList<Coordinate>(); // this.roadsX = new ArrayList<Road>(); // this.routeDescriptionX = new ArrayList<String>(); // this.routeSpeedsX = new ArrayList<Double>(); this.routeX = new Vector<Coordinate>(); this.roadsX = new Vector<Road>(); this.routeDescriptionX = new Vector<String>(); this.routeSpeedsX = new Vector<Double>(); LOGGER.log(Level.FINER, "Planning route for: " + this.agent.toString() + " to: " + this.destinationAgent.toString()); if (atDestination()) { LOGGER.log(Level.WARNING, "Already at destination, cannot create a route for " + this.agent.toString()); return; } Coordinate currentCoord = ContextManager.getAgentGeometry(this.agent).getCoordinate(); Coordinate destCoord = this.destination; // See if a route has already been cached. // CachedRoute cachedRoute = new CachedRoute(currentCoord, destCoord, this.agent.getTransportAvailable()); // synchronized (Route.routeCache) { // if (Route.routeCache.containsKey(cachedRoute)) { // TempLogger.out("Route.setRoute, found a cached route from " + currentCoord + " to " + destCoord // + " using available transport " + this.agent.getTransportAvailable() + ", returning it."); // // Return a clone of the route that is stored in the cache // // TODO do we need clones here? I don't think so... // CachedRoute cr = Route.routeCache.get(cachedRoute); // // this.routeX = Cloning.copy(cr.getRoute()); // // this.roadsX = new ArrayList<Road>(cr.getRoads()); // // this.routeSpeedsX = new ArrayList<Double>(cr.getRouteSpeeds()); // // this.routeDescriptionX = new ArrayList<String>(cr.getDescriptions()); // this.routeX = new Vector<Coordinate>(cr.getRoute()); // this.roadsX = new Vector<Road>(cr.getRoads()); // this.routeSpeedsX = new Vector<Double>(cr.getRouteSpeeds()); // this.routeDescriptionX = new Vector<String>(cr.getDescriptions()); // // return; // } // } // synchronized // No route cached, have to create a new one (and cache it at the end). try { /* * See if the current position and the destination are on road segments. If the destination is not on a road * segment we have to move to the closest road segment, then onto the destination. */ boolean destinationOnRoad = true; Coordinate finalDestination = null; if (!coordOnRoad(currentCoord)) { /* * Not on a road so the first coordinate to add to the route is the point on the closest road segment. */ currentCoord = getNearestRoadCoord(currentCoord); addToRoute(currentCoord, Road.nullRoad, 1, "setRoute() initial"); } if (!coordOnRoad(destCoord)) { /* * Not on a road, so need to set the destination to be the closest point on a road, and set the * destinationOnRoad boolean to false so we know to add the final dest coord at the end of the route */ destinationOnRoad = false; finalDestination = destCoord; // Added to route at end of alg. destCoord = getNearestRoadCoord(destCoord); } /* * Find the nearest junctions to our current position (road endpoints) */ // Start by Finding the road that this coordinate is on /* * TODO EFFICIENCY: often the agent will be creating a new route from a building so will always find the * same road, could use a cache. Even better, could implement a cache in FindNearestObject() method! */ Road currentRoad = Route.findNearestObject(currentCoord, ContextManager.roadProjection, null, GlobalVars.GEOGRAPHY_PARAMS.BUFFER_DISTANCE.LARGE); // Find which Junction is closest to us on the road. List<Junction> currentJunctions = currentRoad.getJunctions(); /* Find the nearest Junctions to our destination (road endpoints) */ // Find the road that this coordinate is on Road destRoad = Route.findNearestObject(destCoord, ContextManager.roadProjection, null, GlobalVars.GEOGRAPHY_PARAMS.BUFFER_DISTANCE.SMALL); // Find which Junction connected to the edge is closest to the coordinate. List<Junction> destJunctions = destRoad.getJunctions(); /* * Now have four possible routes (2 origin junctions, 2 destination junctions) need to pick which junctions * form shortest route */ Junction[] routeEndpoints = new Junction[2]; List<RepastEdge<Junction>> shortestPath = getShortestRoute(currentJunctions, destJunctions, routeEndpoints); // NetworkEdge<Junction> temp = (NetworkEdge<Junction>) // shortestPath.get(0); Junction currentJunction = routeEndpoints[0]; Junction destJunction = routeEndpoints[1]; /* Add the coordinates describing how to get to the nearest junction */ List<Coordinate> tempCoordList = new Vector<Coordinate>(); this.getCoordsAlongRoad(currentCoord, currentJunction.getCoords(), currentRoad, true, tempCoordList); addToRoute(tempCoordList, currentRoad, 1, "getCoordsAlongRoad (toJunction)"); /* * Add the coordinates and speeds etc which describe how to move along the chosen path */ this.getRouteBetweenJunctions(shortestPath, currentJunction); /* * Add the coordinates describing how to get from the final junction to the destination. */ tempCoordList.clear(); this.getCoordsAlongRoad(ContextManager.junctionGeography.getGeometry(destJunction).getCoordinate(), destCoord, destRoad, false, tempCoordList); addToRoute(tempCoordList, destRoad, 1, "getCoordsAlongRoad (fromJunction)"); if (!destinationOnRoad) { addToRoute(finalDestination, Road.nullRoad, 1, "setRoute final"); } // Check that a route has actually been created checkListSizes(); // If the algorithm was better no coordinates would have been duplicated // removePairs(); // Check lists are still the same size. checkListSizes(); } catch (RoutingException e) { LOGGER.log(Level.SEVERE, "Route.setRoute(): Problem creating route for " + this.agent.toString() + " going from " + currentCoord.toString() + " to " + this.destination.toString() + "(" + (this.destinationAgent == null ? "" : this.destinationAgent.toString()) + ") See earlier messages error messages for more info."); throw e; } // Cache the route and route speeds // List<Coordinate> routeClone = Cloning.copy(theRoute); // LinkedHashMap<Coordinate, Double> routeSpeedsClone = Cloning.copy(this.routeSpeeds); // cachedRoute.setRoute(routeClone); // cachedRoute.setRouteSpeeds(routeSpeedsClone); // cachedRoute.setRoute(this.routeX, this.roadsX, this.routeSpeedsX, this.routeDescriptionX); // synchronized (Route.routeCache) { // // Same cached route is both value and key // Route.routeCache.put(cachedRoute, cachedRoute); // } // TempLogger.out("...Route cacheing new route with unique id " + cachedRoute.hashCode()); LOGGER.log(Level.FINER, "Route Finished planning route for " + this.agent.toString() + "with " + this.routeX.size() + " coords in " + (0.000001 * (System.nanoTime() - time)) + "ms."); // Finished, just check that the route arrays are all in sync assert this.roadsX.size() == this.routeX.size() && this.routeDescriptionX.size() == this.routeSpeedsX.size() && this.roadsX.size() == this.routeDescriptionX.size(); } private void checkListSizes() { assert this.roadsX.size() > 0 && this.roadsX.size() == this.routeX.size() && this.routeDescriptionX.size() == this.routeSpeedsX.size() && this.roadsX.size() == this.routeDescriptionX.size() : this.routeX.size() + "," + this.roadsX.size() + "," + this.routeDescriptionX.size() + "," + this.routeSpeedsX.size(); } /** * Convenience function that can be used to add details to the route. This should be used rather than updating * individual lists because it makes sure that all lists stay in sync * * @param coord * The coordinate to add to the route * @param road * The road that the coordinate is part of * @param speed * The speed that the road can be travelled along * @param description * A description of why the coordinate has been added */ private void addToRoute(Coordinate coord, Road road, double speed, String description) { this.routeX.add(coord); this.roadsX.add(road); this.routeSpeedsX.add(speed); this.routeDescriptionX.add(description); } /** * A convenience for adding to the route that will add a number of coordinates with the same description, road and * speed. * * @param coord * A list of coordinates to add to the route * @param road * The road that the coordinates are part of * @param speed * The speed that the road can be travelled along * @param description * A description of why the coordinates have been added */ private void addToRoute(List<Coordinate> coords, Road road, double speed, String description) { for (Coordinate c : coords) { this.routeX.add(c); this.roadsX.add(road); this.routeSpeedsX.add(speed); this.routeDescriptionX.add(description); } } /** * Travel towards our destination, as far as we can go this turn. * <p> * Also adds houses to the agent's cognitive environment. This is done by saving each coordinate the person passes, * creating a polygon with a radius given by the "cognitive_map_search_radius" and adding all houses which touch the * polygon. * <p> * Note: the agent might move their position many times depending on how far they are allowed to move each turn, * this requires many calls to geometry.move(). This function could be improved (quite easily) by working out where * the agent's final destination will be, then calling move() just once. * * @param housesPassed * If not null then the buildings which the agent passed during their travels this iteration will be * calculated and stored in this array. This can be useful if a agent needs to know which houses it has * just passed and, therefore, which are possible victims. This isn't done by default because it's quite * an expensive operation (lots of geographic tests which must be carried out in each iteration). If the * array is null then the houses passed are not calculated. * @return null or the buildings passed during this iteration if housesPassed boolean is true * @throws Exception */ public void travel() throws Exception { // Check that the route has been created if (this.routeX == null) { this.setRoute(); } try { if (this.atDestination()) { return; } double time = System.nanoTime(); // Store the roads the agent walks along (used to populate the awareness space) // List<Road> roadsPassed = new ArrayList<Road>(); double distTravelled = 0; // The distance travelled so far Coordinate currentCoord = null; // Current location Coordinate target = null; // Target coordinate we're heading for (in route list) boolean travelledMaxDist = false; // True when travelled maximum distance this iteration double speed; // The speed to travel to next coord GeometryFactory geomFac = new GeometryFactory(); currentCoord = ContextManager.getAgentGeometry(this.agent).getCoordinate(); while (!travelledMaxDist && !this.atDestination()) { target = this.routeX.get(this.currentPosition); speed = this.routeSpeedsX.get(this.currentPosition); /* * TODO Remember which roads have been passed, used to work out what should be added to cognitive map. * Only add roads once the agent has moved all the way down them */ // roadsPassed.add(this.roads.get(this.previousRouteCoord())); // Work out the distance and angle to the next coordinate double[] distAndAngle = new double[2]; Route.distance(currentCoord, target, distAndAngle); // divide by speed because distance might effectively be shorter double distToTarget = distAndAngle[0] / speed; // If we can get all the way to the next coords on the route then just go there if (distTravelled + distToTarget < GlobalVars.GEOGRAPHY_PARAMS.TRAVEL_PER_TURN) { distTravelled += distToTarget; currentCoord = target; // See if agent has reached the end of the route. if (this.currentPosition == (this.routeX.size() - 1)) { ContextManager.moveAgent(this.agent, geomFac.createPoint(currentCoord)); // ContextManager.agentGeography.move(this.agent, geomFac.createPoint(currentCoord)); break; // Break out of while loop, have reached end of route. } // Haven't reached end of route, increment the counter this.currentPosition++; } // if can get all way to next coord // Check if dist to next coordinate is exactly same as maximum // distance allowed to travel (unlikely but possible) else if (distTravelled + distToTarget == GlobalVars.GEOGRAPHY_PARAMS.TRAVEL_PER_TURN) { travelledMaxDist = true; ContextManager.moveAgent(agent, geomFac.createPoint(target)); // ContextManager.agentGeography.move(agent, geomFac.createPoint(target)); this.currentPosition++; LOGGER.log(Level.WARNING, "Travel(): UNUSUAL CONDITION HAS OCCURED!"); } else { // Otherwise move as far as we can towards the target along the road we're on. // Move along the vector the maximum distance we're allowed this turn (take into account relative // speed) double distToTravel = (GlobalVars.GEOGRAPHY_PARAMS.TRAVEL_PER_TURN - distTravelled) * speed; // Move the agent, first move them to the current coord (the first part of the while loop doesn't do // this for efficiency) // ContextManager.agentGeography.move(this.agent, geomFac.createPoint(currentCoord)); ContextManager.moveAgent(this.agent, geomFac.createPoint(currentCoord)); // Now move by vector towards target (calculated angle earlier). ContextManager.moveAgentByVector(this.agent, distToTravel, distAndAngle[1]); // ContextManager.agentGeography.moveByVector(this.agent, distToTravel, distAndAngle[1]); travelledMaxDist = true; } // else } // while // this.printRoute(); /* * TODO Agent has finished moving, now just add all the buildings and communities passed to their awareness * space (unless they're on a transport route). Note also that if on a transport route without an associated * road no roads are added to the 'roads' map so even if the check wasn't made here no buildings would be * added anyway. */ // Community c = null; // if (!this.onTransportRoute) { // String outputString = "Route.travel() adding following to awareness space for '" // + this.agent.toString() + "':"; // // roadsPassed will have duplicates, this is used to ignore them // Road current = roadsPassed.get(0); // // TODO The next stuff is a mess when it comes to adding communities to the memory. Need to go // // through and make sure communities aren't added too many times (i.e. more than once for each journey) // // and that they are always added when they should be. // // for (Road r : roadsPassed) { // last road in list is the one the // // agent finishes iteration on // if (r != null && roadsPassed.get(0) != null && !current.equals(r)) { // // Check road isn't null () and that buildings on road haven't already been added // // (road can be null when coords that aren't part of a road are added to the route) // current = r; // if (r.equals(this.previousRoad)) { // // The agent has just passed over this road, don't add the buildings or communities again // } else { // outputString += "\n\t" + r.toString() + ": "; // List<Building> passedBuildings = getBuildingsOnRoad(r); // List<Community> passedCommunities = new ArrayList<Community>(); // if (passedBuildings != null) { // There might not be any buildings close to the road (unlikely) // outputString += passedBuildings.toString(); // this.passedObjects(passedBuildings, Building.class); // // For efficiency just find one of the building's communities and hope no other // // communities were passed through - NO! I'VE CHANGED THIS BELOW! // c = passedBuildings.get(0).getCommunity(); // // Check all buildings to make sure that if the agent has passed more than one community // // then they are all added. // for (Building b : passedBuildings) { // if (!passedCommunities.contains(b.getCommunity())) { // passedCommunities.add(b.getCommunity()); // } // } // for (Community com : passedCommunities) { // if (com != null) { // this.passedObject(com, Community.class); // } // } // // } else { // Community won't have been added because no buildings passed, use slow method // c = GlobalVars.COMMUNITY_ENVIRONMENT.getObjectAt(Community.class, currentCoord); // if (c != null) { // this.passedObject(c, Community.class); // } // // TODO I think the following line is wrong, if the agent has made // // a long move they might have passed right through a community that doesn't // // have any buildings, perhaps this should check *all* the communities that touch // // the road, not just the community the agent finished the move in (i.e. currentCoord) // passedCommunities.add(GlobalVars.COMMUNITY_ENVIRONMENT.getObjectAt(Community.class, // currentCoord)); // } // } // } // } // for roadsPassed // TempLogger.out(outputString + "\n"); // } // if !onTransportRoute // else { // TempLogger.out("Route.travel() not adding to burglar '" + this.agent.toString() // + "' awareness space beecause on transport route: "); // } // // // Finally set the previousRoad and previousCommunity so that if these haven't changed in the next // iteration they're not added to // // the cognitive map again. // this.previousRoad = roadsPassed.get(roadsPassed.size() - 1); // // this.previousCommunity = c; // This was the most recent community passed over // // TempLogger.out("...Finished Travelling(" + (0.000001 * (System.nanoTime() - time)) + "ms)"); // // } // synchronized GlobalVars.TRANSPORT_PARAMS.currentBurglar } catch (Exception e) { LOGGER.log(Level.SEVERE, "Route.trave(): Caught error travelling for " + this.agent.toString() + " going to " + "destination " + (this.destinationAgent == null ? "" : this.destinationAgent.toString() + ")")); throw e; } // catch exception } /** * Get the distance (on a network) between the origin and destination. Take into account the Burglar because they * might be able to speed up the route by using different transport methods. Actually calculates the distance * between the nearest Junctions between the source and destination. Note that the GRID environment doesn't have any * transport routes in it so all distances will always be the same regardless of the agent. * * @param agent * @param destination * @return */ public double getDistance(IAgent theBurglar, Coordinate origin, Coordinate destination) { // // See if this distance has already been calculated // if (Route.routeDistanceCache == null) { // Route.routeDistanceCache = new Hashtable<CachedRouteDistance, Double>(); // } // CachedRouteDistance crd = new CachedRouteDistance(origin, destination, theBurglar.getTransportAvailable()); // // synchronized (Route.routeDistanceCache) { // Double dist = Route.routeDistanceCache.get(crd); // if (dist != null) { // TempLogger.out("Route.ggetDistance, found a cached route distance from " + origin + " to " // + destination + " using available transport " + theBurglar.getTransportAvailable() // + ", returning it."); // return dist; // } // } // No distance in the cache, calculate it synchronized (GlobalVars.TRANSPORT_PARAMS.currentBurglarLock) { GlobalVars.TRANSPORT_PARAMS.currentAgent = theBurglar; // Find the closest Junctions to the origin and destination double minOriginDist = Double.MAX_VALUE; double minDestDist = Double.MAX_VALUE; double dist; Junction closestOriginJunc = null; Junction closestDestJunc = null; DistanceOp distOp = null; GeometryFactory geomFac = new GeometryFactory(); // TODO EFFICIENCY: here could iterate over near junctions instead of all? for (Junction j : ContextManager.junctionContext.getObjects(Junction.class)) { // Check that the agent can actually get to the junction (if might be part of a transport route // that the agent doesn't have access to) Point juncPoint = geomFac.createPoint(j.getCoords()); distOp = new DistanceOp(juncPoint, geomFac.createPoint(origin)); dist = distOp.distance(); if (dist < minOriginDist) { minOriginDist = dist; closestOriginJunc = j; } // Destination distOp = new DistanceOp(juncPoint, geomFac.createPoint(destination)); dist = distOp.distance(); if (dist < minDestDist) { minDestDist = dist; closestDestJunc = j; } } // for Junctions // Return the shortest path plus the distance from the origin/destination to their junctions // TODO NOTE: Bug in ShortestPath so have to make finalize is called, otherwise following lines are // neater // - MAYBE THIS HAS BEEN FIXED BY REPAST NOW. // return (new ShortestPath<Junction>(EnvironmentFactory.getRoadNetwork(), // closestOriginJunc)).getPathLength(closestDestJunc)+ minOriginDist + minDestDist ; // TODO : using non-deprecated methods don't work on NGS, probably need to update repast libraries ShortestPath<Junction> p = new ShortestPath<Junction>(ContextManager.roadNetwork, closestOriginJunc); double theDist = p.getPathLength(closestDestJunc); // ShortestPath<Junction> p = new // ShortestPath<Junction>(EnvironmentFactory.getRoadNetwork()); // double theDist = p.getPathLength(closestOriginJunc,closestDestJunc); p.finalize(); p = null; double finalDist = theDist + minOriginDist + minDestDist; // // Cache this distance // synchronized (Route.routeDistanceCache) { // Route.routeDistanceCache.put(crd, finalDist); // } return finalDist; } // synchronized } /** * Find the nearest coordinate which is part of a Road. Returns the coordinate which is actually the closest to the * given coord, not just the corner of the segment which is closest. Uses the DistanceOp class which finds the * closest points between two geometries. * <p> * When first called, the function will populate the 'nearestRoadCoordCache' which calculates where the closest road * coordinate is to each building. The agents will commonly start journeys from within buildings so this will * improve efficiency. * </p> * * @param inCoord * The coordinate from which to find the nearest road coordinate * @return the nearest road coordinate * @throws Exception */ private synchronized Coordinate getNearestRoadCoord(Coordinate inCoord) throws Exception { // double time = System.nanoTime(); synchronized (buildingsOnRoadCacheLock) { if (nearestRoadCoordCache == null) { LOGGER.log(Level.FINE, "Route.getNearestRoadCoord called for first time, " + "creating cache of all roads and the buildings which are on them ..."); // Create a new cache object, this will be read from disk if // possible (which is why the getInstance() method is used // instead of the constructor. String gisDir = ContextManager.getProperty(GlobalVars.GISDataDirectory); File buildingsFile = new File(gisDir + ContextManager.getProperty(GlobalVars.BuildingShapefile)); File roadsFile = new File(gisDir + ContextManager.getProperty(GlobalVars.RoadShapefile)); File serialisedLoc = new File( gisDir + ContextManager.getProperty(GlobalVars.BuildingsRoadsCoordsCache)); nearestRoadCoordCache = NearestRoadCoordCache.getInstance(ContextManager.agentGeography, buildingsFile, ContextManager.roadProjection, roadsFile, serialisedLoc, new GeometryFactory()); } // if not cached } // synchronized return nearestRoadCoordCache.get(inCoord); } /** * Finds the shortest route between multiple origin and destination junctions. Will return the shortest path and * also, via two parameters, can return the origin and destination junctions which make up the shortest route. * * @param currentJunctions * An array of origin junctions * @param destJunctions * An array of destination junctions * @param routeEndpoints * An array of size 2 which can be used to store the origin (index 0) and destination (index 1) Junctions * which form the endpoints of the shortest route. * @return the shortest route between the origin and destination junctions * @throws Exception */ private List<RepastEdge<Junction>> getShortestRoute(List<Junction> currentJunctions, List<Junction> destJunctions, Junction[] routeEndpoints) throws Exception { double time = System.nanoTime(); synchronized (GlobalVars.TRANSPORT_PARAMS.currentBurglarLock) { // This must be set so that NetworkEdge.getWeight() can adjust the weight depending on how this // particular agent is getting around the city GlobalVars.TRANSPORT_PARAMS.currentAgent = this.agent; double shortestPathLength = Double.MAX_VALUE; double pathLength = 0; ShortestPath<Junction> p; List<RepastEdge<Junction>> shortestPath = null; for (Junction o : currentJunctions) { for (Junction d : destJunctions) { if (o == null || d == null) { LOGGER.log(Level.WARNING, "Route.getShortestRoute() error: either the destination or origin " + "junction is null. This can be caused by disconnected roads. It's probably OK" + "to ignore this as a route should still be created anyway."); } else { p = new ShortestPath<Junction>(ContextManager.roadNetwork); pathLength = p.getPathLength(o, d); if (pathLength < shortestPathLength) { shortestPathLength = pathLength; shortestPath = p.getPath(o, d); // ShortestPath<Junction> p2 = new ShortestPath<Junction>(ContextManager.roadNetwork); // shortestPath = p2.getPath(o, d); // p2.finalize(); // p2 = null; // shortestPath = p1.getPath(o, d); // p1.finalize(); p1 = null; routeEndpoints[0] = o; routeEndpoints[1] = d; } // TODO See if the shortestpath bug has been fixed, would make this unnecessary p.finalize(); p = null; } // if junc null } // for dest junctions } // for origin junctions if (shortestPath == null) { String debugString = "Route.getShortestRoute() could not find a route. Looking for the shortest route between :\n"; for (Junction j : currentJunctions) debugString += "\t" + j.toString() + ", roads: " + j.getRoads().toString() + "\n"; for (Junction j : destJunctions) debugString += "\t" + j.toString() + ", roads: " + j.getRoads().toString() + "\n"; throw new RoutingException(debugString); } LOGGER.log(Level.FINER, "Route.getShortestRoute (" + (0.000001 * (System.nanoTime() - time)) + "ms) found shortest path " + "(length: " + shortestPathLength + ") from " + routeEndpoints[0].toString() + " to " + routeEndpoints[1].toString()); return shortestPath; } // synchronized } /** * Calculates the coordinates required to move an agent from their current position to the destination along a given * road. The algorithm to do this is as follows: * <ol> * <li>Starting from the destination coordinate, record each vertex and check inside the booundary of each line * segment until the destination point is found.</li> * <li>Return all but the last vertex, this is the route to the destination.</li> * </ol> * A boolean allows for two cases: heading towards a junction (the endpoint of the line) or heading away from the * endpoint of the line (this function can't be used to go to two midpoints on a line) * * @param currentCoord * @param destinationCoord * @param road * @param toJunction * whether or not we're travelling towards or away from a Junction * @param coordList * A list which will be populated with the coordinates that the agent should follow to move along the * road. * @param roadList * A list of roads associated with each coordinate. * @throws Exception */ private void getCoordsAlongRoad(Coordinate currentCoord, Coordinate destinationCoord, Road road, boolean toJunction, List<Coordinate> coordList) throws RoutingException { Route.checkNotNull(currentCoord, destinationCoord, road, coordList); double time = System.nanoTime(); Coordinate[] roadCoords = ContextManager.roadProjection.getGeometry(road).getCoordinates(); // Check that the either the destination or current coordinate are actually part of the road boolean currentCorrect = false, destinationCorrect = false; for (int i = 0; i < roadCoords.length; i++) { if (toJunction && destinationCoord.equals(roadCoords[i])) { destinationCorrect = true; break; } else if (!toJunction && currentCoord.equals(roadCoords[i])) { currentCorrect = true; break; } } // for if (!(destinationCorrect || currentCorrect)) { String roadCoordsString = ""; for (Coordinate c : roadCoords) roadCoordsString += c.toString() + " - "; throw new RoutingException("Neigher the origin or destination nor the current" + "coordinate are part of the road '" + road.toString() + "' (person '" + this.agent.toString() + "').\n" + "Road coords: " + roadCoordsString + "\n" + "\tOrigin: " + currentCoord.toString() + "\n" + "\tDestination: " + destinationCoord.toString() + " ( " + this.destinationAgent.toString() + " )\n " + "Heading " + (toJunction ? "to" : "away from") + " a junction, so " + (toJunction ? "destination" : "origin") + " should be part of a road segment"); } // Might need to reverse the order of the road coordinates if (toJunction && !destinationCoord.equals(roadCoords[roadCoords.length - 1])) { // If heading towards a junction, destination coordinate must be at end of road segment ArrayUtils.reverse(roadCoords); } else if (!toJunction && !currentCoord.equals(roadCoords[0])) { // If heading away form junction current coord must be at beginning of road segment ArrayUtils.reverse(roadCoords); } GeometryFactory geomFac = new GeometryFactory(); Point destinationPointGeom = geomFac.createPoint(destinationCoord); Point currentPointGeom = geomFac.createPoint(currentCoord); // If still false at end then algorithm hasn't worked boolean foundAllCoords = false; search: for (int i = 0; i < roadCoords.length - 1; i++) { Coordinate[] segmentCoords = new Coordinate[] { roadCoords[i], roadCoords[i + 1] }; // Draw a small buffer around the line segment and look for the coordinate within the buffer Geometry buffer = geomFac.createLineString(segmentCoords) .buffer(GlobalVars.GEOGRAPHY_PARAMS.BUFFER_DISTANCE.SMALL.dist); if (!toJunction) { /* If heading away from a junction, keep adding road coords until we find the destination */ coordList.add(roadCoords[i]); if (destinationPointGeom.within(buffer)) { coordList.add(destinationCoord); foundAllCoords = true; break search; } } else if (toJunction) { /* * If heading towards a junction: find the curent coord, add it to the route, then add all the remaining * coords which make up the road segment */ if (currentPointGeom.within(buffer)) { for (int j = i + 1; j < roadCoords.length; j++) { coordList.add(roadCoords[j]); } coordList.add(destinationCoord); foundAllCoords = true; break search; } } } // for if (foundAllCoords) { LOGGER.log(Level.FINER, "getCoordsAlongRoad (" + (0.000001 * (System.nanoTime() - time)) + "ms)"); return; } else { // If we get here then the route hasn't been created // A load of debugging info String error = "Route: getCoordsAlongRoad: could not find destination coordinates " + "along the road.\n\tHeading *" + (toJunction ? "towards" : "away from") + "* a junction.\n\t Person: " + this.agent.toString() + ")\n\tDestination building: " + destinationAgent.toString() + "\n\tRoad causing problems: " + road.toString() + "\n\tRoad vertex coordinates: " + Arrays.toString(roadCoords); throw new RoutingException(error); /* * Hack: ignore the error, printing a message and just returning the origin destination and coordinates. * This means agent will jump to/from the junction but I can't figure out why the fuck it occasionally * doesn't work!! It's so rare that hopefully this isn't a problem. */ // TempLogger.err("Route: getCoordsAlongRoad: error... (not debugging)."); // List<Coord> coords = new ArrayList<Coord>(); // coords.add(currentCoord); // coords.add(destinationCoord); // for (Coord c : coords) // this.roads.put(c, road); // Remember the roads each coord is // // part of // return coords; } } private static void checkNotNull(Object... args) throws RoutingException { for (Object o : args) { if (o == null) { throw new RoutingException("An input argument is null"); } } return; } /** * Returns all the coordinates that describe how to travel along a path, restricted to road coordinates. In some * cases the route wont have an associated road, this occurs if the route is part of a transport network. In this * case just the origin and destination coordinates are added to the route. * * @param shortestPath * @param startingJunction * The junction the path starts from, this is required so that the algorithm knows which road coordinate * to add first (could be first or last depending on the order that the road coordinates are stored * internally). * @return the coordinates as a mapping between the coord and its associated speed (i.e. how fast the agent can * travel to the next coord) which is dependent on the type of edge and the agent (e.g. * driving/walking/bus). LinkedHashMap is used to guarantee the insertion order of the coords is maintained. * @throws RoutingException */ private void getRouteBetweenJunctions(List<RepastEdge<Junction>> shortestPath, Junction startingJunction) throws RoutingException { double time = System.nanoTime(); if (shortestPath.size() < 1) { // This could happen if the agent's destination is on the same road // as the origin return; } // Lock the currentAgent so that NetworkEdge obejcts know what speed to use (depends on transport available to // the specific agent). synchronized (GlobalVars.TRANSPORT_PARAMS.currentBurglarLock) { GlobalVars.TRANSPORT_PARAMS.currentAgent = this.agent; // Iterate over all edges in the route adding coords and weights as appropriate NetworkEdge<Junction> e; Road r; // Use sourceFirst to represent whether or not the edge's source does actually represent the start of the // edge (agent could be going 'forwards' or 'backwards' over edge boolean sourceFirst; for (int i = 0; i < shortestPath.size(); i++) { e = (NetworkEdge<Junction>) shortestPath.get(i); if (i == 0) { // No coords in route yet, compare the source to the starting junction sourceFirst = (e.getSource().equals(startingJunction)) ? true : false; } else { // Otherwise compare the source to the last coord added to the list sourceFirst = (e.getSource().getCoords().equals(this.routeX.get(this.routeX.size() - 1))) ? true : false; } /* * Now add the coordinates describing how to move along the road. If there is no road associated with * the edge (i.e. it is a transport route) then just add the source/dest coords. Note that the shared * coordinates between two edges will be added twice, these must be removed later */ r = e.getRoad(); /* * Get the speed that the agent will be able to travel along this edge (depends on the transport * available to the agent and the edge). Some speeds will be < 1 if the agent shouldn't be using this * edge but doesn't have any other way of getting to the destination. in these cases set speed to 1 * (equivalent to walking). */ double speed = e.getSpeed(); if (speed < 1) speed = 1; if (r == null) { // No road associated with this edge (it is a // transport link) so just add source if (sourceFirst) { this.addToRoute(e.getSource().getCoords(), r, speed, "getRouteBetweenJunctions - no road"); this.addToRoute(e.getTarget().getCoords(), r, -1, "getRouteBetweenJunctions - no road"); // (Note speed = -1 used because we don't know the weight to the next // coordinate - this can be removed later) } else { this.addToRoute(e.getTarget().getCoords(), r, speed, "getRouteBetweenJunctions - no road"); this.addToRoute(e.getSource().getCoords(), r, -1, "getRouteBetweenJunctions - no road"); } } else { // This edge is a road, add all the coords which make up its geometry Coordinate[] roadCoords = ContextManager.roadProjection.getGeometry(r).getCoordinates(); if (roadCoords.length < 2) throw new RoutingException("Route.getRouteBetweenJunctions: for some reason road " + "'" + r.toString() + "' doesn't have at least two coords as part of its geometry (" + roadCoords.length + ")"); // Make sure the coordinates of the road are added in the correct order if (!sourceFirst) { ArrayUtils.reverse(roadCoords); } // Add all the road geometry's coords for (int j = 0; j < roadCoords.length; j++) { this.addToRoute(roadCoords[j], r, speed, "getRouteBetweenJuctions - on road"); // (Note that last coord will have wrong weight) } // for roadCoords.length } // if road!=null } // Check all lists are still the same size. assert this.roadsX.size() == this.routeX.size() && this.routeDescriptionX.size() == this.routeSpeedsX.size() && this.roadsX.size() == this.routeDescriptionX.size(); // Check all lists are still the same size. assert this.roadsX.size() == this.routeX.size() && this.routeDescriptionX.size() == this.routeSpeedsX.size() && this.roadsX.size() == this.routeDescriptionX.size(); // Finished! LOGGER.log(Level.FINER, "getRouteBetweenJunctions (" + (0.000001 * (System.nanoTime() - time)) + "ms"); return; } // synchronized } // getRouteBetweenJunctions /** * Determine whether or not the person associated with this Route is at their destination. Compares their current * coordinates to the destination coordinates (must be an exact match). * * @return True if the person is at their destination */ public boolean atDestination() { return ContextManager.getAgentGeometry(this.agent).getCoordinate().equals(this.destination); } // /** // * Removes any duplicate coordinates from the curent route (coordinates which // * are the same *and* next to each other in the list). // * <p> // * If my route-generating algorithm was better this would't be necessary. // */ // @Deprecated // private void removePairs() throws RoutingException { // if (this.routeX.size() < 1) { // // No coords to iterate over, probably something has gone wrong // throw new RoutingException("Route.removeDuplicateCoordinates(): WARNING an empty list has been " // + "passed to this function, something has probably gone wrong"); // } // TempLogger.out("ROUTE BEFORE REMOVING PAIRS"); // this.printRoute(); // // // (setRoute() has already checked that lists are same size) // // // Iterate over the list, removing coordinates that are the same as their neighbours. // // (and associated objects in other lists) // Iterator<Road> roadIt = this.roadsX.iterator(); // Iterator<Coordinate> routeIt = this.routeX.iterator(); // Iterator<Double> routeSpeedIt = this.routeSpeedsX.iterator(); // Iterator<String> routeDescIt = this.routeDescriptionX.iterator(); // Coordinate c1, c2; // Road currentRoad = roadIt.next(); // Road nextRoad = null; // routeIt.next(); routeSpeedIt.next(); routeDescIt.next(); // while ( roadIt.hasNext() ) { // nextRoad = roadIt.next(); // routeIt.next(); // routeSpeedIt.next(); // routeDescIt.next(); // // c1 = currentRoad.getCoords(); // c2 = nextRoad.getCoords(); // // if (c1.equals(c2)) { // // Remove objects from the lists // roadIt.remove(); // routeIt.remove(); // routeSpeedIt.remove(); // routeDescIt.remove(); // } // else { // currentRoad = nextRoad; // } // } // // TempLogger.out("ROUTE AFTER REMOVING PAIRS"); // this.printRoute(); // } private void printRoute() { StringBuilder out = new StringBuilder(); out.append("Printing route (" + this.agent.toString() + "). Current position in list is " + this.currentPosition + " ('" + this.routeDescriptionX.get(this.currentPosition) + "')"); for (int i = 0; i < this.routeX.size(); i++) { out.append("\t(" + this.agent.toString() + ") " + this.routeX.get(i).toString() + "\t" + this.routeSpeedsX.get(i).toString() + "\t" + this.roadsX.get(i) + "\t" + this.routeDescriptionX.get(i)); } LOGGER.info(out.toString()); } /** * Find the nearest object in the given geography to the coordinate. * * @param <T> * @param x * The coordinate to search from * @param geography * The given geography to look through * @param closestPoints * An optional List that will be populated with the closest points to x (i.e. the results of * <code>distanceOp.closestPoints()</code>. * @param searchDist * The maximum distance to search for objects in. Small distances are more efficient but larger ones are * less likely to find no objects. * @return The nearest object. * @throws RoutingException * If an object cannot be found. */ public static synchronized <T> T findNearestObject(Coordinate x, Geography<T> geography, List<Coordinate> closestPoints, GlobalVars.GEOGRAPHY_PARAMS.BUFFER_DISTANCE searchDist) throws RoutingException { if (x == null) { throw new RoutingException("The input coordinate is null, cannot find the nearest object"); } T nearestObject = SpatialIndexManager.findNearestObject(geography, x, closestPoints, searchDist); // Old way without using spatial index: // // GeometryFactory geomFac = new GeometryFactory(); // Point point = geomFac.createPoint(x); // // TODO Use an expanding buffer that starts small but gets bigger if no object is found. // // Geometry buffer = point.buffer(searchDist.dist); // double minDist = Double.MAX_VALUE; // T nearestObject = null; // for (T t : geography.getObjectsWithin(buffer.getEnvelopeInternal())) { // DistanceOp distOp = new DistanceOp(point, geography.getGeometry(t)); // double thisDist = distOp.distance(); // if (thisDist < minDist) { // minDist = thisDist; // nearestObject = t; // // Optionally record the closest points // if (closestPoints != null) { // closestPoints.clear(); // // TODO clean conversion of array to List (don't have access // // to internet!) // Coordinate[] crds = distOp.closestPoints(); // List<Coordinate> temp = new ArrayList(crds.length); // for (Coordinate c : crds) // temp.add(c); // closestPoints.addAll(temp); // } // } // if thisDist < minDist // } // for nearRoads if (nearestObject == null) { throw new RoutingException("Couldn't find an object close to these coordinates:\n\t" + x.toString()); } else { return nearestObject; } } /** * Returns the angle of the vector from p0 to p1 relative to the x axis * <p> * The angle will be between -Pi and Pi. I got this directly from the JUMP program source. * * @return the angle (in radians) that p0p1 makes with the positive x-axis. */ public static synchronized double angle(Coordinate p0, Coordinate p1) { double dx = p1.x - p0.x; double dy = p1.y - p0.y; return Math.atan2(dy, dx); } /** * The building which this Route is targeting * * @return the destinationHouse */ public IAgent getDestinationAgent() { if (this.destinationAgent == null) { LOGGER.log(Level.WARNING, "Route: getDestinationBuilding(), warning, no destination building has " + "been set. This might be ok, the agent might be supposed to be heading to a coordinate " + "not a particular building(?)"); return null; } return destinationAgent; } /** * The coordinate the route is targeting * * @return the destination */ public Coordinate getDestination() { return this.destination; } /** * Maintain a cache of all coordinates which are part of a road segment. Store the coords and all the road(s) they * are part of. * * @param coord * The coordinate which should be part of a road geometry * @return The road(s) which the coordinate is part of or null if the coordinate is not part of any road */ private List<Road> getRoadFromCoordCache(Coordinate coord) { populateCoordCache(); // Check the cache has been populated return coordCache.get(coord); } /** * Test if a coordinate is part of a road segment. * * @param coord * The coordinate which we want to test * @return True if the coordinate is part of a road segment */ private boolean coordOnRoad(Coordinate coord) { populateCoordCache(); // check the cache has been populated return coordCache.containsKey(coord); } private synchronized static void populateCoordCache() { double time = System.nanoTime(); if (coordCache == null) { // Fist check cache has been created coordCache = new HashMap<Coordinate, List<Road>>(); LOGGER.log(Level.FINER, "Route.populateCoordCache called for first time, creating new cache of all Road coordinates."); } if (coordCache.size() == 0) { // Now popualte it if it hasn't already // been populated LOGGER.log(Level.FINER, "Route.populateCoordCache: is empty, creating new cache of all Road coordinates."); for (Road r : ContextManager.roadContext.getObjects(Road.class)) { for (Coordinate c : ContextManager.roadProjection.getGeometry(r).getCoordinates()) { if (coordCache.containsKey(c)) { coordCache.get(c).add(r); } else { List<Road> l = new ArrayList<Road>(); l.add(r); // TODO Need to put *new* coordinate here? Not use // existing one in memory? coordCache.put(new Coordinate(c), l); } } } LOGGER.log(Level.FINER, "... finished caching all road coordinates (in " + 0.000001 * (System.nanoTime() - time) + "ms)"); } } /** * Find the buildings which can be accessed from the given road (the given road is the closest to the buildings). * Uses a separate cache object which can be serialised so that the cache doesn't need to be rebuilt every time. * * @param road * @return * @throws Exception */ private List<IAgent> getBuildingsOnRoad(Road road) throws Exception { if (buildingsOnRoadCache == null) { LOGGER.log(Level.FINER, "Route.getBuildingsOnRoad called for first time, " + "creating cache of all roads and the buildings which are on them ..."); // Create a new cache object, this will be read from disk if possible (which is why the // getInstance() method is used instead of the constructor. String gisDir = GlobalVars.GISDataDirectory; File buildingsFile = new File(gisDir + GlobalVars.BuildingShapefile); File roadsFile = new File(gisDir + GlobalVars.RoadShapefile); File serialLoc = new File(gisDir + ContextManager.getProperty(GlobalVars.BuildingsRoadsCache)); buildingsOnRoadCache = BuildingsOnRoadCache.getInstance(ContextManager.agentGeography, buildingsFile, ContextManager.roadProjection, roadsFile, serialLoc, new GeometryFactory()); } // if not cached return buildingsOnRoadCache.get(road); } /** * Calculate the distance (in meters) between two Coordinates, using the coordinate reference system that the * roadGeography is using. For efficiency it can return the angle as well (in the range -0 to 2PI) if returnVals * passed in as a double[2] (the distance is stored in index 0 and angle stored in index 1). * * @param c1 * @param c2 * @param returnVals * Used to return both the distance and the angle between the two Coordinates. If null then the distance * is just returned, otherwise this array is populated with the distance at index 0 and the angle at * index 1. * @return The distance between Coordinates c1 and c2. */ public static synchronized double distance(Coordinate c1, Coordinate c2, double[] returnVals) { // TODO check this now, might be different way of getting distance in new Simphony GeodeticCalculator calculator = new GeodeticCalculator(ContextManager.roadProjection.getCRS()); calculator.setStartingGeographicPoint(c1.x, c1.y); calculator.setDestinationGeographicPoint(c2.x, c2.y); double distance = calculator.getOrthodromicDistance(); if (returnVals != null && returnVals.length == 2) { returnVals[0] = distance; double angle = Math.toRadians(calculator.getAzimuth()); // Angle in range -PI to PI // Need to transform azimuth (in range -180 -> 180 and where 0 points north) // to standard mathematical (range 0 -> 360 and 90 points north) if (angle > 0 && angle < 0.5 * Math.PI) { // NE Quadrant angle = 0.5 * Math.PI - angle; } else if (angle >= 0.5 * Math.PI) { // SE Quadrant angle = (-angle) + 2.5 * Math.PI; } else if (angle < 0 && angle > -0.5 * Math.PI) { // NW Quadrant angle = (-1 * angle) + 0.5 * Math.PI; } else { // SW Quadrant angle = -angle + 0.5 * Math.PI; } returnVals[1] = angle; } return distance; } /** * Converts a distance lat/long distance (e.g. returned by DistanceOp) to meters. The calculation isn't very * accurate because (probably) it assumes that the distance is between two points that lie exactly on a line of * longitude (i.e. one is exactly due north of the other). For this reason the value shouldn't be used in any * calculations which is why it's returned as a String. * * @param dist * The distance (as returned by DistanceOp) to convert to meters * @return The approximate distance in meters as a String (to discourage using this approximate value in * calculations). * @throws Exception * @see com.vividsolutions.jts.operation.distance.DistanceOp */ public static synchronized String distanceToMeters(double dist) throws Exception { // Works by creating two coords (close to a randomly chosen object) which are a certain distance apart // then using similar method as other distance() function GeodeticCalculator calculator = new GeodeticCalculator(ContextManager.roadProjection.getCRS()); Coordinate c1 = ContextManager.agentContext.getRandomObject().getCoords(); calculator.setStartingGeographicPoint(c1.x, c1.y); calculator.setDestinationGeographicPoint(c1.x, c1.y + dist); return String.valueOf(calculator.getOrthodromicDistance()); } public void clearCaches() { if (coordCache != null) coordCache.clear(); if (nearestRoadCoordCache != null) { nearestRoadCoordCache.clear(); nearestRoadCoordCache = null; } if (buildingsOnRoadCache != null) { buildingsOnRoadCache.clear(); buildingsOnRoadCache = null; } // if (routeCache != null) { // routeCache.clear(); // routeCache = null; // } // if (routeDistanceCache != null) { // routeDistanceCache.clear(); // routeDistanceCache = null; // } } // /** // * Will add the given buildings to the awareness space of the Burglar who is // * being controlled by this Route. Also tells the burglar which buildings // * have been passed if appropriate, this is needed for agents who are // * currently looking for a burglary target. // * // * @param buildings // * A list of buildings // */ // @SuppressWarnings("unchecked") // protected <T> void passedObjects(List<T> objects, Class<T> clazz) { // this.agent.addToMemory(objects, clazz); // if (clazz.isAssignableFrom(Building.class)) { // // System.out.println("Route.passedObjects(): "+objects.toString()); // this.agent.buildingsPassed((List<Building>) objects); // } // } /** * Will add the given buildings to the awareness space of the Burglar who is being controlled by this Route. * * @param buildings * A list of buildings */ protected <T> void passedObject(T object, Class<T> clazz) { List<T> list = new ArrayList<T>(1); list.add(object); } } /* ************************************************************************ */ /** * Class can be used to store a cache of all roads and the buildings which can be accessed by them (a map of * Road<->List<Building>. Buildings are 'accessed' by travelling to the road which is nearest to them. * <p> * This class can be serialised so that if the GIS data doesn't change it doesn't have to be re-calculated each time. * However, the Roads and Buildings themselves cannot be serialised because if they are there will be two sets of Roads * and BUildings, the serialised ones and those that were created when the model was initialised. To get round this, an * array which contains the road and building ids is serialised and the cache is re-built using these caches ids after * reading the serialised cache. This means that the id's given to Buildings and Roads must not change (i.e. * auto-increment numbers are no good because if a simulation is restarted the static auto-increment variables will not * be reset to 0). * * @author Nick Malleson */ class BuildingsOnRoadCache implements Serializable { private static Logger LOGGER = Logger.getLogger(BuildingsOnRoadCache.class.getName()); private static final long serialVersionUID = 1L; // The actual cache, this isn't serialised private static transient Hashtable<Road, ArrayList<IAgent>> theCache; // The 'reference' cache, stores the building and road ids and can be // serialised private Hashtable<String, ArrayList<String>> referenceCache; // Check that the road/building data hasn't been changed since the cache was // last created private File buildingsFile; private File roadsFile; // The location that the serialised object might be found. private File serialisedLoc; // The time that this cache was created, can be used to check data hasn't // changed since private long createdTime; // Private constructor because getInstance() should be used private BuildingsOnRoadCache(Geography<IAgent> buildingEnvironment, File buildingsFile, Geography<Road> roadEnvironment, File roadsFile, File serialisedLoc, GeometryFactory geomFac) throws Exception { // this.buildingEnvironment = buildingEnvironment; // this.roadEnvironment = roadEnvironment; this.buildingsFile = buildingsFile; this.roadsFile = roadsFile; this.serialisedLoc = serialisedLoc; theCache = new Hashtable<Road, ArrayList<IAgent>>(); this.referenceCache = new Hashtable<String, ArrayList<String>>(); LOGGER.log(Level.FINE, "BuildingsOnRoadCache() creating new cache with data (and modification date):\n\t" + this.buildingsFile.getAbsolutePath() + " (" + new Date(this.buildingsFile.lastModified()) + ")\n\t" + this.roadsFile.getAbsolutePath() + " (" + new Date(this.roadsFile.lastModified()) + ")\n\t" + this.serialisedLoc.getAbsolutePath()); populateCache(buildingEnvironment, roadEnvironment, geomFac); this.createdTime = new Date().getTime(); serialise(); } public void clear() { theCache.clear(); this.referenceCache.clear(); } private void populateCache(Geography<IAgent> buildingEnvironment, Geography<Road> roadEnvironment, GeometryFactory geomFac) throws Exception { double time = System.nanoTime(); for (IAgent b : buildingEnvironment.getAllObjects()) { // Find the closest road to this building Geometry buildingPoint = geomFac.createPoint(b.getCoords()); double minDistance = Double.MAX_VALUE; Road closestRoad = null; double distance; Envelope e = buildingPoint.buffer(GlobalVars.GEOGRAPHY_PARAMS.BUFFER_DISTANCE.LARGE.dist) .getEnvelopeInternal(); for (Road r : roadEnvironment.getObjectsWithin(e)) { distance = DistanceOp.distance(buildingPoint, ContextManager.roadProjection.getGeometry(r)); if (distance < minDistance) { minDistance = distance; closestRoad = r; } } // for roads // Found the closest road, add the information to the cache if (theCache.containsKey(closestRoad)) { theCache.get(closestRoad).add(b); this.referenceCache.get(closestRoad.getIdentifier()).add(b.getIdentifier()); } else { ArrayList<IAgent> l = new ArrayList<IAgent>(); l.add(b); theCache.put(closestRoad, l); ArrayList<String> l2 = new ArrayList<String>(); l2.add(b.getIdentifier()); this.referenceCache.put(closestRoad.getIdentifier(), l2); } } // for buildings int numRoads = theCache.keySet().size(); int numBuildings = 0; for (List<IAgent> l : theCache.values()) numBuildings += l.size(); LOGGER.log(Level.FINER, "Finished caching roads and buildings. Cached " + numRoads + " roads and " + numBuildings + " buildings in " + 0.000001 * (System.nanoTime() - time) + "ms"); } public List<IAgent> get(Road r) { return theCache.get(r); } private void serialise() throws IOException { double time = System.nanoTime(); FileOutputStream fos = null; ObjectOutputStream out = null; try { if (!this.serialisedLoc.exists()) this.serialisedLoc.createNewFile(); fos = new FileOutputStream(this.serialisedLoc); out = new ObjectOutputStream(fos); out.writeObject(this); out.close(); } catch (IOException ex) { if (serialisedLoc.exists()) serialisedLoc.delete(); // delete to stop problems loading incomplete file next time throw ex; } LOGGER.log(Level.FINER, "Serialised BuildingsOnRoadCache to " + this.serialisedLoc.getAbsolutePath() + " in (" + 0.000001 * (System.nanoTime() - time) + "ms)"); } /** * Used to create a new BuildingsOnRoadCache object. This function is used instead of the constructor directly so * that the class can check if there is a serialised version on disk already. If not then a new one is created and * returned. * * @param buildingEnv * @param buildingsFile * @param roadEnv * @param roadsFile * @param serialisedLoc * @param geomFac * @return * @throws Exception */ public synchronized static BuildingsOnRoadCache getInstance(Geography<IAgent> buildingEnv, File buildingsFile, Geography<Road> roadEnv, File roadsFile, File serialisedLoc, GeometryFactory geomFac) throws Exception { double time = System.nanoTime(); // See if there is a cache object on disk. if (serialisedLoc.exists()) { FileInputStream fis = null; ObjectInputStream in = null; BuildingsOnRoadCache bc = null; try { fis = new FileInputStream(serialisedLoc); in = new ObjectInputStream(fis); bc = (BuildingsOnRoadCache) in.readObject(); in.close(); // Check that the cache is representing the correct data and the // modification dates are ok // (WARNING, if this class is re-compiled the serialised object // will still be read in). if (!buildingsFile.getAbsolutePath().equals(bc.buildingsFile.getAbsolutePath()) || !roadsFile.getAbsolutePath().equals(bc.roadsFile.getAbsolutePath()) || buildingsFile.lastModified() > bc.createdTime || roadsFile.lastModified() > bc.createdTime) { LOGGER.log(Level.FINER, "BuildingsOnRoadCache, found serialised object but it doesn't match the " + "data (or could have different modification dates), will create a new cache."); } else { // Have found a useable serialised cache. Now use the cached // list of id's to construct a // new cache of buildings and roads. // First need to buld list of existing roads and buildings Hashtable<String, Road> allRoads = new Hashtable<String, Road>(); for (Road r : roadEnv.getAllObjects()) allRoads.put(r.getIdentifier(), r); Hashtable<String, IAgent> allBuildings = new Hashtable<String, IAgent>(); for (IAgent b : buildingEnv.getAllObjects()) allBuildings.put(b.getIdentifier(), b); // Now create the new cache theCache = new Hashtable<Road, ArrayList<IAgent>>(); for (String roadId : bc.referenceCache.keySet()) { ArrayList<IAgent> buildings = new ArrayList<IAgent>(); for (String buildingId : bc.referenceCache.get(roadId)) { buildings.add(allBuildings.get(buildingId)); } theCache.put(allRoads.get(roadId), buildings); } LOGGER.log(Level.FINER, "BuildingsOnRoadCache, found serialised cache, returning it (in " + 0.000001 * (System.nanoTime() - time) + "ms)"); return bc; } } catch (IOException ex) { if (serialisedLoc.exists()) serialisedLoc.delete(); // delete to stop problems loading incomplete file next tinme throw ex; } catch (ClassNotFoundException ex) { if (serialisedLoc.exists()) serialisedLoc.delete(); throw ex; } } // No serialised object, or got an error when opening it, just create a // new one return new BuildingsOnRoadCache(buildingEnv, buildingsFile, roadEnv, roadsFile, serialisedLoc, geomFac); } } /* ************************************************************************ */ /** * Caches the nearest road Coordinate to every building for efficiency (agents usually/always need to get from the * centroids of houses to/from the nearest road). * <p> * This class can be serialised so that if the GIS data doesn't change it doesn't have to be re-calculated each time. * * @author Nick Malleson */ class NearestRoadCoordCache implements Serializable { private static Logger LOGGER = Logger.getLogger(NearestRoadCoordCache.class.getName()); private static final long serialVersionUID = 1L; private Hashtable<Coordinate, Coordinate> theCache; // The actual cache // Check that the road/building data hasn't been changed since the cache was // last created private File buildingsFile; private File roadsFile; // The location that the serialised object might be found. private File serialisedLoc; // The time that this cache was created, can be used to check data hasn't // changed since private long createdTime; private GeometryFactory geomFac; private NearestRoadCoordCache(Geography<IAgent> buildingEnvironment, File buildingsFile, Geography<Road> roadEnvironment, File roadsFile, File serialisedLoc, GeometryFactory geomFac) throws Exception { this.buildingsFile = buildingsFile; this.roadsFile = roadsFile; this.serialisedLoc = serialisedLoc; this.theCache = new Hashtable<Coordinate, Coordinate>(); this.geomFac = geomFac; LOGGER.log(Level.FINE, "NearestRoadCoordCache() creating new cache with data (and modification date):\n\t" + this.buildingsFile.getAbsolutePath() + " (" + new Date(this.buildingsFile.lastModified()) + ") \n\t" + this.roadsFile.getAbsolutePath() + " (" + new Date(this.roadsFile.lastModified()) + "):\n\t" + this.serialisedLoc.getAbsolutePath()); populateCache(buildingEnvironment, roadEnvironment); this.createdTime = new Date().getTime(); serialise(); } public void clear() { this.theCache.clear(); } private void populateCache(Geography<IAgent> buildingEnvironment, Geography<Road> roadEnvironment) throws Exception { double time = System.nanoTime(); theCache = new Hashtable<Coordinate, Coordinate>(); // Iterate over every building and find the nearest road point for (IAgent b : buildingEnvironment.getAllObjects()) { List<Coordinate> nearestCoords = new ArrayList<Coordinate>(); Route.findNearestObject(b.getCoords(), roadEnvironment, nearestCoords, GlobalVars.GEOGRAPHY_PARAMS.BUFFER_DISTANCE.LARGE); // Two coordinates returned by closestPoints(), need to find the one // which isn't the building coord Coordinate nearestPoint = null; for (Coordinate c : nearestCoords) { if (!c.equals(b.getCoords())) { nearestPoint = c; break; } } // for nearestCoords if (nearestPoint == null) { throw new Exception("Route.getNearestRoadCoord() error: couldn't find a road coordinate which " + "is close to building " + b.toString()); } theCache.put(b.getCoords(), nearestPoint); } // for Buildings LOGGER.log(Level.FINER, "Finished caching nearest roads (" + (0.000001 * (System.nanoTime() - time)) + "ms)"); } // if nearestRoadCoordCache = null; /** * * @param c * @return * @throws Exception */ public Coordinate get(Coordinate c) throws Exception { if (c == null) { throw new Exception("Route.NearestRoadCoordCache.get() error: the given coordinate is null."); } double time = System.nanoTime(); Coordinate nearestCoord = this.theCache.get(c); if (nearestCoord != null) { LOGGER.log(Level.FINER, "NearestRoadCoordCache.get() (using cache) - (" + (0.000001 * (System.nanoTime() - time)) + "ms)"); return nearestCoord; } // If get here then the coord is not in the cache, agent not starting their journey from a house, search for // it manually. Search all roads in the vicinity, looking for the point which is nearest the person double minDist = Double.MAX_VALUE; Coordinate nearestPoint = null; Point coordGeom = this.geomFac.createPoint(c); // Note: could use an expanding envelope that starts small and gets bigger double bufferDist = GlobalVars.GEOGRAPHY_PARAMS.BUFFER_DISTANCE.LARGE.dist; double bufferMultiplier = 1.0; Envelope searchEnvelope = coordGeom.buffer(bufferDist * bufferMultiplier).getEnvelopeInternal(); StringBuilder debug = new StringBuilder(); // incase the operation fails for (Road r : ContextManager.roadProjection.getObjectsWithin(searchEnvelope)) { DistanceOp distOp = new DistanceOp(coordGeom, ContextManager.roadProjection.getGeometry(r)); double thisDist = distOp.distance(); // BUG?: if an agent is on a really long road, the long road will not be found by getObjectsWithin because // it is not within the buffer debug.append("\troad ").append(r.toString()).append(" is ").append(thisDist) .append(" distance away (at closest point). "); if (thisDist < minDist) { minDist = thisDist; Coordinate[] closestPoints = distOp.closestPoints(); // Two coordinates returned by closestPoints(), need to find the // one which isn''t the coord parameter debug.append("Closest points (").append(closestPoints.length).append(") are: ") .append(Arrays.toString(closestPoints)); nearestPoint = (c.equals(closestPoints[0])) ? closestPoints[1] : closestPoints[0]; debug.append("Nearest point is ").append(nearestPoint.toString()); nearestPoint = (c.equals(closestPoints[0])) ? closestPoints[1] : closestPoints[0]; } // if thisDist < minDist debug.append("\n"); } // for nearRoads if (nearestPoint != null) { LOGGER.log(Level.FINER, "NearestRoadCoordCache.get() (not using cache) - (" + (0.000001 * (System.nanoTime() - time)) + "ms)"); return nearestPoint; } /* IF HERE THEN ERROR, PRINT DEBUGGING INFO */ StringBuilder debugIntro = new StringBuilder(); // Some extra info for debugging debugIntro.append("Route.NearestRoadCoordCache.get() error: couldn't find a coordinate to return.\n"); Iterable<Road> roads = ContextManager.roadProjection.getObjectsWithin(searchEnvelope); debugIntro.append("Looking for nearest road coordinate around ").append(c.toString()).append(".\n"); debugIntro.append("RoadEnvironment.getObjectsWithin() returned ") .append(ContextManager.sizeOfIterable(roads) + " roads, printing debugging info:\n"); debugIntro.append(debug); throw new Exception(debugIntro.toString()); } private void serialise() throws IOException { double time = System.nanoTime(); FileOutputStream fos = null; ObjectOutputStream out = null; try { if (!this.serialisedLoc.exists()) this.serialisedLoc.createNewFile(); fos = new FileOutputStream(this.serialisedLoc); out = new ObjectOutputStream(fos); out.writeObject(this); out.close(); } catch (IOException ex) { if (serialisedLoc.exists()) { // delete to stop problems loading incomplete file next time serialisedLoc.delete(); } throw ex; } LOGGER.log(Level.FINE, "... serialised NearestRoadCoordCache to " + this.serialisedLoc.getAbsolutePath() + " in (" + 0.000001 * (System.nanoTime() - time) + "ms)"); } /** * Used to create a new BuildingsOnRoadCache object. This function is used instead of the constructor directly so * that the class can check if there is a serialised version on disk already. If not then a new one is created and * returned. * * @param buildingEnv * @param buildingsFile * @param roadEnv * @param roadsFile * @param serialisedLoc * @param geomFac * @return * @throws Exception */ public synchronized static NearestRoadCoordCache getInstance(Geography<IAgent> buildingEnv, File buildingsFile, Geography<Road> roadEnv, File roadsFile, File serialisedLoc, GeometryFactory geomFac) throws Exception { double time = System.nanoTime(); // See if there is a cache object on disk. if (serialisedLoc.exists()) { FileInputStream fis = null; ObjectInputStream in = null; NearestRoadCoordCache ncc = null; try { fis = new FileInputStream(serialisedLoc); in = new ObjectInputStream(fis); ncc = (NearestRoadCoordCache) in.readObject(); in.close(); // Check that the cache is representing the correct data and the // modification dates are ok if (!buildingsFile.getAbsolutePath().equals(ncc.buildingsFile.getAbsolutePath()) || !roadsFile.getAbsolutePath().equals(ncc.roadsFile.getAbsolutePath()) || buildingsFile.lastModified() > ncc.createdTime || roadsFile.lastModified() > ncc.createdTime) { LOGGER.log(Level.FINE, "BuildingsOnRoadCache, found serialised object but it doesn't match the " + "data (or could have different modification dates), will create a new cache."); } else { LOGGER.log(Level.FINER, "NearestRoadCoordCache, found serialised cache, returning it (in " + 0.000001 * (System.nanoTime() - time) + "ms)"); return ncc; } } catch (IOException ex) { if (serialisedLoc.exists()) serialisedLoc.delete(); // delete to stop problems loading incomplete file next tinme throw ex; } catch (ClassNotFoundException ex) { if (serialisedLoc.exists()) serialisedLoc.delete(); throw ex; } } // No serialised object, or got an error when opening it, just create a new one return new NearestRoadCoordCache(buildingEnv, buildingsFile, roadEnv, roadsFile, serialisedLoc, geomFac); } } /** * Used to cache routes. Saves the origin and destination coords and the transport available to the agent (if transport * changes then the agent might have to create a new route. * * @author Nick Malleson */ class CachedRoute { private List<Coordinate> theRoute; private List<Double> routeSpeeds; private List<String> routeDescriptions; private List<Road> roads; private Coordinate origin; private Coordinate destination; private List<String> transportAvailable; // Used to generate hash codes (each route must have unique ID) private static int uniqueRouteCacheID; private int uniqueID; public CachedRoute(Coordinate origin, Coordinate destination, List<String> transportAvailable) { this.origin = origin; this.destination = destination; this.transportAvailable = transportAvailable; this.uniqueID = CachedRoute.uniqueRouteCacheID++; } public void setRoute(List<Coordinate> theRoute, List<Road> roads, List<Double> routeSpeeds, List<String> routeDescriptions) { this.theRoute = theRoute; this.roads = roads; this.routeSpeeds = routeSpeeds; this.routeDescriptions = routeDescriptions; } public List<Coordinate> getRoute() { return this.theRoute; } public List<Double> getRouteSpeeds() { return this.routeSpeeds; } public List<Road> getRoads() { return this.roads; } public List<String> getDescriptions() { return this.routeDescriptions; } @Override public String toString() { return "CachedRoute " + this.uniqueID; } /** * Returns true if input object is a CachedRoute and the the origin, destination and transport available are the * same as this CachedRoute */ @Override public boolean equals(Object obj) { if (obj instanceof CachedRoute) { CachedRoute r = (CachedRoute) obj; return (r.origin.equals(this.origin)) && (r.destination.equals(this.destination)) && (r.transportAvailable.equals(this.transportAvailable)); } else { return false; } } /** * Returns: <code>Float.floatToIntBits((float)(this.origin.getX()+this.origin.getY()))</code> */ @Override public int hashCode() { return Float.floatToIntBits((float) (this.origin.x + this.origin.y)); } } /** * Used to cache route distances. Saves the origin and destination coords and the transport available to the agent (if * transport changes then the agent might have to create a new route). * * @author Nick Malleson */ class CachedRouteDistance { private Coordinate origin; private Coordinate destination; private List<String> transportAvailable; private static int uniqueRouteCacheID; // Used to generate hash codes (each // route must have unique ID) private int uniqueID; // private List<Coord> theRoute; // The actual route (a list of coords) public CachedRouteDistance(Coordinate origin, Coordinate destination, List<String> transportAvailable) { this.origin = origin; this.destination = destination; this.transportAvailable = transportAvailable; this.uniqueID = CachedRouteDistance.uniqueRouteCacheID++; } @Override public String toString() { return "CachedRouteDistance " + this.uniqueID; } /** * Returns true if input object is a CachedRoute and the the origin, destination and transport available are the * same as this CachedRoute. Because routes are non-directional the origins and destinations are interchangeable. */ @Override public boolean equals(Object obj) { if (obj instanceof CachedRouteDistance) { CachedRouteDistance r = (CachedRouteDistance) obj; return ((r.origin.equals(this.origin) && r.destination.equals(this.destination)) || (r.origin.equals(this.destination) && r.destination.equals(this.origin))) && r.transportAvailable.equals(this.transportAvailable); } else { return false; } } /** * Returns: <code>Float.floatToIntBits((float)(this.origin.getX()+this.origin.getY()))</code> */ @Override public int hashCode() { return Float.floatToIntBits((float) (this.origin.x + this.origin.y)); } } /** * Convenience class for creating deep copies of lists/maps (copies the values stored as well). Haven't made this * generic because need access to constructors to create new objects (e.g. new Coord(c)) */ final class Cloning { public static List<Coordinate> copy(List<Coordinate> in) { List<Coordinate> out = new ArrayList<Coordinate>(in.size()); for (Coordinate c : in) { // TODO Check this Coordinate constructor does what I expect it to out.add(new Coordinate(c)); } return out; } // Not used now that route speeds are a list, not a map // public static LinkedHashMap<Coordinate, Double> // copy(LinkedHashMap<Coordinate, Double> in) { // // LinkedHashMap<Coordinate, Double> out = new LinkedHashMap<Coordinate, // Double>(in.size()); // for (Coordinate c : in.keySet()) { // out.put(c, in.get(c)); // } // return out; // } }