study: store shapes per node

pull/1846/head
Thibault Duplessis 2016-04-26 10:51:05 +07:00
parent 036524a9a9
commit c583ab4c6f
22 changed files with 132 additions and 86 deletions

View File

@ -165,19 +165,19 @@ object BSON extends Handlers {
def byteArrayO(b: ByteArray): Option[BSONBinary] =
if (b.isEmpty) None else ByteArray.ByteArrayBSONHandler.write(b).some
def bytesO(b: Array[Byte]): Option[BSONBinary] = byteArrayO(ByteArray(b))
def listO(list: List[String]): Option[List[String]] = list match {
def strListO(list: List[String]): Option[List[String]] = list match {
case Nil => None
case List("") => None
case List("", "") => None
case List(a, "") => Some(List(a))
case full => Some(full)
}
def listO[A](list: List[A])(implicit writer: BSONWriter[A, _ <: BSONValue]): Option[Barr] =
if (list.isEmpty) None
else Some(BSONArray(list map writer.write))
def docO(o: Bdoc): Option[Bdoc] = if (o.isEmpty) None else Some(o)
def double(i: Double): BSONDouble = BSONDouble(i)
def doubleO(i: Double): Option[BSONDouble] = if (i != 0) Some(BSONDouble(i)) else None
def intsO(l: List[Int]): Option[Barr] =
if (l.isEmpty) None
else Some(BSONArray(l map BSONInteger.apply))
import scalaz.Functor
def map[M[_]: Functor, A, B <: BSONValue](a: M[A])(implicit writer: BSONWriter[A, B]): M[B] =

View File

@ -101,7 +101,7 @@ object BSONHandlers {
def writes(w: BSON.Writer, o: Game) = BSONDocument(
id -> o.id,
playerIds -> (o.whitePlayer.id + o.blackPlayer.id),
playerUids -> w.listO(List(~o.whitePlayer.userId, ~o.blackPlayer.userId)),
playerUids -> w.strListO(List(~o.whitePlayer.userId, ~o.blackPlayer.userId)),
whitePlayer -> w.docO(playerBSONHandler write ((_: Color) => (_: Player.Id) => (_: Player.UserId) => (_: Player.Win) => o.whitePlayer)),
blackPlayer -> w.docO(playerBSONHandler write ((_: Color) => (_: Player.Id) => (_: Player.UserId) => (_: Player.Win) => o.blackPlayer)),
binaryPieces -> o.binaryPieces,

View File

@ -75,7 +75,7 @@ case object Perf {
def writes(w: BSON.Writer, o: Perf) = BSONDocument(
"gl" -> o.glicko,
"nb" -> w.int(o.nb),
"re" -> w.intsO(o.recent),
"re" -> w.listO(o.recent),
"la" -> o.latest.map(w.date))
}
}

View File

@ -19,6 +19,7 @@ sealed trait Node {
def drops: Option[List[Pos]]
def eval: Option[Node.Eval]
def comments: List[String]
def shapes: List[Node.Shape]
def children: List[Branch]
def opening: Option[FullOpening]
def crazyData: Option[Crazyhouse.Data]
@ -42,6 +43,7 @@ case class Root(
drops: Option[List[Pos]] = None,
eval: Option[Node.Eval] = None,
comments: List[String] = Nil,
shapes: List[Node.Shape] = Nil,
children: List[Branch] = Nil,
opening: Option[FullOpening] = None,
crazyData: Option[Crazyhouse.Data]) extends Node {
@ -66,6 +68,7 @@ case class Branch(
eval: Option[Node.Eval] = None,
nag: Option[String] = None,
comments: List[String] = Nil,
shapes: List[Node.Shape] = Nil,
children: List[Branch] = Nil,
opening: Option[FullOpening] = None,
crazyData: Option[Crazyhouse.Data]) extends Node {
@ -79,6 +82,14 @@ case class Branch(
object Node {
sealed trait Shape
object Shape {
type ID = String
type Brush = String
case class Circle(brush: Brush, orig: Pos) extends Shape
case class Arrow(brush: Brush, orig: Pos, dest: Pos) extends Shape
}
case class Eval(
cp: Option[Int] = None,
mate: Option[Int] = None,
@ -109,6 +120,16 @@ object Node {
"name" -> o.name)
}
private implicit val posWrites: Writes[Pos] = Writes[Pos] { p =>
JsString(p.key)
}
private implicit val shapeCircleWrites = Json.writes[Shape.Circle]
private implicit val shapeArrowWrites = Json.writes[Shape.Arrow]
implicit val shapeWrites: Writes[Shape] = Writes[Shape] {
case s: Shape.Circle => shapeCircleWrites writes s
case s: Shape.Arrow => shapeArrowWrites writes s
}
implicit val nodeJsonWriter: Writes[Node] = Writes { node =>
import node._
(
@ -119,6 +140,7 @@ object Node {
add("eval", eval) _ compose
add("nag", nag) _ compose
add("comments", comments, comments.nonEmpty) _ compose
add("shapes", shapes, shapes.nonEmpty) _ compose
add("opening", opening) _ compose
add("dests", dests.map {
_.map {

View File

@ -9,6 +9,7 @@ import lila.db.BSON
import lila.db.BSON._
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.common.LightUser
import lila.socket.tree.Node.Shape
private object BSONHandlers {
@ -71,6 +72,7 @@ private object BSONHandlers {
move = WithSan(r.get[Uci]("u"), r.str("s")),
fen = r.get[FEN]("f"),
check = r boolD "c",
shapes = r.getsD[Shape]("h"),
by = r str "b",
children = r.get[Node.Children]("n"))
def writes(w: Writer, s: Node) = BSONDocument(
@ -80,6 +82,7 @@ private object BSONHandlers {
"s" -> s.move.san,
"f" -> s.fen,
"c" -> w.boolO(s.check),
"h" -> w.listO(s.shapes),
"b" -> s.by,
"n" -> s.children)
}
@ -89,11 +92,13 @@ private object BSONHandlers {
ply = r int "p",
fen = r.get[FEN]("f"),
check = r boolD "c",
shapes = r.getsD[Shape]("h"),
children = r.get[Node.Children]("n"))
def writes(w: Writer, s: Root) = BSONDocument(
"p" -> s.ply,
"f" -> s.fen,
"c" -> w.boolO(s.check),
"h" -> w.listO(s.shapes),
"n" -> s.children)
}
implicit val ChildrenBSONHandler = new BSONHandler[BSONArray, Node.Children] {

View File

@ -4,6 +4,8 @@ import chess.Color
import chess.variant.Variant
import org.joda.time.DateTime
import lila.socket.tree.Node.Shape
case class Chapter(
_id: Chapter.ID,
studyId: Study.ID,
@ -22,6 +24,11 @@ case class Chapter(
updateRoot { root =>
root.withChildren(_.addNodeAt(node, path))
}
def setShapes(path: Path, shapes: List[Shape]): Option[Chapter] =
updateRoot { root =>
root.withChildren(_.setShapesAt(shapes, path))
}
}
object Chapter {

View File

@ -18,11 +18,13 @@ private final class ChapterMaker(domain: String) {
ply = sit.turns,
fen = FEN(Forsyth.>>(sit)),
check = sit.situation.check,
shapes = Nil,
children = Node.emptyChildren)
case None => Node.Root(
ply = 0,
fen = FEN(variant.initialFen),
check = false,
shapes = Nil,
children = Node.emptyChildren)
}
Chapter.make(

View File

@ -7,6 +7,7 @@ import play.api.libs.json._
import lila.common.LightUser
import lila.common.PimpedJson._
import lila.socket.Socket.Uid
import lila.socket.tree.Node.Shape
object JsonView {
@ -19,9 +20,6 @@ object JsonView {
private implicit val uciCharPairWrites: Writes[UciCharPair] = Writes[UciCharPair] { u =>
JsString(u.toString)
}
private implicit val posWrites: Writes[Pos] = Writes[Pos] { p =>
JsString(p.key)
}
private implicit val posReader: Reads[Pos] = Reads[Pos] { v =>
(v.asOpt[String] flatMap Pos.posAt).fold[JsResult[Pos]](JsError(Nil))(JsSuccess(_))
}
@ -43,15 +41,10 @@ object JsonView {
"ply" -> n.ply,
"fen" -> n.fen,
"check" -> n.check,
"shapes" -> n.shapes,
"children" -> n.children.nodes)
}
private implicit val shapeCircleWrites = Json.writes[Shape.Circle]
private implicit val shapeArrowWrites = Json.writes[Shape.Arrow]
private[study] implicit val shapeWrites: Writes[Shape] = Writes[Shape] {
case s: Shape.Circle => shapeCircleWrites writes s
case s: Shape.Arrow => shapeArrowWrites writes s
}
private[study] implicit val shapeReader: Reads[Shape] = Reads[Shape] { js =>
js.asOpt[JsObject].flatMap { o =>
for {
@ -83,7 +76,6 @@ object JsonView {
"id" -> s.id,
"members" -> s.members,
"position" -> s.position,
"shapes" -> s.shapes,
"ownerId" -> s.ownerId,
"createdAt" -> s.createdAt)
}
@ -99,6 +91,7 @@ object JsonView {
"san" -> n.move.san,
"fen" -> fenWriter.writes(n.fen),
"check" -> n.check,
"shapes" -> n.shapes,
"by" -> n.by,
"children" -> n.children.nodes)
}

View File

@ -3,11 +3,13 @@ package lila.study
import chess.format.{ Uci, UciCharPair, Forsyth, FEN }
import lila.user.User
import lila.socket.tree.Node.Shape
sealed trait RootOrNode {
val ply: Int
val fen: FEN
val check: Boolean
val shapes: List[Shape]
val children: Node.Children
}
@ -17,6 +19,7 @@ case class Node(
move: Uci.WithSan,
fen: FEN,
check: Boolean,
shapes: List[Shape],
by: User.ID,
children: Node.Children) extends RootOrNode {
@ -59,6 +62,12 @@ object Node {
case Some((head, tail)) => updateChildren(head, _.promoteNodeAt(tail))
}
def setShapesAt(shapes: List[Shape], path: Path): Option[Children] = path.split match {
case None => none
case Some((head, Path(Nil))) => updateWith(head, _.copy(shapes = shapes).some)
case Some((head, tail)) => updateChildren(head, _.setShapesAt(shapes, tail))
}
def get(id: UciCharPair): Option[Node] = nodes.find(_.id == id)
def has(id: UciCharPair): Boolean = nodes.exists(_.id == id)
@ -81,6 +90,7 @@ object Node {
ply: Int,
fen: FEN,
check: Boolean,
shapes: List[Shape],
children: Children) extends RootOrNode {
def withChildren(f: Children => Option[Children]) =
@ -100,12 +110,14 @@ object Node {
ply = 0,
fen = FEN(Forsyth.initial),
check = false,
shapes = Nil,
children = emptyChildren)
def fromRootBy(userId: User.ID)(b: lila.socket.tree.Root): Root = Root(
ply = b.ply,
fen = FEN(b.fen),
check = b.check,
shapes = Nil,
children = Children(b.children.toVector.map(fromBranchBy(userId))))
}
@ -115,6 +127,7 @@ object Node {
move = b.move,
fen = FEN(b.fen),
check = b.check,
shapes = Nil,
by = userId,
children = Children(b.children.toVector.map(fromBranchBy(userId))))
}

View File

@ -1,14 +0,0 @@
package lila.study
import chess.Pos
sealed trait Shape
object Shape {
type ID = String
type Brush = String
case class Circle(brush: Brush, orig: Pos) extends Shape
case class Arrow(brush: Brush, orig: Pos, dest: Pos) extends Shape
}

View File

@ -8,6 +8,7 @@ import scala.concurrent.duration._
import lila.hub.TimeBomb
import lila.socket.actorApi.{ Connected => _, _ }
import lila.socket.Socket.Uid
import lila.socket.tree.Node.Shape
import lila.socket.{ SocketActor, History, Historical, AnaDests }
import lila.user.User
@ -55,7 +56,8 @@ private final class Socket(
case ReloadAll(study, chapters) => notifyVersion("reload", JsNull, Messadata())
case ReloadShapes(shapes, uid) => notifyVersion("shapes", Json.obj(
case SetShapes(pos, shapes, uid) => notifyVersion("shapes", Json.obj(
"p" -> pos,
"s" -> shapes,
"w" -> who(uid)
), Messadata())
@ -130,7 +132,7 @@ private object Socket {
case class DelNode(position: Position.Ref, uid: Uid)
case class SetPath(position: Position.Ref, uid: Uid)
case class ReloadMembers(members: StudyMembers)
case class ReloadShapes(shapes: List[Shape], uid: Uid)
case class SetShapes(position: Position.Ref, shapes: List[Shape], uid: Uid)
case class ReloadChapters(chapters: List[Chapter.Metadata])
case class ReloadAll(study: Study, chapters: List[Chapter.Metadata])

View File

@ -12,6 +12,7 @@ import lila.hub.actorApi.map._
import lila.socket.actorApi.{ Connected => _, _ }
import lila.socket.Socket.makeMessage
import lila.socket.Socket.Uid
import lila.socket.tree.Node.Shape
import lila.socket.{ Handler, AnaMove, AnaDests }
import lila.user.User
import makeTimeout.short
@ -77,23 +78,23 @@ private[study] final class SocketHandler(
}
}
case ("setPath", o) => AnaRateLimit(uid.value) {
reading[AtPath](o) { d =>
reading[AtPosition](o) { position =>
member.userId foreach { userId =>
api.setPath(userId, studyId, Position.Ref(d.chapterId, Path(d.path)), uid)
api.setPath(userId, studyId, position.ref, uid)
}
}
}
case ("deleteVariation", o) => AnaRateLimit(uid.value) {
reading[AtPath](o) { d =>
reading[AtPosition](o) { position =>
member.userId foreach { userId =>
api.deleteNodeAt(userId, studyId, Position.Ref(d.chapterId, Path(d.path)), uid)
api.deleteNodeAt(userId, studyId, position.ref, uid)
}
}
}
case ("promoteVariation", o) => AnaRateLimit(uid.value) {
reading[AtPath](o) { d =>
reading[AtPosition](o) { position =>
member.userId foreach { userId =>
api.promoteNodeAt(userId, studyId, Position.Ref(d.chapterId, Path(d.path)), uid)
api.promoteNodeAt(userId, studyId, position.ref, uid)
}
}
}
@ -115,9 +116,11 @@ private[study] final class SocketHandler(
} api.kick(byUserId, studyId, userId)
case ("shapes", o) =>
(o \ "d").asOpt[List[Shape]] foreach { shapes =>
member.userId foreach { userId =>
api.setShapes(userId, studyId, shapes, uid)
reading[AtPosition](o) { position =>
(o \ "d" \ "shapes").asOpt[List[Shape]] foreach { shapes =>
member.userId foreach { userId =>
api.setShapes(userId, studyId, position.ref, shapes take 16, uid)
}
}
}
@ -150,8 +153,10 @@ private[study] final class SocketHandler(
private def reading[A](o: JsValue)(f: A => Unit)(implicit reader: Reads[A]): Unit =
o obj "d" flatMap { d => reader.reads(d).asOpt } foreach f
private case class AtPath(path: String, chapterId: String)
private implicit val atPathReader = Json.reads[AtPath]
private case class AtPosition(path: String, chapterId: String) {
def ref = Position.Ref(chapterId, Path(path))
}
private implicit val atPositionReader = Json.reads[AtPosition]
private case class SetRole(userId: String, role: String)
private implicit val SetRoleReader = Json.reads[SetRole]
private implicit val ChapterDataReader = Json.reads[ChapterMaker.Data]

View File

@ -8,7 +8,6 @@ case class Study(
_id: Study.ID,
members: StudyMembers,
position: Position.Ref,
shapes: List[Shape],
ownerId: User.ID,
createdAt: DateTime) {
@ -24,10 +23,7 @@ case class Study(
def withChapter(c: Chapter.Like) =
if (c.id == position.chapterId) this
else copy(
position = Position.Ref(chapterId = c.id, path = Path.root),
shapes = Nil
)
else copy(position = Position.Ref(chapterId = c.id, path = Path.root))
}
object Study {
@ -47,7 +43,6 @@ object Study {
_id = scala.util.Random.alphanumeric take idSize mkString,
members = StudyMembers(Map(user.id -> owner)),
position = Position.Ref("", Path.root),
shapes = Nil,
ownerId = user.id,
createdAt = DateTime.now)
}

View File

@ -1,12 +1,14 @@
package lila.study
import akka.actor.{ ActorRef, ActorSelection }
import org.apache.commons.lang3.StringEscapeUtils.escapeHtml4
import chess.format.{ Forsyth, FEN }
import lila.chat.actorApi.SystemTalk
import lila.hub.actorApi.map.Tell
import lila.hub.Sequencer
import lila.socket.Socket.Uid
import lila.socket.tree.Node.Shape
import lila.user.{ User, UserRepo }
final class StudyApi(
@ -118,10 +120,19 @@ final class StudyApi(
} >>- reloadMembers(study)
}
def setShapes(userId: User.ID, studyId: Study.ID, shapes: List[Shape], uid: Uid) = sequenceStudy(studyId) { study =>
def setShapes(userId: User.ID, studyId: Study.ID, position: Position.Ref, shapes: List[Shape], uid: Uid) = sequenceStudy(studyId) { study =>
Contribute(userId, study) {
studyRepo.setShapes(study, shapes)
} >>- reloadShapes(study, uid)
chapterRepo.byIdAndStudy(position.chapterId, study.id) flatMap {
_ ?? { chapter =>
chapter.setShapes(position.path, shapes) match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
sendTo(study.id, Socket.SetShapes(position, shapes, uid).pp)
case None => fufail(s"Invalid setShapes $position $shapes") >>- reloadUid(study, uid)
}
}
}
}
}
def addChapter(byUserId: User.ID, studyId: Study.ID, data: ChapterMaker.Data, socket: ActorRef) = sequenceStudy(studyId) { study =>
@ -148,9 +159,7 @@ final class StudyApi(
studyRepo.update(study withChapter chapter) >>- {
reloadAll(study)
study.members.get(byUserId).foreach { member =>
import org.apache.commons.lang3.StringEscapeUtils.escapeHtml4
val message = s"${member.user.name} switched to ${escapeHtml4(chapter.name)}"
chat ! SystemTalk(study.id, message, socket)
chat ! SystemTalk(study.id, escapeHtml4(chapter.name), socket)
}
}
}
@ -200,11 +209,6 @@ final class StudyApi(
sendTo(study.id, Socket.ReloadAll(study, chapters))
}
private def reloadShapes(study: Study, uid: Uid) =
studyRepo.getShapes(study.id).foreach { shapes =>
sendTo(study.id, Socket.ReloadShapes(shapes, uid))
}
private def sequenceStudy(studyId: String)(f: Study => Funit): Funit =
byId(studyId) flatMap {
_ ?? { study =>

View File

@ -5,6 +5,7 @@ import reactivemongo.bson.{ BSONDocument, BSONInteger, BSONRegex, BSONArray, BSO
import scala.concurrent.duration._
import lila.db.dsl._
import lila.socket.tree.Node.Shape
import lila.user.User
private final class StudyRepo(coll: Coll) {
@ -31,15 +32,6 @@ private final class StudyRepo(coll: Coll) {
)
).void
def getShapes(studyId: Study.ID): Fu[List[Shape]] =
coll.primitiveOne[List[Shape]]($id(studyId), "shapes") map (~_)
def setShapes(study: Study, shapes: List[Shape]): Funit =
coll.update(
$id(study.id),
$set("shapes" -> shapes)
).void
def addMember(study: Study, member: StudyMember): Funit =
coll.update(
$id(study.id),

View File

@ -15,6 +15,7 @@ object TreeBuilder {
ply = root.ply,
fen = root.fen.value,
check = root.check,
shapes = root.shapes,
children = toBranches(root.children),
crazyData = none)
@ -24,6 +25,7 @@ object TreeBuilder {
move = node.move,
fen = node.fen.value,
check = node.check,
shapes = node.shapes,
children = toBranches(node.children),
crazyData = none)

View File

@ -24,6 +24,18 @@
max-height: 160px;
overflow: hidden;
overflow-y: auto;
position: relative;
}
.study_box .list .loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
}
.study_box .list .elem {
display: flex;

View File

@ -120,7 +120,7 @@ module.exports = function(opts) {
onChange();
if (!dests) getDests();
this.setAutoShapes();
if (this.study) this.study.onShowGround();
if (node.shapes) this.chessground.setShapes(node.shapes);
}.bind(this);
var getDests = throttle(800, false, function() {

View File

@ -108,6 +108,7 @@ module.exports = {
});
}
}, [
ctrl.vm.loading ? m('div.loading', m.trust(lichess.spinnerHtml)) : null,
ctrl.chapters.list().map(function(chapter) {
var confing = ctrl.chapters.vm.confing === chapter.id;
var active = ctrl.position().chapterId === chapter.id;

View File

@ -52,13 +52,7 @@ module.exports = {
req.chapterId = data.position.chapterId;
return req;
}
var updateShapes = function() {
var shapes = ctrl.vm.path === data.position.path ? data.shapes : [];
ctrl.chessground.setShapes(shapes);
}
ctrl.userJump(data.position.path);
updateShapes();
var samePosition = function(p1, p2) {
return p1.chapterId === p2.chapterId && p1.path === p2.path;
@ -71,7 +65,9 @@ module.exports = {
members.set(s.members);
chapters.set(s.chapters);
ctrl.reloadData(d.analysis);
ctrl.chessground.set({orientation: d.analysis.orientation});
ctrl.chessground.set({
orientation: d.analysis.orientation
});
vm.loading = false;
};
@ -83,7 +79,13 @@ module.exports = {
ctrl.chessground.set({
drawable: {
onChange: function(shapes) {
if (members.canContribute()) send("shapes", shapes);
if (members.canContribute()) {
ctrl.tree.setShapes(shapes, ctrl.vm.path);
send("shapes", addChapterId({
path: ctrl.vm.path,
shapes: shapes
}));
}
}
}
});
@ -116,14 +118,12 @@ module.exports = {
},
setChapter: function(id) {
send("setChapter", id);
vm.loading = true;
},
setTab: function(tab) {
vm.tab(tab);
m.redraw.strategy("all");
},
onShowGround: function() {
updateShapes();
},
socketHandlers: {
path: function(d) {
var position = d.p,
@ -177,9 +177,11 @@ module.exports = {
m.redraw();
},
shapes: function(d) {
members.setActive(d.w.u);
data.shapes = d.s;
updateShapes();
var position = d.p,
who = d.w;
members.setActive(who.u);
if (who.s === sri) return;
if (position.chapterId !== data.position.chapterId) return;
m.redraw();
}
}

View File

@ -8,8 +8,6 @@ module.exports = {
main: function(ctrl) {
if (ctrl.vm.loading) return m.trust(lichess.spinnerHtml);
var activeTab = ctrl.vm.tab();
var makeTab = function(key, name) {

View File

@ -148,6 +148,11 @@ module.exports = function(root) {
if (opening) node.opening = opening;
});
},
setShapes: function(shapes, path) {
return updateAt(path, function(node) {
node.shapes = shapes;
});
},
pathIsMainline: pathIsMainline,
pathExists: pathExists,
deleteNodeAt: deleteNodeAt,