diff --git a/hugvey/central_command.py b/hugvey/central_command.py index a330e22..13f432f 100644 --- a/hugvey/central_command.py +++ b/hugvey/central_command.py @@ -3,22 +3,23 @@ This server controls all hugveys and the processing of their narratives. It exposes itself for control to the panopticon server. """ -import asyncio -import logging -import threading +import os import time import yaml import zmq from zmq.asyncio import Context +import asyncio from hugvey.communication import getTopic, zmqSend, zmqReceive from hugvey.panopticon import Panopticon from hugvey.story import Story from hugvey.voice.google import GoogleVoiceClient from hugvey.voice.player import Player from hugvey.voice.streamer import AudioStreamer +import json +import logging import queue -import os +import threading logger = logging.getLogger("command") @@ -72,7 +73,8 @@ class CentralCommand(object): lang_filename = os.path.join(self.config['web']['files_dir'], lang['file']) self.languageFiles[lang['code']] = lang['file'] with open(lang_filename, 'r') as fp: - self.languages[lang['code']] = yaml.load(fp) + self.languages[lang['code']] = json.load(fp) + print(self.languages) self.panopticon = Panopticon(self, self.config) diff --git a/hugvey/panopticon.py b/hugvey/panopticon.py index 0c87265..8465bc3 100644 --- a/hugvey/panopticon.py +++ b/hugvey/panopticon.py @@ -90,6 +90,11 @@ def getWebSocketHandler(central_command): return WebSocketHandler +class NonCachingStaticFileHandler(tornado.web.StaticFileHandler): + def set_extra_headers(self, path): + # Disable cache + self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + def getUploadHandler(central_command): class UploadHandler(tornado.web.RequestHandler): def post(self): @@ -97,9 +102,10 @@ def getUploadHandler(central_command): langCode = self.get_argument("language") langFile = os.path.join(central_command.config['web']['files_dir'] , central_command.languageFiles[langCode]) - print(self.request.files['json'][0]) storyData = json.loads(self.request.files['json'][0]['body']) - print(storyData) +# print(json.dumps(storyData)) +# self.finish() +# return if 'audio' in self.request.files: msgId = self.get_argument("message_id") @@ -120,9 +126,10 @@ def getUploadHandler(central_command): # fp.write(audioFile['body']) break - with open(langFile, 'r') as fp: - logger.info(f'Save story to {langFile}') -# json.dump(storyData, fp) + print(os.path.abspath(langFile)) + with open(langFile, 'w') as json_fp: + logger.info(f'Save story to {langFile} {json_fp}') + json.dump(storyData, json_fp) self.finish() return UploadHandler @@ -132,7 +139,7 @@ class Panopticon(object): self.config = config self.application = tornado.web.Application([ (r"/ws", getWebSocketHandler(self.command)), - (r"/local/(.*)", tornado.web.StaticFileHandler, + (r"/local/(.*)", NonCachingStaticFileHandler, {"path": config['web']['files_dir']}), (r"/upload", getUploadHandler(self.command)), (r"/(.*)", tornado.web.StaticFileHandler, diff --git a/www/css/styles.css b/www/css/styles.css index 4da4b77..5dd028d 100644 --- a/www/css/styles.css +++ b/www/css/styles.css @@ -2,13 +2,22 @@ body { font-family: "Noto Sans", sans-serif; margin: 0; } -.btn { +.btn, input[type="submit"] { display: inline-block; cursor: pointer; background: #333; padding: 5px; color: white; - border-radius: 5px; } + border-radius: 5px; + margin-right: 5px; + white-space: nowrap; + border: none; } + .btn:hover, input[type="submit"]:hover { + background: #666; } + +@keyframes dash-animation { + to { + stroke-dashoffset: -1000; } } #interface { display: flex; @@ -29,6 +38,8 @@ body { border: solid 1px; box-sizing: border-box; position: relative; } + #status > div#overview { + width: 66.66667%; } #status .hugvey { background-image: linear-gradient(to top, #587457, #35a589); color: white; @@ -52,11 +63,13 @@ body { text-align: center; } #story { - position: relative; } + position: relative; + width: calc(100% - 430px); } #story #controls { position: absolute; top: 5px; - left: 5px; } + left: 5px; + white-space: nowrap; } #story svg#graph { width: 100%; height: 100%; @@ -77,13 +90,22 @@ body { font-size: 11pt; font-family: sans-serif; fill: white; } + #story text.msg_id { + transform: translateY(-20px); + opacity: .5; } + #story text.msg_txt { + font-weight: bold; } #story line { marker-end: url("#arrowHead"); stroke-width: 2px; stroke: black; } - #story line.link--noconditions { - stroke-dasharray: 5 4; - stroke: red; } + #story line.link--noconditions { + stroke-dasharray: 5 4; + stroke: red; } + #story line.dir-highlight { + stroke-dasharray: 5; + animation: dash-animation 20s infinite linear; + stroke-width: 3px; } #story label::after { content: ''; clear: both; @@ -109,6 +131,18 @@ body { padding: 10px; margin-bottom: 10px; background: lightgray; } + #story #msg .direction { + position: relative; } + #story #msg .direction h3 { + margin-top: 0; } + #story #msg .direction .btn--delete { + position: absolute; + top: 5px; + right: 0px; } + #story #msg .direction .condition--add h4 { + margin: 0; } + #story #msg .direction .condition--add h4 + div { + margin-top: 10px; } #story #nodes g:hover circle, #story .selectedMsg circle { stroke: lightgreen; @@ -130,3 +164,33 @@ body { text-shadow: 2px 2px 2px lightgray,-2px 2px 2px lightgray,2px -2px 2px lightgray,-2px -2px 2px lightgray; } #story .condition--add { /* text-align: center; */ } + +.flag-icon { + background-size: contain; + background-position: 50%; + background-repeat: no-repeat; + position: relative; + display: inline-block; + width: 1.33333em; + line-height: 1em; } + .flag-icon:before { + content: '\00a0'; } + .flag-icon.flag-icon-squared { + width: 1em; } + .flag-icon.en-GB { + background-image: url("/images/gb.svg"); } + .flag-icon.de-DE { + background-image: url("/images/de.svg"); } + .flag-icon.fr-FR { + background-image: url("/images/fr.svg"); } + .flag-icon.nl-NL { + background-image: url("/images/nl.svg"); } + +.divToggle { + cursor: pointer; } + .divToggle:hover { + text-decoration: underline; } + .divToggle.opened + div { + display: block; } + .divToggle + div { + display: none; } diff --git a/www/images/be.svg b/www/images/be.svg new file mode 100644 index 0000000..eaf016d --- /dev/null +++ b/www/images/be.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/www/images/de.svg b/www/images/de.svg new file mode 100644 index 0000000..1acf302 --- /dev/null +++ b/www/images/de.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/www/images/fr.svg b/www/images/fr.svg new file mode 100644 index 0000000..712c8a5 --- /dev/null +++ b/www/images/fr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/www/images/gb.svg b/www/images/gb.svg new file mode 100644 index 0000000..d98b6cc --- /dev/null +++ b/www/images/gb.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/www/images/nl.svg b/www/images/nl.svg new file mode 100644 index 0000000..d48808c --- /dev/null +++ b/www/images/nl.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/www/index.html b/www/index.html index 4e8b5fc..159983e 100644 --- a/www/index.html +++ b/www/index.html @@ -11,16 +11,18 @@ -
+
Uptime
{{uptime}}
-
Languages
-
{{lang.code}}
+ +
    +
  • {{lang.code}}
  • +
@@ -43,6 +45,7 @@
+
Save
Create message
diff --git a/www/js/hugvey_console.js b/www/js/hugvey_console.js index af7620e..ef15563 100644 --- a/www/js/hugvey_console.js +++ b/www/js/hugvey_console.js @@ -11,18 +11,18 @@ class Panopticon { hugveys: [] }, methods: { - time_passed: function (hugvey, property) { - console.log("property!", Date(hugvey[property] * 1000)); - return moment(Date(hugvey[property] * 1000)).fromNow(); + time_passed: function( hugvey, property ) { + console.log( "property!", Date( hugvey[property] * 1000 ) ); + return moment( Date( hugvey[property] * 1000 ) ).fromNow(); }, - loadNarrative: function(code, file) { - return panopticon.loadNarrative(code, file); + loadNarrative: function( code, file ) { + return panopticon.loadNarrative( code, file ); } } } ); - - + + this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } ); this.graph = new Graph(); @@ -37,18 +37,18 @@ class Panopticon { this.socket.addEventListener( 'message', ( e ) => { let msg = JSON.parse( e.data ); if ( typeof msg['alert'] !== 'undefined' ) { - alert(msg['alert']); + alert( msg['alert'] ); } - + if ( typeof msg['action'] === 'undefined' ) { console.error( "not a valid message: " + e.data ); return; } switch ( msg['action'] ) { - + case 'status': - this.hugveys.uptime = this.stringToHHMMSS(msg['uptime']); + this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] ); this.hugveys.languages = msg['languages']; this.hugveys.hugveys = msg['hugveys']; break; @@ -57,54 +57,54 @@ class Panopticon { } send( msg ) { - if(this.socket.readyState == WebSocket.OPEN) { - this.socket.send( JSON.stringify( msg ) ); + if ( this.socket.readyState == WebSocket.OPEN ) { + this.socket.send( JSON.stringify( msg ) ); } else { - console.error("Socket not open: ", this.socket.readyState); + console.error( "Socket not open: ", this.socket.readyState ); } } getStatus() { -// console.log('get status', this, panopticon); + // console.log('get status', this, panopticon); panopticon.send( { action: 'get_status' } ); } init() { setInterval( this.getStatus, 3000 ); } - - stringToHHMMSS (string) { - var sec_num = parseInt(string, 10); // don't forget the second param - var hours = Math.floor(sec_num / 3600); - var minutes = Math.floor((sec_num - (hours * 3600)) / 60); - var seconds = sec_num - (hours * 3600) - (minutes * 60); - if (hours < 10) {hours = "0"+hours;} - if (minutes < 10) {minutes = "0"+minutes;} - if (seconds < 10) {seconds = "0"+seconds;} - return hours+':'+minutes+':'+seconds; + stringToHHMMSS( string ) { + var sec_num = parseInt( string, 10 ); // don't forget the second param + var hours = Math.floor( sec_num / 3600 ); + var minutes = Math.floor(( sec_num - ( hours * 3600 ) ) / 60 ); + var seconds = sec_num - ( hours * 3600 ) - ( minutes * 60 ); + + if ( hours < 10 ) { hours = "0" + hours; } + if ( minutes < 10 ) { minutes = "0" + minutes; } + if ( seconds < 10 ) { seconds = "0" + seconds; } + return hours + ':' + minutes + ':' + seconds; } - - - loadNarrative(code, file) { + + + loadNarrative( code, file ) { let req = new XMLHttpRequest(); let graph = this.graph; - req.addEventListener("load", function(e){ - graph.loadData(JSON.parse(this.response), code); -// console.log(, e); - }); - req.open("GET", "/local/" + file); + req.addEventListener( "load", function( e ) { + graph.loadData( JSON.parse( this.response ), code ); + // console.log(, e); + } ); + req.open( "GET", "/local/" + file ); req.send(); } - resume(hv_id) { - this.send({ action: 'resume', hugvey: hv_id }) + resume( hv_id ) { + this.send( { action: 'resume', hugvey: hv_id } ) } - pause(hv_id) { - this.send({ action: 'play', hugvey: hv_id }) + pause( hv_id ) { + this.send( { action: 'play', hugvey: hv_id } ) } - restart(hv_id) { - this.send({ action: 'restart', hugvey: hv_id }) + restart( hv_id ) { + this.send( { action: 'restart', hugvey: hv_id } ) } } @@ -113,16 +113,16 @@ class Panopticon { window.addEventListener( 'load', function() { panopticon = new Panopticon(); panopticon.init(); -}); +} ); -class Graph{ +class Graph { constructor() { this.width = 1280; this.height = 1024; this.nodeSize = 80; this.maxChars = 16; - this.svg = d3.select('#graph'); - this.container = d3.select('#container'); + this.svg = d3.select( '#graph' ); + this.container = d3.select( '#container' ); this.selectedMsg = null; this.language_code = null; this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object @@ -132,79 +132,79 @@ class Graph{ let graph = this; this.controlDown = false; - document.addEventListener('keydown', function(e){ - console.log(e); - if(e.which == "17") { + document.addEventListener( 'keydown', function( e ) { + console.log( e ); + if ( e.which == "17" ) { graph.controlDown = true; - document.body.classList.add('controlDown'); + document.body.classList.add( 'controlDown' ); } - }); - document.addEventListener('keyup', function(e){ - console.log(e); - if(e.which == "17") { + } ); + document.addEventListener( 'keyup', function( e ) { + console.log( e ); + if ( e.which == "17" ) { graph.controlDown = false; - document.body.classList.remove('controlDown'); + document.body.classList.remove( 'controlDown' ); } - }); + } ); let c = this.container; - let zoomed = function(){ - c.attr("transform", d3.event.transform); + let zoomed = function() { + c.attr( "transform", d3.event.transform ); } - this.svg.call(d3.zoom() - .scaleExtent([1 / 2, 8]) - .on("zoom", zoomed)); + this.svg.call( d3.zoom() + .scaleExtent( [1 / 2, 8] ) + .on( "zoom", zoomed ) ); - this.nodesG = this.container.append("g") - .attr("id", "nodes") + this.nodesG = this.container.append( "g" ) + .attr( "id", "nodes" ) - this.linkG = this.container.append("g") - .attr("id", "links"); + this.linkG = this.container.append( "g" ) + .attr( "id", "links" ); - document.getElementById('btn-save').addEventListener('click', function(e){ graph.saveJson(); }); - document.getElementById('btn-addMsg').addEventListener('click', function(e){ graph.createMsg(); }); + document.getElementById( 'btn-save' ).addEventListener( 'click', function( e ) { graph.saveJson(); } ); + document.getElementById( 'btn-addMsg' ).addEventListener( 'click', function( e ) { graph.createMsg(); } ); } - clickMsg(msg) { + clickMsg( msg ) { // event when a message is clicked. - console.log(msg); + console.log( msg ); - if(this.controlDown) { - this.secondarySelectMsg(msg); + if ( this.controlDown ) { + this.secondarySelectMsg( msg ); } else { - this.selectMsg(msg); + this.selectMsg( msg ); } } - secondarySelectMsg(msg) { - if(this.selectedMsg !== null) { - this.addDirection(this.selectedMsg, msg); + secondarySelectMsg( msg ) { + if ( this.selectedMsg !== null ) { + this.addDirection( this.selectedMsg, msg ); } else { - console.error('No message selected as Source'); + console.error( 'No message selected as Source' ); } } - selectMsg(msg) { - let selectedEls = document.getElementsByClassName('selectedMsg'); - while(selectedEls.length > 0){ - selectedEls[0].classList.remove('selectedMsg'); + selectMsg( msg ) { + let selectedEls = document.getElementsByClassName( 'selectedMsg' ); + while ( selectedEls.length > 0 ) { + selectedEls[0].classList.remove( 'selectedMsg' ); } - document.getElementById(msg['@id']).classList.add('selectedMsg'); + document.getElementById( msg['@id'] ).classList.add( 'selectedMsg' ); this.selectedMsg = msg; - this.showMsg(msg); + this.showMsg( msg ); } updateMsg() { // used eg. after a condition creation. - this.showMsg(this.selectedMsg); + this.showMsg( this.selectedMsg ); } - showMsg(msg) { - let msgEl = document.getElementById('msg'); + showMsg( msg ) { + let msgEl = document.getElementById( 'msg' ); msgEl.innerHTML = ""; - let startAttributes = { + let startAttributes = { 'name': msg['@id'] + '-start', 'disabled': true, 'type': 'checkbox', @@ -212,14 +212,14 @@ class Graph{ 'change': this.getEditEventListener() } } - if(msg['start'] == true) { + if ( msg['start'] == true ) { startAttributes['checked'] = 'checked'; } - let msgInfoEl = crel('div', {'class': 'msg__info'}, - crel('h1', {'class':'msg__id'}, msg['@id']), - crel('label', - crel('span', 'Text'), - crel('input', { + let msgInfoEl = crel( 'div', { 'class': 'msg__info' }, + crel( 'h1', { 'class': 'msg__id' }, msg['@id'] ), + crel( 'label', + crel( 'span', 'Text' ), + crel( 'input', { 'name': msg['@id'] + '-text', 'value': msg['text'], 'on': { @@ -227,274 +227,299 @@ class Graph{ } } ) ), - crel('label', - crel('span', 'Start'), - crel('input', startAttributes) + crel( 'label', + crel( 'span', 'Start' ), + crel( 'input', startAttributes ) ) ); - msgEl.appendChild(msgInfoEl); + msgEl.appendChild( msgInfoEl ); + +// crel('form') // let directionHEl = document.createElement('h2'); // directionHEl.innerHTML = "Directions"; - let fromDirections =[] , toDirections = []; + let fromDirections = [], toDirections = []; - for(let direction of this.getDirectionsTo(msg)) { - toDirections.push(this.getDirectionEl(direction, msg)); + for ( let direction of this.getDirectionsTo( msg ) ) { + toDirections.push( this.getDirectionEl( direction, msg ) ); } - for(let direction of this.getDirectionsFrom(msg)) { - fromDirections.push(this.getDirectionEl(direction, msg)); + for ( let direction of this.getDirectionsFrom( msg ) ) { + fromDirections.push( this.getDirectionEl( direction, msg ) ); } - let directionsEl = crel('div', {'class': 'directions'}, - crel('h2', 'Directions'), + let directionsEl = crel( 'div', { 'class': 'directions' }, + crel( 'h2', 'Directions' ), ...toDirections, ...fromDirections ); - msgEl.appendChild(directionsEl); + msgEl.appendChild( directionsEl ); } - getDirectionEl(direction, msg) { - let directionEl = document.createElement('div'); - if(direction['source'] == msg) { - directionEl.innerHTML = `

To ${direction['target']['@id']}

`; - } else { - directionEl.innerHTML = `

From ${direction['source']['@id']}

`; - } - let del = document.createElement('div'); - del.innerHTML = "delete"; - del.classList.add("deleteBtn"); + getDirectionEl( direction, msg ) { let g = this; - del.addEventListener('click', (e) => g.rmDirection(direction)); - directionEl.appendChild(del); - - // TODO; conditions - - for(let conditionId of direction['conditions']) { - let condition = this.getNodeById(conditionId); - directionEl.appendChild(this.getEditConditionFormEl(condition, direction)); + let directionEl = crel('div', + { + 'class': 'direction ' + (direction['source'] == msg ? 'dir-to' : 'dir-from'), + 'on': { + 'mouseover': function(e) { + console.log('over', direction['@id']); + directionEl.classList.add('dir-highlight'); + document.getElementById(direction['@id']).classList.add('dir-highlight'); + }, + 'mouseout': function(e) { + console.log('& out ', direction['@id']); + directionEl.classList.remove('dir-highlight'); + document.getElementById(direction['@id']).classList.remove('dir-highlight'); + } + } + }, + crel( + 'h3', + {'title': direction['@id']}, + direction['source'] == msg ? `To ${direction['target']['@id']}`: `From ${direction['source']['@id']}` + ), + crel('div', { + 'class':'btn btn--delete', + 'on': { + 'click': ( e ) => g.rmDirection( direction ) + } + }, 'delete') + ); + + for ( let conditionId of direction['conditions'] ) { + let condition = this.getNodeById( conditionId ); + directionEl.appendChild( this.getEditConditionFormEl( condition, direction ) ); } - directionEl.appendChild(this.getAddConditionFormEl(direction)); + directionEl.appendChild( this.getAddConditionFormEl( direction ) ); return directionEl; } - getEditConditionFormEl(condition, direction) { - let conditionEl = crel('div', {'class': 'condition condition--edit'}, - crel('h4', {'title': condition['@id']}, condition['type']) + getEditConditionFormEl( condition, direction ) { + let conditionEl = crel( 'div', { 'class': 'condition condition--edit' }, + crel( 'h4', { 'title': condition['@id'] }, condition['type'] ) ) - let labelLabel = document.createElement('label'); + let labelLabel = document.createElement( 'label' ); labelLabel.innerHTML = "Description"; - let labelInput = crel('input',{ - 'name': `${condition['@id']}-label`, - 'value': typeof condition['label'] == 'undefined' ? "" : condition['label'], - 'on': { - 'change': this.getEditEventListener() - } - }); - labelLabel.appendChild(labelInput); - conditionEl.appendChild(labelLabel); + let labelInput = crel( 'input', { + 'name': `${condition['@id']}-label`, + 'value': typeof condition['label'] == 'undefined' ? "" : condition['label'], + 'on': { + 'change': this.getEditEventListener() + } + } ); + labelLabel.appendChild( labelInput ); + conditionEl.appendChild( labelLabel ); - for(let v in condition['vars']) { - let varLabel = document.createElement('label'); + for ( let v in condition['vars'] ) { + let varLabel = document.createElement( 'label' ); varLabel.innerHTML = v; - let varInput = document.createElement('input'); - if(v == 'seconds') { + let varInput = document.createElement( 'input' ); + if ( v == 'seconds' ) { varInput.type = 'number'; } varInput.name = `${condition['@id']}-vars.${v}`; varInput.value = condition['vars'][v]; - varInput.addEventListener('change', this.getEditEventListener()); - varLabel.appendChild(varInput); - conditionEl.appendChild(varLabel); + varInput.addEventListener( 'change', this.getEditEventListener() ); + varLabel.appendChild( varInput ); + conditionEl.appendChild( varLabel ); } return conditionEl; } getConditionTypes() { - if(typeof this.conditionTypes === 'undefined') { + if ( typeof this.conditionTypes === 'undefined' ) { // type: vars: attribtes for crel() this.conditionTypes = { 'timeout': { - 'seconds': {'type': 'number', 'value': 10, 'min':0, 'step': 0.1} + 'seconds': { 'type': 'number', 'value': 10, 'min': 0, 'step': 0.1 } }, 'replyContains': { - 'regex': {'value': '.+'} + 'regex': { 'value': '.+' } } } } return this.conditionTypes; } - fillConditionFormForType(conditionForm, type) { + fillConditionFormForType( conditionForm, type ) { conditionForm.innerHTML = ""; let vars = this.getConditionTypes()[type]; - for(let v in vars){ + for ( let v in vars ) { let attr = vars[v]; attr['name'] = v; conditionForm.appendChild( - crel('label', - crel('span', v), - crel('input', attr) + crel( 'label', + crel( 'span', v ), + crel( 'input', attr ) ) ); } } - getAddConditionFormEl(direction) { + getAddConditionFormEl( direction ) { let optionEls = []; let types = this.getConditionTypes(); - for(let type in types) { - optionEls.push(crel('option', type)); + for ( let type in types ) { + optionEls.push( crel( 'option', type ) ); } - let conditionForm = crel('div', {'class': 'condition--vars'}); + let conditionForm = crel( 'div', { 'class': 'condition--vars' } ); let g = this; - let addConditionEl = crel('div', {'class': 'condition condition--add'}, - crel('form', { - 'on': { - 'submit': function(e) { - e.preventDefault(); - let form = new FormData(e.target); - console.log('submit', form); - let type = form.get('type'); - form.delete('type'); - let label = form.get('label'); - form.delete('label'); - let vars = {}; - for(var pair of form.entries()) { - vars[pair[0]] = pair[1]; - } - g.addConditionForDirection(type, label, vars, direction); + let addConditionEl = crel( 'div', { 'class': 'condition condition--add' }, + crel( 'form', { + 'on': { + 'submit': function( e ) { + e.preventDefault(); + let form = new FormData( e.target ); + console.log( 'submit', form ); + let type = form.get( 'type' ); + form.delete( 'type' ); + let label = form.get( 'label' ); + form.delete( 'label' ); + let vars = {}; + for ( var pair of form.entries() ) { + vars[pair[0]] = pair[1]; } + g.addConditionForDirection( type, label, vars, direction ); } - }, - crel("h4", "Create New Condition"), - crel("label", - crel('span', "Type"), - crel('select', { - 'name': 'type', - 'on': { - 'change': function(e){ - g.fillConditionFormForType(conditionForm, e.target.value); + } + }, + crel( "h4", { + 'class': "divToggle", + 'on': { + 'click': function(e) { this.classList.toggle('opened'); } + } + }, "Create New Condition" ), + crel('div', {'class': 'divToggle-target'}, + crel( "label", + crel( 'span', "Type" ), + crel( 'select', { + 'name': 'type', + 'on': { + 'change': function( e ) { + g.fillConditionFormForType( conditionForm, e.target.value ); + } } - }}, optionEls), - ), - crel("label", - crel('span', "Description"), - crel('input', {'name': 'label'}) - ), - conditionForm, - crel('input', { - 'type':'submit', - 'value':'create' - }) + }, optionEls ), + ), + crel( "label", + crel( 'span', "Description" ), + crel( 'input', { 'name': 'label' } ) + ), + conditionForm, + crel( 'input', { + 'type': 'submit', + 'value': 'create' + } ) + ) ) ); - this.fillConditionFormForType(conditionForm, optionEls[0].value); + this.fillConditionFormForType( conditionForm, optionEls[0].value ); return addConditionEl; } - rmConditionFromDirection(condition, direction) { + rmConditionFromDirection( condition, direction ) { let id = condition['@id']; // TODO - if(typeof direction != 'undefined') { + if ( typeof direction != 'undefined' ) { } - this._rmNode(id); + this._rmNode( id ); } - getConditionEl(condition) { - let conditionEl = document.createElement('div'); + getConditionEl( condition ) { + let conditionEl = document.createElement( 'div' ); return conditionEl; } - getDirectionsFrom(msg) { - return this.directions.filter(d => d['source'] == msg); + getDirectionsFrom( msg ) { + return this.directions.filter( d => d['source'] == msg ); } - getDirectionsTo(msg) { - return this.directions.filter(d => d['target'] == msg); + getDirectionsTo( msg ) { + return this.directions.filter( d => d['target'] == msg ); } addMsg() { let msg = { - "@id": "n" + Date.now().toString(36), + "@id": this.language_code.substring( 0, 2 ) + "-n" + Date.now().toString( 36 ), "@type": "Msg", "text": "New", "start": false } - this.data.push(msg); + this.data.push( msg ); this.updateFromData(); this.build(); return msg; } - rmMsg(msg) { - let invalidatedDirections = this.directions.filter(d => d['source'] == msg || d['target'] == msg); - console.log('invalidated', invalidatedDirections); - for(let dir of invalidatedDirections) { - let i = this.data.indexOf(dir); - this.data.splice(i, 1); + rmMsg( msg ) { + let invalidatedDirections = this.directions.filter( d => d['source'] == msg || d['target'] == msg ); + console.log( 'invalidated', invalidatedDirections ); + for ( let dir of invalidatedDirections ) { + let i = this.data.indexOf( dir ); + this.data.splice( i, 1 ); } - this._rmNode(msg); + this._rmNode( msg ); } - _rmNode(node) { + _rmNode( node ) { // remove msg/direction/condition/etc - let i = this.data.indexOf(node); - this.data.splice(i, 1); + let i = this.data.indexOf( node ); + this.data.splice( i, 1 ); this.updateFromData(); this.build(); return this.data; } - addConditionForDirection(type, label, vars, direction) { - let con = this.addCondition(type, label, vars, true); - direction['conditions'].push(con['@id']); + addConditionForDirection( type, label, vars, direction ) { + let con = this.addCondition( type, label, vars, true ); + direction['conditions'].push( con['@id'] ); this.updateFromData(); this.build(); this.updateMsg(); } - addCondition(type, label, vars, skip) { + addCondition( type, label, vars, skip ) { let con = { - "@id": "c" + Date.now().toString(36), + "@id": this.language_code.substring( 0, 2 ) + "-c" + Date.now().toString( 36 ), "@type": "Condition", "type": type, "label": label, "vars": vars } - this.data.push(con); - if(skip !== true) { + this.data.push( con ); + if ( skip !== true ) { this.updateFromData(); this.build(); } return con; } - addDirection(source, target) { + addDirection( source, target ) { let dir = { - "@id": "d" + Date.now().toString(36), + "@id": this.language_code.substring( 0, 2 ) + "-d" + Date.now().toString( 36 ), "@type": "Direction", "source": source, "target": target, "conditions": [] } - this.data.push(dir); + this.data.push( dir ); this.updateFromData(); this.build(); return dir; } - rmDirection(dir) { - this._rmNode(dir); + rmDirection( dir ) { + this._rmNode( dir ); } createMsg() { @@ -502,29 +527,29 @@ class Graph{ this.build(); } - getNodeById(id) { - return this.data.filter(node => node['@id'] == id)[0]; + getNodeById( id ) { + return this.data.filter( node => node['@id'] == id )[0]; } /** * Use wrapper method, because for event handlers 'this' will refer to * the input object */ - getEditEventListener(){ + getEditEventListener() { let graph = this; - let el = function(e){ - let parts = e.srcElement.name.split('-'); + let el = function( e ) { + let parts = e.srcElement.name.split( '-' ); let id = parts[0], field = parts[1]; - console.log(this, graph); - let node = graph.getNodeById(id); - let path = field.split('.'); // use vars.test to set ['vars']['test'] = value - var res=node; - for (var i=0;i node['@type'] == 'Msg'); - this.directions = this.data.filter((node) => node['@type'] == 'Direction'); - this.conditions = this.data.filter((node) => node['@type'] == 'Condition'); - this.interruptions = this.data.filter((node) => node['@type'] == 'Interruption'); + this.messages = this.data.filter(( node ) => node['@type'] == 'Msg' ); + this.directions = this.data.filter(( node ) => node['@type'] == 'Direction' ); + this.conditions = this.data.filter(( node ) => node['@type'] == 'Condition' ); + this.interruptions = this.data.filter(( node ) => node['@type'] == 'Interruption' ); + + document.getElementById('current_lang').innerHTML = ""; + document.getElementById('current_lang').appendChild(crel('span', { + 'class': 'flag-icon ' + this.language_code + })); // save state; this.saveState(); } saveState() { - window.localStorage.setItem("lastState", this.getJsonString()); + window.localStorage.setItem( "lastState", this.getJsonString() ); } hasSavedState() { - return window.localStorage.getItem("lastState") !== null; + return window.localStorage.getItem( "lastState" ) !== null; } loadFromState() { - this.loadData(JSON.parse(window.localStorage.getItem("lastState"))); + this.loadData( JSON.parse( window.localStorage.getItem( "lastState" ) ) ); } - build(isInit) { - this.simulation = d3.forceSimulation(this.messages) - .force("link", d3.forceLink(this.directions).id(d => d['@id'])) - .force("charge", d3.forceManyBody().strength(-1000)) - .force("center", d3.forceCenter(this.width / 2, this.height / 2)) - .force("collide", d3.forceCollide(this.nodeSize*2)) + build( isInit ) { + this.simulation = d3.forceSimulation( this.messages ) + .force( "link", d3.forceLink( this.directions ).id( d => d['@id'] ) ) + .force( "charge", d3.forceManyBody().strength( -1000 ) ) + .force( "center", d3.forceCenter( this.width / 2, this.height / 2 ) ) + .force( "collide", d3.forceCollide( this.nodeSize * 2 ) ) ; // Update existing nodes let node = this.nodesG - .selectAll("g") - .data(this.messages, n => n['@id']) - ; + .selectAll( "g" ) + .data( this.messages, n => n['@id'] ) + ; // Update existing nodes let newNode = node.enter(); - let newNodeG = newNode.append("g") - .attr('id', d => d['@id']) - .call(d3.drag(this.simulation)) - .on('click', function(d){ - this.clickMsg(d); - }.bind(this)) - ; - console.log('a'); - let circle = newNodeG.append("circle") - .attr('r', this.nodeSize) - // .text(d => d.id) - ; - let text = newNodeG.append("text") - ; + let newNodeG = newNode.append( "g" ) + .attr( 'id', d => d['@id'] ) + .call( d3.drag( this.simulation ) ) + .on( 'click', function( d ) { + this.clickMsg( d ); + }.bind( this ) ) + ; + console.log( 'a' ); + let circle = newNodeG.append( "circle" ) + .attr( 'r', this.nodeSize ) + // .text(d => d.id) + ; + let textId = newNodeG.append( "text" ).attr( 'class', 'msg_id' ); + let textContent = newNodeG.append( "text" ).attr( 'class', 'msg_txt' ); - // remove - node.exit().remove(); - node = node.merge(newNodeG); + // remove + node.exit().remove(); + node = node.merge( newNodeG ); - // for all existing nodes: - node.attr('class', msg => { - let classes = []; - if( this.selectedMsg == msg) classes.push('selectedMsg'); - if( msg['start'] == true ) classes.push('startMsg'); - if(this.getDirectionsFrom(msg).length < 1) { - classes.push('endMsg'); - if(this.getDirectionsTo(msg).length < 1) classes.push('orphanedMsg'); - } + // for all existing nodes: + node.attr( 'class', msg => { + let classes = []; + if ( this.selectedMsg == msg ) classes.push( 'selectedMsg' ); + if ( msg['start'] == true ) classes.push( 'startMsg' ); + if ( this.getDirectionsFrom( msg ).length < 1 ) { + classes.push( 'endMsg' ); + if ( this.getDirectionsTo( msg ).length < 1 ) classes.push( 'orphanedMsg' ); + } - return classes.join(' '); - }) + return classes.join( ' ' ); + } ) - let link = this.linkG - .selectAll("line") - .data(this.directions) - ; - let newLink = link.enter() - .append("line") - ; - //remove - link.exit().remove(); - link = link.merge(newLink); + let link = this.linkG + .selectAll( "line" ) + .data( this.directions ) + ; + let newLink = link.enter() + .append( "line" ) + ; + + //remove + link.exit().remove(); + link = link.merge( newLink ); - link.attr('class', l => { return `link ` + (l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions"); }); + link.attr( 'class', l => { return `link ` + ( l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions" ); } ); + link.attr('id', (l) => l['@id']); - // console.log('c'); - let formatText = (t) => { - if(t.length > this.maxChars) { - return t.substr(0, this.maxChars - 3) + '...'; - } else { - return t; - } - }; + // console.log('c'); + let formatText = ( t ) => { + if ( t.length > this.maxChars ) { + return t.substr( 0, this.maxChars - 3 ) + '...'; + } else { + return t; + } + }; - node.selectAll("text").text(d => formatText(`(${d['@id']}) ${d['text']}`)); - // console.log('q'); - // // TODO: update text - // let text = newNodeG.append("text") - // // .attr('stroke', "black") - // .text(d => formatText(`(${d['@id']}) ${d['text']}`)) - // // .attr('title', d => d.label) - // ; + node.selectAll( "text.msg_id" ).text( d => d['@id'] ); + node.selectAll( "text.msg_txt" ).text( d => formatText( `${d['text']}` ) ); + // console.log('q'); + // // TODO: update text + // let text = newNodeG.append("text") + // // .attr('stroke', "black") + // .text(d => formatText(`(${d['@id']}) ${d['text']}`)) + // // .attr('title', d => d.label) + // ; - let n = this.nodesG; - this.simulation.on("tick", () => { + let n = this.nodesG; + this.simulation.on( "tick", () => { link - .each(function(d){ + .each( function( d ) { let sourceX, targetX, midX, dx, dy, angle; // This mess makes the arrows exactly perfect. // thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4 - if( d.source.x < d.target.x ){ - sourceX = d.source.x; - targetX = d.target.x; - } else if( d.target.x < d.source.x ){ - targetX = d.target.x; - sourceX = d.source.x; - } else if (d.target.isCircle) { - targetX = sourceX = d.target.x; - } else if (d.source.isCircle) { - targetX = sourceX = d.source.x; + if ( d.source.x < d.target.x ) { + sourceX = d.source.x; + targetX = d.target.x; + } else if ( d.target.x < d.source.x ) { + targetX = d.target.x; + sourceX = d.source.x; + } else if ( d.target.isCircle ) { + targetX = sourceX = d.target.x; + } else if ( d.source.isCircle ) { + targetX = sourceX = d.source.x; } else { - midX = (d.source.x + d.target.x) / 2; - if(midX > d.target.x){ - midX = d.target.x; - } else if(midX > d.source.x){ - midX = d.source.x; - } else if(midX < d.target.x){ - midX = d.target.x; - } else if(midX < d.source.x){ - midX = d.source.x; - } - targetX = sourceX = midX; + midX = ( d.source.x + d.target.x ) / 2; + if ( midX > d.target.x ) { + midX = d.target.x; + } else if ( midX > d.source.x ) { + midX = d.source.x; + } else if ( midX < d.target.x ) { + midX = d.target.x; + } else if ( midX < d.source.x ) { + midX = d.source.x; + } + targetX = sourceX = midX; } dx = targetX - sourceX; dy = d.target.y - d.source.y; - angle = Math.atan2(dx, dy); + angle = Math.atan2( dx, dy ); // Compute the line endpoint such that the arrow // is touching the edge of the node rectangle perfectly. - d.sourceX = sourceX + Math.sin(angle) * this.nodeSize; - d.targetX = targetX - Math.sin(angle) * this.nodeSize; - d.sourceY = d.source.y + Math.cos(angle) * this.nodeSize; - d.targetY = d.target.y - Math.cos(angle) * this.nodeSize; - }.bind(this)) - .attr("x1", function(d) { return d.sourceX; }) - .attr("y1", function(d) { return d.sourceY; }) - .attr("x2", function(d) { return d.targetX; }) - .attr("y2", function(d) { return d.targetY; }); + d.sourceX = sourceX + Math.sin( angle ) * this.nodeSize; + d.targetX = targetX - Math.sin( angle ) * this.nodeSize; + d.sourceY = d.source.y + Math.cos( angle ) * this.nodeSize; + d.targetY = d.target.y - Math.cos( angle ) * this.nodeSize; + }.bind( this ) ) + .attr( "x1", function( d ) { return d.sourceX; } ) + .attr( "y1", function( d ) { return d.sourceY; } ) + .attr( "x2", function( d ) { return d.targetX; } ) + .attr( "y2", function( d ) { return d.targetY; } ); - node.attr("transform", d => `translate(${d.x},${d.y})`); - // .attr("cy", d => d.y); - }); + node.attr( "transform", d => `translate(${d.x},${d.y})` ); + // .attr("cy", d => d.y); + } ); - // this.simulation.alpha(1); - // this.simulation.restart(); - if(typeof isInit != 'undefined' && isInit) { - for (let i = 0, n = Math.ceil(Math.log(this.simulation.alphaMin()) / Math.log(1 - this.simulation.alphaDecay())); i < n; ++i) { - this.simulation.tick(); - } + // this.simulation.alpha(1); + // this.simulation.restart(); + if ( typeof isInit != 'undefined' && isInit ) { + for ( let i = 0, n = Math.ceil( Math.log( this.simulation.alphaMin() ) / Math.log( 1 - this.simulation.alphaDecay() ) ); i < n; ++i ) { + this.simulation.tick(); } - return this.svg.node(); } + return this.svg.node(); + } } diff --git a/www/scss/styles.scss b/www/scss/styles.scss index 0cb1bcc..b6dbcca 100644 --- a/www/scss/styles.scss +++ b/www/scss/styles.scss @@ -1,15 +1,31 @@ +$status_width: 430px; +$status_width_open: 860px; + body{ font-family: "Noto Sans", sans-serif; margin: 0; } -.btn{ +.btn, input[type="submit"]{ display:inline-block; cursor: pointer; background: #333; padding: 5px; color: white; border-radius: 5px; + margin-right: 5px; + white-space: nowrap; + border: none; + + &:hover{ + background: #666; + } +} + +@keyframes dash-animation { + to { + stroke-dashoffset: -1000; + } } #interface{ @@ -17,6 +33,9 @@ body{ flex-direction: row; height: 100vh; width: 100vw; + + &.showStatus{ + } } #status{ @@ -33,6 +52,10 @@ body{ border: solid 1px; box-sizing: border-box; position: relative; + + &#overview{ + width: 100% / 3 * 2; + } } .hugvey{ @@ -72,11 +95,13 @@ body{ #story{ position: relative; + width: calc(100% - #{$status_width}); #controls{ position:absolute; top: 5px; left: 5px; + white-space: nowrap; } svg#graph{ width: 100%; @@ -106,15 +131,29 @@ body{ font-size: 11pt; font-family: sans-serif; fill: white; + + &.msg_id { + transform: translateY(-20px); + opacity: .5; + } + &.msg_txt{ + font-weight: bold; + } } line{ marker-end: url('#arrowHead'); stroke-width: 2px; stroke: black; - } - line.link--noconditions{ - stroke-dasharray: 5 4; - stroke: red; + + &.link--noconditions{ + stroke-dasharray: 5 4; + stroke: red; + } + &.dir-highlight{ + stroke-dasharray: 5; + animation: dash-animation 20s infinite linear; + stroke-width: 3px; + } } label::after { content: ''; @@ -148,6 +187,27 @@ body{ margin-bottom: 10px; background:lightgray; } + + .direction{ + position: relative; + h3{ + margin-top:0; + } + .btn--delete{ + position: absolute; + top: 5px; + right: 0px; + } + + .condition--add{ + h4{ + margin: 0; + } + h4 +div { + margin-top: 10px; + } + } + } } #nodes g:hover circle, @@ -179,4 +239,54 @@ body{ .condition--add{ /* text-align: center; */ } -} \ No newline at end of file +} + + +.flag-icon { + background-size: contain; + background-position: 50%; + background-repeat: no-repeat; + position: relative; + display: inline-block; + width: (4 / 3) * 1em; + line-height: 1em; + &:before { + content: '\00a0'; + } + &.flag-icon-squared { + width: 1em; + } + + &.en-GB { + background-image: url('/images/gb.svg'); + } + + &.de-DE { + background-image: url('/images/de.svg'); + } + + &.fr-FR { + background-image: url('/images/fr.svg'); + } + &.nl-NL { + background-image: url('/images/nl.svg'); + } + +} + +.divToggle{ + cursor: pointer; + &:hover{ + text-decoration: underline; + } + &.opened { + + div{ + display: block; + } + } + + div{ + display: none; + } +} + +