Skip to content

Commit

Permalink
enhance BoundaryFactory to handle relations with more than one bounda…
Browse files Browse the repository at this point in the history
…ry polygon
  • Loading branch information
danielfeismann committed May 13, 2024
1 parent 706c341 commit 8ccbdf6
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 127 deletions.
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.osmogrid.model.OsmoGridModel.{EnhancedOsmEntity, LvOsmoGridModel}
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 @@ object BoundaryFactory {
* @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 @@ object BoundaryFactory {
.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,124 +66,161 @@ object BoundaryFactory {
* @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 { node =>
// Turn nodes into coordinates
GeoUtils.buildCoordinate(node.latitude, node.longitude)
.map(n => GeoUtils.buildCoordinate(n.latitude, n.longitude))
}
.toArray

GeoUtils.buildPolygon(coordinates)
// 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

}

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

private def addWayNodesToPolygonSequence(

Check failure on line 160 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#L160

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 {
// 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)) =>
if (existingFirst == existingLast) {
// Polygon was closed by the way before.
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) =>
if (existingLast == currentFirst)
// 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)
(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)
(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)
(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)
// 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
(existingNodes)
)
}
case None =>
// No nodes added yet, just put current ones in place
currentNodes
(currentNodes)
}
(result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ object OsmoGridModelPartitioner extends LazyLogging {
*/
def partition(
osmoGridModel: LvOsmoGridModel,
areas: ParMap[AreaKey, Polygon]
areas: ParMap[AreaKey, Seq[Polygon]]
): ParMap[AreaKey, LvOsmoGridModel] = {
val buildings = assign(
osmoGridModel.buildings,
Expand Down Expand Up @@ -85,10 +85,9 @@ object OsmoGridModelPartitioner extends LazyLogging {

private def assign(
enhancedEntities: ParSeq[EnhancedOsmEntity],
areas: ParMap[AreaKey, Polygon],
areas: ParMap[AreaKey, Seq[Polygon]],
allocationStrategy: EntityAllocationStrategy
): ParMap[AreaKey, ParSeq[EnhancedOsmEntity]] = {

enhancedEntities
.flatMap { entity =>
assign(entity, areas, allocationStrategy).map(_ -> entity)
Expand All @@ -101,10 +100,9 @@ object OsmoGridModelPartitioner extends LazyLogging {

private def assign(
enhancedEntity: EnhancedOsmEntity,
areas: ParMap[AreaKey, Polygon],
areas: ParMap[AreaKey, Seq[Polygon]],
allocationStrategy: EntityAllocationStrategy
): Iterable[AreaKey] = {

val entityVotes = vote(enhancedEntity.entity, enhancedEntity, areas)

allocationStrategy match {
Expand All @@ -128,7 +126,7 @@ object OsmoGridModelPartitioner extends LazyLogging {
private def vote(
entity: OsmEntity,
enhancedEntity: EnhancedOsmEntity,
areas: ParMap[AreaKey, Polygon]
areas: ParMap[AreaKey, Seq[Polygon]]
): Map[AreaKey, Int] =
entity match {
case node: OsmEntity.Node =>
Expand All @@ -142,7 +140,7 @@ object OsmoGridModelPartitioner extends LazyLogging {
private def vote(
relation: OsmEntity.Relation,
enhancedEntity: EnhancedOsmEntity,
areas: ParMap[AreaKey, Polygon]
areas: ParMap[AreaKey, Seq[Polygon]]
): Map[AreaKey, Int] =
relation.members
.flatMap { member =>
Expand All @@ -168,7 +166,7 @@ object OsmoGridModelPartitioner extends LazyLogging {
private def vote(
way: OsmEntity.Way,
enhancedEntity: EnhancedOsmEntity,
areas: ParMap[AreaKey, Polygon]
areas: ParMap[AreaKey, Seq[Polygon]]
): Map[AreaKey, Int] =
way.nodes
.flatMap(enhancedEntity.node)
Expand All @@ -182,14 +180,12 @@ object OsmoGridModelPartitioner extends LazyLogging {

private def vote(
node: OsmEntity.Node,
areas: ParMap[AreaKey, Polygon]
areas: ParMap[AreaKey, Seq[Polygon]]
): Map[AreaKey, Int] = {
val point = GeoUtils.buildPoint(node.latitude, node.longitude)
areas.iterator
.collectFirst {
case areaId -> polygon if polygon.covers(point) =>
Map(areaId -> 1)
}
.getOrElse(Map.empty)
areas.iterator.collect {
case (areaId, polygons) if polygons.exists(_.covers(point)) =>
areaId -> 1
}.toMap
}
}
10 changes: 10 additions & 0 deletions src/main/scala/edu/ie3/osmogrid/model/OsmoGridModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ sealed trait OsmoGridModel {
}

object OsmoGridModel {
def filterNodes(entities: Seq[EnhancedOsmEntity]): Map[Long, Node] = {
entities.foldLeft(Map.empty[Long, Node]) {
(matchedSubEntities, curEntity) =>
curEntity.entity match {
case entity: Node =>
matchedSubEntities + (entity.id -> entity)
case _ => matchedSubEntities
}
}
}

def filterForSubstations(
entities: ParSeq[EnhancedOsmEntity]
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading

0 comments on commit 8ccbdf6

Please sign in to comment.