Skip to content

Commit

Permalink
router tests
Browse files Browse the repository at this point in the history
  • Loading branch information
antivoland committed Jul 23, 2023
1 parent 1188c5c commit 390e170
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 47 deletions.
21 changes: 20 additions & 1 deletion TECH.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ This project consists of a backend written in Java and a simple JavaScript front
* [Selectize.js](https://github.com/selectize/selectize.js)
* [sweetalert2](https://github.com/sweetalert2/sweetalert2)

## Video

I recorded the video demonstrating UI with a screen recorder and compressed using the following magic:

```
ffmpeg -i ui.720p.mov -f mp4 -vcodec libx264 -preset fast -profile:v main -acodec aac ui.mp4
```

# Test datasets

I used 4 airports for the sake of testing:
Expand Down Expand Up @@ -58,4 +66,15 @@ dst: buz
dst: null
src: null

ffmpeg -i ui.720p.mov -f mp4 -vcodec libx264 -preset fast -profile:v main -acodec aac output.mp4
ffmpeg -i ui.720p.mov -f mp4 -vcodec libx264 -preset fast -profile:v main -acodec aac ui.mp4


grep 'LYR' routes.dat --color=always

grep -E '^.*,[A-Za-z0-9]{3,4},.*,(LYR|ENSB),.*$' routes.dat

grep -E '^.*,(TLL|EETN),.*,(OSL|ENGM|TOS/ENTC),.*$' routes.dat


grep -E '^.*(TLL|EETN).*(OSL|TOS).*$' routes.dat
grep '^(.*?)[LYR](.*?)$' routes.dat --color=always
2 changes: 1 addition & 1 deletion src/main/java/antivoland/transporeon/api/RouteAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public RouteAPI(Router router) {
Route route(@RequestParam(value = "srcCode") String srcCode,
@RequestParam(value = "dstCode") String dstCode,
@RequestParam(value = "limited", defaultValue = "true") boolean limited) {
return router.findShortestRoute(Code.code(srcCode), Code.code(dstCode), limited);
return router.findShortestRoute(srcCode, dstCode, limited);
}

@ResponseStatus(NOT_FOUND)
Expand Down
22 changes: 17 additions & 5 deletions src/main/java/antivoland/transporeon/model/Router.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static java.lang.String.format;

@Component
public class Router {
Expand All @@ -21,11 +24,20 @@ public Router(World world) {
this.world = world;
}

public Route findShortestRoute(Code srcCode, Code dstCode) {
public Route findShortestRoute(String srcCode, String dstCode) {
return findShortestRoute(srcCode, dstCode, true);
}

public Route findShortestRoute(Code srcCode, Code dstCode, boolean limited) {
public Route findShortestRoute(String srcCode, String dstCode, boolean limited) {
return findShortestRoute(
Optional.ofNullable(Code.code(srcCode)).orElseThrow(() ->
new IllegalArgumentException(format("SRC code %s is invalid", srcCode))),
Optional.ofNullable(Code.code(dstCode)).orElseThrow(() ->
new IllegalArgumentException(format("DST code %s is invalid", dstCode))),
limited);
}

private Route findShortestRoute(Code srcCode, Code dstCode, boolean limited) {
if (srcCode == null) throw new IllegalArgumentException("SRC code is missing");
if (dstCode == null) throw new IllegalArgumentException("DST code is missing");
Spot src = world.spot(srcCode);
Expand All @@ -38,17 +50,17 @@ public Route findShortestRoute(Code srcCode, Code dstCode, boolean limited) {
private Route findShortestRoute(Spot src, Spot dst, boolean limited) {
AddressableHeap<Double, Route> heap = new FibonacciHeap<>();
Map<Stop, AddressableHeap.Handle<Double, Route>> seen = new HashMap<>();
seen.put(Stop.first(src.id), heap.insert(0d, new Route(Stop.first(src.id))));
seen.put(Stop.first(src.id), heap.insert(0d, new Route(src.id)));

while (!heap.isEmpty()) {
AddressableHeap.Handle<Double, Route> min = heap.deleteMin();
Route minRoute = min.getValue();
Stop minStop = minRoute.lastStop();
Stop minStop = minRoute.lastStop;
if (minStop.spotId == dst.id) return minRoute;
for (Move move : world.outgoingMoves(minStop.spotId)) {
if (!minRoute.canMove(move)) continue;
Route route = minRoute.move(move);
Stop stop = route.lastStop();
Stop stop = route.lastStop;
if (limited && route.numberOfFlights() > MAX_NUMBER_OF_FLIGHTS) continue;
AddressableHeap.Handle<Double, Route> stopHandle = seen.get(stop);
if (stopHandle == null) {
Expand Down
30 changes: 12 additions & 18 deletions src/main/java/antivoland/transporeon/model/route/Route.java
Original file line number Diff line number Diff line change
@@ -1,44 +1,38 @@
package antivoland.transporeon.model.route;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.ToString;

@ToString
public class Route {
public final Stop[] stops; // todo: just src and dst spots
public final Move[] moves;
public final double kmDistance;
@JsonIgnore
public final Stop lastStop;

public Route(Stop firstStop) {
this(new Stop[]{firstStop}, new Move[]{});
public Route(int srcId) {
this(new Move[0], Stop.first(srcId));
}

public Stop lastStop() {
return stops[stops.length - 1];
}

private Route(Stop[] stops, Move[] moves) {
this.stops = stops;
private Route(Move[] moves, Stop lastStop) {
this.moves = moves;
double kmDistance = 0;
for (Move move : moves) {
kmDistance += move.kmDistance;
}
this.kmDistance = kmDistance;
this.lastStop = lastStop;
}

public boolean canMove(Move move) {
return lastStop().type != StopType.ENTERED_BY_GROUND || move.type != MoveType.BY_GROUND;
return lastStop.type != StopType.ENTERED_BY_GROUND || move.type != MoveType.BY_GROUND;
}

public Route move(Move move) {
Stop[] newStops = new Stop[stops.length + 1];
System.arraycopy(stops, 0, newStops, 0, stops.length);
newStops[stops.length] = Stop.enteredBy(move);

Move[] newMoves = new Move[moves.length + 1];
System.arraycopy(moves, 0, newMoves, 0, moves.length);
newMoves[moves.length] = move;
return new Route(newStops, newMoves);
Move[] moves = new Move[this.moves.length + 1];
System.arraycopy(this.moves, 0, moves, 0, this.moves.length);
moves[this.moves.length] = move;
return new Route(moves, Stop.enteredBy(move));
}

public int numberOfFlights() {
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/antivoland/transporeon/model/CodeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package antivoland.transporeon.model;

import java.util.Arrays;
import java.util.Objects;
import java.util.Set;

import static java.util.stream.Collectors.toSet;
import static org.assertj.core.api.Assertions.assertThat;

// todo: implement
public class CodeTest {
public static Set<Code> codes(String... values) {
Set<Code> codes = Arrays
.stream(values)
.map(Code::code)
.filter(Objects::nonNull)
.collect(toSet());
assertThat(codes).hasSameSizeAs(values);
return codes;
}
}
143 changes: 134 additions & 9 deletions src/test/java/antivoland/transporeon/model/RouterTest.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,147 @@
package antivoland.transporeon.model;

import antivoland.transporeon.model.Code;
import antivoland.transporeon.model.Router;
import antivoland.transporeon.exception.RouteNotFoundException;
import antivoland.transporeon.model.route.MoveType;
import antivoland.transporeon.model.route.Route;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Disabled
import static antivoland.transporeon.model.CodeTest.codes;
import static antivoland.transporeon.model.DistanceCalculator.kmDistance;
import static org.assertj.core.api.Assertions.*;

@SpringBootTest
class RouterTest {
@Autowired
Router router;

@Test
void test() {
Route route = router.findShortestRoute(Code.code("TLL"), Code.code("LYR"));
System.out.println(route);
/*
Svalbard airport is the northernmost airport in the world. It is possible
to get there by air either from Oslo or from Tromsø:
grep -E '^.*,[A-Za-z0-9]{3,4},.*,(LYR|ENSB),.*$' routes.dat
DY,3737,OSL,644,LYR,658,,0,73H
SK,4319,OSL,644,LYR,658,,0,738 73W
SK,4319,TOS,663,LYR,658,,0,73H 738 73W
And I want to travel there from Tallinn, so the route will include either
Oslo or Tromsø:
grep -E '^.*,(TLL|EETN),.*,(OSL|ENGM|TOS|ENTC),.*$' routes.dat
DY,3737,TLL,415,OSL,644,,0,733 73H
OV,2218,TLL,415,OSL,644,,0,E70
SK,4319,TLL,415,OSL,644,Y,0,E70
There are direct flights from Tallinn to Oslo. Tromsø is located north of
Oslo and much closer to the end point of the route. And if we repeat the
flights exploration then we'll find the better route including a stop in
Stockholm Arlanda Airport: (TLL|EETN)->(ARN|ESSA)->(TOS|ENTC)->(LYR|ENSB)
*/
@ParameterizedTest
@CsvSource({"TLL,LYR", "TLL,ENSB", "EETN,LYR", "EETN,ENSB"})
void testLimitedTravelToSvalbard(String srcCode, String dstCode) {
Route route = router.findShortestRoute(srcCode, dstCode);
assertThat(route).isNotNull();
assertThat(route.moves).isNotNull().hasSize(3);

assertThat(route.moves[0].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[0].src.codes).isEqualTo(codes("TLL", "EETN"));
assertThat(route.moves[0].dst.codes).isEqualTo(codes("ARN", "ESSA"));

assertThat(route.moves[1].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[1].src.codes).isEqualTo(codes("ARN", "ESSA"));
assertThat(route.moves[1].dst.codes).isEqualTo(codes("TOS", "ENTC"));

assertThat(route.moves[2].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[2].src.codes).isEqualTo(codes("TOS", "ENTC"));
assertThat(route.moves[2].dst.codes).isEqualTo(codes("LYR", "ENSB"));

assertThat(router.findShortestRoute(srcCode, dstCode, false).kmDistance)
.isCloseTo(route.kmDistance, offset(0.01));
}

/*
Testing shortest routes is an uneasy activity. At the moment I have no
better idea than finding some interesting routes via the project's UI and
testing some particular stuff. At the moment I have no better idea than to
find some interesting routes using the project's UI and test some
particular things.
For instance, there is a route from Tallinn to Andrau Airpark, which is
not much longer than just the distance between these locations.
*/
@ParameterizedTest
@CsvSource({"TLL,AAP", "TLL,KAAP", "EETN,AAP", "EETN,KAAP"})
void testLimitedTravelToAndrauAirpark(String srcCode, String dstCode) {
Route route = router.findShortestRoute(srcCode, dstCode);
assertThat(route).isNotNull();
assertThat(route.moves).isNotNull().hasSize(5);

assertThat(route.moves[0].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[0].src.codes).isEqualTo(codes("TLL", "EETN"));
assertThat(route.moves[0].dst.codes).isEqualTo(codes("TRD", "ENVA"));

assertThat(route.moves[1].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[1].src.codes).isEqualTo(codes("TRD", "ENVA"));
assertThat(route.moves[1].dst.codes).isEqualTo(codes("KEF", "BIKF"));

assertThat(route.moves[2].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[2].src.codes).isEqualTo(codes("KEF", "BIKF"));
assertThat(route.moves[2].dst.codes).isEqualTo(codes("YYZ", "CYYZ"));

assertThat(route.moves[3].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[3].src.codes).isEqualTo(codes("YYZ", "CYYZ"));
assertThat(route.moves[3].dst.codes).isEqualTo(codes("IAH", "KIAH"));

assertThat(route.moves[4].type).isEqualTo(MoveType.BY_GROUND);
assertThat(route.moves[4].src.codes).isEqualTo(codes("IAH", "KIAH"));
assertThat(route.moves[4].dst.codes).isEqualTo(codes("AAP", "KAAP"));

double kmDistance = kmDistance(route.moves[0].src, route.moves[4].dst);
assertThat(route.kmDistance).isCloseTo(kmDistance, offset(60.0));

assertThat(router.findShortestRoute(srcCode, dstCode, false).kmDistance)
.isCloseTo(route.kmDistance, offset(0.01));
}

/*
Another even better example is an unlimited route from Tallinn to Augusta
Regional Airport, which is only 10 km longer than just the distance
between locations. And the limited route doesn't exist in this case.
*/
@ParameterizedTest
@CsvSource({"TLL,AGS", "TLL,KAGS", "EETN,AGS", "EETN,KAGS"})
void testUnlimitedTravelToAugustaRegionalAirport(String srcCode, String dstCode) {
Route route = router.findShortestRoute(srcCode, dstCode, false);
assertThat(route).isNotNull();
assertThat(route.moves).isNotNull().hasSize(5);

assertThat(route.moves[0].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[0].src.codes).isEqualTo(codes("TLL", "EETN"));
assertThat(route.moves[0].dst.codes).isEqualTo(codes("TRD", "ENVA"));

assertThat(route.moves[1].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[1].src.codes).isEqualTo(codes("TRD", "ENVA"));
assertThat(route.moves[1].dst.codes).isEqualTo(codes("KEF", "BIKF"));

assertThat(route.moves[2].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[2].src.codes).isEqualTo(codes("KEF", "BIKF"));
assertThat(route.moves[2].dst.codes).isEqualTo(codes("IAD", "KIAD"));

assertThat(route.moves[3].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[3].src.codes).isEqualTo(codes("IAD", "KIAD"));
assertThat(route.moves[3].dst.codes).isEqualTo(codes("CLT", "KCLT"));

assertThat(route.moves[4].type).isEqualTo(MoveType.BY_AIR);
assertThat(route.moves[4].src.codes).isEqualTo(codes("CLT", "KCLT"));
assertThat(route.moves[4].dst.codes).isEqualTo(codes("AGS", "KAGS"));

double kmDistance = kmDistance(route.moves[0].src, route.moves[4].dst);
assertThat(route.kmDistance).isCloseTo(kmDistance, offset(10.0));

assertThatThrownBy(() -> router.findShortestRoute(srcCode, dstCode))
.isInstanceOf(RouteNotFoundException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ void test() {

assertThat(segmentationBasedChangeIds.equals(bruteForceChangeIds))
.withFailMessage(() -> "Brute force and a segmentation-based approach produce different changes")
.isTrue(); // todo: few corner spots are not detected as changes
.isTrue();
}

static Set<ChangeId> detectChanges(Supplier<ChangeDetector> detector, Collection<Spot> spots) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package antivoland.transporeon.model.change;

import antivoland.transporeon.model.Code;
import antivoland.transporeon.model.Spot;
import antivoland.transporeon.model.change.ChangeDetectorTest.ChangeId;
import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import static antivoland.transporeon.model.CodeTest.codes;
import static antivoland.transporeon.model.change.ChangeDetectorTest.MAX_DISTANCE_KM;
import static java.util.stream.Collectors.toSet;
import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -46,14 +48,4 @@ void test() {
new ChangeId(4547, 4543),
new ChangeId(4547, 4544)));
}

static Set<Code> codes(String... values) {
Set<Code> codes = Arrays
.stream(values)
.map(Code::code)
.filter(Objects::nonNull)
.collect(toSet());
assertThat(codes).hasSameSizeAs(values);
return codes;
}
}

0 comments on commit 390e170

Please sign in to comment.