Realiseren
R1
Beschrijving
Ik kan software realiseren conform de requirements van de opdracht en met kwaliteitsstandaarden zoals ze gebruikt worden in software engineering of zoals ze gehanteerd worden binnen het bedrijf.
Bewijs volgens Stageplan
Screenshots van geschreven code die de goede verzorging van design patterns en/of documentatie aantonen die in lijn liggen met de eisen binnen het bedrijf.
Bewijs
Requirements van de opdracht
De verschillende requirements voor RiverScout staan beschreven bij Analyseren 2.
Om aan deze requirements te voldoen hebben we voornamelijk al aan het begin onze resultaten vergeleken met de huidige routeplanner. Daarnaast is alle data binnen RiverScout opengesteld door middel van een API, er kunnen dus gemakkelijk routes opgevraagd worden en informatie van een bepaald punt in de grafiek. Ook lag de focus op de snelheid en efficiëntie van de geschreven code om routes zo snel en goed mogelijk terug te geven.
Om de routeplanner ook in een echte omgeving te gebruiken heeft RiverScout de huidige routeplanner vervangen in de ontwikkelomgeving om zo routes op te kunnen vragen vanuit het platform van Teqplay zelf.
Kwaliteitsstandaarden van code
Aangezien er geen kwaliteitsstandaarden zijn voor Kotlin, gebruiken we de kwaliteitsstandaarden voor Java die ook op het JVM werkt en kan samenwerken met Kotlin. Door de verschillende mogelijkheden in Kotlin kan de gebruikte code soms afwijken van de gegeven regel voor Java.
Java - Teqplay's coding standards
Hieronder een voorbeeld van code uit het RiverScout project, inclusief commentaar over de gevolgde regels.
2
2.1 Er bevinden zich geen TAB karakters in de bestanden.
2.2 De file encoding is altijd UTF-8.3
3.1 Er is maar één top-level class.4
4.8 In het hele project wordt geen gebruik gemaakt van literals.5
5.1 Class/interface namen beginnen altijd met een hoofdletter en worden verder ge-camel-cased.
5.2 Instantie variabelen starten altijd met een kleine letter en worden verder ge-camel-cased.
5.3 Alle methodes starten altijd met een kleine letter en worden verder ge-camel-cased.\7
Alle niet-private, niet-triviale methoden moeten KotlinDocs hebben.8.1
Er wordt gewerkt met een development branch, voor nieuwe onderdelen wordt een nieuwe branch aangemaakt.
package com.riverscout.utils
/**
* Utilities for using with a Database
*
* @author Maurice
*/
object DatabaseUtils {
/**
* Appends different filters to each other
*/
fun appendFilters(filters: List<String?>): String? {
val output = filters.filterNotNull()
4
4.1 Braces worden altijd gebruikt voorif
,else
,for
,do
enwhile
statements.
4.2 Code is altijd indented met 4 spaties en de voortzetting is tweemaal zo veel (8).
return if (output.isEmpty()) {
null
} else {
output.joinToString(",")
}
}
4
4.4 Er zijn niet meer dan 120 karakters op één lijn (inclusief spaties).
4.5 Haakjes worden gebruikt om te groeperen (waar nodig)
/**
* Creates and filters for given options
*
* @return filter parameter
*/
fun createAndFilters(
options: Map<String, Any?>,
isString: (Any) -> Boolean = { it is String },
withCurlyBrackets: Boolean = false
): String? {
val filters: MutableList<Pair<String, Any>> = mutableListOf()
4
4.6 Alle control flow statements hebben haakjes op de regel waar ze starten en zijn omringd met spaties.
for (option in options) {
if (option.value != null) {
val value = option.value!!
filters.add(option.key to mapFilterValue(value, isString(value)))
}
}
return if (filters.isEmpty()) null else {
if (withCurlyBrackets) {
filters.joinToString(",") { "{ "${it.first}" : ${it.second} }" }
} else {
"{ ${filters.joinToString(",") { ""${it.first}" : ${it.second}" }} }"
}
}
}
/* ... */
/**
* Maps a value to it's appropriate Mongo representation
*/
private fun mapFilterValue(option: Any, asString: Boolean): Any {
4
4.7 Alle statements in eenwhen
worden beëindigd.
return when {
asString -> "\"$option\""
option is List<*> -> "[${option.filterNotNull().map {
mapFilterValue(it, it is String)
}.joinToString(",")}]"
else -> option
}
}
}
4
4.3 Er moet altijd één statement per line zijn en elke statement is gevolgd door een line break.
4.9 Detry
,catch
enfinally
sleutelwoorden staan altijd op een nieuwe lijn. Excepties worden nooit doorgeslikt.5
5.6 We gebruiken geen members die alleenstatic
zijn.6
6.2 Excepties worden alleen geworpen, wanneer deze zich in een niet-herstelbare staat bevindt, en vang hem op wanneer deze zich in een staat bevindt om te herstellen of te stoppen en informeer de gebruiker. Uitzondering mag nooit deel uitmaken van een "normale" stroom.
var offset = 0
var totalCount = 1
val count = 500
val routes: MutableList<FISRoute> = mutableListOf()
try {
while (offset < totalCount) {
val apiResponse = URL("$baseURL/$geoGeneration/$extensionURL?count=$count&offset=$offset").readText()
val json = fillSource(jsonGet(apiResponse))
routes.addAll(json.sourceList)
totalCount = json.sourceTotalCount
offset += count
}
} catch (error: Exception) {
logger.error("Can't connect to vaarweginformatie.nl")
error.printStackTrace()
}
5
5.4 Enum constantes zijn volledig in hoofdletters en woorden zijn gescheiden met underscores.
enum class GraphStoreType(
val source: (List<String>?) -> GraphObjectSource,
val apply: (Map<String, String>) -> GraphStore,
val function: GraphFunction
) {
FIS_NAVIGABILITY({ GraphObjectSource.from(FIS, FIS_NAVIGABILITY, it) }, { GraphStoreBaseType.CEMT_CLASS.apply(FIS_NAVIGABILITY, it) }, GraphFunction.DISTANCE_MARKER),
FIS_PLEASURE_CRAFT({ GraphObjectSource.from(FIS, FIS_PLEASURE_CRAFT, it) }, { GraphStoreBaseType.PLEASURE_CRAFT.apply(FIS_PLEASURE_CRAFT, it) }, GraphFunction.DISTANCE_MARKER),
FIS_MAXIMUM_DIMENSIONS({ GraphObjectSource.from(FIS, FIS_MAXIMUM_DIMENSIONS, it) }, { GraphStoreBaseType.MAXIMUM_DIMENSIONS.apply(FIS_MAXIMUM_DIMENSIONS, it) }, GraphFunction.DISTANCE_MARKER),
/* ... */
}
5
5.5static final
members zijn alleen met hoofdletters en woorden zijn gescheiden met underscores.
object GraphReader {
/* ... */
const val DIR = "materials/export/graph"
const val DIR_SIMPLIFIED = "$DIR/simplified"
/* ... */
}
6
6.1 Bij het overschrijven van methodes van een super class, gebruik altijdoverride
6.2 Bij het overschrijven van eenobject
'sequals(object)
ofhashCode()
, schrijf altijd de andere over en doe dat op basis van de gebruikte attributen.
override fun toString(): String {
return GraphReader.concatenate(getInfo().map { it.second })
}
override fun getInfo(): List<Pair<String, String>> {
return listOf(
"generalDepth" to "$generalDepth",
"generalLength" to "$generalLength",
"generalWidth" to "$generalWidth"
)
}
override operator fun equals(other: Any?): Boolean {
return if (other is GraphNote) {
toString() == other.toString()
} else {
false
}
}
8
8.2 Alle (kleine) niet-private methodes moeten unit-tests hebben. Elke classX
heeft zijn eigen unit-test class,XTest
in dezelfde package (ondersrc/test
).Notitie: hierbij wordt ook aangegeven om van een bepaalde benamingsstrategie gebruik te maken, echter wordt hier in de praktijk van afgeweken. Hierdoor wijken deze benamingen bij de tests van RiverScout ook af.
/**
* Tests for [CSVReader]
*
* @author Maurice
*/
class CSVReaderTest {
private val file = "../core/src/test/resources/csv-test.csv"
/**
* Tests if a CSV can be properly read
*/
@Test
fun testRead() {
val input = CSVReader.read(file, ";")
val output = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9")
)
assertEquals(output, input)
}
}
Feedback
De kwaliteit van het project ten opzichte van de code standaarden is op een niveau dat hoger dan de verwachting was. Mede door de code reviews is er rekening gehouden met de code standaarden van Teqplay.
Ten slotte is het systeem zo ontwikkeld, dat deze in productie gebruikt wordt.
Reflectie
Ik heb heel erg hard gewerkt om het project zo succesvol mogelijk te maken en zo'n hoog mogelijke kwaliteit te kunnen leveren qua werking en code. Ik ben erg blij dat dit opgeleverd heeft dat de routeplanner daadwerkelijk gebruikt wordt in productie.
R2
Beschrijving
Ik maak gebruik van testen en testautomatisering.
Bewijs volgens Stageplan
Screenshots van geschreven unit- en integratietests en een verslag van de uitgevoerde systeemtesten.
Bewijs
Beschrijving
Voor alle nuttige onderdelen in het RiverScout project zijn testen geschreven. Hieronder zijn een aantal voorbeelden gegeven van unit- en integratietests.
Unit-tests
Graph Tests
Hier worden de verschillende onderdelen van een grafiek getest. Om dit te kunnen doen is er een grafiek gemaakt die dient als voorbeeld.De verschillende tests die uitgevoerd worden hebben te maken met het krijgen van een lege grafiek, het verwijderen van dubbele objecten en lijnen, het verbeteren, afronden en simplificeren van de grafiek.
package com.riverscout.models.graph.type
import com.riverscout.GraphResource
import com.riverscout.graph.GraphProcessor
import com.riverscout.models.graph.entity.GraphConnection
import com.riverscout.models.graph.entity.GraphObject
import com.riverscout.models.graph.misc.GraphFunction
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Tests for [Graph]
*
* @author Maurice
*/
internal class GraphTest {
@Before
fun before() {
GraphProcessor.open()
}
@After
fun after() {
GraphProcessor.close()
}
/**
* JUNCTIONS: 0, 4, 8
* 4
* |
* 3
* |
* 2 -- 1 -- 0 -- 5 -- 6
* | | /
* | 7 /
* 9 | /
* |_ 10 --- 8 /
*/
private val lat = 52.2
private val lon = 5.0
private val d = 0.02
private val objects = listOf(
GraphObject(0, lat, lon, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(1, lat - d, lon, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(2, lat - d * 2, lon, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(3, lat, lon - d, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(4, lat, lon - d * 2, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(5, lat + d, lon, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(6, lat + d * 2, lon, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(7, lat, lon + d, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(8, lat, lon + d * 2, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(9, lat - d * 2, lon + d * 1.5, GraphFunction.DISTANCE_MARKER, 0),
GraphObject(10, lat - d * 1.5, lon + d * 2, GraphFunction.DISTANCE_MARKER, 0)
)
private val lines = listOf(
GraphConnection(objects[0], objects[1]),
GraphConnection(objects[0], objects[3]),
GraphConnection(objects[0], objects[5]),
GraphConnection(objects[0], objects[7]),
GraphConnection(objects[1], objects[2]),
GraphConnection(objects[3], objects[4]),
GraphConnection(objects[5], objects[6]),
GraphConnection(objects[7], objects[8]),
GraphConnection(objects[6], objects[8]),
GraphConnection(objects[2], objects[9]),
GraphConnection(objects[9], objects[10]),
GraphConnection(objects[10], objects[8])
)
/**
* Get an empty graph
*/
@Test
fun testEmptyGraph() {
val graph = Graph.empty()
assertTrue(graph.objects.isEmpty() && graph.lines.isEmpty() && graph.connections.isEmpty())
}
/**
* Objects with the same nodeId (duplicates) should be removed
*/
@Test
fun removeDuplicateObjects() {
val objectsWithDuplicates: MutableList<GraphObject> = mutableListOf()
objectsWithDuplicates.addAll(GraphResource.objects)
objectsWithDuplicates.addAll(listOf(
GraphObject(1, 52.42907, 5.41641, GraphFunction.JUNCTION, 0),
GraphObject(2, 52.42907, 5.41641, GraphFunction.JUNCTION, 0)
))
val graph = Graph(objectsWithDuplicates, emptyList(), emptyList())
assertEquals(GraphResource.objects.toString(), graph.objects.toString(), "\n${GraphResource.objects}\n${graph.objects}\n")
}
/**
* Lines with the same connecting objects (order does not matter) should be removed
*/
@Test
fun removeDuplicateLines() {
val linesWithDuplicates: MutableList<GraphConnection> = mutableListOf()
linesWithDuplicates.addAll(GraphResource.lines)
linesWithDuplicates.addAll(listOf(
GraphConnection(GraphResource.objects[0], GraphResource.objects[1]),
GraphConnection(GraphResource.objects[2], GraphResource.objects[1]),
GraphConnection(GraphResource.objects[1], GraphResource.objects[0])
))
val graph = Graph(GraphResource.objects, linesWithDuplicates, emptyList())
assertEquals(GraphResource.lines.toString(), graph.lines.toString())
}
/**
* An improved [Graph] should not contain connections if they can be replaced by lines or are invalid
*/
@Test
fun testImprove() {
val newObject =
GraphObject(4, 52.42910, 5.41641, GraphFunction.JUNCTION, 0)
val graph = Graph(listOf(
GraphResource.objects[0],
GraphResource.objects[1],
newObject
), listOf(
GraphConnection(GraphResource.objects[0], newObject),
GraphConnection(GraphResource.objects[1], newObject)
), listOf(
GraphConnection(newObject, newObject)
))
val mergedAndImprovedGraph = graph.improve()
assertEquals("3,2,0", "${mergedAndImprovedGraph.objects.size},${mergedAndImprovedGraph.lines.size},${mergedAndImprovedGraph.connections.size}")
}
/**
* A [Graph] should be returned where the [GraphFunction.JUNCTION] and [GraphFunction.DISTANCE_MARKER]
* are correctly distributed
*/
@Test
fun testFinish() {
val graph = Graph(objects, lines, emptyList()).finish()
val list = listOf(0, 4, 8)
for (obj in graph.objects) {
var b = obj.geoFunction == GraphFunction.DISTANCE_MARKER
if (obj.nodeNumber in list) {
b = !b
}
assertTrue(b)
}
assertEquals(11, graph.objects.size)
assertEquals(12, graph.lines.size)
}
/**
* All excess lines should be removed and only the important [GraphFunction.JUNCTION] and lines should stay
*/
@Test
fun testSimplify() {
val graph = Graph(objects, lines, emptyList()).finish().simplify().graph
assertEquals(3, graph.objects.size)
assertEquals(2, graph.lines.size)
}
}
Graph Route Merging Tests
Hierin worden alle verschillende scenario's getest voor het samenvoegen van bronnen om zo routes te genereren.
Het algoritme om routes te combineren is erg complex en heeft een hoop scenario's die getest moeten worden, om deze reden is niet het volledige bestand te zien.
Om te controleren of het algoritme routes goed kan combineren zijn er verschillende routes gemaakt die op telkens andere manieren met elkaar verbonden kunnen worden. Echter moeten deze routes altijd op dezelfde manier verbonden zijn als ze als uitkomst uit het algoritme komen. Om deze uitkomst te kunnen controleren is voor elke mogelijkheid een bepaalde volgorde van objecten en lijnen vastgesteld.
package com.riverscout.logic
import com.riverscout.graph.GraphProcessor
import com.riverscout.models.graph.CustomGraphRoute
import com.riverscout.models.graph.GraphRoute
import com.riverscout.models.graph.store.GraphStore
import com.riverscout.models.graph.store.GraphStoreType
import com.riverscout.models.graph.store.type.TestGraphStore
import com.riverscout.models.graph.type.Graph
import com.riverscout.models.route.Location
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertTrue
/**
* Tests for [GraphRouteMergingLogic]
*
* Tested attributes:
* - route sources
* - additional route sources
* - layered route sources
* - routes with distance between them
* - reversed routes
* - routes combined with route points
* - zippable routes
*
* @author Maurice
*/
class GraphRouteMergingLogicTest {
@Before
fun before() {
GraphProcessor.open()
}
@After
fun after() {
GraphProcessor.close()
}
fun GraphRouteMock(
locations: List<Location>
) = CustomGraphRoute(locations, 0)
private val fullRouteLocations = listOf(
Location(51.9, 4.3),
Location(51.91, 4.35),
Location(51.9, 4.4),
Location(51.91, 4.45),
Location(51.9, 4.5),
Location(51.91, 4.55),
Location(51.9, 4.6)
)
private val segment1 = listOf(
Location(51.9, 4.3),
Location(51.9, 4.315),
Location(51.91, 4.335),
Location(51.91, 4.35)
)
private val segment2 = listOf(
Location(51.9, 4.4),
Location(51.905, 4.43),
Location(51.91, 4.44),
Location(51.91, 4.46),
Location(51.905, 4.47),
Location(51.9, 4.5)
)
private val layer = listOf(
Location(51.91, 4.45),
Location(51.92, 4.44),
Location(51.93, 4.46),
Location(51.94, 4.45)
)
private val fullRoute = GraphRouteMock({ fullRouteLocations }())
private val fullRouteSplit1 =
GraphRouteMock({ fullRouteLocations.subList(0, 3) }())
private val fullRouteSplitBetween =
GraphRouteMock({ fullRouteLocations.subList(3, 4) }())
private val fullRouteSplit2 =
GraphRouteMock({ fullRouteLocations.subList(4, fullRouteLocations.size) }())
private val fullRouteReversed = GraphRouteMock({ fullRoute.locations.reversed() }())
private val fullRouteSplit1Reversed = GraphRouteMock({ fullRouteSplit1.locations.reversed() }())
private val fullRouteSplit2Reversed = GraphRouteMock({ fullRouteSplit2.locations.reversed() }())
private val additionWithConnectedSegments1 =
GraphRouteMock({ segment1 }())
private val additionWithPartialFirstConnectedSegments1 =
GraphRouteMock({ segment1.subList(0, segment1.size - 1) }())
private val additionWithPartialLastConnectedSegments1 =
GraphRouteMock({ segment1.subList(1, segment1.size) }())
private val additionWithoutConnectedSegments1 =
GraphRouteMock({ segment1.subList(1, segment1.size - 1) }())
private val additionWithConnectedSegments2 =
GraphRouteMock({ segment2 }())
private val additionWithPartialFirstConnectedSegments2 =
GraphRouteMock({ segment2.subList(0, segment2.size - 1) }())
private val additionWithPartialLastConnectedSegments2 =
GraphRouteMock({ segment2.subList(1, segment2.size) }())
private val additionWithoutConnectedSegments2 =
GraphRouteMock({ segment2.subList(1, segment2.size - 1) }())
private val additionWithoutConnectedSegments2Reversed =
GraphRouteMock({ additionWithoutConnectedSegments2.locations.reversed() }())
private val additionWithoutConnectedSegments2Point =
GraphRouteMock({ listOf(additionWithoutConnectedSegments2.locations.first()) }())
private val layerRoute = GraphRouteMock(layer)
private val layerRouteWithDifferentRouteId = CustomGraphRoute(layer, 1)
private fun checkGraph(objectsString: String, linesString: String, graph: Graph) {
for (obj in graph.objects) {
for (store in obj.stores) {
store.type = GraphStoreType.EMPTY
}
}
val objects = graph.objects
val lines = graph.lines
val objectsCheckString = objects.asSequence().map { it -> it.toLocation().toCoords() }.joinToString(" & ")
val linesCheckString = lines.asSequence().map { it ->
"${it.locFrom.toCoords()}<>${it.locTo.toCoords()}"
}.joinToString(" & ")
val objectsEquals = objectsString == objectsCheckString
val linesEquals = linesString == linesCheckString
val linesStringSplit = linesString.split(" & ")
val linesCheckStringSplit = linesCheckString.split(" & ")
var linesCheckEquals = true
for (linesStringSplitItem in linesStringSplit) {
val test = linesCheckStringSplit.contains(linesStringSplitItem)
linesCheckEquals = linesCheckEquals && test
}
assertTrue(objectsEquals, "objects are not correct:\n\t$objectsString \n\t$objectsCheckString (expected)")
assertTrue(linesEquals, "lines are not correct:\n\t$linesString \n\t$linesCheckString (expected) \n\tresembles: $linesCheckEquals")
}
/*
FULL ROUTE
*/
@Test
fun testFullRouteWithoutAdditions() {
val routeSources =
listOf(
(GraphStoreType.EMPTY.getSourcePair() to { _: GraphRoute -> listOf(TestGraphStore(GraphStoreType.EMPTY) as GraphStore) }) to listOf(fullRoute)
)
val graph = GraphRouteMergingLogic.setupGraph(routeSources, emptyList(), emptyList(), emptyList())
val objectsString = "51.9,4.3 & 51.91,4.35 & 51.9,4.4 & 51.91,4.45 & 51.9,4.5 & 51.91,4.55 & 51.9,4.6"
val linesString =
"51.9,4.3<>51.91,4.35 & 51.91,4.35<>51.9,4.4 & 51.9,4.4<>51.91,4.45 & 51.91,4.45<>51.9,4.5 & 51.9,4.5<>51.91,4.55 & 51.91,4.55<>51.9,4.6"
checkGraph(objectsString, linesString, graph)
}
/* ... */
}
Integratietests
Graph Supplier Tests
Hierin wordt getest of alle gegevens van Rijkswaterstaat van de bron FIS (Fairway Information Service) bereikbaar zijn en of ze correct verwerkt worden binnen RiverScout
package com.riverscout.graph.source
import com.riverscout.models.fis.FISJsonCreator
import org.junit.Test
import kotlin.test.assertTrue
/**
* Tests for [GraphSupplier]
*
* @author Maurice
*/
class GraphSupplierTestIT {
/**
* Read information from the FIS
*/
@Test
fun testReadFIS() {
val fetches = listOf(
"navigability" to { FISJsonCreator.getNavigability() },
"pleasure craft" to { FISJsonCreator.getPleasureCraftWaterwayClassification() },
"maximum dimensions" to { FISJsonCreator.getMaximumDimensions() },
"harbours" to { FISJsonCreator.getHarbours() },
"trajectory" to { FISJsonCreator.getTrajectory() },
"fairway" to { FISJsonCreator.getFairway() },
"fairway connections" to { FISJsonCreator.getFairwayConnections() },
"bridges" to { FISJsonCreator.getBridgesWithOpenings() },
"locks" to { FISJsonCreator.getLocksWithChambers() }
)
for ((title, fetch) in fetches) {
try {
println("Fetching $title...")
val list = fetch()
assertTrue(list.isNotEmpty(), "List is empty for $title")
} catch (e: Exception) {
assertTrue(false, e.toString())
}
}
}
}
Systeemtesten
Het systeem, hier RiverScout, moet voldoen aan alle genoemde requirements bij Analyseren 2.
Het aanmaken van de grafiek door middel van bronnen van onder andere Rijkswaterstaat
Het aanmaken van grafieken gebruikt meerdere bronnen, zoals genoemd bij samenvoegen van verschillende grafieken van verschillende versies en bronnen.
Één van de bronnen die gebruikt kan worden bij het aanmaken van de grafieken is de FIS, het Fairway Information Service van Rijkswaterstaat.
De volledige grafiek beschikbaar stellen via een API
De volledige grafiek is beschikbaar gesteld via de API. Alle gegevens van de grafieken kunnen opgevraagd worden: alle objecten en lijnen die voorkomen in een grafiek, de beheerinformatie van de grafiek en alle routes en informatie van de grafiek.
Het beschikbaar stellen van het opvragen van routes door middel van een API
Dit is de belangrijkste voor RiverScout, omdat dit het daadwerkelijk een routeplanner maakt. Routes kunnen worden opgevraagd via de API inclusief een aantal verschillende parameters.
De aanvraag voor routes snel afhandelen en routes teruggeven, rekening houdend met bijvoorbeeld de dimensies van een schip
Voor het snel kunnen afhandelen en teruggeven van routes is er veel gelet op de performance van de routeplanner. De interactie tussen de verschillende modules en het zo optimaal mogelijk gebruik maken van het geheugen is hier een verreiste. Om aan deze requirement te voldoen is veel tijd besteed aan het optimaliseren van de routeplanner in z'n geheel.
Een gedeelte van de optimalisatie zat ook in het rekening houden met bijvoorbeeld de dimensies van een schip. De gegevens van een schip meenemen en vervolgens alle routes controleren met deze gegevens, duurde in eerste instantie vrij lang. Door een aantal gegevens over verschillende routes te cachen en van te voren al te weten welke gegevens het schip aan moet voldoen, versnelt dit proces enorm. Het controleren of een schip door verschillende routes kan varen duurt dan ook maar enkele milliseconden.
Soortgelijke (of betere) resultaten krijgen in vergelijking met de huidige routeplanner
Voor deze requirement moest de RiverScout routeplanner vergeleken worden met de huidige routeplanner waar Teqplay gebruik van maakt. Bij het vergelijken van de resultaten van de routeplanners merkten we al in een vroeg stadium dat we meer routes terug konden geven. Na het werken aan de efficiëntie van RiverScout konden we zelfs sneller en meer resultaten teruggeven aan de gebruiker.
De RiverScout routeplanner geeft altijd soortgelijke en meestal betere resultaten in vergelijking met de huidige routeplanner.
Het openstellen van alle routes in de grafiek
Alle routes van de grafieken van RiverScout zijn opengesteld via de API. Hierdoor kan onder andere de frontend van RiverScout hier gebruik van maken om alle routes weer te geven.
Het ingeven van zelfgemaakte routes
Voor het ingeven van zelfgemaakte routes moet een aanvraag verstuurd worden naar de API met een lijst aan gegevens. Er zijn meerdere manieren voor het aanmaken van routes, de makkelijkste hier is het meegeven van een lijst van coördinaten. Deze lijst van coördinaten is ook wat de frontend van RiverScout stuurt naar de API om routes aan te maken.
Het aanpassen van routes
Het aanpassen van routes wordt ook mogelijk gemaakt door de API, het aanpassen van routes kan op twee manieren:
- Het ingeven van zelfgemaakte routes
- Het aanpassen van routes door objecten toe te voegen, verplaatsen of verwijderen
Het kunnen testen van zojuist aangemaakte routes
Om aangemaakte routes te testen moeten deze routes opgeslagen zijn in een sandbox. Om deze routes in een sandbox te krijgen moet als eerste de grafiek opnieuw gegenereerd worden zodat de nieuwe routes in de grafiek worden opgenomen. Daarna moet de sandbox waarop de grafiek staat aangesloten geupdate worden zodat alle gegevens in de sandbox ook worden geupdate met de nieuwe gegevens uit de grafiek. Na het update van de sandbox zijn alle nieuwe gegevens ook hier beschikbaar en kan de gebruiker de routes testen.
Testresultaat
Hieronder staan screenshots van de hiervoor genoemde tests waarin terugkomt dat alle unit-tests en integratietests succesvol zijn uitgevoerd.
Unit-tests
Alle operaties op een grafiek succesvol uitgevoerd
Alle mogelijike combinaties van het samenvoegen van routes succesvol uitgevoerd
Integratietests
Het ophalen van de FIS van Rijkswaterstaat succesvol uitgevoerd
Conclusie
RiverScout, zoals genoemd bij Analyseren 4, voldoet aan de opgestelde functionaliteiten, heeft een robuuste werking, is gemakkelijk te gebruiken voor het converteren bij huidige systemen, heeft weinig externe koppelingen, heeft een erg goede performance en is goed gedocumenteerd.
RiverScout heeft aan alle tests voldaan en ook zijn alle verdere punten van de product owner verwerkt in het product, draait de applicatie al een langere tijd stabiel op de studentdemo server en is deze aangesloten op de development server van Teqplay.
De RiverScout routeplanner is dus geaccepteerd voor de opgestelde acceptatietest.
Feedback
De applicatie draait stabiel en is goed getest, hiervoor heb ik veel tijd besteed aan het toevoegen van tests, kijken naar geheugengebruik en het laten goed draaien op de studentdemo server.
Ten slotte is het systeem zo ontwikkeld dat deze in productie gebruikt wordt.
Reflectie
Ik heb ook veel tijd besteed aan het testen van de routeplanner. Het hebben van een zo'n hoog mogelijke dekking van de code zorgt er namelijk voor dat je zeker bent dat de code nog werkt na een bepaalde verandering. Dit zorgt er ook voor dat ik meer vertrouwen heb bij het deployen van een nieuwe versie. Door het intensief testen van het project zijn er na de eerste keer deployen geen fouten meer voorgekomen.