diff --git a/.sbtopts b/.sbtopts index bb6a6283..9cc1318c 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1,4 +1,4 @@ -J-Xms256M -J-Xmx1024M -J-Xss2M --J-XX:MaxMetaspaceSize=512M +-J-XX:MaxMetaspaceSize=1024M diff --git a/build.sbt b/build.sbt index a46d65b6..c38bc01e 100644 --- a/build.sbt +++ b/build.sbt @@ -96,6 +96,8 @@ addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.fu val monix = "io.monix" %% "monix" % "3.0.0-M3" val googleMaps = "com.google.maps" % "google-maps-services" % "0.2.6" val squants = "org.typelevel" %% "squants" % "1.3.0" +val cats = "org.typelevel" %% "cats-core" % "1.0.1" +val enumeratum = "com.beachape" %% "enumeratum" % "1.5.12" val scalacache = ((version: String) => Seq( @@ -113,6 +115,8 @@ val testKit = Seq( libraryDependencies ++= Seq( monix % Provided, squants % Provided, + cats, + enumeratum, googleMaps ) ++ scalacache ++ testKit.map(_ % Test) diff --git a/src/main/scala/com/guizmaii/distances/DistanceApi.scala b/src/main/scala/com/guizmaii/distances/DistanceApi.scala index a57e0855..a65041b4 100644 --- a/src/main/scala/com/guizmaii/distances/DistanceApi.scala +++ b/src/main/scala/com/guizmaii/distances/DistanceApi.scala @@ -5,23 +5,34 @@ import com.guizmaii.distances.utils.WithCache import monix.eval.Task import monix.execution.CancelableFuture -import scala.collection.immutable.Seq +trait DistanceApi extends WithCache[(TravelMode, SerializableDistance)] { -trait DistanceApi extends WithCache[SerializableDistance] { + def distanceT( + origin: LatLong, + destination: LatLong, + travelMode: List[TravelMode] = List(TravelMode.Driving) + ): Task[Map[TravelMode, Distance]] - def distanceT(origin: LatLong, destination: LatLong): Task[Distance] + def distance( + origin: LatLong, + destination: LatLong, + travelMode: List[TravelMode] = List(TravelMode.Driving) + ): CancelableFuture[Map[TravelMode, Distance]] - def distance(origin: LatLong, destination: LatLong): CancelableFuture[Distance] - - def distanceFromPostalCodesT(geocoder: Geocoder)(origin: PostalCode, destination: PostalCode): Task[Distance] + def distanceFromPostalCodesT(geocoder: Geocoder)( + origin: PostalCode, + destination: PostalCode, + travelMode: List[TravelMode] = List(TravelMode.Driving) + ): Task[Map[TravelMode, Distance]] def distanceFromPostalCodes(geocoder: Geocoder)( origin: PostalCode, - destination: PostalCode - ): CancelableFuture[Distance] + destination: PostalCode, + travelMode: List[TravelMode] = List(TravelMode.Driving) + ): CancelableFuture[Map[TravelMode, Distance]] - def distancesT(paths: Seq[DirectedPath]): Task[Seq[DirectedPathWithDistance]] + def distancesT(paths: List[DirectedPath]): Task[Map[(TravelMode, LatLong, LatLong), Distance]] - def distances(paths: Seq[DirectedPath]): CancelableFuture[Seq[DirectedPathWithDistance]] + def distances(paths: List[DirectedPath]): CancelableFuture[Map[(TravelMode, LatLong, LatLong), Distance]] } diff --git a/src/main/scala/com/guizmaii/distances/Types.scala b/src/main/scala/com/guizmaii/distances/Types.scala index 952ac1d2..31455ac3 100644 --- a/src/main/scala/com/guizmaii/distances/Types.scala +++ b/src/main/scala/com/guizmaii/distances/Types.scala @@ -1,8 +1,12 @@ package com.guizmaii.distances +import com.google.maps.model.{LatLng => GoogleLatLng, TravelMode => GoogleTravelMode} +import enumeratum.{Enum, EnumEntry} import squants.space.Length import squants.space.LengthConversions._ +import scala.collection.immutable +import scala.collection.immutable.Seq import scala.concurrent.duration._ object Types { @@ -12,7 +16,9 @@ object Types { final case class PostalCode(value: String) extends AnyVal - final case class LatLong(latitude: Double, longitude: Double) + final case class LatLong(latitude: Double, longitude: Double) { + private[distances] def toGoogleLatLng: GoogleLatLng = new GoogleLatLng(latitude, longitude) + } final case class Distance(length: Length, duration: Duration) @@ -24,7 +30,44 @@ object Types { final lazy val Inf: Distance = Distance(Double.PositiveInfinity meters, Duration.Inf) } - type DirectedPath = (LatLong, LatLong) - type DirectedPathWithDistance = (LatLong, LatLong, Distance) + final case class DirectedPath(origin: LatLong, destination: LatLong, travelModes: List[TravelMode] = List(TravelMode.Driving)) + + sealed trait TravelMode extends EnumEntry + object TravelMode extends Enum[TravelMode] { + + val values: immutable.IndexedSeq[TravelMode] = findValues + + case object Driving extends TravelMode + case object Bicycling extends TravelMode + case object Unknown extends TravelMode + + implicit final class RichTravelMode(val travelMode: TravelMode) extends AnyVal { + def toGoogleTravelMode: GoogleTravelMode = + travelMode match { + case Driving => GoogleTravelMode.DRIVING + case Bicycling => GoogleTravelMode.BICYCLING + case Unknown => GoogleTravelMode.UNKNOWN + } + } + + implicit final class RichGoogleTravelMode(val travelMode: GoogleTravelMode) extends AnyVal { + + /** + * For now, I don't want to handle WALKING and TRANSIT. + * + * @return + */ + def fromGoogleTravelMode: TravelMode = { + import GoogleTravelMode._ + + travelMode match { + case DRIVING => Driving + case BICYCLING => Bicycling + case UNKNOWN | WALKING | TRANSIT => Unknown + } + } + } + + } } diff --git a/src/main/scala/com/guizmaii/distances/implementations/google/distanceapi/GoogleDistanceApi.scala b/src/main/scala/com/guizmaii/distances/implementations/google/distanceapi/GoogleDistanceApi.scala index 3b9511b9..7149b755 100644 --- a/src/main/scala/com/guizmaii/distances/implementations/google/distanceapi/GoogleDistanceApi.scala +++ b/src/main/scala/com/guizmaii/distances/implementations/google/distanceapi/GoogleDistanceApi.scala @@ -1,6 +1,11 @@ package com.guizmaii.distances.implementations.google.distanceapi -import com.google.maps.DistanceMatrixApi +import cats._ +import cats.data._ +import cats.implicits._ +import com.google.maps.DirectionsApi.RouteRestriction +import com.google.maps.{DistanceMatrixApi, DistanceMatrixApiRequest} +import com.google.maps.model.{TrafficModel, TransitMode, Unit => GoogleDistanceUnit} import com.guizmaii.distances.Types._ import com.guizmaii.distances.implementations.cache.GeoCache import com.guizmaii.distances.implementations.google.GoogleGeoApiContext @@ -8,66 +13,127 @@ import com.guizmaii.distances.{DistanceApi, Geocoder} import monix.eval.Task import monix.execution.CancelableFuture -import scala.collection.immutable.Seq - final class GoogleDistanceApi( geoApiContext: GoogleGeoApiContext, - override protected val alternativeCache: Option[GeoCache[SerializableDistance]] = None + override protected val alternativeCache: Option[GeoCache[(TravelMode, SerializableDistance)]] = None ) extends DistanceApi { import com.guizmaii.distances.utils.MonixSchedulers.AlwaysAsyncForkJoinScheduler._ import com.guizmaii.distances.utils.RichImplicits._ - private def toGoogleRepresentation(latLong: LatLong): String = s"${latLong.latitude},${latLong.longitude}" +// + //override def distanceFromPostalCodesT(geocoder: Geocoder)( + // origin: PostalCode, + // destination: PostalCode + //): Task[Distance] = + // if (origin == destination) Task.now(Distance.zero) + // else Task.zip2(geocoder.geocodeT(origin), geocoder.geocodeT(destination)).flatMap((distanceT _).tupled) +// + //override def distance(origin: LatLong, destination: LatLong): CancelableFuture[Distance] = + // distanceT(origin, destination).runAsync +// + //override def distanceFromPostalCodes(geocoder: Geocoder)( + // origin: PostalCode, + // destination: PostalCode + //): CancelableFuture[Distance] = distanceFromPostalCodesT(geocoder)(origin, destination).runAsync +// + //override def distancesT(paths: Seq[DirectedPath]): Task[Seq[DirectedPathWithDistance]] = Task.sequence { + // paths.map { + // case (origin, destination) => + // if (origin == destination) Task.now((origin, destination, Distance.zero)) + // else distanceT(origin, destination).map(distance => (origin, destination, distance)) + // } + //} +// + //override def distances(paths: Seq[DirectedPath]): CancelableFuture[Seq[DirectedPathWithDistance]] = + // distancesT(paths).runAsync + + import TravelMode._ - override def distanceT(origin: LatLong, destination: LatLong): Task[Distance] = { - def fetch: Task[SerializableDistance] = + override def distanceT( + origin: LatLong, + destination: LatLong, + travelModes: List[TravelMode] = List(TravelMode.Driving) + ): Task[Map[TravelMode, Distance]] = { + def fetch(mode: TravelMode): Task[(TravelMode, SerializableDistance)] = DistanceMatrixApi - .getDistanceMatrix( - geoApiContext.geoApiContext, - Array(toGoogleRepresentation(origin)), - Array(toGoogleRepresentation(destination)) - ) + .newRequest(geoApiContext.geoApiContext) + .mode(mode.toGoogleTravelMode) + .origins(origin.toGoogleLatLng) + .destinations(destination.toGoogleLatLng) + .units(GoogleDistanceUnit.METRIC) .toTask - .map(_.rows.head.elements.head.asSerializableDistance) + .map(res => mode -> res.rows.head.elements.head.asSerializableDistance) + + def fetchAndCache(mode: TravelMode): Task[(TravelMode, Distance)] = { + val key = (mode, origin, destination) + cache.getOrTask(key)(fetch(mode)).map { case (m, serializableDistance) => m -> Distance.apply(serializableDistance) } + } - val key = origin -> destination - cache - .getOrTask(key)(fetch) - .map(Distance.apply) + if (origin == destination) Task.now(travelModes.map(_ -> Distance.zero).toMap) + else travelModes.map(fetchAndCache).sequence.map(_.toMap) } + override def distance( + origin: LatLong, + destination: LatLong, + travelModes: List[TravelMode] = List(TravelMode.Driving) + ): CancelableFuture[Map[TravelMode, Distance]] = distanceT(origin, destination, travelModes).runAsync + override def distanceFromPostalCodesT(geocoder: Geocoder)( origin: PostalCode, - destination: PostalCode - ): Task[Distance] = - if (origin == destination) Task.now(Distance.zero) - else Task.zip2(geocoder.geocodeT(origin), geocoder.geocodeT(destination)).flatMap((distanceT _).tupled) - - override def distance(origin: LatLong, destination: LatLong): CancelableFuture[Distance] = - distanceT(origin, destination).runAsync + destination: PostalCode, + travelModes: List[TravelMode] = List(TravelMode.Driving) + ): Task[Map[TravelMode, Distance]] = { + if (origin == destination) Task.now(travelModes.map(_ -> Distance.zero).toMap) + else + Task + .zip2(geocoder.geocodeT(origin), geocoder.geocodeT(destination)) + .flatMap { case (o, d) => distanceT(o, d, travelModes) } + } override def distanceFromPostalCodes(geocoder: Geocoder)( origin: PostalCode, - destination: PostalCode - ): CancelableFuture[Distance] = distanceFromPostalCodesT(geocoder)(origin, destination).runAsync - - override def distancesT(paths: Seq[DirectedPath]): Task[Seq[DirectedPathWithDistance]] = Task.sequence { - paths.map { - case (origin, destination) => - if (origin == destination) Task.now((origin, destination, Distance.zero)) - else distanceT(origin, destination).map(distance => (origin, destination, distance)) + destination: PostalCode, + travelModes: List[TravelMode] = List(TravelMode.Driving) + ): CancelableFuture[Map[TravelMode, Distance]] = + distanceFromPostalCodesT(geocoder)(origin, destination, travelModes).runAsync + + override def distancesT(paths: List[DirectedPath]): Task[Map[(TravelMode, LatLong, LatLong), Distance]] = { + //def fetch(mode: TravelMode): Task[(TravelMode, SerializableDistance)] = + // DistanceMatrixApi + // .newRequest(geoApiContext.geoApiContext) + // .mode(mode.toGoogleTravelMode) + // .origins(origin.toGoogleLatLng) + // .destinations(destination.toGoogleLatLng) + // .units(GoogleDistanceUnit.METRIC) + // .toTask + // .map(res => mode -> res.rows.head.elements.head.asSerializableDistance) + + def fetchAndCache(mode: TravelMode): Task[(TravelMode, Distance)] = { + val key = (mode, origin, destination) + cache.getOrTask(key)(fetch(mode)).map { case (m, serializableDistance) => m -> Distance.apply(serializableDistance) } } + + paths + .distinctBy { case (origin, destination, t) => DirectedPath(origin, destination, t) } + .map { + case DirectedPath(origin, destination, travelModes) => + if (origin == destination) Task.now(travelModes.map(mode => (mode, origin, destination) -> Distance.zero).toMap) + else {} + } + + ??? + } - override def distances(paths: Seq[DirectedPath]): CancelableFuture[Seq[DirectedPathWithDistance]] = - distancesT(paths).runAsync + override def distances(paths: List[DirectedPath]): CancelableFuture[Map[(TravelMode, LatLong, LatLong), Distance]] = ??? } object GoogleDistanceApi { def apply(geoApiContext: GoogleGeoApiContext): GoogleDistanceApi = new GoogleDistanceApi(geoApiContext) - def apply(geoApiContext: GoogleGeoApiContext, geoCache: GeoCache[SerializableDistance]): GoogleDistanceApi = + def apply(geoApiContext: GoogleGeoApiContext, geoCache: GeoCache[(TravelMode, SerializableDistance)]): GoogleDistanceApi = new GoogleDistanceApi(geoApiContext, Some(geoCache)) } diff --git a/src/main/scala/com/guizmaii/distances/utils/RichImplicits.scala b/src/main/scala/com/guizmaii/distances/utils/RichImplicits.scala index e493b3f9..dffe6b02 100644 --- a/src/main/scala/com/guizmaii/distances/utils/RichImplicits.scala +++ b/src/main/scala/com/guizmaii/distances/utils/RichImplicits.scala @@ -5,6 +5,8 @@ import com.google.maps.model.{DistanceMatrixElement, LatLng} import com.guizmaii.distances.Types.{LatLong, SerializableDistance} import monix.eval.Task +import scala.collection.{IterableLike, TraversableLike} +import scala.collection.generic.CanBuildFrom import scala.concurrent.Promise private[distances] object RichImplicits { @@ -12,10 +14,6 @@ private[distances] object RichImplicits { def toInnerLatLong: LatLong = LatLong(latitude = latLng.lat, longitude = latLng.lng) } - implicit final class RichInnerLatLng(val latLong: LatLong) extends AnyVal { - def toGoogleLatLong: LatLng = new LatLng(latLong.latitude, latLong.longitude) - } - private[this] final class CallBack[T](promise: Promise[T]) extends PendingResult.Callback[T] { override def onResult(t: T): Unit = { val _ = promise.success(t) @@ -39,4 +37,30 @@ private[distances] object RichImplicits { SerializableDistance(value = element.distance.inMeters.toDouble, duration = element.duration.inSeconds.toDouble) } + implicit final class RichCollection[A, Repr](val xs: IterableLike[A, Repr]) extends AnyVal { + + /** + * Come from here: https://stackoverflow.com/a/34456552/2431728 + * + * @param f + * @param cbf + * @tparam B + * @return + */ + def distinctBy[B, That](f: A => B)(implicit cbf: CanBuildFrom[Repr, A, That]): That = { + val builder = cbf(xs.repr) + val i = xs.toIterator + var set = Set[B]() + while (i.hasNext) { + val o = i.next + val b = f(o) + if (!set(b)) { + set += b + builder += o + } + } + builder.result + } + } + }