Skip to content

Commit

Permalink
#368 migrate node analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
vmarc committed Jul 16, 2024
1 parent 392fac2 commit e4aadae
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 196 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import kpn.server.analyzer.engine.analysis.route.analyzers.FixmeTodoRouteAnalyze
import kpn.server.analyzer.engine.analysis.route.analyzers.GeometryDigestAnalyzer
import kpn.server.analyzer.engine.analysis.route.analyzers.IncompleteOkRouteAnalyzer
import kpn.server.analyzer.engine.analysis.route.analyzers.IncompleteRouteAnalyzer
import kpn.server.analyzer.engine.analysis.route.analyzers.OldRouteNodeAnalyzer
import kpn.server.analyzer.engine.analysis.route.analyzers.OldRouteNodeTagAnalyzer
import kpn.server.analyzer.engine.analysis.route.analyzers.OldRouteStructureAnalyzer
import kpn.server.analyzer.engine.analysis.route.analyzers.OldRouteTileAnalyzer
Expand Down Expand Up @@ -84,7 +83,7 @@ class RouteDetailMainAnalyzer(
OldRouteNodeTagAnalyzer,
RouteNameAnalyzer,

OldRouteNodeAnalyzer,
//OldRouteNodeAnalyzer,
RouteNameFromNodesAnalyzer,
ExpectedNameRouteAnalyzer, // <== needs further updating
SuspiciousWaysRouteAnalyzer, // OK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ case class RouteNameAnalysis(
derivedFromDeprecatedNoteTag: Boolean = false
) {

def isStartNodeNameSameAsEndNodeName: Boolean = startNodeName.isDefined && endNodeName.isDefined && startNodeName == endNodeName
def isStartNodeNameSameAsEndNodeName: Boolean = {
startNodeName.isDefined && endNodeName.isDefined && startNodeName == endNodeName
}

def hasStandardNodeNames: Boolean = {
startNodeName match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ case class RouteNodeAnalysis(
) {
def nodes: Seq[RouteNodeData] = startNode.toSeq ++ endNode.toSeq ++ startTentacleNodes ++ endTentacleNodes

def nodeIds: Seq[Long] = nodes.map(_.node.id)
def nodeIds: Seq[Long] = nodes.map(_.nodeId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ class RouteNodeAnalysisFormatter(analysis: RouteNodeAnalysis) {

def nodeStrings: Seq[String] = {
List(
nodeStrings("Start", analysis.startNode.toSeq),
nodeStrings("End", analysis.endNode.toSeq),
nodeStrings("Start tentacle from", analysis.startTentacleNodes),
nodeStrings("End tentacle to", analysis.endTentacleNodes),
nodeStrings("Redundant", analysis.redundantNodes),
nodeStrings("start", analysis.startNode.toSeq),
nodeStrings("end", analysis.endNode.toSeq),
nodeStrings("start-tentacle", analysis.startTentacleNodes),
nodeStrings("end-tentacle", analysis.endTentacleNodes),
nodeStrings("redundant", analysis.redundantNodes),
).flatten
}

private def nodeStrings(title: String, routeNodeDatas: Seq[RouteNodeData]): Seq[String] = {
routeNodeDatas.map(n => s"$title=(${nodeString(n)})")
private def nodeStrings(title: String, nodeDatas: Seq[RouteNodeData]): Seq[String] = {
nodeDatas.map(n => s"$title=${nodeString(n)}")
}

private def nodeString(routeNodeData: RouteNodeData): String = {
"%s/%s/%s".format(
routeNodeData.node.id,
routeNodeData.name,
if (routeNodeData.isInWay) "W" else "R",
private def nodeString(nodeData: RouteNodeData): String = {
"%s(%s)%s".format(
nodeData.nodeId,
nodeData.alternateName,
if (nodeData.isInWay) "W" else "R",
)
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package kpn.server.analyzer.engine.analysis.route

import kpn.api.common.data.Node

case class RouteNodeData(
node: Node,
nodeId: Long,
// latitude: String,
// longitude: String,
name: String,
alternateName: String,
// longName: Option[String] = None,
// definedInRelation: Boolean = false,
// definedInWay: Boolean = false,
isInWay: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -48,108 +48,116 @@ class RouteNodeAnalyzer(context: RouteDetailAnalysisContext) {
}

private def analyzeRouteWithSingleNetworkType(networkType: NetworkType): RouteDetailAnalysisContext = {
val routeNodeDatas = findRouteNodes(networkType)
val routeNodeAnalysis = if (routeNodeDatas.isEmpty) {
val nodeDatas = findRouteNodes(networkType)
val nodeAnalysis = if (nodeDatas.isEmpty) {
facts += RouteWithoutNodes
RouteNodeAnalysis()
}
else {
if (routeNodeDatas.isEmpty) {
if (nodeDatas.isEmpty) {
RouteNodeAnalysis()
}
else {
val wayRouteNodeDatas = routeNodeDatas.filter(_.isInWay)
val wayRouteNodeDatas = nodeDatas.filter(_.isInWay)
val startNodeName = determineStartNodeName(nodeDatas, wayRouteNodeDatas)
val endNodeNameOption: Option[String] = determineEndNodeName(startNodeName, nodeDatas, wayRouteNodeDatas)

val startNodeName = {
if (wayRouteNodeDatas.nonEmpty) {
wayRouteNodeDatas.head.name // prefer node included in way over node that is only included in the relation
}
else {
routeNodeDatas.head.name
}
}
val startNodes = withSuffixes(nodeDatas.filter(_.name == startNodeName).reverse)
val endNodes = withSuffixes(nodeDatas.filter(n => endNodeNameOption.contains(n.name)))
val nodeIds = startNodes.map(_.nodeId) ++ endNodes.map(_.nodeId)
val redundantNodes = nodeDatas.filterNot(n => nodeIds.contains(n.nodeId))

val endNodeNameOption: Option[String] = {
val nonStartNodes = wayRouteNodeDatas.filter(_.name != startNodeName)
if (nonStartNodes.nonEmpty) {
Some(nonStartNodes.last.name)
}
else {
val allNonStartNodes = routeNodeDatas.filter(_.name != startNodeName)
if (allNonStartNodes.nonEmpty) {
Some(allNonStartNodes.last.name)
}
else {
None // no route nodes with name not equal to startNodeName
}
}
}

val startNodes = routeNodeDatas.filter(_.name == startNodeName)
val endNodes = routeNodeDatas.filter(n => endNodeNameOption.contains(n.name))
val redundantNodes = routeNodeDatas.filterNot(n => startNodes.contains(n) || endNodes.contains(n))

val routeNodeAnalysis = RouteNodeAnalysis(
startNode = startNodes.lastOption,
val nodeAnalysis = RouteNodeAnalysis(
startNode = startNodes.headOption,
endNode = endNodes.headOption,
startTentacleNodes = startNodes.dropRight(1),
startTentacleNodes = startNodes.drop(1),
endTentacleNodes = endNodes.drop(1),
redundantNodes = redundantNodes
)

if (routeNodeAnalysis.startNode.nonEmpty && !routeNodeAnalysis.startNode.exists(_.isInWay)) {
if (nodeAnalysis.startNode.nonEmpty && !nodeAnalysis.startNode.exists(_.isInWay)) {
facts += RouteNodeMissingInWays
}
else if (routeNodeAnalysis.endNode.nonEmpty && !routeNodeAnalysis.endNode.exists(_.isInWay)) {
else if (nodeAnalysis.endNode.nonEmpty && !nodeAnalysis.endNode.exists(_.isInWay)) {
facts += RouteNodeMissingInWays
}

if (routeNodeAnalysis.redundantNodes.nonEmpty) {
if (nodeAnalysis.redundantNodes.nonEmpty) {
facts += RouteRedundantNodes
}

routeNodeAnalysis
nodeAnalysis
}
}

context.copy(
_nodeAnalysis = Some(routeNodeAnalysis)
_nodeAnalysis = Some(nodeAnalysis)
).withFacts(facts.toSeq *)
}

private def determineStartNodeName(nodeDatas: Seq[RouteNodeData], wayNodeDatas: Seq[RouteNodeData]): String = {
if (wayNodeDatas.nonEmpty) {
wayNodeDatas.head.name // prefer node included in way over node that is only included in the relation
}
else {
nodeDatas.head.name
}
}

private def determineEndNodeName(
startNodeName: String,
nodeDatas: Seq[RouteNodeData],
wayNodeDatas: Seq[RouteNodeData]
): Option[String] = {

val nonStartNodes = wayNodeDatas.filter(_.name != startNodeName)
if (nonStartNodes.nonEmpty) {
Some(nonStartNodes.last.name)
}
else {
val allNonStartNodes = nodeDatas.filter(_.name != startNodeName)
if (allNonStartNodes.nonEmpty) {
Some(allNonStartNodes.last.name)
}
else {
None // no route nodes with name not equal to startNodeName
}
}
}

private def findRouteNodes(networkType: NetworkType): Seq[RouteNodeData] = {
val routeNodeDatas = ListBuffer[RouteNodeData]()
val nodeDatas = ListBuffer[RouteNodeData]()
context.relation.members.foreach {
case wayMember: WayMember =>
wayMember.way.nodes.distinct.foreach { node =>
wayNodeData(networkType, node) match {
case None => // not a node network node
case Some(routeNodeData) =>
if (!routeNodeDatas.filter(_.isInWay).map(_.node.id).contains(routeNodeData.node.id)) {
routeNodeDatas += routeNodeData
case Some(nodeData) =>
if (!nodeDatas.filter(_.isInWay).map(_.nodeId).contains(nodeData.nodeId)) {
nodeDatas += nodeData
}
}
}

case nodeMember: NodeMember =>
if (routeNodeDatas.map(_.node.id).contains(nodeMember.node.id)) {
if (nodeDatas.map(_.nodeId).contains(nodeMember.node.id)) {
// we prefer the position of the node in the ways over the position in the route relation
}
else {
standaloneNodeData(networkType, nodeMember.node) match {
case None => // not a node network node
case Some(routeNodeData) =>
if (!routeNodeDatas.filterNot(_.isInWay).map(_.node.id).contains(routeNodeData.node.id)) {
routeNodeDatas += routeNodeData
case Some(nodeData) =>
if (!nodeDatas.filterNot(_.isInWay).map(_.nodeId).contains(nodeData.nodeId)) {
nodeDatas += nodeData
}
}
}
case _ =>
}

val wayNodeIds = routeNodeDatas.toSeq.filter(_.isInWay).map(_.node.id)
routeNodeDatas.toSeq.filter { routeNodeData =>
routeNodeData.isInWay || !wayNodeIds.contains(routeNodeData.node.id)
val wayNodeIds = nodeDatas.toSeq.filter(_.isInWay).map(_.nodeId)
nodeDatas.toSeq.filter { nodeData =>
nodeData.isInWay || !wayNodeIds.contains(nodeData.nodeId)
}
}

Expand All @@ -172,7 +180,8 @@ class RouteNodeAnalyzer(context: RouteDetailAnalysisContext) {
private def wayNodeData(networkType: NetworkType, node: Node): Option[RouteNodeData] = {
nodeName(networkType, node).map { name =>
RouteNodeData(
node,
node.id,
name,
name,
isInWay = true
)
Expand All @@ -182,7 +191,8 @@ class RouteNodeAnalyzer(context: RouteDetailAnalysisContext) {
private def standaloneNodeData(networkType: NetworkType, node: Node): Option[RouteNodeData] = {
nodeName(networkType, node).map { name =>
RouteNodeData(
node,
node.id,
name,
name,
isInWay = false
)
Expand Down Expand Up @@ -217,4 +227,21 @@ class RouteNodeAnalyzer(context: RouteDetailAnalysisContext) {
None
}
}

private def withSuffixes(nodeDatas: Seq[RouteNodeData]): Seq[RouteNodeData] = {
nodeDatas.zipWithIndex.map { case (nodeData, index) =>
val suffixes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
if (nodeDatas.size == 1) {
nodeData.copy(alternateName = nodeData.name)
}
else {
if (index < suffixes.length) {
nodeData.copy(alternateName = s"${nodeData.name}.${suffixes(index)}")
}
else {
nodeData.copy(alternateName = nodeData.name)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class RouteLinksReport(context: RouteDetailAnalysisContext) {
}

private def nodes(nodeIds: Seq[Long], nodeType: String, nodeDatas: Seq[RouteNodeData]): Seq[String] = {
val filteredNodeDatas = nodeDatas.filter(n => nodeIds.contains(n.node.id))
filteredNodeDatas.map(nodeData => s"$nodeType=${nodeData.node.id}(${nodeData.name})")
val filteredNodeDatas = nodeDatas.filter(n => nodeIds.contains(n.nodeId))
filteredNodeDatas.map(nodeData => s"$nodeType=${nodeData.nodeId}(${nodeData.name})")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ object RouteNodeAnalysisReport {
private def routeNodeReport(nodeType: String, routeNode: RouteNodeData): String = {
s"""<tr>
| <td>$nodeType</td>
| <td>${routeNode.node.id}</td>
| <td>${routeNode.nodeId}</td>
| <td>${routeNode.name}</td>
| <td>${yes(routeNode.isInWay)}</td>
|</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class RoutePathReport(context: RouteDetailAnalysisContext) {
context.segments.flatMap(_.elements).map { element =>
val fromNode = element.fromNetworkNode
val toNode = element.toNetworkNode
val from = fromNode.map(n => s"${ReportUtil.osmNodeLink(n.node.id)}(${n.name})").getOrElse("")
val to = toNode.map(n => s"${ReportUtil.osmNodeLink(n.node.id)}(${n.name})").getOrElse("")
val from = fromNode.map(n => s"${ReportUtil.osmNodeLink(n.nodeId)}(${n.name})").getOrElse("")
val to = toNode.map(n => s"${ReportUtil.osmNodeLink(n.nodeId)}(${n.name})").getOrElse("")
val elementIds = "" + element.id

s"""<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ class RouteSegmentReport(context: RouteDetailAnalysisContext) {

private def segmentElements(segment: RouteAnalysisSegment): String = {
segment.elements.map { element =>
val from = element.fromNetworkNode.map(node => s"from=${ReportUtil.osmNodeLink(node.node.id)}(${node.name})").getOrElse("")
val to = element.toNetworkNode.map(node => s"to=${ReportUtil.osmNodeLink(node.node.id)}(${node.name})").getOrElse("")
val from = element.fromNetworkNode.map(node => s"from=${ReportUtil.osmNodeLink(node.nodeId)}(${node.name})").getOrElse("")
val to = element.toNetworkNode.map(node => s"to=${ReportUtil.osmNodeLink(node.nodeId)}(${node.name})").getOrElse("")
s"""<tr>
| <td colspan="5">
| Segment element ${element.id} ${element.direction.toString.toLowerCase} $from $to
Expand Down Expand Up @@ -125,7 +125,7 @@ class RouteSegmentReport(context: RouteDetailAnalysisContext) {
}

private def nodes(nodeIds: Seq[Long], nodeType: String, nodeDatas: Seq[RouteNodeData]): Seq[String] = {
val filteredNodeDatas = nodeDatas.filter(n => nodeIds.contains(n.node.id))
filteredNodeDatas.map(nodeData => s"$nodeType=${nodeData.node.id}(${nodeData.name})")
val filteredNodeDatas = nodeDatas.filter(n => nodeIds.contains(n.nodeId))
filteredNodeDatas.map(nodeData => s"$nodeType=${nodeData.nodeId}(${nodeData.name})")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ class RouteSegmentAnalyzer(context: RouteDetailAnalysisContext) {
RoutePathDirection.Bidirectional
}

val fromNetworkNode = context.nodeAnalysis.nodes.find(_.node.id == fromNodeId)
val toNetworkNode = context.nodeAnalysis.nodes.find(_.node.id == toNodeId)
val fromNetworkNode = context.nodeAnalysis.nodes.find(_.nodeId == fromNodeId)
val toNetworkNode = context.nodeAnalysis.nodes.find(_.nodeId == toNodeId)

buildElement(
direction,
Expand All @@ -228,8 +228,8 @@ class RouteSegmentAnalyzer(context: RouteDetailAnalysisContext) {
private def buildFragmentElement(routeLinkWay: RouteLinkWay, direction: RoutePathDirection, nodeIds: Seq[Long]): RouteAnalysisElement = {
// this is a closed loop at the end of the route
val fragment = toFragment(routeLinkWay, nodeIds)
val fromNetworkNode = context.nodeAnalysis.nodes.find(_.node.id == fragment.fromNodeId)
val toNetworkNode = context.nodeAnalysis.nodes.find(_.node.id == fragment.toNodeId)
val fromNetworkNode = context.nodeAnalysis.nodes.find(_.nodeId == fragment.fromNodeId)
val toNetworkNode = context.nodeAnalysis.nodes.find(_.nodeId == fragment.toNodeId)

buildElement(
direction,
Expand Down
Loading

0 comments on commit e4aadae

Please sign in to comment.