Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle case causing exception when adding ways to polygon sequence #428

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- LV Coordinator dies unexpectedly [#361](https://github.com/ie3-institute/OSMoGrid/issues/361)
- Some bugs fixed [#405](https://github.com/ie3-institute/OSMoGrid/issues/405)
- Fixed number of parallel lines from zero to one [#419](https://github.com/ie3-institute/OSMoGrid/issues/419)
- Handle additional case when building a polygon from ways [#427](https://github.com/ie3-institute/OSMoGrid/issues/427)

### Removed
- Legacy Java code
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/edu/ie3/osmogrid/io/input/ReaderSink.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

package edu.ie3.osmogrid.io.input

import edu.ie3.osmogrid.exception.{InputDataException, PbfReadFailedException}
import edu.ie3.osmogrid.exception.PbfReadFailedException
import edu.ie3.osmogrid.model.OsmoGridModel.LvOsmoGridModel
import edu.ie3.osmogrid.model.{OsmoGridModel, SourceFilter}
import edu.ie3.util.osm.model.OsmContainer.ParOsmContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import edu.ie3.util.geo.GeoUtils
import edu.ie3.util.osm.model.OsmEntity
import edu.ie3.util.osm.model.OsmEntity.Relation.RelationMemberType
import org.locationtech.jts.geom.Polygon
import org.locationtech.jts.geom.{Coordinate, Polygon}

import scala.collection.mutable.ListBuffer
import scala.collection.parallel.ParMap
import scala.util.{Failure, Try}

Expand All @@ -30,12 +31,12 @@
* @param administrativeLevel
* the administrative level for which boundary polygons should be created
* @return
* a map of boundary id to polygon
* a map of boundary id to polygons (can be multiple polygons per key)
*/
def buildBoundaryPolygons(
osmoGridModel: LvOsmoGridModel,
administrativeLevel: BoundaryAdminLevelValue
): ParMap[AreaKey, Polygon] = {
): ParMap[AreaKey, Seq[Polygon]] = {
osmoGridModel.boundaries
.filter {
case EnhancedOsmEntity(
Expand All @@ -53,10 +54,10 @@
.map { enhancedEntity =>
enhancedEntity.entity.id -> buildBoundaryPolygon(enhancedEntity)
}
.toMap
.to(ParMap)
}

/** Creates a boundary polygon for given boundary relation.
/** Creates boundary polygons for given boundary relation.
*
* Boundary relations consist of one or more ways, of which two consecutive
* ways share the first and last node. The nodes in ways can be ordered in
Expand All @@ -65,120 +66,191 @@
* @param enhancedRelation
* the relation wrapped in an [[EnhancedOsmEntity]]
* @return
* the boundary polygon
* the boundary polygons
*/

private def buildBoundaryPolygon(
enhancedRelation: EnhancedOsmEntity
): Polygon = {
): List[Polygon] = {

// split seq of nodes when first nodes appears again. Repeat till it matches the last one

val relation = enhancedRelation.entity match {
case relation: OsmEntity.Relation => relation
case other =>
throw new RuntimeException(
s"Wrong entity type ${other.getClass}, Relation is required"
)
}

val coordinates = (relation.members
val allNodesOfPolygonsSeq: Seq[AreaKey] = relation.members
.flatMap {
// Filter for ways only
case OsmEntity.Relation.RelationMember(id, relationType, _) =>
relationType match {
case RelationMemberType.Way =>
enhancedRelation.way(id)
case _ =>
// We only want ways, discard all other types
None
}
case OsmEntity.Relation.RelationMember(id, RelationMemberType.Way, _) =>
enhancedRelation.way(id)
case _ => None
}
.foldLeft(Seq.empty[Long]) { case (existingNodes, currentWay) =>
addWayNodesToPolygonSequence(existingNodes, currentWay)
.recoverWith { case exc =>
Failure(
new RuntimeException(
s"Could not create Polygon from relation ${relation.id}",
exc
.foldLeft(Seq.empty[Seq[AreaKey]]) { case (existingNodes, currentWay) =>
val resultNodes: Seq[AreaKey] =
addWayNodesToPolygonSequence(existingNodes.flatten, currentWay)
.recoverWith { case exc =>
Failure(
new RuntimeException(
s"Could not create Polygon from relation ${relation.id}",
exc
)
)
}
.getOrElse(
// Current way is empty, carry on with old sequence
existingNodes.flatten
)
}
.fold(throw _, identity)
} match {
// Sanity check: in the end, first node should equal the last node
case nodes
if nodes.headOption
.zip(nodes.lastOption)
.exists { case (first, last) => first != last } =>
throw new RuntimeException(
s"First node should be last in boundary relation ${relation.id}."
)
case nodes if nodes.isEmpty =>
throw new RuntimeException(
s"Empty boundary relation ${relation.id}."
)
case nodes => nodes
})
.map { node =>
// Turn node ids into nodes

(Seq(resultNodes))
}
.flatten

val listOfSeq: Map[Int, Seq[AreaKey]] = splitByKey(allNodesOfPolygonsSeq)
val polygonList: ListBuffer[Polygon] = ListBuffer.empty[Polygon]

listOfSeq.values.foreach { seq =>
val latLonCoordinates: Seq[Coordinate] = seq.flatMap { node =>
enhancedRelation
.node(node)
.getOrElse(
throw new RuntimeException(
s"Node $node not found in enhanced relation $enhancedRelation"
)
)
.map(n => GeoUtils.buildCoordinate(n.latitude, n.longitude))
}
.map { node =>
// Turn nodes into coordinates
GeoUtils.buildCoordinate(node.latitude, node.longitude)

// Ensure the LinearRing is closed
val closedCoordinates =
if (
latLonCoordinates.nonEmpty &&
latLonCoordinates.headOption == latLonCoordinates.lastOption
)
latLonCoordinates
else
throw new RuntimeException(
s"First node should be last in boundary relation ${relation.id}."
)

val polygon: Polygon = GeoUtils.buildPolygon(closedCoordinates.toArray)
polygonList += polygon
}

polygonList.toList

}

/** Split node sequences of polygons into parts. If the sequence can't be
* split because it represents only one polygon, the sequence is returned
* with the first node added at the end to close the polygon.
*
* @param seq
* Sequence to split
* @tparam A
* type of sequence
* @return
* Indexed map with the split sequences
*/

private def splitByKey[A](seq: Seq[A]): Map[Int, Seq[A]] = {
val result = seq
.foldLeft((Map.empty[Int, Seq[A]], Map.empty[A, Int], 0)) {
case ((acc, indexes, splitIdx), elem) =>
indexes.get(elem) match {
case Some(prevIdx) =>
// Element has been seen before
val newSeq = seq.slice(prevIdx, splitIdx + 1)
(
acc + (acc.size + 1 -> newSeq),
indexes + (elem -> splitIdx),
splitIdx + 1
)
case None =>
// Element has not been seen before
(
acc,
indexes + (elem -> splitIdx),
splitIdx + 1
)
}
}
.toArray
._1

GeoUtils.buildPolygon(coordinates)
// If the result map is empty, return the whole sequence with the first element appended at the end
// (to close the polygon)
if (result.isEmpty) {
seq.headOption match {
case Some(firstElem) => Map(1 -> (seq :+ firstElem))
case None =>
Map(1 -> seq)
}
} else {
result
}
}

private def addWayNodesToPolygonSequence(

Check failure on line 190 in src/main/scala/edu/ie3/osmogrid/lv/region_coordinator/BoundaryFactory.scala

View check run for this annotation

SonarQubeGithubPRChecks / OSMoGrid Sonarqube Results

src/main/scala/edu/ie3/osmogrid/lv/region_coordinator/BoundaryFactory.scala#L190

Refactor this method to reduce its Cognitive Complexity from 27 to the 15 allowed.
existingNodes: Seq[AreaKey],
currentWay: OsmEntity.Way
): Try[Seq[AreaKey]] = Try {
): Try[(Seq[AreaKey])] = Try {
danielfeismann marked this conversation as resolved.
Show resolved Hide resolved
// Construct one single sequence of nodes by joining the ways.
// Each way can be ordered in correct or in reverse order
val currentNodes = currentWay.nodes
existingNodes.headOption.zip(existingNodes.lastOption) match {
case Some((existingFirst, existingLast)) =>
currentNodes.headOption
.zip(currentNodes.lastOption)
.map { case (currentFirst, currentLast) =>
// Run through a bunch of cases. In the end, we want [a, b, c, d, e]
if (existingLast == currentFirst)
// [a, b, c] and [c, d, e]
// All in correct order
existingNodes ++ currentNodes.drop(1)
else if (existingLast == currentLast)
// [a, b, c] and [e, d, c]
// Additional sequence needs to be flipped
existingNodes ++ currentNodes.reverse.drop(1)
else if (existingFirst == currentFirst)
// [c, b, a] and [c, d, e]
// Existing sequence in wrong order:
// this should only happen if we have only added one
// sequence so far and that sequence was in wrong order
existingNodes.reverse ++ currentNodes.drop(1)
else if (existingFirst == currentLast)
// [c, b, a] and [e, d, c]; a != e since we already covered this with first case
// Existing sequence in wrong order, similar to above
// but additional sequence has be flipped as well
existingNodes.reverse ++ currentNodes.reverse.drop(1)
else
throw new RuntimeException(
s"Last node $existingLast was not found in way ${currentWay.id}"
)
}
.getOrElse(
// Current way is empty, carry on with old sequence
existingNodes
)
val currentNodes: Seq[AreaKey] = currentWay.nodes
val result = existingNodes.headOption.zip(existingNodes.lastOption) match {
case Some((existingFirst: AreaKey, existingLast: AreaKey)) =>
if (existingFirst.equals(existingLast)) {
existingNodes ++ currentNodes
} else {

currentNodes.headOption
.zip(currentNodes.lastOption)
.map { case (currentFirst, currentLast) =>
// Run through a bunch of cases. In the end, we want [a, b, c, d, e ... a]
if (
existingFirst == currentFirst && existingLast == currentLast
) {
// [a, b, c] and [a, d, c]
// Additional sequence needs to be flipped
// Polygon will be closed by this way
(existingNodes ++ currentNodes.reverse.drop(1))
} else if (
existingFirst == currentLast && existingLast == currentFirst
) {
// [a, b, c] and [c, d, a]
// All in correct order
// Polygon will be closed by this way
(existingNodes ++ currentNodes.drop(1))
} else if (existingLast == currentFirst)
// [a, b, c] and [c, d, e]
// All in correct order
(existingNodes ++ currentNodes.drop(1))
else if (existingLast == currentLast)
// [a, b, c] and [e, d, c]
// Additional sequence needs to be flipped
(existingNodes ++ currentNodes.reverse.drop(1))
else if (existingFirst == currentFirst)
// [c, b, a] and [c, d, e]
// Existing sequence in wrong order:
// this should only happen if we have only added one
// sequence so far and that sequence was in wrong order
(existingNodes.reverse ++ currentNodes.drop(1))
else if (existingFirst == currentLast)
// [c, b, a] and [e, d, c]; a != e since we already covered this with first case
// Existing sequence in wrong order, similar to above
// but additional sequence has to be flipped as well
(existingNodes.reverse ++ currentNodes.reverse.drop(1))
else
throw new RuntimeException(
s"Last node $existingLast was not found in way ${currentWay.id}"
)
}
.getOrElse(
// Current way is empty, carry on with old sequence
(existingNodes)
)
}
case None =>
// No nodes added yet, just put current ones in place
currentNodes
(currentNodes)
}
(result)
}
}
Loading