Skip to content

Commit 8cfe930

Browse files
committed
Open source the library
1 parent c523d9f commit 8cfe930

21 files changed

+591
-0
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CHANGELOG.md merge=union

.gitignore

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Created by .ignore support plugin (hsz.mobi)
2+
### Scala template
3+
*.class
4+
*.log
5+
### JetBrains template
6+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
7+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8+
9+
# User-specific stuff:
10+
.idea/**/workspace.xml
11+
.idea/**/tasks.xml
12+
.idea/dictionaries
13+
14+
# Sensitive or high-churn files:
15+
.idea/**/dataSources/
16+
.idea/**/dataSources.ids
17+
.idea/**/dataSources.xml
18+
.idea/**/dataSources.local.xml
19+
.idea/**/sqlDataSources.xml
20+
.idea/**/dynamic.xml
21+
.idea/**/uiDesigner.xml
22+
23+
# Gradle:
24+
.idea/**/gradle.xml
25+
.idea/**/libraries
26+
27+
# CMake
28+
cmake-build-debug/
29+
30+
# Mongo Explorer plugin:
31+
.idea/**/mongoSettings.xml
32+
33+
## File-based project format:
34+
*.iws
35+
36+
## Plugin-specific files:
37+
38+
# IntelliJ
39+
out/
40+
41+
# mpeltonen/sbt-idea plugin
42+
.idea_modules/
43+
44+
# JIRA plugin
45+
atlassian-ide-plugin.xml
46+
47+
# Cursive Clojure plugin
48+
.idea/replstate.xml
49+
50+
# Crashlytics plugin (for Android Studio and IntelliJ)
51+
com_crashlytics_export_strings.xml
52+
crashlytics.properties
53+
crashlytics-build.properties
54+
fabric.properties
55+
### SBT template
56+
# Simple Build Tool
57+
# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
58+
59+
dist/*
60+
target/
61+
lib_managed/
62+
src_managed/
63+
project/boot/
64+
project/plugins/project/
65+
.history
66+
.cache
67+
.lib/
68+
/.idea/

.sbtopts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-J-Xms256M
2+
-J-Xmx1024M
3+
-J-Xss2M
4+
-J-XX:MaxMetaspaceSize=512M

.scalafmt.conf

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
style = defaultWithAlign
2+
maxColumn = 140

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Change Log
2+
All notable changes to this project will be documented in this file.
3+
This project adheres to [Semantic Versioning](http://semver.org/).
4+
5+
More infos about this file : http://keepachangelog.com/
6+
7+
## [Unreleased] - no_due_date
8+
9+
- **Open source the library**

build.sbt

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
organization := "com.guizmaii"
2+
3+
name := "scala-distances"
4+
5+
version := "0.1"
6+
7+
scalaVersion := "2.12.4"
8+
crossScalaVersions := Seq("2.11.12", scalaVersion.value)
9+
10+
scalafmtOnCompile := true
11+
12+
scalacOptions ++= Seq(
13+
"-deprecation",
14+
"-target:jvm-1.8",
15+
"-encoding",
16+
"UTF-8",
17+
"-feature",
18+
"-language:existentials",
19+
"-language:higherKinds",
20+
"-language:implicitConversions",
21+
"-language:postfixOps",
22+
"-unchecked",
23+
// "-Xfatal-warnings",
24+
// "-Ywarn-unused-import"
25+
"-Xlint",
26+
"-Xlint:missing-interpolator",
27+
"-Yno-adapted-args",
28+
"-Ywarn-unused",
29+
"-Ywarn-dead-code",
30+
"-Ywarn-numeric-widen",
31+
"-Ywarn-value-discard",
32+
"-Xfuture"
33+
)
34+
35+
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)
36+
37+
val monix = "io.monix" %% "monix" % "3.0.0-M2"
38+
val googleMaps = "com.google.maps" % "google-maps-services" % "0.2.5"
39+
val squants = "org.typelevel" %% "squants" % "1.3.0"
40+
41+
val scalacache = ((version: String) =>
42+
Seq(
43+
"com.github.cb372" %% "scalacache-core" % version,
44+
"com.github.cb372" %% "scalacache-caffeine" % version,
45+
"com.github.cb372" %% "scalacache-redis" % version,
46+
"com.github.cb372" %% "scalacache-monix" % version
47+
))("0.21.0")
48+
49+
val testKit = Seq(
50+
"org.scalacheck" %% "scalacheck" % "1.13.5",
51+
"org.scalatest" %% "scalatest" % "3.0.4"
52+
)
53+
54+
libraryDependencies ++= Seq(
55+
monix % Provided,
56+
squants % Provided,
57+
googleMaps
58+
) ++ scalacache ++ testKit.map(_ % Test)

project/build.properties

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version = 1.0.4

project/plugins.sbt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC13")
2+
3+
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.3")
4+
5+
addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.14")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.guizmaii.distances
2+
3+
import com.guizmaii.distances.Types._
4+
import com.guizmaii.distances.utils.WithCache
5+
import monix.eval.Task
6+
import monix.execution.CancelableFuture
7+
8+
import scala.collection.immutable.Seq
9+
10+
trait DistanceApi extends WithCache[SerializableDistance] {
11+
12+
def distanceT(origin: LatLong, destination: LatLong): Task[Distance]
13+
14+
def distance(origin: LatLong, destination: LatLong): CancelableFuture[Distance]
15+
16+
def distanceFromPostalCodesT(geocoder: Geocoder)(origin: PostalCode, destination: PostalCode): Task[Distance]
17+
18+
def distanceFromPostalCodes(geocoder: Geocoder)(
19+
origin: PostalCode,
20+
destination: PostalCode
21+
): CancelableFuture[Distance]
22+
23+
def distancesT(paths: Seq[DirectedPath]): Task[Seq[DirectedPathWithDistance]]
24+
25+
def distances(paths: Seq[DirectedPath]): CancelableFuture[Seq[DirectedPathWithDistance]]
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.guizmaii.distances
2+
3+
import com.guizmaii.distances.Types.{LatLong, PostalCode}
4+
import com.guizmaii.distances.utils.WithCache
5+
import monix.eval.Task
6+
import monix.execution.CancelableFuture
7+
8+
trait Geocoder extends WithCache[LatLong] {
9+
def geocodeT(postalCode: PostalCode): Task[LatLong]
10+
def geocode(postalCode: PostalCode): CancelableFuture[LatLong]
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.guizmaii.distances
2+
3+
import squants.space.Length
4+
import squants.space.LengthConversions._
5+
6+
import scala.concurrent.duration._
7+
8+
object Types {
9+
10+
private[distances] final case class SerializableDistance(value: Double, duration: Double)
11+
12+
final case class PostalCode(value: String) extends AnyVal
13+
14+
final case class LatLong(latitude: Double, longitude: Double)
15+
16+
final case class Distance(length: Length, duration: Duration) extends Ordered[Distance] {
17+
override def compare(that: Distance): Int = this.length.compareTo(that.length)
18+
}
19+
20+
object Distance {
21+
private[distances] def apply(s: SerializableDistance): Distance =
22+
Distance(length = s.value meters, duration = s.duration seconds)
23+
24+
final lazy val Inf: Distance = Distance(Double.PositiveInfinity meters, Duration.Inf)
25+
}
26+
27+
final case class DirectedPath(origin: LatLong, destination: LatLong)
28+
29+
final case class DirectedPathWithDistance(origin: LatLong, destination: LatLong, distance: Distance)
30+
31+
object DirectedPathWithDistance {
32+
def apply(path: DirectedPath, distance: Distance): DirectedPathWithDistance =
33+
DirectedPathWithDistance(origin = path.origin, destination = path.destination, distance = distance)
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.guizmaii.distances.implementations.cache
2+
3+
import monix.eval.Task
4+
5+
import scala.concurrent.duration.Duration
6+
import scala.reflect.ClassTag
7+
8+
private[distances] abstract class GeoCache[E <: Serializable: ClassTag] {
9+
10+
import scalacache._
11+
12+
// TODO: Wait an answer for: https://twitter.com/guizmaii/status/934923692148232193
13+
implicit val task: Mode[Task] = new Mode[Task] {
14+
val M: Async[Task] = CatsEffect.asyncForCatsEffectAsync[Task]
15+
}
16+
17+
protected val expiration: Duration
18+
19+
implicit val innerCache: Cache[E]
20+
21+
def getOrTask(keyParts: Any*)(orElse: => Task[E]): Task[E] = cachingF(keyParts)(ttl = Some(expiration))(orElse)
22+
23+
def setT(keyParts: Any*)(value: E): Task[Any] = put(keyParts)(value = value, ttl = Some(expiration))
24+
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.guizmaii.distances.implementations.cache
2+
3+
import scala.concurrent.duration.Duration
4+
import scala.reflect.ClassTag
5+
6+
final class InMemoryGeoCache[E <: Serializable: ClassTag](override val expiration: Duration) extends GeoCache[E] {
7+
import scalacache._
8+
import scalacache.caffeine._
9+
10+
override implicit val innerCache: Cache[E] = CaffeineCache[E]
11+
}
12+
13+
object InMemoryGeoCache {
14+
def apply[E <: Serializable: ClassTag](expiration: Duration): InMemoryGeoCache[E] = new InMemoryGeoCache(expiration)
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.guizmaii.distances.implementations.cache
2+
3+
import scala.concurrent.duration.Duration
4+
import scala.reflect.ClassTag
5+
6+
final class RedisGeoCache[E <: Serializable: ClassTag](host: String, port: Int, override val expiration: Duration) extends GeoCache[E] {
7+
import scalacache._
8+
import scalacache.redis._
9+
import scalacache.serialization.binary._
10+
11+
override implicit val innerCache: Cache[E] = RedisCache[E](host, port)
12+
}
13+
14+
object RedisGeoCache {
15+
def apply[E <: Serializable: ClassTag](host: String, port: Int, expiration: Duration): RedisGeoCache[E] =
16+
new RedisGeoCache(host, port, expiration)
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.guizmaii.distances.implementations.google
2+
3+
import com.google.maps.GeoApiContext
4+
5+
final case class GoogleGeoApiContext(googleApiKey: String) {
6+
7+
/**
8+
* More infos about the rate limit:
9+
* - https://developers.google.com/maps/documentation/distance-matrix/usage-limits
10+
*/
11+
val geoApiContext: GeoApiContext = new GeoApiContext.Builder().apiKey(googleApiKey).queryRateLimit(100).build()
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.guizmaii.distances.implementations.google.distanceapi
2+
3+
import com.google.maps.DistanceMatrixApi
4+
import com.guizmaii.distances.Types._
5+
import com.guizmaii.distances.implementations.cache.GeoCache
6+
import com.guizmaii.distances.implementations.google.GoogleGeoApiContext
7+
import com.guizmaii.distances.{DistanceApi, Geocoder}
8+
import monix.eval.Task
9+
import monix.execution.CancelableFuture
10+
11+
import scala.collection.immutable.Seq
12+
13+
final class GoogleDistanceApi(
14+
geoApiContext: GoogleGeoApiContext,
15+
override protected val alternativeCache: Option[GeoCache[SerializableDistance]] = None
16+
) extends DistanceApi {
17+
18+
import com.guizmaii.distances.utils.MonixSchedulers.AlwaysAsyncForkJoinScheduler._
19+
import com.guizmaii.distances.utils.RichImplicits._
20+
21+
private def toGoogleRepresentation(latLong: LatLong): String = s"${latLong.latitude},${latLong.longitude}"
22+
23+
override def distanceT(origin: LatLong, destination: LatLong): Task[Distance] = {
24+
def fetch: Task[SerializableDistance] =
25+
DistanceMatrixApi
26+
.getDistanceMatrix(
27+
geoApiContext.geoApiContext,
28+
Array(toGoogleRepresentation(origin)),
29+
Array(toGoogleRepresentation(destination))
30+
)
31+
.toTask
32+
.map(_.rows.head.elements.head.asSerializableDistance)
33+
34+
val key = origin -> destination
35+
cache
36+
.getOrTask(key)(fetch)
37+
.map(Distance.apply)
38+
}
39+
40+
override def distanceFromPostalCodesT(geocoder: Geocoder)(
41+
origin: PostalCode,
42+
destination: PostalCode
43+
): Task[Distance] =
44+
geocoder
45+
.geocodeT(origin)
46+
.zip(geocoder.geocodeT(destination))
47+
.flatMap((distanceT _).tupled)
48+
49+
override def distance(origin: LatLong, destination: LatLong): CancelableFuture[Distance] =
50+
distanceT(origin, destination).runAsync
51+
52+
override def distanceFromPostalCodes(geocoder: Geocoder)(
53+
origin: PostalCode,
54+
destination: PostalCode
55+
): CancelableFuture[Distance] = distanceFromPostalCodesT(geocoder)(origin, destination).runAsync
56+
57+
override def distancesT(paths: Seq[DirectedPath]): Task[Seq[DirectedPathWithDistance]] = Task.sequence {
58+
paths.map { path =>
59+
distanceT(path.origin, path.destination).map(distance => DirectedPathWithDistance(path, distance))
60+
}
61+
}
62+
63+
override def distances(paths: Seq[DirectedPath]): CancelableFuture[Seq[DirectedPathWithDistance]] =
64+
distancesT(paths).runAsync
65+
66+
}

0 commit comments

Comments
 (0)