| Pages: [1] 2 :: one page |
| Author |
Thread Statistics | Show CCP posts - 0 post(s) |

Alvaliella
|
Posted - 2008.11.05 17:15:00 -
[1]
I'm trying to make a trade tool and I'm currently investigating how to work out the shortest route between two systems.
I'm planning on using the A* algorithm but I was wondering what to use for H? I'm guessing that the distance from the x,y,z position of the current node to the x,y,z position of the goal node would be useable but that seems prone to error.
Any ideas for what would be good to use for H please? Thanks! |

Ambo
State Protectorate
|
Posted - 2008.11.05 18:16:00 -
[2]
x,y,z distances are pretty useless when working out number of jumps because some jumps are waaaay longer than others.
Personally I didn't use an A* algorithm but I'd think H should be the number of inter-region jumps and, if you're already in the right region, then the number of inter-constellation jumps. If you're in the right constellation then a complete search will take virtually no time at all. --------------------------------------
Trader? Investor? Just want to track your finances? Check out EMMA |

Alvaliella
|
Posted - 2008.11.05 18:40:00 -
[3]
Originally by: Ambo x,y,z distances are pretty useless when working out number of jumps because some jumps are waaaay longer than others.
Personally I didn't use an A* algorithm but I'd think H should be the number of inter-region jumps and, if you're already in the right region, then the number of inter-constellation jumps. If you're in the right constellation then a complete search will take virtually no time at all.
Thanks for the reply Ambo. I was planning on using the x,y,z distances to evaluate how suitable the next potential jump nodes would be- whichever node was physically closest to the goal node would be selected as the most suitable... I can see it backfiring in cases where nodes move you physically farther away before getting you much closer with a massive jump though 
If you don't mind me asking, what did you use to find you routes? Dijkstra's algorithm or did you just keep expanding nodes until one hit the desired region, then constellation etc? |

Ambo
State Protectorate
|
Posted - 2008.11.05 20:43:00 -
[4]
I'm not sure if it has a name or not... I used a recursive procedure that stars from the destination and works backwards.
First I do regions, I then use that list of potential regions in the route to get the route through constellations, finally, I do the same for systems.
I suppose it's essentially a restricted bredth-first search with a lot of cacheing to speed things up, prevent back-tracking, etc. Frankly, I think it's over complicated and a little slow... I really should re write it as an A* algorithm. --------------------------------------
Trader? Investor? Just want to track your finances? Check out EMMA |

Alvaliella
|
Posted - 2008.11.05 21:09:00 -
[5]
Originally by: Ambo I'm not sure if it has a name or not... I used a recursive procedure that stars from the destination and works backwards.
First I do regions, I then use that list of potential regions in the route to get the route through constellations, finally, I do the same for systems.
I suppose it's essentially a restricted bredth-first search with a lot of cacheing to speed things up, prevent back-tracking, etc. Frankly, I think it's over complicated and a little slow... I really should re write it as an A* algorithm.
Thanks for the info, I might give that approach a shot and see what happens! The region & constellation connection data might make it equivalent to the A* method... will see what happens anyway |

Jethro Jechonias
Ki Tech Industries
|
Posted - 2008.11.05 21:55:00 -
[6]
Edited by: Jethro Jechonias on 05/11/2008 22:02:32
You could try using a lookup table has a heuristic guide.
Given that distances are symmetrical, you could halve the size of your lookup table by ordering the query elements.
For example:
h(a, b) { if (a == b) { return 0; // if input is SolarSystemID return 1; // if input is ConstellationID or RegionID } else if (b < a) { return h(b, a); } else { return lookup[a][b]; } }
The size of the jagged array (lookup[][] in the pseudo-code above) would depend on what level of detail you use with the array.
If you limit it to just regions, the array would contain 2,016 elements. Using constellations it would contain 281,625 elements. If you go right down to the solar system level, you would need 13,522,600 elements.
Given that the maximum distance between any two systems is 99 jumps, you could use a single byte as the array element. Thus the size of array needed for a region / constellation / solar system lookup table would be 2KB / 276KB / 13MB.
The lookup table could be loaded from the same source as your jump table. Either a file or database.
|

Alvaliella
|
Posted - 2008.11.05 22:46:00 -
[7]
Originally by: Jethro Jechonias Edited by: Jethro Jechonias on 05/11/2008 22:24:42
You could try using a lookup table has a heuristic guide.
Given that distances are symmetrical, you could halve the size of your lookup table by ordering the query elements.
For example:
h(a, b) { if (a == b) { return 0; // if input is SolarSystemID return 1; // if input is ConstellationID or RegionID } else if (b < a) { return h(b, a); } else { return lookup[a][b]; } }
The size of the jagged array (lookup[][] in the pseudo-code above) would depend on what level of detail you use with the array.
If you limit it to just regions, the array would contain 2,016 elements. Using constellations it would contain 281,625 elements. If you go right down to the solar system level, you would need 13,522,600 elements.
Given that the maximum distance between any two systems is 99 jumps, you could use a single byte as the array element. Thus the size of array needed for a region / constellation / solar system lookup table would be 2KB / 276KB / 13MB.
The lookup table could be loaded from the same source as your jump table. Either a file or database. Given that you would be hard pressed to get the Jumps table down to less than 58KB, the regional lookup table is probably quite reasonable for any application and even the constellation lookup table doesn't look too bad.
Thanks for the suggestion Jethro but I'm afraid that it's almost over my head I'm not really a programmer, just a determined jack of all trades 
I do understand your idea of referring to a table for guidance on the suitability of a node but I can't think of how I'd put such a table together. I have all of the region, constellation & solar system jump connection data but I dunno how I could crunch it into something meaningful so that I could ask the table whether x node would be better to go to than y node in order to get to z node etc.
Meh, I'll figure something out anyway- I'm currently filtering down the data as much as possible (removing all systems below a certain security level) which trims the lists of possible jumps down a lot- I reckon it might be possible to just use brute force and expand nodes from the start/end position until they meet 
|

Jethro Jechonias
Ki Tech Industries
|
Posted - 2008.11.05 22:59:00 -
[8]
What language are you developing in?
Your question has got me think that A* might be a huge improvement over my current methods, so I am starting to build something that works along those lines in C#.
The table is not too hard to put together in SQL. The catch is that the SQL version is much larger than the memory structure that you can use in C#, and that is why I am working on putting one together.
|

Alvaliella
|
Posted - 2008.11.05 23:19:00 -
[9]
Originally by: Jethro Jechonias What language are you developing in?
Your question has got me think that A* might be a huge improvement over my current methods, so I am starting to build something that works along those lines in C#.
The table is not too hard to put together in SQL. The catch is that the SQL version is much larger than the memory structure that you can use in C#, and that is why I am working on putting one together.
Promise not to laugh?... I'm using VBA with Excel 
I'm making a giant market order crunching machine and Excel seemed to be the most hassle free option at the time- it's going great so far it must be said! It was a doddle to get all of the relevant Eve data from SQL into worksheets, automate importing of market export files & crunching the numbers etc.
I dunno how you could get over the memory limitation in C#... if you're using the A* algorithm then maybe you could use SQL to split up all of the jump information into separate tables for each region/constellation and only pull in the data for the region/constellation that you're currently in.
If you use the x,y,z distance as the heuristic (crappy but better than nothing), then you could work blind & "feel" your way there- the x,y,z of your destination never changes and you'd get your current x,y,z from the region/constellation file that you current have in memory.
I came across this in my research- it might be of use?
Eve Toolbox AutoPilot source |

Jethro Jechonias
Ki Tech Industries
|
Posted - 2008.11.06 00:30:00 -
[10]
Edited by: Jethro Jechonias on 06/11/2008 00:33:06
Originally by: Alvaliella I dunno how you could get over the memory limitation in C#... if you're using the A* algorithm then maybe you could use SQL to split up all of the jump information into separate tables for each region/constellation and only pull in the data for the region/constellation that you're currently in.
If I store the information in SQL, I am looking at a table with 9 bytes per row: 4 bytes for each of the Celestial ID's and 1 byte for the distance.
If I store the information in a C# jagged array, I am looking at 1 byte per row: there is no need to store the Celestial ID's as they form the dimensions of the array.
All I need is a means of converting the Celestial ID to a continuos range of integers and back again: that looks like it will be simple enough to do.
So by moving from SQL to C# I can decrease my memory requirements by approx 9x: well worth the effort.
Not sure how you might go about implementing it in VBA. The Regional distance table is small enough that you could implement it as a sheet in Excel. It is the Jump Table and such that would be much harder to implement.
Using a Regional distance table is likely to be a much better huristic than using the x,y,z distance; possibly supplimented it with a limited Constellation table.
Footnote: by Celectial ID, I mean either SolarSystemID, ConstellationID, or RegionID depending on which lookup table I am using.
|

Ambo
State Protectorate
|
Posted - 2008.11.06 08:24:00 -
[11]
Incidentally, when I was writting my algorithm I initally found that my program would usually give the fasted route but would somtimes come up with longer routes than the in-game auto pilot. After a while I realised that this was always occuring for routes where both systems where in the same region but the quickest route went through a different region. i.e. If you're in the right region, that does not mean you want to ignore inter-region jumps.
Just thought I'd highlight this potential stumbling block
I'm also going to be working on a proper A* version because I think it could be a fair bit faster and simpler than my current solution. --------------------------------------
Trader? Investor? Just want to track your finances? Check out EMMA |

Xaroth Brook
Minmatar BIG Libertas Fidelitas
|
Posted - 2008.11.06 08:33:00 -
[12]
If you try A*, use it as suggested above, though I personally would add some 'changes' to that idea.
1) Calculate from-region to-region path, store that 'map' of nodes traversed 2) Calculate from-constellation to-constellation path, using ONLY the regions in the map and their adjacent ones (while 2 regions might be tied together it might be faster to nip into a neighbouring region to get there quicker (going / instead of _| .. ) store this in a map as well 3) Calculate from-sol to-sol path using the constellation map and 1(or 2) adjacent constellations.
If you were to just calculate the from-sol to-sol path you'll be taking minutes for an accurate path (in eve going from A to B won't always be in a straight line, so you can't use guessing methods in your formulla based on direction), by ruling out half of EVE's regions, and of those regions almost half of the constellations you'll be taking down the calculation time to seconds (if not less)
I used to have a snippet that calculated that but i think i lost it in one of the recent reformats i had to do, but the above concept pretty much sums it up.
It was like a baby, it landed on my lap and was helpless and totally defenseless. Then I shot it and bragged about it on a killboard.
|

Alvaliella
|
Posted - 2008.11.06 09:07:00 -
[13]
Edited by: Alvaliella on 06/11/2008 09:08:52 Thanks for the replies & help all- some very good ideas there and I hadn't thought about the region-to-region issue (i.e. that it's not always quicker). I'll try calculating the path for regions first, then constellations then solar systems as it seems best.
Does anyone know how CCP make the AutoPilot work? The more I work on this, the more amazed I'm getting at how fast it can work out a route **hugs on board computer**
|

Jethro Jechonias
Ki Tech Industries
|
Posted - 2008.11.06 15:44:00 -
[14]
Originally by: Alvaliella Does anyone know how CCP make the AutoPilot work?
CCP's implementation of the autopilot is very ineffiencient. I wouldn't recommend using that as a guide on how to build an autopilot.
|

sumtool
Amarr Sniggerdly Pandemic Legion
|
Posted - 2008.11.08 20:58:00 -
[15]
The basic way of doing it is to use Dijkstra's algorithm, systems will become the nodes with jumps being the paths between said nodes. You can modify the algorithm by assigning different weights to the jumps to influence whether a certain path gets chosen or not.
I.e. start off all jumps at weight 10, but jumps that go from hisec -> lowsec end up on 5 -- this way the algorithm will try to keep you out of lowsec.
For a full explanation on the algorithm: http://en.wikipedia.org/wiki/Dijkstra%27s_Algorithm
|

Kazuo Ishiguro
House of Marbles Zzz
|
Posted - 2008.11.09 11:23:00 -
[16]
Originally by: Jethro Jechonias
Originally by: Alvaliella Does anyone know how CCP make the AutoPilot work?
CCP's implementation of the autopilot is very ineffiencient. I wouldn't recommend using that as a guide on how to build an autopilot.
It does a good job at finding the quickest route between any 2 systems. It'd be better if it settled for a compromise with longer routes rather than trying to find an optimal solution. --- Can't afford that BPO? Look here. 20:1 mineral compression The EVE f@h team |

Blazing Fire
Interstellar Operations Incorporated
|
Posted - 2008.11.13 08:45:00 -
[17]
I use A* with x,y,z and the number of jumps from the start point for H.
Sometimes it gives 1-3 jumps longer routes then the EVE Auto Pilot.
I don't bother to include the security status in the H. If I don't want to go into lowsec/nulsec, I just don't add those systems in the Open list. Of course this does not work if you need to go there. If you want to, the sec status doesn't matter, you just want the shortest path.
Keep in mind that shortes path in number of jumps, but it may not be the fastest. Sometimes the travel time on alternate routes is much small because closer distance between stargates. |

Entity
X-Factor Industries Synthetic Existence
|
Posted - 2008.11.13 17:34:00 -
[18]
Originally by: Kazuo Ishiguro
Originally by: Jethro Jechonias
Originally by: Alvaliella Does anyone know how CCP make the AutoPilot work?
CCP's implementation of the autopilot is very ineffiencient. I wouldn't recommend using that as a guide on how to build an autopilot.
It does a good job at finding the quickest route between any 2 systems. It'd be better if it settled for a compromise with longer routes rather than trying to find an optimal solution.
Dijkstra is near instant for any single route. Don't confuse finding shortest route between A and B with finding the shortest tour through a variable number of waypoints. _
Got Item? | EVE API? |

Kniht
D.M.T inc Bionic Dawn
|
Posted - 2008.11.15 00:55:00 -
[19]
While dijkstra works, it's designed for a weighted graph. If you just want shortest path by stargates, breadth-first search with trimming already visited nodes is all you need. It's what dijkstra becomes when all edge weights are identical, except you store less state and it's almost trivial to write from scratch.
My current route planner uses this, is very fast, and always finds one of the shortest routes. (You can continue the search and find all equally shortest routes, but I currently have no need for this.)
|

Kazuo Ishiguro
House of Marbles Zzz
|
Posted - 2008.11.15 01:16:00 -
[20]
Are the locations of stargates within systems available as part of the data dump? If so, someone could come up with a route planner that minimises time in warp as well as jumps... |

Ki Anna
Ki Tech Industries
|
Posted - 2008.11.15 01:22:00 -
[21]
Originally by: Kazuo Ishiguro Are the locations of stargates within systems available as part of the data dump? If so, someone could come up with a route planner that minimises time in warp as well as jumps...
Yup, that data is there. |

Blazing Fire
Interstellar Operations Incorporated
|
Posted - 2008.11.17 12:23:00 -
[22]
Originally by: Kazuo Ishiguro Are the locations of stargates within systems available as part of the data dump? If so, someone could come up with a route planner that minimises time in warp as well as jumps...
I have done this for my tools. It is very easy done with precalculated SQL table. All you have to do is to caclulate the distance from each gate to the others in the same systems and store the results in a table. Mine contains source system ID, target system Id, ID of the system where teh gates are located and the precalculated distance between the gates.
When using gate to gate distance and jumps from the start system and the 3d distance from the current system to the target system for Path scoring, I get similar if not the same results as the ingame autopilot.
|

Blazing Fire
Interstellar Operations Incorporated
|
Posted - 2008.11.17 12:31:00 -
[23]
in short if the Path score is F=G+H, where G is the movement cost fro mthe starting system to the current node, and H is the estimated movement cost from the current node to the destination I use this:
G=gate to gate distance/100000000000000 + jumps from start node H=3D distance from current node to the target system/100000000000000
This gives me pretty good results. |

Kazuo Ishiguro
House of Marbles Zzz
|
Posted - 2008.11.17 19:27:00 -
[24]
I've done a fair bit of research on how much distance ships cover during warp - you accelerate exponentially for 10 seconds and slow down exponentially for 20 seconds. Thus a lower bound for each jump is 30 seconds (this is also enforced by the session change timer).
During this time, excluding any additional ground covered while at full warp, you typically cover a distance equivalent to travelling at full warp speed for 1.33 seconds.
Also, it's worth finding your average grid load time (leave the log window open while on autopilot for 10-20 jumps and all the relevant data will end up in a single text file). --- Can't afford that BPO? Look here. 20:1 mineral compression The EVE f@h team |

Blazing Fire
Interstellar Operations Incorporated
|
Posted - 2008.11.18 14:06:00 -
[25]
Originally by: Kazuo Ishiguro I've done a fair bit of research on how much distance ships cover during warp - you accelerate exponentially for 10 seconds and slow down exponentially for 20 seconds. Thus a lower bound for each jump is 30 seconds (this is also enforced by the session change timer).
During this time, excluding any additional ground covered while at full warp, you typically cover a distance equivalent to travelling at full warp speed for 1.33 seconds.
Also, it's worth finding your average grid load time (leave the log window open while on autopilot for 10-20 jumps and all the relevant data will end up in a single text file).
Ok, I have done some testing. I have started to calculate the Path score in time needed for travel. I have included the time needed to warp from gate to gate, the session change time, the align time, the time to accelerate and decelerate to/from warp.
The results are: *Same routes for short distances(20 jumps) *A bit different routes for longer distances (+50 jumps) *4 to 10 more time needed to calcualte the route *4 to 10 times increase of the number of visited by the algorithm systems
Bottom line - It is not worth it.
|

Alvaliella
|
Posted - 2008.11.23 16:53:00 -
[26]
Edited by: Alvaliella on 23/11/2008 16:57:45 Thanks for all the replies & discussion- some points raised pre-emptively answer a few other questions that I was wondering about
I just wanted to give some "closure" to my original question in case anyone has similar concerns in the future.
After much research & musing I gave Dijkstra a try using the method described here.
When I first implemented this in Excel VBA, it took around 6.5 seconds to solve for a node (i.e. work out the shortest paths from the starting node to all other nodes in the system).
After code tweaking, pruning of the Eve data (e.g. removal of systems below a security threshold etc.) and optimising/preprocessing the remaining data, I got the solution time down to ~1.5 seconds for starting node to all others, and ~0.5 seconds for starting node to a particular destination node.
I'm pretty happy with this solution time and I don't think that A* would offer any major time savings (for this particular application)... I also like the fact that Dijkstra guarantees the shortest route so I know that my routes will match those of the autopilot.
My conclusion: Use Dijkstra over A* |

Ambo
State Protectorate
|
Posted - 2008.11.23 21:53:00 -
[27]
Edited by: Ambo on 23/11/2008 21:54:10
Originally by: Alvaliella Edited by: Alvaliella on 23/11/2008 16:57:45 Thanks for all the replies & discussion- some points raised pre-emptively answer a few other questions that I was wondering about
I just wanted to give some "closure" to my original question in case anyone has similar concerns in the future.
After much research & musing I gave Dijkstra a try using the method described here.
When I first implemented this in Excel VBA, it took around 6.5 seconds to solve for a node (i.e. work out the shortest paths from the starting node to all other nodes in the system).
After code tweaking, pruning of the Eve data (e.g. removal of systems below a security threshold etc.) and optimising/preprocessing the remaining data, I got the solution time down to ~1.5 seconds for starting node to all others, and ~0.5 seconds for starting node to a particular destination node.
I'm pretty happy with this solution time and I don't think that A* would offer any major time savings (for this particular application)... I also like the fact that Dijkstra guarantees the shortest route so I know that my routes will match those of the autopilot.
My conclusion: Use Dijkstra over A*
Interesting stuff.
I've been doing some investigation of my own. I've got a basic Dijkstra that solves for any node-node pretty fast. However, it's not always fast than my current solution, some example results were:
my solution - none-node 9 jumps - 0.16s my solution - none-node 60 jumps - 1.3s
Dijkstra - node-node 9 jumps - 0.28s Dijkstra - node-node 60 jumps - 9.8s
In other words, both solutions are fast enough for short routes but neither are really as fast as I want for longer routes.
I think that running Dijkstra for regions, then constellations, then systems rather than just systems right off the bat would result in better times for the longer routes but would surely slow up the shorted routes...
I've also not got around to fully implementing an A* algorithm yet.
Your removal of systems below a certain security level is not an option for me. I've also yet to introduce any preference system for high security systems or anything like that.
I'll be sure to keep you updated once I've done some more coding and run a few more tests.  --------------------------------------
Trader? Investor? Just want to track your finances? Check out EMMA |

Ambo
State Protectorate
|
Posted - 2008.11.25 16:21:00 -
[28]
Just thought I'd update this with my new findings.
Search routines I've tried:
My own (still not sure if it even has a proper name.. a recursive reverse search with the search space restricted to systems that are members of the best constellation route + neighbours - the constellation route having been calculated in the same manner from constellations in the best region route + neighbours. Essentially, this is equivalent to my best-first implementation) Dijkstra A* Best-first
Pre-processing, optimisations and restrictions:
1) Both my own method and Dijkstra require no pre-processing of the datadump tables. 2) A* and best-first can either use a distance-based heuristic (no pre-processing needed but slower) or a constellation jumps based heuristic (can either have no pre-processing and be slow or pre-generate a table with all routes between constellations and thier lengths (takes a few minutes and added about 20 Mb to the DB on my system) 3) My method is very tricky to ristrict in any way (i.e. prefer high-sec routes, etc). All the others are fairly easy to tweak with different path wieghts for different route requirements. 4) This one may sound obvious but optimise your algorithm. Generalised solutions will always perform worse so put timers in to measure what is slow, watch for places where you can cache and reuse data, etc. 5) My Dijkstra algorithm is highly optimised for unit path lenghts (i.e. all jumps having a 'cost' of 1).
Test Results Note - these are all from a slower machine that the previous results I posted. ;)
59 jumps - 9S-GPT in Outer Passage to XG-D1L in Cloud Ring (actually 50 jumps, see obsevations)
1st run 2nd run My algorithm 4.5 3.7 Dijkstra 5.9 2.6 A* (constellation jumps) 40.5 38.6 A* (actual distance) 4.2 3.3 Best first (constellation jumps) 2.5 1.1 Best first (actual distance) 3.0 2.1 (Result was a 104 jump route!)
6 jumps = Jita to Vuorrassi
1st run 2nd run My algorithm 1.4 0.4 Dijkstra 2.1 0.6 A* (constellation jumps) 3.5 1.1 A* (actual distance) 1.5 0.5 Best first (constellation jumps) 1.7 0.6 Best first (actual distance) 1.2 0.2 (Result was a 7 jump route)
Observations
Having got all I could out of Dijkstra and my own algorithm, I set about implementing A* using constellation jumps as the estimated distnace remaining to the destination. Clearly, this gave very disapointing performance because constellation jumps was simply not a good enough approximation of the actual number of jumps that would be required.
I tried using the actual distance between systems as a heuristic and was pleasantly suprised by the speed. However, I then noticed a problem. With the 59 jump route (from 9S-GPT in Outer Passage to XG-D1L in Cloud Ring), A* came up with a route of 50 jumps?! After some head-scratching and investigating, I found that the A* route was passing through regions that were excluded from my algorithm and Dijkstra because they were not either on or neighbouring the shortest regional route from Outer Passage to Cloud Ring (Outer Passage->Perrigen Falls->Venal->Tribute->Pure Blind->Cloud Ring). In fact, the real shortest route went through completely different regions (Outer Passage->The Spire->Etherium Reach->The Forge->Metropolis->Sinq Laison->Essence->Placid->Cloud Ring). --------------------------------------
Trader? Investor? Just want to track your finances? Check out EMMA |

Ambo
State Protectorate
|
Posted - 2008.11.25 16:22:00 -
[29]
Unsure how to combat this problem in Dijkstra and my own algorithm, I decided to try optimising A* as much as possible. I pretty soon realised that the A* using constellation jump heuristic would be far more effective if I choose the next possible node based solely on the number of constellation jumps remaining. Turns out my A* was now a best first search. I was frankly amazed by the performance of this algorithm on the longer routes, we might finally have a winner.
Alas, Best first using a constellation jump hueristic may have been very fast but also suffered from the same problem as Dijkstra and my original algorithm! I tried best first using the actual distance heuristic instead and the results seemed ok... until I looked at the route itself and found that it was significantly longer than it should be.
Thoughts
1) I remain convinced that there is a better solution. However, given the results I've had so far, I'd have to say the best algorithm is A* using the actual distance as the heuristic (actually distance / 10000000000000000). 2) Number of constellation/region jumps is clearly too unreliable as a heuristic. actual distance is also not great. Need to find somthing else...
--------------------------------------
Trader? Investor? Just want to track your finances? Check out EMMA |

Ki Tarra
Caldari Ki Tech Industries
|
Posted - 2008.11.25 17:51:00 -
[30]
Ambo, what enviorment are you running this in and what units are you measuring the runtimes in?
|
| |
|
| Pages: [1] 2 :: one page |
| First page | Previous page | Next page | Last page |