Commit 8205d21e authored by Mathieu leclaire's avatar Mathieu leclaire

Ugrade lib versions and add graph editor demonstrator

parent e24c480e
......@@ -26,7 +26,7 @@ object Client {
@JSExport
def run() {
val submitButton1 = button("Click me")(
/* val submitButton1 = button("Click me")(
cursor := "pointer",
onclick := { () =>
Post[Api].hello(5).call().foreach { i =>
......@@ -53,6 +53,16 @@ object Client {
dom.document.body.appendChild(h1(helloValue).render)
dom.document.body.appendChild(h1(caseClassValue).render)
}
}*/
val nodes = scala.Array(
new Task("1",Var("one"),Var((400,600))),
new Task("2",Var("two"),Var((1000,600))),
new Task("3",Var("three"),Var((400,100))),
new Task("4",Var("four"),Var((1000,100))),
new Task("5",Var("five"),Var((105,60)))
)
val edges = scala.Array(new Edge(Var(nodes(0)),Var(nodes(1))),new Edge(Var(nodes(0)),Var(nodes(2))),new Edge(Var(nodes(3)),Var(nodes(1))))
val window = new Window(nodes,edges)
}
}
......
package client
/*
* Copyright (C) 21/08/14 // mathieu.leclaire@openmole.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import fr.iscpif.scaladget.d3mapping._
import org.scalajs.dom.{HTMLElement, Document, Element}
import org.scalajs.dom.SVGPoint
import org.scalajs.dom
import scalatags.JsDom.all._
import scala.scalajs.js
//import scala.scalajs.js._
import js.Dynamic.{literal => lit, newInstance => jsnew}
import rx._
import fr.iscpif.scaladget.d3._
trait GraphElement <: EventStates {
def literal: js.Dynamic
}
trait EventStates {
val selected: Var[Boolean] = Var(false)
}
class Task(val id: String,
val title: Var[String] = Var(""),
val location: Var[(Double, Double)] = Var((0.0, 0.0))) extends GraphElement {
def literal = lit("id" -> id, "title" -> title(), "x" -> location()._1, "y" -> location()._2)
}
class Edge(val source: Var[Task],
val target: Var[Task]) extends GraphElement {
def literal = lit("source" -> source().literal, "target" -> target().literal)
}
class Window(nodes: Array[Task] = Array(), edges: Array[Edge] = Array()) {
val svg = d3.select("body")
.append("svg")
.attr("width", "2500px")
.attr("height", "2500px")
val graph = new GraphCreator(svg,
nodes,
edges
)
}
case class Consts(selectedClass: js.String = "selected",
circleGClass: String = "conceptG",
graphClass: js.String = "graph",
activeEditId: js.String = "active-editing",
DELETE_KEY: js.Number = 46,
nodeRadius: js.Number = 50
)
class GraphCreator(svgSelection: Selection, _tasks: Array[Task], _edges: Array[Edge]) {
implicit def dynamicToString(d: js.Dynamic): String = d.asInstanceOf[js.String]
implicit def dynamicToBoolean(d: js.Dynamic): Boolean = d.asInstanceOf[js.Boolean]
// SVG DEFINITIONS //
val consts = new Consts
val svgG = svgSelection.append("g").classed(consts.graphClass, true)
val dragLine = svgG.append("svg:path")
.attr("class", "link dragline hidden")
.attr("d", "M0,0L0,0")
.style("marker-end", "url(#mark-end-arrow)")
val defs = svgSelection.append("svg:defs")
val pathRoot = svgG.append("g")
val circleRoot = svgG.append("g")
val mouseDownTask: Var[Option[Task]] = Var(None)
svgSelection
.on("mousemove", (_: js.Any, _: js.Number) => mousemove)
.on("mouseup.scene", (_: js.Any, _: js.Number) => mouseup)
// define arrow markers for graph links
defs.append("svg:marker")
.attr("id", "end-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 32)
.attr("markerWidth", 3.5)
.attr("markerHeight", 3.5)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
// define arrow markers for leading arrow
defs.append("svg:marker")
.attr("id", "mark-end-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 7)
.attr("markerWidth", 3.5)
.attr("markerHeight", 3.5)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
val tasks: Var[Array[Var[Task]]] = Var(Array())
_tasks.map {
addTask
}
val edges: Var[Array[Var[Edge]]] = Var(Array())
_edges.map {
addEdge
}
// GLOBAL EVENTS //
d3.select(dom.window)
.on("keydown", (_: js.Any, _: js.Number) => {
d3.event.keyCode match {
case consts.DELETE_KEY =>
tasks().filter(t => t().selected()).map { t =>
removeTask(t)
}
edges().filter(e => e().selected()).map { e =>
removeEdge(e)
}
case _ =>
}
})
def mousemove = {
Seq(mouseDownTask()).flatten.map { t =>
val x = d3.event.clientX
val y = d3.event.clientY
if (d3.event.shiftKey) {
dragLine.attr("d", "M" + t.location()._1 + "," + t.location()._2 + "L" + x + "," + y)
}
else {
t.location() = (x, y)
}
}
}
def mouseup = {
// Hide the drag line
mouseDownTask() = None
dragLine
.classed("hidden", true)
.style("marker-end", " ")
}
// ADD, SELECT AND REMOVE ITEMS //
def unselectTasks = tasks().foreach { t => t().selected() = false}
def unselectEdges = edges().foreach { e => e().selected() = false}
def removeTask(t: Var[Task]) = {
tasks() = tasks() diff Array(t)
edges() = edges().filterNot(e => e().source() == t() || e().target() == t())
}
def removeEdge(e: Var[Edge]) = {
edges() = edges() diff Array(e)
}
def addTask(id: String, title: String, x: Double, y: Double): Unit = addTask(new Task(id, Var(title), Var((x, y))))
def addTask(task: Task): Unit = {
tasks() = tasks() :+ Var(task)
Obs(tasks) {
val mysel = circleRoot.selectAll("g").data(tasks(), (task: Var[Task], n: js.Number) => {
task().id.toString
})
val newG = mysel.enter().append("g")
newG.append("circle").attr("r", consts.nodeRadius)
Rx {
newG.classed(consts.circleGClass, true)
.attr("transform", (task: Var[Task]) => {
val loc = task().location()
"translate(" + loc._1 + "," + loc._2 + ")"
})
newG.classed(consts.selectedClass, (task: Var[Task]) => {
task().selected()
})
}
newG.on("mousedown", (t: Var[Task], n: js.Number) => {
mouseDownTask() = Some(t())
d3.event.stopPropagation
unselectTasks
unselectEdges
t().selected() = !t().selected()
if (d3.event.shiftKey) {
val x = t().location()._1
val y = t().location()._2
dragLine
.style("marker-end", "url(#mark-end-arrow)")
.classed("hidden", false)
.attr("d", "M" + x + "," + y + "L" + x + "," + y)
}
})
.on("mouseup.task", (t: Var[Task], n: js.Number) => {
Seq(mouseDownTask()).flatten.map { mdt =>
if (t() != mdt) {
addEdge(mdt, t())
}
}
}
)
mysel.exit().remove()
}
}
def addEdge(source: Task, target: Task): Unit = addEdge(new Edge(Var(source), Var(target)))
def addEdge(edge: Edge): Unit = {
edges() = edges() :+ Var(edge)
Obs(edges) {
val mysel = pathRoot.selectAll("path").data(edges(), (edge: Var[Edge], n: js.Number) => {
edge().source().id + "+" + edge().target().id
})
val newPath = mysel.enter().append("path")
Rx {
newPath.style("marker-end", "url(#end-arrow)")
.classed("link", true)
.attr("d", (edge: Var[Edge], n: js.Number) => {
val source = edge().source().location()
val target = edge().target().location()
"M" + source._1 + "," + source._2 + "L" + target._1 + "," + target._2
})
// update existing paths
newPath.style("marker-end", "url(#end-arrow)")
.classed(consts.selectedClass, (edge: Var[Edge], n: Number) => {
edge().selected()
}
)
newPath.on("mousedown", (edge: Var[Edge], n: js.Number) => {
unselectTasks
unselectEdges
edge().selected() = !edge().selected()
})
}
mysel.exit().remove()
}
}
}
\ No newline at end of file
......@@ -29,12 +29,12 @@ object ScalaTraJSTagsWireRxBuild extends Build {
scalaVersion := ScalaVersion,
resolvers ++= Resolvers,
libraryDependencies ++= Seq(
"com.lihaoyi" %%% "autowire" % "0.2.2",
"com.lihaoyi" %%% "upickle" % "0.2.2",
"com.lihaoyi" %%% "autowire" % "0.2.3",
"com.lihaoyi" %%% "upickle" % "0.2.4",
"com.scalatags" %%% "scalatags" % "0.4.0",
"com.scalarx" %%% "scalarx" % "0.2.6",
"org.scala-lang.modules.scalajs" %%% "scalajs-dom" % "0.6",
"org.scala-lang.modules.scalajs" %%% "scalajs-jquery" % "0.6"
"fr.iscpif" %%% "scaladget" % "0.1.0",
"org.scala-lang.modules.scalajs" %%% "scalajs-dom" % "0.6"
),
//jsCall := "Client().run();",
outputPath := "server/src/main/webapp/"
......@@ -51,8 +51,8 @@ object ScalaTraJSTagsWireRxBuild extends Build {
scalaVersion := ScalaVersion,
resolvers ++= Resolvers,
libraryDependencies ++= Seq(
"com.lihaoyi" %% "autowire" % "0.2.2",
"com.lihaoyi" %% "upickle" % "0.2.2",
"com.lihaoyi" %% "autowire" % "0.2.3",
"com.lihaoyi" %% "upickle" % "0.2.4",
"com.scalatags" %% "scalatags" % "0.4.0",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
......
......@@ -5,6 +5,6 @@ resolvers ++= Seq(Resolver.url("scala-js-releases",
resolvers += "Typesafe repository" at
"http://repo.typesafe.com/typesafe/releases/"
addSbtPlugin("fr.iscpif" %% "jsmanager" % "0.2.0")
addSbtPlugin("fr.iscpif" %% "jsmanager" % "0.4.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
\ No newline at end of file
......@@ -33,8 +33,9 @@ class Server extends ScalatraServlet {
tags.html(
tags.head(
tags.meta(tags.httpEquiv := "Content-Type", tags.content := "text/html; charset=UTF-8"),
tags.script(tags.`type` := "text/javascript", tags.src := "js/client-fastopt.js"),
tags.script(tags.`type` := "text/javascript", tags.src := "js/client-opt.js")
tags.link(tags.rel := "stylesheet", tags.`type` := "text/css", href := "css/d3.css"),
tags.script(tags.`type` := "text/javascript", tags.src := "js/client-opt.js"),
tags.script(tags.`type` := "text/javascript", tags.src := "js/d3.v3.min.js")
),
tags.body(tags.onload := "Client().run();")
)
......
body{
margin: 0;
padding: 0;
overflow:hidden;
}
p{
text-align: center;
overflow: overlay;
position: relative;
}
body{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: rgb(248, 248, 248)
}
#toolbox{
position: absolute;
bottom: 0;
left: 0;
margin-bottom: 0.5em;
margin-left: 1em;
border: 2px solid #EEEEEE;
border-radius: 5px;
padding: 1em;
z-index: 5;
}
#toolbox input{
width: 30px;
opacity: 0.4;
}
#toolbox input:hover{
opacity: 1;
cursor: pointer;
}
#hidden-file-upload{
display: none;
}
#download-input{
margin: 0 0.5em;
}
.conceptG text{
pointer-events: none;
}
marker{
fill: #333;
}
g.conceptG circle{
fill: #F6FBFF;
stroke: #333;
stroke-width: 2px;
z-index: 21;
}
g.conceptG:hover circle{
fill: rgb(200, 238, 1);
}
g.selected circle{
fill: rgb(0, 232, 255);
}
g.selected:hover circle{
fill: rgb(250, 232, 255);
}
path.link {
fill: none;
stroke: #333;
stroke-width: 6px;
cursor: default;
}
path.link:hover{
stroke: rgb(94, 196, 204);
}
g.connect-node circle{
fill: #BEFFFF;
}
path.link.hidden{
stroke-width: 0;
}
path.link.selected {
stroke: rgb(229, 172, 247);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment