Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README_DEV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Documentación
No he comentado el código porque creo que es bastante sencillo. No obstante los tests creo que explican bastante sobre
lo que hace cada función.


# Docker
No tengo muy claro como se esperaba la creación del contenedor de docker. Así que lo he hecho muy sencillo.
Para crear la imagen (tras crear el fatjar)

```bash
docker build -t solution:1.0.0 -f src/main/docker/Dockerfile .
```
Para ejecutar la aplicación
```bash
docker run --rm -it -v ${PWD}/data:/opt/data solution:1.0.0
```
21 changes: 21 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Dependencies._

organization := "inspide"
name := "solution"
version := "1.0.0"

scalaVersion := "2.12.10"

assemblyJarName in assembly := "solution.jar"

assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case x => MergeStrategy.first
}

resolvers += "Bintray" at "https://jcenter.bintray.com/"
resolvers += "Osgeo Repo" at "http://download.osgeo.org/webdav/geotools/"
resolvers += "Boundless" at "http://repo.boundlessgeo.com/main"

libraryDependencies ++= coreDependencies
dependencyOverrides ++= coreDependencyOverrides
27 changes: 27 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import sbt._

object Dependencies {
val GPXParserVersion = "1.12"
val GeotoolsVersion = "22.3"

val ScalatestVersion = "3.1.0"

val coreMainDependencies = Seq(
"me.himanshusoni.gpxparser" % "gpx-parser" % GPXParserVersion % Compile,
"org.geotools" % "gt-referencing" % GeotoolsVersion % Compile,

/**
* The next line avoids a problem downloading this transitive dependency.
* Please check https://stackoverflow.com/a/26993223
*/
"javax.media" % "jai_core" % "1.1.3" % Compile from "http://download.osgeo.org/webdav/geotools/javax/media/jai_core/1.1.3/jai_core-1.1.3.jar"
)

val testDependencies = Seq(
"org.scalatest" %% "scalatest" % ScalatestVersion % Test,
)

val coreDependencies = coreMainDependencies ++ testDependencies
val coreDependencyOverrides = Seq()

}
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.2.8
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")
4 changes: 4 additions & 0 deletions src/main/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM azul/zulu-openjdk:8u242
WORKDIR /opt/
COPY target/scala-2.12/solution.jar .
ENTRYPOINT ["java", "-jar", "solution.jar", "data"]
13 changes: 13 additions & 0 deletions src/main/scala/inspide/test/CSV.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package inspide.test

object CSV {

implicit val ordering = Ordering[Double].reverse

val CsvHead = "id\ttotalNumberOfPoints\ttotalDistance\tavgDistance\tminDistance\tmaxDistance\n"

val results2csv = (results: Results) => results.productIterator.mkString("\t")

val toCSV = (seqOfResults: Seq[Results]) => CsvHead + seqOfResults.sortBy(_.totalDistance).map(results2csv).mkString("\n")

}
28 changes: 28 additions & 0 deletions src/main/scala/inspide/test/DistanceCalculator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package inspide.test

import org.geotools.referencing.GeodeticCalculator


object DistanceCalculator {

private val fixDecimals = (precission: Int) => (value: Double) => BigDecimal(value).setScale(precission, BigDecimal.RoundingMode.HALF_UP).toDouble
private val fixTo3Decimals = fixDecimals(3)

val calculateDistance = (segment: Segment) => {
val gc = new GeodeticCalculator()
gc.setStartingGeographicPoint(segment.from.lon, segment.from.lat)
gc.setDestinationGeographicPoint(segment.to.lon, segment.to.lat)
fixTo3Decimals(gc.getOrthodromicDistance)
}

val toSegments = (points: Seq[Point]) => points.zip(points.tail).map(p => Segment(p._1, p._2))

val calculateTotalDistance = (segments: Seq[Segment]) => fixTo3Decimals(segments.map(calculateDistance).sum)

val calculateAverageDistance = (segments: Seq[Segment]) => fixTo3Decimals(calculateTotalDistance(segments) / segments.length)

val getMinimunDistance = (segments: Seq[Segment]) => fixTo3Decimals(segments.map(calculateDistance).min)

val getMaximunDistance = (segments: Seq[Segment]) => fixTo3Decimals(segments.map(calculateDistance).max)

}
60 changes: 60 additions & 0 deletions src/main/scala/inspide/test/GPXParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package inspide.test

import java.io.{File, FileInputStream, InputStream}

import inspide.test.DistanceCalculator._
import me.himanshusoni.gpxparser.modal.{GPX, Track, TrackSegment}
import me.himanshusoni.gpxparser.{GPXParser => GPXFileParser}

import scala.collection.JavaConverters._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object GPXParser {

private val createInputStream = (file: File) => Future {
new FileInputStream(file)
}

private val toPoints = (trackSegment: TrackSegment) => trackSegment.getWaypoints.asScala.toList.map(wp => Point(wp.getLatitude, wp.getLongitude))

private val toListOfPoints = (track: Track) => track.getTrackSegments.asScala.toList.map(toPoints).head

private val toListOfTrackSegments = (tracks: Seq[Track]) => tracks.map(toListOfPoints).head

private val toListOfTracks = (gpx: GPX) => gpx.getTracks.asScala.toList

private val gpxParser = new GPXFileParser()

/**
* This must be done because GPXFileParser is not thread-safe. Internally it uses a SimpleDateFormat that is created
* statically. SimpleDataFormat is not thread-safe and the usage of Futures ends up with a lot of stack traces. I would
* use Task of Monix or IO from Cats instead of plain Scala Futures. But I wanted to strictly stick to the definition
* of this problem, so I have not use any other libraries.
* The conflictive line is at [[https://github.com/himanshu-soni/gpx-parser/blob/master/src/main/java/me/himanshusoni/gpxparser/BaseGPX.java#L15]]
*/
private val parseGPX = (inputStream: InputStream) => Future {
gpxParser.synchronized {
gpxParser.parseGPX(inputStream)
}
}

val getPoints = (file: File) => for {
is <- createInputStream(file)
gpx <- parseGPX(is)
tracks <- Future(toListOfTracks(gpx))
trackPoints <- Future(toListOfTrackSegments(tracks))
_ <- Future(is.close())
} yield trackPoints

val getResults = (file: File) => for {
points <- getPoints(file)
segments <- Future(toSegments(points))
totalPoints <- Future(points.size)
totalDistance <- Future(calculateTotalDistance(segments))
avgDistance <- Future(calculateAverageDistance(segments))
minDistance <- Future(getMinimunDistance(segments))
maxDistance <- Future(getMaximunDistance(segments))
} yield Results(file.getName, totalPoints, totalDistance, avgDistance, minDistance, maxDistance)

}
7 changes: 7 additions & 0 deletions src/main/scala/inspide/test/Output.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package inspide.test

trait Output {

def print(s: String) = Console.print(s)

}
24 changes: 24 additions & 0 deletions src/main/scala/inspide/test/SolutionApp.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package inspide.test

import java.nio.file.Paths

import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration

class SolutionApp(directory: String) extends Output {

val mainFn = for {
gpxFiles <- Future(Paths.get(directory).toFile.listFiles().toList)
results <- Future.sequence(gpxFiles.map(GPXParser.getResults))
csv <- Future(CSV.toCSV(results))
out <- Future(print(csv))
} yield out

}

object SolutionApp extends App {

Await.result(new SolutionApp(args(0)).mainFn, Duration.Inf)

}
10 changes: 10 additions & 0 deletions src/main/scala/inspide/test/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package inspide

package object test {

case class Point(lat: Double, lon: Double)
case class Segment(from: Point, to: Point)

case class Results(id:String, totalNumberOfPoints: Int, totalDistance: Double, avgDistance: Double, minDistance: Double, maxDistance: Double)

}
37 changes: 37 additions & 0 deletions src/test/resources/test.gpx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx creator="Wikiloc - https://www.wikiloc.com" version="1.1" xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>Wikiloc - Panoramica madrid</name>
<author>
<name>vmoscardo</name>
<link href="https://www.wikiloc.com/wikiloc/user.do?id=1931448">
<text>vmoscardo on Wikiloc</text>
</link>
</author>
<link href="https://www.wikiloc.com/car-trails/panoramica-madrid-12109675">
<text>Panoramica madrid on Wikiloc</text>
</link>
<time>2016-01-30T13:41:52Z</time>
</metadata>
<trk>
<name>Panoramica madrid</name>
<cmt></cmt>
<desc></desc>
<trkseg>
<trkpt lat="40.492861" lon="-3.593716">
<ele>877.089</ele>
<time>2016-01-30T11:32:41Z</time>
</trkpt>
<trkpt lat="40.492528" lon="-3.593178">
<ele>842.006</ele>
<time>2016-01-30T11:32:42Z</time>
</trkpt>
<trkpt lat="40.493062" lon="-3.593395">
<ele>895.026</ele>
<time>2016-01-30T11:32:43Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>
23 changes: 23 additions & 0 deletions src/test/scala/inspide/test/CSVSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package inspide.test

import org.scalatest.GivenWhenThen
import org.scalatest.featurespec.AnyFeatureSpecLike

class CSVSpec extends AnyFeatureSpecLike with GivenWhenThen {

private val ExpectedResults = Seq(
Results("f1.gpx", 1, 2, 3, 4, 5),
Results("f2.gpx", 6, 7, 8, 9, 10),
)

Scenario("Create CSV") {
When("the results are formatted as CSV")
val csv = CSV.toCSV(ExpectedResults)
Then("the csv is the expected")
assert(csv ==
StringContext.treatEscapes("""id\ttotalNumberOfPoints\ttotalDistance\tavgDistance\tminDistance\tmaxDistance
|f2.gpx\t6\t7.0\t8.0\t9.0\t10.0
|f1.gpx\t1\t2.0\t3.0\t4.0\t5.0""".stripMargin))
}

}
78 changes: 78 additions & 0 deletions src/test/scala/inspide/test/DistanceCalculatorSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package inspide.test

import org.scalatest.GivenWhenThen
import org.scalatest.featurespec.AnyFeatureSpecLike

/**
* The distance fixtures have been calculated at https://edwilliams.org/gccalc.htm
*/
class DistanceCalculatorSpec extends AnyFeatureSpecLike with GivenWhenThen {

import inspide.test.DistanceCalculator._

Feature("Calculate distances") {

Scenario("Distance between points") {
Given("three points")
val point1 = Point(40.492861, -3.593716)
val point2 = Point(40.492528, -3.593178)
val point3 = Point(40.493062, -3.593395)
When("the distance between two of them is calculated")
val distanceFrom1To2 = calculateDistance(Segment(point1, point2))
val distanceFrom2To3 = calculateDistance(Segment(point2, point3))
Then("the results are the expected")
assert(distanceFrom1To2 == 58.716)
assert(distanceFrom2To3 == 62.086)
}

Scenario("Total distance between points") {
Given("three points")
val point1 = Point(40.492861, -3.593716)
val point2 = Point(40.492528, -3.593178)
val point3 = Point(40.493062, -3.593395)
When("the total distance is calculated")
val segments = toSegments(Seq(point1, point2, point3))
val distance = calculateTotalDistance(segments)
Then("the results are the expected")
assert(distance == 120.802)
}

Scenario("Average distance between points") {
Given("three points")
val point1 = Point(40.492861, -3.593716)
val point2 = Point(40.492528, -3.593178)
val point3 = Point(40.493062, -3.593395)
When("the average distance is calculated")
val segments = toSegments(Seq(point1, point2, point3))
val avgDistance = calculateAverageDistance(segments)
Then("the results are the expected")
assert(avgDistance == 60.401)
}

Scenario("Minimum distance between points") {
Given("three points")
val point1 = Point(40.492861, -3.593716)
val point2 = Point(40.492528, -3.593178)
val point3 = Point(40.493062, -3.593395)
When("the minimum distance is calculated")
val segments = toSegments(Seq(point1, point2, point3))
val minDistance = getMinimunDistance(segments)
Then("the results are the expected")
assert(minDistance == 58.716)
}

Scenario("Maximum distance between points") {
Given("three points")
val point1 = Point(40.492861, -3.593716)
val point2 = Point(40.492528, -3.593178)
val point3 = Point(40.493062, -3.593395)
When("the maximum distance is calculated")
val segments = toSegments(Seq(point1, point2, point3))
val maxDistance = getMaximunDistance(segments)
Then("the results are the expected")
assert(maxDistance == 62.086)
}

}

}
Loading