Skip to content

Commit f0bb0d2

Browse files
committed
feat: new version of scala-distances
BREAKING CHANGE: now returns elevationProfile and does not return/save all subpaths anymore
1 parent 5a1f21e commit f0bb0d2

File tree

15 files changed

+172
-56
lines changed

15 files changed

+172
-56
lines changed

.gitlab-ci.yml

-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ include:
55
compile scala-distances:
66
extends:
77
- .sbt-compile-cross
8-
tags: []
98

109
unused-dependencies:
1110
extends: .sbt-unused-dependencies
@@ -14,12 +13,9 @@ test scala-distances:
1413
extends:
1514
- .sbt-test-cross
1615
- .redis
17-
tags: []
1816

1917
version:
2018
extends: .version
21-
tags: []
2219

2320
publish on sonatype:
2421
extends: .sonatype-publish
25-
tags: []

build.sbt

+21-26
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,28 @@ import CompileFlags._
22
import sbt.Keys.crossScalaVersions
33
import DependenciesScopesHandler._
44
import Dependencies._
5+
import PublishSettings.localCacheSettings
56

67
lazy val scala212 = "2.12.13"
78
lazy val scala213 = "2.13.10"
89
lazy val supportedScalaVersions = List(scala213, scala212)
910

10-
Global / onChangedBuildSource := ReloadOnSourceChanges
11-
ThisBuild / organization := "com.colisweb"
12-
ThisBuild / scalaVersion := scala213
13-
ThisBuild / crossScalaVersions := supportedScalaVersions
14-
ThisBuild / scalafmtOnCompile := true
15-
ThisBuild / scalafmtCheck := true
16-
ThisBuild / scalafmtSbtCheck := true
17-
ThisBuild / scalacOptions ++= crossScalacOptions(scalaVersion.value)
1811

19-
ThisBuild / pushRemoteCacheTo := Some(
20-
MavenCache("local-cache", baseDirectory.value / sys.env.getOrElse("CACHE_PATH", "sbt-cache"))
21-
)
12+
Global / onChangedBuildSource := ReloadOnSourceChanges
13+
14+
inThisBuild {
15+
List(
16+
organization := "com.colisweb",
17+
scalaVersion := scala213,
18+
crossScalaVersions := supportedScalaVersions,
19+
scalafmtOnCompile := true,
20+
scalafmtCheck := true,
21+
scalafmtSbtCheck := true,
22+
Test / fork := true,
23+
scalacOptions ++= crossScalacOptions(scalaVersion.value),
24+
localCacheSettings
25+
)
26+
}
2227
//// Main projects
2328

2429
lazy val root = Project(id = "scala-distances", base = file("."))
@@ -29,12 +34,9 @@ lazy val root = Project(id = "scala-distances", base = file("."))
2934

3035
lazy val core = project
3136
.settings(moduleName := "scala-distances-core")
37+
.settings(libraryDependencies ++= compileDependencies(cats, scalaCache, squants))
3238
.settings(
33-
libraryDependencies ++= compileDependencies(
34-
cats,
35-
scalaCache,
36-
squants
37-
) ++ testDependencies(
39+
libraryDependencies ++= testDependencies(
3840
monix,
3941
mockitoScalaScalatest,
4042
scalacheck,
@@ -49,13 +51,7 @@ lazy val `google-provider` = project
4951
.in(file("providers/google"))
5052
.settings(moduleName := "scala-distances-provider-google")
5153
.settings(
52-
libraryDependencies ++= compileDependencies(
53-
catsEffect,
54-
enumeratum,
55-
googleMaps,
56-
loggingInterceptor,
57-
refined
58-
)
54+
libraryDependencies ++= compileDependencies(catsEffect, enumeratum, googleMaps, loggingInterceptor, refined)
5955
)
6056
.dependsOn(core)
6157

@@ -96,10 +92,9 @@ lazy val `caffeine-cache` = project
9692
lazy val tests = project
9793
.settings(noPublishSettings)
9894
.dependsOn(core % "test->test;compile->compile", `google-provider`, `here-provider`, `redis-cache`, `caffeine-cache`)
99-
.settings(libraryDependencies ++= testDependencies(pureconfig, refinedPureconfig, scalaCacheCatsEffect))
95+
.settings(libraryDependencies ++= testDependencies(pureconfig, refinedPureconfig, scalaCacheCatsEffect, approvals))
10096

101-
/** Copied from Cats
102-
*/
97+
/** Copied from Cats */
10398
lazy val noPublishSettings = Seq(
10499
publish := {},
105100
publishLocal := {},

core/src/main/scala/com/colisweb/distances/bird/HaversineDistanceApi.scala

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ package com.colisweb.distances.bird
22

33
import cats.Applicative
44
import com.colisweb.distances.DistanceApi
5-
import com.colisweb.distances.model.path.DirectedPath
65
import com.colisweb.distances.model.{FixedSpeedTransportation, OriginDestination, PathResult}
76

87
class HaversineDistanceApi[F[_]: Applicative, P: OriginDestination: FixedSpeedTransportation]
98
extends DistanceApi[F, P] {
109
import com.colisweb.distances.model.syntax._
1110

1211
override def distance(path: P): F[PathResult] = {
13-
val distanceInKilometers = Haversine.distanceInKm(path.origin, path.destination)
14-
val timeInSeconds = DurationFromSpeed.durationForDistance(distanceInKilometers, path.speed)
15-
Applicative[F].pure(
16-
PathResult(distanceInKilometers, timeInSeconds, List(DirectedPath(path.origin, path.destination)))
17-
)
12+
val distanceInKilometers = Haversine.distanceInKm(path.origin, path.destination)
13+
val timeInSeconds = DurationFromSpeed.durationForDistance(distanceInKilometers, path.speed)
14+
val rollingResistanceCoefficient = 0.0125
15+
// we don't know the elevation so only the distance is taken into account
16+
val elevationProfile = distanceInKilometers * 1000 * rollingResistanceCoefficient
17+
Applicative[F].pure(PathResult(distanceInKilometers, timeInSeconds, Some(elevationProfile)))
1818
}
1919
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package com.colisweb.distances.model
22

3-
import com.colisweb.distances.model.path.DirectedPath
4-
5-
final case class PathResult(distance: DistanceInKm, duration: DurationInSeconds, paths: List[DirectedPath])
3+
final case class PathResult(
4+
distance: DistanceInKm,
5+
duration: DurationInSeconds,
6+
elevationProfile: Option[Double] = None
7+
) {
8+
val speedInKmPerHour: SpeedInKmH = distance / (duration / 3600)
9+
val speedInMetersPerSecond: Double = distance * 1000 / duration
10+
}

core/src/test/scala/com/colisweb/distances/cache/CacheSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class CacheSpec extends AnyWordSpec with Matchers {
1919
}
2020

2121
private val path: DirectedPath = DirectedPath(Point(0, 0), Point(0, 0))
22-
private val pathResult: PathResult = PathResult(1, 1, List(path))
22+
private val pathResult: PathResult = PathResult(1, 1)
2323

2424
private val distanceMap: Distances[Try, DirectedPath] =
2525
FromMapDistances[Try]

core/src/test/scala/com/colisweb/distances/correction/CorrectPastDepartureTimeSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class CorrectPastDepartureTimeSpec
3535

3636
override protected def beforeEach(): Unit = {
3737
Mockito.reset(base)
38-
base.distance(any[DirectedPathWithModeAt]) returns PathResult(0d, 0, Nil)
38+
base.distance(any[DirectedPathWithModeAt]) returns PathResult(0d, 0)
3939
()
4040
}
4141

core/src/test/scala/com/colisweb/distances/generator/Generators.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ object Generators {
6565
path <- genPathBetween
6666
distanceAndDuration <- genDistanceAndDurationUnrelated
6767
(distance, duration) = distanceAndDuration
68-
} yield (path, PathResult(distance, duration, List(DirectedPath(path.origin, path.destination))))
68+
} yield (path, PathResult(distance, duration))
6969

7070
def genPathSimpleAndDistanceUnrelatedSet: Gen[Map[DirectedPath, PathResult]] =
7171
Gen.listOf(genPathSimpleAndDistanceUnrelated).map(_.toMap)

project/Dependencies.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sbt._
22

33
object Versions {
4+
final val approvals = "1.3.1"
45
final val cats = "2.9.0"
56
final val catsEffect = "2.3.3"
67
final val circe = "0.14.2"
@@ -15,14 +16,15 @@ object Versions {
1516
final val requests = "0.7.1"
1617
final val scalaCache = "0.28.0"
1718
final val scalacheck = "1.17.0"
18-
final val scalaCompat = "2.8.1"
19+
final val scalaCompat = "2.9.0"
1920
final val scalatest = "3.2.13"
2021
final val scalatestPlus = "3.1.0.0-RC2"
2122
final val squants = "1.8.3"
2223
}
2324

2425
object Dependencies {
2526

27+
final val approvals = "com.colisweb" %% "approvals-scala" % Versions.approvals
2628
final val cats = "org.typelevel" %% "cats-core" % Versions.cats
2729
final val catsEffect = "org.typelevel" %% "cats-effect" % Versions.catsEffect
2830
final val circe = "io.circe" %% "circe-core" % Versions.circe

project/PublishSettings.scala

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../scala-common/scala/PublishSettings.scala

providers/google/src/main/scala/com/colisweb/distances/providers/google/GoogleDistanceDirectionsProvider.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class GoogleDistanceDirectionsProvider[F[_]](
3535
bestRoute = chooseBestRoute(response.routes.toList)
3636
distancesAndDuration = extractResponse(bestRoute)
3737
(distance, duration) = distancesAndDuration
38-
} yield PathResult(distance, duration, Nil)
38+
} yield PathResult(distance, duration)
3939

4040
}
4141

providers/google/src/main/scala/com/colisweb/distances/providers/google/GoogleDistanceMatrixProvider.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class GoogleDistanceMatrixProvider[F[_]](
3535
response <- requestExecutor.run(requestMaybeWithTraffic)
3636
distanceAndDuration <- extractSingleResponse(response)
3737
(distance, duration) = distanceAndDuration
38-
} yield PathResult(distance, duration, Nil)
38+
} yield PathResult(distance, duration)
3939
}
4040

4141
private def requestWithPossibleTraffic(

providers/here/src/main/scala/com/colisweb/distances/providers/here/HereRoutingProvider.scala

+18-3
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ class HereRoutingProvider[F[_]](hereRoutingContext: HereRoutingContext, executor
8383

8484
private def parseResult(origin: Point, destination: Point, r: Response): F[PathResult] =
8585
if (r.routes.nonEmpty) {
86-
val bestRoute = routingMode.best(r.routes)
87-
val roadSegments = parseRoadSegments(origin, destination, bestRoute)
88-
F.pure(PathResult(bestRoute.distance, bestRoute.duration, roadSegments))
86+
val bestRoute = routingMode.best(r.routes)
87+
val roadSegments = parseRoadSegments(origin, destination, bestRoute)
88+
val elevationProfile = computeElevationProfile(roadSegments, bestRoute.distance)
89+
F.pure(PathResult(bestRoute.distance, bestRoute.duration, Some(elevationProfile)))
8990
} else {
9091
F.raiseError(NoRouteFoundError(origin, destination))
9192
}
@@ -132,6 +133,20 @@ class HereRoutingProvider[F[_]](hereRoutingContext: HereRoutingContext, executor
132133
case _ => None
133134
}
134135
}
136+
137+
// unit is in meters
138+
// it is part of a modified version of the formula from:
139+
// A new model and approach to electric and diesel-powered vehicle routing, Murakami (2017)
140+
private[here] def computeElevationProfile(subPaths: List[DirectedPath], totalDistance: DistanceInKm): Double = {
141+
val totalBirdDistanceInKm = subPaths.foldLeft(0d) { case (acc, path) => acc + path.birdDistanceInKm }
142+
val rollingResistanceCoefficient = 0.0125
143+
144+
subPaths.foldLeft(0d) { case (acc, path) =>
145+
val approxSubPathDistanceInKm = totalDistance * (path.birdDistanceInKm / totalBirdDistanceInKm)
146+
val angle = path.elevationAngleInRadians
147+
acc + (approxSubPathDistanceInKm * 1000 * (math.sin(angle) + rollingResistanceCoefficient * math.cos(angle)))
148+
}
149+
}
135150
}
136151

137152
private[here] object HereRoutingProvider {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
elevation profile - angles
2+
0.0 <== , List()
3+
125.09469 <== , List(0.0)
4+
250.18939 <== , List(0.0, 0.0)
5+
375.28408 <== , List(0.0, 0.0, 0.0)
6+
2806.61649 <== , List(15.0)
7+
5902.97115 <== , List(30.0)
8+
10132.67027 <== , List(45.0)
9+
-2556.4271 <== , List(-15.0)
10+
-5652.78176 <== , List(-30.0)
11+
-9882.48088 <== , List(-45.0)
12+
250.18939 <== , List(15.0, -15.0)
13+
250.18939 <== , List(45.0, -45.0)
14+
250.18939 <== , List(89.0, -89.0)
15+
-974.037 <== , List(3.0, -10.0)
16+
5454.51693 <== , List(30.0, -1.0)
17+
10063.93017 <== , List(10.0, 45.0, -2.0)
18+
-7469.65678 <== , List(-45.0, 0.0, 1.0)

tests/src/test/scala/com/colisweb/distances/DistanceApiSpec.scala

+8-8
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ class DistanceApiSpec extends AnyWordSpec with Matchers with ScalaFutures with B
351351
results: Map[(Point, Point), (DistanceInKm, DurationInSeconds)],
352352
trafficTime: Option[Instant],
353353
run: RunSync[F],
354-
checkPolyline: Boolean = false
354+
checkElevationProfile: Boolean = false
355355
): Unit = {
356356

357357
"return approximate distance and duration from Paris 01 to Marseille 01" in {
@@ -364,8 +364,8 @@ class DistanceApiSpec extends AnyWordSpec with Matchers with ScalaFutures with B
364364
val distanceFrom01to02 = run(api.distance(driveFrom01to02))
365365
val (distance, duration) = results(paris01 -> marseille01)
366366

367-
if (checkPolyline)
368-
distanceFrom01to02.paths should not be empty
367+
if (checkElevationProfile)
368+
distanceFrom01to02.elevationProfile should not be empty
369369

370370
distanceFrom01to02.distance shouldBe distance +- distance / 8
371371
distanceFrom01to02.duration shouldBe duration +- duration / 8
@@ -376,7 +376,7 @@ class DistanceApiSpec extends AnyWordSpec with Matchers with ScalaFutures with B
376376
api: DistanceApi[F, DirectedPathWithModeAt],
377377
trafficTime: Option[Instant],
378378
run: RunSync[F],
379-
checkPolyline: Boolean = false
379+
checkElevationProfile: Boolean = false
380380
): Unit = {
381381

382382
"return zero between the same points" in {
@@ -389,8 +389,8 @@ class DistanceApiSpec extends AnyWordSpec with Matchers with ScalaFutures with B
389389

390390
val distance = run(api.distance(path))
391391

392-
if (checkPolyline)
393-
distance.paths should not be empty
392+
if (checkElevationProfile)
393+
distance.elevationProfile should not be empty
394394

395395
distance.distance shouldBe 0d
396396
distance.duration shouldBe 0
@@ -451,14 +451,14 @@ class DistanceApiSpec extends AnyWordSpec with Matchers with ScalaFutures with B
451451
hereApi,
452452
trafficTime = Some(futureTime),
453453
run,
454-
checkPolyline = true
454+
checkElevationProfile = true
455455
)
456456
approximateTests(
457457
hereApi,
458458
hereResults,
459459
trafficTime = None,
460460
run,
461-
checkPolyline = true
461+
checkElevationProfile = true
462462
)
463463
}
464464

0 commit comments

Comments
 (0)