Repository: finnishtransportagency/viite
Pull request #1798: VIITE-3493 add validation for crossroad case
Author: login: LauriLitmanen
Created: 2025-05-05T11:16:52Z | Merged: 2025-05-08T11:09:15Z



Diff:
diff --git a/viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala b/viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala
index fe587ef3ef..c5577e6920 100644
--- a/viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala
+++ b/viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala
@@ -851,6 +851,27 @@ class LinearLocationDAO extends BaseDAO {
     queryList(query)
   }
 
+  /**
+   * Fetches active linear locations that intersect with a given point.
+   *
+   * @param point A Point object representing the spatial location to check.
+   * @return A sequence of LinearLocation objects that are active and intersect with the point.
+   */
+  def fetchActiveLinearLocationsWithPoint(point: Point): Seq[LinearLocation] = {
+    val wktPoint = s"POINT(${point.x} ${point.y})"
+    val query =
+      sql"""
+      $selectFromLinearLocation
+      WHERE loc.valid_to IS NULL
+        AND loc.roadway_number IN (
+          SELECT roadway_number FROM roadway
+          WHERE valid_to IS NULL AND end_date IS NULL
+        )
+        AND ST_DWithin(geometry, ST_GeomFromText($wktPoint, 3067), $MaxDistanceForConnectedLinks)
+      """
+    queryList(query)
+  }
+
   def fetchUpdatedSince(sinceDate: DateTime): Seq[LinearLocation] = {
     time(logger, "Fetch linear locations updated since date") {
       val query =
diff --git a/viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala b/viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala
index 04cf5dd055..e70718a1ed 100644
--- a/viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala
+++ b/viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala
@@ -411,6 +411,134 @@ class DynamicRoadNetworkService(linearLocationDAO: LinearLocationDAO, roadwayDAO
       TiekamuRoadLinkErrorMetaData(errorRoadsParts.head, errorRoadways.head.roadwayNumber, errorLinearLocations.map(_.id), errorLink)
     }
 
+    def validateCombinationCases(otherChangesWithSameNewLinkId: Seq[TiekamuRoadLinkChange], change: TiekamuRoadLinkChange, linearLocations: Seq[LinearLocation]): Seq[TiekamuRoadLinkChangeError] = {
+      def areHomogeneous(linearLocations: Seq[LinearLocation]): Boolean = {
+        val roadwayNumbers = linearLocations.map(_.roadwayNumber).toSet
+        val roadways = roadwayDAO.fetchAllByRoadwayNumbers(roadwayNumbers)
+        val roadGroups = roadways.groupBy(rw => (rw.roadPart, rw.track))
+        roadGroups.size == 1
+      }
+
+      def changesInSameRoadway(linearLocations: Seq[LinearLocation]): Boolean = {
+        val roadwayNumbers = linearLocations.map(_.roadwayNumber).toSet
+        val roadways = roadwayDAO.fetchAllByRoadwayNumbers(roadwayNumbers)
+        val roadGroups = roadways.groupBy(rw => (rw.roadPart, rw.track))
+        val singleRoadGroup = roadGroups.head._2
+        singleRoadGroup.map(_.roadwayNumber).distinct.size == 1
+      }
+
+      def homogeneityValidationForCombinationCase(): Seq[TiekamuRoadLinkChangeError] = {
+        val oldLinkIds = otherChangesWithSameNewLinkId.map(_.oldLinkId) :+ change.oldLinkId
+        val oldLinearLocations = linearLocations.filter(ll => oldLinkIds.contains(ll.linkId))
+        val homogeneous = areHomogeneous(oldLinearLocations)
+
+        if (!homogeneous) {
+          Seq(
+            TiekamuRoadLinkChangeError(
+              "Two or more links with non-homogeneous road addresses (road number, road part number, track) cannot merge together.",
+              change,
+              getMetaData(change, linearLocations)
+            )
+          )
+        } else {
+          Seq.empty
+        }
+      }
+
+      def continuityValidationForCombinationCase(): Seq[TiekamuRoadLinkChangeError] = {
+        val oldLinkIds = otherChangesWithSameNewLinkId.map(_.oldLinkId) :+ change.oldLinkId
+        val oldLinearLocations = linearLocations.filter(ll => oldLinkIds.contains(ll.linkId))
+
+        val continuityCheckPassed = if (changesInSameRoadway(oldLinearLocations)) {
+          checkRoadAddressContinuityForSingleRoadway(oldLinearLocations, change)
+        } else {
+          checkRoadAddressContinuityBetweenRoadways(oldLinearLocations, change)
+        }
+
+        if (!continuityCheckPassed) {
+          Seq(
+            TiekamuRoadLinkChangeError(
+              "Road address not continuous, cannot merge links together.",
+              change,
+              getMetaData(change, linearLocations)
+            )
+          )
+        } else {
+          Seq.empty
+        }
+      }
+
+      def returnCrossRoadValidationError(change: TiekamuRoadLinkChange, linearLocationsAtPoint:Seq[LinearLocation]): Option[TiekamuRoadLinkChangeError] = {
+        Some(
+          TiekamuRoadLinkChangeError(
+            "Links cannot be merged together, found linear location(s) that connect(s) between the two links. (Cross road case)",
+            change,
+            getMetaData(change, linearLocationsAtPoint)
+          )
+        )
+      }
+
+      def crossRoadValidationForCombinationCase(): Seq[TiekamuRoadLinkChangeError] = {
+        val oldLinkIds = otherChangesWithSameNewLinkId.map(_.oldLinkId) :+ change.oldLinkId
+        val oldLinearLocations = linearLocations.filter(ll => oldLinkIds.contains(ll.linkId))
+
+        oldLinearLocations.flatMap { oldLinearLocation =>
+          val connectingOldLinearLocationOpt = oldLinearLocations.find { another =>
+            oldLinearLocation.connected(another)
+          }
+
+          connectingOldLinearLocationOpt.flatMap { connectingOld =>
+            val linearLocationStartAndEndPoints = Seq(oldLinearLocation.getFirstPoint, oldLinearLocation.getLastPoint)
+            val startingPoint = connectingOld.getFirstPoint
+            val endPoint = connectingOld.getLastPoint
+
+            val connectingPoint =
+              if (linearLocationStartAndEndPoints.exists(p => p.connected(startingPoint))) startingPoint
+              else endPoint
+
+            // Find all the active linear locations that have geometry on the connecting point
+            val linearLocationsAtPoint = linearLocationDAO.fetchActiveLinearLocationsWithPoint(connectingPoint)
+            // Filter out the linear locations that are part of the change, leaving only the possibly "problematic" linear locations
+            // that may prevent the combination due to a crossing/junction
+            val problematicLinearLocations = linearLocationsAtPoint.filterNot(ll =>
+              oldLinearLocations.exists(_.linkId == ll.linkId)
+            )
+
+            if (problematicLinearLocations.nonEmpty) {
+              val problematicLinearLocationChanges = problematicLinearLocations.flatMap(ll => {
+                val linkId = ll.linkId
+                tiekamuRoadLinkChanges.filter(ch => ch.oldLinkId == linkId)
+              })
+
+              if (problematicLinearLocationChanges.nonEmpty) { // If there are changes to the problematic linear locations then we need further validation
+                // Get the new links from KGV links
+                val newLinkIds = problematicLinearLocationChanges.map(_.newLinkId).toSet
+                val kgvLinksForChanges = kgvRoadLinks.filter(kgvLink => newLinkIds.contains(kgvLink.linkId))
+                // Get the start and end points
+                val startAndEndPoints = kgvLinksForChanges.flatMap(kgvLink => {
+                  Seq(kgvLink.geometry.head, kgvLink.geometry.last)
+                })
+
+                if (startAndEndPoints.contains(connectingPoint)){ // If the connecting point still has a linear location connected after the days link changes
+                  // Then it is an error
+                  returnCrossRoadValidationError(change, linearLocationsAtPoint)
+                } else {
+                  None
+                }
+              } else { // If there are no changes for the problematic linear location in the same day
+                // Then it is an error
+                returnCrossRoadValidationError(change, linearLocationsAtPoint)
+              }
+            } else {
+              None
+            }
+          }
+        }
+      }
+
+      homogeneityValidationForCombinationCase() ++ continuityValidationForCombinationCase() ++ crossRoadValidationForCombinationCase()
+    }
+
     time(logger, "Validating TiekamuRoadLinkChange sets") {
       var tiekamuRoadLinkChangeErrors = new ListBuffer[TiekamuRoadLinkChangeError]()
       tiekamuRoadLinkChanges.foreach(change => {
@@ -449,25 +577,7 @@ class DynamicRoadNetworkService(linearLocationDAO: LinearLocationDAO, roadwayDAO
 
         // if there are combined links (A + B = C)
         else if (otherChangesWithSameNewLinkId.nonEmpty) {
-          val oldLinkIds = otherChangesWithSameNewLinkId.map(_.oldLinkId) ++ Seq(change.oldLinkId)
-          val oldLinearLocations = linearLocations.filter(ll => oldLinkIds.contains(ll.linkId))
-          val roadways = roadwayDAO.fetchAllByRoadwayNumbers(oldLinearLocations.map(_.roadwayNumber).toSet)
-          // group the roadways with roadPart and track
-          val roadGroups = roadways.groupBy(rw => (rw.roadPart, rw.track))
-          // if there are change infos that combine two or more links but the road address is not homogeneous between those merging links then its an error
-          val isRoadAddressHomogeneous = roadGroups.size < 2
-          val changesInSameRoadway = roadGroups.head._2.map(_.roadwayNumber).distinct.size == 1 // used only when we already know roadGroup has only one item, thus handling .head only is enough
-          isRoadAddressHomogeneous match {
-            case false => tiekamuRoadLinkChangeErrors += TiekamuRoadLinkChangeError("Two or more links with non homogeneous road addresses (road number, road part number, track) cannot merge together", change, getMetaData(change, linearLocations))
-            case true => {
-              changesInSameRoadway match {
-                case true => if (!checkRoadAddressContinuityForSingleRoadway(oldLinearLocations, change))
-                  tiekamuRoadLinkChangeErrors += TiekamuRoadLinkChangeError("Road address not continuous, cannot merge links together", change, getMetaData(change, linearLocations))
-                case false => if (!checkRoadAddressContinuityBetweenRoadways(oldLinearLocations, change))
-                  tiekamuRoadLinkChangeErrors += TiekamuRoadLinkChangeError("Road address not continuous, cannot merge links together", change, getMetaData(change, linearLocations))
-              }
-            }
-          }
+          tiekamuRoadLinkChangeErrors ++= validateCombinationCases(otherChangesWithSameNewLinkId, change, linearLocations)
         }
       })
       tiekamuRoadLinkChangeErrors
diff --git a/viite-backend/viite-main/src/test/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkServiceSpec.scala b/viite-backend/viite-main/src/test/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkServiceSpec.scala
index 5a4a1322f2..cdf6030422 100644
--- a/viite-backend/viite-main/src/test/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkServiceSpec.scala
+++ b/viite-backend/viite-main/src/test/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkServiceSpec.scala
@@ -91,4 +91,224 @@ class DynamicRoadNetworkServiceSpec extends AnyFunSuite with Matchers{
       res.length should be (0)
     }
   }
+
+  test("When Validating invalid TiekamuRoadLinkChanges (non-homogeneous and discontinuous combine case) Then should return TiekamuRoadLinkErrors") {
+
+    /**
+     * Link A is on road part 18344/1
+     * Link B is on road part 46001/1
+     *
+     * The link changes:
+     *
+     *         Before:
+     *      A         B
+     *  -------->---------->
+     *
+     *          After:
+     *          C (the goal would be to replace A and B with this one)
+     *  ------------------->
+     */
+    runWithRollback {
+      val linkA = "testtest-test-test-test-test:a"
+      val linkB = "testtest-test-test-test-test:b"
+      val newLinkC = "testtest-test-test-test-test:c"
+      val geometryA = Seq(Point(0.0, 0.0), Point(50.0,0.0))
+      val geometryB = Seq(Point(50.0, 0.0), Point(100.0, 0.0))
+      val newGeometryC = Seq(Point(0.0, 0.0), Point(100.0, 0.0))
+      val orderNumber1 = 1.0
+      val roadwayNumber1 = Sequences.nextRoadwayNumber
+      val roadwayNumber2 = Sequences.nextRoadwayNumber
+      val roadPart1 = RoadPart(18344, 1)
+      val roadPart2 = RoadPart(46001, 1)
+
+      // Create Roadways
+      roadwayDAO.create(Seq(
+        Roadway(Sequences.nextRoadwayId, roadwayNumber1, roadPart1, AdministrativeClass.State, Track.Combined,Discontinuity.EndOfRoad, AddrMRange(0, 50), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9,TerminationCode.NoTermination, DateTime.now().minusDays(1),None),
+        Roadway(Sequences.nextRoadwayId, roadwayNumber2, roadPart2, AdministrativeClass.State, Track.Combined,Discontinuity.EndOfRoad, AddrMRange(0, 50), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9,TerminationCode.NoTermination, DateTime.now().minusDays(1),None)
+      ))
+
+      // Create LinearLocations
+      val linearLocationIds = linearLocationDAO.create(Seq(
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkA, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.RoadAddressCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.RoadAddressCP))),
+          geometryA,LinkGeomSource.NormalLinkInterface,
+          roadwayNumber1,Some(DateTime.now().minusDays(1)), None),
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkB, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.RoadAddressCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.RoadAddressCP))),
+          geometryB, LinkGeomSource.NormalLinkInterface,
+          roadwayNumber2, Some(DateTime.now().minusDays(1)), None)
+      ))
+      val linearlocations = linearLocationDAO.fetchByIdMassQuery(linearLocationIds)
+
+      // Create RoadwayPoints
+      val roadwayPointId1 = roadwayPointDAO.create(roadwayNumber1, 0, "test")
+      val roadwayPointId2 = roadwayPointDAO.create(roadwayNumber1, 50, "test")
+      val roadwayPointId3 = roadwayPointDAO.create(roadwayNumber2, 0, "test")
+      val roadwayPointId4 = roadwayPointDAO.create(roadwayNumber2, 50, "test")
+
+      // Create CalibrationPoints
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId1, linkA, roadwayNumber1, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId2, linkA, roadwayNumber1, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId3, linkB, roadwayNumber2, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId4, linkB, roadwayNumber2, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+
+
+      // Create TiekamuRoadLinkChanges
+      val tiekamuRoadLinkChanges = Seq(
+        TiekamuRoadLinkChange(linkA, 0.0, 50.0, newLinkC, 0.0, 50.0, digitizationChange = false),
+        TiekamuRoadLinkChange(linkB, 0.0, 50.0, newLinkC, 50.0, 100.0, digitizationChange = false)
+      )
+
+      // Create KGVRoadLinks
+      val kgvRoadLinks = Seq(
+        RoadLink(linkA, geometryA, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(linkB, geometryB, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(newLinkC, newGeometryC, 100.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = "")
+      )
+
+      val complementaryLinks = Seq()
+
+      // The testing itself
+      val res = dynamicRoadNetworkService.validateTiekamuRoadLinkChanges(tiekamuRoadLinkChanges,linearlocations, kgvRoadLinks, complementaryLinks)
+      // 4 validation errors,
+      // 2 per link (A and B),
+      // "Road address not continuous, cannot merge links together."
+      // "Two or more links with non-homogeneous road addresses (road number, road part number, track) cannot merge together."
+      res.length should be (4)
+    }
+  }
+
+  test("When Validating invalid TiekamuRoadLinkChanges (active linear location prevents combination of two links) Then should return TiekamuRoadLinkErrors") {
+    /**
+     * Link change A + B = C
+     * Y = Active linear location
+     *
+     *          Before:
+     *
+     *    A             B
+     * ----------->------------>
+     *            ^
+     *            |
+     *            | Y
+     *            |
+     *
+     *           After:
+     *
+     *             C
+     * ------------------------->
+     *             ^
+     *             |
+     *             | Y
+     *             |
+     */
+    runWithRollback {
+      val linkId1 = "testtest-test-test-test-test:1"
+      val linkId2 = "testtest-test-test-test-test:2"
+      val linkId9 = "testtest-test-test-test-test:9"
+      val newLinkId1 = "testtest-test-test-test-test:3"
+      val newLinkId2 = "testtest-test-test-test-test:4"
+      val geometry1 = Seq(Point(0.0, 0.0), Point(50.0,0.0))
+      val geometry2 = Seq(Point(50.0, 0.0), Point(100.0, 0.0))
+      val newGeometry1 = Seq(Point(0.0, 0.0), Point(100.0, 0.0))
+      val crossingRoadsGeometry = Seq(Point(50.0, 0.0), Point(50.0, 50.0))
+      val orderNumber1 = 1.0
+      val orderNumber2 = 2.0
+      val roadwayNumber1 = Sequences.nextRoadwayNumber
+      val roadwayNumber2 = Sequences.nextRoadwayNumber
+
+      // Create Roadways
+      roadwayDAO.create(Seq(
+        Roadway(Sequences.nextRoadwayId, roadwayNumber1, RoadPart(18344, 1), AdministrativeClass.State, Track.Combined,Discontinuity.EndOfRoad, AddrMRange(0, 100), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9,TerminationCode.NoTermination, DateTime.now().minusDays(1),None),
+        Roadway(Sequences.nextRoadwayId, roadwayNumber2, RoadPart(46021, 1), AdministrativeClass.State, Track.Combined, Discontinuity.EndOfRoad, AddrMRange(0,50), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9, TerminationCode.NoTermination, DateTime.now().minusDays(1), None)
+      ))
+
+      // Create LinearLocations
+      val linearLocationIds = linearLocationDAO.create(Seq(
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkId1, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.RoadAddressCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.JunctionPointCP))),
+          geometry1,LinkGeomSource.NormalLinkInterface,
+          roadwayNumber1,Some(DateTime.now().minusDays(1)), None),
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber2,
+          linkId2, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(50), Some(CalibrationPointType.JunctionPointCP)), CalibrationPointReference(Some(100), Some(CalibrationPointType.RoadAddressCP))),
+          geometry2, LinkGeomSource.NormalLinkInterface,
+          roadwayNumber1, Some(DateTime.now().minusDays(1)), None),
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkId9, 0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.JunctionPointCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.RoadAddressCP))),
+          crossingRoadsGeometry, LinkGeomSource.NormalLinkInterface,
+          roadwayNumber2, Some(DateTime.now().minusDays(1)), None)
+      ))
+      val linearlocations = linearLocationDAO.fetchByIdMassQuery(linearLocationIds)
+
+      // Create RoadwayPoints
+      val roadwayPointId1 = roadwayPointDAO.create(roadwayNumber1, 0, "test")
+      val roadwayPointId5 = roadwayPointDAO.create(roadwayNumber1, 50, "test")
+      val roadwayPointId2 = roadwayPointDAO.create(roadwayNumber1, 100, "test")
+      val roadwayPointId3 = roadwayPointDAO.create(roadwayNumber2, 0, "test")
+      val roadwayPointId4 = roadwayPointDAO.create(roadwayNumber2, 50, "test")
+
+      // RoadAddress Calibration Points
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId1, linkId1, roadwayNumber1, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId2, linkId2, roadwayNumber1, 100,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId4, linkId9, roadwayNumber2, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+
+      // Junction Calibration Points
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId5, linkId1, roadwayNumber1, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.JunctionPointCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId5, linkId2, roadwayNumber1, 50,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.JunctionPointCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId3, linkId9, roadwayNumber2, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.JunctionPointCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+
+
+      // Create TiekamuRoadLinkChanges
+      val tiekamuRoadLinkChanges = Seq(
+        TiekamuRoadLinkChange(linkId1, 0.0, 50.0, newLinkId1, 0.0, 50.0, digitizationChange = false),
+        TiekamuRoadLinkChange(linkId2, 0.0, 50.0, newLinkId1, 50.0, 100.0, digitizationChange = false)
+      )
+
+      // Create KGVRoadLinks
+      val kgvRoadLinks = Seq(
+        RoadLink(linkId1, geometry1, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(linkId2, geometry2, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(linkId9, crossingRoadsGeometry, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(newLinkId1, newGeometry1, 100.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = "")
+      )
+
+      val complementaryLinks = Seq()
+
+      // The testing itself
+      val res = dynamicRoadNetworkService.validateTiekamuRoadLinkChanges(tiekamuRoadLinkChanges,linearlocations, kgvRoadLinks, complementaryLinks)
+      // 2 validation errors
+      // 1 for each link (A and B)
+      // Links cannot be merged together, found linear location(s) that connect(s) between the two links. (Cross road case)
+      res.length should be (2)
+    }
+  }
 }


Reviews:
user: login: marjukka-kokkonen-sitowise
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:44:21Z
user: login: marjukka-kokkonen-sitowise
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:21:46Z
user: login: jounijoh
body: 👍
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:15:07Z
user: login: marjukka-kokkonen-sitowise
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T06:45:23Z
user: login: marjukka-kokkonen-sitowise
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:02:30Z
user: login: marjukka-kokkonen-sitowise
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:59:03Z

Review comments:
diff_hunk: @@ -851,6 +851,21 @@ class LinearLocationDAO extends BaseDAO {
     queryList(query)
   }
 
+  def fetchActiveLinearLocationsWithPoint(point: Point): Seq[LinearLocation] = {
path: viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala
user: login: marjukka-kokkonen-sitowise
body: Tälle joku kommentti, mitä se "withPoint" tekee, ns. mihin tää funkkari tarvitaan.
created_at: 2025-05-05T11:44:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -851,6 +851,21 @@ class LinearLocationDAO extends BaseDAO {
     queryList(query)
   }
 
+  def fetchActiveLinearLocationsWithPoint(point: Point): Seq[LinearLocation] = {
+    val wktPoint = s"POINT(${point.x} ${point.y})"
+    val query =
+      sql"""
+          $selectFromLinearLocation
+          WHERE loc.valid_to IS NULL
+            AND loc.roadway_number IN (
+              SELECT roadway_number FROM roadway
+              WHERE valid_to IS NULL AND end_date IS NULL
+            )
+            AND ST_Intersects(geometry, ST_GeomFromText($wktPoint, 3067))
path: viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala
user: login: marjukka-kokkonen-sitowise
body: Tän ST-intersectsin toleranssi näyttää olevan "a distance tolerance of 0.00001"; onks toi _point_ aina niin tarkka, että viiva osuu tohon?
created_at: 2025-05-05T11:47:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,4 +91,221 @@ class DynamicRoadNetworkServiceSpec extends AnyFunSuite with Matchers{
       res.length should be (0)
     }
   }
+
+  test("When Validating invalid TiekamuRoadLinkChanges (non-homogeneous and discontinuous combine case) Then should return TiekamuRoadLinkErrors") {
+
+    /**
+     * Link A is on road part 18344/1
+     * Link B is on road part 46001/1
+     *
+     * The link changes:
+     *
+     *         Before:
+     *      A         B
+     *  -------->---------->
+     *
+     *          After:
+     *          C
+     *  ------------------->
path: viite-backend/viite-main/src/test/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkServiceSpec.scala
user: login: marjukka-kokkonen-sitowise
body: Tähän kommenttiin voisi vielä tähdentää, että tuo yhtenäinen linkki on tässä tapauksessa viallinen.
created_at: 2025-05-05T11:49:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,4 +91,221 @@ class DynamicRoadNetworkServiceSpec extends AnyFunSuite with Matchers{
       res.length should be (0)
     }
   }
+
+  test("When Validating invalid TiekamuRoadLinkChanges (non-homogeneous and discontinuous combine case) Then should return TiekamuRoadLinkErrors") {
+
+    /**
+     * Link A is on road part 18344/1
+     * Link B is on road part 46001/1
+     *
+     * The link changes:
+     *
+     *         Before:
+     *      A         B
+     *  -------->---------->
+     *
+     *          After:
+     *          C
+     *  ------------------->
+     */
+    runWithRollback {
+      val linkA = "testtest-test-test-test-test:a"
+      val linkB = "testtest-test-test-test-test:b"
+      val newLinkC = "testtest-test-test-test-test:c"
+      val geometryA = Seq(Point(0.0, 0.0), Point(50.0,0.0))
+      val geometryB = Seq(Point(50.0, 0.0), Point(100.0, 0.0))
+      val newGeometryC = Seq(Point(0.0, 0.0), Point(100.0, 0.0))
+      val orderNumber1 = 1.0
+      val roadwayNumber1 = Sequences.nextRoadwayNumber
+      val roadwayNumber2 = Sequences.nextRoadwayNumber
+      val roadPart1 = RoadPart(18344, 1)
+      val roadPart2 = RoadPart(46001, 1)
+
+      // Create Roadways
+      roadwayDAO.create(Seq(
+        Roadway(Sequences.nextRoadwayId, roadwayNumber1, roadPart1, AdministrativeClass.State, Track.Combined,Discontinuity.EndOfRoad, AddrMRange(0, 50), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9,TerminationCode.NoTermination, DateTime.now().minusDays(1),None),
+        Roadway(Sequences.nextRoadwayId, roadwayNumber2, roadPart2, AdministrativeClass.State, Track.Combined,Discontinuity.EndOfRoad, AddrMRange(0, 50), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9,TerminationCode.NoTermination, DateTime.now().minusDays(1),None)
+      ))
+
+      // Create LinearLocations
+      val linearLocationIds = linearLocationDAO.create(Seq(
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkA, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.RoadAddressCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.RoadAddressCP))),
+          geometryA,LinkGeomSource.NormalLinkInterface,
+          roadwayNumber1,Some(DateTime.now().minusDays(1)), None),
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkB, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.RoadAddressCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.RoadAddressCP))),
+          geometryB, LinkGeomSource.NormalLinkInterface,
+          roadwayNumber2, Some(DateTime.now().minusDays(1)), None)
+      ))
+      val linearlocations = linearLocationDAO.fetchByIdMassQuery(linearLocationIds)
+
+      // Create RoadwayPoints
+      val roadwayPointId1 = roadwayPointDAO.create(roadwayNumber1, 0, "test")
+      val roadwayPointId2 = roadwayPointDAO.create(roadwayNumber1, 50, "test")
+      val roadwayPointId3 = roadwayPointDAO.create(roadwayNumber2, 0, "test")
+      val roadwayPointId4 = roadwayPointDAO.create(roadwayNumber2, 50, "test")
+
+      // Create CalibrationPoints
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId1, linkA, roadwayNumber1, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId2, linkA, roadwayNumber1, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId3, linkB, roadwayNumber2, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId4, linkB, roadwayNumber2, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+
+
+      // Create TiekamuRoadLinkChanges
+      val tiekamuRoadLinkChanges = Seq(
+        TiekamuRoadLinkChange(linkA, 0.0, 50.0, newLinkC, 0.0, 50.0, digitizationChange = false),
+        TiekamuRoadLinkChange(linkB, 0.0, 50.0, newLinkC, 50.0, 100.0, digitizationChange = false)
+      )
+
+      // Create KGVRoadLinks
+      val kgvRoadLinks = Seq(
+        RoadLink(linkA, geometryA, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(linkB, geometryB, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(newLinkC, newGeometryC, 100.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = "")
+      )
+
+      val complementaryLinks = Seq()
+
+      // The testing itself
+      val res = dynamicRoadNetworkService.validateTiekamuRoadLinkChanges(tiekamuRoadLinkChanges,linearlocations, kgvRoadLinks, complementaryLinks)
+      // 4 validation errors,
+      // 2 per link (A and B),
+      // "Road address not continuous, cannot merge links together."
+      // "Two or more links with non-homogeneous road addresses (road number, road part number, track) cannot merge together."
+      res.length should be (4)
+    }
+  }
+
+  test("When Validating invalid TiekamuRoadLinkChanges (active linear location prevents combination of two links) Then should return TiekamuRoadLinkErrors") {
+    /**
+     * Link change A + B = C
+     * Y = Active linear location
+     *
+     *          Before:
+     *
+     *    A             B
+     * ----------->------------>
+     *            ^
+     *            |
+     *            | Y
+     *            |
+     *
+     *           After:
+     *
+     *             C
+     * ------------------------->
+     *             ^
+     *             |
+     *             | Y
+     *             |
+     */
+    runWithRollback {
+      val linkId1 = "testtest-test-test-test-test:1"
+      val linkId2 = "testtest-test-test-test-test:2"
+      val linkId9 = "testtest-test-test-test-test:9"
+      val newLinkId1 = "testtest-test-test-test-test:3"
+      val newLinkId2 = "testtest-test-test-test-test:4"
+      val geometry1 = Seq(Point(0.0, 0.0), Point(50.0,0.0))
+      val geometry2 = Seq(Point(50.0, 0.0), Point(100.0, 0.0))
+      val newGeometry1 = Seq(Point(0.0, 0.0), Point(100.0, 0.0))
+      val crossingRoadsGeometry = Seq(Point(50.0, 0.0), Point(50.0, 50.0))
+      val orderNumber1 = 1.0
+      val orderNumber2 = 2.0
+      val roadwayNumber1 = Sequences.nextRoadwayNumber
+      val roadwayNumber2 = Sequences.nextRoadwayNumber
+
+      // Create Roadways
+      roadwayDAO.create(Seq(
+        Roadway(Sequences.nextRoadwayId, roadwayNumber1, RoadPart(18344, 1), AdministrativeClass.State, Track.Combined,Discontinuity.EndOfRoad, AddrMRange(0, 100), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9,TerminationCode.NoTermination, DateTime.now().minusDays(1),None),
+        Roadway(Sequences.nextRoadwayId, roadwayNumber2, RoadPart(46021, 1), AdministrativeClass.State, Track.Combined, Discontinuity.EndOfRoad, AddrMRange(0,50), false, DateTime.now().minusDays(1), None, "test", Some("Test road"), 9, TerminationCode.NoTermination, DateTime.now().minusDays(1), None)
+      ))
+
+      // Create LinearLocations
+      val linearLocationIds = linearLocationDAO.create(Seq(
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkId1, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.RoadAddressCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.JunctionPointCP))),
+          geometry1,LinkGeomSource.NormalLinkInterface,
+          roadwayNumber1,Some(DateTime.now().minusDays(1)), None),
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber2,
+          linkId2, 0.0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(50), Some(CalibrationPointType.JunctionPointCP)), CalibrationPointReference(Some(100), Some(CalibrationPointType.RoadAddressCP))),
+          geometry2, LinkGeomSource.NormalLinkInterface,
+          roadwayNumber1, Some(DateTime.now().minusDays(1)), None),
+        LinearLocation(Sequences.nextLinearLocationId, orderNumber1,
+          linkId9, 0, 50.0,
+          SideCode.TowardsDigitizing, 10000000000L,
+          (CalibrationPointReference(Some(0), Some(CalibrationPointType.JunctionPointCP)), CalibrationPointReference(Some(50), Some(CalibrationPointType.RoadAddressCP))),
+          crossingRoadsGeometry, LinkGeomSource.NormalLinkInterface,
+          roadwayNumber2, Some(DateTime.now().minusDays(1)), None)
+      ))
+      val linearlocations = linearLocationDAO.fetchByIdMassQuery(linearLocationIds)
+
+      // Create RoadwayPoints
+      val roadwayPointId1 = roadwayPointDAO.create(roadwayNumber1, 0, "test")
+      val roadwayPointId5 = roadwayPointDAO.create(roadwayNumber1, 50, "test")
+      val roadwayPointId2 = roadwayPointDAO.create(roadwayNumber1, 100, "test")
+      val roadwayPointId3 = roadwayPointDAO.create(roadwayNumber2, 0, "test")
+      val roadwayPointId4 = roadwayPointDAO.create(roadwayNumber2, 50, "test")
+
+      // RoadAddress Calibration Points
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId1, linkId1, roadwayNumber1, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId2, linkId2, roadwayNumber1, 100,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId4, linkId9, roadwayNumber2, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.RoadAddressCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+
+      // Junction Calibration Points
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId5, linkId1, roadwayNumber1, 50,
+        CalibrationPointLocation.EndOfLink, CalibrationPointType.JunctionPointCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId5, linkId2, roadwayNumber1, 50,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.JunctionPointCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+      CalibrationPointDAO.create(CalibrationPoint(Sequences.nextCalibrationPointId, roadwayPointId3, linkId9, roadwayNumber2, 0,
+        CalibrationPointLocation.StartOfLink, CalibrationPointType.JunctionPointCP,
+        Some(DateTime.now().minusDays(1)), None, "Test", Some(DateTime.now())))
+
+
+      // Create TiekamuRoadLinkChanges
+      val tiekamuRoadLinkChanges = Seq(
+        TiekamuRoadLinkChange(linkId1, 0.0, 50.0, newLinkId1, 0.0, 50.0, digitizationChange = false),
+        TiekamuRoadLinkChange(linkId2, 0.0, 50.0, newLinkId1, 50.0, 100.0, digitizationChange = false)
+      )
+
+      // Create KGVRoadLinks
+      val kgvRoadLinks = Seq(
+        RoadLink(linkId1, geometry1, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(linkId2, geometry2, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(linkId9, crossingRoadsGeometry, 50.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = ""),
+        RoadLink(newLinkId1, newGeometry1, 100.0, AdministrativeClass.State, TrafficDirection.TowardsDigitizing, None, None, LifecycleStatus.InUse, LinkGeomSource.NormalLinkInterface, 9, sourceId = "")
+      )
+
+      val complementaryLinks = Seq()
+
+      // The testing itself
+      val res = dynamicRoadNetworkService.validateTiekamuRoadLinkChanges(tiekamuRoadLinkChanges,linearlocations, kgvRoadLinks, complementaryLinks)
+      res.length should be (2)
path: viite-backend/viite-main/src/test/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkServiceSpec.scala
user: login: marjukka-kokkonen-sitowise
body:  // <why 2>
created_at: 2025-05-05T13:20:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -851,6 +851,21 @@ class LinearLocationDAO extends BaseDAO {
     queryList(query)
   }
 
+  def fetchActiveLinearLocationsWithPoint(point: Point): Seq[LinearLocation] = {
+    val wktPoint = s"POINT(${point.x} ${point.y})"
+    val query =
+      sql"""
+          $selectFromLinearLocation
+          WHERE loc.valid_to IS NULL
+            AND loc.roadway_number IN (
+              SELECT roadway_number FROM roadway
+              WHERE valid_to IS NULL AND end_date IS NULL
+            )
+            AND ST_Intersects(geometry, ST_GeomFromText($wktPoint, 3067))
path: viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala
user: login: marjukka-kokkonen-sitowise
body: Vaaka- ja pystysuorissa viivoissa osuu aina, mutta vinoon menevissä (joita ovat melkein kaikki) ei yleensä osu, jos pointin tarkkuus on esim. Viiteessä preferoitu 0,001.
created_at: 2025-05-05T13:21:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -851,6 +851,21 @@ class LinearLocationDAO extends BaseDAO {
     queryList(query)
   }
 
+  def fetchActiveLinearLocationsWithPoint(point: Point): Seq[LinearLocation] = {
+    val wktPoint = s"POINT(${point.x} ${point.y})"
+    val query =
+      sql"""
+          $selectFromLinearLocation
+          WHERE loc.valid_to IS NULL
+            AND loc.roadway_number IN (
+              SELECT roadway_number FROM roadway
+              WHERE valid_to IS NULL AND end_date IS NULL
+            )
+            AND ST_Intersects(geometry, ST_GeomFromText($wktPoint, 3067))
path: viite-backend/viite-main/src/main/scala/fi/liikennevirasto/viite/dao/LinearLocationDAO.scala
user: login: marjukka-kokkonen-sitowise
body: (jes, fiksattu)
created_at: 2025-05-08T06:45:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -492,13 +502,29 @@ class DynamicRoadNetworkService(linearLocationDAO: LinearLocationDAO, roadwayDAO
             )
 
             if (problematicLinearLocations.nonEmpty) {
-              Some(
-                TiekamuRoadLinkChangeError(
-                  "Links cannot be merged together, found linear location(s) that connect(s) between the two links. (Cross road case)",
-                  change,
-                  getMetaData(change, linearLocationsAtPoint)
-                )
-              )
+              val changes = problematicLinearLocations.flatMap(ll => {
+                val linkId = ll.linkId
+                tiekamuRoadLinkChanges.filter(ch => ch.oldLinkId == linkId)
path: viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala
user: login: marjukka-kokkonen-sitowise
body: tälle kommentti, miks verrataan oldiin ( / miks lopputuloksen nimi on "changes")
created_at: 2025-05-08T06:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -492,13 +502,29 @@ class DynamicRoadNetworkService(linearLocationDAO: LinearLocationDAO, roadwayDAO
             )
 
             if (problematicLinearLocations.nonEmpty) {
-              Some(
-                TiekamuRoadLinkChangeError(
-                  "Links cannot be merged together, found linear location(s) that connect(s) between the two links. (Cross road case)",
-                  change,
-                  getMetaData(change, linearLocationsAtPoint)
-                )
-              )
+              val changes = problematicLinearLocations.flatMap(ll => {
+                val linkId = ll.linkId
+                tiekamuRoadLinkChanges.filter(ch => ch.oldLinkId == linkId)
path: viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala
user: login: marjukka-kokkonen-sitowise
body: ...tai/ja. Itseasiassa, tolle problematicLinearLocations tossa ylempänä vois lisätä selitteen, miks ne on ongelmallisia
created_at: 2025-05-08T06:58:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -492,13 +502,29 @@ class DynamicRoadNetworkService(linearLocationDAO: LinearLocationDAO, roadwayDAO
             )
 
             if (problematicLinearLocations.nonEmpty) {
-              Some(
-                TiekamuRoadLinkChangeError(
-                  "Links cannot be merged together, found linear location(s) that connect(s) between the two links. (Cross road case)",
-                  change,
-                  getMetaData(change, linearLocationsAtPoint)
-                )
-              )
+              val changes = problematicLinearLocations.flatMap(ll => {
+                val linkId = ll.linkId
+                tiekamuRoadLinkChanges.filter(ch => ch.oldLinkId == linkId)
+              })
+
+              if (changes.nonEmpty) { // If there are changes to the problematic linear locations then we need further validation
+                // Fetch the new links
+                val kgvLinksForChanges = kgvClient.roadLinkVersionsData.fetchByLinkIds(changes.map(_.newLinkId).toSet)
+                // Get the start and end points
+                val startAndEndPoints = kgvLinksForChanges.flatMap(kgvLink => {
+                  Seq(kgvLink.geometry.head, kgvLink.geometry.last)
+                })
+
+                if (startAndEndPoints.contains(connectingPoint)){ // If the connecting point still has a linear location connected after the days link changes
path: viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala
user: login: marjukka-kokkonen-sitowise
body: (Excellent commenting.)
created_at: 2025-05-08T06:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -492,13 +502,29 @@ class DynamicRoadNetworkService(linearLocationDAO: LinearLocationDAO, roadwayDAO
             )
 
             if (problematicLinearLocations.nonEmpty) {
-              Some(
-                TiekamuRoadLinkChangeError(
-                  "Links cannot be merged together, found linear location(s) that connect(s) between the two links. (Cross road case)",
-                  change,
-                  getMetaData(change, linearLocationsAtPoint)
-                )
-              )
+              val changes = problematicLinearLocations.flatMap(ll => {
+                val linkId = ll.linkId
+                tiekamuRoadLinkChanges.filter(ch => ch.oldLinkId == linkId)
path: viite-backend/viite-main/src/main/scala/fi/vaylavirasto/viite/dynamicnetwork/DynamicRoadNetworkService.scala
user: login: marjukka-kokkonen-sitowise
body: !!!😄 

👌 💯 

created_at: 2025-05-08T10:58:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: sasuolanderSito
created_at: 2025-05-08T11:02:38Z
body: Pitäiskö olla testi tilanteessa jossa ei ole virhettä ?, Tässä kommentoin sivusta.
author_association: NONE
user: login: marjukka-kokkonen-sitowise
created_at: 2025-05-08T11:10:50Z
body: Ei päästä tällä vielä siihen.

Bulletproofille ratkaisulle keksittiin toistaiseksi vain sellainen toteutus, jossa tarttee mallintaa koko verkko, eli käytännössä Viite 2.0, niin ei lähdetty tekemään sitä nyt. 😄 
author_association: CONTRIBUTOR
user: login: marjukka-kokkonen-sitowise
created_at: 2025-05-08T11:11:45Z
body: Eiku, en ehkä ymmärtäny kysymystä.
author_association: CONTRIBUTOR
user: login: sasuolanderSito
created_at: 2025-05-08T11:19:24Z
body: > Eiku, en ehkä ymmärtäny kysymystä.

Aattelin vaan että kansii testata negatiivinen ja positiivinen tulos. Olis testien kautta määritetty missä risteys on oikein.
author_association: NONE