diff --git a/buildSrc/src/main/kotlin/Configuration.kt b/buildSrc/src/main/kotlin/Configuration.kt index 05261ad..685c6b5 100644 --- a/buildSrc/src/main/kotlin/Configuration.kt +++ b/buildSrc/src/main/kotlin/Configuration.kt @@ -23,7 +23,6 @@ object Project { object ProjectComponents { // user interface layer const val main = ":rtron-main" - const val documentation = ":rtron-documentation" // model transformation layer const val model = ":rtron-model" diff --git a/rtron-cli/src/main/kotlin/io/rtron/cli/SubcommandOpendriveToCitygml.kt b/rtron-cli/src/main/kotlin/io/rtron/cli/SubcommandOpendriveToCitygml.kt index 55b947c..9c7a87f 100644 --- a/rtron-cli/src/main/kotlin/io/rtron/cli/SubcommandOpendriveToCitygml.kt +++ b/rtron-cli/src/main/kotlin/io/rtron/cli/SubcommandOpendriveToCitygml.kt @@ -81,6 +81,7 @@ class SubcommandOpendriveToCitygml : CliktCommand( help = "warning angle tolerance between two geometry elements in the plan view", ).double() .default(OpendriveEvaluatorParameters.DEFAULT_PLAN_VIEW_GEOMETRY_ANGLE_WARNING_TOLERANCE) + private val reprojectModel by option(help = "reproject the geometries into a different geospatial coordinate reference system").flag() private val crsEpsg by option(help = "EPSG code of the coordinate reference system used in the OpenDRIVE datasets").int() .default(Opendrive2RoadspacesParameters.DEFAULT_CRS_EPSG) private val addOffset by option(help = "offset values by which the model is translated along the x, y, and z axis").double().triple() @@ -125,6 +126,7 @@ class SubcommandOpendriveToCitygml : CliktCommand( planViewGeometryDistanceWarningTolerance = planViewGeometryDistanceWarningTolerance, planViewGeometryAngleTolerance = planViewGeometryAngleTolerance, planViewGeometryAngleWarningTolerance = planViewGeometryAngleWarningTolerance, + reprojectModel = reprojectModel, crsEpsg = crsEpsg, offsetX = addOffset.first, offsetY = addOffset.second, diff --git a/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlParameters.kt b/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlParameters.kt index c19609c..2fc9048 100644 --- a/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlParameters.kt +++ b/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlParameters.kt @@ -28,6 +28,7 @@ import io.rtron.transformer.evaluator.roadspaces.RoadspacesEvaluatorParameters import io.rtron.transformer.modifiers.opendrive.cropper.OpendriveCropperParameters import io.rtron.transformer.modifiers.opendrive.offset.adder.OpendriveOffsetAdderParameters import io.rtron.transformer.modifiers.opendrive.remover.OpendriveObjectRemoverParameters +import io.rtron.transformer.modifiers.opendrive.reprojector.OpendriveReprojectorParameters import kotlinx.serialization.Serializable @Serializable @@ -42,6 +43,7 @@ data class OpendriveToCitygmlParameters( OpendriveEvaluatorParameters.DEFAULT_PLAN_VIEW_GEOMETRY_ANGLE_TOLERANCE, val planViewGeometryAngleWarningTolerance: Double = OpendriveEvaluatorParameters.DEFAULT_PLAN_VIEW_GEOMETRY_ANGLE_WARNING_TOLERANCE, + val reprojectModel: Boolean = OpendriveReprojectorParameters.DEFAULT_REPROJECT_MODEL, val crsEpsg: Int = Opendrive2RoadspacesParameters.DEFAULT_CRS_EPSG, val offsetX: Double = OpendriveOffsetAdderParameters.DEFAULT_OFFSET_X, val offsetY: Double = OpendriveOffsetAdderParameters.DEFAULT_OFFSET_Y, @@ -102,6 +104,13 @@ data class OpendriveToCitygmlParameters( offsetHeading = OpendriveOffsetAdderParameters.DEFAULT_OFFSET_HEADING, ) + fun deriveOpendriveReprojectorParameters() = + OpendriveReprojectorParameters( + reprojectModel = reprojectModel, + targetCrsEpsg = crsEpsg, + deviationWarningTolerance = OpendriveReprojectorParameters.DEFAULT_DEVIATION_WARNING_TOLERANCE, + ) + fun deriveOpendriveCropperParameters() = OpendriveCropperParameters( numberTolerance = tolerance, diff --git a/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlProcessor.kt b/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlProcessor.kt index d45f630..53dec46 100644 --- a/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlProcessor.kt +++ b/rtron-main/src/main/kotlin/io/rtron/main/processor/OpendriveToCitygmlProcessor.kt @@ -35,6 +35,7 @@ import io.rtron.transformer.modifiers.opendrive.cropper.OpendriveCropper import io.rtron.transformer.modifiers.opendrive.offset.adder.OpendriveOffsetAdder import io.rtron.transformer.modifiers.opendrive.offset.resolver.OpendriveOffsetResolver import io.rtron.transformer.modifiers.opendrive.remover.OpendriveObjectRemover +import io.rtron.transformer.modifiers.opendrive.reprojector.OpendriveReprojector import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.createDirectories @@ -95,9 +96,19 @@ class OpendriveToCitygmlProcessor( return@processAllFiles } + // reproject OpenDRIVE model + val opendriveReprojector = OpendriveReprojector(parameters.deriveOpendriveReprojectorParameters()) + val opendriveReprojectorResult = opendriveReprojector.modify(modifiedOpendriveModel) + opendriveReprojectorResult.second.serializeToJsonFile(outputSubDirectoryPath / OPENDRIVE_REPROJECTOR_REPORT_PATH) + val opendriveReprojected = + opendriveReprojectorResult.first.handleEmpty { + logger.warn { "OpendriveReprojector: ${opendriveReprojectorResult.second.message}" } + return@processAllFiles + } + // offset OpenDRIVE model val opendriveOffsetAdder = OpendriveOffsetAdder(parameters.deriveOpendriveOffsetAdderParameters()) - val opendriveOffsetAddedResult = opendriveOffsetAdder.modify(modifiedOpendriveModel) + val opendriveOffsetAddedResult = opendriveOffsetAdder.modify(opendriveReprojected) opendriveOffsetAddedResult.second.serializeToJsonFile(outputSubDirectoryPath / OPENDRIVE_OFFSET_ADDER_REPORT_PATH) // resolve the offset @@ -158,12 +169,13 @@ class OpendriveToCitygmlProcessor( val REPORTS_PATH = Path("reports") val OPENDRIVE_SCHEMA_VALIDATOR_REPORT_PATH = REPORTS_PATH / Path("01_opendrive_schema_validator_report.json") val OPENDRIVE_EVALUATOR_REPORT_PATH = REPORTS_PATH / Path("02_opendrive_evaluator_report.json") - val OPENDRIVE_OFFSET_ADDER_REPORT_PATH = REPORTS_PATH / Path("03_opendrive_offset_adder_report.json") - val OPENDRIVE_OFFSET_RESOLVER_REPORT_PATH = REPORTS_PATH / Path("04_opendrive_offset_resolver_report.json") - val OPENDRIVE_CROP_REPORT_PATH = REPORTS_PATH / Path("05_opendrive_crop_report.json") - val OPENDRIVE_OBJECT_REMOVER_REPORT_PATH = REPORTS_PATH / Path("06_opendrive_object_remover_report.json") - val OPENDRIVE_TO_ROADSPACES_REPORT_PATH = REPORTS_PATH / Path("07_opendrive_to_roadspaces_report.json") - val ROADSPACES_EVALUATOR_REPORT_PATH = REPORTS_PATH / Path("08_roadspaces_evaluator_report.json") - val ROADSPACES_TO_CITYGML_REPORT_PATH = REPORTS_PATH / Path("09_roadspaces_to_citygml_report.json") + val OPENDRIVE_REPROJECTOR_REPORT_PATH = REPORTS_PATH / Path("03_opendrive_reprojector_report.json") + val OPENDRIVE_OFFSET_ADDER_REPORT_PATH = REPORTS_PATH / Path("04_opendrive_offset_adder_report.json") + val OPENDRIVE_OFFSET_RESOLVER_REPORT_PATH = REPORTS_PATH / Path("05_opendrive_offset_resolver_report.json") + val OPENDRIVE_CROP_REPORT_PATH = REPORTS_PATH / Path("06_opendrive_crop_report.json") + val OPENDRIVE_OBJECT_REMOVER_REPORT_PATH = REPORTS_PATH / Path("07_opendrive_object_remover_report.json") + val OPENDRIVE_TO_ROADSPACES_REPORT_PATH = REPORTS_PATH / Path("08_opendrive_to_roadspaces_report.json") + val ROADSPACES_EVALUATOR_REPORT_PATH = REPORTS_PATH / Path("09_roadspaces_evaluator_report.json") + val ROADSPACES_TO_CITYGML_REPORT_PATH = REPORTS_PATH / Path("10_roadspaces_to_citygml_report.json") } } diff --git a/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/Rotation2D.kt b/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/Rotation2D.kt index 6b1447f..b8b1c8e 100644 --- a/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/Rotation2D.kt +++ b/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/Rotation2D.kt @@ -100,6 +100,8 @@ class Rotation2D( roll: Double = 0.0, ) = Rotation3D(angle, pitch, roll) + override fun toString(): String = "Rotation2D(angle=$angle)" + companion object { val ZERO = Rotation2D(0.0) diff --git a/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/point/Vector2D.kt b/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/point/Vector2D.kt index 300cc56..583cf13 100644 --- a/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/point/Vector2D.kt +++ b/rtron-math/src/main/kotlin/io/rtron/math/geometry/euclidean/twod/point/Vector2D.kt @@ -18,6 +18,7 @@ package io.rtron.math.geometry.euclidean.twod.point import io.rtron.math.geometry.euclidean.threed.point.Vector3D import io.rtron.math.geometry.euclidean.twod.Rotation2D +import io.rtron.math.linear.RealVector import io.rtron.math.std.fuzzyEquals as doubleFuzzyEquals import org.apache.commons.math3.geometry.euclidean.twod.Vector2D as CMVector2D import org.joml.Vector2d as JOMLVector2D @@ -109,6 +110,8 @@ data class Vector2D( fun toVector2DJOML() = JOMLVector2D(this.x, this.y) + fun toRealVector() = RealVector(doubleArrayOf(this.x, this.y)) + companion object { val ZERO = Vector2D(0.0, 0.0) val X_AXIS = Vector2D(1.0, 0.0) diff --git a/rtron-math/src/main/kotlin/io/rtron/math/linear/RealMatrix.kt b/rtron-math/src/main/kotlin/io/rtron/math/linear/RealMatrix.kt index 340a5f0..9f892be 100644 --- a/rtron-math/src/main/kotlin/io/rtron/math/linear/RealMatrix.kt +++ b/rtron-math/src/main/kotlin/io/rtron/math/linear/RealMatrix.kt @@ -17,6 +17,7 @@ package io.rtron.math.linear import io.rtron.math.geometry.euclidean.threed.point.Vector3D +import io.rtron.math.geometry.euclidean.twod.point.Vector2D import io.rtron.math.std.reshapeByColumnDimension import org.apache.commons.math3.linear.Array2DRowRealMatrix import org.apache.commons.math3.linear.MatrixUtils @@ -195,6 +196,14 @@ class RealMatrix( return RealMatrix(matrixValues.toTypedArray()) } + /** + * Creates a [RealMatrix] from a list of 3D vectors + * + * @param vectors list of 3D vectors whereby each vector will be represented as a row + */ + @JvmName("ofListVector2D") + fun of(vectors: List): RealMatrix = RealMatrix(vectors.map { it.toRealVector() }) + /** * Creates a [RealMatrix] from a list of 3D vectors * diff --git a/rtron-math/src/main/kotlin/io/rtron/math/processing/Vector2DListExtensions.kt b/rtron-math/src/main/kotlin/io/rtron/math/processing/Vector2DListExtensions.kt index 6164971..27cd5dc 100644 --- a/rtron-math/src/main/kotlin/io/rtron/math/processing/Vector2DListExtensions.kt +++ b/rtron-math/src/main/kotlin/io/rtron/math/processing/Vector2DListExtensions.kt @@ -17,3 +17,9 @@ fun List.isClockwiseOrdered(): Boolean { * Returns true, if the list of [Vector2D] has an anti-clockwise order. */ fun List.isCounterClockwiseOrdered(): Boolean = !this.isClockwiseOrdered() + +/** + * Calculates the centroid of a list of [Vector2D]. + * See the wikipedia article of [Centroid](https://en.wikipedia.org/wiki/Centroid). + */ +fun List.calculateCentroid(): Vector2D = this.reduce { sum, point -> sum + point }.div(this.size.toDouble()) diff --git a/rtron-math/src/main/kotlin/io/rtron/math/transform/Affine2D.kt b/rtron-math/src/main/kotlin/io/rtron/math/transform/Affine2D.kt index 980df06..19202f2 100644 --- a/rtron-math/src/main/kotlin/io/rtron/math/transform/Affine2D.kt +++ b/rtron-math/src/main/kotlin/io/rtron/math/transform/Affine2D.kt @@ -148,6 +148,23 @@ class Affine2D( return Affine2D(matrix) } + /** + * Creates an [Affine2D] transformation matrix from a [matrix] with column and row dimension of 2. + */ + fun of(matrix: RealMatrix): Affine2D { + require(matrix.columnDimension == 2) { "Wrong column dimension ${matrix.columnDimension}." } + require(matrix.rowDimension == 2) { "Wrong row dimension ${matrix.rowDimension}." } + require(matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0] > 0.0) { + "Determinant must be greater than zero, since it must not be reflection." + } + val jomlMatrix = JOMLMatrix3d() + jomlMatrix.m00 = matrix[0][0] + jomlMatrix.m01 = matrix[1][0] + jomlMatrix.m10 = matrix[0][1] + jomlMatrix.m11 = matrix[1][1] + return Affine2D(jomlMatrix) + } + /** * Creates an [Affine2D] transformation matrix from a [rotation]. */ diff --git a/rtron-model/src/main/kotlin/io/rtron/model/opendrive/road/planview/RoadPlanViewGeometry.kt b/rtron-model/src/main/kotlin/io/rtron/model/opendrive/road/planview/RoadPlanViewGeometry.kt index 17766ff..da5ea36 100644 --- a/rtron-model/src/main/kotlin/io/rtron/model/opendrive/road/planview/RoadPlanViewGeometry.kt +++ b/rtron-model/src/main/kotlin/io/rtron/model/opendrive/road/planview/RoadPlanViewGeometry.kt @@ -19,6 +19,9 @@ package io.rtron.model.opendrive.road.planview import arrow.core.None import arrow.core.Option import arrow.optics.optics +import io.rtron.math.geometry.euclidean.twod.Pose2D +import io.rtron.math.geometry.euclidean.twod.Rotation2D +import io.rtron.math.geometry.euclidean.twod.point.Vector2D import io.rtron.model.opendrive.core.OpendriveElement @optics @@ -35,4 +38,6 @@ data class RoadPlanViewGeometry( var y: Double = Double.NaN, ) : OpendriveElement() { companion object + + fun getPose(): Pose2D = Pose2D(Vector2D(x, y), Rotation2D(hdg)) } diff --git a/rtron-transformer/build.gradle.kts b/rtron-transformer/build.gradle.kts index c72fbbe..3c5986d 100644 --- a/rtron-transformer/build.gradle.kts +++ b/rtron-transformer/build.gradle.kts @@ -22,5 +22,7 @@ dependencies { implementation(Dependencies.slf4jSimple) // geo libraries + implementation(Dependencies.proj4) + implementation(Dependencies.proj4Epsg) implementation(Dependencies.citygml4jXml) } diff --git a/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojector.kt b/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojector.kt new file mode 100644 index 0000000..2868e2b --- /dev/null +++ b/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojector.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2019-2024 Chair of Geoinformatics, Technical University of Munich + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rtron.transformer.modifiers.opendrive.reprojector + +import arrow.core.None +import arrow.core.Option +import arrow.core.Some +import arrow.core.getOrElse +import arrow.core.toOption +import io.github.oshai.kotlinlogging.KotlinLogging +import io.rtron.math.geometry.euclidean.twod.point.Vector2D +import io.rtron.math.linear.RealMatrix +import io.rtron.math.linear.SingularValueDecomposition +import io.rtron.math.processing.calculateCentroid +import io.rtron.math.transform.Affine2D +import io.rtron.model.opendrive.OpendriveModel +import io.rtron.model.opendrive.additions.extensions.updateAdditionalIdentifiers +import io.rtron.model.opendrive.additions.optics.everyRoadPlanViewGeometry +import io.rtron.model.opendrive.core.HeaderGeoReference +import org.locationtech.proj4j.CRSFactory +import org.locationtech.proj4j.CoordinateReferenceSystem +import org.locationtech.proj4j.CoordinateTransform +import org.locationtech.proj4j.CoordinateTransformFactory +import org.locationtech.proj4j.ProjCoordinate + +class OpendriveReprojector( + val parameters: OpendriveReprojectorParameters, +) { + // Properties and Initializers + private val logger = KotlinLogging.logger {} + + // Methods + + fun modify(opendriveModel: OpendriveModel): Pair, OpendriveReprojectorReport> { + val report = OpendriveReprojectorReport(parameters) + var modifiedOpendriveModel = opendriveModel.copy() + modifiedOpendriveModel.updateAdditionalIdentifiers() + + if (!parameters.reprojectModel) { + report.message = "No reprojection configured." + return Some(modifiedOpendriveModel) to report + } + if (parameters.getTargetCrsEpsg().isNone()) { + report.message = "No target coordinate reference system EPSG code configured." + return None to report + } + if (modifiedOpendriveModel.header.geoReference.isNone()) { + report.message = "No source georeference defined in OpenDRIVE model." + return None to report + } + + // setup coordinate transform + val crsFactory = CRSFactory() + val sourceCrs: CoordinateReferenceSystem = + try { + crsFactory.createFromParameters( + "source", + modifiedOpendriveModel.header.geoReference.getOrNull()!!.content, + ) + } catch (e: Exception) { + report.message = "Source CRS could not be identified: " + e.message.toOption().getOrElse { "" } + return None to report + } + val targetCrs = crsFactory.createFromName("epsg:${parameters.targetCrsEpsg}") + val ctFactory = CoordinateTransformFactory() + val coordinateTransform: CoordinateTransform = ctFactory.createTransform(sourceCrs, targetCrs) + + // reproject extracted geometry points + val planViewSourceCoordinates = + modifiedOpendriveModel.road + .flatMap { it.planView.geometry } + .map { ProjCoordinate(it.x, it.y) } + val planViewTargetCoordinates = + planViewSourceCoordinates.map { + val result = ProjCoordinate() + coordinateTransform.transform(it, result) + result + } + + // estimate rigid affine transform + val (affine, maximumDeviation) = estimateRigidAffine(planViewSourceCoordinates, planViewTargetCoordinates) + report.maximumDeviation = maximumDeviation + if (maximumDeviation > parameters.deviationWarningTolerance) { + logger.warn { + "Maximum deviation is $maximumDeviation and thus exceeding the warning threshold " + + "of ${parameters.deviationWarningTolerance}." + } + } + + // set new georeference and apply affine transform to the model + modifiedOpendriveModel.header.geoReference = Some(HeaderGeoReference(targetCrs.parameterString)) + everyRoadPlanViewGeometry.modify(modifiedOpendriveModel) { currentPlanViewGeometry -> + val transformedPose = affine.transform(currentPlanViewGeometry.getPose()) + currentPlanViewGeometry.x = transformedPose.point.x + currentPlanViewGeometry.y = transformedPose.point.y + currentPlanViewGeometry.hdg = transformedPose.rotation.angle + currentPlanViewGeometry + } + + report.message = "Reprojection of translation with ${affine.extractTranslation()} and " + + "rotation with ${affine.extractRotation()} successfully applied. " + return Some(modifiedOpendriveModel) to report + } + + /** + * Estimates a rigid affine matrix from two lists of corresponding coordinates. + * It uses a singular value decomposition for estimating the rotation. + * + * Further details can be found [here](https://nghiaho.com/?page_id=671). + */ + private fun estimateRigidAffine( + sourceCoordinates: List, + targetCoordinates: List, + ): Pair { + val sourcePoints = sourceCoordinates.map { Vector2D(it.x, it.y) } + val targetPoints = targetCoordinates.map { Vector2D(it.x, it.y) } + + // calculate centriods and local + val sourcePointsCentroid = sourcePoints.calculateCentroid() + val localSourcePoints = sourcePoints.map { it - sourcePointsCentroid } + val targetPointsCentroid = targetPoints.calculateCentroid() + val localTargetPoints = targetPoints.map { it - targetPointsCentroid } + + // calculate covariance + val sourceMatrix = RealMatrix.of(localSourcePoints).transpose() + val targetMatrix = RealMatrix.of(localTargetPoints) + val covariance = sourceMatrix.multiply(targetMatrix) + + // estimate affine transform matrix + val svd = SingularValueDecomposition(covariance) + val rotationAffine = Affine2D.of(svd.matrixVT.multiply(svd.matrixU.transpose())) + val translationAffine = Affine2D.of(targetPointsCentroid - rotationAffine.transform(sourcePointsCentroid)) + val affine = translationAffine.append(rotationAffine) + + // determine maximum deviation between rigid affine and geospatial projection + val maximumDeviation = + sourcePoints.zip(targetPoints).maxOfOrNull { + affine.transform(it.first).distance(it.second) + }!! + + return affine to maximumDeviation + } +} diff --git a/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorParameters.kt b/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorParameters.kt new file mode 100644 index 0000000..e7956b9 --- /dev/null +++ b/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorParameters.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2024 Chair of Geoinformatics, Technical University of Munich + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rtron.transformer.modifiers.opendrive.reprojector + +import arrow.core.None +import arrow.core.Some +import kotlinx.serialization.Serializable + +@Serializable +data class OpendriveReprojectorParameters( + /** perform the geospatial reprojection of the model geometries */ + val reprojectModel: Boolean, + /** [EPSG code](https://en.wikipedia.org/wiki/EPSG_Geodetic_Parameter_Dataset) of the target coordinate reference system (obligatory for working with GIS applications) */ + val targetCrsEpsg: Int, + /** tolerance threshold when it t */ + val deviationWarningTolerance: Double, +) { + fun getTargetCrsEpsg() = if (targetCrsEpsg == 0) None else Some(targetCrsEpsg) + + companion object { + const val DEFAULT_REPROJECT_MODEL = false + const val DEFAULT_TARGET_CRS_EPSG = 0 + const val DEFAULT_DEVIATION_WARNING_TOLERANCE = 0.03 + } +} diff --git a/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorReport.kt b/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorReport.kt new file mode 100644 index 0000000..74b2b33 --- /dev/null +++ b/rtron-transformer/src/main/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorReport.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2024 Chair of Geoinformatics, Technical University of Munich + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rtron.transformer.modifiers.opendrive.reprojector + +import kotlinx.serialization.Serializable + +@Serializable +data class OpendriveReprojectorReport( + val parameters: OpendriveReprojectorParameters, + var message: String = "", + var maximumDeviation: Double = 0.0, +) diff --git a/rtron-transformer/src/test/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorTest.kt b/rtron-transformer/src/test/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorTest.kt new file mode 100644 index 0000000..2dc3037 --- /dev/null +++ b/rtron-transformer/src/test/kotlin/io/rtron/transformer/modifiers/opendrive/reprojector/OpendriveReprojectorTest.kt @@ -0,0 +1,31 @@ +package io.rtron.transformer.modifiers.opendrive.reprojector + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.shouldBe +import org.locationtech.proj4j.CRSFactory +import org.locationtech.proj4j.CoordinateReferenceSystem +import org.locationtech.proj4j.CoordinateTransform +import org.locationtech.proj4j.CoordinateTransformFactory +import org.locationtech.proj4j.ProjCoordinate + +class OpendriveReprojectorTest : FunSpec({ + + test("basic coordinate projection works") { + val crsFactory = CRSFactory() + val sourceCrs: CoordinateReferenceSystem = + crsFactory.createFromParameters( + "WGS84", + "+proj=tmerc +lat_0=48.1485460905528 +lon_0=11.5679503890009 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs", + ) + val targetCrs = crsFactory.createFromName("epsg:25832") + val wgsToUtm: CoordinateTransform = CoordinateTransformFactory().createTransform(sourceCrs, targetCrs) + val sourceCoordinate = ProjCoordinate(167.3432219777634, -277.563889501929, 0.15239999999999998) + + val result = ProjCoordinate() + wgsToUtm.transform(sourceCoordinate, result) + + result.x.shouldBe(691176.5699790819 plusOrMinus 0.000001) + result.y shouldBe(5335728.14692931 plusOrMinus 0.000001) + } +})