From a8579445e3aba9eadef491492dba53a26f9806c2 Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Thu, 13 Feb 2020 23:26:52 +0100 Subject: [PATCH 1/9] Core functions --- build.sbt | 16 ++++ project/Dependencies.scala | 21 +++++ project/build.properties | 1 + project/plugins.sbt | 1 + .../inspide/test/DistanceCalculator.scala | 28 +++++++ src/main/scala/inspide/test/package.scala | 8 ++ .../inspide/test/DistanceCalculatorSpec.scala | 79 +++++++++++++++++++ 7 files changed, 154 insertions(+) create mode 100644 build.sbt create mode 100644 project/Dependencies.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/inspide/test/DistanceCalculator.scala create mode 100644 src/main/scala/inspide/test/package.scala create mode 100644 src/test/scala/inspide/test/DistanceCalculatorSpec.scala diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..61a270c --- /dev/null +++ b/build.sbt @@ -0,0 +1,16 @@ +import Dependencies._ + +organization := "inspide" +name := "solution" +version := "1.0.0" + +scalaVersion := "2.12.10" + +enablePlugins(JavaAppPackaging) + + +resolvers += "Osgeo Repo" at "http://download.osgeo.org/webdav/geotools/" +resolvers += "Boundless" at "http://repo.boundlessgeo.com/main" + +libraryDependencies ++= coreDependencies +dependencyOverrides ++= coreDependencyOverrides diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 0000000..c42b2d4 --- /dev/null +++ b/project/Dependencies.scala @@ -0,0 +1,21 @@ +import sbt._ + +object Dependencies { + val Poi4sVersion = "0.0.2" + val GeotoolsVersion = "22.3" + + val ScalatestVersion = "3.1.0" + + val coreMainDependencies = Seq( + "software.purpledragon" %% "poi4s-gpx" % Poi4sVersion % Compile, + "org.geotools" % "gt-referencing" % GeotoolsVersion % Compile, + ) + + val testDependencies = Seq( + "org.scalatest" %% "scalatest" % ScalatestVersion % Test, + ) + + val coreDependencies = coreMainDependencies ++ testDependencies + val coreDependencyOverrides = Seq() + +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c0bab04 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..b248342 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.1") diff --git a/src/main/scala/inspide/test/DistanceCalculator.scala b/src/main/scala/inspide/test/DistanceCalculator.scala new file mode 100644 index 0000000..eb94f9a --- /dev/null +++ b/src/main/scala/inspide/test/DistanceCalculator.scala @@ -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]) => segments.map(calculateDistance).sum + + val calculateAverageDistance = (segments: Seq[Segment]) => calculateTotalDistance(segments) / segments.length + + val getMinimunDistance = (segments: Seq[Segment]) => segments.map(calculateDistance).min + + val getMaximunDistance = (segments: Seq[Segment]) => segments.map(calculateDistance).max + +} diff --git a/src/main/scala/inspide/test/package.scala b/src/main/scala/inspide/test/package.scala new file mode 100644 index 0000000..0e6642e --- /dev/null +++ b/src/main/scala/inspide/test/package.scala @@ -0,0 +1,8 @@ +package inspide + +package object test { + + case class Point(lat: Double, lon: Double) + case class Segment(from: Point, to: Point) + +} diff --git a/src/test/scala/inspide/test/DistanceCalculatorSpec.scala b/src/test/scala/inspide/test/DistanceCalculatorSpec.scala new file mode 100644 index 0000000..35a4cc9 --- /dev/null +++ b/src/test/scala/inspide/test/DistanceCalculatorSpec.scala @@ -0,0 +1,79 @@ +package inspide.test + +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpecLike +import software.purpledragon.poi4s.model.Waypoint + +/** + * 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 == 58.716 + 62.086) + } + + 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 == (58.716 + 62.086) / 2) + } + + 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) + } + + } + +} From 29d48a385f08f968938c5f683835f7c76d7677d1 Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Thu, 13 Feb 2020 23:37:15 +0100 Subject: [PATCH 2/9] User assembly plugin instead of sbt-native --- build.sbt | 6 +++++- project/plugins.sbt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 61a270c..e2e36fb 100644 --- a/build.sbt +++ b/build.sbt @@ -6,8 +6,12 @@ version := "1.0.0" scalaVersion := "2.12.10" -enablePlugins(JavaAppPackaging) +assemblyJarName in assembly := "solution.jar" +assemblyMergeStrategy in assembly := { + case PathList("META-INF", xs @ _*) => MergeStrategy.discard + case x => MergeStrategy.first +} resolvers += "Osgeo Repo" at "http://download.osgeo.org/webdav/geotools/" resolvers += "Boundless" at "http://repo.boundlessgeo.com/main" diff --git a/project/plugins.sbt b/project/plugins.sbt index b248342..c9c0554 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.1") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") \ No newline at end of file From 8da580613e90ee15448f081c87d0007ca296d9a3 Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Fri, 14 Feb 2020 01:44:50 +0100 Subject: [PATCH 3/9] Finished the calculations for a single file --- build.sbt | 1 + project/Dependencies.scala | 4 +- src/main/scala/inspide/test/GPXParser.scala | 46 ++++++++++++++++ src/main/scala/inspide/test/package.scala | 2 + src/test/resources/test.gpx | 37 +++++++++++++ .../inspide/test/DistanceCalculatorSpec.scala | 1 - .../scala/inspide/test/GPXParserSpec.scala | 52 +++++++++++++++++++ 7 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/main/scala/inspide/test/GPXParser.scala create mode 100644 src/test/resources/test.gpx create mode 100644 src/test/scala/inspide/test/GPXParserSpec.scala diff --git a/build.sbt b/build.sbt index e2e36fb..2dacfc2 100644 --- a/build.sbt +++ b/build.sbt @@ -15,6 +15,7 @@ assemblyMergeStrategy in assembly := { resolvers += "Osgeo Repo" at "http://download.osgeo.org/webdav/geotools/" resolvers += "Boundless" at "http://repo.boundlessgeo.com/main" +resolvers += "Bintray" at "https://jcenter.bintray.com/" libraryDependencies ++= coreDependencies dependencyOverrides ++= coreDependencyOverrides diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c42b2d4..44e6f11 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,13 +1,13 @@ import sbt._ object Dependencies { - val Poi4sVersion = "0.0.2" + val GPXParserVersion = "1.12" val GeotoolsVersion = "22.3" val ScalatestVersion = "3.1.0" val coreMainDependencies = Seq( - "software.purpledragon" %% "poi4s-gpx" % Poi4sVersion % Compile, + "me.himanshusoni.gpxparser" % "gpx-parser" % GPXParserVersion % Compile, "org.geotools" % "gt-referencing" % GeotoolsVersion % Compile, ) diff --git a/src/main/scala/inspide/test/GPXParser.scala b/src/main/scala/inspide/test/GPXParser.scala new file mode 100644 index 0000000..415b59b --- /dev/null +++ b/src/main/scala/inspide/test/GPXParser.scala @@ -0,0 +1,46 @@ +package inspide.test + +import java.io.{File, FileInputStream} + +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 + + val getPoints = (file: File) => for { + fileParser <- Future(new GPXFileParser) + is <- createInputStream(file) + gpx <- Future(fileParser.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(totalPoints, totalDistance, avgDistance, minDistance, maxDistance) + +} diff --git a/src/main/scala/inspide/test/package.scala b/src/main/scala/inspide/test/package.scala index 0e6642e..cb6b27a 100644 --- a/src/main/scala/inspide/test/package.scala +++ b/src/main/scala/inspide/test/package.scala @@ -5,4 +5,6 @@ package object test { case class Point(lat: Double, lon: Double) case class Segment(from: Point, to: Point) + case class Results(totalNumberOfPoints: Int, totalDistance: Double, avgDistance: Double, minDistance: Double, maxDistance: Double) + } diff --git a/src/test/resources/test.gpx b/src/test/resources/test.gpx new file mode 100644 index 0000000..8b8c28a --- /dev/null +++ b/src/test/resources/test.gpx @@ -0,0 +1,37 @@ + + + + Wikiloc - Panoramica madrid + + vmoscardo + + vmoscardo on Wikiloc + + + + Panoramica madrid on Wikiloc + + + + + Panoramica madrid + + + + + 877.089 + + + + 842.006 + + + + 895.026 + + + + + \ No newline at end of file diff --git a/src/test/scala/inspide/test/DistanceCalculatorSpec.scala b/src/test/scala/inspide/test/DistanceCalculatorSpec.scala index 35a4cc9..6e560ab 100644 --- a/src/test/scala/inspide/test/DistanceCalculatorSpec.scala +++ b/src/test/scala/inspide/test/DistanceCalculatorSpec.scala @@ -2,7 +2,6 @@ package inspide.test import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpecLike -import software.purpledragon.poi4s.model.Waypoint /** * The distance fixtures have been calculated at https://edwilliams.org/gccalc.htm diff --git a/src/test/scala/inspide/test/GPXParserSpec.scala b/src/test/scala/inspide/test/GPXParserSpec.scala new file mode 100644 index 0000000..58ea004 --- /dev/null +++ b/src/test/scala/inspide/test/GPXParserSpec.scala @@ -0,0 +1,52 @@ +package inspide.test + +import java.nio.file.Paths + + +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpecLike + +import scala.concurrent.Await +import scala.concurrent.duration._ + + +class GPXParserSpec extends AnyFeatureSpecLike with GivenWhenThen { + + private val ExpectedSegments = List( + Point(40.492861, -3.593716), + Point(40.492528, -3.593178), + Point(40.493062, -3.593395) + ) + + private val ExpectedResults = Results( + 3, + 58.716 + 62.086, + (58.716 + 62.086) / 2, + 58.716, + 62.086 + ) + + Feature("Parse GPX files") { + + Scenario("Parse GPX file") { + Given("a GPX file") + val file = Paths.get("src/test/resources", "test.gpx").toFile + When("the points are required") + val points = GPXParser.getPoints(file) + Then("the segments are the expected") + assert(Await.result(points, 5 seconds) == ExpectedSegments) + } + + Scenario("Get results for file") { + Given("a GPX file") + val file = Paths.get("src/test/resources", "test.gpx").toFile + When("the results are required") + val results = GPXParser.getResults(file) + + Then("the results are the expected") + assert(Await.result(results, 5 seconds) == ExpectedResults) + } + + } + +} From cfdf562749478b46e2942bb538fde76c71a8cdec Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Fri, 14 Feb 2020 17:13:37 +0100 Subject: [PATCH 4/9] Main method created --- src/main/scala/inspide/test/CSV.scala | 13 +++++++++ .../inspide/test/DistanceCalculator.scala | 8 ++--- src/main/scala/inspide/test/GPXParser.scala | 4 +-- src/main/scala/inspide/test/Output.scala | 7 +++++ src/main/scala/inspide/test/SolutionApp.scala | 29 +++++++++++++++++++ src/main/scala/inspide/test/package.scala | 2 +- src/test/scala/inspide/test/CSVSpec.scala | 23 +++++++++++++++ .../inspide/test/DistanceCalculatorSpec.scala | 4 +-- .../scala/inspide/test/GPXParserSpec.scala | 5 ++-- .../scala/inspide/test/SolutionAppSpec.scala | 24 +++++++++++++++ 10 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 src/main/scala/inspide/test/CSV.scala create mode 100644 src/main/scala/inspide/test/Output.scala create mode 100644 src/main/scala/inspide/test/SolutionApp.scala create mode 100644 src/test/scala/inspide/test/CSVSpec.scala create mode 100644 src/test/scala/inspide/test/SolutionAppSpec.scala diff --git a/src/main/scala/inspide/test/CSV.scala b/src/main/scala/inspide/test/CSV.scala new file mode 100644 index 0000000..dcb6642 --- /dev/null +++ b/src/main/scala/inspide/test/CSV.scala @@ -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") + +} diff --git a/src/main/scala/inspide/test/DistanceCalculator.scala b/src/main/scala/inspide/test/DistanceCalculator.scala index eb94f9a..600c18b 100644 --- a/src/main/scala/inspide/test/DistanceCalculator.scala +++ b/src/main/scala/inspide/test/DistanceCalculator.scala @@ -17,12 +17,12 @@ object DistanceCalculator { val toSegments = (points: Seq[Point]) => points.zip(points.tail).map(p => Segment(p._1, p._2)) - val calculateTotalDistance = (segments: Seq[Segment]) => segments.map(calculateDistance).sum + val calculateTotalDistance = (segments: Seq[Segment]) => fixTo3Decimals(segments.map(calculateDistance).sum) - val calculateAverageDistance = (segments: Seq[Segment]) => calculateTotalDistance(segments) / segments.length + val calculateAverageDistance = (segments: Seq[Segment]) => fixTo3Decimals(calculateTotalDistance(segments) / segments.length) - val getMinimunDistance = (segments: Seq[Segment]) => segments.map(calculateDistance).min + val getMinimunDistance = (segments: Seq[Segment]) => fixTo3Decimals(segments.map(calculateDistance).min) - val getMaximunDistance = (segments: Seq[Segment]) => segments.map(calculateDistance).max + val getMaximunDistance = (segments: Seq[Segment]) => fixTo3Decimals(segments.map(calculateDistance).max) } diff --git a/src/main/scala/inspide/test/GPXParser.scala b/src/main/scala/inspide/test/GPXParser.scala index 415b59b..09516bd 100644 --- a/src/main/scala/inspide/test/GPXParser.scala +++ b/src/main/scala/inspide/test/GPXParser.scala @@ -25,7 +25,7 @@ object GPXParser { private val toListOfTracks = (gpx: GPX) => gpx.getTracks.asScala.toList val getPoints = (file: File) => for { - fileParser <- Future(new GPXFileParser) + fileParser <- Future(new GPXFileParser()) is <- createInputStream(file) gpx <- Future(fileParser.parseGPX(is)) tracks <- Future(toListOfTracks(gpx)) @@ -41,6 +41,6 @@ object GPXParser { avgDistance <- Future(calculateAverageDistance(segments)) minDistance <- Future(getMinimunDistance(segments)) maxDistance <- Future(getMaximunDistance(segments)) - } yield Results(totalPoints, totalDistance, avgDistance, minDistance, maxDistance) + } yield Results(file.getName ,totalPoints, totalDistance, avgDistance, minDistance, maxDistance) } diff --git a/src/main/scala/inspide/test/Output.scala b/src/main/scala/inspide/test/Output.scala new file mode 100644 index 0000000..54c42c0 --- /dev/null +++ b/src/main/scala/inspide/test/Output.scala @@ -0,0 +1,7 @@ +package inspide.test + +trait Output { + + def print(s: String) = Console.print(s) + +} diff --git a/src/main/scala/inspide/test/SolutionApp.scala b/src/main/scala/inspide/test/SolutionApp.scala new file mode 100644 index 0000000..063f8c7 --- /dev/null +++ b/src/main/scala/inspide/test/SolutionApp.scala @@ -0,0 +1,29 @@ +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 { + + val directory = args.toList match { + case Nil => "data" + case dir :: _ => dir + } + + Await.result(new SolutionApp(directory).mainFn, Duration.Inf) + +} diff --git a/src/main/scala/inspide/test/package.scala b/src/main/scala/inspide/test/package.scala index cb6b27a..cd899bb 100644 --- a/src/main/scala/inspide/test/package.scala +++ b/src/main/scala/inspide/test/package.scala @@ -5,6 +5,6 @@ package object test { case class Point(lat: Double, lon: Double) case class Segment(from: Point, to: Point) - case class Results(totalNumberOfPoints: Int, totalDistance: Double, avgDistance: Double, minDistance: Double, maxDistance: Double) + case class Results(id:String, totalNumberOfPoints: Int, totalDistance: Double, avgDistance: Double, minDistance: Double, maxDistance: Double) } diff --git a/src/test/scala/inspide/test/CSVSpec.scala b/src/test/scala/inspide/test/CSVSpec.scala new file mode 100644 index 0000000..59f0792 --- /dev/null +++ b/src/test/scala/inspide/test/CSVSpec.scala @@ -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)) + } + +} diff --git a/src/test/scala/inspide/test/DistanceCalculatorSpec.scala b/src/test/scala/inspide/test/DistanceCalculatorSpec.scala index 6e560ab..3b52022 100644 --- a/src/test/scala/inspide/test/DistanceCalculatorSpec.scala +++ b/src/test/scala/inspide/test/DistanceCalculatorSpec.scala @@ -34,7 +34,7 @@ class DistanceCalculatorSpec extends AnyFeatureSpecLike with GivenWhenThen { val segments = toSegments(Seq(point1, point2, point3)) val distance = calculateTotalDistance(segments) Then("the results are the expected") - assert(distance == 58.716 + 62.086) + assert(distance == 120.802) } Scenario("Average distance between points") { @@ -46,7 +46,7 @@ class DistanceCalculatorSpec extends AnyFeatureSpecLike with GivenWhenThen { val segments = toSegments(Seq(point1, point2, point3)) val avgDistance = calculateAverageDistance(segments) Then("the results are the expected") - assert(avgDistance == (58.716 + 62.086) / 2) + assert(avgDistance == 60.401) } Scenario("Minimum distance between points") { diff --git a/src/test/scala/inspide/test/GPXParserSpec.scala b/src/test/scala/inspide/test/GPXParserSpec.scala index 58ea004..d825a6b 100644 --- a/src/test/scala/inspide/test/GPXParserSpec.scala +++ b/src/test/scala/inspide/test/GPXParserSpec.scala @@ -19,9 +19,10 @@ class GPXParserSpec extends AnyFeatureSpecLike with GivenWhenThen { ) private val ExpectedResults = Results( + "test.gpx", 3, - 58.716 + 62.086, - (58.716 + 62.086) / 2, + 120.802, + 60.401, 58.716, 62.086 ) diff --git a/src/test/scala/inspide/test/SolutionAppSpec.scala b/src/test/scala/inspide/test/SolutionAppSpec.scala new file mode 100644 index 0000000..ebd5952 --- /dev/null +++ b/src/test/scala/inspide/test/SolutionAppSpec.scala @@ -0,0 +1,24 @@ +package inspide.test + + +import org.scalatest.featurespec.AnyFeatureSpecLike + +import scala.concurrent.Await +import scala.concurrent.duration._ + +class SolutionAppSpec extends AnyFeatureSpecLike { + + trait FakeOutput extends Output { + var out = "" + override def print(str: String) = out = out + str + } + + Scenario("Generate results") { + val app = new SolutionApp("src/test/resources") with FakeOutput + Await.result(app.mainFn, 30 seconds) + assert(app.out == StringContext.treatEscapes( + """id\ttotalNumberOfPoints\ttotalDistance\tavgDistance\tminDistance\tmaxDistance + |test.gpx\t3\t120.802\t60.401\t58.716\t62.086""".stripMargin)) + } + +} From f5c6189f4c4390aa41bdbe4e80fc0c0ba0f0e826 Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Fri, 14 Feb 2020 18:13:42 +0100 Subject: [PATCH 5/9] Documentation --- README_DEV.md | 15 +++++++++++++++ src/main/scala/inspide/test/SolutionApp.scala | 7 +------ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 README_DEV.md diff --git a/README_DEV.md b/README_DEV.md new file mode 100644 index 0000000..d1142fa --- /dev/null +++ b/README_DEV.md @@ -0,0 +1,15 @@ +#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. + +#Errores en stderr +Aunque no he entrado en profundidad, diría que la librería que parsea GPX tiene un error de programación. En +https://github.com/himanshu-soni/gpx-parser/blob/master/src/main/java/me/himanshusoni/gpxparser/BaseGPX.java#L15 crea un +SimpledateFormat estático y SimpleDateFormat no es thread-safe. Normalmente yo no usaría Future, usaría Task de Monix o +IO de Cats para ejecutar los side-effects. He usado Future para ceñirme estríctamente al enunciado sobre el uso de +librerías. + +Para ver mejor los resultados sugiero usar +```scala +java -jar target/scala-2.12/solution.jar data 2> /dev/null +``` diff --git a/src/main/scala/inspide/test/SolutionApp.scala b/src/main/scala/inspide/test/SolutionApp.scala index 063f8c7..ea7c543 100644 --- a/src/main/scala/inspide/test/SolutionApp.scala +++ b/src/main/scala/inspide/test/SolutionApp.scala @@ -19,11 +19,6 @@ class SolutionApp(directory: String) extends Output { object SolutionApp extends App { - val directory = args.toList match { - case Nil => "data" - case dir :: _ => dir - } - - Await.result(new SolutionApp(directory).mainFn, Duration.Inf) + Await.result(new SolutionApp(args(0)).mainFn, Duration.Inf) } From 656d308f383965114fe2ca15a879b70218d8609a Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Fri, 14 Feb 2020 18:38:43 +0100 Subject: [PATCH 6/9] docker deployment --- README_DEV.md | 13 +++++++++++++ src/main/docker/Dockerfile | 4 ++++ 2 files changed, 17 insertions(+) create mode 100644 src/main/docker/Dockerfile diff --git a/README_DEV.md b/README_DEV.md index d1142fa..52309de 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -13,3 +13,16 @@ Para ver mejor los resultados sugiero usar ```scala java -jar target/scala-2.12/solution.jar data 2> /dev/null ``` + +* Si es necesario podría buscar otra librería para parsear los GPX + +#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 +``` diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile new file mode 100644 index 0000000..73dd18e --- /dev/null +++ b/src/main/docker/Dockerfile @@ -0,0 +1,4 @@ +FROM azul/zulu-openjdk:8u242 +WORKDIR /opt/ +COPY target/scala-2.12/solution.jar . +ENTRYPOINT ["java", "-jar", "solution.jar", "data"] \ No newline at end of file From 54f16189ec05288e6589d4ef1f6b2d61034f51aa Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Fri, 14 Feb 2020 18:56:23 +0100 Subject: [PATCH 7/9] finish documentation --- README_DEV.md | 55 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/README_DEV.md b/README_DEV.md index 52309de..a09c79b 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -1,28 +1,37 @@ -#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. +# Compilación +La primera vez que se ejecute sbt clean assembly puede ocurrir que falle la descarga de una librería +(javax.media#jai_core;1.1.3!jai_core.jar) que esta en las dependencias transitivas. ***Ejecutar de nuevo*** y no debería +volver a fallar. -#Errores en stderr -Aunque no he entrado en profundidad, diría que la librería que parsea GPX tiene un error de programación. En -https://github.com/himanshu-soni/gpx-parser/blob/master/src/main/java/me/himanshusoni/gpxparser/BaseGPX.java#L15 crea un -SimpledateFormat estático y SimpleDateFormat no es thread-safe. Normalmente yo no usaría Future, usaría Task de Monix o -IO de Cats para ejecutar los side-effects. He usado Future para ceñirme estríctamente al enunciado sobre el uso de -librerías. -Para ver mejor los resultados sugiero usar -```scala -java -jar target/scala-2.12/solution.jar data 2> /dev/null -``` +# 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. -* Si es necesario podría buscar otra librería para parsear los GPX -#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 +# Errores en stderr +Aunque no he entrado en profundidad, diría que la librería que parsea GPX tiene un error de programación. En +https://github.com/himanshu-soni/gpx-parser/blob/master/src/main/java/me/himanshusoni/gpxparser/BaseGPX.java#L15 crea un +SimpledateFormat estático y SimpleDateFormat no es thread-safe. Normalmente yo no usaría Future, usaría Task de Monix o +IO de Cats para ejecutar los side-effects. He usado Future para ceñirme estríctamente al enunciado sobre el uso de +librerías. + +Para ver mejor los resultados sugiero usar + +```scala +java -jar target/scala-2.12/solution.jar data 2> /dev/null +``` + +Si es necesario podría buscar otra librería para parsear los GPX + +# 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 ``` From fa05b9d4ee725a96fa277255827358ba0f7005a0 Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Sun, 16 Feb 2020 13:34:40 +0100 Subject: [PATCH 8/9] Synchronize the usage of GPXFileParser because it is not thread safe (see my previous commit to check the differences) --- README_DEV.md | 15 -------------- src/main/scala/inspide/test/GPXParser.scala | 22 +++++++++++++++++---- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/README_DEV.md b/README_DEV.md index a09c79b..24cbf61 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -7,22 +7,7 @@ volver a fallar. # 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. - - -# Errores en stderr -Aunque no he entrado en profundidad, diría que la librería que parsea GPX tiene un error de programación. En -https://github.com/himanshu-soni/gpx-parser/blob/master/src/main/java/me/himanshusoni/gpxparser/BaseGPX.java#L15 crea un -SimpledateFormat estático y SimpleDateFormat no es thread-safe. Normalmente yo no usaría Future, usaría Task de Monix o -IO de Cats para ejecutar los side-effects. He usado Future para ceñirme estríctamente al enunciado sobre el uso de -librerías. - -Para ver mejor los resultados sugiero usar - -```scala -java -jar target/scala-2.12/solution.jar data 2> /dev/null -``` -Si es necesario podría buscar otra librería para parsear los GPX # Docker No tengo muy claro como se esperaba la creación del contenedor de docker. Así que lo he hecho muy sencillo. diff --git a/src/main/scala/inspide/test/GPXParser.scala b/src/main/scala/inspide/test/GPXParser.scala index 09516bd..7b0f1a6 100644 --- a/src/main/scala/inspide/test/GPXParser.scala +++ b/src/main/scala/inspide/test/GPXParser.scala @@ -1,6 +1,6 @@ package inspide.test -import java.io.{File, FileInputStream} +import java.io.{File, FileInputStream, InputStream} import inspide.test.DistanceCalculator._ import me.himanshusoni.gpxparser.modal.{GPX, Track, TrackSegment} @@ -24,10 +24,24 @@ object GPXParser { 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 { - fileParser <- Future(new GPXFileParser()) is <- createInputStream(file) - gpx <- Future(fileParser.parseGPX(is)) + gpx <- parseGPX(is) tracks <- Future(toListOfTracks(gpx)) trackPoints <- Future(toListOfTrackSegments(tracks)) _ <- Future(is.close()) @@ -41,6 +55,6 @@ object GPXParser { avgDistance <- Future(calculateAverageDistance(segments)) minDistance <- Future(getMinimunDistance(segments)) maxDistance <- Future(getMaximunDistance(segments)) - } yield Results(file.getName ,totalPoints, totalDistance, avgDistance, minDistance, maxDistance) + } yield Results(file.getName, totalPoints, totalDistance, avgDistance, minDistance, maxDistance) } From 2f3f5e224b114ca125d7df59f69679e9a7d81b22 Mon Sep 17 00:00:00 2001 From: Sergio Aparicio Date: Sun, 16 Feb 2020 14:08:46 +0100 Subject: [PATCH 9/9] change the build script to avoid a possible problem downloading a transitive dependency --- README_DEV.md | 6 ------ build.sbt | 2 +- project/Dependencies.scala | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README_DEV.md b/README_DEV.md index 24cbf61..a730097 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -1,9 +1,3 @@ -# Compilación -La primera vez que se ejecute sbt clean assembly puede ocurrir que falle la descarga de una librería -(javax.media#jai_core;1.1.3!jai_core.jar) que esta en las dependencias transitivas. ***Ejecutar de nuevo*** y no debería -volver a fallar. - - # 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. diff --git a/build.sbt b/build.sbt index 2dacfc2..bc95779 100644 --- a/build.sbt +++ b/build.sbt @@ -13,9 +13,9 @@ assemblyMergeStrategy in assembly := { 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" -resolvers += "Bintray" at "https://jcenter.bintray.com/" libraryDependencies ++= coreDependencies dependencyOverrides ++= coreDependencyOverrides diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 44e6f11..45f3c50 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,6 +9,12 @@ object Dependencies { 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(