Refactor Interruptions to Diversions & some fixes in status display
This commit is contained in:
parent
eb28ce2ac9
commit
a1f66a6a01
18 changed files with 287 additions and 919 deletions
|
@ -6,3 +6,7 @@ voice:
|
||||||
target_rate: 16000
|
target_rate: 16000
|
||||||
port: 4444
|
port: 4444
|
||||||
input_name: null
|
input_name: null
|
||||||
|
file_address: "http://192.168.178.185:8888"
|
||||||
|
play_device: null
|
||||||
|
play_volume: 80
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,6 @@ class CentralCommand(object):
|
||||||
self.languageFiles[lang['code']] = lang['file']
|
self.languageFiles[lang['code']] = lang['file']
|
||||||
with open(lang_filename, 'r') as fp:
|
with open(lang_filename, 'r') as fp:
|
||||||
self.languages[lang['code']] = json.load(fp)
|
self.languages[lang['code']] = json.load(fp)
|
||||||
print(self.languages)
|
|
||||||
|
|
||||||
self.panopticon = Panopticon(self, self.config)
|
self.panopticon = Panopticon(self, self.config)
|
||||||
|
|
||||||
|
@ -85,9 +84,9 @@ class CentralCommand(object):
|
||||||
return status
|
return status
|
||||||
|
|
||||||
hv = self.hugveys[hv_id]
|
hv = self.hugveys[hv_id]
|
||||||
status['status'] = 'running' if hv.isRunning.is_set() else 'paused'
|
status['status'] = hv.getStatus()
|
||||||
status['language'] = hv.language_code
|
status['language'] = hv.language_code
|
||||||
status['msg'] = hv.story.currentMessage.id
|
status['msg'] = hv.story.currentMessage.id if hv.story.currentMessage else None
|
||||||
status['counts'] = hv.story.getStoryCounts()
|
status['counts'] = hv.story.getStoryCounts()
|
||||||
status['finished'] = hv.story.isFinished()
|
status['finished'] = hv.story.isFinished()
|
||||||
|
|
||||||
|
@ -168,16 +167,12 @@ class CentralCommand(object):
|
||||||
async with self.hugveyLock: # lock to prevent duplicates on creation
|
async with self.hugveyLock: # lock to prevent duplicates on creation
|
||||||
if not hugvey_id in self.hugveys:
|
if not hugvey_id in self.hugveys:
|
||||||
logger.info(f'Instantiate hugvey #{hugvey_id}')
|
logger.info(f'Instantiate hugvey #{hugvey_id}')
|
||||||
print('a')
|
|
||||||
h = HugveyState(hugvey_id, self)
|
h = HugveyState(hugvey_id, self)
|
||||||
print('a')
|
|
||||||
h.config(msg['host'], msg['ip'])
|
h.config(msg['host'], msg['ip'])
|
||||||
print('b')
|
|
||||||
self.hugveys[hugvey_id] = h
|
self.hugveys[hugvey_id] = h
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=h.start, name=f"hugvey#{hugvey_id}")
|
target=h.start, name=f"hugvey#{hugvey_id}")
|
||||||
thread.start()
|
thread.start()
|
||||||
print('c')
|
|
||||||
else:
|
else:
|
||||||
logger.info(f'Reconfigure hugvey #{hugvey_id}')
|
logger.info(f'Reconfigure hugvey #{hugvey_id}')
|
||||||
# (re)configure exisitng hugveys
|
# (re)configure exisitng hugveys
|
||||||
|
@ -210,7 +205,8 @@ class CentralCommand(object):
|
||||||
logger.debug("Message contains: {}".format(msg))
|
logger.debug("Message contains: {}".format(msg))
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
await self.hugveys[hugvey_id].eventQueue.put(msg)
|
self.hugveys[hugvey_id].queueEvent(msg)
|
||||||
|
# await self.hugveys[hugvey_id].eventQueue.put(msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(f"Exception while running event loop:")
|
logger.critical(f"Exception while running event loop:")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
@ -242,6 +238,10 @@ class HugveyState(object):
|
||||||
"""Represents the state of a Hugvey client on the server.
|
"""Represents the state of a Hugvey client on the server.
|
||||||
Manages server connections & voice parsing etc.
|
Manages server connections & voice parsing etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
STATE_PAUSE = "paused"
|
||||||
|
STATE_GONE = "gone"
|
||||||
|
STATE_RUNNING = "running"
|
||||||
|
|
||||||
def __init__(self, id: int, command: CentralCommand):
|
def __init__(self, id: int, command: CentralCommand):
|
||||||
|
|
||||||
|
@ -255,7 +255,16 @@ class HugveyState(object):
|
||||||
self.language_code = 'en-GB'
|
self.language_code = 'en-GB'
|
||||||
self.story = Story(self)
|
self.story = Story(self)
|
||||||
self.story.setStoryData(self.command.languages[self.language_code])
|
self.story.setStoryData(self.command.languages[self.language_code])
|
||||||
|
self.streamer = None
|
||||||
|
self.status = self.STATE_PAUSE
|
||||||
|
self.google = None
|
||||||
|
self.notShuttingDown = True # TODO: allow shutdown of object
|
||||||
|
|
||||||
|
def getStatus(self):
|
||||||
|
if self.story.isFinished():
|
||||||
|
return "finished"
|
||||||
|
return self.status
|
||||||
|
|
||||||
def config(self, hostname, ip):
|
def config(self, hostname, ip):
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
|
@ -279,13 +288,17 @@ class HugveyState(object):
|
||||||
"""
|
"""
|
||||||
Start the tasks
|
Start the tasks
|
||||||
"""
|
"""
|
||||||
|
self.isRunning.set()
|
||||||
|
self.status = self.STATE_RUNNING
|
||||||
|
|
||||||
tasks = asyncio.gather(
|
tasks = asyncio.gather(
|
||||||
self.catchException(self.processAudio()),
|
self.catchException(self.processAudio()),
|
||||||
self.catchException(self.handleEvents()),
|
self.catchException(self.handleEvents()),
|
||||||
self.catchException(self.playStory()),
|
self.catchException(self.playStory()),
|
||||||
loop=self.loop)
|
loop=self.loop)
|
||||||
|
# self.pause()
|
||||||
self.loop.run_until_complete(tasks)
|
self.loop.run_until_complete(tasks)
|
||||||
self.isRunning.set()
|
|
||||||
|
|
||||||
async def catchException(self, awaitable):
|
async def catchException(self, awaitable):
|
||||||
try:
|
try:
|
||||||
|
@ -336,46 +349,73 @@ class HugveyState(object):
|
||||||
self.story.setStoryData(self.command.languages[language_code])
|
self.story.setStoryData(self.command.languages[language_code])
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
self.google.pause()
|
self.logger.info('Pause')
|
||||||
|
if self.google:
|
||||||
|
self.google.pause()
|
||||||
self.story.pause()
|
self.story.pause()
|
||||||
self.isRunning.clear()
|
self.isRunning.clear()
|
||||||
|
self.status = self.STATE_PAUSE
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
self.google.resume()
|
self.logger.info('Resume')
|
||||||
|
if self.google:
|
||||||
|
self.google.resume()
|
||||||
self.story.resume()
|
self.story.resume()
|
||||||
self.isRunning.set()
|
self.isRunning.set()
|
||||||
|
self.status = self.STATE_RUNNING
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
|
self.logger.info('Restart')
|
||||||
self.story.reset()
|
self.story.reset()
|
||||||
self.resume()
|
self.resume()
|
||||||
self.isRunning.set()
|
self.isRunning.set()
|
||||||
|
|
||||||
|
def gone(self):
|
||||||
|
'''Status to 'gone' as in, shutdown/crashed/whatever
|
||||||
|
'''
|
||||||
|
self.pause()
|
||||||
|
self.logger.info('Gone')
|
||||||
|
self.status = self.STATE_GONE
|
||||||
|
|
||||||
|
|
||||||
async def playStory(self):
|
async def playStory(self):
|
||||||
await self.story.start()
|
await self.story.start()
|
||||||
|
|
||||||
|
def getStreamer(self):
|
||||||
|
if not self.streamer:
|
||||||
|
self.streamer = AudioStreamer(
|
||||||
|
self.command.config['voice']['chunk'],
|
||||||
|
self.ip,
|
||||||
|
int(self.command.config['voice']['port']))
|
||||||
|
|
||||||
|
if self.command.debug:
|
||||||
|
self.logger.warn("Debug on: Connecting Audio player")
|
||||||
|
self.player = Player(
|
||||||
|
self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate'])
|
||||||
|
self.streamer.addConsumer(self.player)
|
||||||
|
|
||||||
|
self.logger.info("Start Speech")
|
||||||
|
self.google = GoogleVoiceClient(
|
||||||
|
hugvey=self,
|
||||||
|
src_rate=self.command.config['voice']['src_rate'],
|
||||||
|
credential_file=self.command.config['voice']['google_credentials'],
|
||||||
|
language_code=self.language_code
|
||||||
|
)
|
||||||
|
self.streamer.addConsumer(self.google)
|
||||||
|
return self.streamer
|
||||||
|
|
||||||
async def processAudio(self):
|
async def processAudio(self):
|
||||||
'''
|
'''
|
||||||
Start the audio streamer service
|
Start the audio streamer service
|
||||||
'''
|
'''
|
||||||
|
|
||||||
self.logger.info("Start audio stream")
|
self.logger.info("Start audio stream")
|
||||||
streamer = AudioStreamer(
|
|
||||||
self.command.config['voice']['chunk'],
|
|
||||||
self.ip,
|
|
||||||
int(self.command.config['voice']['port']))
|
|
||||||
|
|
||||||
if self.command.debug:
|
while self.notShuttingDown:
|
||||||
self.logger.warn("Debug on: Connecting Audio player")
|
# self.isRunning.wait()
|
||||||
self.player = Player(
|
|
||||||
self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate'])
|
self.logger.info("Start audio stream")
|
||||||
streamer.addConsumer(self.player)
|
await self.getStreamer().run()
|
||||||
|
self.logger.warn("stream has left the building")
|
||||||
self.logger.info("Start Speech")
|
# if we end up here, the streamer finished, probably meaning hte hugvey shutdown
|
||||||
self.google = GoogleVoiceClient(
|
self.gone()
|
||||||
hugvey=self,
|
|
||||||
src_rate=self.command.config['voice']['src_rate'],
|
|
||||||
credential_file=self.command.config['voice']['google_credentials'],
|
|
||||||
language_code=self.language_code
|
|
||||||
)
|
|
||||||
streamer.addConsumer(self.google)
|
|
||||||
|
|
||||||
await streamer.run()
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import yaml
|
import yaml
|
||||||
|
import alsaaudio
|
||||||
import zmq
|
import zmq
|
||||||
from zmq.asyncio import Context
|
from zmq.asyncio import Context
|
||||||
|
|
||||||
|
@ -112,13 +113,14 @@ class VoiceServer(object):
|
||||||
r = await future
|
r = await future
|
||||||
|
|
||||||
class CommandHandler(object):
|
class CommandHandler(object):
|
||||||
def __init__(self, hugvey_id, cmd_address = "tcp://127.0.0.1:5555", publish_address = "tcp://0.0.0.0:5555"):
|
def __init__(self, hugvey_id, cmd_address, publish_address, file_address):
|
||||||
self.eventQueue = []
|
self.eventQueue = []
|
||||||
self.ctx = Context.instance()
|
self.ctx = Context.instance()
|
||||||
self.hugvey_id = hugvey_id
|
self.hugvey_id = hugvey_id
|
||||||
self.cmd_address = cmd_address
|
self.cmd_address = cmd_address
|
||||||
self.publish_address = publish_address
|
self.publish_address = publish_address
|
||||||
self.playPopen = None
|
self.playPopen = None
|
||||||
|
self.file_address = file_address
|
||||||
# self.showMyself() # queue message for connection request
|
# self.showMyself() # queue message for connection request
|
||||||
|
|
||||||
def handle(self, cmd):
|
def handle(self, cmd):
|
||||||
|
@ -132,20 +134,34 @@ class CommandHandler(object):
|
||||||
if cmd['action'] == 'show_yourself':
|
if cmd['action'] == 'show_yourself':
|
||||||
self.showMyself()
|
self.showMyself()
|
||||||
if cmd['action'] == 'play':
|
if cmd['action'] == 'play':
|
||||||
self.cmdPlay(cmd['id'], cmd['msg'])
|
self.cmdPlay(cmd)
|
||||||
|
|
||||||
|
|
||||||
def cmdPlay(self, msgId, msgText, pitch=50):
|
def cmdPlay(self, cmd):
|
||||||
logger.info("Play: {}".format(msgText))
|
msgId= cmd['id']
|
||||||
self.playPopen = subprocess.Popen(['espeak', '-p','{0}'.format(pitch), msgText], stdout=subprocess.PIPE)
|
pitch = cmd['pitch'] if 'pitch' in cmd else 50
|
||||||
returnCode = self.playPopen.wait()
|
file = cmd['file'] if 'file' in cmd else None
|
||||||
self.playPopen = None
|
text = cmd['msg'] if 'msg' in cmd else None
|
||||||
|
|
||||||
if returnCode:
|
if file is None and text is None:
|
||||||
logger.warn("Had returncode on play: {}".format(returnCode))
|
logger.critical("No file nor text given: {}".format(cmd))
|
||||||
else:
|
else:
|
||||||
logger.debug("Finished playback. Return code: {}".format(returnCode))
|
if file is not None:
|
||||||
|
logger.info("Play: {}".format(file))
|
||||||
|
file = self.file_address + "/" + file
|
||||||
|
self.playPopen = subprocess.Popen(['play', file], stdout=subprocess.PIPE)
|
||||||
|
returnCode = self.playPopen.wait()
|
||||||
|
self.playPopen = None
|
||||||
|
else:
|
||||||
|
logger.info("Speak: {}".format(text))
|
||||||
|
self.playPopen = subprocess.Popen(['espeak', '-p','{0}'.format(pitch), text], stdout=subprocess.PIPE)
|
||||||
|
returnCode = self.playPopen.wait()
|
||||||
|
self.playPopen = None
|
||||||
|
|
||||||
|
if returnCode:
|
||||||
|
logger.warn("Had returncode on play: {}".format(returnCode))
|
||||||
|
else:
|
||||||
|
logger.debug("Finished playback. Return code: {}".format(returnCode))
|
||||||
|
|
||||||
self.sendMessage({
|
self.sendMessage({
|
||||||
'event': 'playbackFinish',
|
'event': 'playbackFinish',
|
||||||
|
@ -237,6 +253,9 @@ class Hugvey(object):
|
||||||
def start(self):
|
def start(self):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
if self.config['voice']['play_device']:
|
||||||
|
alsaaudio.Mixer(self.config['voice']['play_device']).setvolume(self.config['voice']['play_volume'])
|
||||||
|
|
||||||
self.voice_server = VoiceServer(
|
self.voice_server = VoiceServer(
|
||||||
loop = loop,
|
loop = loop,
|
||||||
voice_port = int(self.config['voice']['port']),
|
voice_port = int(self.config['voice']['port']),
|
||||||
|
@ -248,6 +267,7 @@ class Hugvey(object):
|
||||||
hugvey_id = self.id,
|
hugvey_id = self.id,
|
||||||
cmd_address = self.config['events']['cmd_address'],
|
cmd_address = self.config['events']['cmd_address'],
|
||||||
publish_address = self.config['events']['publish_address'],
|
publish_address = self.config['events']['publish_address'],
|
||||||
|
file_address = self.config['voice']['file_address']
|
||||||
)
|
)
|
||||||
logger.info('start')
|
logger.info('start')
|
||||||
# self.voice_server.asyncStart(loop)
|
# self.voice_server.asyncStart(loop)
|
||||||
|
|
|
@ -56,6 +56,7 @@ def getWebSocketHandler(central_command):
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.send({'alert': 'Invalid request: {}'.format(e)})
|
self.send({'alert': 'Invalid request: {}'.format(e)})
|
||||||
|
logger.exception(e)
|
||||||
|
|
||||||
def send(self, message):
|
def send(self, message):
|
||||||
j = json.dumps(message)
|
j = json.dumps(message)
|
||||||
|
@ -80,13 +81,13 @@ def getWebSocketHandler(central_command):
|
||||||
self.send(msg)
|
self.send(msg)
|
||||||
|
|
||||||
def msgResume(self, hv_id):
|
def msgResume(self, hv_id):
|
||||||
central_command.hugveys[hv_id].eventQueue.put({'event': 'resume'})
|
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'resume'})
|
||||||
|
|
||||||
def msgPause(self, hv_id):
|
def msgPause(self, hv_id):
|
||||||
central_command.hugveys[hv_id].eventQueue.put({'event': 'pause'})
|
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'pause'})
|
||||||
|
|
||||||
def msgRestart(self, hv_id):
|
def msgRestart(self, hv_id):
|
||||||
central_command.hugveys[hv_id].eventQueue.put({'event': 'restart'})
|
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'restart'})
|
||||||
|
|
||||||
return WebSocketHandler
|
return WebSocketHandler
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,14 @@ class Message(object):
|
||||||
self.text = text
|
self.text = text
|
||||||
self.isStart = False
|
self.isStart = False
|
||||||
self.reply = None
|
self.reply = None
|
||||||
|
self.audioFile= None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initFromJson(message, data, story):
|
def initFromJson(message, data, story):
|
||||||
msg = message(data['@id'], data['text'])
|
msg = message(data['@id'], data['text'])
|
||||||
msg.isStart = data['start'] if 'start' in data else False
|
msg.isStart = data['start'] if 'start' in data else False
|
||||||
|
if 'audio' in data:
|
||||||
|
msg.audioFile = data['audio']['file']
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def setReply(self, text):
|
def setReply(self, text):
|
||||||
|
@ -123,9 +126,9 @@ class Direction(object):
|
||||||
return direction
|
return direction
|
||||||
|
|
||||||
|
|
||||||
class Interruption(object):
|
class Diversion(object):
|
||||||
"""
|
"""
|
||||||
An Interruption. Used to catch events outside of story flow.
|
An Diversion. Used to catch events outside of story flow.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, id):
|
def __init__(self, id):
|
||||||
|
@ -136,20 +139,20 @@ class Interruption(object):
|
||||||
self.conditions.append(condition)
|
self.conditions.append(condition)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initFromJson(interruptionClass, data, story):
|
def initFromJson(diversionClass, data, story):
|
||||||
interrupt = interruptionClass(data['@id'])
|
diversion = diversionClass(data['@id'])
|
||||||
if 'conditions' in data:
|
if 'conditions' in data:
|
||||||
for conditionId in data['conditions']:
|
for conditionId in data['conditions']:
|
||||||
c = story.get(conditionId)
|
c = story.get(conditionId)
|
||||||
interrupt.addCondition(c)
|
diversion.addCondition(c)
|
||||||
return interrupt
|
return diversion
|
||||||
|
|
||||||
|
|
||||||
storyClasses = {
|
storyClasses = {
|
||||||
'Msg': Message,
|
'Msg': Message,
|
||||||
'Direction': Direction,
|
'Direction': Direction,
|
||||||
'Condition': Condition,
|
'Condition': Condition,
|
||||||
'Interruption': Interruption,
|
'Diversion': Diversion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -231,7 +234,7 @@ class Story(object):
|
||||||
# return counts
|
# return counts
|
||||||
return {
|
return {
|
||||||
'messages': len([e for e in self.log if isinstance(e, Message)]),
|
'messages': len([e for e in self.log if isinstance(e, Message)]),
|
||||||
'interruptions': len([e for e in self.log if isinstance(e, Interruption)])
|
'diversions': len([e for e in self.log if isinstance(e, Diversion)])
|
||||||
}
|
}
|
||||||
|
|
||||||
def setStoryData(self, story_data):
|
def setStoryData(self, story_data):
|
||||||
|
@ -244,7 +247,7 @@ class Story(object):
|
||||||
currentId = self.currentMessage.id if self.currentMessage else None
|
currentId = self.currentMessage.id if self.currentMessage else None
|
||||||
|
|
||||||
self.elements = {}
|
self.elements = {}
|
||||||
self.interruptions = []
|
self.diversions = []
|
||||||
self.directionsPerMsg = {}
|
self.directionsPerMsg = {}
|
||||||
self.startMessage = None # The entrypoint to the graph
|
self.startMessage = None # The entrypoint to the graph
|
||||||
self.reset()
|
self.reset()
|
||||||
|
@ -291,8 +294,8 @@ class Story(object):
|
||||||
|
|
||||||
self.elements[obj.id] = obj
|
self.elements[obj.id] = obj
|
||||||
|
|
||||||
if type(obj) == Interruption:
|
if type(obj) == Diversion:
|
||||||
self.interruptions.append(obj)
|
self.diversions.append(obj)
|
||||||
|
|
||||||
if type(obj) == Direction:
|
if type(obj) == Direction:
|
||||||
if obj.msgFrom.id not in self.directionsPerMsg:
|
if obj.msgFrom.id not in self.directionsPerMsg:
|
||||||
|
@ -399,11 +402,18 @@ class Story(object):
|
||||||
message.id, message.text))
|
message.id, message.text))
|
||||||
self.log.append(message)
|
self.log.append(message)
|
||||||
# TODO: prep events & timer etc.
|
# TODO: prep events & timer etc.
|
||||||
self.hugvey.sendCommand({
|
if message.audioFile:
|
||||||
'action': 'play',
|
self.hugvey.sendCommand({
|
||||||
'msg': message.text,
|
'action': 'play',
|
||||||
'id': message.id,
|
'file': message.audioFile,
|
||||||
})
|
'id': message.id,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.hugvey.sendCommand({
|
||||||
|
'action': 'play',
|
||||||
|
'msg': message.text,
|
||||||
|
'id': message.id,
|
||||||
|
})
|
||||||
|
|
||||||
logger.debug("Pending directions: ")
|
logger.debug("Pending directions: ")
|
||||||
|
|
||||||
|
@ -426,8 +436,8 @@ class Story(object):
|
||||||
await self._renderer()
|
await self._renderer()
|
||||||
|
|
||||||
def isFinished(self):
|
def isFinished(self):
|
||||||
if hasattr(self, 'finish_time'):
|
if hasattr(self, 'finish_time') and self.finish_time:
|
||||||
return self.finish_time
|
return time.time() - self.finish_time
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ class AudioStreamer(object):
|
||||||
address = "tcp://{}:{}".format(self.address, self.port)
|
address = "tcp://{}:{}".format(self.address, self.port)
|
||||||
self.ctx = Context.instance()
|
self.ctx = Context.instance()
|
||||||
self.socket = self.ctx.socket(zmq.SUB)
|
self.socket = self.ctx.socket(zmq.SUB)
|
||||||
|
self.socket.setsockopt(zmq.RCVTIMEO, 4000) # timeout: 8 sec
|
||||||
self.socket.subscribe('')
|
self.socket.subscribe('')
|
||||||
# self.socket.setsockopt(zmq.CONFLATE, 1)
|
# self.socket.setsockopt(zmq.CONFLATE, 1)
|
||||||
self.socket.connect(address)
|
self.socket.connect(address)
|
||||||
|
@ -35,13 +36,16 @@ class AudioStreamer(object):
|
||||||
logger.info("Attempt connection on {}:{}".format(self.address, self.port))
|
logger.info("Attempt connection on {}:{}".format(self.address, self.port))
|
||||||
# s.connect((self.address, self.port))
|
# s.connect((self.address, self.port))
|
||||||
#
|
#
|
||||||
while self.isRunning:
|
try:
|
||||||
data = await self.socket.recv()
|
while self.isRunning:
|
||||||
# logger.debug('chunk received')
|
data = await self.socket.recv()
|
||||||
self.process(data)
|
# logger.debug('chunk received')
|
||||||
|
self.process(data)
|
||||||
logger.info("Close socket on {}:{}".format(self.address, self.port))
|
except zmq.error.Again as timeout_e:
|
||||||
self.socket.close()
|
logger.warn("Timeout of audiostream. Hugvey shutdown?")
|
||||||
|
finally:
|
||||||
|
logger.info("Close socket on {}:{}".format(self.address, self.port))
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.isRunning = False
|
self.isRunning = False
|
||||||
|
|
|
@ -2,3 +2,4 @@ pyzmq
|
||||||
pyaudio
|
pyaudio
|
||||||
coloredlogs
|
coloredlogs
|
||||||
pyyaml
|
pyyaml
|
||||||
|
pyalsaaudio
|
||||||
|
|
|
@ -19,6 +19,10 @@ body {
|
||||||
to {
|
to {
|
||||||
stroke-dashoffset: -1000; } }
|
stroke-dashoffset: -1000; } }
|
||||||
|
|
||||||
|
img.icon {
|
||||||
|
height: .9em;
|
||||||
|
width: .9em; }
|
||||||
|
|
||||||
#interface {
|
#interface {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -40,6 +44,14 @@ body {
|
||||||
position: relative; }
|
position: relative; }
|
||||||
#status > div#overview {
|
#status > div#overview {
|
||||||
width: 66.66667%; }
|
width: 66.66667%; }
|
||||||
|
#status .counts dd, #status .counts dt {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margiN: 0; }
|
||||||
|
#status .counts dd:hover, #status .counts dt:hover {
|
||||||
|
width: auto; }
|
||||||
#status .hugvey {
|
#status .hugvey {
|
||||||
background-image: linear-gradient(to top, #587457, #35a589);
|
background-image: linear-gradient(to top, #587457, #35a589);
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -57,6 +69,27 @@ body {
|
||||||
#status .hugvey.hugvey--off {
|
#status .hugvey.hugvey--off {
|
||||||
background-image: linear-gradient(to top, #575d74, #3572a5); }
|
background-image: linear-gradient(to top, #575d74, #3572a5); }
|
||||||
#status .hugvey.hugvey--off::after {
|
#status .hugvey.hugvey--off::after {
|
||||||
|
content: 'off';
|
||||||
|
font-style: italic;
|
||||||
|
color: gray;
|
||||||
|
text-align: center; }
|
||||||
|
#status .hugvey.hugvey--gone {
|
||||||
|
background-image: linear-gradient(to top, orange, #ce5c00); }
|
||||||
|
#status .hugvey.hugvey--gone::after {
|
||||||
|
content: 'disconnected';
|
||||||
|
font-style: italic;
|
||||||
|
color: gray;
|
||||||
|
text-align: center; }
|
||||||
|
#status .hugvey.hugvey--paused {
|
||||||
|
background-image: linear-gradient(to top, #888a85, #555753); }
|
||||||
|
#status .hugvey.hugvey--paused::after {
|
||||||
|
content: 'disconnected';
|
||||||
|
font-style: italic;
|
||||||
|
color: gray;
|
||||||
|
text-align: center; }
|
||||||
|
#status .hugvey.hugvey--finished {
|
||||||
|
background-image: linear-gradient(to top, #888a85, #35a589); }
|
||||||
|
#status .hugvey.hugvey--finished::after {
|
||||||
content: 'disconnected';
|
content: 'disconnected';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: gray;
|
color: gray;
|
||||||
|
|
1
www/images/icon-diversions.svg
Normal file
1
www/images/icon-diversions.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M384 320H256c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32zM192 32c0-17.67-14.33-32-32-32H32C14.33 0 0 14.33 0 32v128c0 17.67 14.33 32 32 32h95.72l73.16 128.04C211.98 300.98 232.4 288 256 288h.28L192 175.51V128h224V64H192V32zM608 0H480c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32V32c0-17.67-14.33-32-32-32z"/></svg>
|
After Width: | Height: | Size: 479 B |
1
www/images/icon-finished.svg
Normal file
1
www/images/icon-finished.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M466.515 66.928C487.731 57.074 512 72.551 512 95.944v243.1c0 10.526-5.161 20.407-13.843 26.358-35.837 24.564-74.335 40.858-122.505 40.858-67.373 0-111.63-34.783-165.217-34.783-50.853 0-86.124 10.058-114.435 22.122V488c0 13.255-10.745 24-24 24H56c-13.255 0-24-10.745-24-24V101.945C17.497 91.825 8 75.026 8 56 8 24.296 34.345-1.254 66.338.048c28.468 1.158 51.779 23.968 53.551 52.404.52 8.342-.81 16.31-3.586 23.562C137.039 68.384 159.393 64 184.348 64c67.373 0 111.63 34.783 165.217 34.783 40.496 0 82.612-15.906 116.95-31.855zM96 134.63v70.49c29-10.67 51.18-17.83 73.6-20.91v-71.57c-23.5 2.17-40.44 9.79-73.6 21.99zm220.8 9.19c-26.417-4.672-49.886-13.979-73.6-21.34v67.42c24.175 6.706 47.566 16.444 73.6 22.31v-68.39zm-147.2 40.39v70.04c32.796-2.978 53.91-.635 73.6 3.8V189.9c-25.247-7.035-46.581-9.423-73.6-5.69zm73.6 142.23c26.338 4.652 49.732 13.927 73.6 21.34v-67.41c-24.277-6.746-47.54-16.45-73.6-22.32v68.39zM96 342.1c23.62-8.39 47.79-13.84 73.6-16.56v-71.29c-26.11 2.35-47.36 8.04-73.6 17.36v70.49zm368-221.6c-21.3 8.85-46.59 17.64-73.6 22.47v71.91c27.31-4.36 50.03-14.1 73.6-23.89V120.5zm0 209.96v-70.49c-22.19 14.2-48.78 22.61-73.6 26.02v71.58c25.07-2.38 48.49-11.04 73.6-27.11zM316.8 212.21v68.16c25.664 7.134 46.616 9.342 73.6 5.62v-71.11c-25.999 4.187-49.943 2.676-73.6-2.67z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
www/images/icon-interruptions.svg
Normal file
1
www/images/icon-interruptions.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M532 386.2c27.5-27.1 44-61.1 44-98.2 0-80-76.5-146.1-176.2-157.9C368.3 72.5 294.3 32 208 32 93.1 32 0 103.6 0 192c0 37 16.5 71 44 98.2-15.3 30.7-37.3 54.5-37.7 54.9-6.3 6.7-8.1 16.5-4.4 25 3.6 8.5 12 14 21.2 14 53.5 0 96.7-20.2 125.2-38.8 9.2 2.1 18.7 3.7 28.4 4.9C208.1 407.6 281.8 448 368 448c20.8 0 40.8-2.4 59.8-6.8C456.3 459.7 499.4 480 553 480c9.2 0 17.5-5.5 21.2-14 3.6-8.5 1.9-18.3-4.4-25-.4-.3-22.5-24.1-37.8-54.8zm-392.8-92.3L122.1 305c-14.1 9.1-28.5 16.3-43.1 21.4 2.7-4.7 5.4-9.7 8-14.8l15.5-31.1L77.7 256C64.2 242.6 48 220.7 48 192c0-60.7 73.3-112 160-112s160 51.3 160 112-73.3 112-160 112c-16.5 0-33-1.9-49-5.6l-19.8-4.5zM498.3 352l-24.7 24.4 15.5 31.1c2.6 5.1 5.3 10.1 8 14.8-14.6-5.1-29-12.3-43.1-21.4l-17.1-11.1-19.9 4.6c-16 3.7-32.5 5.6-49 5.6-54 0-102.2-20.1-131.3-49.7C338 339.5 416 272.9 416 192c0-3.4-.4-6.7-.7-10C479.7 196.5 528 238.8 528 288c0 28.7-16.2 50.6-29.7 64z"/></svg>
|
After Width: | Height: | Size: 971 B |
1
www/images/icon-laughs.svg
Normal file
1
www/images/icon-laughs.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm141.4 389.4c-37.8 37.8-88 58.6-141.4 58.6s-103.6-20.8-141.4-58.6S48 309.4 48 256s20.8-103.6 58.6-141.4S194.6 56 248 56s103.6 20.8 141.4 58.6S448 202.6 448 256s-20.8 103.6-58.6 141.4zM328 224c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm-160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm194.4 64H133.6c-8.2 0-14.5 7-13.5 15 7.5 59.2 58.9 105 121.1 105h13.6c62.2 0 113.6-45.8 121.1-105 1-8-5.3-15-13.5-15z"/></svg>
|
After Width: | Height: | Size: 595 B |
1
www/images/icon-messages.svg
Normal file
1
www/images/icon-messages.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M144 208c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm112 0c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm112 0c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zM256 32C114.6 32 0 125.1 0 240c0 47.6 19.9 91.2 52.9 126.3C38 405.7 7 439.1 6.5 439.5c-6.6 7-8.4 17.2-4.6 26S14.4 480 24 480c61.5 0 110-25.7 139.1-46.3C192 442.8 223.2 448 256 448c141.4 0 256-93.1 256-208S397.4 32 256 32zm0 368c-26.7 0-53.1-4.1-78.4-12.1l-22.7-7.2-19.5 13.8c-14.3 10.1-33.9 21.4-57.5 29 7.3-12.1 14.4-25.7 19.9-40.2l10.6-28.1-20.6-21.8C69.7 314.1 48 282.2 48 240c0-88.2 93.3-160 208-160s208 71.8 208 160-93.3 160-208 160z"/></svg>
|
After Width: | Height: | Size: 733 B |
1
www/images/icon-timeouts.svg
Normal file
1
www/images/icon-timeouts.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-280c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32z"/></svg>
|
After Width: | Height: | Size: 376 B |
|
@ -7,6 +7,7 @@
|
||||||
<script src="/js/moment.min.js"></script>
|
<script src="/js/moment.min.js"></script>
|
||||||
<script src="/js/d3.v5.min.js"></script>
|
<script src="/js/d3.v5.min.js"></script>
|
||||||
<script src="/js/crel.min.js"></script>
|
<script src="/js/crel.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="/css/styles.css"></link>
|
<link rel="stylesheet" href="/css/styles.css"></link>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
@ -18,28 +19,33 @@
|
||||||
<dt>Uptime</dt>
|
<dt>Uptime</dt>
|
||||||
<dd>{{uptime}}</dd>
|
<dd>{{uptime}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<ul id='languages'>
|
<ul id='languages'>
|
||||||
<li v-for="lang in languages" :title="lang.file"
|
<li v-for="lang in languages" :title="lang.file"
|
||||||
class="btn lang--btn" @click="loadNarrative(lang.code, lang.file)"><span :class="['flag-icon', lang.code]"></span> {{lang.code}}</li>
|
:id="'lang-' + lang.code" class="btn lang--btn"
|
||||||
|
@click="loadNarrative(lang.code, lang.file)"><span
|
||||||
|
:class="['flag-icon', lang.code]"></span> {{lang.code}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='hugvey' v-for="hv in hugveys"
|
<div class='hugvey' v-for="hv in hugveys"
|
||||||
:class="[{'hugvey--off': hv.status == 'off'},{'hugvey--on': hv.status != 'off'},{'hugvey--paused': hv.status == 'paused'},{'hugvey--running': hv.status == 'running'}]">
|
:class="[{'hugvey--on': hv.status != 'off'},'hugvey--' + hv.status]">
|
||||||
<h1>
|
<h1>
|
||||||
{{ hv.id }}
|
{{ hv.id }}
|
||||||
<!-- / {{ hv.status }} -->
|
|
||||||
</h1>
|
</h1>
|
||||||
<div v-if="hv.status != 'off'">
|
<div class='status'>{{ hv.status }}</div>
|
||||||
{{ hv.language }} / {{ hv.msg }}
|
<div v-if="hv.status != 'off' && hv.status != 'gone'">
|
||||||
<div v-if="hv.finished != false">Finished: {{time_passed(hv,
|
{{ hv.language }} / {{ hv.msg }}
|
||||||
|
<div v-if="hv.finished != false"><img class='icon' :src="'/images/icon-finished.svg'" title="Finished"> {{timer(hv,
|
||||||
'finished')}}</div>
|
'finished')}}</div>
|
||||||
<div v-for="c, key in hv.counts">
|
<div class='counts'>
|
||||||
<dt>{{key}}</dt>
|
<div class='count' v-for="c, key in hv.counts">
|
||||||
<dd>{{c}}</dd>
|
<img class='icon' :src="'/images/icon-' + key + '.svg'" :title="key">
|
||||||
|
{{c}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hv.status != 'running'" @click="panopticon.pause(hv.id)">Pause</div>
|
<div class='btn' v-if="hv.status == 'running'" @click="pause(hv.id)">Pause</div>
|
||||||
<div v-if="hv.status != 'paused'" @click="panopticon.resume(hv.id)">Resume</div>
|
<div class='btn' v-if="hv.status == 'paused'" @click="resume(hv.id)">Resume</div>
|
||||||
|
<div class='btn' v-if="hv.status == 'finished'" @click="restart(hv.id)">Restart</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@ var panopticon;
|
||||||
class Panopticon {
|
class Panopticon {
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log( "Init panopticon" );
|
console.log( "Init panopticon" );
|
||||||
|
this.languages = []
|
||||||
this.hugveys = new Vue( {
|
this.hugveys = new Vue( {
|
||||||
el: "#status",
|
el: "#status",
|
||||||
data: {
|
data: {
|
||||||
|
@ -15,15 +16,27 @@ class Panopticon {
|
||||||
console.log( "property!", Date( hugvey[property] * 1000 ) );
|
console.log( "property!", Date( hugvey[property] * 1000 ) );
|
||||||
return moment( Date( hugvey[property] * 1000 ) ).fromNow();
|
return moment( Date( hugvey[property] * 1000 ) ).fromNow();
|
||||||
},
|
},
|
||||||
|
timer: function(hugvey, property) {
|
||||||
|
return panopticon.stringToHHMMSS( hugvey[property] );
|
||||||
|
},
|
||||||
loadNarrative: function( code, file ) {
|
loadNarrative: function( code, file ) {
|
||||||
return panopticon.loadNarrative( code, file );
|
return panopticon.loadNarrative( code, file );
|
||||||
|
},
|
||||||
|
pause: function(hv_id) {
|
||||||
|
return panopticon.pause(hv_id);
|
||||||
|
},
|
||||||
|
resume: function(hv_id) {
|
||||||
|
return panopticon.resume(hv_id);
|
||||||
|
},
|
||||||
|
restart: function(hv_id) {
|
||||||
|
return panopticon.restart(hv_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } );
|
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
|
||||||
this.graph = new Graph();
|
this.graph = new Graph();
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,8 +61,10 @@ class Panopticon {
|
||||||
switch ( msg['action'] ) {
|
switch ( msg['action'] ) {
|
||||||
|
|
||||||
case 'status':
|
case 'status':
|
||||||
|
console.debug(msg);
|
||||||
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
|
this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] );
|
||||||
this.hugveys.languages = msg['languages'];
|
this.hugveys.languages = msg['languages'];
|
||||||
|
this.languages = msg['languages'];
|
||||||
this.hugveys.hugveys = msg['hugveys'];
|
this.hugveys.hugveys = msg['hugveys'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -85,8 +100,14 @@ class Panopticon {
|
||||||
return hours + ':' + minutes + ':' + seconds;
|
return hours + ':' + minutes + ':' + seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadNarrative( code, file ) {
|
loadNarrative( code, file ) {
|
||||||
|
if(typeof file == 'undefined') {
|
||||||
|
for (let lang of this.languages) {
|
||||||
|
if (lang['code'] == code) {
|
||||||
|
file = lang['file'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let req = new XMLHttpRequest();
|
let req = new XMLHttpRequest();
|
||||||
let graph = this.graph;
|
let graph = this.graph;
|
||||||
req.addEventListener( "load", function( e ) {
|
req.addEventListener( "load", function( e ) {
|
||||||
|
@ -128,7 +149,7 @@ class Graph {
|
||||||
this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
this.directions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
this.directions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
this.conditions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
this.conditions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
this.interruptions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
this.diversions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
|
|
||||||
let graph = this;
|
let graph = this;
|
||||||
this.controlDown = false;
|
this.controlDown = false;
|
||||||
|
@ -259,6 +280,9 @@ class Graph {
|
||||||
panopticon.graph.saveJson(msg['@id'], e.target, function(e2){
|
panopticon.graph.saveJson(msg['@id'], e.target, function(e2){
|
||||||
console.log(e);
|
console.log(e);
|
||||||
audioSpan.innerHTML = e.target.files[0].name + "<sup>*</sup>";
|
audioSpan.innerHTML = e.target.files[0].name + "<sup>*</sup>";
|
||||||
|
// reload graph:
|
||||||
|
console.log('reload', panopticon.graph.language_code);
|
||||||
|
panopticon.loadNarrative(panopticon.graph.language_code);
|
||||||
});
|
});
|
||||||
// console.log(this,e);
|
// console.log(this,e);
|
||||||
}
|
}
|
||||||
|
@ -596,7 +620,7 @@ class Graph {
|
||||||
getJsonString() {
|
getJsonString() {
|
||||||
// recreate array to have the right order of items.
|
// recreate array to have the right order of items.
|
||||||
this.data = [...this.messages, ...this.conditions,
|
this.data = [...this.messages, ...this.conditions,
|
||||||
...this.directions, ...this.interruptions]
|
...this.directions, ...this.diversions]
|
||||||
let d = [];
|
let d = [];
|
||||||
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x', 'y', 'vx', 'vy']
|
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x', 'y', 'vx', 'vy']
|
||||||
for ( let node of this.data ) {
|
for ( let node of this.data ) {
|
||||||
|
@ -673,7 +697,7 @@ class Graph {
|
||||||
this.messages = this.data.filter(( node ) => node['@type'] == 'Msg' );
|
this.messages = this.data.filter(( node ) => node['@type'] == 'Msg' );
|
||||||
this.directions = this.data.filter(( node ) => node['@type'] == 'Direction' );
|
this.directions = this.data.filter(( node ) => node['@type'] == 'Direction' );
|
||||||
this.conditions = this.data.filter(( node ) => node['@type'] == 'Condition' );
|
this.conditions = this.data.filter(( node ) => node['@type'] == 'Condition' );
|
||||||
this.interruptions = this.data.filter(( node ) => node['@type'] == 'Interruption' );
|
this.diversions = this.data.filter(( node ) => node['@type'] == 'Diversion' );
|
||||||
|
|
||||||
document.getElementById('current_lang').innerHTML = "";
|
document.getElementById('current_lang').innerHTML = "";
|
||||||
document.getElementById('current_lang').appendChild(crel('span', {
|
document.getElementById('current_lang').appendChild(crel('span', {
|
||||||
|
|
|
@ -1,832 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" dir="ltr">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Pillow Talk - Narrative Builder</title>
|
|
||||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
// https://raw.githubusercontent.com/KoryNunn/crel/master/crel.min.js
|
|
||||||
!function(n,e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):n.crel=e()}(this,function(){function n(a){var d,s=arguments,p=s[1],y=2,m=s.length,x=n[o];if(a=n[u](a)?a:c.createElement(a),m>1){if((!e(p,r)||n[f](p)||Array.isArray(p))&&(--y,p=null),m-y==1&&e(s[y],"string"))a.textContent=s[y];else for(;y<m;++y)null!==(d=s[y])&&l(a,d);for(var v in p)if(x[v]){var A=x[v];e(A,t)?A(a,p[v]):a[i](A,p[v])}else e(p[v],t)?a[v]=p[v]:a[i](v,p[v])}return a}var e=function(n,e){return typeof n===e},t="function",r="object",i="setAttribute",o="attrMap",f="isNode",u="isElement",c=document,a=function(n){return n instanceof Node},d=function(n){return n instanceof Element},l=function(e,t){if(Array.isArray(t))return void t.map(function(n){l(e,n)});n[f](t)||(t=c.createTextNode(t)),e.appendChild(t)};return n[o]={},n[u]=d,n[f]=a,e(Proxy,"undefined")||(n.proxy=new Proxy(n,{get:function(e,t){return!(t in n)&&(n[t]=n.bind(null,t)),n[t]}})),n});
|
|
||||||
|
|
||||||
crel.attrMap['on'] = function(element, value) {
|
|
||||||
for (var eventName in value) {
|
|
||||||
element.addEventListener(eventName, value[eventName]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style media="screen">
|
|
||||||
body{
|
|
||||||
margin:0;
|
|
||||||
overflow: hidden;
|
|
||||||
font-family: "Noto Sans", sans-serif;
|
|
||||||
}
|
|
||||||
svg{
|
|
||||||
width:100vw;
|
|
||||||
height: 100vh;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
svg:active{
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
circle{
|
|
||||||
cursor: pointer;
|
|
||||||
fill: rgb(119, 97, 142);
|
|
||||||
}
|
|
||||||
.startMsg circle{
|
|
||||||
fill: lightseagreen;
|
|
||||||
}
|
|
||||||
.endMsg circle{
|
|
||||||
fill: lightslategray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orphanedMsg{
|
|
||||||
fill: lightcoral;
|
|
||||||
}
|
|
||||||
text{
|
|
||||||
text-anchor: middle;
|
|
||||||
font-size: 11pt;
|
|
||||||
font-family: sans-serif;
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
line{
|
|
||||||
marker-end: url('#arrowHead');
|
|
||||||
stroke-width: 2px;
|
|
||||||
stroke: black;
|
|
||||||
}
|
|
||||||
line.link--noconditions{
|
|
||||||
stroke-dasharray: 5 4;
|
|
||||||
stroke: red;
|
|
||||||
}
|
|
||||||
label::after {
|
|
||||||
content: '';
|
|
||||||
clear: both;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
label{
|
|
||||||
width:100%;
|
|
||||||
font-weight:bold;
|
|
||||||
display: block;
|
|
||||||
margin: 0 -10px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
label input,label select{
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
label:nth-child(odd){
|
|
||||||
background-color: rgba(255,255,255,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#msg{
|
|
||||||
position: absolute;
|
|
||||||
top:0;
|
|
||||||
right:0;
|
|
||||||
width: 30%;
|
|
||||||
max-height:100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
#msg .msg__info, #msg .directions > div{
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
background:lightgray;
|
|
||||||
}
|
|
||||||
#opener{
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nodes g:hover circle,
|
|
||||||
.selectedMsg circle {
|
|
||||||
stroke: lightgreen;
|
|
||||||
stroke-width: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controlDown #nodes g:hover circle,
|
|
||||||
.secondaryMsg circle {
|
|
||||||
stroke: lightgreen;
|
|
||||||
stroke-width: 5;
|
|
||||||
stroke-dasharray: 10 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.condition h4{
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.condition + .condition::before {
|
|
||||||
content: "OR";
|
|
||||||
display: block;
|
|
||||||
border-bottom: solid 2px;
|
|
||||||
height: 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 2px 2px 2px lightgray,-2px 2px 2px lightgray,2px -2px 2px lightgray,-2px -2px 2px lightgray;
|
|
||||||
|
|
||||||
}
|
|
||||||
.condition--add{
|
|
||||||
/* text-align: center; */
|
|
||||||
}
|
|
||||||
.btn{
|
|
||||||
padding: 5px;
|
|
||||||
background:lightgray;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
.btn:hover{
|
|
||||||
background: lightblue;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="opener">
|
|
||||||
<h1>Hugvey</h1>
|
|
||||||
<h3>Select a narrative json file</h3>
|
|
||||||
<input id='fileOpener' type="file" />
|
|
||||||
<div>
|
|
||||||
<input id='fileLoad' type="submit" value="Load file" />
|
|
||||||
<input id='stateLoad' type="button" value="Load last state" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="msg"></div>
|
|
||||||
<div id="controls">
|
|
||||||
<div id='btn-save' class='btn'>Save JSON</div>
|
|
||||||
<div id='btn-addMsg' class='btn'>New Message</div>
|
|
||||||
</div>
|
|
||||||
<svg id='graph' viewbox="0 0 1280 1024" preserveAspectRatio="xMidYMid">
|
|
||||||
<defs>
|
|
||||||
<marker markerHeight="8" markerWidth="8" refY="0" refX="12" viewBox="0 -6 16 12" preserveAspectRatio="none" orient="auto" id="arrowHead"><path d="M0,-6L16,0L0,6" fill="black"></path></marker>
|
|
||||||
</defs>
|
|
||||||
<g id='container'>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</body>
|
|
||||||
<script type="text/javascript">
|
|
||||||
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.selectedMsg = null;
|
|
||||||
this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
|
||||||
this.directions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
|
||||||
this.conditions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
|
||||||
this.interruptions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
|
||||||
|
|
||||||
let graph = this;
|
|
||||||
this.controlDown = false;
|
|
||||||
document.addEventListener('keydown', function(e){
|
|
||||||
console.log(e);
|
|
||||||
if(e.which == "17") {
|
|
||||||
graph.controlDown = true;
|
|
||||||
document.body.classList.add('controlDown');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('keyup', function(e){
|
|
||||||
console.log(e);
|
|
||||||
if(e.which == "17") {
|
|
||||||
graph.controlDown = false;
|
|
||||||
document.body.classList.remove('controlDown');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let c = this.container;
|
|
||||||
let zoomed = function(){
|
|
||||||
c.attr("transform", d3.event.transform);
|
|
||||||
}
|
|
||||||
this.svg.call(d3.zoom()
|
|
||||||
.scaleExtent([1 / 2, 8])
|
|
||||||
.on("zoom", zoomed));
|
|
||||||
|
|
||||||
this.nodesG = this.container.append("g")
|
|
||||||
.attr("id", "nodes")
|
|
||||||
|
|
||||||
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(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
clickMsg(msg) {
|
|
||||||
// event when a message is clicked.
|
|
||||||
console.log(msg);
|
|
||||||
|
|
||||||
if(this.controlDown) {
|
|
||||||
this.secondarySelectMsg(msg);
|
|
||||||
} else {
|
|
||||||
this.selectMsg(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
secondarySelectMsg(msg) {
|
|
||||||
if(this.selectedMsg !== null) {
|
|
||||||
this.addDirection(this.selectedMsg, msg);
|
|
||||||
} else {
|
|
||||||
console.error('No message selected as Source');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectMsg(msg) {
|
|
||||||
let selectedEls = document.getElementsByClassName('selectedMsg');
|
|
||||||
while(selectedEls.length > 0){
|
|
||||||
selectedEls[0].classList.remove('selectedMsg');
|
|
||||||
}
|
|
||||||
document.getElementById(msg['@id']).classList.add('selectedMsg');
|
|
||||||
|
|
||||||
this.selectedMsg = msg;
|
|
||||||
|
|
||||||
this.showMsg(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMsg() {
|
|
||||||
// used eg. after a condition creation.
|
|
||||||
this.showMsg(this.selectedMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
showMsg(msg) {
|
|
||||||
let msgEl = document.getElementById('msg');
|
|
||||||
msgEl.innerHTML = "";
|
|
||||||
let startAttributes = {
|
|
||||||
'name': msg['@id'] + '-start',
|
|
||||||
'disabled': true,
|
|
||||||
'type': 'checkbox',
|
|
||||||
'on': {
|
|
||||||
'change': this.getEditEventListener()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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', {
|
|
||||||
'name': msg['@id'] + '-text',
|
|
||||||
'value': msg['text'],
|
|
||||||
'on': {
|
|
||||||
'change': this.getEditEventListener()
|
|
||||||
}
|
|
||||||
} )
|
|
||||||
),
|
|
||||||
crel('label',
|
|
||||||
crel('span', 'Start'),
|
|
||||||
crel('input', startAttributes)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
msgEl.appendChild(msgInfoEl);
|
|
||||||
|
|
||||||
// let directionHEl = document.createElement('h2');
|
|
||||||
// directionHEl.innerHTML = "Directions";
|
|
||||||
|
|
||||||
let fromDirections =[] , toDirections = [];
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
let directionsEl = crel('div', {'class': 'directions'},
|
|
||||||
crel('h2', 'Directions'),
|
|
||||||
...toDirections, ...fromDirections
|
|
||||||
);
|
|
||||||
msgEl.appendChild(directionsEl);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectionEl(direction, msg) {
|
|
||||||
let directionEl = document.createElement('div');
|
|
||||||
if(direction['source'] == msg) {
|
|
||||||
directionEl.innerHTML = `<h3>To ${direction['target']['@id']}</h3>`;
|
|
||||||
} else {
|
|
||||||
directionEl.innerHTML = `<h3>From ${direction['source']['@id']}</h3>`;
|
|
||||||
}
|
|
||||||
let del = document.createElement('div');
|
|
||||||
del.innerHTML = "delete";
|
|
||||||
del.classList.add("deleteBtn");
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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'])
|
|
||||||
)
|
|
||||||
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);
|
|
||||||
|
|
||||||
for(let v in condition['vars']) {
|
|
||||||
let varLabel = document.createElement('label');
|
|
||||||
varLabel.innerHTML = v;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return conditionEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getConditionTypes() {
|
|
||||||
if(typeof this.conditionTypes === 'undefined') {
|
|
||||||
// type: vars: attribtes for crel()
|
|
||||||
this.conditionTypes = {
|
|
||||||
'timeout': {
|
|
||||||
'seconds': {'type': 'number', 'value': 10, 'min':0, 'step': 0.1}
|
|
||||||
},
|
|
||||||
'replyContains': {
|
|
||||||
'regex': {'value': '.+'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.conditionTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
fillConditionFormForType(conditionForm, type) {
|
|
||||||
conditionForm.innerHTML = "";
|
|
||||||
let vars = this.getConditionTypes()[type];
|
|
||||||
for(let v in vars){
|
|
||||||
let attr = vars[v];
|
|
||||||
attr['name'] = v;
|
|
||||||
conditionForm.appendChild(
|
|
||||||
crel('label',
|
|
||||||
crel('span', v),
|
|
||||||
crel('input', attr)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAddConditionFormEl(direction) {
|
|
||||||
|
|
||||||
let optionEls = [];
|
|
||||||
let types = this.getConditionTypes();
|
|
||||||
for(let type in types) {
|
|
||||||
optionEls.push(crel('option', type));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
crel("h4", "Create New Condition"),
|
|
||||||
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'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.fillConditionFormForType(conditionForm, optionEls[0].value);
|
|
||||||
|
|
||||||
return addConditionEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
rmConditionFromDirection(condition, direction) {
|
|
||||||
let id = condition['@id'];
|
|
||||||
// TODO
|
|
||||||
if(typeof direction != 'undefined') {
|
|
||||||
|
|
||||||
}
|
|
||||||
this._rmNode(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getConditionEl(condition) {
|
|
||||||
let conditionEl = document.createElement('div');
|
|
||||||
|
|
||||||
return conditionEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectionsFrom(msg) {
|
|
||||||
return this.directions.filter(d => d['source'] == msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectionsTo(msg) {
|
|
||||||
return this.directions.filter(d => d['target'] == msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
addMsg() {
|
|
||||||
let msg = {
|
|
||||||
"@id": "n" + Date.now().toString(36),
|
|
||||||
"@type": "Msg",
|
|
||||||
"text": "New",
|
|
||||||
"start": false
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
this._rmNode(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
_rmNode(node) {
|
|
||||||
// remove msg/direction/condition/etc
|
|
||||||
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']);
|
|
||||||
this.updateFromData();
|
|
||||||
this.build();
|
|
||||||
|
|
||||||
this.updateMsg();
|
|
||||||
}
|
|
||||||
|
|
||||||
addCondition(type, label, vars, skip) {
|
|
||||||
let con = {
|
|
||||||
"@id": "c" + Date.now().toString(36),
|
|
||||||
"@type": "Condition",
|
|
||||||
"type": type,
|
|
||||||
"label": label,
|
|
||||||
"vars": vars
|
|
||||||
}
|
|
||||||
this.data.push(con);
|
|
||||||
if(skip !== true) {
|
|
||||||
this.updateFromData();
|
|
||||||
this.build();
|
|
||||||
}
|
|
||||||
return con;
|
|
||||||
}
|
|
||||||
|
|
||||||
addDirection(source, target) {
|
|
||||||
let dir = {
|
|
||||||
"@id": "d" + Date.now().toString(36),
|
|
||||||
"@type": "Direction",
|
|
||||||
"source": source,
|
|
||||||
"target": target,
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
this.data.push(dir);
|
|
||||||
this.updateFromData();
|
|
||||||
this.build();
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
rmDirection(dir) {
|
|
||||||
this._rmNode(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
createMsg() {
|
|
||||||
this.addMsg();
|
|
||||||
this.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
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(){
|
|
||||||
let graph = this;
|
|
||||||
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<path.length;i++){
|
|
||||||
if(i == (path.length -1)) {
|
|
||||||
console.log('last', path[i]);
|
|
||||||
res[path[i]] = e.srcElement.value;
|
|
||||||
} else {
|
|
||||||
res=res[path[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// node[field] = e.srcElement.value;
|
|
||||||
|
|
||||||
graph.build();
|
|
||||||
}
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
getJsonString() {
|
|
||||||
// recreate array to have the right order of items.
|
|
||||||
this.data = [...this.messages, ...this.conditions,
|
|
||||||
...this.directions, ...this.interruptions]
|
|
||||||
let d = [];
|
|
||||||
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x','y', 'vx','vy']
|
|
||||||
for(let node of this.data) {
|
|
||||||
let n = {};
|
|
||||||
console.log(node['source']);
|
|
||||||
for (let e in node) {
|
|
||||||
if (node.hasOwnProperty(e) && toRemove.indexOf(e) == -1 ) {
|
|
||||||
if(this.data.indexOf(node[e]) != -1) {
|
|
||||||
n[e] = node[e]['@id'];
|
|
||||||
} else {
|
|
||||||
n[e] = node[e];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.push(n);
|
|
||||||
}
|
|
||||||
return JSON.stringify(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveJson() {
|
|
||||||
var blob = new Blob([this.getJsonString()], {type: 'application/json'});
|
|
||||||
if(window.navigator.msSaveOrOpenBlob) {
|
|
||||||
window.navigator.msSaveBlob(blob, "pillow_talk.json");
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
var elem = window.document.createElement('a');
|
|
||||||
elem.href = window.URL.createObjectURL(blob);
|
|
||||||
elem.download = "pillow_talk.json";
|
|
||||||
document.body.appendChild(elem);
|
|
||||||
elem.click();
|
|
||||||
document.body.removeChild(elem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData(data) {
|
|
||||||
this.data = data;
|
|
||||||
this.updateFromData();
|
|
||||||
this.build(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFromData() {
|
|
||||||
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');
|
|
||||||
|
|
||||||
// save state;
|
|
||||||
this.saveState();
|
|
||||||
}
|
|
||||||
|
|
||||||
saveState() {
|
|
||||||
window.localStorage.setItem("lastState", this.getJsonString());
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSavedState() {
|
|
||||||
return window.localStorage.getItem("lastState") !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFromState() {
|
|
||||||
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))
|
|
||||||
;
|
|
||||||
|
|
||||||
// Update existing nodes
|
|
||||||
let node = this.nodesG
|
|
||||||
.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")
|
|
||||||
;
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
link.attr('class', l => { return `link ` + (l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions"); });
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
// ;
|
|
||||||
|
|
||||||
let n = this.nodesG;
|
|
||||||
this.simulation.on("tick", () => {
|
|
||||||
link
|
|
||||||
.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;
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
dx = targetX - sourceX;
|
|
||||||
dy = d.target.y - d.source.y;
|
|
||||||
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; });
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.svg.node();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var graph = new Graph();
|
|
||||||
|
|
||||||
var openerDiv = document.getElementById("opener");
|
|
||||||
var fileOpenerBtn = document.getElementById('fileOpener');
|
|
||||||
var openerSubmit = document.getElementById('fileLoad');
|
|
||||||
var loadFileFromInput = function (inputEl) {
|
|
||||||
if(inputEl.files && inputEl.files[0]){
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = function (e) {
|
|
||||||
var output=e.target.result;
|
|
||||||
let j = JSON.parse(output);
|
|
||||||
graph.loadData(j);
|
|
||||||
openerDiv.parentElement.removeChild(openerDiv);
|
|
||||||
};//end onload()
|
|
||||||
reader.readAsText(inputEl.files[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fileOpenerBtn.addEventListener('change', function(){ loadFileFromInput(this); })
|
|
||||||
|
|
||||||
if(fileOpenerBtn.files.length) {
|
|
||||||
openerSubmit.addEventListener('click', function() { loadFileFromInput(fileOpener); });
|
|
||||||
} else {
|
|
||||||
openerSubmit.parentElement.removeChild(openerSubmit);
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadStateBtn = document.getElementById('stateLoad');
|
|
||||||
if(graph.hasSavedState()) {
|
|
||||||
loadStateBtn.addEventListener('click', function() {
|
|
||||||
graph.loadFromState();
|
|
||||||
|
|
||||||
openerDiv.parentElement.removeChild(openerDiv);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
loadStateBtn.parentElement.removeChild(loadStateBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</html>
|
|
|
@ -28,6 +28,11 @@ body{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img.icon{
|
||||||
|
height: .9em;
|
||||||
|
width: .9em;
|
||||||
|
}
|
||||||
|
|
||||||
#interface{
|
#interface{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -58,6 +63,19 @@ body{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.counts{
|
||||||
|
dd, dt{
|
||||||
|
display: inline-block;
|
||||||
|
width: 30px;
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margiN: 0;
|
||||||
|
&:hover{
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hugvey{
|
.hugvey{
|
||||||
background-image: linear-gradient(to top, #587457, #35a589);
|
background-image: linear-gradient(to top, #587457, #35a589);
|
||||||
color:white;
|
color:white;
|
||||||
|
@ -81,6 +99,39 @@ body{
|
||||||
|
|
||||||
&.hugvey--off{
|
&.hugvey--off{
|
||||||
background-image: linear-gradient(to top, #575d74, #3572a5);
|
background-image: linear-gradient(to top, #575d74, #3572a5);
|
||||||
|
&::after{
|
||||||
|
content: 'off';
|
||||||
|
font-style: italic;
|
||||||
|
color: gray;
|
||||||
|
text-align:center;
|
||||||
|
// font-size: 30pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hugvey--gone{
|
||||||
|
background-image: linear-gradient(to top, orange, rgb(206, 92, 0));
|
||||||
|
&::after{
|
||||||
|
content: 'disconnected';
|
||||||
|
font-style: italic;
|
||||||
|
color: gray;
|
||||||
|
text-align:center;
|
||||||
|
// font-size: 30pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hugvey--paused{
|
||||||
|
background-image: linear-gradient(to top, rgb(136, 138, 133), rgb(85, 87, 83));
|
||||||
|
&::after{
|
||||||
|
content: 'disconnected';
|
||||||
|
font-style: italic;
|
||||||
|
color: gray;
|
||||||
|
text-align:center;
|
||||||
|
// font-size: 30pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hugvey--finished{
|
||||||
|
background-image: linear-gradient(to top, rgb(136, 138, 133), #35a589);
|
||||||
&::after{
|
&::after{
|
||||||
content: 'disconnected';
|
content: 'disconnected';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
Loading…
Reference in a new issue