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 Feb 3, 2024
1 parent b9b3736 commit 83e65b8
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 46 deletions.
6 changes: 5 additions & 1 deletion server/src/main/scala/kpn/api/common/data/WayMember.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ case class WayMember(way: Way, role: Option[String]) extends Member {

def hasRoleBackward: Boolean = role.contains("backward")

def isUnidirectional: Boolean = hasRoleForward || hasRoleBackward
def isRoundabout: Boolean = way.tags.has("junction", "roundabout")

def isUnidirectional: Boolean = {
hasRoleForward || hasRoleBackward
}

def startNode: Node = {
if (hasRoleBackward) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
trace(s"way ${contextCurrentWayMember.way.id} role=${contextCurrentWayMember.role}")
}

if (contextCurrentWayMember.way.tags.has("junction", "roundabout")) {
processRoundabout()
if (isClosedLoop(contextCurrentWayMember)) {
processClosedLoop()
}
else if (contextCurrentWayMember.isUnidirectional) {
else if (contextCurrentWayMember.isUnidirectional || contextCurrentWayMember.isRoundabout /* TODO different for hiking routes? */ ) {
processUnidirectionalWay()
}
else {
Expand Down Expand Up @@ -117,14 +117,14 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
}
}

private def processRoundabout(): Unit = {
private def processClosedLoop(): Unit = {
val calculatedStartNodeId = lastForwardFragment.map(_.endNodeId)

calculatedStartNodeId match {
case None =>
contextNextWayMember match {
case None =>
// The roundabout is the first fragment of the current element and there is no next element
// The closed loop is the first fragment of the current element and there is no next element

val nodeIds = contextCurrentWayMember.way.nodes.map(_.id)

Expand All @@ -140,11 +140,11 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean

case Some(nextWayMember) =>

// The roundabout is the first fragment of the current element
// The closed loop is the first fragment of the current element

val connectingNodeIds1 = contextCurrentWayMember.way.nodes.map(_.id)

val connectingNodeIds2 = if (nextWayMember.way.tags.has("junction", "roundabout")) {
val connectingNodeIds2 = if (isClosedLoop(nextWayMember)) {
nextWayMember.way.nodes.map(_.id)
}
else {
Expand Down Expand Up @@ -173,7 +173,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
case None =>

if (traceEnabled) {
trace(s" current roundabout way does not connect to next way")
trace(s" current closed loop way does not connect to next way")
}

finalizeCurrentElement()
Expand All @@ -194,7 +194,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
finalizeCurrentElement()
val wayNodeIds = contextCurrentWayMember.way.nodes.map(_.id)
// TODO for walking routes, should choose shortest path here instead of adding both forward and backward element
StructureUtil.roundaboutNodeIds(wayNodeIds.head, nodeId, wayNodeIds) match {
StructureUtil.closedLoopNodeIds(wayNodeIds.head, nodeId, wayNodeIds) match {
case None => throw new Exception("internal error TODO better message")
case Some(nodeIds) =>
val forwardFragment = StructureFragment(contextCurrentWayMember.way, nodeIds)
Expand All @@ -203,7 +203,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
elements.addOne(element)
}

StructureUtil.roundaboutNodeIds(nodeId, wayNodeIds.head, wayNodeIds) match {
StructureUtil.closedLoopNodeIds(nodeId, wayNodeIds.head, wayNodeIds) match {
case None => throw new Exception("internal error TODO better message")
case Some(nodeIds) =>
val backwardFragment = StructureFragment(contextCurrentWayMember.way, nodeIds)
Expand All @@ -220,7 +220,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
case None =>

val wayNodeIds = contextCurrentWayMember.way.nodes.map(_.id)
StructureUtil.roundaboutNodeIds(startNodeId, wayNodeIds) match {
StructureUtil.closedLoopNodeIds(startNodeId, wayNodeIds) match {
case None =>
throw new Exception("internal error TODO better message")
case Some(nodeIds) =>
Expand All @@ -241,7 +241,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean

val connectingNodeIds1 = contextCurrentWayMember.way.nodes.map(_.id)

val connectingNodeIds2 = if (nextWayMember.way.tags.has("junction", "roundabout")) {
val connectingNodeIds2 = if (isClosedLoop(nextWayMember)) {
nextWayMember.way.nodes.map(_.id)
}
else {
Expand Down Expand Up @@ -269,10 +269,10 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
connectingNodeId match {
case None =>
if (traceEnabled) {
trace(s" current roundabout way does not connect to next way")
trace(s" current closed loop way does not connect to next way")
}
val wayNodeIds = contextCurrentWayMember.way.nodes.map(_.id)
StructureUtil.roundaboutNodeIds(startNodeId, wayNodeIds) match {
StructureUtil.closedLoopNodeIds(startNodeId, wayNodeIds) match {
case None => throw new Exception("internal error TODO better message")
case Some(nodeIds) =>
val fragment = StructureFragment(contextCurrentWayMember.way, nodeIds)
Expand All @@ -287,7 +287,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
finalizeCurrentElement()
val wayNodeIds = contextCurrentWayMember.way.nodes.map(_.id)
// TODO for walking routes, should choose shortest path here instead of adding both forward and backward element
StructureUtil.roundaboutNodeIds(startNodeId, nodeId, wayNodeIds) match {
StructureUtil.closedLoopNodeIds(startNodeId, nodeId, wayNodeIds) match {
case None => throw new Exception("internal error TODO better message")
case Some(nodeIds) =>
val forwardFragment = StructureFragment(contextCurrentWayMember.way, nodeIds)
Expand All @@ -296,7 +296,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
elements.addOne(element)
}

StructureUtil.roundaboutNodeIds(nodeId, startNodeId, wayNodeIds) match {
StructureUtil.closedLoopNodeIds(nodeId, startNodeId, wayNodeIds) match {
case None => throw new Exception("internal error TODO better message")
case Some(nodeIds) =>
val backwardFragment = StructureFragment(contextCurrentWayMember.way, nodeIds)
Expand Down Expand Up @@ -325,7 +325,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
contextCurrentWayMember.way.nodes.head.id
)

val connectingNodeIds2 = if (nextWayMember.way.tags.has("junction", "roundabout")) {
val connectingNodeIds2 = if (isClosedLoop(nextWayMember)) {
nextWayMember.way.nodes.map(_.id)
}
else {
Expand Down Expand Up @@ -378,7 +378,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
}
}

private def addBidirectionalFragmentByLookingAheadAtRoundabout(nextWayMember: WayMember): Unit = {
private def addBidirectionalFragmentByLookingAheadAtClosedLoop(nextWayMember: WayMember): Unit = {

val connectingNodeIds1 = Seq(
contextCurrentWayMember.way.nodes.last.id,
Expand Down Expand Up @@ -505,7 +505,7 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean

// the very first way member in the route is unidirectional
// TODO does the element direction really depend on the role? or always 'Down'?
if (contextCurrentWayMember.hasRoleForward) {
if (contextCurrentWayMember.hasRoleForward || contextCurrentWayMember.isRoundabout) {
elementDirection = Some(ElementDirection.Down)
} else if (contextCurrentWayMember.hasRoleBackward) {
elementDirection = Some(ElementDirection.Up)
Expand Down Expand Up @@ -653,4 +653,9 @@ class StructureElementAnalyzer(wayMembers: Seq[WayMember], traceEnabled: Boolean
println(message)
}
}

private def isClosedLoop(wayMember: WayMember): Boolean = {
val way = wayMember.way
way.nodes.size > 2 && way.nodes.head == way.nodes.last
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package kpn.server.analyzer.engine.monitor.structure

object StructureUtil {

def roundaboutNodeIds(startNodeId: Long, nodeIds: Seq[Long]): Option[Seq[Long]] = {
def closedLoopNodeIds(startNodeId: Long, nodeIds: Seq[Long]): Option[Seq[Long]] = {
val reducedNodeIds = if (nodeIds.head == nodeIds.last) {
nodeIds.dropRight(1)
}
else {
throw new Exception("roundabout is expected to have identical start and end node")
throw new Exception("a closed loop is expected to have identical start and end node")
}

val startIndex = reducedNodeIds.indexOf(startNodeId)
Expand All @@ -19,12 +19,12 @@ object StructureUtil {
}
}

def roundaboutNodeIds(startNodeId: Long, endNodeId: Long, nodeIds: Seq[Long]): Option[Seq[Long]] = {
def closedLoopNodeIds(startNodeId: Long, endNodeId: Long, nodeIds: Seq[Long]): Option[Seq[Long]] = {
val reducedNodeIds = if (nodeIds.head == nodeIds.last) {
nodeIds.dropRight(1)
}
else {
throw new Exception("roundabout is expected to have identical start and end node")
throw new Exception("a closed loop is expected to have identical start and end node")
}
val startIndex = reducedNodeIds.indexOf(startNodeId)
if (startIndex >= 0) {
Expand All @@ -37,7 +37,7 @@ object StructureUtil {
Some(reducedNodeIds.drop(startIndex) ++ reducedNodeIds.take(endIndex + 1))
}
else {
roundaboutNodeIds(startNodeId, nodeIds)
closedLoopNodeIds(startNodeId, nodeIds)
}
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kpn.server.analyzer.engine.analysis.caseStudies.CaseStudy

class StructureAnalyzerTest extends UnitTest {

ignore("case study") {
test("case study") {
val relation = CaseStudy.load("/case-studies/monitor/4840541.xml")
println(s"relation.members.size=${relation.members.size}")
val elementGroups = StructureElementAnalyzer.analyze(relation.members)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import kpn.core.util.UnitTest

class StructureUtilTest extends UnitTest {

test("roundaboutNodeIds startNodeId") {
StructureUtil.roundaboutNodeIds(1, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(1, 2, 3, 4, 5, 1)))
StructureUtil.roundaboutNodeIds(2, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(2, 3, 4, 5, 1, 2)))
StructureUtil.roundaboutNodeIds(4, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(4, 5, 1, 2, 3, 4)))
StructureUtil.roundaboutNodeIds(5, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(5, 1, 2, 3, 4, 5)))
StructureUtil.roundaboutNodeIds(6, Seq(1, 2, 3, 4, 5, 1)) should equal(None)
test("closedLoopNodeIds startNodeId") {
StructureUtil.closedLoopNodeIds(1, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(1, 2, 3, 4, 5, 1)))
StructureUtil.closedLoopNodeIds(2, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(2, 3, 4, 5, 1, 2)))
StructureUtil.closedLoopNodeIds(4, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(4, 5, 1, 2, 3, 4)))
StructureUtil.closedLoopNodeIds(5, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(5, 1, 2, 3, 4, 5)))
StructureUtil.closedLoopNodeIds(6, Seq(1, 2, 3, 4, 5, 1)) should equal(None)
}

test("roundaboutNodeIds startNodeId endNodeId") {
StructureUtil.roundaboutNodeIds(1, 3, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(1, 2, 3)))
StructureUtil.roundaboutNodeIds(4, 2, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(4, 5, 1, 2)))
StructureUtil.roundaboutNodeIds(5, 1, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(5, 1)))
StructureUtil.roundaboutNodeIds(1, 6, Seq(1, 2, 3, 4, 5, 1)) should equal(None)
StructureUtil.roundaboutNodeIds(6, 1, Seq(1, 2, 3, 4, 5, 1)) should equal(None)
test("closedLoopNodeIds startNodeId endNodeId") {
StructureUtil.closedLoopNodeIds(1, 3, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(1, 2, 3)))
StructureUtil.closedLoopNodeIds(4, 2, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(4, 5, 1, 2)))
StructureUtil.closedLoopNodeIds(5, 1, Seq(1, 2, 3, 4, 5, 1)) should equal(Some(Seq(5, 1)))
StructureUtil.closedLoopNodeIds(1, 6, Seq(1, 2, 3, 4, 5, 1)) should equal(None)
StructureUtil.closedLoopNodeIds(6, 1, Seq(1, 2, 3, 4, 5, 1)) should equal(None)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class Structure_05_SingleWayRoundaboutNotALoopTest extends UnitTest {
Seq(
Seq(
"1>4 (Down)",
"4>1 (Up)",
)
)
)
Expand All @@ -39,13 +38,7 @@ class Structure_05_SingleWayRoundaboutNotALoopTest extends UnitTest {
nodeIds = Seq(1, 2, 3, 4)
)
),
backwardPath = Some(
TestStructurePath(
startNodeId = 4,
endNodeId = 1,
nodeIds = Seq(4, 3, 2, 1)
)
)
backwardPath = None
)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package kpn.server.analyzer.engine.monitor.structure

import kpn.api.custom.Tags
import kpn.core.util.UnitTest

class Structure_61_NonCircularRoundaboutTest extends UnitTest {

private def setup = new StructureTestSetupBuilder() {
memberWay(11, "", 1, 2, 3)
memberWayWithTags(12, "", Tags.from("junction" -> "roundabout"), 3, 4, 5)
memberWay(13, "", 5, 7, 8)
}.build

test("reference") {
setup.reference().shouldMatchTo(
Seq(
"1 p n ■ loop fp bp head tail d forward",
"2 p ■ n ■ loop fp bp head tail d roundabout_right",
"3 p ■ n loop fp bp head tail d forward"
)
)
}

test("elements") {
setup.elementGroups().shouldMatchTo(
Seq(
Seq(
"1>3",
"3>5 (Down)",
"5>8",
),
)
)
}

test("structure") {
pending
val structure = setup.structure()
structure.shouldMatchTo(
TestStructure(
forwardPath = Some(
TestStructurePath(
startNodeId = 1,
endNodeId = 8,
nodeIds = Seq(1, 2, 3, 4, 5, 7, 8)
)
),
backwardPath = Some(
TestStructurePath(
startNodeId = 8,
endNodeId = 1,
nodeIds = Seq(8, 7, 5)
)
)
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package kpn.server.analyzer.engine.monitor.structure

import kpn.api.custom.Tags
import kpn.core.util.UnitTest

class Structure_62_NonCircularRoundaboutTest extends UnitTest {

private def setup = new StructureTestSetupBuilder() {
memberWay(11, "", 1, 2, 3)
memberWayWithTags(12, "", Tags.from("junction" -> "roundabout"), 3, 4, 5)
memberWayWithTags(13, "", Tags.from("junction" -> "roundabout"), 5, 6, 3)
memberWay(14, "", 5, 7, 8)
}.build

test("reference") {
setup.reference().shouldMatchTo(
Seq(
"1 p n ■ loop fp bp head tail d forward",
"2 p ■ n ■ loop fp bp head tail d roundabout_right",
"3 p ■ n ■ loop fp bp head tail d roundabout_right",
"4 p ■ n loop fp bp head tail d forward"
)
)
}

test("elements") {
pending
setup.elementGroups().shouldMatchTo(
Seq(
Seq(
"1>3",
"3>5 (Down)",
"5>3 (Up)",
"5>8",
),
)
)
}

test("structure") {
pending
val structure = setup.structure()
structure.shouldMatchTo(
TestStructure(
forwardPath = Some(
TestStructurePath(
startNodeId = 1,
endNodeId = 8,
nodeIds = Seq(1, 2, 3, 4, 5, 7, 8)
)
),
backwardPath = Some(
TestStructurePath(
startNodeId = 8,
endNodeId = 1,
nodeIds = Seq(8, 7, 5, 6, 3, 2, 1)
)
)
)
)
}
}

0 comments on commit 83e65b8

Please sign in to comment.