Java tutorial
/* * Copyright (C) 2011-2016 Rinde van Lon, iMinds-DistriNet, KU Leuven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.rinde.rinsim.core.model.road; import static com.github.rinde.rinsim.geom.Graphs.shortestPathEuclideanDistance; import static com.github.rinde.rinsim.geom.Graphs.unmodifiableGraph; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; import java.util.Queue; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.measure.quantity.Duration; import javax.measure.unit.Unit; import org.apache.commons.math3.random.RandomGenerator; import com.github.rinde.rinsim.core.model.road.GraphRoadModel.Loc; import com.github.rinde.rinsim.core.model.time.TimeLapse; import com.github.rinde.rinsim.geom.Connection; import com.github.rinde.rinsim.geom.ConnectionData; import com.github.rinde.rinsim.geom.Graph; import com.github.rinde.rinsim.geom.MultiAttributeData; import com.github.rinde.rinsim.geom.Point; import com.google.common.base.Optional; import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; import com.google.common.math.DoubleMath; /** * A {@link RoadModel} that uses a {@link Graph} as road structure. * {@link RoadUser}s can only be added on nodes on the graph. This model assumes * that the {@link Graph} does <b>not</b> change. Modifying the graph after * passing it to the model may break this model, for support for modifications * take a look at {@link DynamicGraphRoadModel}. The graph can define * {@link Connection} specific speed limits using {@link MultiAttributeData}. * * @author Rinde van Lon * @author Bartosz Michalik changes wrt. models infrastructure */ public class GraphRoadModel extends AbstractRoadModel<Loc> { /** * Precision. */ protected static final double DELTA = 0.000001; /** * The graph that is used as road structure. */ protected final Graph<? extends ConnectionData> graph; /** * Creates a new instance using the specified {@link Graph} as road structure. * The default units are used as defined by {@link AbstractRoadModel}. * @param g The graph which will be used as road structure. * @param b The builder that contains the properties. */ protected GraphRoadModel(Graph<? extends ConnectionData> g, RoadModelBuilders.AbstractGraphRMB<?, ?, ?> b) { super(b.getDistanceUnit(), b.getSpeedUnit()); graph = g; } @Override public void addObjectAt(RoadUser newObj, Point pos) { checkArgument(graph.containsNode(pos), "Object must be initiated on a crossroad."); super.addObjectAt(newObj, asLoc(pos)); } /** * Computes the distance that can be traveled on the connection between * <code>from</code> and <code>to</code> at the specified <code>speed</code> * and using the available <code>time</code>. This method can optionally be * overridden to change the move behavior of the model. The return value of * the method is interpreted in the following way: * <ul> * <li><code>if travelableDistance < distance(from,to)</code> then there is * either: * <ul> * <li>not enough time left to travel the whole distance</li> * <li>another reason (e.g. an obstacle on the way) that prevents traveling * the whole distance</li> * </ul> * <li><code>if travelableDistance ≥ distance(from,to)</code> then it is * possible to travel the whole distance at once.</li> * </ul> * Note that <code>from</code> and <code>to</code> do not necessarily * correspond to the start and end points of a connection. However, it is * guaranteed that the two points are <i>on</i> the same connection. * @param from The start position for this travel. * @param to The destination position for this travel. * @param speed The travel speed. * @param timeLeft The time available for traveling. * @param timeUnit Unit in which <code>timeLeft</code> is expressed. * @return The distance that can be traveled, must be ≥ 0. */ protected double computeTravelableDistance(Loc from, Point to, double speed, long timeLeft, Unit<Duration> timeUnit) { return speed * unitConversion.toInTime(timeLeft, timeUnit); } /** * Determines if the {@link #doFollowPath(MovingRoadUser, Queue, TimeLapse)} * method should continue moving towards the next hop in path. This method can * be overridden to add additional constraints. * @param curLoc The current location of the moving object. * @param path The path of the moving object. * @param time The time available for moving. * @return <code>true</code> if the object should keep moving, * <code>false</code> otherwise. */ protected boolean keepMoving(Loc curLoc, Queue<Point> path, TimeLapse time) { return time.hasTimeLeft() && !path.isEmpty(); } @Override protected MoveProgress doFollowPath(MovingRoadUser object, Queue<Point> path, TimeLapse time) { final Loc objLoc = verifyLocation(objLocs.get(object)); Loc tempLoc = objLoc; final MoveProgress.Builder mpBuilder = MoveProgress.builder(unitConversion, time); boolean cont = true; long timeLeft = time.getTimeLeft(); while (timeLeft > 0 && !path.isEmpty() && cont) { checkMoveValidity(tempLoc, path.peek()); // speed in internal speed unit final double speed = getMaxSpeed(object, tempLoc, path.peek()); checkState(speed >= 0, "Found a bug in getMaxSpeed, return value must be >= 0, but is %s.", speed); // distance that can be traveled in current conn with timeleft final double travelableDistance = computeTravelableDistance(tempLoc, path.peek(), speed, timeLeft, time.getTimeUnit()); checkState(travelableDistance >= 0d, "Found a bug in computeTravelableDistance, return value must be >= 0," + " but is %s.", travelableDistance); final double connLength = unitConversion.toInDist(computeDistanceOnConnection(tempLoc, path.peek())); checkState(connLength >= 0d, "Found a bug in computeDistanceOnConnection, return value must be " + ">= 0, but is %s.", connLength); double traveledDistance; if (travelableDistance >= connLength) { // jump to next node in path (this may be a node or a point on a // connection) tempLoc = verifyLocation(asLoc(path.remove())); traveledDistance = connLength; mpBuilder.addNode(tempLoc); } else { // travelableDistance < connLength cont = false; traveledDistance = travelableDistance; final Connection<?> conn = getConnection(tempLoc, path.peek()); tempLoc = verifyLocation( newLoc(conn, tempLoc.relativePos + unitConversion.toExDist(travelableDistance))); } mpBuilder.addDistance(traveledDistance); final long timeSpent = DoubleMath.roundToLong( unitConversion.toExTime(traveledDistance / speed, time.getTimeUnit()), RoundingMode.HALF_DOWN); timeLeft -= timeSpent; } time.consume(time.getTimeLeft() - timeLeft); // update location and construct a MoveProgress object objLocs.put(object, tempLoc); return mpBuilder.build(); } /** * Check if it is possible to move from <code>objLoc</code> to * <code>nextHop</code>. * @param objLoc The current location. * @param nextHop The destination node. * @throws IllegalArgumentException if it the proposed move is invalid. */ protected void checkMoveValidity(Loc objLoc, Point nextHop) { // in case we start from an conn and our next destination is to go to // the end of the current conn then its ok. Otherwise more checks are // required.. if (objLoc.isOnConnection() && !nextHop.equals(objLoc.conn.get().to())) { // check if next destination is a MidPoint checkArgument(nextHop instanceof Loc, "Illegal path for this object, from a position on a connection we can" + " not jump to another connection or go back. From %s, to %s.", objLoc, nextHop); final Loc dest = (Loc) nextHop; // check for same conn checkArgument(objLoc.isOnSameConnection(dest), "Illegal path for this object, first point is not on the same " + "connection as the object."); // check for relative position checkArgument(objLoc.relativePos <= dest.relativePos, "Illegal path for this object, can not move backward over an " + "connection."); } else if (!objLoc.isOnConnection() && !nextHop.equals(objLoc) && !graph.hasConnection(objLoc, nextHop)) { // in case we start from a node and we are not going to another node checkArgument(nextHop instanceof Loc, "Illegal path, first point should be directly connected to object " + "location."); final Loc dest = (Loc) nextHop; checkArgument(graph.hasConnection(objLoc, dest.conn.get().to()), "Illegal path, first point is on an connection not connected to " + "object location. "); checkArgument(objLoc.equals(dest.conn.get().from()), "Illegal path, first point is on a different connection."); } } /** * Compute distance between the two specified points, both of which need to be * on the same connection (or are equal). If points are equal the distance is * 0. This method uses length stored in {@link ConnectionData} objects when * available. * @param from Start of the connection. * @param to End of the connection. * @return the distance between two points, must be ≥ 0. * @throws IllegalArgumentException when two points are part of the graph but * are not equal or there is no connection between them */ protected double computeDistanceOnConnection(Point from, Point to) { if (from.equals(to)) { return 0; } final Connection<?> conn = getConnection(from, to); if (isOnConnection(from) && isOnConnection(to)) { return Math.abs(((Loc) from).relativePos - ((Loc) to).relativePos); } else if (isOnConnection(from)) { return conn.getLength() - ((Loc) from).relativePos; } else if (isOnConnection(to)) { return ((Loc) to).relativePos; } return conn.getLength(); } /** * Checks if the point is on a connection. * @param p The point to check. * @return <code>true</code> if the point is on a connection, * <code>false</code> otherwise. */ protected static boolean isOnConnection(Point p) { return p instanceof Loc && ((Loc) p).isOnConnection(); } /** * Checks whether the specified location is valid. * @param l The location to check. * @return The location if it is valid. * @throws VerifyException if the location is not valid. */ protected Loc verifyLocation(Loc l) { verify(l.isOnConnection() || graph.containsNode(l), "Location points to non-existing node: %s.", l); verify(!l.isOnConnection() || graph.hasConnection(l.conn.get().from(), l.conn.get().to()), "Location points to non-existing connection: %s.", l.conn); return l; } /** * Compute speed of the object taking into account the speed limits of the * object. * @param object traveling object * @param from the point on the graph object is located * @param to the next point on the path it want to reach * @return The maximum speed in the internal unit, must be ≥ 0. */ protected double getMaxSpeed(MovingRoadUser object, Point from, Point to) { final double objSpeed = unitConversion.toInSpeed(object.getSpeed()); if (!from.equals(to)) { final Connection<?> conn = getConnection(from, to); if (conn.data().isPresent() && conn.data().get() instanceof MultiAttributeData) { final MultiAttributeData maed = (MultiAttributeData) conn.data().get(); if (maed.getMaxSpeed().isPresent()) { return Math.min(unitConversion.toInSpeed(maed.getMaxSpeed().get()), objSpeed); } return objSpeed; } } return objSpeed; } /** * Precondition: the specified {@link Point}s are part of a {@link Connection} * which exists in the {@link Graph}. This method figures out which * {@link Connection} the two {@link Point}s share. * @param from The start point. * @param to The end point. * @return The {@link Connection} shared by the points. */ protected Connection<?> getConnection(Point from, Point to) { final boolean fromIsOnConn = isOnConnection(from); final boolean toIsOnConn = isOnConnection(to); Connection<?> conn; final String errorMsg = "The specified points must be part of the same connection."; if (fromIsOnConn) { final Loc start = (Loc) from; if (toIsOnConn) { checkArgument(start.isOnSameConnection((Loc) to), errorMsg); } else { checkArgument(start.conn.get().to().equals(to), errorMsg); } conn = start.conn.get(); } else if (toIsOnConn) { final Loc end = (Loc) to; checkArgument(end.conn.get().from().equals(from), errorMsg); conn = end.conn.get(); } else { checkArgument(graph.hasConnection(from, to), "The specified points (%s and %s) must be part of an existing " + "connection in the graph.", from, to); conn = graph.getConnection(from, to); } return conn; } @Override public List<Point> getShortestPathTo(Point from, Point to) { final List<Point> path = new ArrayList<>(); Point start = from; if (isOnConnection(from)) { start = asLoc(from).conn.get().to(); path.add(from); } Point end = to; if (isOnConnection(to)) { end = asLoc(to).conn.get().from(); } path.addAll(doGetShortestPathTo(start, end)); if (isOnConnection(to)) { path.add(to); } return path; } /** * Uses the A* algorithm: * {@link com.github.rinde.rinsim.geom.Graphs#shortestPathEuclideanDistance}. * This method can optionally be overridden by subclasses to define another * shortest path algorithm. * @param from The start point of the path. * @param to The end point of the path. * @return The shortest path. */ protected List<Point> doGetShortestPathTo(Point from, Point to) { return shortestPathEuclideanDistance(graph, from, to); } /** * @return An unmodifiable view on the graph. */ public Graph<? extends ConnectionData> getGraph() { return unmodifiableGraph(graph); } /** * Retrieves the connection which the specified {@link RoadUser} is at. If the * road user is at a node, {@link Optional#absent()} is returned instead. * @param obj The object which position is checked. * @return A {@link Connection} if <code>obj</code> is on one, * {@link Optional#absent()} otherwise. */ public Optional<? extends Connection<?>> getConnection(RoadUser obj) { final Loc point = objLocs.get(obj); if (isOnConnection(point)) { return Optional.of(graph.getConnection(point.conn.get().from(), point.conn.get().to())); } return Optional.absent(); } /** * Creates a new {@link Loc} based on the provided {@link Point}. * @param p The point used as input. * @return A {@link Loc} with identical position as the specified * {@link Point}. */ protected static Loc asLoc(Point p) { if (p instanceof Loc) { return (Loc) p; } return new Loc(p.x, p.y, null, -1, 0); } /** * Creates a new {@link Loc} based on the provided {@link Connection} and the * relative position. The new {@link Loc} will be placed on the connection * with a distance of <code>relativePos</code> to the start of the connection. * @param conn The {@link Connection} to use. * @param relativePos The relative position measured from the start of the * {@link Connection}. * @return A new {@link Loc} */ protected static Loc newLoc(Connection<? extends ConnectionData> conn, double relativePos) { final Point diff = Point.diff(conn.to(), conn.from()); final double roadLength = conn.getLength(); final double perc = relativePos / roadLength; if (perc + DELTA >= 1) { return new Loc(conn.to().x, conn.to().y, null, -1, 0); } return new Loc(conn.from().x + perc * diff.x, conn.from().y + perc * diff.y, conn, roadLength, relativePos); } @Override protected Point locObj2point(Loc locObj) { return locObj; } @Override protected Loc point2LocObj(Point point) { return asLoc(point); } @Override public Point getRandomPosition(RandomGenerator rnd) { return graph.getRandomNode(rnd); } @Override @Nonnull public <U> U get(Class<U> type) { return type.cast(self); } @Deprecated @Override public ImmutableList<Point> getBounds() { throw new UnsupportedOperationException("Not yet implemented."); } /** * Location representation in a {@link Graph} for the {@link GraphRoadModel} . * @author Rinde van Lon */ protected static final class Loc extends Point { private static final long serialVersionUID = 7070585967590832300L; /** * The length of the current connection. */ public final double connLength; /** * The relative position of this instance compared to the start of the * connection. */ public final double relativePos; /** * The {@link Connection} which this position is on if present. */ public final Optional<? extends Connection<?>> conn; Loc(double pX, double pY, @Nullable Connection<? extends ConnectionData> pConn, double pConnLength, double pRelativePos) { super(pX, pY); connLength = pConnLength; relativePos = pRelativePos; conn = Optional.fromNullable(pConn); } /** * @return <code>true</code> if the position is on a connection. */ public boolean isOnConnection() { return conn.isPresent(); } /** * Check if this position is on the same connection as the provided * location. * @param l The location to compare with. * @return <code>true</code> if both {@link Loc}s are on the same * connection, <code>false</code> otherwise. */ public boolean isOnSameConnection(Loc l) { return conn.equals(l.conn); } } }