From 9178a571768dd284d8341c57e877875b3ed89532 Mon Sep 17 00:00:00 2001
From: Nikolaus Krismer <nikolaus.krismer@uibk.ac.at>
Date: Tue, 12 Aug 2014 19:23:48 +0200
Subject: [PATCH] experimenting with new sql queries for getEarliestArrivalTime
 and getLatestDepartureTime. This should reduce the number of sql queries by
 far...

---
 .../inf/isochrone/algorithm/Isochrone.java    | 231 ++++++++++--------
 .../it/unibz/inf/isochrone/db/Database.java   |  65 ++---
 .../it/unibz/inf/isochrone/network/Link.java  |   2 +-
 .../inf/isochrone/network/MemoryOutput.java   |   4 +-
 .../it/unibz/inf/isochrone/network/Node.java  |   2 +-
 .../inf/isochrone/network/NodeConnection.java | 135 ++++++++++
 6 files changed, 310 insertions(+), 129 deletions(-)
 create mode 100644 src/main/java/it/unibz/inf/isochrone/network/NodeConnection.java

diff --git a/src/main/java/it/unibz/inf/isochrone/algorithm/Isochrone.java b/src/main/java/it/unibz/inf/isochrone/algorithm/Isochrone.java
index ee83cfc8..717142ff 100644
--- a/src/main/java/it/unibz/inf/isochrone/algorithm/Isochrone.java
+++ b/src/main/java/it/unibz/inf/isochrone/algorithm/Isochrone.java
@@ -8,14 +8,17 @@ import it.unibz.inf.isochrone.network.Link;
 import it.unibz.inf.isochrone.network.Location;
 import it.unibz.inf.isochrone.network.MemoryOutput;
 import it.unibz.inf.isochrone.network.Node;
+import it.unibz.inf.isochrone.network.NodeConnection;
 import it.unibz.inf.isochrone.util.EnumContainer.Direction;
 import it.unibz.inf.isochrone.util.Query;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.PriorityQueue;
 import java.util.Set;
 
@@ -30,10 +33,33 @@ import java.util.Set;
  */
 public abstract class Isochrone {
 	private static final double COMPARE_PRECISION = 0.0000001d;
-	private final Query query;
-	private final PriorityQueue<Node> queue;
-	private Set<Integer> codes;
-	protected Database database;
+	private final Collection<Integer> dateCodes;
+	private final PriorityQueue<Node> nodeQueue;
+	private final long qDuration;
+	private final boolean qIsExpiring;
+	private final boolean qIsIncoming;
+	private final StartNodes qStartNodes;
+	private final long qFromTime;
+	private final long qToTime;
+	private final double qWalkingSpeed;
+	private final class StartNodes {
+		private final Collection<Integer> startNodes;
+		private final Collection<Location> startLocations;
+
+		private StartNodes(final Query query) {
+			startNodes = query.getStartNodes();
+			startLocations = query.getStartLocations();
+		}
+
+		private void initialize(final IOutput output) {
+			if (startNodes.isEmpty()) {
+				initializeStartLocations(output, startLocations);
+			} else {
+				initializeStartNodes(startNodes);
+			}
+		}
+	}
+	protected final Database database;
 
 	// Constructors
 
@@ -46,15 +72,22 @@ public abstract class Isochrone {
 	 * @throws AlgorithmException thrown if there are no date codes in the database for the specified query
 	 */
 	public Isochrone(final ConfigDataset config, final Database db, final Query query) throws AlgorithmException {
-		this.database = db;
-		this.query = query;
-		this.queue = new PriorityQueue<>();
-
-		if (database == null) {
-			database = new Database(config, query.getMode(), query.getDir());
+		this.database = (db == null) ? new Database(config, query.getMode(), query.getDir()) : db;
+		this.dateCodes = getDateCodes(query.getTime());
+		if (dateCodes.isEmpty()) {
+			// was solved in uniBz version by switching to unimodal mode... we do not do this any more
+			// (since user does not get informated about the change)
+			throw new AlgorithmException("Couldn't find datecodes in the databse for the given date");
 		}
 
-		initDateCodes();
+		this.nodeQueue = new PriorityQueue<>();
+		this.qDuration = query.getDuration().longValue();
+		this.qIsExpiring = query.isExpireNodes();
+		this.qIsIncoming = (Direction.INCOMING == query.getDir());
+		this.qStartNodes = new StartNodes(query);
+		this.qToTime = query.getToTime();
+		this.qFromTime = query.getFromTime();
+		this.qWalkingSpeed = query.getWalkingSpeed();
 	}
 
 	/**
@@ -122,19 +155,12 @@ public abstract class Isochrone {
 	 */
 	// TODO: Add some more tests for this method. If isochrones get too small (2points for 15min isochrone from FUB) nothing fails ;-(
 	public <T extends AbstractOutput> T compute(final T output) {
-		final boolean isNodeExpires = query.isExpireNodes();
 		output.beforeCalculation();
-
-		final Collection<Integer> startNodes = query.getStartNodes();
-		if (startNodes.isEmpty()) {
-			initializeStartLocations(output, query.getStartLocations());
-		} else {
-			initializeStartNodes(startNodes);
-		}
+		qStartNodes.initialize(output);
 
 		Node node = null;
-		while ((node = queue.poll()) != null) {
-			final int nodeId = node.getID();
+		while ((node = nodeQueue.poll()) != null) {
+			final int nodeId = node.getId();
 			final Collection<Link> adjacents = calcAdjLinks(nodeId);
 			output.addNode(node);
 			node.close();
@@ -142,7 +168,7 @@ public abstract class Isochrone {
 			updateNodeQueue(expandLinks(node, adjacents));
 			addLinks(output, adjacents);
 
-			if (isNodeExpires && node.isExpired()) {
+			if (qIsExpiring && node.isExpired()) {
 				removeNode(nodeId);
 			}
 		}
@@ -189,104 +215,119 @@ public abstract class Isochrone {
 		}
 	}
 
-	/**
-	 * Expand the given link (that is connected to the given node)
-	 * and adjusts the link offsets based on the distance of the current Node
-	 * and the maximum duration of the query.
-	 *
-	 * @param node current node
-	 * @param links links that are connected to the node
-	 * @return nodes that can be reached in the available time (or an empty list if time has run out/no nodes are reachable)
-	 */
 	private Collection<Node> expandLinks(final Node node, final Collection<Link> links) {
-		final long duration = query.getDuration().longValue();
-		final double walkingSpeed = query.getWalkingSpeed();
-		final boolean isExpiring = query.isExpireNodes();
-		final boolean isIncoming = (Direction.INCOMING == query.getDir());
+		final Collection<Node> resultNodes = new ArrayList<>();
 
-		final Collection<Node> resultNodes = new ArrayList<>(links.size());
+		final NodeConnection nConnections = new NodeConnection(node);
 		for (final Link link : links) {
 			final Node adjacentNode = getNode(link.getOppositeOf(node));
 			adjacentNode.visitNrAdjacentLinks((short) 1);
 
-			final double linkLength = link.getLength();
-			final boolean isContinuous = link.isContinuous();
-			final double nodeDistance = node.getDistance();
+			if (link.isContinuous()) {
+				final double nodeDistance = node.getDistance();
+				final double linkLength = link.getLength();
 
-			if (isContinuous) {
-				double remainingDistance = duration - nodeDistance;
-				if (isIncoming) {
-					link.setStartOffset(Math.max(0, linkLength - (remainingDistance * walkingSpeed)));
+				if (qIsIncoming) {
+					link.setStartOffset(Math.max(0, linkLength - (qDuration - nodeDistance) * qWalkingSpeed));
 					link.setEndOffset(linkLength);
 				} else {
-					remainingDistance = Math.max(0, duration - nodeDistance);
+					final double remainingDistance = qDuration - nodeDistance < 0 ? 0 : qDuration - nodeDistance;
 					link.setStartOffset(0);
-					link.setEndOffset(Math.min(linkLength, remainingDistance * walkingSpeed));
+					link.setEndOffset(Math.min(linkLength, remainingDistance * qWalkingSpeed));
 				}
 			}
 
 			if (adjacentNode.isClosed()) {
-				if (isExpiring && adjacentNode.isExpired()) {
-					final int adjId = adjacentNode.getID();
-					if (!isContinuous || adjId != node.getID()) {
-						removeNode(adjId);
-					}
+				if (qIsExpiring && adjacentNode.isExpired() && adjacentNode.getId() != node.getId()) {
+					removeNode(adjacentNode.getId());
 				}
 			} else {
-				final double newDistance;
-				if (isContinuous) {
-					newDistance = nodeDistance + linkLength / walkingSpeed;
-				} else {
-					final Set<Short> routes = new HashSet<>();
-					routes.add((short) link.getRoute());
-					newDistance = getAdjNodeCost(node, adjacentNode, Arrays.asList(new Short[] {(short) link.getRoute()}));
-				}
-
-				if (newDistance <= duration && newDistance < adjacentNode.getDistance()) {
-					adjacentNode.setDistance(newDistance);
-					resultNodes.add(adjacentNode);
-				}
+				nConnections.addTargetLink(adjacentNode, link);
 			}
 		}
 
+		resultNodes.addAll(expandContinuousLinks(nConnections));
+		resultNodes.addAll(expandDiscreteLinks(nConnections));
+
 		return resultNodes;
 	}
 
 	/**
-	 * Gets date codes for a given time.
+	 * Expand the given continuous link (that is connected to the given node)
+	 * and adjusts the link offsets based on the distance of the current Node
+	 * and the maximum duration of the query.
 	 *
-	 * @param time the given
-	 * @return returns a set of date codes
+	 * @param nConnection information about the the sourceNode, its links and reachable nodes
+	 * @return another node, if it can be reached in the available time (or null if time has run out)
 	 */
-	private Set<Integer> getDateCodes(final Calendar time) {
-		return database.getDateCodes(time);
+	private Collection<Node> expandContinuousLinks(final NodeConnection nConnection) {
+		if (!nConnection.containsContinuousTargets()) {
+			return Collections.emptyList();
+		}
+
+		final Set<Node> resultCollection = new HashSet<>();
+		final Node sourceNode = nConnection.getSourceNode();
+
+		for (final Entry<Node, Link> e : nConnection.getContinuousTargets().entrySet()) {
+			final Node adjacentNode = e.getKey();
+			final Link link = e.getValue();
+			final double newDistance = sourceNode.getDistance() + link.getLength() / qWalkingSpeed;
+
+			if (newDistance <= qDuration && newDistance < adjacentNode.getDistance()) {
+				adjacentNode.setDistance(newDistance);
+				resultCollection.add(adjacentNode);
+			}
+		}
+
+		return resultCollection;
 	}
 
 	/**
-	 * Get the cost to travel to an adjacent node.
+	 * Expand the given discrete link (that is connected to the
+	 * given node and adjust the link offsets based on the
+	 * distance of the current Node and the maximum duration of
+	 * the query.
 	 *
-	 * @param node the id of the node from which the calculation is started
-	 * @param adjNode the id of the node which is traveled to
-	 * @param routeIds the routes that should be considered
-	 * @return the cost of traveling from the node to adjNode
+	 * @param nConnection information about the the sourceNode, its links and reachable nodes
+	 * @return all nodes that can be reached in the available time (or an empty set if there is no time to reach another node)
 	 */
-	private double getAdjNodeCost(final Node node, final Node adjNode, final Collection<Short> routeIds) {
-		return database.getAdjNodeCost(node, adjNode, routeIds, codes, query.getFromTime(), query.getToTime());
+	private Collection<Node> expandDiscreteLinks(final NodeConnection nConnection) {
+		if (!nConnection.containsDiscreteTargets()) {
+			return Collections.emptyList();
+		}
+
+		final Set<Node> resultCollection = new HashSet<>();
+		final Map<Node, Double> newDistances = getAdjNodeCost(nConnection);
+		for (final Entry<Node, Double> e : newDistances.entrySet()) {
+			final Node adjacentNode = e.getKey();
+			final double newDistance = e.getValue();
+			if (newDistance <= qDuration && newDistance < adjacentNode.getDistance()) {
+				adjacentNode.setDistance(newDistance);
+				resultCollection.add(adjacentNode);
+			}
+		}
+
+		return resultCollection;
 	}
 
 	/**
-	 * Initialize the datecodes for the isochrone.
+	 * Gets date codes for a given time.
 	 *
-	 * @throws AlgorithmException thrown if there are no date codes for the query time in the database
+	 * @param time the given
+	 * @return returns a set of date codes
 	 */
-	private void initDateCodes() throws AlgorithmException {
-		codes = getDateCodes(query.getTime());
+	private Collection<Integer> getDateCodes(final Calendar time) {
+		return database.getDateCodes(time);
+	}
 
-		if (codes.isEmpty()) {
-			// was solved in uniBz version by switching to unimodal mode... we do not do this any more
-			// (since user does not get informated about the change)
-			throw new AlgorithmException("Couldn't find datecodes in the databse for the given date");
-		}
+	/**
+	 * Get the cost to travel to an adjacent node.
+	 *
+	 * @param nConnection information about to where start the calculation, where to travel to and which routes are considered
+	 * @return the cost of traveling from the node to adjNode
+	 */
+	private Map<Node, Double> getAdjNodeCost(final NodeConnection nConnection) {
+		return database.getAdjNodeCost(nConnection, dateCodes, qFromTime, qToTime);
 	}
 
 	/**
@@ -296,35 +337,31 @@ public abstract class Isochrone {
 	 * @param locations The locations from which the isochrone should start.
 	 */
 	private void initializeStartLocations(final IOutput output, final Collection<Location> locations) {
-		final long duration = query.getDuration().longValue();
-		final double walkingSpeed = query.getWalkingSpeed();
-		final boolean isIncoming = (Direction.INCOMING == query.getDir());
-
 		for (final Location location : locations) {
 			final Link link = getLink(location.getLinkId());
 			final double locationOffset = location.getOffset();
 			double distance;
 			Node node;
 
-			if (isIncoming) {
+			if (qIsIncoming) {
 				node = getNode(link.getStartNode());
-				distance = locationOffset / walkingSpeed;
+				distance = locationOffset / qWalkingSpeed;
 				if (locationOffset > 0 && locationOffset < link.getLength()) {
-					link.setStartOffset(Math.max(0, locationOffset - duration * walkingSpeed));
+					link.setStartOffset(Math.max(0, locationOffset - qDuration * qWalkingSpeed));
 					link.setEndOffset(locationOffset);
 					output.addLink(link);
 				}
 			} else {
 				node = getNode(link.getEndNode());
-				distance = (link.getLength() - locationOffset) / walkingSpeed;
+				distance = (link.getLength() - locationOffset) / qWalkingSpeed;
 				if (locationOffset > 0 && locationOffset < link.getLength()) {
 					link.setStartOffset(locationOffset);
-					link.setEndOffset(Math.min(link.getLength(), Math.abs(link.getLength() - locationOffset - duration * walkingSpeed)));
+					link.setEndOffset(Math.min(link.getLength(), Math.abs(link.getLength() - locationOffset - qDuration * qWalkingSpeed)));
 					output.addLink(link);
 				}
 			}
 
-			if (distance <= duration && distance < node.getDistance()) {
+			if (distance <= qDuration && distance < node.getDistance()) {
 				node.setDistance(distance);
 				updateNodeQueue(node);
 			}
@@ -350,8 +387,8 @@ public abstract class Isochrone {
 	 */
 	private void updateNodeQueue(final Node node) {
 		if (node != null) {
-			queue.remove(node);
-			queue.offer(node);
+			nodeQueue.remove(node);
+			nodeQueue.offer(node);
 		}
 	}
 
diff --git a/src/main/java/it/unibz/inf/isochrone/db/Database.java b/src/main/java/it/unibz/inf/isochrone/db/Database.java
index caf37f15..f66cf793 100644
--- a/src/main/java/it/unibz/inf/isochrone/db/Database.java
+++ b/src/main/java/it/unibz/inf/isochrone/db/Database.java
@@ -5,6 +5,7 @@ import it.unibz.inf.isochrone.config.ConfigDataset;
 import it.unibz.inf.isochrone.config.ConfigIsochrone;
 import it.unibz.inf.isochrone.network.Link;
 import it.unibz.inf.isochrone.network.Node;
+import it.unibz.inf.isochrone.network.NodeConnection;
 import it.unibz.inf.isochrone.util.EnumContainer.Direction;
 import it.unibz.inf.isochrone.util.EnumContainer.Mode;
 import it.unibz.inf.isochrone.util.Point;
@@ -76,11 +77,11 @@ public class Database {
 		final String configVertex = config.getTableVertex();
 		final String configVertexDensity = config.getTableVertexDensity();
 
-		queryLatestDepartureTimeHomo = "SELECT TIME_D, TIME_A, ROUTE_ID FROM " + configSchedule
-				+ " WHERE SOURCE = ? AND  TARGET = ? AND ROUTE_ID IN (%S) AND TIME_A>=? AND TIME_A<=? AND SERVICE_ID IN (%S) AND TIME_D >= ?";
+		queryLatestDepartureTimeHomo = "SELECT SOURCE, TARGET, TIME_D, TIME_A, ROUTE_ID FROM " + configSchedule
+				+ " WHERE SOURCE IN (%S) AND TARGET = ? AND ROUTE_ID IN (%S) AND TIME_A>=? AND TIME_A<=? AND SERVICE_ID IN (%S) AND TIME_D >= ?";
 
-		queryEarliestArrivalTimeHomo = "SELECT TIME_D, TIME_A, ROUTE_ID FROM " + configSchedule
-				+ " WHERE SOURCE = ? AND TARGET = ? AND ROUTE_ID IN (%S) AND TIME_D>=? AND TIME_D<=? AND SERVICE_ID IN (%S) AND TIME_A <= ?";
+		queryEarliestArrivalTimeHomo = "SELECT SOURCE, TARGET, TIME_D, TIME_A, ROUTE_ID FROM " + configSchedule
+				+ " WHERE SOURCE = ? AND TARGET IN (%S) AND ROUTE_ID IN (%S) AND TIME_D>=? AND TIME_D<=? AND SERVICE_ID IN (%S) AND TIME_A <= ?";
 
 		queryGetAllEdges = "SELECT ID, SOURCE, TARGET, LENGTH, EDGE_MODE, ROUTE_ID FROM " + configEdges;
 
@@ -217,25 +218,27 @@ public class Database {
 	/**
 	 * Gets the cost to go from one node to another node.
 	 *
-	 * @param node the node from which we should start calculating
-	 * @param adjNode the node to which we want to calculate the cost
-	 * @param routeIds the routeIds that should be taken into consideration
+	 * @param nConnection information about to where start the calculation, where to travel to and which routes are considered
 	 * @param dateCodes the days that should be taken into the account
 	 * @param fromTime the earliest time for which we want to calculate the departure time
 	 * @param toTime the latest time for which we want to calculate the departure time
 	 * @return the cost of for traveling from the start node to the end node
 	 */
-	public double getAdjNodeCost(final Node node, final Node adjNode, final Collection<Short> routeIds, final Set<Integer> dateCodes, final long fromTime, final long toTime) {
+	public Map<Node, Double> getAdjNodeCost(final NodeConnection nConnection, final Collection<Integer> dateCodes, final long fromTime, final long toTime) {
+		final Map<Node, Double> resultMap = new HashMap<>();
+		final Node node = nConnection.getSourceNode();
 		double minDistance = Double.POSITIVE_INFINITY;
 
 		ResultSet rs = null;
 		try {
 			if (isIncoming) {
-				rs = getLatestDepartureTime(adjNode.getID(), node.getID(), routeIds, dateCodes, fromTime, Math.round(toTime - node.getDistance()));
+				rs = getLatestDepartureTime(nConnection, dateCodes, fromTime, Math.round(toTime - node.getDistance()));
 				while (rs.next()) {
 					final long departureTime = rs.getLong("TIME_D");
 					final long arrivalTime = rs.getLong("TIME_A");
 					final short routeId = rs.getShort("ROUTE_ID");
+					final short adjNodeId = rs.getShort("SOURCE");
+					final Node adjNode = nConnection.getDiscreteTargetNode(adjNodeId);
 					adjNode.setDepartureTime(routeId, departureTime);
 					node.setArrivalTime(routeId, arrivalTime);
 					final double distance = toTime - departureTime;
@@ -245,13 +248,15 @@ public class Database {
 					}
 				}
 			} else {
-				rs = getEarliestArrivalTime(node.getID(), adjNode.getID(), routeIds, dateCodes, Math.round(fromTime + node.getDistance()), toTime);
+				rs = getEarliestArrivalTime(nConnection, dateCodes, Math.round(fromTime + node.getDistance()), toTime);
 				while (rs.next()) {
 					final long departureTime = rs.getLong("TIME_D");
 					final long arrivalTime = rs.getLong("TIME_A");
 					final short routeId = rs.getShort("ROUTE_ID");
-					adjNode.setArrivalTime(routeId, arrivalTime);
+					final short adjNodeId = rs.getShort("TARGET");
+					final Node adjNode = nConnection.getDiscreteTargetNode(adjNodeId);
 					node.setDepartureTime(routeId, departureTime);
+					adjNode.setArrivalTime(routeId, arrivalTime);
 					final double distance = arrivalTime > 0 ? arrivalTime - fromTime : Double.POSITIVE_INFINITY;
 					if (distance < minDistance) {
 						minDistance = distance;
@@ -265,7 +270,7 @@ public class Database {
 			DbUtils.closeQuietly(rs);
 		}
 
-		return minDistance;
+		return resultMap;
 	}
 
 	/**
@@ -329,23 +334,25 @@ public class Database {
 	/**
 	 * Gets the earliest arrival time at the target when traveled from the source.
 	 *
-	 * @param sourceId the id of the source node
-	 * @param targetId the id of the target node
-	 * @param routeIds the identifiers (as set) of the routes that should be considered
+	 * @param nConnection (contains the source node id, the targetIds and the route ids)
 	 * @param dates the date codes for which the times are calculated
 	 * @param from the earliest time for which we want to calculate the departure time
 	 * @param to the latest time for which we want to calculate the departure time
 	 * @throws SQLException thrown is the DB update statement can not be executed (or the prepared statement parameters could not be set)
 	 * @return a resultSet with all the times that come into consideration
 	 */
-	public ResultSet getEarliestArrivalTime(final int sourceId, final int targetId, final Collection<Short> routeIds, final Set<Integer> dates, final long from, final long to) throws SQLException {
-		final String sql = String.format(queryEarliestArrivalTimeHomo, preparePlaceHolders(routeIds.size()), preparePlaceHolders(dates.size()));
+	public ResultSet getEarliestArrivalTime(final NodeConnection nConnection, final Collection<Integer> dates, final long from, final long to) throws SQLException {
+		final int sId = nConnection.getSourceNode().getId();
+		final Collection<Integer> tIds = nConnection.getDiscreteTargetNodeIds();
+		final Collection<Integer> rIds = nConnection.getDiscreteRouteIds();
+
+		final String sql = String.format(queryEarliestArrivalTimeHomo, preparePlaceHolders(tIds.size()), preparePlaceHolders(rIds.size()), preparePlaceHolders(dates.size()));
 		final PreparedStatement statement = getPstmt(sql);
 
 		// CHECKSTYLE:OFF MagicNumber
-		statement.setInt(1, sourceId);
-		statement.setInt(2, targetId);
-		int idx = setValues(3, statement, routeIds.toArray());
+		statement.setInt(1, sId);
+		int idx = setValues(2, statement, tIds.toArray());
+		idx = setValues(idx, statement, rIds.toArray());
 		statement.setLong(idx++, from);
 		statement.setLong(idx++, to);
 		idx = setValues(idx, statement, dates.toArray());
@@ -358,23 +365,25 @@ public class Database {
 	/**
 	 * Gets the latest departure time to start from source, to arrive at the target in time.
 	 *
-	 * @param sourceId the id of the source node
-	 * @param targetId the id of the target node
-	 * @param routeIds the identifiers (as set) of the routes that should be considered
+	 * @param nConnection (contains the sourceIds, the target node id and the route ids)
 	 * @param dates the date codes for which the times are calculated
 	 * @param from the earliest time for which we want to calculate the departure time
 	 * @param to the latest time for which we want to calculate the departure time
 	 * @throws SQLException thrown is the DB update statement can not be executed (or the prepared statement parameters could not be set)
 	 * @return a resultSet with all the times that come into consideration
 	 */
-	public ResultSet getLatestDepartureTime(final int sourceId, final int targetId, final Collection<Short> routeIds, final Set<Integer> dates, final long from, final long to) throws SQLException {
-		final String sql = String.format(queryLatestDepartureTimeHomo, preparePlaceHolders(routeIds.size()), preparePlaceHolders(dates.size()));
+	public ResultSet getLatestDepartureTime(final NodeConnection nConnection, final Collection<Integer> dates, final long from, final long to) throws SQLException {
+		final int tId = nConnection.getSourceNode().getId();
+		final Collection<Integer> sIds = nConnection.getDiscreteTargetNodeIds();
+		final Collection<Integer> routeIds = nConnection.getDiscreteRouteIds();
+
+		final String sql = String.format(queryLatestDepartureTimeHomo, preparePlaceHolders(sIds.size()), preparePlaceHolders(routeIds.size()), preparePlaceHolders(dates.size()));
 		final PreparedStatement statement = getPstmt(sql);
 
 		// CHECKSTYLE:OFF MagicNumber
-		statement.setInt(1, sourceId);
-		statement.setInt(2, targetId);
-		int idx = setValues(3, statement, routeIds.toArray());
+		int idx = setValues(1, statement, sIds.toArray());
+		statement.setInt(idx++, tId);
+		idx = setValues(idx, statement, routeIds.toArray());
 		statement.setLong(idx++, from);
 		statement.setLong(idx++, to);
 		idx = setValues(idx, statement, dates.toArray());
diff --git a/src/main/java/it/unibz/inf/isochrone/network/Link.java b/src/main/java/it/unibz/inf/isochrone/network/Link.java
index a9d15add..799fe594 100644
--- a/src/main/java/it/unibz/inf/isochrone/network/Link.java
+++ b/src/main/java/it/unibz/inf/isochrone/network/Link.java
@@ -145,7 +145,7 @@ public class Link {
 	 * @return opposite Node of this link (or null)
 	 */
 	public int getOppositeOf(final Node node) {
-		if (getStartNode() == node.getID()) {
+		if (getStartNode() == node.getId()) {
 			return endNode;
 		}
 
diff --git a/src/main/java/it/unibz/inf/isochrone/network/MemoryOutput.java b/src/main/java/it/unibz/inf/isochrone/network/MemoryOutput.java
index 013b9a12..2836d2bd 100644
--- a/src/main/java/it/unibz/inf/isochrone/network/MemoryOutput.java
+++ b/src/main/java/it/unibz/inf/isochrone/network/MemoryOutput.java
@@ -44,8 +44,8 @@ public class MemoryOutput extends AbstractOutput {
 	}
 
 	@Override
-	public void addNodes(final Collection<Node> node) {
-		nodes.addAll(nodes);
+	public void addNodes(final Collection<Node> nodeCollection) {
+		nodes.addAll(nodeCollection);
 	}
 
 }
diff --git a/src/main/java/it/unibz/inf/isochrone/network/Node.java b/src/main/java/it/unibz/inf/isochrone/network/Node.java
index b0cfb06b..d3a828bc 100644
--- a/src/main/java/it/unibz/inf/isochrone/network/Node.java
+++ b/src/main/java/it/unibz/inf/isochrone/network/Node.java
@@ -113,7 +113,7 @@ public class Node implements Comparable<Node> {
 		return distance;
 	}
 
-	public int getID() {
+	public int getId() {
 		return id;
 	}
 
diff --git a/src/main/java/it/unibz/inf/isochrone/network/NodeConnection.java b/src/main/java/it/unibz/inf/isochrone/network/NodeConnection.java
new file mode 100644
index 00000000..02bb939c
--- /dev/null
+++ b/src/main/java/it/unibz/inf/isochrone/network/NodeConnection.java
@@ -0,0 +1,135 @@
+package it.unibz.inf.isochrone.network;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class NodeConnection {
+	private final Node sourceNode;
+	private final Map<Node, Link> continuousTargetConnections;
+	private final Map<Node, Link> discreteTargetConnections;
+
+	// Collections
+
+	public NodeConnection(final Node sourceNode) {
+		this.sourceNode = sourceNode;
+		this.continuousTargetConnections = new HashMap<>();
+		this.discreteTargetConnections = new HashMap<>();
+	}
+
+	// Getters
+
+	public Node getSourceNode() {
+		return sourceNode;
+	}
+
+	public Map<Node, Link> getContinuousTargets() {
+		return continuousTargetConnections;
+	}
+
+	public Node getContinuousTargetNode(final int nodeId) {
+		return getNodeById(continuousTargetConnections, nodeId);
+	}
+
+	public Collection<Integer> getContinuousTargetNodeIds() {
+		return getIdsForTargetNodes(continuousTargetConnections);
+	}
+
+	public Collection<Integer> getContinuousRouteIds() {
+		return getIdsForTargetNodes(continuousTargetConnections);
+	}
+
+	public Collection<Integer> getContiunousRouteIds() {
+		return getIdsForRoutes(continuousTargetConnections);
+	}
+
+	public Map<Node, Link> getDiscreteTargets() {
+		return discreteTargetConnections;
+	}
+
+	public Node getDiscreteTargetNode(final int nodeId) {
+		return getNodeById(discreteTargetConnections, nodeId);
+	}
+
+	public Collection<Integer> getDiscreteTargetNodeIds() {
+		return getIdsForTargetNodes(discreteTargetConnections);
+	}
+
+	public Collection<Integer> getDiscreteRouteIds() {
+		return getIdsForRoutes(discreteTargetConnections);
+	}
+
+	public Map<Node, Link> getTargets() {
+		final Map<Node, Link> allTargets = new HashMap<>();
+		allTargets.putAll(continuousTargetConnections);
+		allTargets.putAll(discreteTargetConnections);
+
+		return allTargets;
+	}
+
+	public Collection<Integer> getTargetNodeIds() {
+		return getIdsForTargetNodes(getTargets());
+	}
+
+	public Collection<Integer> getRouteIds() {
+		return getIdsForRoutes(getTargets());
+	}
+
+	// Public methods
+
+	public void addTargetLink(final Node targetNode, final Link link) {
+		if (link.isContinuous()) {
+			continuousTargetConnections.put(targetNode, link);
+		} else {
+			discreteTargetConnections.put(targetNode, link);
+		}
+	}
+
+	public boolean containsTargets() {
+		return containsContinuousTargets() || containsDiscreteTargets();
+	}
+
+	public boolean containsContinuousTargets() {
+		return !continuousTargetConnections.isEmpty();
+	}
+
+	public boolean containsDiscreteTargets() {
+		return !discreteTargetConnections.isEmpty();
+	}
+
+	// Private static methods
+
+	private static Node getNodeById(final Map<Node, Link> map, final int nodeId) {
+		final Set<Entry<Node, Link>> entries = map.entrySet();
+		for (final Entry<Node, Link> entry : entries) {
+			if (nodeId == entry.getKey().getId()) {
+				return entry.getKey();
+			}
+		}
+
+		return null;
+	}
+
+	private static Collection<Integer> getIdsForRoutes(final Map<Node, Link> m) {
+		final Collection<Link> linkSet = m.values();
+		final Set<Integer> resultSet = new HashSet<>(linkSet.size());
+		for (final Link l : linkSet) {
+			resultSet.add(l.getRoute());
+		}
+
+		return resultSet;
+	}
+
+	private static Collection<Integer> getIdsForTargetNodes(final Map<Node, Link> m) {
+		final Set<Node> nodeSet = m.keySet();
+		final Set<Integer> resultSet = new HashSet<>(nodeSet.size());
+		for (final Node n : nodeSet) {
+			resultSet.add(n.getId());
+		}
+
+		return resultSet;
+	}
+}
-- 
GitLab