From 1d35bd417caf25a98837bd95ed5c457bbe5c03aa Mon Sep 17 00:00:00 2001 From: Crosson David Date: Wed, 24 Jul 2024 09:50:38 +0200 Subject: [PATCH] Add distance between two places related to #12 --- .../fr/janalyse/sotohp/model/PhotoPlace.scala | 60 +++++++++++++++-- .../sotohp/model/PhotoPlaceSpec.scala | 65 +++++++++++++------ 2 files changed, 102 insertions(+), 23 deletions(-) diff --git a/modules/model/src/main/scala/fr/janalyse/sotohp/model/PhotoPlace.scala b/modules/model/src/main/scala/fr/janalyse/sotohp/model/PhotoPlace.scala index ca889e0..03a9bf8 100644 --- a/modules/model/src/main/scala/fr/janalyse/sotohp/model/PhotoPlace.scala +++ b/modules/model/src/main/scala/fr/janalyse/sotohp/model/PhotoPlace.scala @@ -29,12 +29,14 @@ object DecimalDegrees { } extension (dd: LatitudeDecimalDegrees) { - @targetName("doubleValue_latitude") def doubleValue: Double = dd + def toRadians: Double = dd.toRadians } extension (dd: LongitudeDecimalDegrees) { @targetName("doubleValue_longitude") def doubleValue: Double = dd + @targetName("doubleValue_toRadians") + def toRadians: Double = dd.toRadians } } @@ -106,15 +108,15 @@ case class PhotoPlace( latitude: LatitudeDecimalDegrees, longitude: LongitudeDecimalDegrees, altitude: Option[AltitudeMeanSeaLevel], - deducted: Boolean + deducted: Boolean ) object PhotoPlace { def apply( latitudeDMS: LatitudeDegreeMinuteSeconds, longitudeDMS: LongitudeDegreeMinuteSeconds, - altitudeMeanSeaLevel: Option[AltitudeMeanSeaLevel], - deducted: Boolean + altitudeMeanSeaLevel: Option[AltitudeMeanSeaLevel] = None, + deducted: Boolean = false ): PhotoPlace = { PhotoPlace( latitudeDMS.toDecimalDegrees, @@ -123,4 +125,54 @@ object PhotoPlace { deducted ) } + + def fromDecimalDegrees( + latitudeDMS: LatitudeDecimalDegrees, + longitudeDMS: LongitudeDecimalDegrees, + altitudeMeanSeaLevel: Option[AltitudeMeanSeaLevel] = None, + deducted: Boolean = false + ): PhotoPlace = { + PhotoPlace( + latitudeDMS, + longitudeDMS, + altitudeMeanSeaLevel, + deducted + ) + } + + def fromLocationSpecs( + latitudeSpec: String, + longitudeSpec: String, + altitudeMeanSeaLevel: Option[AltitudeMeanSeaLevel] = None, + deducted: Boolean = false + ): Try[PhotoPlace] = { + for { + latitudeDMS <- LatitudeDegreeMinuteSeconds.fromSpec(latitudeSpec) + longitudeDMS <- LongitudeDegreeMinuteSeconds.fromSpec(longitudeSpec) + } yield PhotoPlace( + latitudeDMS, + longitudeDMS, + altitudeMeanSeaLevel, + deducted + ) + } + + extension (from: PhotoPlace) { + def distanceTo(to: PhotoPlace): Double = { + val earthRadius = 6371000d + val deltaLatitude = to.latitude.toRadians - from.latitude.toRadians + val deltaLongitude = to.longitude.toRadians - from.longitude.toRadians + + val a = + Math.sin(deltaLatitude / 2) * Math.sin(deltaLatitude / 2) + + Math.sin(deltaLongitude / 2) * Math.sin(deltaLongitude / 2) * + Math.cos(from.latitude.toRadians) * + Math.cos(to.latitude.toRadians) + + val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + earthRadius * c + } + } + } diff --git a/modules/model/src/test/scala/fr/janalyse/sotohp/model/PhotoPlaceSpec.scala b/modules/model/src/test/scala/fr/janalyse/sotohp/model/PhotoPlaceSpec.scala index f5e59c3..de3237d 100644 --- a/modules/model/src/test/scala/fr/janalyse/sotohp/model/PhotoPlaceSpec.scala +++ b/modules/model/src/test/scala/fr/janalyse/sotohp/model/PhotoPlaceSpec.scala @@ -4,7 +4,7 @@ import zio.* import zio.ZIO.* import zio.test.* import fr.janalyse.sotohp.model.DegreeMinuteSeconds.* -import fr.janalyse.sotohp.model.DecimalDegrees.* +import fr.janalyse.sotohp.model.DecimalDegrees.{LatitudeDecimalDegrees, *} import scala.util.{Success, Try} @@ -19,7 +19,7 @@ object PhotoPlaceSpec extends ZIOSpecDefault { TestDataSet("from wikipedia decimal places 5 case", "0° 00′ 0.036″ N", 9.999999999999999e-6), TestDataSet("alternative representation 1", "3°58'24\" S", -3.9733333333333336d), TestDataSet("alternative representation 2", "03°58'24\" S", -3.9733333333333336d), - //TestDataSet("alternative representation 3", "-3°58'24\" S", -3.9733333333333336d), // TODO Check the meaning of negative values in DMS part + // TestDataSet("alternative representation 3", "-3°58'24\" S", -3.9733333333333336d), // TODO Check the meaning of negative values in DMS part TestDataSet("alternative representation 4", "3° 58' 24\" S", -3.9733333333333336d), TestDataSet("alternative representation 5", "3° 58' 24'' S", -3.9733333333333336d), TestDataSet("alternative representation 6", "3° 58' 24″ S", -3.9733333333333336d), @@ -34,28 +34,55 @@ object PhotoPlaceSpec extends ZIOSpecDefault { ) override def spec = - suite("Degrees minutes seconds")( - suite("for latitude")( - for { - TestDataSet(testName, givenDMSSpec, expectedDegrees) <- latitudeTestDataset - } yield test(testName)( + suite("PhotoPlace Test Suite")( + suite("DegreesMinutesSeconds features")( + suite("should support various encoding for latitude")( for { - dms <- from(LatitudeDegreeMinuteSeconds.fromSpec(givenDMSSpec)) - } yield assertTrue( - dms.toDecimalDegrees == LatitudeDecimalDegrees(expectedDegrees) + TestDataSet(testName, givenDMSSpec, expectedDegrees) <- latitudeTestDataset + } yield test(testName)( + for { + dms <- from(LatitudeDegreeMinuteSeconds.fromSpec(givenDMSSpec)) + } yield assertTrue( + dms.toDecimalDegrees == LatitudeDecimalDegrees(expectedDegrees) + ) ) - ) - ), - suite("for longitude")( - for { - TestDataSet(testName, givenDMSSpec, expectedDegrees) <- longitudeTestDataSet - } yield test(testName)( + ), + suite("should support various encoding for latitude")( for { - dms <- from(LongitudeDegreeMinuteSeconds.fromSpec(givenDMSSpec)) - } yield assertTrue( - dms.toDecimalDegrees == LongitudeDecimalDegrees(expectedDegrees) + TestDataSet(testName, givenDMSSpec, expectedDegrees) <- longitudeTestDataSet + } yield test(testName)( + for { + dms <- from(LongitudeDegreeMinuteSeconds.fromSpec(givenDMSSpec)) + } yield assertTrue( + dms.toDecimalDegrees == LongitudeDecimalDegrees(expectedDegrees) + ) ) ) + ), + suite("Distance features")( + test("should return zero when the same place is given") { + val from = PhotoPlace.fromDecimalDegrees(LatitudeDecimalDegrees(0d), LongitudeDecimalDegrees(0d)) + val to = from + assertTrue( + from.distanceTo(to) == 0 + ) + }, + test("should return the right distance between two places") { + ZIO.fromTry( + for { + paris <- PhotoPlace.fromLocationSpecs("48° 51' 52.9776'' N", "2° 20' 56.4504'' E") + brest <- PhotoPlace.fromLocationSpecs("48° 23' 23.9964'' N", "4° 29' 24.0000'' W") + dist1 = paris.distanceTo(brest) + dist2 = brest.distanceTo(paris) + } yield { + assertTrue( + dist1 == dist2, + dist1 < 506_000, + dist1 > 504_000 + ) + } + ) + } ) ) }