Skip to content

Commit

Permalink
Reworked using Udash Scala.js
Browse files Browse the repository at this point in the history
  • Loading branch information
OndrejSpanel committed Oct 22, 2019
2 parents b6fbd86 + c71cf51 commit ac24da7
Show file tree
Hide file tree
Showing 203 changed files with 5,114 additions and 4,125 deletions.
13 changes: 8 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
# IntelliJ
/out/

# IntelliJ seems to build the artifact here when "root" is placed in file(".")
/classes/

# mpeltonen/sbt-idea plugin
.idea_modules/

/target/
/project/target/
target

/project/project/
/shared/target/
/push-uploader/target/

/src/main/resources/secret.txt
/backend/src/main/resources/secret.txt

/backend/src/main/resources/mixtio-*.json
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package com.github.opengrabeso.mixtio

import java.time.temporal.ChronoUnit

import com.google.appengine.api.ThreadManager
import mapbox.GetElevation
import org.joda.time.{ReadablePeriod, Seconds, DateTime => ZonedDateTime}
import java.time.{ZonedDateTime, Duration => JDuration}

import com.github.opengrabeso.mixtio.requests.BackgroundTasks

import scala.collection.immutable.SortedMap
import shared.Util._
import common.Util._
import common.model._
import shared.Timing

import scala.annotation.tailrec
import scala.concurrent.Await
import scala.concurrent.duration.Duration

@SerialVersionUID(-4477339787979943124L)
@SerialVersionUID(12L)
case class GPSPoint(latitude: Double, longitude: Double, elevation: Option[Int])(val in_accuracy: Option[Double]) {
override def toString = {
s"GPSPoint($latitude,$longitude,$elevation)($in_accuracy)"
Expand Down Expand Up @@ -162,7 +167,7 @@ sealed abstract class DataStream extends Serializable {
def timeOffset(bestOffset: Int): DataStream = {
val adjusted = stream.map{
case (k,v) =>
k.plus(bestOffset*1000) -> v
k.plus(bestOffset, ChronoUnit.SECONDS) -> v
}
pickData(adjusted)
}
Expand Down Expand Up @@ -205,9 +210,6 @@ object DataStreamGPS {
private type DistStream = SortedMap[ZonedDateTime, Double]
private type DistList = List[(ZonedDateTime, Double)]

// median, 80% percentile, max
case class SpeedStats(median: Double, fast: Double, max: Double)

object FilterSettings {
def none = new FilterSettings(0, 0, "None")
def weak = new FilterSettings(5, 3, "Weak")
Expand Down Expand Up @@ -279,7 +281,7 @@ object DataStreamGPS {
else {
assert(distDeltas.head._2 == 0)
val route = distDeltas.tail.scanLeft(distDeltas.head) { case ((tSum, dSum), (t, d)) =>
val dt = Seconds.secondsBetween(tSum, t).getSeconds
val dt = ChronoUnit.SECONDS.between(tSum, t)
t -> (dSum + d * dt)
}
route
Expand Down Expand Up @@ -354,7 +356,7 @@ object DataStreamGPS {
}
}

def dropEmptyPrefix(stream: ValueList, timeOffset: ReadablePeriod, compare: (ZonedDateTime, ZonedDateTime) => Boolean): ZonedDateTime = {
def dropEmptyPrefix(stream: ValueList, timeOffset: JDuration, compare: (ZonedDateTime, ZonedDateTime) => Boolean): ZonedDateTime = {
val prefixTime = detectEmptyPrefix(stream.head._1, new GPSRect(stream.head._2), stream, None)
prefixTime.map { case (prefTime, prefRect) =>
// trace back the prefix rectangle size
Expand All @@ -364,6 +366,7 @@ object DataStreamGPS {

val gpsDist = DataStreamGPS.distStreamFromGPSList(prefixRaw.map(_._2)).reverse

@scala.annotation.tailrec
def trackBackDistance(distances: Seq[Double], trace: Double, ret: Int): Int = {
if (trace <=0 || distances.isEmpty) ret
else {
Expand All @@ -385,8 +388,8 @@ object DataStreamGPS {
// drop beginning and end with no activity
def dropAlmostEmpty(stream: ValueList): Option[(ZonedDateTime, ZonedDateTime)] = {
if (stream.nonEmpty) {
val droppedPrefixTime = dropEmptyPrefix(stream, Seconds.seconds(-10), _ <= _)
val droppedPostfixTime = dropEmptyPrefix(stream.reverse, Seconds.seconds(+10), _ >= _)
val droppedPrefixTime = dropEmptyPrefix(stream, JDuration.ofSeconds(-10), _ <= _)
val droppedPostfixTime = dropEmptyPrefix(stream.reverse, JDuration.ofSeconds(+10), _ >= _)
if (droppedPrefixTime >= droppedPostfixTime) None
else Some((droppedPrefixTime, droppedPostfixTime))
} else None
Expand All @@ -395,7 +398,7 @@ object DataStreamGPS {

private def distStreamToCSV(ds: DistStream): String = {
val times = ds.keys.toSeq
val diffs = 0L +: (times zip times.drop(1)).map { case (t1, t2) => t2.getMillis - t1.getMillis }
val diffs = 0L +: (times zip times.drop(1)).map { case (t1, t2) => ChronoUnit.MILLIS.between(t1, t2) }
(ds zip diffs).map { case (kv, duration) =>
s"${kv._1},${duration/1000.0},${kv._2}"
}.mkString("\n")
Expand Down Expand Up @@ -462,132 +465,6 @@ class DataStreamGPS(override val stream: DataStreamGPS.GPSStream) extends DataSt
distStreamToCSV(smooth)
}

/*
* @param timeOffset in seconds
* */
private def errorToStream(offsetStream: DistList, speedStream: DistList): Double = {
if (offsetStream.isEmpty || speedStream.isEmpty) {
Double.MaxValue
} else {
// TODO: optimize: move speed smoothing out of this function
def maxTime(a: ZonedDateTime, b: ZonedDateTime) = if (a>b) a else b
def minTime(a: ZonedDateTime, b: ZonedDateTime) = if (a<b) a else b
val begMatch = maxTime(offsetStream.head._1, startTime.get)
val endMatch = minTime(offsetStream.last._1, endTime.get)
// ignore non-matching parts (prefix, postfix)
def selectInner[T](data: List[(ZonedDateTime, T)]) = data.dropWhile(_._1 < begMatch).takeWhile(_._1 < endMatch)
val distToMatch = selectInner(offsetStream)

val distPairs = distToMatch zip distToMatch.drop(1) // drop(1), not tail, because distToMatch may be empty
val speedToMatch = distPairs.map {
case ((aTime, aDist), (bTime, _)) => aTime -> aDist / Seconds.secondsBetween(aTime, bTime).getSeconds
}
val smoothedSpeed = selectInner(speedStream)

def compareSpeedHistory(fineSpeed: DistList, coarseSpeed: DistList, error: Double): Double = {
//
if (fineSpeed.isEmpty || coarseSpeed.isEmpty) error
else {
if (fineSpeed.head._1 < coarseSpeed.head._1) compareSpeedHistory(fineSpeed.tail, coarseSpeed, error)
else {
def square(x: Double) = x * x
val itemError = square(fineSpeed.head._2 - coarseSpeed.head._2)
compareSpeedHistory(fineSpeed.tail, coarseSpeed.tail, error + itemError * itemError)
}
}
}

if (smoothedSpeed.isEmpty || speedToMatch.isEmpty) {
Double.MaxValue
} else {
val error = compareSpeedHistory(smoothedSpeed, speedToMatch, 0)
error
}
}

}

/*
* @param 10 sec distance stream (provided by a Quest) */
private def findOffset(distanceStream: DistStream) = {
val distanceList = distanceStream.toList
val maxOffset = 60
val offsets = -maxOffset to maxOffset
val speedStream = computeSpeedStream.toList
val errors = for (offset <- offsets) yield {
val offsetStream = distanceList.map { case (k,v) =>
k.plus(Seconds.seconds(offset)) -> v
}
errorToStream(offsetStream, speedStream)
}
// TODO: prefer most central best error
val (minError, minErrorOffset) = (errors zip offsets).minBy(_._1)
// compute confidence: how much is the one we have selected reliable?
// the ones close may have similar metrics, that is expected, but none far away should have it


def confidenceForSolution(offsetCandidate: Int) = {
val confidences = (errors zip offsets).map { case (err, off) =>
if (off == offsetCandidate) 0
else {
val close = 1 - (off - offsetCandidate).abs / (2 * maxOffset).toDouble
(err - minError) * close
}
}

val confidence = confidences.sum
confidence
}

// smoothing causes offset in one direction
//val empiricalOffset = 30 // this is for Quest smoothing 5 and GPS smoothing (smoothingInterval) 30
val empiricalOffset = 13 // this is for Quest smoothing 5 and GPS smoothing (smoothingInterval) 60
(minErrorOffset + empiricalOffset, confidenceForSolution(minErrorOffset))
}

def adjustHrdStream(dist: DataStreamDist#DataMap): Int = {

// try first: assume user stops watch first, GPS pod quickly after, i.e. offset can be determined based on the end times

if (false) {
val distances = (dist.values.tail zip dist.values).map(ab => ab._1 - ab._2)

def smoothDistances(todo: Iterable[Double], window: Vector[Double], done: List[Double]): List[Double] = {
if (todo.isEmpty) done
else {
val smoothSize = 5
val newWindow = if (window.size < smoothSize) window :+ todo.head
else window.tail :+ todo.head
smoothDistances(todo.tail, newWindow, done :+ newWindow.sum / newWindow.size)
}
}

val distancesSmooth = smoothDistances(distances, Vector(), Nil)

//val distances10x = distancesSmooth.flatMap(d => List.fill(10)(d/10)).mkString("\n")
val distancesWithTimes = SortedMap((dist.keys zip distancesSmooth).toSeq: _*)
val (bestOffset, confidence) = findOffset(distancesWithTimes)
println(s"Quest offset $bestOffset from distance ${dist.last._2}, confidence $confidence")
}
val useEndOffset = false
//hrdMove.timeOffset(bestOffset)
if (useEndOffset) {
val gpsAfterHrd = 3000 // time in ms it takes to stop GPS after stopping HR

// match values: stream.last._1 = dist.stream.last._1 + xxxx + gpsAfterHrd

val endOffset = if (dist.nonEmpty) (stream.last._1.getMillis - dist.last._1.getMillis - gpsAfterHrd).toInt else 0

println(s"Offset based on stop time: $endOffset")
if (endOffset.abs < 10) {
// some smart verification between estimated and measured end offset
(endOffset / 1000.0).round.toInt
} else {
0
}
} else 0
}


override def optimize(eventTimes: EventTimes) = {

Expand Down Expand Up @@ -656,7 +533,7 @@ class DataStreamGPS(override val stream: DataStreamGPS.GPSStream) extends DataSt
val timing = Timing.start()
val cache = new GetElevation.TileCache
// TODO: handle 50 threads per request limitation gracefully
implicit val threadFactor = ThreadManager.currentRequestThreadFactory()
implicit val threadFactor = BackgroundTasks.currentRequestThreadFactory
val elevationFutures = stream.toVector.flatMap {
case (k, v) =>
v.elevation.map(elev => (k, elev, cache.possibleRange(v.longitude, v.latitude)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ abstract class DefineRequest(val handleUri: String, val method: Method = Method.
val xmlPrefix = """<?xml version="1.0" encoding="UTF-8"?>""" + "\n"
xmlPrefix + nodes.toString
}
} else resp.raw
} else resp
}

def html(request: Request, resp: Response): NodeSeq
Expand All @@ -72,7 +72,7 @@ abstract class DefineRequest(val handleUri: String, val method: Method = Method.
<table>
<tr>
<td>
<a href="/">{shared.appName}</a>
<a href="/">{appName}</a>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -102,7 +102,7 @@ abstract class DefineRequest(val handleUri: String, val method: Method = Method.
<a href="http://labs.strava.com/" id="powered_by_strava" rel="nofollow">
<img align="left" src="static/api_logo_pwrdBy_strava_horiz_white.png" style="max-height:46px"/>
</a>
<p style="color:#fff"><a href="https://darksky.net/poweredby/" style="color:#fff">Powered by Dark Sky</a> © 2016 - 2018 <a href={s"https://github.com/OndrejSpanel/${shared.gitHubName}"} style="color:inherit">Ondřej Španěl</a></p>
<p style="color:#fff"><a href="https://darksky.net/poweredby/" style="color:#fff">Powered by Dark Sky</a> © 2016 - 2018 <a href={s"https://github.com/OndrejSpanel/${gitHubName}"} style="color:inherit">Ondřej Španěl</a></p>
<div/>
</div>
}
Expand Down Expand Up @@ -179,7 +179,7 @@ abstract class DefineRequest(val handleUri: String, val method: Method = Method.
<html>
<head>
{headPrefix}
<title>{shared.appName}</title>
<title>{appName}</title>
</head>
<body>
{
Expand All @@ -196,12 +196,12 @@ abstract class DefineRequest(val handleUri: String, val method: Method = Method.
if (showSuuntoUploadInstructions) {
<h4>Suunto Upload</h4>
<p>
If you want to upload Suunto files, start the {shared.appName} Start application
If you want to upload Suunto files, start the {appName} Start application
which will open a new web page with the upload progress.
</p>
<p>
The application can be downloaded from
<a href={s"https://github.com/OndrejSpanel/${shared.gitHubName}/releases"}>GitHub {shared.gitHubName} Releases page</a>
<a href={s"https://github.com/OndrejSpanel/${gitHubName}/releases"}>GitHub {gitHubName} Releases page</a>
.
</p>
} else NodeSeq.Empty
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.github.opengrabeso.mixtio

import java.time.temporal.ChronoUnit

import Main._
import org.joda.time.{Days, DateTime => ZonedDateTime}
import java.time.{ZoneId, ZonedDateTime}

trait FileStore {
type FileItem
Expand Down Expand Up @@ -37,7 +39,7 @@ trait FileStore {

def cleanup(): Int = {
val list = listAllItems()
val now = new ZonedDateTime()
val now = ZonedDateTime.now()

val ops = for (i <- list) yield {
val name = itemName(i)
Expand All @@ -48,8 +50,8 @@ trait FileStore {
maxDays <- maxAgeInDays
modTime <- itemModified(i)
} yield {
val fileTime = new ZonedDateTime(modTime)
val age = Days.daysBetween(fileTime, now).getDays
val fileTime = ZonedDateTime.ofInstant(modTime.toInstant, ZoneId.systemDefault)
val age = ChronoUnit.DAYS.between(fileTime, now)
if (age >= maxDays) {
deleteItem(i)
println(s"Cleaned $name age $age, date $fileTime")
Expand Down
Loading

0 comments on commit ac24da7

Please sign in to comment.