As the maps are represented as directed graphs, the normal algorithm used to calculate the shortest path between two nodes is the Dijkstra algorithm. This algorithm is used in conjunction with a modified version called the A* algorithm which uses the fact that the best path ought to lie in the direction towards the destination.
The server covers a large area, hence the map database is large. For memory efficiency we have partitioned the road network into sufficiently large maps. This means that the origin and the destination are not necessarily in the same map. For calculation efficiency we introduced hierarchical maps.
As with all modules, there is a route leader and zero or more available RouteModule
instances. The task of distributing sub routes is done by RouteSender
object in the server.
If the distance between the origins and the destinations are small we have to consider all roads when calculating the best route, i.e. we are using a level 0 map. If the origin and the destination lies within the same map the algorithm calculates the route and returns the RMSubRoutes
to the destination and to the external nodes that have costs that are less than the cost to the destination.
When routing over large distances, e.g. from Stockholm to Berlin, we cannot consider all the small roads in between, hence we have a hierarchical structure for the maps.
The difference between higher level routing and lower level routing is that the RouteModule
does not route on all nodes when the route should become a higher level route. The RouteModule
uses two bounding boxes to decide which nodes that should be included in the route calculation. The inner bounding box is created by taking the coordinate of the origin and expanding the box with a constant distance in the x
and y
directions. The outer bounding box is created the same way but with a larger increase in size. Inside the inner bounding box the following connections are allowed (higher level node
means that the node exists on the overview map):
Connections from a lower level node to a lower level node.
Connections from a higher level node to a higher level node.
Connections from a lower level node to a higher level node
Inside the outer bounding box the following connections are allowed:
Connections from a lower level node to a higher level node.
Connections from a higher level node to a higher level node.
This decreases the number of nodes and connections that have to be considered by the Dijkstra algorithm, hopefully without decreasing the optimality of the route very much.
To quickly reach the destination we will use an algorithm that attracts the route towards the destination node, i.e. the A* algorithm. This is done by adding a second cost that represents the estimated cost of getting from the origin to the destination. This cost is used to decide which node to remove from the heap. When we update a node we first calculate the cost of getting from the origin to the current node and store this value in the normal cost. Then an estimation of the distance from the current node to the closest destination will be calculated and added to the estimation cost. This cost has to be lower than the actual cost to guarantee an optimal path. If we are using distance as cost parameter we would like to use the flight distance as the estimation. This is an expensive calculation, but it seems that the cost of reading/writing nodes from memory greatly overshadows this problem, so the estimated distance is calculated as sqrt( deltax2 + deltay2 ). If the cost represents time we use dist/max(speed) for the estimation. Currently max(speed)
is set to 120 km/h.
For each RoutingMap
loaded in the RouteModule
, a CalcRoute
object is created. The RoutingMap
is deleted when the CalcRoute
is deleted. CalcRoute
is the class that does the actual routing.
route Main function that loops through functions below for each request.
removeDups Removes identical origins and destinations.
routeOnOneSegment Creates routes where the origin and destination is on the same segment.
initRoute Prepares the map and queues for the Dijkstra algorithm.
calcCostDijkstra This function uses the Dijkstra algorithm or the A* algorithm to create routes to the destination nodes.
calcCostExternalDijkstra Calculates routes from the origin to all external connections.
readResult Reads the routes from external nodes and/or destinations back to origins.
This method removes duplicate origins and duplicate destinations. This is necessary for e.g. calcCostDijkstra
which ends when all destinations are found.
The method routeOnOneSegment
is called before initRoute
and loops over all origins and destinations and checks if the node id is the same for the current origin and destination. If the node id is the same, it calculates a route on the segment if the offset for the destination is greater than the offset for the origin. The RouteModule
uses offset from the current node (unlike the rest of MC2 that always uses offset from node zero) which makes calculations easier to follow. The one segment routes created by the method are put into the result list and then it is up to the server to decide whether to use it or not. State changes and penalties are added to the route if it is impossible to drive on the segment.
The method initRoute
enqueues the start nodes into the priority queue. If the originalRequest-flag is set in the packet from the server, initRouteFirstTime
is called instead. This behaviour should change, since there can be both first time
-routes and continued routes in the same request from the server. initRouteFirstTime
takes the offset into consideration and does not enqueue the start nodes themselves, but takes the origins and enqueues the nodes to which the origins have connections. The offset cost of the origin nodes are subtracted from the cost of the enqueued nodes. initRouteFirstTime
also enqueues nodes with restrictions which make the nodes forbidden for the vehicle selected by the user. These are enqueued in a separate queue, m_notValidPriorityQueue
. If there are no valid nodes in the ordinary priority queue, the nodes in m_notValidPriorityQueue
will be expanded until a valid node is reached. This node is then inserted into the normal priority queue and the ordinary routing can begin.
These two methods do essentially the same thing, i.e. calculates the shortest path from the origin to the destination(s) using the Dijkstra algorithm. The difference it that calcCostDijkstra
stops after finding one or many destinations (depending on the input) or until the heap is empty and calcCostExternalDijkstra
runs until the heap is empty. The methods dequeue nodes from the priority queue. Every connection from the dequeued node is checked for vehicle restrictions and every connected node is checked for node restrictions. If the connection and new node have restrictions which are allowed, the real cost and estimated cost of the new node are updated with the connection costs and, if the cost is less than the cut off, it is enqueued into the priority queue. The gradient of the new node is set to the old node so that the route can be followed backwards from the destination when the result is read. This is repeated until the stop criteria for the function is fulfilled.
After the Dijkstra functions are run readResult
is called. The main task for the method is to create RMSubRoutes
by following the gradients of the destination nodes back to the origins. But before this can be done it has to check if the destination could be reached the normal way or not. calcCostDijkstra
and calcCostExternalDijkstra
only consider the nodes that can be reached using the vehicle selected by the user. Sometimes that vehicle is not allowed to be used all the way to the destination. If this is the case we try to calculate the last part of the route using a pedestrian as vehicle. It is done like this:
The destinations are checked. If no destinations have been reached with the normal vehicle we continue to the next step.
The estimated cost of the destination nodes are set to 0
.
The destination nodes are equeued into the m_notValidPriorityQueue
.
Routes are made from the destinations using m_notValidPriorityQueue
and backward routing, but only the estimated cost of the nodes are used (in the method expandNonValidNodesResult
).
If a node with the real cost set (by calcCostDijkstra
) is reached it is put into m_normal-PriorityQueue
.
All the nodes affected by expandNonValidNodesResult
except the ones in m_normal-PriorityQueue
are reset to default values.
expandNodesResult
is run. It calculates forward routes from the nodes in m_normal-PriorityQueue
to the destination without considering vehicle restrictions.
When this is done readResulFromDestination
is called for all destinations that were reached and the RMSubRoutes
are created. These are then sent to the server.
The BucketHeap is the heap type used by the Dijkstra algorithm in the RouteModule
.
The BucketHeap is a data structure specifically designed to be used as a heap in the Dijkstra algorithm. Its idea is that the heap consists of several buckets in which to put nodes of the graphs (=maps). A specific range of costs has been assigned to each bucket so that each bucket contains nodes with about the same cost. In a bucket, the ordering of the nodes is not important, and therefore nodes can be inserted into the heap very fast. The only thing to be done is finding the correct bucket. Removing a node from a bucket is somewhat more slow, the bucket containing the nodes with the least cost has to be searched for the minimum cost with linear search, which is slow. To improve the speed of the heap, no sorting should be done unless there is a destination node in the current bucket.
The only supported operations on the BucketHeap are enqueue
, dequeue
, isEmpty
and reset
. The reason that no other operations are defined is that the kind of Dijkstra used does only need these. The heap work in the following way:
The buckets of the heaps all carry nodes with ascending costs, i.e. one bucket contains nodes carrying the currently lowest costs, the next bucket contains the following node costs and so on. The cost span is the same for all buckets. The bucket with the lowest costs, the minimum cost bucket, and the lowest cost available for enqueue itself are stored in variables. The maximum cost is decided by the number of buckets and the cost span of a bucket. This implies that only a finite cost range of node costs can exist at a time in the heaps. However, the Dijkstra algorithm will never enqueue a node with cost lower than the lowest cost the heap can handle (that node cannot have been previously dequeued). If, for any reason, this happens anyway, the node will be enqueued in the minimum costs bucket and will be found. If a node with larger cost is to be enqueued, it will simply have to be put into a special, extra large bucket that is used for too large costs, the large costs bucket.
As the buckets have a finite size, overflow might occur. Therefore, to every bucket (also the large costs bucket) an overflow list is attached to take care of the overflow nodes. Normally, these lists will be empty.
The reset
operation simply marks the buckets as empty and sets all other variables (like the minimum cost the heap is supposed to handle and the bucket with the minimum cost) to zero or something equivalent. If overflow has occurred during the routing, the overflow lists will be cleared.
There are three cases that can occur when starting enqueue
. If the cost of the node is so large that it is larger than the cost span of the heap the node has to be put into the large costs bucket. This very large bucket should be emptied by dequeue
at certain conditions. If the cost is smaller than what the minimum cost bucket holds, the node will be enqueued into that bucket anyway. This will not pose a problem, since dequeue
will find that node in its linear search and return it. Although the approach with too small costs is straightforward it should never happen, as Dijkstra and Dijkstra-like algorithms never will enqueue a node with smaller cost than the lowest cost in the heap. There are some special cases though, when a modified algorithm makes this happen. The third and most common case of costs is when the cost of a node fits into the cost span. The correct bucket to put the node into is computed, and the node is enqueued. Also, a variable containing the total number of nodes is increased.
If the chosen bucket is full when one wants to put more nodes in it, overflows, the node will be put in the respective overflow list by a special function overflowEnqueue
.
The operation isEmpty
is the simplest. It simply checks if all buckets are empty. If so is the case, the function returns true
, otherwise false
. This will be done by checking if the total number of nodes is zero.
The dequeue
-operation is the most time consuming and complex of the BucketHeap, because all work has been left to this function. First a consolidating function, flushHeap
is called. It examines the minimum cost bucket and if the number of nodes in this bucket is zero, the next bucket in cost order becomes the minimum cost bucket and the old bucket is assigned costs one step higher than the previously largest costs. If the first bucket that was the minimum cost bucket has again become minimum cost bucket flushHeap
calls another function, tryToEmptyLargeCostsBucket
, that will try to empty the large costs bucket carrying the nodes that previously had too large costs to be enqueued into the normal buckets. There is a good chance that many of the nodes in this bucket now successfully can be moved to the normal heap, because the node costs handled by the heap now are substantially larger. The reason for moving the nodes back to the normal heap is that dequeuing never will take place in the large costs bucket. The function tryToEmptyLargeCostsBucket
examines every element in the large costs bucket and, if the current node is enqueueable, puts it into the right bucket, just as enqueue
would. If the node still has a too large cost it will remain in the large costs bucket. When tryToEmptyLargeCostsBucket
and flushHeap
are done, dequeue
has a minimum cost bucket containing nodes and starts to search (with linear search) for the node with the smallest cost. When that node is found the total number of nodes in the heap is decreased and the minimum cost node is returned. If this bucket has a non-empty overflow list the function overflowDequeue
will browse the list and the minimum cost bucket to find the minimum cost node.
The buckets are implemented as a matrix with the rows as buckets and the columns representing spaces in the buckets. The large costs bucket is an extra large array (if the length of a bucket array is $A$, the length of the large costs bucket has length A*, just to make sure that it is so large that overflow almost never occurs). The number of nodes in a bucket is stored as an array with the size of the number of rows in the matrix. Thus, when performing reset
only the array is set to zero, and the time consuming reset of the heap matrix is omitted. The number of nodes in the heap is stored in a variable and then isEmpty
only needs to check if this variable is zero or not.
When enqueue
decides which bucket (row in the matrix) a node should be put into the following piece of code is used:
Here rowIndex
denotes the row chosen, cost
is the cost of the node to enqueue, m_costsStartAt
is the lower cost bound of the heap and m_heapStartIndex
is the index of the matrix that denotes the minimum cost bucket. ROWS
is the number of rows in the matrix (number of buckets). The MATRIX_DIFF
is the cost span of a bucket which equals 2MATRIX_DIFF_EXPONENTIAL. The logical shift operation >>
is used instead of dividing, as this is an essentially faster operation.
The four important heap constants are ROWS
(= the number of buckets), COLUMNS
(= the number of possible spaces to put a node into in each bucket), MATRIX_DIFF
(= the cost difference between two rows in the matrix, i.e. between two buckets) and MATRIX_DIFF_EXPONENTIAL
which was explained above. These parameters have an enormous influence on the performance of the Bucket Heap and the choice of these constants is a matter of optimization. See figure below for an example of the BucketHeap.
The tree of classes under RouteRequest
takes care of the intelligence that controls how to do routing between maps, whereas the RouteModule
does the routing on each map. These two instances communicate through the classes held in ServersShared
described in a separate section below. Dependencies within the server as well as to the shared objects are found in this figure.
This class handles questions concerning mapping from coordinates to item. It finds the item closest to the coordinate. Note that two maps can overlap, and hence several maps may contain the required information.
processPacket Takes care of a CoordinateReplyPacket
after a request has been sent, and either generates a new request or prepares the answer.
getNextPacket Returns the next packet from m_packetsReadyToSend
addCoordinate Adds a coordinate to the question.
getCoordinateReply Returns a CoordinateReplyPacket
as an answer to the original question.
This class is used to complete data about each OrigDestInfo
in a request, and uses a CoordinateObject
for one type of questions.
processPacket Takes care of a CoordinateReplyPacket
or an ItemReplyPacket
after a request has been sent.
getNextPacket Returns the next packet from m_outgoingQueue
.
Inheriting from Shared class SubRouteVector
this class will help other server classes to keep track of what SubRoutes
have been routed on and which to continue.
insertSubRoute Inserts an element last in the vector. Also tells the SubRoute what index it is.
getCutOff Returns the greatest of the costs to get to a destination if we should return the routes to all destinations. Returns the cheapest cost to one destination if this is the last part of the route. MAX_UINT32 if we have not reached all destinations yet.
containsCheaperThan Returns true if the vector contains a subroute with cheaper or same real cost to the same destination node.
merge Adds all the SubRoutes from sourceVector, except the last one, which instead is returned.
getResultVector Function for finding the SubRoutes that make up a route with specified index to m_destIndexArray
when routing is finished.
Main server class for holding information about a group of SubRoutes
while routing. When routing is finished, the SubRoutes
will instead be put in a ServerSubRouteVector
.
Entries consist of SubRoutePair
objects (see pair
in map in STL). Key is estimated cost in the node where the SubRoute
ends. By using this key, two things will go faster:
Assigning a new best SubRoute when the previous is dequeued.
Finding and removing SubRoutes above cutoff cost.
Data in the pair is a pointer to a SubRoute
. Class inherits from multimap
(STL).
moveAllSubRoutes Finds and dequeues all remaining SubRoutes in the container and moves them to specified resultVector.
dequeueSubRoutesFromBestMap Finds and dequeues all SubRoutes
ending in m_mapIDBestSubRoute
.
checkAndInsertSubRoute Uses multimap function ‘insert’ on a SubRoute
pointer.
insertSubRoute Uses multimap function ‘insert’ on a SubRoute pointer.
insertOrigDestInfo Inserts an OrigDestInfo
as a new SubRoute
into the container.
updateContainer Calls clearGreaterThanCutOff
and then updates m_mapIDBestSubRoute
.
updateCutOff Sets the value of m_cutOff
if new cutOff smaller than m_cutOff
.
getSize Returns the size of the SubRouteContainer
This class does all the tasks involved in going up and down a level in the routing. That includes finding origin and destination nodes on higher level, and keeping track of which maps to route on after routing on higher level.
processPacket Takes care of a range of different ReplyPacket
s after a request has been sent, and either generates a new request or prepares an answer.
getNextPacket Returns the next packet from m_outgoingQueue
.
allowedMapID Returns true if specified mapID is in m_destMapSet
.
getLowerLevelDest Returns an OrigDestInfo from m_matchHighLowIDMap
specified by map ID and node ID.
getHigherLevelOrigList Returns m_higherLevelOrigList
which must previously have been created by this object.
getHigherLevelDestList Returns m_higherLevelDestList
which must previously have been created by this object.
Adminstrates the packets for a route request. Divides the question into subroute requests for a single map, and then creates a RequestPacket
for that question. The packet is sent through the server to the RouteModule
to be calculated.
processPacket Takes care of a range of different ReplyPacket
packets after a request has been sent, and either generates a new request or prepares an answer.
getNextPacket Returns the next packet from m_outgoingQueue
.
getRoute Goes through the m_finishedSubRouteVector
and backtracks the finished routes. Each of these is a SubRouteVector
. Return type is SubRouteVectorVector
.
insertOrigDestInfo Inserts an origin as destination in a SubRoute to initialize routing there. Then adds this SubRoute to m_requestSubRouteContainer
.
Class for taking care of a route. Any number of origins and destinations can be used but in most cases only one origin and one destination is used or either multiple origin or multiple destinations but not both.
processPacket Takes care of a range of different ReplyPacket
packets and rebuilds the outgoing list.
getNextPacket Returns the next packet from getNextPacket
in m_routeSender
.
getRoute Returns the answer to the constructor question when the routing is finished.
Creates a new list of split packets internally in the object.
Class that creates an expanded list of directions corresponding to a finished route.
processPacket Takes care of a ExpandRouteReplyPacket
and merges all to one route.
getNextPacket Returns the next ExpandRouteRequestPacket
packets from m_requestPacketList
.
getList Returns the answer to the constructor question when the routing is finished.
This class administrates a routing question.
processPacket Takes care of a ReplyPacket
and sends on to the appropriate child.
getNextPacket Returns the next RequestPacket
from the appropriate child.
getExpandedRoute Get the route as an ExpandedRoute
object that is easy to use.
addOrigin Adds an origin for the routing. Must be called before the RouteRequest
is processed.
addDestination Adds a destination for the routing. Must be called before the RouteRequest
is processed.
getStatus The status after the RouteRequest
has been processed. StringTable::OK if routing was successful.