Skip to content

Commit

Permalink
Add initial timeline (see #39)
Browse files Browse the repository at this point in the history
  • Loading branch information
v6ak committed Sep 27, 2023
1 parent 6e1dfb3 commit e5f6862
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 4 deletions.
9 changes: 9 additions & 0 deletions client/src/main/scala/com/v6ak/scalajs/time/Pace.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.v6ak.scalajs.time

case class Pace(secondsPerKm: Int) {
def minPart: Int = secondsPerKm / 60

def secPart: Int = secondsPerKm % 60

override def toString: String = f"$minPart:$secPart%02d"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ package com.v6ak.scalajs.time

import com.v6ak.scalajs.regex.JsPattern.wrapString

import java.math.MathContext

case class TimeInterval(totalMinutes: Int) extends AnyVal{
def /(trackLength: BigDecimal): Pace = {
val paceSecPerKm = totalMinutes * 60.0 / trackLength
Pace(paceSecPerKm.round(MathContext.UNLIMITED).toInt)
}

def hours = totalMinutes/60
def minutes = totalMinutes%60
def -(other: TimeInterval) = TimeInterval(this.totalMinutes - other.totalMinutes)
Expand Down
12 changes: 9 additions & 3 deletions client/src/main/scala/com/v6ak/zbdb/Gender.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.v6ak.zbdb

abstract sealed class Gender()
abstract sealed class Gender(){
def inflect[T](feminine:T, masculine: T): T
}

object Gender{
case object Male extends Gender
case object Female extends Gender
case object Male extends Gender {
override def inflect[T](feminine: T, masculine: T): T = masculine
}
case object Female extends Gender {
override def inflect[T](feminine: T, masculine: T): T = feminine
}
}
13 changes: 12 additions & 1 deletion client/src/main/scala/com/v6ak/zbdb/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ object Renderer{
final class Renderer private(participantTable: ParticipantTable, processingErrors: Seq[(Seq[String], Throwable)], content: Node, additionalPlots: Seq[(String, String)], enableHorizontalStickyness: Boolean, year: String, yearLinksOption: Option[Seq[(String, String)]]) {

private val plotRenderer = new PlotRenderer(participantTable)
private val timeLineRenderer = new TimeLineRenderer(participantTable, plotRenderer)

import Renderer._
import Bootstrap._
Expand Down Expand Up @@ -267,7 +268,17 @@ final class Renderer private(participantTable: ParticipantTable, processingError
Seq(row),
plot.generator,
Seq(span(`class`:=s"glyphicon glyphicon-${plot.glyphiconName}", aria.hidden := "true"))
)(title := name)
)(title := name),
button(
`class` := "btn btn-default btn-xs",
Seq(span(`class`:=s"glyphicon glyphicon-list", aria.hidden := "true")),
onclick := { _: Any =>
val (dialog, jqModal, modalBodyId) = modal(s"Časová osa pro #${row.id}: ${row.fullNameWithNick}")
dom.document.body.appendChild(dialog)
dom.document.getElementById(modalBodyId).appendChild(timeLineRenderer.timeLine(row).render)
jqModal.modal(js.Dictionary("keyboard" -> true))
}
)
)

private def showCells(cells: Seq[String]): Frag = addSeparators[Frag](", ")(cells.map(c => code(c)))
Expand Down
126 changes: 126 additions & 0 deletions client/src/main/scala/com/v6ak/zbdb/TimeLineRenderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.v6ak.zbdb

import com.example.RichMoment.toRichMoment
import com.example.moment.Moment
import com.v6ak.scalajs.time.TimeInterval
import HtmlUtils._
import org.scalajs.dom
import scalatags.JsDom.all.{i => iTag, name => _, _}
import scala.scalajs.js


final case class FullPartInfo(
previousPartMetaOption: Option[Part],
partMeta: Part,
part: PartTimeInfo,
nextPartOption: Option[PartTimeInfo],
)

final class TimeLineRenderer(participantTable: ParticipantTable, plotRenderer: PlotRenderer) {
import participantTable._
import plotRenderer.{Plots, initializePlot}

private def timePoint(time: Moment, content: Frag) = tr(
`class` := "timeline-point",
td(`class` := "timeline-point-time", time.hoursAndMinutes),
td(`class` := "timeline-content", content),
)

private def process(content: Frag, className: String = "") = tr(
`class` := s"timeline-process $className",
td(`class` := "timeline-process-time"),
td(`class` := "timeline-content", content),
)

private def gaveUp(content: Frag) = process(content, className = "gave-up")
private def pause(content: Frag) = process(content, className = "pause")
private def finish(content: Frag) = process(content, className = "finish")

def timeLine(row: Participant) = {
val prevParts = Seq(None) ++ parts.map(Some(_))
val nextPartInfos: Seq[Option[PartTimeInfo]] = row.partTimes.drop(1).map(Some(_)) ++ Seq(None)
div(
table(
`class` := "timeline",
(
prevParts lazyZip
parts lazyZip
row.partTimes lazyZip
nextPartInfos
).map(FullPartInfo).zipWithIndex.flatMap{ case (fpi, pos) =>
partTimeLine(row)(pos, fpi)
}
),
chartButtons(row),
)
}

private def partTimeLine(row: Participant)(
pos: Int,
fullPartInfo: FullPartInfo
): Seq[Frag] = {
import fullPartInfo._
import row.gender
Seq(
timePoint(
part.startTime,
s"${gender.inflect("vyšla", "vyšel")} ${
previousPartMetaOption.fold("ze startu")(pm =>
s"z $pos. stanoviště ${pm.place}"
)
}"
),
) ++ (part match {
case PartTimeInfo.Finished(_startTime, endTime, intervalTime) => Seq(
process(Seq[Frag](
strong(s"${partMeta.trackLength}km"),
s" cesta ${gender.inflect("", "mu")} trvala $intervalTime",
br(),
"Průměrná rychlost ",
strong(f"${partMeta.trackLength * 60 / intervalTime.totalMinutes}%1.2f km/h"),
" = tempo ",
strong(f"${intervalTime / partMeta.trackLength} / km"),
)),
timePoint(
endTime,
gender.inflect("dorazila", "dorazil") + s" do ${pos + 1}. stanoviště ${partMeta.place}"
),
nextPartOption.fold(
if (pos == parts.size-1) finish("cíl")
else gaveUp(gender.inflect("vzdala", "vzdal") + s" to na $pos. stanovišti")
) { nextPart =>
pause(Seq[Frag](
"čekání na stanovišti: ",
strong(s"${TimeInterval((nextPart.startTime - endTime)/60/1000)}")
))
}
)
case PartTimeInfo.Unfinished(_startTime) => Seq(
gaveUp(s"${gender.inflect("Vzdala", "Vzdal")} to při cestě na další stanoviště.")
)
})
}


private def chartButtons(row: Participant) = div(`class` := "chart-buttons")(
for {
plot <- Plots
name = s"Graf ${plot.nameGenitive}"
} yield chartButton(
name,
Seq(row),
plot.generator,
Seq(span(`class` := s"glyphicon glyphicon-${plot.glyphiconName}", aria.hidden := "true"))
)(title := name),
)

private def chartButton(title: String, rowsLoader: => Seq[Participant], plotDataGenerator: Seq[Participant] => PlotData, description: Frag) =
button(`class` := "btn btn-default btn-l")(description, " ", title)(onclick := { _: Any =>
val (dialog, jqModal, modalBodyId) = modal(title)
jqModal.on("shown.bs.modal", { () => initializePlot(modalBodyId, plotDataGenerator(rowsLoader)) })
dom.document.body.appendChild(dialog)
jqModal.modal(js.Dictionary("keyboard" -> true))
})


}
38 changes: 38 additions & 0 deletions server/app/assets/main.less
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,44 @@ body{
padding-left: 5px;
}

.timeline {
margin-bottom: 20px;
&, td {
border-width: 0;
}
.timeline-point {
.timeline-point-time {
text-align: right;
padding-left: 10px;
padding-right: 10px;
border-right: 5px solid blue;
}
}
.timeline-process {
.timeline-process-time {
border-right: 5px dotted blue;
}
}
.timeline-process.pause {
.timeline-process-time {
border-right: 5px double blue;
}
}
.timeline-process.gave-up {
.timeline-process-time {
border-right: 5px double red;
}
}
.timeline-process.finish {
.timeline-process-time {
border-right: 5px double green;
}
}
.timeline-content {
padding-left: 10px;
}
}

@media screen {
.age{
display: block;
Expand Down

0 comments on commit e5f6862

Please sign in to comment.