mirror of
https://github.com/status-im/whispervis.git
synced 2025-02-08 11:23:53 +00:00
Initial commit
This commit is contained in:
commit
7d860ad225
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
whispervis
|
||||
node_modules/
|
||||
network.json
|
||||
data.json
|
||||
propagation.json
|
45
main.go
Normal file
45
main.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/divan/graph-experiments/graph"
|
||||
"github.com/divan/graph-experiments/layout"
|
||||
)
|
||||
|
||||
func main() {
|
||||
iterations := flag.Int("i", 600, "Graph layout iterations to run (0 = auto, buggy)")
|
||||
flag.Parse()
|
||||
|
||||
data, err := graph.NewGraphFromJSON("network.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Loaded graph: %d nodes, %d links\n", len(data.Nodes()), len(data.Links()))
|
||||
|
||||
plog, err := LoadPropagationData("propagation.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Loaded propagation data: %d timestamps\n", len(plog.Timestamps))
|
||||
|
||||
log.Printf("Initializing layout...")
|
||||
repelling := layout.NewGravityForce(-100.0, layout.BarneHutMethod)
|
||||
springs := layout.NewSpringForce(0.01, 5.0, layout.ForEachLink)
|
||||
drag := layout.NewDragForce(0.4, layout.ForEachNode)
|
||||
layout3D := layout.New(data, repelling, springs, drag)
|
||||
|
||||
ws := NewWSServer(layout3D)
|
||||
if *iterations == 0 {
|
||||
ws.layout.Calculate()
|
||||
} else {
|
||||
ws.layout.CalculateN(*iterations)
|
||||
}
|
||||
ws.updateGraph(data)
|
||||
ws.updatePropagationData(plog)
|
||||
|
||||
log.Printf("Starting web server...")
|
||||
startWeb(ws)
|
||||
select {}
|
||||
}
|
30
propagation.go
Normal file
30
propagation.go
Normal file
@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
// PropagationLog represnts log of p2p message propagation
|
||||
// with relative timestamps (starting from T0).
|
||||
type PropagationLog struct {
|
||||
Timestamps []int // timestamps in milliseconds starting from T0
|
||||
Indices [][]int // indices of links for each step, len should be equal to len of Timestamps field
|
||||
Nodes [][]int // indices of nodes involved
|
||||
}
|
||||
|
||||
func LoadPropagationData(filename string) (*PropagationLog, error) {
|
||||
fd, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
var plog PropagationLog
|
||||
err = json.NewDecoder(fd).Decode(&plog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &plog, nil
|
||||
}
|
53
web.go
Normal file
53
web.go
Normal file
@ -0,0 +1,53 @@
|
||||
//go:generate browserify web/index.js web/js/ws.js -o web/bundle.js
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func startWeb(ws *WSServer) {
|
||||
port := ":20002"
|
||||
go func() {
|
||||
fs := http.FileServer(http.Dir("web"))
|
||||
http.Handle("/", noCacheMiddleware(fs))
|
||||
http.HandleFunc("/ws", ws.Handle)
|
||||
log.Fatal(http.ListenAndServe(port, nil))
|
||||
}()
|
||||
url := "http://localhost" + port
|
||||
fmt.Println("Please go to this url:", url)
|
||||
/*
|
||||
time.Sleep(1 * time.Second)
|
||||
startBrowser(url)
|
||||
*/
|
||||
}
|
||||
|
||||
// startBrowser tries to open the URL in a browser
|
||||
// and reports whether it succeeds.
|
||||
//
|
||||
// Orig. code: golang.org/x/tools/cmd/cover/html.go
|
||||
func startBrowser(url string) error {
|
||||
// try to start the browser
|
||||
var args []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
args = []string{"open"}
|
||||
case "windows":
|
||||
args = []string{"cmd", "/c", "start"}
|
||||
default:
|
||||
args = []string{"xdg-open"}
|
||||
}
|
||||
cmd := exec.Command(args[0], append(args[1:], url)...)
|
||||
fmt.Println("If browser window didn't appear, please go to this url:", url)
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func noCacheMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Cache-Control", "max-age=0, no-cache")
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
5712
web/bundle.js
Normal file
5712
web/bundle.js
Normal file
File diff suppressed because one or more lines are too long
11
web/css/controls.css
Normal file
11
web/css/controls.css
Normal file
@ -0,0 +1,11 @@
|
||||
body {
|
||||
background: #c0c0c0 -webkit-gradient(linear, left top, left bottom, from(#fff), to(#c0c0c0)) no-repeat;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
height: 1000px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "Helvetica Neue";
|
||||
font-weight: 300;
|
||||
}
|
11
web/css/pure-min.css
vendored
Normal file
11
web/css/pure-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
19
web/data/data.js
Normal file
19
web/data/data.js
Normal file
@ -0,0 +1,19 @@
|
||||
var graphData = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "Foo",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "Bar",
|
||||
"group": 1
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"source": "Foo",
|
||||
"target": "Bar",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
};
|
1
web/data/data/labels.json
Normal file
1
web/data/data/labels.json
Normal file
@ -0,0 +1 @@
|
||||
["Foo","Bar"]
|
BIN
web/data/data/links.bin
Normal file
BIN
web/data/data/links.bin
Normal file
Binary file not shown.
1
web/data/data/meta.json
Normal file
1
web/data/data/meta.json
Normal file
@ -0,0 +1 @@
|
||||
{"date":1512329825919,"nodeCount":2,"linkCount":1,"nodeFile":"data/labels.json","linkFile":"data/links.bin","version":"1.0.1"}
|
BIN
web/data/data/positions.bin
Normal file
BIN
web/data/data/positions.bin
Normal file
Binary file not shown.
299
web/data/data_orig.json
Normal file
299
web/data/data_orig.json
Normal file
@ -0,0 +1,299 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "\"Back()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"Element\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"Front()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"Init()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"Len()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"List\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"MoveAfter()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"MoveBefore()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"MoveToBack()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"MoveToFront()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"New()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"Next()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"Prev()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"PushBack()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"PushBackList()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"PushFront()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"PushFrontList()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"Remove\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"insert()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"insertValue()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"lazyInit()\"",
|
||||
"group": 1
|
||||
},
|
||||
{
|
||||
"id": "\"remove()\"",
|
||||
"group": 1
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"source": "\"Element\"",
|
||||
"target": "\"Element\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"Element\"",
|
||||
"target": "\"Next()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"Element\"",
|
||||
"target": "\"Prev()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"Back()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"Element\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"Front()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"Init()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"Len()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"MoveAfter()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"MoveBefore()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"MoveToBack()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"MoveToFront()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"PushBack()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"PushBackList()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"PushFront()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"PushFrontList()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"List\"",
|
||||
"target": "\"Remove\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveAfter()\"",
|
||||
"target": "\"insert()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveAfter()\"",
|
||||
"target": "\"remove()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveBefore()\"",
|
||||
"target": "\"insert()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveBefore()\"",
|
||||
"target": "\"remove()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveToBack()\"",
|
||||
"target": "\"insert()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveToBack()\"",
|
||||
"target": "\"remove()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveToFront()\"",
|
||||
"target": "\"insert()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"MoveToFront()\"",
|
||||
"target": "\"remove()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"New()\"",
|
||||
"target": "\"Init()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushBack()\"",
|
||||
"target": "\"insertValue()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushBack()\"",
|
||||
"target": "\"lazyInit()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushBackList()\"",
|
||||
"target": "\"Front()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushBackList()\"",
|
||||
"target": "\"Len()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushBackList()\"",
|
||||
"target": "\"insertValue()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushBackList()\"",
|
||||
"target": "\"lazyInit()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushFront()\"",
|
||||
"target": "\"insertValue()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushFront()\"",
|
||||
"target": "\"lazyInit()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushFrontList()\"",
|
||||
"target": "\"Back()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushFrontList()\"",
|
||||
"target": "\"Len()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushFrontList()\"",
|
||||
"target": "\"insertValue()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"PushFrontList()\"",
|
||||
"target": "\"lazyInit()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"Remove\"",
|
||||
"target": "\"remove()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"insertValue()\"",
|
||||
"target": "\"insert()\"",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"source": "\"lazyInit()\"",
|
||||
"target": "\"Init()\"",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
54
web/index.html
Normal file
54
web/index.html
Normal file
@ -0,0 +1,54 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Status Whisper network graph</title>
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="css/controls.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/pure-min.css" type="text/css">
|
||||
<style>
|
||||
.button-large {
|
||||
font-size: 100%;
|
||||
width: 100px;
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.button-success {
|
||||
background: rgb(28, 184, 65);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
color: white;
|
||||
background: rgb(66, 184, 221);
|
||||
}
|
||||
|
||||
.button-error {
|
||||
background: rgb(202, 60, 60);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-5">
|
||||
<h1>Status Whisper Network Graph</h1>
|
||||
<p>This graph represents Status cluster and connected peers, as
|
||||
reported by instrumented nodes and Status app with metrics option
|
||||
enabled.</p>
|
||||
<div>
|
||||
<button class="pure-button pure-button-primary" id="replayButton">Replay</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-4-5">
|
||||
<canvas id="preview"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<script src="node_modules/three/build/three.min.js"></script>
|
||||
<script src="js/controls/TrackballControls.js"></script>
|
||||
<script src="js/controls/FlyControls.js"></script>
|
||||
|
||||
<script src="bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
239
web/index.js
Normal file
239
web/index.js
Normal file
@ -0,0 +1,239 @@
|
||||
var { colorStr2Hex, autoColorNodes } = require('./js/colors.js');
|
||||
require('./js/keys.js');
|
||||
var accessorFn = require('./js/shitty_hacks.js');
|
||||
var { animatePropagation, restoreTimeout, updateRestoreTimeout, delayFactor, updateDelayFactor } = require('./js/animation.js');
|
||||
var { NewEthereumGeometry } = require('./js/ethereum.js');
|
||||
|
||||
var Stats = require('stats-js');
|
||||
const dat = require('dat.gui');
|
||||
|
||||
|
||||
// WebGL
|
||||
let canvas = document.getElementById("preview");
|
||||
var renderer = new THREE.WebGLRenderer({ canvas: canvas });
|
||||
|
||||
var graphData, plog;
|
||||
var positions = Array();
|
||||
|
||||
function setGraphData(data) {
|
||||
graphData = data;
|
||||
|
||||
initGraph();
|
||||
}
|
||||
|
||||
function setPropagation(plogData) {
|
||||
plog = plogData;
|
||||
animatePropagation(nodesGroup, linksGroup, plogData);
|
||||
}
|
||||
|
||||
function updatePositions(data) {
|
||||
positions = data;
|
||||
|
||||
redrawGraph();
|
||||
}
|
||||
|
||||
module.exports = { updatePositions, setGraphData, setPropagation };
|
||||
|
||||
// Setup scene
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x000011);
|
||||
|
||||
// Add lights
|
||||
scene.add(new THREE.AmbientLight(0xbbbbbb));
|
||||
scene.add(new THREE.DirectionalLight(0xffffff, 0.6));
|
||||
|
||||
var linksGroup = new THREE.Group();
|
||||
scene.add(linksGroup);
|
||||
var nodesGroup = new THREE.Group();
|
||||
scene.add(nodesGroup);
|
||||
|
||||
// Setup camera
|
||||
var camera = new THREE.PerspectiveCamera();
|
||||
camera.far = 20000;
|
||||
|
||||
var tbControls = new THREE.TrackballControls(camera, renderer.domElement);
|
||||
var flyControls = new THREE.FlyControls(camera, renderer.domElement);
|
||||
|
||||
var animate = function () {
|
||||
// frame cycle
|
||||
tbControls.update();
|
||||
flyControls.update(1);
|
||||
|
||||
renderer.render(scene, camera);
|
||||
stats.update();
|
||||
requestAnimationFrame( animate );
|
||||
};
|
||||
|
||||
var width = window.innerWidth * 80 / 100 - 20;
|
||||
var height = window.innerHeight - 20;
|
||||
var nodeRelSize = 1;
|
||||
var nodeResolution = 8;
|
||||
|
||||
// Stats
|
||||
var stats = new Stats();
|
||||
document.body.appendChild( stats.domElement );
|
||||
stats.domElement.style.position = 'absolute';
|
||||
stats.domElement.style.right = '15px';
|
||||
stats.domElement.style.bottom = '20px';
|
||||
|
||||
// Dat GUI
|
||||
const gui = new dat.GUI();
|
||||
var f1 = gui.addFolder('Animation');
|
||||
var restoreCtl = f1.add({ restoreTimeout: restoreTimeout }, 'restoreTimeout').name('Restore timeout');
|
||||
restoreCtl.onFinishChange(function(value) {
|
||||
updateRestoreTimeout(value);
|
||||
});
|
||||
var factorCtl = f1.add({ delayFactor: delayFactor }, 'delayFactor').name('Delay factor');
|
||||
factorCtl.onFinishChange(function(value) {
|
||||
updateDelayFactor(value);
|
||||
});
|
||||
|
||||
var initGraph = function () {
|
||||
resizeCanvas();
|
||||
|
||||
// parse links
|
||||
graphData.links.forEach(link => {
|
||||
link.source = link["source"];
|
||||
link.target = link["target"];
|
||||
});
|
||||
|
||||
// Add WebGL objects
|
||||
// Clear the place
|
||||
while (nodesGroup.children.length) {
|
||||
nodesGroup.remove(nodesGroup.children[0])
|
||||
}
|
||||
while (linksGroup.children.length) {
|
||||
linksGroup.remove(linksGroup.children[0])
|
||||
}
|
||||
|
||||
// Render nodes
|
||||
const nameAccessor = accessorFn("name");
|
||||
const valAccessor = accessorFn("weight");
|
||||
const colorAccessor = accessorFn("color");
|
||||
let sphereGeometries = {}; // indexed by node value
|
||||
let sphereMaterials = {}; // indexed by color
|
||||
|
||||
autoColorNodes(graphData.nodes);
|
||||
graphData.nodes.forEach((node, idx) => {
|
||||
let val = valAccessor(node) || 1;
|
||||
if (!sphereGeometries.hasOwnProperty(val)) {
|
||||
sphereGeometries[val] = NewEthereumGeometry(val);
|
||||
}
|
||||
|
||||
const color = colorAccessor(node);
|
||||
if (!sphereMaterials.hasOwnProperty(color)) {
|
||||
sphereMaterials[color] = new THREE.MeshStandardMaterial({
|
||||
color: colorStr2Hex(color || '#00ff00'),
|
||||
transparent: false,
|
||||
opacity: 0.75
|
||||
});
|
||||
}
|
||||
|
||||
const sphere = new THREE.Mesh(sphereGeometries[val], sphereMaterials[color]);
|
||||
|
||||
sphere.name = nameAccessor(node); // Add label
|
||||
sphere.__data = node; // Attach node data
|
||||
|
||||
nodesGroup.add(node.__sphere = sphere);
|
||||
if (positions[idx] !== undefined) {
|
||||
sphere.position.set(positions[idx].x, positions[idx].y, positions[idx].z);
|
||||
}
|
||||
});
|
||||
|
||||
const linkColorAccessor = accessorFn("color");
|
||||
let lineMaterials = {}; // indexed by color
|
||||
console.log("Adding links", graphData.links.lengh);
|
||||
graphData.links.forEach(link => {
|
||||
const color = linkColorAccessor(link);
|
||||
if (!lineMaterials.hasOwnProperty(color)) {
|
||||
lineMaterials[color] = new THREE.LineBasicMaterial({
|
||||
color: /*colorStr2Hex(color || '#f0f0f0')*/ '#f0f0f0',
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
});
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(2 * 3), 3));
|
||||
const lineMaterial = lineMaterials[color];
|
||||
const line = new THREE.Line(geometry, lineMaterial);
|
||||
|
||||
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of spheres by rendering them last
|
||||
|
||||
linksGroup.add(link.__line = line);
|
||||
});
|
||||
|
||||
// correct camera position
|
||||
if (camera.position.x === 0 && camera.position.y === 0) {
|
||||
// If camera still in default position (not user modified)
|
||||
camera.lookAt(nodesGroup.position);
|
||||
camera.position.z = Math.cbrt(graphData.nodes.length) * 50;
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
if (width && height) {
|
||||
renderer.setSize(width, height);
|
||||
camera.aspect = width/height;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var redrawGraph = function () {
|
||||
graphData.nodes.forEach((node, idx) => {
|
||||
const sphere = node.__sphere;
|
||||
if (!sphere) return;
|
||||
|
||||
sphere.position.x = positions[idx].x;
|
||||
sphere.position.y = positions[idx].y || 0;
|
||||
sphere.position.z = positions[idx].z || 0;
|
||||
});
|
||||
|
||||
|
||||
graphData.links.forEach(link => {
|
||||
const line = link.__line;
|
||||
if (!line) return;
|
||||
|
||||
linePos = line.geometry.attributes.position;
|
||||
|
||||
// TODO: move this index into map/cache or even into original graph data
|
||||
let start, end;
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
if (graphData.nodes[i].id === link.source) {
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
if (graphData.nodes[i].id === link["target"]) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
linePos.array[0] = positions[start].x;
|
||||
linePos.array[1] = positions[start].y || 0;
|
||||
linePos.array[2] = positions[start].z || 0;
|
||||
linePos.array[3] = positions[end].x;
|
||||
linePos.array[4] = positions[end].y || 0;
|
||||
linePos.array[5] = positions[end].z || 0;
|
||||
|
||||
linePos.needsUpdate = true;
|
||||
line.geometry.computeBoundingSphere();
|
||||
});
|
||||
};
|
||||
|
||||
// replay restarts propagation animation.
|
||||
function replay() {
|
||||
console.log(linksGroup, plog);
|
||||
animatePropagation(nodesGroup, linksGroup, plog);
|
||||
}
|
||||
// js functions after browserify cannot be accessed from html,
|
||||
// so instead of using onclick="replay()" we need to attach listener
|
||||
// here.
|
||||
// Did I already say that whole frontend ecosystem is a one giant
|
||||
// museum of hacks for hacks on top of hacks?
|
||||
var replayButton = document.getElementById('replayButton');
|
||||
replayButton.addEventListener('click', replay);
|
||||
|
||||
animate();
|
79
web/js/animation.js
Normal file
79
web/js/animation.js
Normal file
@ -0,0 +1,79 @@
|
||||
var gradient = require('d3-scale-chromatic').interpolateCool;
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
let mat = new THREE.LineBasicMaterial({
|
||||
color: '#cc0000',
|
||||
transparent: true,
|
||||
opacity: 0.7
|
||||
});
|
||||
|
||||
// Params
|
||||
var restoreTimeout = 250; // ms
|
||||
function updateRestoreTimeout(value) {
|
||||
console.log("Updating restore timeout to ", value);
|
||||
restoreTimeout = value;
|
||||
}
|
||||
var delayFactor = 3; // multiplication factor for blink timeout
|
||||
function updateDelayFactor(value) {
|
||||
delayFactor = value;
|
||||
}
|
||||
|
||||
function blinkLink(links, indices) {
|
||||
indices.forEach(idx => {
|
||||
if (links.children[idx].material === mat) {
|
||||
return
|
||||
}
|
||||
let oldMat = links.children[idx].material;
|
||||
links.children[idx].material = mat;
|
||||
|
||||
console.log("Blinking with restore timeout", restoreTimeout);
|
||||
setTimeout(function() {
|
||||
links.children[idx].material = oldMat;
|
||||
}, restoreTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
var nodeCounters = {};
|
||||
var maxCounter = 0;
|
||||
var nodeMaterials = {};
|
||||
|
||||
// blinkNodes updates nodes color increasing its temperature
|
||||
// in a heatmap style
|
||||
function blinkNodes(nodes, indices) {
|
||||
indices.forEach(idx => {
|
||||
nodeCounters[idx] = nodeCounters[idx] ? nodeCounters[idx]+1 : 1;
|
||||
if (nodeCounters[idx] > maxCounter) {
|
||||
maxCounter = nodeCounters[idx];
|
||||
}
|
||||
});
|
||||
// TODO: FIXME: how to make simple map counter without this fucking bullshit?
|
||||
Object.keys(nodeCounters).forEach(idx => {
|
||||
let c = nodeCounters[idx];
|
||||
let scale = c / maxCounter;
|
||||
let color = gradient(scale);
|
||||
if (nodeMaterials[color] === undefined) {
|
||||
nodeMaterials[color] = new THREE.MeshStandardMaterial({color: new THREE.Color(color)});
|
||||
}
|
||||
nodes.children[idx].material = nodeMaterials[color];
|
||||
});
|
||||
}
|
||||
|
||||
function animatePropagation(nodes, links, plog) {
|
||||
maxCounter = 0;
|
||||
nodeCounters = {};
|
||||
plog.Timestamps.forEach((ts, idx) => {
|
||||
setTimeout(function() {
|
||||
blinkLink(links, plog.Indices[idx]);
|
||||
}, ts*delayFactor);
|
||||
setTimeout(function() {
|
||||
blinkNodes(nodes, plog.Nodes[idx]);
|
||||
}, ts*delayFactor);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { animatePropagation,
|
||||
restoreTimeout, updateRestoreTimeout,
|
||||
delayFactor, updateDelayFactor }
|
20
web/js/colors.js
Normal file
20
web/js/colors.js
Normal file
@ -0,0 +1,20 @@
|
||||
var schemePaired = require('d3-scale-chromatic').schemePaired;
|
||||
var tinyColor = require('tinycolor2');
|
||||
|
||||
const colorStr2Hex = str => isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str;
|
||||
|
||||
function autoColorNodes(nodes) {
|
||||
const colors = schemePaired; // Paired color set from color brewer
|
||||
|
||||
const uncoloredNodes = nodes.filter(node => !node.color);
|
||||
const nodeGroups = {};
|
||||
|
||||
uncoloredNodes.forEach(node => { nodeGroups[node["group"]] = null });
|
||||
Object.keys(nodeGroups).forEach((group, idx) => { nodeGroups[group] = idx });
|
||||
|
||||
uncoloredNodes.forEach(node => {
|
||||
node.color = colorStr2Hex(colors[nodeGroups[node["group"]] % colors.length]);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { colorStr2Hex, autoColorNodes };
|
292
web/js/controls/FlyControls.js
Normal file
292
web/js/controls/FlyControls.js
Normal file
@ -0,0 +1,292 @@
|
||||
/**
|
||||
* @author James Baicoianu / http://www.baicoianu.com/
|
||||
*/
|
||||
|
||||
THREE.FlyControls = function ( object, domElement ) {
|
||||
|
||||
this.object = object;
|
||||
|
||||
this.domElement = ( domElement !== undefined ) ? domElement : document;
|
||||
if ( domElement ) this.domElement.setAttribute( 'tabindex', - 1 );
|
||||
|
||||
// API
|
||||
|
||||
this.movementSpeed = 1.0;
|
||||
this.rollSpeed = 0.005;
|
||||
|
||||
this.dragToLook = false;
|
||||
this.autoForward = false;
|
||||
|
||||
// disable default target object behavior
|
||||
|
||||
// internals
|
||||
|
||||
this.tmpQuaternion = new THREE.Quaternion();
|
||||
|
||||
this.mouseStatus = 0;
|
||||
|
||||
this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 };
|
||||
this.moveVector = new THREE.Vector3( 0, 0, 0 );
|
||||
this.rotationVector = new THREE.Vector3( 0, 0, 0 );
|
||||
|
||||
this.handleEvent = function ( event ) {
|
||||
|
||||
if ( typeof this[ event.type ] == 'function' ) {
|
||||
|
||||
this[ event.type ]( event );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.keydown = function( event ) {
|
||||
|
||||
if ( event.altKey ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
//event.preventDefault();
|
||||
|
||||
switch ( event.keyCode ) {
|
||||
|
||||
case 16: /* shift */ this.movementSpeedMultiplier = .1; break;
|
||||
|
||||
case 87: /*W*/ this.moveState.forward = 1; break;
|
||||
case 83: /*S*/ this.moveState.back = 1; break;
|
||||
|
||||
case 65: /*A*/ this.moveState.left = 1; break;
|
||||
case 68: /*D*/ this.moveState.right = 1; break;
|
||||
|
||||
case 82: /*R*/ this.moveState.up = 1; break;
|
||||
case 70: /*F*/ this.moveState.down = 1; break;
|
||||
|
||||
case 38: /*up*/ this.moveState.pitchUp = 1; break;
|
||||
case 40: /*down*/ this.moveState.pitchDown = 1; break;
|
||||
|
||||
case 37: /*left*/ this.moveState.yawLeft = 1; break;
|
||||
case 39: /*right*/ this.moveState.yawRight = 1; break;
|
||||
|
||||
case 81: /*Q*/ this.moveState.rollLeft = 1; break;
|
||||
case 69: /*E*/ this.moveState.rollRight = 1; break;
|
||||
|
||||
}
|
||||
|
||||
this.updateMovementVector();
|
||||
this.updateRotationVector();
|
||||
|
||||
};
|
||||
|
||||
this.keyup = function( event ) {
|
||||
|
||||
switch ( event.keyCode ) {
|
||||
|
||||
case 16: /* shift */ this.movementSpeedMultiplier = 1; break;
|
||||
|
||||
case 87: /*W*/ this.moveState.forward = 0; break;
|
||||
case 83: /*S*/ this.moveState.back = 0; break;
|
||||
|
||||
case 65: /*A*/ this.moveState.left = 0; break;
|
||||
case 68: /*D*/ this.moveState.right = 0; break;
|
||||
|
||||
case 82: /*R*/ this.moveState.up = 0; break;
|
||||
case 70: /*F*/ this.moveState.down = 0; break;
|
||||
|
||||
case 38: /*up*/ this.moveState.pitchUp = 0; break;
|
||||
case 40: /*down*/ this.moveState.pitchDown = 0; break;
|
||||
|
||||
case 37: /*left*/ this.moveState.yawLeft = 0; break;
|
||||
case 39: /*right*/ this.moveState.yawRight = 0; break;
|
||||
|
||||
case 81: /*Q*/ this.moveState.rollLeft = 0; break;
|
||||
case 69: /*E*/ this.moveState.rollRight = 0; break;
|
||||
|
||||
}
|
||||
|
||||
this.updateMovementVector();
|
||||
this.updateRotationVector();
|
||||
|
||||
};
|
||||
|
||||
this.mousedown = function( event ) {
|
||||
|
||||
if ( this.domElement !== document ) {
|
||||
|
||||
this.domElement.focus();
|
||||
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if ( this.dragToLook ) {
|
||||
|
||||
this.mouseStatus ++;
|
||||
|
||||
} else {
|
||||
|
||||
switch ( event.button ) {
|
||||
|
||||
case 0: this.moveState.forward = 1; break;
|
||||
case 2: this.moveState.back = 1; break;
|
||||
|
||||
}
|
||||
|
||||
this.updateMovementVector();
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.mousemove = function( event ) {
|
||||
|
||||
// (divan: disabled)
|
||||
if (false && (! this.dragToLook || this.mouseStatus > 0)) {
|
||||
var container = this.getContainerDimensions();
|
||||
var halfWidth = container.size[ 0 ] / 2;
|
||||
var halfHeight = container.size[ 1 ] / 2;
|
||||
|
||||
this.moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth;
|
||||
this.moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight;
|
||||
|
||||
this.updateRotationVector();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.mouseup = function( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if ( this.dragToLook ) {
|
||||
|
||||
this.mouseStatus --;
|
||||
|
||||
this.moveState.yawLeft = this.moveState.pitchDown = 0;
|
||||
|
||||
} else {
|
||||
|
||||
switch ( event.button ) {
|
||||
|
||||
case 0: this.moveState.forward = 0; break;
|
||||
case 2: this.moveState.back = 0; break;
|
||||
|
||||
}
|
||||
|
||||
this.updateMovementVector();
|
||||
|
||||
}
|
||||
|
||||
this.updateRotationVector();
|
||||
|
||||
};
|
||||
|
||||
this.update = function( delta ) {
|
||||
|
||||
var moveMult = delta * this.movementSpeed;
|
||||
var rotMult = delta * this.rollSpeed;
|
||||
|
||||
this.object.translateX( this.moveVector.x * moveMult );
|
||||
this.object.translateY( this.moveVector.y * moveMult );
|
||||
this.object.translateZ( this.moveVector.z * moveMult );
|
||||
|
||||
this.tmpQuaternion.set( this.rotationVector.x * rotMult, this.rotationVector.y * rotMult, this.rotationVector.z * rotMult, 1 ).normalize();
|
||||
this.object.quaternion.multiply( this.tmpQuaternion );
|
||||
|
||||
// expose the rotation vector for convenience
|
||||
this.object.rotation.setFromQuaternion( this.object.quaternion, this.object.rotation.order );
|
||||
|
||||
|
||||
};
|
||||
|
||||
this.updateMovementVector = function() {
|
||||
|
||||
var forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0;
|
||||
|
||||
this.moveVector.x = ( - this.moveState.left + this.moveState.right );
|
||||
this.moveVector.y = ( - this.moveState.down + this.moveState.up );
|
||||
this.moveVector.z = ( - forward + this.moveState.back );
|
||||
|
||||
//console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] );
|
||||
|
||||
};
|
||||
|
||||
this.updateRotationVector = function() {
|
||||
|
||||
this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp );
|
||||
this.rotationVector.y = ( - this.moveState.yawRight + this.moveState.yawLeft );
|
||||
this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft );
|
||||
|
||||
//console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] );
|
||||
|
||||
};
|
||||
|
||||
this.getContainerDimensions = function() {
|
||||
|
||||
if ( this.domElement != document ) {
|
||||
|
||||
return {
|
||||
size : [ this.domElement.offsetWidth, this.domElement.offsetHeight ],
|
||||
offset : [ this.domElement.offsetLeft, this.domElement.offsetTop ]
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
return {
|
||||
size : [ window.innerWidth, window.innerHeight ],
|
||||
offset : [ 0, 0 ]
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function bind( scope, fn ) {
|
||||
|
||||
return function () {
|
||||
|
||||
fn.apply( scope, arguments );
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function contextmenu( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
}
|
||||
|
||||
this.dispose = function() {
|
||||
|
||||
//this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
|
||||
//this.domElement.removeEventListener( 'mousedown', _mousedown, false );
|
||||
//this.domElement.removeEventListener( 'mousemove', _mousemove, false );
|
||||
//this.domElement.removeEventListener( 'mouseup', _mouseup, false );
|
||||
|
||||
window.removeEventListener( 'keydown', _keydown, false );
|
||||
window.removeEventListener( 'keyup', _keyup, false );
|
||||
|
||||
};
|
||||
|
||||
//var _mousemove = bind( this, this.mousemove );
|
||||
var _mousedown = bind( this, this.mousedown );
|
||||
var _mouseup = bind( this, this.mouseup );
|
||||
var _keydown = bind( this, this.keydown );
|
||||
var _keyup = bind( this, this.keyup );
|
||||
|
||||
this.domElement.addEventListener( 'contextmenu', contextmenu, false );
|
||||
|
||||
//this.domElement.addEventListener( 'mousemove', _mousemove, false );
|
||||
//this.domElement.addEventListener( 'mousedown', _mousedown, false );
|
||||
//this.domElement.addEventListener( 'mouseup', _mouseup, false );
|
||||
|
||||
window.addEventListener( 'keydown', _keydown, false );
|
||||
window.addEventListener( 'keyup', _keyup, false );
|
||||
|
||||
this.updateMovementVector();
|
||||
this.updateRotationVector();
|
||||
|
||||
};
|
625
web/js/controls/TrackballControls.js
Normal file
625
web/js/controls/TrackballControls.js
Normal file
@ -0,0 +1,625 @@
|
||||
/**
|
||||
* @author Eberhard Graether / http://egraether.com/
|
||||
* @author Mark Lundin / http://mark-lundin.com
|
||||
* @author Simone Manini / http://daron1337.github.io
|
||||
* @author Luca Antiga / http://lantiga.github.io
|
||||
*/
|
||||
|
||||
THREE.TrackballControls = function ( object, domElement ) {
|
||||
|
||||
var _this = this;
|
||||
var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
|
||||
|
||||
this.object = object;
|
||||
this.domElement = ( domElement !== undefined ) ? domElement : document;
|
||||
|
||||
// API
|
||||
|
||||
this.enabled = true;
|
||||
|
||||
this.screen = { left: 0, top: 0, width: 0, height: 0 };
|
||||
|
||||
this.rotateSpeed = 1.0;
|
||||
this.zoomSpeed = 1.2;
|
||||
this.panSpeed = 0.3;
|
||||
|
||||
this.noRotate = false;
|
||||
this.noZoom = false;
|
||||
this.noPan = false;
|
||||
|
||||
this.staticMoving = false;
|
||||
this.dynamicDampingFactor = 0.2;
|
||||
|
||||
this.minDistance = 0;
|
||||
this.maxDistance = Infinity;
|
||||
|
||||
this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];
|
||||
|
||||
// internals
|
||||
|
||||
this.target = new THREE.Vector3();
|
||||
|
||||
var EPS = 0.000001;
|
||||
|
||||
var lastPosition = new THREE.Vector3();
|
||||
|
||||
var _state = STATE.NONE,
|
||||
_prevState = STATE.NONE,
|
||||
|
||||
_eye = new THREE.Vector3(),
|
||||
|
||||
_movePrev = new THREE.Vector2(),
|
||||
_moveCurr = new THREE.Vector2(),
|
||||
|
||||
_lastAxis = new THREE.Vector3(),
|
||||
_lastAngle = 0,
|
||||
|
||||
_zoomStart = new THREE.Vector2(),
|
||||
_zoomEnd = new THREE.Vector2(),
|
||||
|
||||
_touchZoomDistanceStart = 0,
|
||||
_touchZoomDistanceEnd = 0,
|
||||
|
||||
_panStart = new THREE.Vector2(),
|
||||
_panEnd = new THREE.Vector2();
|
||||
|
||||
// for reset
|
||||
|
||||
this.target0 = this.target.clone();
|
||||
this.position0 = this.object.position.clone();
|
||||
this.up0 = this.object.up.clone();
|
||||
|
||||
// events
|
||||
|
||||
var changeEvent = { type: 'change' };
|
||||
var startEvent = { type: 'start' };
|
||||
var endEvent = { type: 'end' };
|
||||
|
||||
|
||||
// methods
|
||||
|
||||
this.handleResize = function () {
|
||||
|
||||
if ( this.domElement === document ) {
|
||||
|
||||
this.screen.left = 0;
|
||||
this.screen.top = 0;
|
||||
this.screen.width = window.innerWidth;
|
||||
this.screen.height = window.innerHeight;
|
||||
|
||||
} else {
|
||||
|
||||
var box = this.domElement.getBoundingClientRect();
|
||||
// adjustments come from similar code in the jquery offset() function
|
||||
var d = this.domElement.ownerDocument.documentElement;
|
||||
this.screen.left = box.left + window.pageXOffset - d.clientLeft;
|
||||
this.screen.top = box.top + window.pageYOffset - d.clientTop;
|
||||
this.screen.width = box.width;
|
||||
this.screen.height = box.height;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.handleEvent = function ( event ) {
|
||||
|
||||
if ( typeof this[ event.type ] == 'function' ) {
|
||||
|
||||
this[ event.type ]( event );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var getMouseOnScreen = ( function () {
|
||||
|
||||
var vector = new THREE.Vector2();
|
||||
|
||||
return function getMouseOnScreen( pageX, pageY ) {
|
||||
|
||||
vector.set(
|
||||
( pageX - _this.screen.left ) / _this.screen.width,
|
||||
( pageY - _this.screen.top ) / _this.screen.height
|
||||
);
|
||||
|
||||
return vector;
|
||||
|
||||
};
|
||||
|
||||
}() );
|
||||
|
||||
var getMouseOnCircle = ( function () {
|
||||
|
||||
var vector = new THREE.Vector2();
|
||||
|
||||
return function getMouseOnCircle( pageX, pageY ) {
|
||||
|
||||
vector.set(
|
||||
( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ),
|
||||
( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional
|
||||
);
|
||||
|
||||
return vector;
|
||||
|
||||
};
|
||||
|
||||
}() );
|
||||
|
||||
this.rotateCamera = ( function() {
|
||||
|
||||
var axis = new THREE.Vector3(),
|
||||
quaternion = new THREE.Quaternion(),
|
||||
eyeDirection = new THREE.Vector3(),
|
||||
objectUpDirection = new THREE.Vector3(),
|
||||
objectSidewaysDirection = new THREE.Vector3(),
|
||||
moveDirection = new THREE.Vector3(),
|
||||
angle;
|
||||
|
||||
return function rotateCamera() {
|
||||
|
||||
moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
|
||||
angle = moveDirection.length();
|
||||
|
||||
if ( angle ) {
|
||||
|
||||
_eye.copy( _this.object.position ).sub( _this.target );
|
||||
|
||||
eyeDirection.copy( _eye ).normalize();
|
||||
objectUpDirection.copy( _this.object.up ).normalize();
|
||||
objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
|
||||
|
||||
objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
|
||||
objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
|
||||
|
||||
moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
|
||||
|
||||
axis.crossVectors( moveDirection, _eye ).normalize();
|
||||
|
||||
angle *= _this.rotateSpeed;
|
||||
quaternion.setFromAxisAngle( axis, angle );
|
||||
|
||||
_eye.applyQuaternion( quaternion );
|
||||
_this.object.up.applyQuaternion( quaternion );
|
||||
|
||||
_lastAxis.copy( axis );
|
||||
_lastAngle = angle;
|
||||
|
||||
} else if ( ! _this.staticMoving && _lastAngle ) {
|
||||
|
||||
_lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor );
|
||||
_eye.copy( _this.object.position ).sub( _this.target );
|
||||
quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
|
||||
_eye.applyQuaternion( quaternion );
|
||||
_this.object.up.applyQuaternion( quaternion );
|
||||
|
||||
}
|
||||
|
||||
_movePrev.copy( _moveCurr );
|
||||
|
||||
};
|
||||
|
||||
}() );
|
||||
|
||||
|
||||
this.zoomCamera = function () {
|
||||
|
||||
var factor;
|
||||
|
||||
if ( _state === STATE.TOUCH_ZOOM_PAN ) {
|
||||
|
||||
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
|
||||
_touchZoomDistanceStart = _touchZoomDistanceEnd;
|
||||
_eye.multiplyScalar( factor );
|
||||
|
||||
} else {
|
||||
|
||||
factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;
|
||||
|
||||
if ( factor !== 1.0 && factor > 0.0 ) {
|
||||
|
||||
_eye.multiplyScalar( factor );
|
||||
|
||||
}
|
||||
|
||||
if ( _this.staticMoving ) {
|
||||
|
||||
_zoomStart.copy( _zoomEnd );
|
||||
|
||||
} else {
|
||||
|
||||
_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.panCamera = ( function() {
|
||||
|
||||
var mouseChange = new THREE.Vector2(),
|
||||
objectUp = new THREE.Vector3(),
|
||||
pan = new THREE.Vector3();
|
||||
|
||||
return function panCamera() {
|
||||
|
||||
mouseChange.copy( _panEnd ).sub( _panStart );
|
||||
|
||||
if ( mouseChange.lengthSq() ) {
|
||||
|
||||
mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );
|
||||
|
||||
pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
|
||||
pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );
|
||||
|
||||
_this.object.position.add( pan );
|
||||
_this.target.add( pan );
|
||||
|
||||
if ( _this.staticMoving ) {
|
||||
|
||||
_panStart.copy( _panEnd );
|
||||
|
||||
} else {
|
||||
|
||||
_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}() );
|
||||
|
||||
this.checkDistances = function () {
|
||||
|
||||
if ( ! _this.noZoom || ! _this.noPan ) {
|
||||
|
||||
if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) {
|
||||
|
||||
_this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) );
|
||||
_zoomStart.copy( _zoomEnd );
|
||||
|
||||
}
|
||||
|
||||
if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {
|
||||
|
||||
_this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );
|
||||
_zoomStart.copy( _zoomEnd );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.update = function () {
|
||||
|
||||
_eye.subVectors( _this.object.position, _this.target );
|
||||
|
||||
if ( ! _this.noRotate ) {
|
||||
|
||||
_this.rotateCamera();
|
||||
|
||||
}
|
||||
|
||||
if ( ! _this.noZoom ) {
|
||||
|
||||
_this.zoomCamera();
|
||||
|
||||
}
|
||||
|
||||
if ( ! _this.noPan ) {
|
||||
|
||||
_this.panCamera();
|
||||
|
||||
}
|
||||
|
||||
_this.object.position.addVectors( _this.target, _eye );
|
||||
|
||||
_this.checkDistances();
|
||||
|
||||
_this.object.lookAt( _this.target );
|
||||
|
||||
if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) {
|
||||
|
||||
_this.dispatchEvent( changeEvent );
|
||||
|
||||
lastPosition.copy( _this.object.position );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.reset = function () {
|
||||
|
||||
_state = STATE.NONE;
|
||||
_prevState = STATE.NONE;
|
||||
|
||||
_this.target.copy( _this.target0 );
|
||||
_this.object.position.copy( _this.position0 );
|
||||
_this.object.up.copy( _this.up0 );
|
||||
|
||||
_eye.subVectors( _this.object.position, _this.target );
|
||||
|
||||
_this.object.lookAt( _this.target );
|
||||
|
||||
_this.dispatchEvent( changeEvent );
|
||||
|
||||
lastPosition.copy( _this.object.position );
|
||||
|
||||
};
|
||||
|
||||
// listeners
|
||||
|
||||
function keydown( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
window.removeEventListener( 'keydown', keydown );
|
||||
|
||||
_prevState = _state;
|
||||
|
||||
if ( _state !== STATE.NONE ) {
|
||||
|
||||
return;
|
||||
|
||||
} else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) {
|
||||
|
||||
_state = STATE.ROTATE;
|
||||
|
||||
} else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) {
|
||||
|
||||
_state = STATE.ZOOM;
|
||||
|
||||
} else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) {
|
||||
|
||||
_state = STATE.PAN;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function keyup( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
_state = _prevState;
|
||||
|
||||
window.addEventListener( 'keydown', keydown, false );
|
||||
|
||||
}
|
||||
|
||||
function mousedown( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if ( _state === STATE.NONE ) {
|
||||
|
||||
_state = event.button;
|
||||
|
||||
}
|
||||
|
||||
if ( _state === STATE.ROTATE && ! _this.noRotate ) {
|
||||
|
||||
_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
|
||||
_movePrev.copy( _moveCurr );
|
||||
|
||||
} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
|
||||
|
||||
_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||
_zoomEnd.copy( _zoomStart );
|
||||
|
||||
} else if ( _state === STATE.PAN && ! _this.noPan ) {
|
||||
|
||||
_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||
_panEnd.copy( _panStart );
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener( 'mousemove', mousemove, false );
|
||||
document.addEventListener( 'mouseup', mouseup, false );
|
||||
|
||||
_this.dispatchEvent( startEvent );
|
||||
|
||||
}
|
||||
|
||||
function mousemove( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if ( _state === STATE.ROTATE && ! _this.noRotate ) {
|
||||
|
||||
_movePrev.copy( _moveCurr );
|
||||
_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
|
||||
|
||||
} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
|
||||
|
||||
_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||
|
||||
} else if ( _state === STATE.PAN && ! _this.noPan ) {
|
||||
|
||||
_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function mouseup( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
_state = STATE.NONE;
|
||||
|
||||
document.removeEventListener( 'mousemove', mousemove );
|
||||
document.removeEventListener( 'mouseup', mouseup );
|
||||
_this.dispatchEvent( endEvent );
|
||||
|
||||
}
|
||||
|
||||
function mousewheel( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
switch ( event.deltaMode ) {
|
||||
|
||||
case 2:
|
||||
// Zoom in pages
|
||||
_zoomStart.y -= event.deltaY * 0.025;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Zoom in lines
|
||||
_zoomStart.y -= event.deltaY * 0.01;
|
||||
break;
|
||||
|
||||
default:
|
||||
// undefined, 0, assume pixels
|
||||
_zoomStart.y -= event.deltaY * 0.00025;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
_this.dispatchEvent( startEvent );
|
||||
_this.dispatchEvent( endEvent );
|
||||
|
||||
}
|
||||
|
||||
function touchstart( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
switch ( event.touches.length ) {
|
||||
|
||||
case 1:
|
||||
_state = STATE.TOUCH_ROTATE;
|
||||
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
|
||||
_movePrev.copy( _moveCurr );
|
||||
break;
|
||||
|
||||
default: // 2 or more
|
||||
_state = STATE.TOUCH_ZOOM_PAN;
|
||||
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
|
||||
|
||||
var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
|
||||
var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
|
||||
_panStart.copy( getMouseOnScreen( x, y ) );
|
||||
_panEnd.copy( _panStart );
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
_this.dispatchEvent( startEvent );
|
||||
|
||||
}
|
||||
|
||||
function touchmove( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
switch ( event.touches.length ) {
|
||||
|
||||
case 1:
|
||||
_movePrev.copy( _moveCurr );
|
||||
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
|
||||
break;
|
||||
|
||||
default: // 2 or more
|
||||
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
|
||||
|
||||
var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
|
||||
var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
|
||||
_panEnd.copy( getMouseOnScreen( x, y ) );
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function touchend( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
switch ( event.touches.length ) {
|
||||
|
||||
case 0:
|
||||
_state = STATE.NONE;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
_state = STATE.TOUCH_ROTATE;
|
||||
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
|
||||
_movePrev.copy( _moveCurr );
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
_this.dispatchEvent( endEvent );
|
||||
|
||||
}
|
||||
|
||||
function contextmenu( event ) {
|
||||
|
||||
if ( _this.enabled === false ) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
}
|
||||
|
||||
this.dispose = function() {
|
||||
|
||||
this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
|
||||
this.domElement.removeEventListener( 'mousedown', mousedown, false );
|
||||
this.domElement.removeEventListener( 'wheel', mousewheel, false );
|
||||
|
||||
this.domElement.removeEventListener( 'touchstart', touchstart, false );
|
||||
this.domElement.removeEventListener( 'touchend', touchend, false );
|
||||
this.domElement.removeEventListener( 'touchmove', touchmove, false );
|
||||
|
||||
document.removeEventListener( 'mousemove', mousemove, false );
|
||||
document.removeEventListener( 'mouseup', mouseup, false );
|
||||
|
||||
window.removeEventListener( 'keydown', keydown, false );
|
||||
window.removeEventListener( 'keyup', keyup, false );
|
||||
|
||||
};
|
||||
|
||||
this.domElement.addEventListener( 'contextmenu', contextmenu, false );
|
||||
this.domElement.addEventListener( 'mousedown', mousedown, false );
|
||||
this.domElement.addEventListener( 'wheel', mousewheel, false );
|
||||
|
||||
this.domElement.addEventListener( 'touchstart', touchstart, false );
|
||||
this.domElement.addEventListener( 'touchend', touchend, false );
|
||||
this.domElement.addEventListener( 'touchmove', touchmove, false );
|
||||
|
||||
window.addEventListener( 'keydown', keydown, false );
|
||||
window.addEventListener( 'keyup', keyup, false );
|
||||
|
||||
this.handleResize();
|
||||
|
||||
// force an update at start
|
||||
this.update();
|
||||
|
||||
};
|
||||
|
||||
THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
|
||||
THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;
|
26
web/js/ethereum.js
Normal file
26
web/js/ethereum.js
Normal file
@ -0,0 +1,26 @@
|
||||
function NewEthereumGeometry(scale) {
|
||||
let geom = new THREE.Geometry();
|
||||
geom.vertices.push(
|
||||
new THREE.Vector3( scale*1, 0, 0 ),
|
||||
new THREE.Vector3( -scale*1, 0, 0 ),
|
||||
new THREE.Vector3( 0, scale*1.5, 0 ),
|
||||
new THREE.Vector3( 0, scale*-1.5, 0 ),
|
||||
new THREE.Vector3( 0, 0, scale*1 ),
|
||||
new THREE.Vector3( 0, 0, -scale*1 )
|
||||
);
|
||||
geom.faces.push(
|
||||
new THREE.Face3( 0, 2, 4 ),
|
||||
new THREE.Face3( 0, 4, 3 ),
|
||||
new THREE.Face3( 0, 3, 5 ),
|
||||
new THREE.Face3( 0, 5, 2 ),
|
||||
new THREE.Face3( 1, 2, 5 ),
|
||||
new THREE.Face3( 1, 5, 3 ),
|
||||
new THREE.Face3( 1, 3, 4 ),
|
||||
new THREE.Face3( 1, 4, 2 )
|
||||
);
|
||||
geom.computeBoundingSphere();
|
||||
geom.computeFaceNormals();
|
||||
return geom;
|
||||
}
|
||||
|
||||
module.exports = { NewEthereumGeometry };
|
2
web/js/keys.js
Normal file
2
web/js/keys.js
Normal file
@ -0,0 +1,2 @@
|
||||
document.addEventListener("keydown", function(event) {
|
||||
});
|
18
web/js/shitty_hacks.js
Normal file
18
web/js/shitty_hacks.js
Normal file
@ -0,0 +1,18 @@
|
||||
// accessorFn
|
||||
function accessorFn(p) {
|
||||
if (p instanceof Function) {
|
||||
return p // fn
|
||||
}
|
||||
|
||||
if (typeof p === 'string') {
|
||||
return function(obj) {
|
||||
return obj[p]; // property name
|
||||
}
|
||||
}
|
||||
|
||||
return function (obj){
|
||||
return p; // constant
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = accessorFn;
|
26
web/js/ws.js
Normal file
26
web/js/ws.js
Normal file
@ -0,0 +1,26 @@
|
||||
var graph = require('../index.js');
|
||||
|
||||
var ws = new WebSocket('ws://' + window.location.host + '/ws');
|
||||
|
||||
// request graphData and initial positions from websocket connection
|
||||
ws.onopen = function (event) {
|
||||
ws.send('{"cmd": "init"}');
|
||||
};
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
let msg = JSON.parse(event.data);
|
||||
switch(msg.type) {
|
||||
case "graph":
|
||||
graph.setGraphData(msg.graph);
|
||||
break;
|
||||
case "propagation":
|
||||
graph.setPropagation(msg.propagation);
|
||||
break;
|
||||
case "positions":
|
||||
console.log("Updating positions...");
|
||||
graph.updatePositions(msg.positions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ws };
|
50
web/package-lock.json
generated
Normal file
50
web/package-lock.json
generated
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "clean",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"d3-color": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.0.3.tgz",
|
||||
"integrity": "sha1-vHZD/KjlOoNH4vva/6I2eWtYUJs="
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.1.6.tgz",
|
||||
"integrity": "sha512-mOnv5a+pZzkNIHtw/V6I+w9Lqm9L5bG3OTXPM5A+QO0yyVMQ4W1uZhR+VOJmazaOZXri2ppbiZ5BUNWT0pFM9A==",
|
||||
"requires": {
|
||||
"d3-color": "1.0.3"
|
||||
}
|
||||
},
|
||||
"d3-scale-chromatic": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.2.0.tgz",
|
||||
"integrity": "sha512-qQUhLi8fPe/F0b0M46C6eFUbms5IIMHuhJ5DKjjzBUvm1b6aPtygJzGbrMdMUD/ckLBq+NdWwHeN2cpMDp4Q5Q==",
|
||||
"requires": {
|
||||
"d3-color": "1.0.3",
|
||||
"d3-interpolate": "1.1.6"
|
||||
}
|
||||
},
|
||||
"dat.gui": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.1.tgz",
|
||||
"integrity": "sha512-fTv1JQqjwA1Rtjy/i0ThdLgUOSWA3hOJrvNEl3XrfaMvCcFdV0h01bo3aGXknLx3okJ7AO9qqgVz6TbuRHxYAw=="
|
||||
},
|
||||
"stats-js": {
|
||||
"version": "1.0.0-alpha1",
|
||||
"resolved": "https://registry.npmjs.org/stats-js/-/stats-js-1.0.0-alpha1.tgz",
|
||||
"integrity": "sha1-RK6C5kIRI7q5l1DZXD9VJrjyV9s="
|
||||
},
|
||||
"three": {
|
||||
"version": "0.88.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.88.0.tgz",
|
||||
"integrity": "sha1-QlbC/Djk+yOg0j66K2zOTfjkZtU="
|
||||
},
|
||||
"tinycolor2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
|
||||
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
|
||||
}
|
||||
}
|
||||
}
|
18
web/package.json
Normal file
18
web/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "clean",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-scale-chromatic": "^1.2.0",
|
||||
"dat.gui": "^0.7.1",
|
||||
"stats-js": "^1.0.0-alpha1",
|
||||
"three": "^0.88.0",
|
||||
"tinycolor2": "^1.4.1"
|
||||
}
|
||||
}
|
107
ws.go
Normal file
107
ws.go
Normal file
@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/divan/graph-experiments/graph"
|
||||
"github.com/divan/graph-experiments/layout"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WSServer struct {
|
||||
upgrader websocket.Upgrader
|
||||
hub []*websocket.Conn
|
||||
|
||||
Positions []*position
|
||||
layout layout.Layout
|
||||
graph *graph.Graph
|
||||
propagation *PropagationLog
|
||||
}
|
||||
|
||||
func NewWSServer(layout layout.Layout) *WSServer {
|
||||
ws := &WSServer{
|
||||
upgrader: websocket.Upgrader{},
|
||||
layout: layout,
|
||||
}
|
||||
ws.updatePositions()
|
||||
return ws
|
||||
}
|
||||
|
||||
type WSResponse struct {
|
||||
Type MsgType `json:"type"`
|
||||
Positions []*position `json:"positions,omitempty"`
|
||||
Graph json.RawMessage `json:"graph,omitempty"`
|
||||
Propagation *PropagationLog `json:"propagation,omitempty"`
|
||||
}
|
||||
|
||||
type WSRequest struct {
|
||||
Cmd WSCommand `json:"cmd"`
|
||||
}
|
||||
|
||||
type MsgType string
|
||||
type WSCommand string
|
||||
|
||||
// WebSocket response types
|
||||
const (
|
||||
RespPositions MsgType = "positions"
|
||||
RespGraph MsgType = "graph"
|
||||
RespPropagation MsgType = "propagation"
|
||||
)
|
||||
|
||||
// WebSocket commands
|
||||
const (
|
||||
CmdInit WSCommand = "init"
|
||||
)
|
||||
|
||||
func (ws *WSServer) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := ws.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
ws.hub = append(ws.hub, c)
|
||||
|
||||
for {
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("read:", mt, err)
|
||||
break
|
||||
}
|
||||
ws.processRequest(c, mt, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WSServer) processRequest(c *websocket.Conn, mtype int, data []byte) {
|
||||
var cmd WSRequest
|
||||
err := json.Unmarshal(data, &cmd)
|
||||
if err != nil {
|
||||
log.Fatal("unmarshal command", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch cmd.Cmd {
|
||||
case CmdInit:
|
||||
ws.sendGraphData(c)
|
||||
ws.updatePositions()
|
||||
ws.sendPositions(c)
|
||||
ws.sendPropagationData(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WSServer) sendMsg(c *websocket.Conn, msg *WSResponse) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Println("write:", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.WriteMessage(1, data)
|
||||
if err != nil {
|
||||
log.Println("write:", err)
|
||||
return
|
||||
}
|
||||
}
|
37
ws_graph.go
Normal file
37
ws_graph.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/divan/graph-experiments/export"
|
||||
"github.com/divan/graph-experiments/graph"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (ws *WSServer) sendGraphData(c *websocket.Conn) {
|
||||
var buf bytes.Buffer
|
||||
err := export.NewJSON(&buf, false).ExportGraph(ws.graph)
|
||||
if err != nil {
|
||||
log.Fatal("Can't marshal graph to JSON")
|
||||
}
|
||||
msg := &WSResponse{
|
||||
Type: RespGraph,
|
||||
Graph: json.RawMessage(buf.Bytes()),
|
||||
}
|
||||
|
||||
ws.sendMsg(c, msg)
|
||||
}
|
||||
|
||||
func (ws *WSServer) updateGraph(g *graph.Graph) {
|
||||
ws.graph = g
|
||||
|
||||
ws.broadcastGraphData()
|
||||
}
|
||||
|
||||
func (ws *WSServer) broadcastGraphData() {
|
||||
for i := 0; i < len(ws.hub); i++ {
|
||||
ws.sendGraphData(ws.hub[i])
|
||||
}
|
||||
}
|
41
ws_positions.go
Normal file
41
ws_positions.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import "github.com/gorilla/websocket"
|
||||
|
||||
type position struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Z int `json:"z"`
|
||||
}
|
||||
|
||||
func (ws *WSServer) sendPositions(c *websocket.Conn) {
|
||||
msg := &WSResponse{
|
||||
Type: RespPositions,
|
||||
Positions: ws.Positions,
|
||||
}
|
||||
|
||||
ws.sendMsg(c, msg)
|
||||
}
|
||||
|
||||
func (ws *WSServer) updatePositions() {
|
||||
// positions
|
||||
nodes := ws.layout.Nodes()
|
||||
positions := []*position{}
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
pos := &position{
|
||||
X: nodes[i].X,
|
||||
Y: nodes[i].Y,
|
||||
Z: nodes[i].Z,
|
||||
}
|
||||
positions = append(positions, pos)
|
||||
}
|
||||
ws.Positions = positions
|
||||
|
||||
ws.broadcastPositions()
|
||||
}
|
||||
|
||||
func (ws *WSServer) broadcastPositions() {
|
||||
for i := 0; i < len(ws.hub); i++ {
|
||||
ws.sendPositions(ws.hub[i])
|
||||
}
|
||||
}
|
26
ws_propagation.go
Normal file
26
ws_propagation.go
Normal file
@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (ws *WSServer) sendPropagationData(c *websocket.Conn) {
|
||||
msg := &WSResponse{
|
||||
Type: RespPropagation,
|
||||
Propagation: ws.propagation,
|
||||
}
|
||||
|
||||
ws.sendMsg(c, msg)
|
||||
}
|
||||
|
||||
func (ws *WSServer) updatePropagationData(plog *PropagationLog) {
|
||||
ws.propagation = plog
|
||||
|
||||
ws.broadcastPropagationData()
|
||||
}
|
||||
|
||||
func (ws *WSServer) broadcastPropagationData() {
|
||||
for i := 0; i < len(ws.hub); i++ {
|
||||
ws.sendPropagationData(ws.hub[i])
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user