Skip to content

Commit

Permalink
#278 monitor route analysis - wip
Browse files Browse the repository at this point in the history
  • Loading branch information
vmarc committed Jan 10, 2024
1 parent ecaf63b commit f4cec17
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ package kpn.server.analyzer.engine.monitor
object MonitorRouteElement {
def from(fragments: Seq[MonitorRouteFragment]): MonitorRouteElement = {
val oneWay = fragments.exists(_.oneWay)
MonitorRouteElement(fragments, oneWay)
MonitorRouteElement(0, fragments, oneWay)
}
}

case class MonitorRouteElement(fragments: Seq[MonitorRouteFragment], oneWay: Boolean) {
case class MonitorRouteElement(id: Long, fragments: Seq[MonitorRouteFragment], oneWay: Boolean) {

def startNodeId: Long = {
fragments.head.startNode.id
}

def endNodeId: Long = {
fragments.last.endNode.id
}

def string: String = {
val startNodeId = fragments.head.startNode.id
val endNodeIds = fragments.map(_.endNode.id)
val nodeString = startNodeId.toString + endNodeIds.mkString(">", ">", "")
val oneWayString = if (oneWay) " (oneWay)" else ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package kpn.server.analyzer.engine.monitor

import kpn.api.common.data.Member
import kpn.api.common.data.RelationMember
import kpn.api.common.data.WayMember

import scala.collection.mutable

class MonitorRouteElementAnalyzer {

private val connectionAnalyzer = new MonitorRouteConnectionAnalyzer
private val nextRouteAnalyzer = new MonitorRouteNextRouteAnalyzer

private var startWayMember: Option[WayMember] = None
private var processedStartWayMember = false

val elements = mutable.Buffer[MonitorRouteElement]()
val currentElementFragments = mutable.Buffer[MonitorRouteFragment]()
var loopFragments: Option[Seq[MonitorRouteFragment]] = None
var endNodeId: Long = 0

def analyzeRoute(members: Seq[Member]): Seq[MonitorRouteElement] = {
members.foreach { member =>
member match {
case wayMember: WayMember => processWayMember(wayMember)
case relationMember: RelationMember => processRelationMember(relationMember)
case _ => None
}
}

startWayMember match {
case None => // nothing more to do here
case Some(wayMember) =>
if (processedStartWayMember == false) {
currentElementFragments.addOne(MonitorRouteFragment.from(wayMember))
}
}

if (currentElementFragments.nonEmpty) {
elements.addOne(MonitorRouteElement.from(currentElementFragments.toSeq))
}
elements.toSeq
}

private def processRelationMember(relationMember: RelationMember): Unit = {
// still to be implemented
}

private def processWayMember(currentWayMember: WayMember): Unit = {
if (currentWayMember.way.nodes.length >= 2) {
if (!processedStartWayMember) {
startWayMember match {
case None =>
startWayMember = Some(currentWayMember)
case Some(wayMember) =>
processStart(wayMember, currentWayMember)
}
}
else {
processNextWayMember(currentWayMember)
}
}
}

private def processStart(wayMember1: WayMember, wayMember2: WayMember): Unit = {
val routeWays = connectionAnalyzer.analyze(wayMember1, wayMember2)
if (routeWays.isEmpty) {
// false start, the first 2 ways do not connect
// make way1 a separate segment
// make way2 the first way
elements.addOne(MonitorRouteElement.from(Seq(MonitorRouteFragment.from(wayMember1))))
startWayMember = Some(wayMember2)
}
else {
currentElementFragments.addAll(routeWays)
startWayMember = None
processedStartWayMember = true
endNodeId = routeWays.last.endNode.id
}
}

private def processNextWayMember(wayMember: WayMember): Unit = {
loopFragments match {
case Some(loopRouteWays) => processLoop(loopRouteWays, wayMember)
case None => processNextWayMemberAnalyze(wayMember)
}
}

private def processNextWayMemberAnalyze(wayMember: WayMember): Unit = {
nextRouteAnalyzer.analyze(endNodeId, wayMember) match {
case Some(routeWay) =>
endNodeId = routeWay.endNode.id

currentElementFragments.addOne(routeWay)

val startNodeIds = currentElementFragments.map(_.startNode.id)

if (startNodeIds.contains(endNodeId)) {
// loop, split currentSegment
val index = currentElementFragments.lastIndexWhere(_.startNode.id == endNodeId)
val segmentBeforeLoop = currentElementFragments.take(index)
loopFragments = Some(currentElementFragments.drop(index).toSeq)
elements.addOne(MonitorRouteElement.from(segmentBeforeLoop.toSeq))
currentElementFragments.clear()
}

case None =>
if (currentElementFragments.nonEmpty) {
elements.addOne(MonitorRouteElement.from(currentElementFragments.toSeq))
currentElementFragments.clear()
}
startWayMember = Some(wayMember)
processedStartWayMember = false
}
}

private def processLoop(loopRouteWays: Seq[MonitorRouteFragment], wayMember: WayMember): Unit = {
val startNodeId = wayMember.way.nodes.head.id
val endNodeId = wayMember.way.nodes.last.id

val index = loopRouteWays.indexWhere { routeWay =>
routeWay.endNode.id == startNodeId
}
if (index >= 0) {
val routeWays1 = loopRouteWays.take(index + 1)
val routeWays2 = loopRouteWays.drop(index + 1)
elements.addOne(MonitorRouteElement.from(routeWays1))
elements.addOne(MonitorRouteElement.from(routeWays2))
}
else {
elements.addOne(MonitorRouteElement.from(loopRouteWays))
}

startWayMember = Some(wayMember)
processedStartWayMember = false
}

private def debug(message: String): Unit = {
println(message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package kpn.server.analyzer.engine.monitor

import kpn.api.common.data.Member
import kpn.api.custom.Relation

import scala.collection.mutable

class MonitorRouteMemberAnalyzer {

// see: MonitorFilter.ignoredRoles Seq("place_of_worship", "guest_house", "outer", "inner")
private val rolesIgnore = Seq(
"outer",
"inner",
"place_of_worship",
"guest_house"
)

private val roles = Seq(
"alternative",
"excursion",
"approach",
"connection",
"variant",
"link",
)

def analyzeRoute(relation: Relation): Seq[MonitorRouteMemberGroup] = {

var currentMemberGroupOption: Option[MonitorRouteMemberGroup] = None
val mainMembers = mutable.Buffer[Member]()
val memberGroups = mutable.Buffer[MonitorRouteMemberGroup]()

relation.members.foreach { member =>
if (isRelevant(member)) {
alternativeRole(member) match {
case Some(role) =>
currentMemberGroupOption match {
case None =>
// first member with this role
currentMemberGroupOption = Some(MonitorRouteMemberGroup(Some(role), Seq(member)))

case Some(currentMemberGroup) =>
if (!currentMemberGroup.role.contains(role)) {
memberGroups.addOne(currentMemberGroup)
currentMemberGroupOption = Some(MonitorRouteMemberGroup(Some(role), Seq(member)))
}
currentMemberGroupOption = Some(
currentMemberGroup.copy(
members = currentMemberGroup.members :+ member
)
)
}

case None =>
currentMemberGroupOption match {
case None =>
case Some(currentMemberGroup) =>
memberGroups.addOne(currentMemberGroup)
currentMemberGroupOption = None
}
mainMembers.addOne(member)
}
}
}

currentMemberGroupOption match {
case None =>
case Some(currentMemberGroup) =>
memberGroups.addOne(currentMemberGroup)
currentMemberGroupOption = None
}
memberGroups.addOne(MonitorRouteMemberGroup(None, mainMembers.toSeq))
memberGroups.toSeq
}

private def isRelevant(member: Member): Boolean = {
if (member.isWay || member.isRelation) {
member.role match {
case Some(role) => !rolesIgnore.contains(role)
case None => true
}
}
else {
false
}
}

private def alternativeRole(member: Member): Option[String] = {
member.role match {
case Some(role) =>
if (roles.contains(role)) {
Some(role)
}
else {
None
}
case None => None
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kpn.server.analyzer.engine.monitor

import kpn.api.common.data.Member

case class MonitorRouteMemberGroup(role: Option[String], members: Seq[Member])
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package kpn.server.analyzer.engine.monitor

import kpn.core.util.UnitTest

class MonitorRouteElementAnalyzerTest extends UnitTest {

test("continous") {
analyze(
new MonitorRouteTestData() {
memberWay(10, "", 1, 2, 3)
memberWay(11, "", 3, 4, 5)
memberWay(12, "", 5, 6, 7)
}
).shouldMatchTo(
Seq(
"1>3>5>7"
)
)
}

test("continous - first way reversed") {
analyze(
new MonitorRouteTestData() {
memberWay(10, "", 3, 2, 1)
memberWay(11, "", 3, 4, 5)
memberWay(12, "", 5, 6, 7)
}
).shouldMatchTo(
Seq(
"1>3>5>7"
)
)
}

test("continous - second and third way reversed") {
analyze(
new MonitorRouteTestData() {
memberWay(10, "", 1, 2, 3)
memberWay(11, "", 5, 4, 3)
memberWay(12, "", 7, 6, 5)
}
).shouldMatchTo(
Seq(
"1>3>5>7"
)
)
}

test("2 gaps - 3 segments") {
analyze(
new MonitorRouteTestData() {
memberWay(10, "", 1, 2)
memberWay(11, "", 3, 4)
memberWay(12, "", 4, 5)
memberWay(13, "", 6, 7)
memberWay(14, "", 7, 8)
}
).shouldMatchTo(
Seq(
"1>2",
"3>4>5",
"6>7>8"
)
)
}

test("start way has forward role") {
analyze(
new MonitorRouteTestData() {
memberWay(10, "forward", 1, 2)
memberWay(11, "", 3, 2)
}
).shouldMatchTo(
Seq(
"1>2>3 (oneWay)"
)
)
}

test("start way has forward role, no connection to second way") {
analyze(
new MonitorRouteTestData() {
memberWay(10, "forward", 2, 1)
memberWay(11, "", 3, 2)
}
).shouldMatchTo(
Seq(
"2>1 (oneWay)",
"3>2"
)
)
}

test("forward") {
val result = analyze(
new MonitorRouteTestData() {
memberWay(10, "", 1, 2)
memberWay(11, "forward", 2, 3)
memberWay(12, "forward", 3, 8)
memberWay(13, "forward", 8, 7)
memberWay(14, "forward", 7, 2)
memberWay(15, "", 8, 9)
}
).shouldMatchTo(
Seq(
"1>2",
"2>3>8 (oneWay)",
"8>7>2 (oneWay)",
"8>9",
)
)
}

private def analyze(data: MonitorRouteTestData): Seq[String] = {
val relation = data.relation
val elements = new MonitorRouteElementAnalyzer().analyzeRoute(relation.members)
elements.map(_.string)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kpn.server.analyzer.engine.monitor

class MonitorRouteMemberAnalyzerTest {

}

0 comments on commit f4cec17

Please sign in to comment.