9 Commits
daniel ... tmp

Author SHA1 Message Date
19fcc4d7de fix setting cursor when clicked
cursor should not be set when dragging (pan)
2024-09-25 10:48:28 +02:00
ce5d489b2d show time labels only on first chart 2024-09-25 09:23:04 +02:00
5f5e336370 improve time tick labels more 2024-09-25 09:02:34 +02:00
ac542b1694 improve time axis labels
+ change poll interval to 1 sec
2024-09-25 07:51:43 +02:00
4e27d66d36 fix mechanism to strip overlapping data for live update
fix bug when when there was no last value yet
2024-09-25 07:46:28 +02:00
638f77a047 Doc + working live 2024-09-13 16:40:06 +02:00
45e1957fd7 Autoscale considers new ChartJS format, data reloads onzoom and onpan 2024-09-13 16:11:40 +02:00
d5a5c6553e Zoom and pan are synchronized, changed afterBuildTicksSignature 2024-09-13 15:47:53 +02:00
ab9f7b8ab0 initial test + libraries 2024-09-13 11:45:06 +02:00
84 changed files with 40030 additions and 2792 deletions

3
.gitignore vendored
View File

@ -5,7 +5,6 @@
__pycache__ __pycache__
.idea .idea
log log
webserver.log
client/favicon_package_v0 client/favicon_package_v0
client/favicon.ico client/favicon.ico
client/favicon192.png_old client/favicon192.png_old

View File

@ -1,32 +1,29 @@
# seweb # SEAWeb
**The Web GUI client for Sample Environment at SINQ** The WEB GUI client of SEA.
This repository contains the code of the server for the control and graphical parts, plus the client code (HTML, CSS, JS). This repository contains the code of the server for the control and graphical parts, plus the client code (HTML, CSS, JS).
**Migration** **IMPORTANT**
Remarks for the migration from ChartJS 2.9.4 to 4.4.4. This branch is an attempt to migrate from ChartJS 2.9.4 to 4.4.4.
TESTED ON SAFARI : with this new version, the application takes much less RAM, and does not crash at some point. The user can still experience some latencies, but it might be due to the presence of too many time axis labels + the fact that each graphs has its own (for the moment). TESTED ON SAFARI : with this new version, the application takes much less RAM, and does not crash at some point. The user can still experience some latencies, but it might be due to the presence of too many time axis labels + the fact that each graphs has its own (for the moment).
Here is a list of what has been done : Here is a list of what has been done :
- Upgraded the ChartJS zoom plugin library, and changed the corresponding options in the chart configuration. The previous version was not working with the version 4.4.4 of ChartJS - Uprgaded the ChartJS zoom plugin library, and changed the corresponding options in the chart configuration. The previous version was not working with the version 4.4.4 of ChartJS
- Installing the date library Luxon and its adpater for ChartJS. This is needed because since version 3.x, time axes need these libraries. - Installing the date library Luxon and its adpater for ChartJS. This is needed because since version 3.x, time axes need these libraries.
- Renamed or moved all needed parameters in the ChartJS configuration. - Renamed or moved all needed parameters in the ChartJS configuration.
- Changed all `xAxes` and `yAxes` references to `x` and `y`. - Changed all `xAxes` and `yAxes` references to `x` and `y`.
- Adapted `afterBuildTicks` callbacks with the new signature (only `axis` is given) - Adapted `afterBuildTicks` callbacks with the new signature (only `axis` is given)
- Changed all references to `ticks.max|min` : these two properties are one step higher, at the level of the axis, not the ticks - Changed all references to `ticks.max|min` : these two properties are one step higher, at the level of the axis, not the ticks
- Change the implementation of the callback located in `options.scales.x.ticks` at chart creation in the Chart class,
so it considers that the label is a timestamp. -> move code to afterBuildTicks
Reference : https://www.chartjs.org/docs/latest/axes/labelling.html#creating-custom-tick-formats
- improvment of labeling, using beforeFit instead of ticks.callback to modify labels
- fixed flase cursor appearance when panning
- Make the zoom type toggle work again.
- Display only one time axis.
Here is a list of what needs to be done : Here is a list of what needs to be done :
- Change the implementation of the callback located in `options.scales.x.ticks` at chart creation in the Chart class, so it considers that the label is a timestamp. Reference : https://www.chartjs.org/docs/latest/axes/labelling.html#creating-custom-tick-formats
- Labels of the x axis are not displayed in the desired format, and do not rescale properly on zooming/dezooming. There can be too much labels, that make them rotate and invisible.
- The cursor now also displays when the click ends, which is not the same behavior as before.
- Make the zoom type toggle work again.
- Make the zoom via touchpad less sensitive. The recent tests have shown that the zoom via gesture is very sensitive. Two things can be looked for : 1. see if there is the possibility to adapt the sensitivity of the zoom for the touchpad only or 2. update the library Hammer.js which is used by ChartJS to handle this type of gesture (even if the current version is 0.0.1 version later than the last one, this might be an explanation). - Make the zoom via touchpad less sensitive. The recent tests have shown that the zoom via gesture is very sensitive. Two things can be looked for : 1. see if there is the possibility to adapt the sensitivity of the zoom for the touchpad only or 2. update the library Hammer.js which is used by ChartJS to handle this type of gesture (even if the current version is 0.0.1 version later than the last one, this might be an explanation).
- Display only one time axis.
**Summary** **Summary**
@ -101,7 +98,7 @@ seagraph.py <-- Its content is used if the server is run
1. Clone this repository on the `~` folder on your machine 1. Clone this repository on the `~` folder on your machine
2. If not done yet, activate a Python environnment 2. If not done yet, activate a Python environnment
3. Run the command `cd seweb` 3. Run the command `cd seaweb`
### Configuring the application ### Configuring the application
@ -118,33 +115,29 @@ For the `generic.ini` and `<instrument>.ini` files, go to `./doc/variables_confi
### Starting the application ### Starting the application
1. Run the command `cd ~/seweb` 1. Run the command `cd ~/seaweb`
2. Depending on if you want to start the right part or no, go to the file `./client/jsFiles/SEAWebClientMain.js` 2. Depending on if you want to start the right part or no, go to the file `./client/jsFiles/SEAWebClientMain.js`
3. Edit the line 79 : `(.treat("hideRightPart", "hr", to_bool, <value>))` by replacing `<value>` with `false` to have the right part, or `true` to hide it. 3. Edit the line 79 : `(.treat("hideRightPart", "hr", to_bool, <value>))` by replacing `<value>` with `false` to have the right part, or `true` to hide it.
4. 4.
To start the server the history from InfluxDB (NICOS cache), run the command : To start the server without the right part and the history from InfluxDB (NICOS cache), run the command :
`./secop-webserver port=<port> instrument=<instrument> hostport=<host:port>` `python ./seaweb.py type=influx port=<port> instrument=<instrument>`
Dummy server (with dummy graphics): With the right part :
`./dummy-webserver port=<port> instrument=<instrument> hostport=<host:port>` `python ./seaweb.py type=influxsea port=<port> sea=<sea_address> instrument=<instrument>`
Where : Where :
- `<port>` : the port of the machine to start the server with (for example : 8841) - `<port>` : the port of the machine to start the server with (for example : 8841)
- `<host:port>` : the address of the SECoP server - `<sea_address>` : the address of the SEA server (host:port) in order to have a running right part (for example : samenv:8664)
- `<instrument>` : the name of the instrument (for example : lab4) - `<instrument>` : the name of the instrument (for example : lab4)
### Stopping the application ### Stopping the application
If the server is started diretly in a Terminal: press ctrl-C 1. Run the command `ps ax | grep seaweb`
If the server is started in the background (with '&' appended to the command):
1. Run the command `ps ax | grep webserver`
2. In the output of the previous command, identifiy the PID corresponding to the server process 2. In the output of the previous command, identifiy the PID corresponding to the server process
3. Run the command `kill <previously_identified_PID>` 3. Run the command `kill <previously_identified_PID>`
@ -169,4 +162,4 @@ If the server is started in the background (with '&' appended to the command):
- For choosing the color, the user should have the possibility to use a color picker to choose an arbitrary color, without loosing the possibility to choose one of the predefined color - For choosing the color, the user should have the possibility to use a color picker to choose an arbitrary color, without loosing the possibility to choose one of the predefined color
- Finally, for maitenance, an idea could be to have the possibility for an informed user to send its configuration to directly overwrite the `<instrument>.ini` file. - Finally, for maitenance, an idea could be to have the possibility for an informed user to send its configuration to directly overwrite the `<instrument>.ini` file.
- For the export : - For the export :
- The binning option should be checked since the user changes the input. - The binning option should be checked since the user changes the input.

101
base.py
View File

@ -1,101 +0,0 @@
import sys
import time
import uuid
ONEYEAR = 366 * 24 * 3600
def get_abs_time(times):
"""Gets the absolute times for the given potential relative times.
If a given timestamp is less than one year, then the value is
relative (to now, rounded up to a full second) and converted
into an absolute timestamp
Parameters :
times([(float)]) : an array of unix timestamps or relative duration (< 1 year) as floats
Returns :
[(float)] : an array of absolute unix timestamps as floats
"""
now = int(time.time() + 0.999)
return [t + now if t < ONEYEAR else t for t in times]
class Logger(object):
def __init__(self, logpath):
self.terminal = sys.stdout
self.log = open(logpath, "a")
def write(self, message):
self.terminal.write(message)
self.log.write(message)
def flush(self):
pass
class HandlerBase:
def __init__(self):
self.handlers = {k[2:]: getattr(self, k) for k in dir(type(self)) if k.startswith('w_')}
class Client(HandlerBase):
def __init__(self, server, streams, instrument_name, device_name):
super().__init__()
self.id = uuid.uuid4().hex[0:15]
self.nodes = {}
self.node_map = {}
if streams:
for uri in streams:
urisplit = uri.rsplit('://')
kind = urisplit[0] if len(urisplit) == 2 else 'secop'
node = server.interactor_classes[kind](uri, self.node_map)
self.nodes[uri] = node
self.server = server
self.instrument_name = instrument_name
self.device_name = device_name # do not know if this is needed
self.updates = {}
def poll(self):
updates = sum((n.get_updates() for n in self.nodes.values()), start=[])
result = [dict(type='update', updates=updates)] if updates else []
graph_updates = self.handlers.get('graphpoll', object)()
if graph_updates:
result.append(graph_updates)
return result
def w_getblock(self, path):
path = path.split(',')[-1] # TODO: why this?
if path == "main": # TODO: change to "-main-"?
components = []
for node in self.nodes.values():
node.add_main_components(components)
return dict(type='draw', path='main', title='modules', components=components)
node = self.node_map[path]
return dict(type='draw', path=path, title=path, components=node.get_components(path))
def w_updateblock(self, path):
if path == 'main': # TODO: change to "-main-"?
for node in self.nodes.values():
node.update_main()
else:
node = self.node_map[path]
node.update_params(path)
return dict(type='accept-block')
def w_console(self): # TODO: check if still used
return dict(type='accept-console')
def w_sendcommand(self, command):
result = None
for node in self.nodes.values():
result = node.handle_command(command)
if result is not None:
break
if isinstance(result, dict):
return dict(type='accept-command', result=result)
return dict(type='accept-command')
def info(self):
return ["na"]

View File

@ -1,7 +1,4 @@
from configparser import ConfigParser from configparser import ConfigParser
import logging
class ChartConfig: class ChartConfig:
""" """
Class that holds the chart section of a configuration file (for an instrument). Class that holds the chart section of a configuration file (for an instrument).
@ -9,38 +6,46 @@ class ChartConfig:
Attributes : Attributes :
chart_config (Section) : the Section corresponding to the "chart" section in the given configuration file chart_config (Section) : the Section corresponding to the "chart" section in the given configuration file
""" """
KEYS = ["cat", "color", "unit", "label"]
def __init__(self, path): def __init__(self, path):
""" """
Parameters : Parameters :
path (str) : the path to the configuration file path (str) : the path to the configuration file
""" """
self.variables = {}
cfgp = ConfigParser(interpolation=None) cfgp = ConfigParser(interpolation=None)
cfgp.optionxform = str
cfgp.read(path) cfgp.read(path)
try: self.chart_config = cfgp["chart"]
section = cfgp["chart"]
except KeyError: def get_variable_parameter_config(self, key):
return """
for key, raw_value in section.items(): Gets the configuration of the given key in the configuration file (chart section).
Parameters :
key (str) : the key to look for in the chart section (<variable>[.<param>])
Returns :
{"cat":(str), "color":(str), "unit":(str)} | None : a dictionnary representing the different options for the given key in the chart section.
The different options are in this dict if they are found in the chart section for the given key. Returns None if the key is not in the chart section,
or if there is a syntax problem for the given key.
"""
config = {}
positionnal = ["cat", "color", "unit"]
if key in self.chart_config.keys():
raw_value = self.chart_config[key]
arguments = raw_value.split(",") arguments = raw_value.split(",")
keyword_mode = False keyword_mode = False
config = {'cat': '*'}
for i, argument in enumerate(arguments): for i, argument in enumerate(arguments):
argname, _, argvalue = argument.rpartition(':') pieces = argument.split(":")
if argname: if len(pieces) == 2:
keyword_mode = True keyword_mode = True
config[argname] = argvalue if pieces[1] != "":
config[pieces[0]] = pieces[1]
else: else:
if keyword_mode: if not keyword_mode: #everything is going well
logging.error('positional arg after keywd arg: %s=%r', key, raw_value) if pieces[0] != "":
else: config[positionnal[i]] = pieces[0]
try: else: #we cannot have a positionnal argument after a keyword argument
if argvalue: return None
config[self.KEYS[i]] = argvalue return config
except Exception as e: else:
logging.error('%r in %s=%r', e, key, raw_value) return None
self.variables[key] = config

View File

@ -2,12 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="icon" type="image/png" href="/favicon192.png" sizes=192x192>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" /> content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
@ -34,6 +30,8 @@
<!-- CSS-Files --> <!-- CSS-Files -->
<link rel="stylesheet" href="externalFiles/alertify.css"> <link rel="stylesheet" href="externalFiles/alertify.css">
<link rel="stylesheet" href="externalFiles/swiper-bundle.min.css">
<link rel="stylesheet" href="cssFiles/SEAWebClientSwiper.css">
<link rel="stylesheet" href="cssFiles/SEAWebClientGroup.css"> <link rel="stylesheet" href="cssFiles/SEAWebClientGroup.css">
<link rel="stylesheet" href="cssFiles/SEAWebClientConsole.css"> <link rel="stylesheet" href="cssFiles/SEAWebClientConsole.css">
<link rel="stylesheet" href="cssFiles/SEAWebClientGraphics.css"> <link rel="stylesheet" href="cssFiles/SEAWebClientGraphics.css">
@ -41,6 +39,8 @@
<!-- JS-Files --> <!-- JS-Files -->
<script src="externalFiles/alertify.js"></script> <script src="externalFiles/alertify.js"></script>
<script src="externalFiles/eventsource.js"></script> <script src="externalFiles/eventsource.js"></script>
<!-- <script src="externalFiles/d3.min.js"></script> -->
<script src="externalFiles/swiper-bundle.min.js"></script>
<!-- <script src="externalFiles/Chart.bundle.min.js"></script> --> <!-- <script src="externalFiles/Chart.bundle.min.js"></script> -->
<script src="externalFiles/chart.umd.min.js"></script> <script src="externalFiles/chart.umd.min.js"></script>
<script src="externalFiles/luxon.min.js"></script> <script src="externalFiles/luxon.min.js"></script>
@ -51,6 +51,7 @@
<!-- <script src="externalFiles/chartjs-zoom.js"></script> --> <!-- <script src="externalFiles/chartjs-zoom.js"></script> -->
<script src="jsFiles/SEAWebClientLocalStorage.js"></script> <script src="jsFiles/SEAWebClientLocalStorage.js"></script>
<script src="jsFiles/SEAWebClientResponsivity.js"></script> <script src="jsFiles/SEAWebClientResponsivity.js"></script>
<script src="jsFiles/SEAWebClientSwiper.js"></script>
<script src="jsFiles/SEAWebClientGroup.js"></script> <script src="jsFiles/SEAWebClientGroup.js"></script>
<script src="jsFiles/SEAWebClientConsole.js"></script> <script src="jsFiles/SEAWebClientConsole.js"></script>
<script src="jsFiles/SEAWebClientGraphics.js"></script> <script src="jsFiles/SEAWebClientGraphics.js"></script>
@ -76,15 +77,7 @@
</div> </div>
</div> </div>
<div id="center"></div> <div id="center"></div>
<div class = "icon-close-container icon-main-container"> <div id="close-cross">&#215;</div>
<img class = "icon-close icon-main" src="res/icon_close.png">
</div>
<div class="icon-log-container icon-main-container">
<img class = "icon-log icon-main" src="res/icon_log.png">
</div>
<div class="icon-lock-container icon-main-container">
<img class = "icon-lock icon-main" src="res/icon_lock_closed.png">
</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,96 @@
<!--- OBSOLETE! -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style"
content="black-translucent" />
<title>SEAWebClient_SelectInstrument</title>
<!-- CSS-Files -->
<link rel="stylesheet" href="cssFiles/SEAWebClientStart.css">
<!-- javascript-Files -->
<script src="jsFiles/SEAWebClientStart.js"></script>
</head>
<body>
<div class="start-panel">
<span class="start-text-wrapper">select instrument</span>
</div>
<div class="start-content">
<!-- div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8850/SEAWebClient.html") -->
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8802/SEAWebClient.html")>HRPT</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8803/SEAWebClient.html")>ZEBRA</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8804/SEAWebClient.html")>POLDI</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8805/SEAWebClient.html")>FOCUS</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8806/SEAWebClient.html")>TASP</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8807/SEAWebClient.html")>RITA2</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8808/SEAWebClient.html")>EIGER</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8809/SEAWebClient.html")>SANS 1</div>
<!-- div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8859/SEAWebClient.html") -->
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8860/SEAWebClient.html")>AMOR</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8861/SEAWebClient.html")>BOA</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8840/SEAWebClient.html")>PREP0</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8841/SEAWebClient.html")>PREP1</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8842/SEAWebClient.html")>PREP2</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8843/SEAWebClient.html")>PREP3</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8844/SEAWebClient.html")>PREP4</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8845/SEAWebClient.html")>PREP5</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8846/SEAWebClient.html")>PREP6</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8847/SEAWebClient.html")>PREP7</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8848/SEAWebClient.html")>PREP8</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8849/SEAWebClient.html")>PREP9</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8850/SEAWebClient.html")>PREPA</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8851/SEAWebClient.html")>PREPB</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8852/SEAWebClient.html")>PREPC</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8853/SEAWebClient.html")>PREPD</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8860/SEAWebClient.html")>LAB0</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8861/SEAWebClient.html")>LAB1</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8862/SEAWebClient.html")>LAB2</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8863/SEAWebClient.html")>LAB3</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8864/SEAWebClient.html")>LAB4</div>
<div class="start-row-links start-link" tabindex = 0 onclick = followLink("http://samenv.psi.ch:8869/SEAWebClient.html")>PPMS</div>
<div class="start-settings">
<div class = start-settings-label>
<span>settings</span>
<span class = "start-settings-show-hide" onclick = toggleSettings()>show</span>
</div>
<div class="start-settings-checkboxes">
<div class="start-row start-row-checkboxes"> <span class="start-left">show graphics?
</span> <span class="start-right"> <input id="check0" name = "sg"
class="start-checkbox" type="checkbox"></input> <label for="check0"
class="start-label"></label>
</span> </div>
<div class="start-row start-row-checkboxes"> <span class="start-left">show console?
</span> <span class="start-right"> <input id="check1" name = "sc"
class="start-checkbox" type="checkbox" checked></input> <label for="check1"
class="start-label"></label>
</span> </div>
<div class="start-row start-row-checkboxes"> <span class="start-left">show overview?
</span> <span class="start-right"> <input id="check2" name = "so"
class="start-checkbox" type="checkbox" checked></input> <label for="check2"
class="start-label"></label>
</span> </div>
<div class="start-row start-row-checkboxes"> <span class="start-left">show main?
</span> <span class="start-right"> <input id="check3" name = "sm"
class="start-checkbox" type="checkbox" checked></input> <label for="check3"
class="start-label"></label>
</span> </div>
<div class="start-row start-row-checkboxes"> <span class="start-left">debug communication?
</span> <span class="start-right"> <input id="check4" name = "dc"
class="start-checkbox" type="checkbox"></input> <label for="check4"
class="start-label"></label>
</span> </div>
<div class="start-row start-row-checkboxes"> <span class="start-left">debug graphics?
</span> <span class="start-right"> <input id="check5" name = "dg"
class="start-checkbox" type="checkbox"></input> <label for="check5"
class="start-label"></label>
</span> </div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -8,7 +8,6 @@
width: 100%; width: 100%;
padding: 26px 0px 0px 0px; padding: 26px 0px 0px 0px;
background-color: darkgray; background-color: darkgray;
display: none;
} }
.commandline { .commandline {
@ -27,12 +26,13 @@
} }
.history { .history {
position: absolute;
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
padding: 80px 8px 50px 8px; padding: 80px 8px 50px 8px;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
background-color: lightgray; background-color: white;
color: #303030; color: black;
} }

View File

@ -37,7 +37,7 @@
width: 100%; width: 100%;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
/* margin-top: 30px; */ margin-top: 30px;
} }
.graph{ .graph{

View File

@ -18,11 +18,17 @@
overflow-y: hidden; overflow-y: hidden;
} }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ .link {
/* panel */ transition: 0.4s;
cursor: pointer;
color: steelblue;
text-decoration: underline;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ .link:focus {
/* row */ color: orangered;
outline: none;
}
.row { .row {
padding: 4px 0px 4px 0px; padding: 4px 0px 4px 0px;
@ -30,207 +36,46 @@
min-height: 24px; min-height: 24px;
display: block; display: block;
border-bottom: dotted darkgray 2px; border-bottom: dotted darkgray 2px;
overflow: hidden;
transition: 0.2s; transition: 0.2s;
position: relative; position: relative;
} }
.row-clickable { .clickable:hover {
cursor: pointer; background-color: lightgray;
} }
.row-clickable:hover { .link-static {
background-color:lightgray; padding-left: 4px;
background-color: #303030;
color: white;
border-bottom: none;
} }
.row-disabled {
background-color: WhiteSmoke;
color: dimgray
}
.row-waiting-for-answer {
background-color: LightGoldenrodYellow;
}
/* ------------------------------ icon-modules ------------------------------ */
.icon-modules {
display: inline-block;
width: 12px;
height: 12px;
line-height: 16px;
opacity: .8;
vertical-align: top;
margin-bottom: 4px;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* left */
.col-left {
display: inline-block;
width: 40%;
vertical-align: top;
}
/* ------------------------------ info ------------------------------ */
.info-box { .info-box {
margin: 4px 0px 4px 0px;
padding: 4px; padding: 4px;
border-radius: 4px; border-radius: 4px;
background-color: #303030; background-color: darkslategray;
color: white; color: white;
display: none;
width: 100%; width: 100%;
display: none;
}
.info-box-visible-by-click {
display: inline-block;
}
.icon-info {
margin-left: 4px;
}
.icon-info:hover + .info-box {
display: inline-block;
}
/* ------------------------------ icon-status ------------------------------ */
.icon-status {
border-radius: 50%;
background-color: lightgray;
margin-right: 4px;
}
.icon-status-idle {
background-color: lightgray;
}
.icon-status-idle {
background-color: white;
}
.icon-status-busy {
background-color: yellow;
}
.icon-status-warn {
background-color: orange;
}
.icon-status-error {
background-color: red;
}
.status-info {
color: white;
background-color: #303030;
position: absolute;
left: 20px;
top: 2px;
padding: 2px;
border-radius: 6px;
display: none;
z-index: 100;
}
.icon-status:hover ~ .status-info {
display: inline-block;
}
/* ------------------------------ pushbutton ------------------------------ */
.push-button-active {
display: inline-block;
height: 20px;
padding: 0 4px 0 4px;
border-radius: 6px;
cursor: pointer; cursor: pointer;
text-align: center;
/* border: 1px solid #303030; */
color: #303030;
background: #dddddd;
/* box-shadow: 2px 4px 4px lightgray; */
} }
.push-button-active:hover { .col-left {
background: whitesmoke; min-height: 24px;
/* box-shadow: 1px 2px 2px dimgray; */ line-height: 24px;
float: left;
} }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ .event-toggle-info {
/* right */ color: darkslategray;
.col-right-modules {
display: inline-block;
width: 60%;
}
.col-right-parameters {
display: inline-block;
width: 60%;
}
.col-right-value {
display: block;
text-align: right;
}
.col-right-value-with-write-permission {
padding-right: 20px;
}
.col-right-disabled {
display: none;
}
/* ------------------------------ edit-icon ------------------------------ */
.icon-edit {
position: absolute;
top: 7px;
right: 0;
cursor: pointer; cursor: pointer;
width: 14px;
height: 14px;
} }
.icon-edit:hover { .col-right {
transform: scale(.8); float: right;
opacity: .6;
}
.icon-edit-hidden {
display: none;
}
/* ------------------------------ okay-icon ------------------------------ */
.icon-okay {
width: 14px;
height: 14px;
margin-top: 7px;
margin-left: 4px;
}
.icon-okay:hover {
transform: scale(.8);
opacity: .6;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* INPUT ELEMENTS */
.input-element {
display: block;
text-align: right;
}
.input-element-hidden {
display: none;
} }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
@ -242,9 +87,6 @@
border: solid 2px dimgray; border: solid 2px dimgray;
color: black; color: black;
text-align: right; text-align: right;
margin-top: 4px;
position: relative;
z-index: 100;
} }
::-ms-clear { /* remove the x in the input box on IE */ ::-ms-clear { /* remove the x in the input box on IE */
@ -253,21 +95,22 @@
} }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* CHECKBOX */ /* CHECKBOX */
/* .parameter-checkbox { .parameter-checkbox {
opacity: 0; opacity: 0;
float: left; float: left;
} */ }
/* .parameter-checkbox + .parameter-label { .parameter-checkbox + .parameter-label {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} */ float: left;
}
/* .parameter-checkbox:focus+.parameter-label { .parameter-checkbox:focus+.parameter-label {
opacity: 0.8; opacity: 0.8;
} */ }
/* .parameter-checkbox+.parameter-label::before { .parameter-checkbox+.parameter-label::before {
content: ' '; content: ' ';
position: absolute; position: absolute;
left: -24px; left: -24px;
@ -277,9 +120,9 @@
display: block; display: block;
background: lightgray; background: lightgray;
border: 2px solid dimgray; border: 2px solid dimgray;
} */ }
/* .parameter-checkbox+.parameter-label::after { .parameter-checkbox+.parameter-label::after {
content: ' '; content: ' ';
position: absolute; position: absolute;
left: -19px; left: -19px;
@ -296,14 +139,14 @@
-webkit-transform: scale(0); -webkit-transform: scale(0);
transform: scale(0); transform: scale(0);
opacity: 0; opacity: 0;
} */ }
/* .parameter-checkbox:checked+.parameter-label::after { .parameter-checkbox:checked+.parameter-label::after {
-ms-transform: scale(1); -ms-transform: scale(1);
-webkit-transform: scale(1); -webkit-transform: scale(1);
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
} */ }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* RADIO */ /* RADIO */
@ -327,25 +170,9 @@ option {
} }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* PANEL <- moved here from SEAWebClientSwiper.css */ /* PUSH-BUTTON */
.panel {
background-color: #303030;
/* position: absolute; */
z-index: 20;
width: 100%;
height: 30px;
}
.panel:not(.graphics) {
color: white;
text-align: center;
padding: 6px 6px 6px 6px;
}
.panel.graphics{ .push-button {
display: flex; border: 2px solid dimgray;
justify-content: flex-end; border-radius: 4px;
}
.toggle-updates-graphics {
float: right;
} }

View File

@ -112,27 +112,6 @@ meta, body {
vertical-align: top; vertical-align: top;
} }
#control_bar {
border: none;
}
.control-global{
width: 20px !important;
height: 20px !important;
margin-top: 2px;
}
.menu-title-container img,
#export-popup-header img {
margin-top: 3px !important;
height: 20px !important;
width: 20px !important;
}
.panel .menu {
border: 1px solid #303030;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* CENTER */ /* CENTER */
#center { #center {
@ -149,90 +128,14 @@ meta, body {
border: solid 4px dimgray; border: solid 4px dimgray;
} }
.grid-container {
width: 100%;
height: 100%;
overflow: hidden;
padding-bottom: 30px;
overflow: hidden;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* PANEL */ /* CLOSE CROSS */
.panel {
background-color: #303030;
/* position: absolute; */
z-index: 20;
width: 100%;
height: 30px;
}
.panel:not(.graphics) {
color: white;
text-align: center;
padding: 6px 6px 6px 6px;
}
.panel.graphics{ #close-cross{
display: flex; z-index: 50;
justify-content: flex-end; top: 9px;
} right: 12px;
.panel-graphics-wide {
padding-right: 28px;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* MAIN ICONS */
.icon-main-container {
z-index: 1001;
position: fixed; position: fixed;
color: white; color: white;
cursor: pointer; cursor: pointer;
}
.icon-main {
height: 18px;
width: 18px;
}
.icon-main:hover {
transform: scale(90%);
transition: .5s;
}
.icon-main-container-hidden {
display: none;
}
/* ----------------------------------------------------------------------------------------------------- */
/* CLOSE CROSS */
.icon-close-container {
top: 9px;
right: 12px;
}
/* ----------------------------------------------------------------------------------------------------- */
/* LOG ICON */
.icon-log-container {
bottom: 9px;
right: 42px;
}
.icon-log-container-hidden {
display: none;
}
/* ----------------------------------------------------------------------------------------------------- */
/* LOCK ICON */
.icon-lock-container {
bottom: 9px;
right: 12px;
}
.icon-lock-container-hidden {
display: none;
} }

View File

@ -0,0 +1,52 @@
@CHARSET "UTF-8";
.swiper-container-main {
width: 100%;
height: 100%;
overflow: hidden;
}
.swiper-slide-main {
background-color: white;
overflow: hidden;
width: 100%;
height: 100%;
padding-bottom: 30px;
}
.swiper-button-prev.swiper-button-disabled,
.swiper-button-next.swiper-button-disabled {
opacity: 0;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
/* PANEL */
.panel {
background-color: #303030;
position: absolute;
z-index: 20;
width: 100%;
height: 30px;
}
.panel:not(.graphics) {
color: white;
text-align: center;
padding: 6px 6px 6px 6px;
}
.panel.graphics{
display: flex;
justify-content: flex-end;
}
.slide-close-icon {
transition: 0.4s;
cursor: pointer;
float: right;
padding-right: 6px;
height: 100%;
fill: white;
}
.toggle-updates-graphics {
float: right;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,678 @@
//'use strict';
import {Chart} from 'chart.js';
import Hammer from 'hammerjs';
var helpers = Chart.helpers;
// Take the zoom namespace of Chart
var zoomNS = Chart.Zoom = Chart.Zoom || {};
// Where we store functions to handle different scale types
var zoomFunctions = zoomNS.zoomFunctions = zoomNS.zoomFunctions || {};
var panFunctions = zoomNS.panFunctions = zoomNS.panFunctions || {};
function resolveOptions(chart, options) {
var deprecatedOptions = {};
if (typeof chart.options.pan !== 'undefined') {
deprecatedOptions.pan = chart.options.pan;
}
if (typeof chart.options.zoom !== 'undefined') {
deprecatedOptions.zoom = chart.options.zoom;
}
var props = chart.$zoom;
options = props._options = helpers.merge({}, [options, deprecatedOptions]);
// Install listeners. Do this dynamically based on options so that we can turn zoom on and off
// We also want to make sure listeners aren't always on. E.g. if you're scrolling down a page
// and the mouse goes over a chart you don't want it intercepted unless the plugin is enabled
var node = props._node;
var zoomEnabled = options.zoom && options.zoom.enabled;
var dragEnabled = options.zoom.drag;
if (zoomEnabled && !dragEnabled) {
node.addEventListener('wheel', props._wheelHandler);
} else {
node.removeEventListener('wheel', props._wheelHandler);
}
if (zoomEnabled && dragEnabled) {
node.addEventListener('mousedown', props._mouseDownHandler);
node.ownerDocument.addEventListener('mouseup', props._mouseUpHandler);
} else {
node.removeEventListener('mousedown', props._mouseDownHandler);
node.removeEventListener('mousemove', props._mouseMoveHandler);
node.ownerDocument.removeEventListener('mouseup', props._mouseUpHandler);
}
}
function storeOriginalOptions(chart) {
var originalOptions = chart.$zoom._originalOptions;
helpers.each(chart.scales, function(scale) {
if (!originalOptions[scale.id]) {
originalOptions[scale.id] = helpers.clone(scale.options);
}
});
helpers.each(originalOptions, function(opt, key) {
if (!chart.scales[key]) {
delete originalOptions[key];
}
});
}
/**
* @param {string} mode can be 'x', 'y' or 'xy'
* @param {string} dir can be 'x' or 'y'
* @param {Chart} chart instance of the chart in question
*/
function directionEnabled(mode, dir, chart) {
if (mode === undefined) {
return true;
} else if (typeof mode === 'string') {
return mode.indexOf(dir) !== -1;
} else if (typeof mode === 'function') {
return mode({chart: chart}).indexOf(dir) !== -1;
}
return false;
}
function rangeMaxLimiter(zoomPanOptions, newMax) {
if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMax &&
!helpers.isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) {
var rangeMax = zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes];
if (newMax > rangeMax) {
newMax = rangeMax;
}
}
return newMax;
}
function rangeMinLimiter(zoomPanOptions, newMin) {
if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMin &&
!helpers.isNullOrUndef(zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes])) {
var rangeMin = zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes];
if (newMin < rangeMin) {
newMin = rangeMin;
}
}
return newMin;
}
function zoomCategoryScale(scale, zoom, center, zoomOptions) {
var labels = scale.chart.data.labels;
var minIndex = scale.min;
var lastLabelIndex = labels.length - 1;
var maxIndex = scale.max;
var sensitivity = zoomOptions.sensitivity;
var chartCenter = scale.isHorizontal() ? scale.left + (scale.width / 2) : scale.top + (scale.height / 2);
var centerPointer = scale.isHorizontal() ? center.x : center.y;
zoomNS.zoomCumulativeDelta = zoom > 1 ? zoomNS.zoomCumulativeDelta + 1 : zoomNS.zoomCumulativeDelta - 1;
if (Math.abs(zoomNS.zoomCumulativeDelta) > sensitivity) {
if (zoomNS.zoomCumulativeDelta < 0) {
if (centerPointer >= chartCenter) {
if (minIndex <= 0) {
maxIndex = Math.min(lastLabelIndex, maxIndex + 1);
} else {
minIndex = Math.max(0, minIndex - 1);
}
} else if (centerPointer < chartCenter) {
if (maxIndex >= lastLabelIndex) {
minIndex = Math.max(0, minIndex - 1);
} else {
maxIndex = Math.min(lastLabelIndex, maxIndex + 1);
}
}
zoomNS.zoomCumulativeDelta = 0;
} else if (zoomNS.zoomCumulativeDelta > 0) {
if (centerPointer >= chartCenter) {
minIndex = minIndex < maxIndex ? minIndex = Math.min(maxIndex, minIndex + 1) : minIndex;
} else if (centerPointer < chartCenter) {
maxIndex = maxIndex > minIndex ? maxIndex = Math.max(minIndex, maxIndex - 1) : maxIndex;
}
zoomNS.zoomCumulativeDelta = 0;
}
scale.options.min = rangeMinLimiter(zoomOptions, labels[minIndex]);
scale.options.max = rangeMaxLimiter(zoomOptions, labels[maxIndex]);
}
}
function zoomNumericalScale(scale, zoom, center, zoomOptions) {
var range = scale.max - scale.min;
var newDiff = range * (zoom - 1);
var centerPoint = scale.isHorizontal() ? center.x : center.y;
var minPercent = (scale.getValueForPixel(centerPoint) - scale.min) / range;
var maxPercent = 1 - minPercent;
var minDelta = newDiff * minPercent;
var maxDelta = newDiff * maxPercent;
console.log("SCOPT", scale.options)
scale.options.min = rangeMinLimiter(zoomOptions, scale.min + minDelta);
scale.options.max = rangeMaxLimiter(zoomOptions, scale.max - maxDelta);
}
function zoomScale(scale, zoom, center, zoomOptions) {
var fn = zoomFunctions[scale.type];
if (fn) {
fn(scale, zoom, center, zoomOptions);
}
}
/**
* @param chart The chart instance
* @param {number} percentZoomX The zoom percentage in the x direction
* @param {number} percentZoomY The zoom percentage in the y direction
* @param {{x: number, y: number}} focalPoint The x and y coordinates of zoom focal point. The point which doesn't change while zooming. E.g. the location of the mouse cursor when "drag: false"
* @param {string} whichAxes `xy`, 'x', or 'y'
* @param {number} animationDuration Duration of the animation of the redraw in milliseconds
*/
function doZoom(chart, percentZoomX, percentZoomY, focalPoint, whichAxes, animationDuration) {
var ca = chart.chartArea;
if (!focalPoint) {
focalPoint = {
x: (ca.left + ca.right) / 2,
y: (ca.top + ca.bottom) / 2,
};
}
var zoomOptions = chart.$zoom._options.zoom;
if (zoomOptions.enabled) {
storeOriginalOptions(chart);
// Do the zoom here
var zoomMode = typeof zoomOptions.mode === 'function' ? zoomOptions.mode({chart: chart}) : zoomOptions.mode;
// Which axe should be modified when figers were used.
var _whichAxes;
if (zoomMode === 'xy' && whichAxes !== undefined) {
// based on fingers positions
_whichAxes = whichAxes;
} else {
// no effect
_whichAxes = 'xy';
}
helpers.each(chart.scales, function(scale) {
if (scale.isHorizontal() && directionEnabled(zoomMode, 'x', chart) && directionEnabled(_whichAxes, 'x', chart)) {
zoomOptions.scaleAxes = 'x';
zoomScale(scale, percentZoomX, focalPoint, zoomOptions);
} else if (!scale.isHorizontal() && directionEnabled(zoomMode, 'y', chart) && directionEnabled(_whichAxes, 'y', chart)) {
// Do Y zoom
zoomOptions.scaleAxes = 'y';
zoomScale(scale, percentZoomY, focalPoint, zoomOptions);
}
});
if (animationDuration) {
// needs to create specific animation mode
if (!chart.options.animation.zoom) {
chart.options.animation.zoom = {
duration: animationDuration,
easing: 'easeOutQuad',
};
}
chart.update('zoom');
} else {
chart.update('none');
}
if (typeof zoomOptions.onZoom === 'function') {
zoomOptions.onZoom({chart: chart});
}
}
}
function panCategoryScale(scale, delta, panOptions) {
var labels = scale.chart.data.labels;
var lastLabelIndex = labels.length - 1;
var offsetAmt = Math.max(scale.ticks.length, 1);
var panSpeed = panOptions.speed;
var minIndex = scale.min;
var step = Math.round(scale.width / (offsetAmt * panSpeed));
var maxIndex;
zoomNS.panCumulativeDelta += delta;
minIndex = zoomNS.panCumulativeDelta > step ? Math.max(0, minIndex - 1) : zoomNS.panCumulativeDelta < -step ? Math.min(lastLabelIndex - offsetAmt + 1, minIndex + 1) : minIndex;
zoomNS.panCumulativeDelta = minIndex !== scale.min ? 0 : zoomNS.panCumulativeDelta;
maxIndex = Math.min(lastLabelIndex, minIndex + offsetAmt - 1);
scale.options.min = rangeMinLimiter(panOptions, labels[minIndex]);
scale.options.max = rangeMaxLimiter(panOptions, labels[maxIndex]);
}
function panNumericalScale(scale, delta, panOptions) {
var scaleOpts = scale.options;
var prevStart = scale.min;
var prevEnd = scale.max;
var newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart) - delta);
var newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd) - delta);
var rangeMin = newMin;
var rangeMax = newMax;
var diff;
if (panOptions.scaleAxes && panOptions.rangeMin &&
!helpers.isNullOrUndef(panOptions.rangeMin[panOptions.scaleAxes])) {
rangeMin = panOptions.rangeMin[panOptions.scaleAxes];
}
if (panOptions.scaleAxes && panOptions.rangeMax &&
!helpers.isNullOrUndef(panOptions.rangeMax[panOptions.scaleAxes])) {
rangeMax = panOptions.rangeMax[panOptions.scaleAxes];
}
console.log("SCOPT pan", scaleOpts);
if (newMin >= rangeMin && newMax <= rangeMax) {
scaleOpts.min = newMin;
scaleOpts.max = newMax;
} else if (newMin < rangeMin) {
diff = prevStart - rangeMin;
scaleOpts.min = rangeMin;
scaleOpts.max = prevEnd - diff;
} else if (newMax > rangeMax) {
diff = rangeMax - prevEnd;
scaleOpts.max = rangeMax;
scaleOpts.min = prevStart + diff;
}
}
function panScale(scale, delta, panOptions) {
var fn = panFunctions[scale.type];
if (fn) {
fn(scale, delta, panOptions);
}
}
function doPan(chartInstance, deltaX, deltaY) {
storeOriginalOptions(chartInstance);
var panOptions = chartInstance.$zoom._options.pan;
if (panOptions.enabled) {
var panMode = typeof panOptions.mode === 'function' ? panOptions.mode({chart: chartInstance}) : panOptions.mode;
helpers.each(chartInstance.scales, function(scale) {
if (scale.isHorizontal() && directionEnabled(panMode, 'x', chartInstance) && deltaX !== 0) {
panOptions.scaleAxes = 'x';
panScale(scale, deltaX, panOptions);
} else if (!scale.isHorizontal() && directionEnabled(panMode, 'y', chartInstance) && deltaY !== 0) {
panOptions.scaleAxes = 'y';
panScale(scale, deltaY, panOptions);
}
});
chartInstance.update('none');
if (typeof panOptions.onPan === 'function') {
panOptions.onPan({chart: chartInstance});
}
}
}
function getXAxis(chartInstance) {
var scales = chartInstance.scales;
var scaleIds = Object.keys(scales);
for (var i = 0; i < scaleIds.length; i++) {
var scale = scales[scaleIds[i]];
if (scale.isHorizontal()) {
return scale;
}
}
}
function getYAxis(chartInstance) {
var scales = chartInstance.scales;
var scaleIds = Object.keys(scales);
for (var i = 0; i < scaleIds.length; i++) {
var scale = scales[scaleIds[i]];
if (!scale.isHorizontal()) {
return scale;
}
}
}
// Store these for later
zoomNS.zoomFunctions.category = zoomCategoryScale;
zoomNS.zoomFunctions.time = zoomNumericalScale;
zoomNS.zoomFunctions.linear = zoomNumericalScale;
zoomNS.zoomFunctions.logarithmic = zoomNumericalScale;
zoomNS.panFunctions.category = panCategoryScale;
zoomNS.panFunctions.time = panNumericalScale;
zoomNS.panFunctions.linear = panNumericalScale;
zoomNS.panFunctions.logarithmic = panNumericalScale;
// Globals for category pan and zoom
zoomNS.panCumulativeDelta = 0;
zoomNS.zoomCumulativeDelta = 0;
// Chartjs Zoom Plugin
var zoomPlugin = {
id: 'zoom',
defaults: {
pan: {
enabled: false,
mode: 'xy',
speed: 20,
threshold: 10
},
zoom: {
enabled: false,
mode: 'xy',
sensitivity: 3,
speed: 0.1
}
},
afterInit: function(chartInstance) {
chartInstance.resetZoom = function() {
storeOriginalOptions(chartInstance);
var originalOptions = chartInstance.$zoom._originalOptions;
helpers.each(chartInstance.scales, function(scale) {
var options = scale.options;
if (originalOptions[scale.id]) {
options.min = originalOptions[scale.id].min;
options.max = originalOptions[scale.id].max;
} else {
delete options.min;
delete options.max;
}
});
chartInstance.update();
};
},
beforeUpdate: function(chart, args, options) {
resolveOptions(chart, options);
},
beforeInit: function(chartInstance, pluginOptions) {
chartInstance.$zoom = {
_originalOptions: {}
};
var node = chartInstance.$zoom._node = chartInstance.ctx.canvas;
resolveOptions(chartInstance, pluginOptions);
var options = chartInstance.$zoom._options;
var panThreshold = options.pan && options.pan.threshold;
chartInstance.$zoom._mouseDownHandler = function(event) {
node.addEventListener('mousemove', chartInstance.$zoom._mouseMoveHandler);
chartInstance.$zoom._dragZoomStart = event;
};
chartInstance.$zoom._mouseMoveHandler = function(event) {
if (chartInstance.$zoom._dragZoomStart) {
chartInstance.$zoom._dragZoomEnd = event;
chartInstance.update('none');
}
};
chartInstance.$zoom._mouseUpHandler = function(event) {
if (!chartInstance.$zoom._dragZoomStart) {
return;
}
node.removeEventListener('mousemove', chartInstance.$zoom._mouseMoveHandler);
var beginPoint = chartInstance.$zoom._dragZoomStart;
var offsetX = beginPoint.target.getBoundingClientRect().left;
var startX = Math.min(beginPoint.clientX, event.clientX) - offsetX;
var endX = Math.max(beginPoint.clientX, event.clientX) - offsetX;
var offsetY = beginPoint.target.getBoundingClientRect().top;
var startY = Math.min(beginPoint.clientY, event.clientY) - offsetY;
var endY = Math.max(beginPoint.clientY, event.clientY) - offsetY;
var dragDistanceX = endX - startX;
var dragDistanceY = endY - startY;
// Remove drag start and end before chart update to stop drawing selected area
chartInstance.$zoom._dragZoomStart = null;
chartInstance.$zoom._dragZoomEnd = null;
var zoomThreshold = (options.zoom && options.zoom.threshold) || 0;
if (dragDistanceX <= zoomThreshold && dragDistanceY <= zoomThreshold) {
return;
}
var chartArea = chartInstance.chartArea;
var zoomOptions = chartInstance.$zoom._options.zoom;
var chartDistanceX = chartArea.right - chartArea.left;
var xEnabled = directionEnabled(zoomOptions.mode, 'x', chartInstance);
var zoomX = xEnabled && dragDistanceX ? 1 + ((chartDistanceX - dragDistanceX) / chartDistanceX) : 1;
var chartDistanceY = chartArea.bottom - chartArea.top;
var yEnabled = directionEnabled(zoomOptions.mode, 'y', chartInstance);
var zoomY = yEnabled && dragDistanceY ? 1 + ((chartDistanceY - dragDistanceY) / chartDistanceY) : 1;
doZoom(chartInstance, zoomX, zoomY, {
x: (startX - chartArea.left) / (1 - dragDistanceX / chartDistanceX) + chartArea.left,
y: (startY - chartArea.top) / (1 - dragDistanceY / chartDistanceY) + chartArea.top
}, undefined, zoomOptions.drag.animationDuration);
if (typeof zoomOptions.onZoomComplete === 'function') {
zoomOptions.onZoomComplete({chart: chartInstance});
}
};
var _scrollTimeout = null;
chartInstance.$zoom._wheelHandler = function(event) {
// Prevent the event from triggering the default behavior (eg. Content scrolling).
if (event.cancelable) {
event.preventDefault();
}
// Firefox always fires the wheel event twice:
// First without the delta and right after that once with the delta properties.
if (typeof event.deltaY === 'undefined') {
return;
}
var rect = event.target.getBoundingClientRect();
var offsetX = event.clientX - rect.left;
var offsetY = event.clientY - rect.top;
var center = {
x: offsetX,
y: offsetY
};
var zoomOptions = chartInstance.$zoom._options.zoom;
var speedPercent = zoomOptions.speed;
if (event.deltaY >= 0) {
speedPercent = -speedPercent;
}
doZoom(chartInstance, 1 + speedPercent, 1 + speedPercent, center);
clearTimeout(_scrollTimeout);
_scrollTimeout = setTimeout(function() {
if (typeof zoomOptions.onZoomComplete === 'function') {
zoomOptions.onZoomComplete({chart: chartInstance});
}
}, 250);
};
if (Hammer) {
var mc = new Hammer.Manager(node);
mc.add(new Hammer.Pinch());
mc.add(new Hammer.Pan({
threshold: panThreshold
}));
// Hammer reports the total scaling. We need the incremental amount
var currentPinchScaling;
var handlePinch = function(e) {
var diff = 1 / (currentPinchScaling) * e.scale;
var rect = e.target.getBoundingClientRect();
var offsetX = e.center.x - rect.left;
var offsetY = e.center.y - rect.top;
var center = {
x: offsetX,
y: offsetY
};
// fingers position difference
var x = Math.abs(e.pointers[0].clientX - e.pointers[1].clientX);
var y = Math.abs(e.pointers[0].clientY - e.pointers[1].clientY);
// diagonal fingers will change both (xy) axes
var p = x / y;
var xy;
if (p > 0.3 && p < 1.7) {
xy = 'xy';
} else if (x > y) {
xy = 'x'; // x axis
} else {
xy = 'y'; // y axis
}
doZoom(chartInstance, diff, diff, center, xy);
var zoomOptions = chartInstance.$zoom._options.zoom;
if (typeof zoomOptions.onZoomComplete === 'function') {
zoomOptions.onZoomComplete({chart: chartInstance});
}
// Keep track of overall scale
currentPinchScaling = e.scale;
};
mc.on('pinchstart', function() {
currentPinchScaling = 1; // reset tracker
});
mc.on('pinch', handlePinch);
mc.on('pinchend', function(e) {
handlePinch(e);
currentPinchScaling = null; // reset
zoomNS.zoomCumulativeDelta = 0;
});
var currentDeltaX = null;
var currentDeltaY = null;
var panning = false;
var handlePan = function(e) {
if (currentDeltaX !== null && currentDeltaY !== null) {
panning = true;
var deltaX = e.deltaX - currentDeltaX;
var deltaY = e.deltaY - currentDeltaY;
currentDeltaX = e.deltaX;
currentDeltaY = e.deltaY;
doPan(chartInstance, deltaX, deltaY);
}
};
mc.on('panstart', function(e) {
currentDeltaX = 0;
currentDeltaY = 0;
handlePan(e);
});
mc.on('panmove', handlePan);
mc.on('panend', function() {
currentDeltaX = null;
currentDeltaY = null;
zoomNS.panCumulativeDelta = 0;
setTimeout(function() {
panning = false;
}, 500);
var panOptions = chartInstance.$zoom._options.pan;
if (typeof panOptions.onPanComplete === 'function') {
panOptions.onPanComplete({chart: chartInstance});
}
});
chartInstance.$zoom._ghostClickHandler = function(e) {
if (panning && e.cancelable) {
e.stopImmediatePropagation();
e.preventDefault();
}
};
node.addEventListener('click', chartInstance.$zoom._ghostClickHandler);
chartInstance._mc = mc;
}
},
beforeDatasetsDraw: function(chartInstance) {
var ctx = chartInstance.ctx;
if (chartInstance.$zoom._dragZoomEnd) {
var xAxis = getXAxis(chartInstance);
var yAxis = getYAxis(chartInstance);
var beginPoint = chartInstance.$zoom._dragZoomStart;
var endPoint = chartInstance.$zoom._dragZoomEnd;
var startX = xAxis.left;
var endX = xAxis.right;
var startY = yAxis.top;
var endY = yAxis.bottom;
if (directionEnabled(chartInstance.$zoom._options.zoom.mode, 'x', chartInstance)) {
var offsetX = beginPoint.target.getBoundingClientRect().left;
startX = Math.min(beginPoint.clientX, endPoint.clientX) - offsetX;
endX = Math.max(beginPoint.clientX, endPoint.clientX) - offsetX;
}
if (directionEnabled(chartInstance.$zoom._options.zoom.mode, 'y', chartInstance)) {
var offsetY = beginPoint.target.getBoundingClientRect().top;
startY = Math.min(beginPoint.clientY, endPoint.clientY) - offsetY;
endY = Math.max(beginPoint.clientY, endPoint.clientY) - offsetY;
}
var rectWidth = endX - startX;
var rectHeight = endY - startY;
var dragOptions = chartInstance.$zoom._options.zoom.drag;
ctx.save();
ctx.beginPath();
ctx.fillStyle = dragOptions.backgroundColor || 'rgba(225,225,225,0.3)';
ctx.fillRect(startX, startY, rectWidth, rectHeight);
if (dragOptions.borderWidth > 0) {
ctx.lineWidth = dragOptions.borderWidth;
ctx.strokeStyle = dragOptions.borderColor || 'rgba(225,225,225)';
ctx.strokeRect(startX, startY, rectWidth, rectHeight);
}
ctx.restore();
}
},
destroy: function(chartInstance) {
if (!chartInstance.$zoom) {
return;
}
var props = chartInstance.$zoom;
var node = props._node;
node.removeEventListener('mousedown', props._mouseDownHandler);
node.removeEventListener('mousemove', props._mouseMoveHandler);
node.ownerDocument.removeEventListener('mouseup', props._mouseUpHandler);
node.removeEventListener('wheel', props._wheelHandler);
node.removeEventListener('click', props._ghostClickHandler);
delete chartInstance.$zoom;
var mc = chartInstance._mc;
if (mc) {
mc.remove('pinchstart');
mc.remove('pinch');
mc.remove('pinchend');
mc.remove('panstart');
mc.remove('pan');
mc.remove('panend');
mc.destroy();
}
}
};
Chart.register(zoomPlugin);
export default zoomPlugin;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
client/externalFiles/d3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

15
client/externalFiles/oldswiper.min.css vendored Normal file

File diff suppressed because one or more lines are too long

18
client/externalFiles/oldswiper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,576 @@
/**
* Swiper 3.4.0
* Most modern mobile touch slider and framework with hardware accelerated transitions
*
* http://www.idangero.us/swiper/
*
* Copyright 2016, Vladimir Kharlampidi
* The iDangero.us
* http://www.idangero.us/
*
* Licensed under MIT
*
* Released on: October 16, 2016
*/
.swiper-container {
margin-left: auto;
margin-right: auto;
position: relative;
overflow: hidden;
/* Fix of Webkit flickering */
z-index: 1;
}
.swiper-container-no-flexbox .swiper-slide {
float: left;
}
.swiper-container-vertical > .swiper-wrapper {
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-flex-direction: column;
-webkit-flex-direction: column;
flex-direction: column;
}
.swiper-wrapper {
position: relative;
width: 100%;
height: 100%;
z-index: 1;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-transition-property: -webkit-transform;
-moz-transition-property: -moz-transform;
-o-transition-property: -o-transform;
-ms-transition-property: -ms-transform;
transition-property: transform;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.swiper-container-android .swiper-slide,
.swiper-wrapper {
-webkit-transform: translate3d(0px, 0, 0);
-moz-transform: translate3d(0px, 0, 0);
-o-transform: translate(0px, 0px);
-ms-transform: translate3d(0px, 0, 0);
transform: translate3d(0px, 0, 0);
}
.swiper-container-multirow > .swiper-wrapper {
-webkit-box-lines: multiple;
-moz-box-lines: multiple;
-ms-flex-wrap: wrap;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
}
.swiper-container-free-mode > .swiper-wrapper {
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
margin: 0 auto;
}
.swiper-slide {
-webkit-flex-shrink: 0;
-ms-flex: 0 0 auto;
flex-shrink: 0;
width: 100%;
height: 100%;
position: relative;
}
/* Auto Height */
.swiper-container-autoheight,
.swiper-container-autoheight .swiper-slide {
height: auto;
}
.swiper-container-autoheight .swiper-wrapper {
-webkit-box-align: start;
-ms-flex-align: start;
-webkit-align-items: flex-start;
align-items: flex-start;
-webkit-transition-property: -webkit-transform, height;
-moz-transition-property: -moz-transform;
-o-transition-property: -o-transform;
-ms-transition-property: -ms-transform;
transition-property: transform, height;
}
/* a11y */
.swiper-container .swiper-notification {
position: absolute;
left: 0;
top: 0;
pointer-events: none;
opacity: 0;
z-index: -1000;
}
/* IE10 Windows Phone 8 Fixes */
.swiper-wp8-horizontal {
-ms-touch-action: pan-y;
touch-action: pan-y;
}
.swiper-wp8-vertical {
-ms-touch-action: pan-x;
touch-action: pan-x;
}
/* Arrows */
.swiper-button-prev,
.swiper-button-next {
position: absolute;
top: 70%;
width: 30px;
height: 30px;
margin-top: -10px;
z-index: 10;
cursor: pointer;
-moz-background-size: 27px 44px;
-webkit-background-size: 27px 44px;
background-size: 27px 44px;
background-position: center;
background-repeat: no-repeat;
opacity: 0.6;
}
.swiper-button-prev.swiper-button-disabled,
.swiper-button-next.swiper-button-disabled {
opacity: 0.2;
cursor: auto;
pointer-events: none;
}
.swiper-button-prev,
.swiper-container-rtl .swiper-button-next {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23007aff'%2F%3E%3C%2Fsvg%3E");
left: 10px;
right: auto;
}
.swiper-button-prev.swiper-button-black,
.swiper-container-rtl .swiper-button-next.swiper-button-black {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E");
}
.swiper-button-prev.swiper-button-white,
.swiper-container-rtl .swiper-button-next.swiper-button-white {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E");
}
.swiper-button-next,
.swiper-container-rtl .swiper-button-prev {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23007aff'%2F%3E%3C%2Fsvg%3E");
right: 10px;
left: auto;
}
.swiper-button-next.swiper-button-black,
.swiper-container-rtl .swiper-button-prev.swiper-button-black {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E");
}
.swiper-button-next.swiper-button-white,
.swiper-container-rtl .swiper-button-prev.swiper-button-white {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E");
}
/* Pagination Styles */
.swiper-pagination {
position: absolute;
text-align: center;
-webkit-transition: 300ms;
-moz-transition: 300ms;
-o-transition: 300ms;
transition: 300ms;
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
-o-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
z-index: 10;
}
.swiper-pagination.swiper-pagination-hidden {
opacity: 0;
}
/* Common Styles */
.swiper-pagination-fraction,
.swiper-pagination-custom,
.swiper-container-horizontal > .swiper-pagination-bullets {
bottom: 10px;
left: 0;
width: 100%;
}
/* Bullets */
.swiper-pagination-bullet {
width: 8px;
height: 8px;
display: inline-block;
border-radius: 100%;
background: #000;
opacity: 0.2;
}
button.swiper-pagination-bullet {
border: none;
margin: 0;
padding: 0;
box-shadow: none;
-moz-appearance: none;
-ms-appearance: none;
-webkit-appearance: none;
appearance: none;
}
.swiper-pagination-clickable .swiper-pagination-bullet {
cursor: pointer;
}
.swiper-pagination-white .swiper-pagination-bullet {
background: #fff;
}
.swiper-pagination-bullet-active {
opacity: 1;
background: #007aff;
}
.swiper-pagination-white .swiper-pagination-bullet-active {
background: #fff;
}
.swiper-pagination-black .swiper-pagination-bullet-active {
background: #000;
}
.swiper-container-vertical > .swiper-pagination-bullets {
right: 10px;
top: 50%;
-webkit-transform: translate3d(0px, -50%, 0);
-moz-transform: translate3d(0px, -50%, 0);
-o-transform: translate(0px, -50%);
-ms-transform: translate3d(0px, -50%, 0);
transform: translate3d(0px, -50%, 0);
}
.swiper-container-vertical > .swiper-pagination-bullets .swiper-pagination-bullet {
margin: 5px 0;
display: block;
}
.swiper-container-horizontal > .swiper-pagination-bullets .swiper-pagination-bullet {
margin: 0 5px;
}
/* Progress */
.swiper-pagination-progress {
background: rgba(0, 0, 0, 0.25);
position: absolute;
}
.swiper-pagination-progress .swiper-pagination-progressbar {
background: #007aff;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
-webkit-transform: scale(0);
-ms-transform: scale(0);
-o-transform: scale(0);
transform: scale(0);
-webkit-transform-origin: left top;
-moz-transform-origin: left top;
-ms-transform-origin: left top;
-o-transform-origin: left top;
transform-origin: left top;
}
.swiper-container-rtl .swiper-pagination-progress .swiper-pagination-progressbar {
-webkit-transform-origin: right top;
-moz-transform-origin: right top;
-ms-transform-origin: right top;
-o-transform-origin: right top;
transform-origin: right top;
}
.swiper-container-horizontal > .swiper-pagination-progress {
width: 100%;
height: 4px;
left: 0;
top: 0;
}
.swiper-container-vertical > .swiper-pagination-progress {
width: 4px;
height: 100%;
left: 0;
top: 0;
}
.swiper-pagination-progress.swiper-pagination-white {
background: rgba(255, 255, 255, 0.5);
}
.swiper-pagination-progress.swiper-pagination-white .swiper-pagination-progressbar {
background: #fff;
}
.swiper-pagination-progress.swiper-pagination-black .swiper-pagination-progressbar {
background: #000;
}
/* 3D Container */
.swiper-container-3d {
-webkit-perspective: 1200px;
-moz-perspective: 1200px;
-o-perspective: 1200px;
perspective: 1200px;
}
.swiper-container-3d .swiper-wrapper,
.swiper-container-3d .swiper-slide,
.swiper-container-3d .swiper-slide-shadow-left,
.swiper-container-3d .swiper-slide-shadow-right,
.swiper-container-3d .swiper-slide-shadow-top,
.swiper-container-3d .swiper-slide-shadow-bottom,
.swiper-container-3d .swiper-cube-shadow {
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
-ms-transform-style: preserve-3d;
transform-style: preserve-3d;
}
.swiper-container-3d .swiper-slide-shadow-left,
.swiper-container-3d .swiper-slide-shadow-right,
.swiper-container-3d .swiper-slide-shadow-top,
.swiper-container-3d .swiper-slide-shadow-bottom {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.swiper-container-3d .swiper-slide-shadow-left {
background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
/* Safari 4+, Chrome */
background-image: -webkit-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Chrome 10+, Safari 5.1+, iOS 5+ */
background-image: -moz-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 3.6-15 */
background-image: -o-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Opera 11.10-12.00 */
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 16+, IE10, Opera 12.50+ */
}
.swiper-container-3d .swiper-slide-shadow-right {
background-image: -webkit-gradient(linear, right top, left top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
/* Safari 4+, Chrome */
background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Chrome 10+, Safari 5.1+, iOS 5+ */
background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 3.6-15 */
background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Opera 11.10-12.00 */
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 16+, IE10, Opera 12.50+ */
}
.swiper-container-3d .swiper-slide-shadow-top {
background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
/* Safari 4+, Chrome */
background-image: -webkit-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Chrome 10+, Safari 5.1+, iOS 5+ */
background-image: -moz-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 3.6-15 */
background-image: -o-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Opera 11.10-12.00 */
background-image: linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 16+, IE10, Opera 12.50+ */
}
.swiper-container-3d .swiper-slide-shadow-bottom {
background-image: -webkit-gradient(linear, left bottom, left top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
/* Safari 4+, Chrome */
background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Chrome 10+, Safari 5.1+, iOS 5+ */
background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 3.6-15 */
background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Opera 11.10-12.00 */
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
/* Firefox 16+, IE10, Opera 12.50+ */
}
/* Coverflow */
.swiper-container-coverflow .swiper-wrapper,
.swiper-container-flip .swiper-wrapper {
/* Windows 8 IE 10 fix */
-ms-perspective: 1200px;
}
/* Cube + Flip */
.swiper-container-cube,
.swiper-container-flip {
overflow: visible;
}
.swiper-container-cube .swiper-slide,
.swiper-container-flip .swiper-slide {
pointer-events: none;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-ms-backface-visibility: hidden;
backface-visibility: hidden;
z-index: 1;
}
.swiper-container-cube .swiper-slide .swiper-slide,
.swiper-container-flip .swiper-slide .swiper-slide {
pointer-events: none;
}
.swiper-container-cube .swiper-slide-active,
.swiper-container-flip .swiper-slide-active,
.swiper-container-cube .swiper-slide-active .swiper-slide-active,
.swiper-container-flip .swiper-slide-active .swiper-slide-active {
pointer-events: auto;
}
.swiper-container-cube .swiper-slide-shadow-top,
.swiper-container-flip .swiper-slide-shadow-top,
.swiper-container-cube .swiper-slide-shadow-bottom,
.swiper-container-flip .swiper-slide-shadow-bottom,
.swiper-container-cube .swiper-slide-shadow-left,
.swiper-container-flip .swiper-slide-shadow-left,
.swiper-container-cube .swiper-slide-shadow-right,
.swiper-container-flip .swiper-slide-shadow-right {
z-index: 0;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-ms-backface-visibility: hidden;
backface-visibility: hidden;
}
/* Cube */
.swiper-container-cube .swiper-slide {
visibility: hidden;
-webkit-transform-origin: 0 0;
-moz-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
width: 100%;
height: 100%;
}
.swiper-container-cube.swiper-container-rtl .swiper-slide {
-webkit-transform-origin: 100% 0;
-moz-transform-origin: 100% 0;
-ms-transform-origin: 100% 0;
transform-origin: 100% 0;
}
.swiper-container-cube .swiper-slide-active,
.swiper-container-cube .swiper-slide-next,
.swiper-container-cube .swiper-slide-prev,
.swiper-container-cube .swiper-slide-next + .swiper-slide {
pointer-events: auto;
visibility: visible;
}
.swiper-container-cube .swiper-cube-shadow {
position: absolute;
left: 0;
bottom: 0px;
width: 100%;
height: 100%;
background: #000;
opacity: 0.6;
-webkit-filter: blur(50px);
filter: blur(50px);
z-index: 0;
}
/* Fade */
.swiper-container-fade.swiper-container-free-mode .swiper-slide {
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
}
.swiper-container-fade .swiper-slide {
pointer-events: none;
-webkit-transition-property: opacity;
-moz-transition-property: opacity;
-o-transition-property: opacity;
transition-property: opacity;
}
.swiper-container-fade .swiper-slide .swiper-slide {
pointer-events: none;
}
.swiper-container-fade .swiper-slide-active,
.swiper-container-fade .swiper-slide-active .swiper-slide-active {
pointer-events: auto;
}
.swiper-zoom-container {
width: 100%;
height: 100%;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-pack: center;
-moz-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
-webkit-box-align: center;
-moz-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
text-align: center;
}
.swiper-zoom-container > img,
.swiper-zoom-container > svg,
.swiper-zoom-container > canvas {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* Scrollbar */
.swiper-scrollbar {
border-radius: 10px;
position: relative;
-ms-touch-action: none;
background: rgba(0, 0, 0, 0.1);
}
.swiper-container-horizontal > .swiper-scrollbar {
position: absolute;
left: 1%;
bottom: 3px;
z-index: 50;
height: 5px;
width: 98%;
}
.swiper-container-vertical > .swiper-scrollbar {
position: absolute;
right: 3px;
top: 1%;
z-index: 50;
width: 5px;
height: 98%;
}
.swiper-scrollbar-drag {
height: 100%;
width: 100%;
position: relative;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
left: 0;
top: 0;
}
.swiper-scrollbar-cursor-drag {
cursor: move;
}
/* Preloader */
.swiper-lazy-preloader {
width: 42px;
height: 42px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -21px;
margin-top: -21px;
z-index: 10;
-webkit-transform-origin: 50%;
-moz-transform-origin: 50%;
transform-origin: 50%;
-webkit-animation: swiper-preloader-spin 1s steps(12, end) infinite;
-moz-animation: swiper-preloader-spin 1s steps(12, end) infinite;
animation: swiper-preloader-spin 1s steps(12, end) infinite;
}
.swiper-lazy-preloader:after {
display: block;
content: "";
width: 100%;
height: 100%;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%236c6c6c'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
background-position: 50%;
-webkit-background-size: 100%;
background-size: 100%;
background-repeat: no-repeat;
}
.swiper-lazy-preloader-white:after {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%23fff'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
}
@-webkit-keyframes swiper-preloader-spin {
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes swiper-preloader-spin {
100% {
transform: rotate(360deg);
}
}

File diff suppressed because it is too large Load Diff

12
client/externalFiles/swiper.min.css vendored Normal file

File diff suppressed because one or more lines are too long

13
client/externalFiles/swiper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 318 B

BIN
client/favicon192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -10,9 +10,9 @@ var timeoutID; // We need this ID to reset the timer every 30 seconds
function buildUpdateConnection() { function buildUpdateConnection() {
// Establishes server-sent-event-connection, which is used for all sorts of // Establishes server-sent-event-connection, which is used for all sorts of
// updates and exists as long as the client is running. // updates and exists as long as the client is running.
// Executed at program start (see also SEAWebClientMain.js). // Executed at programstart (see also SEAWebClientMain.js).
var path = "http://" + hostPort + "/update?" + window.clientTags; var path = "http://" + hostPort + "/update";
if (debugCommunication) { if (debugCommunication) {
console.log("%cto server (SSE): " + path, console.log("%cto server (SSE): " + path,
"color:white;background:lightblue"); "color:white;background:lightblue");
@ -22,7 +22,8 @@ function buildUpdateConnection() {
var src = new EventSource(path); var src = new EventSource(path);
} catch (e) { } catch (e) {
console.log(e) console.log(e)
alertify.prompt("NETWORK ERROR", alertify.prompt(
"NETWORK ERROR",
"Failed to establish connection to data-server at the given address!" "Failed to establish connection to data-server at the given address!"
+ "Try to enter HOST and PORT of the data-server manually!", + "Try to enter HOST and PORT of the data-server manually!",
hostPort, function(evt, value) { hostPort, function(evt, value) {
@ -42,7 +43,9 @@ function buildUpdateConnection() {
src.onerror = function(e) { src.onerror = function(e) {
console.log(e); console.log(e);
console.log('EVTSRC error') console.log('EVTSRC error')
alertify.prompt("NETWORK ERROR", alertify
.prompt(
"NETWORK ERROR",
"Failed to establish connection to data-server at the given address!" "Failed to establish connection to data-server at the given address!"
+ "Try to enter HOST and PORT of the data-server manually!", + "Try to enter HOST and PORT of the data-server manually!",
hostPort, function(evt, value) { hostPort, function(evt, value) {
@ -56,6 +59,7 @@ function buildUpdateConnection() {
function handleUpdateMessage(src, message) { function handleUpdateMessage(src, message) {
// Handles incoming SSE-messages depending on type of message. // Handles incoming SSE-messages depending on type of message.
if (debugCommunication > 1) { if (debugCommunication > 1) {
console.log("%cfrom server (SSE): " + message.type, console.log("%cfrom server (SSE): " + message.type,
"color:white;background:lightgray", message); "color:white;background:lightgray", message);
@ -69,27 +73,25 @@ function handleUpdateMessage(src, message) {
// id-message: Confirms establishment of SSE-connection and determines // id-message: Confirms establishment of SSE-connection and determines
// specific ID of the client // specific ID of the client
case "id": case "id":
for (var i = 0; i < swiper.length; i++) {
swiper[i].removeAllSlides();
}
clientID = message.id; clientID = message.id;
if ("device" in message) { if ("device" in message) {
if (message.device == "_inst_select") { if (message.device == "_inst_select") {
window.clientTitle = "select instrument"; clientTitle = "select instrument";
console.log('IDselect') console.log('IDselect')
pushInitCommand("getblock?path=_inst_select&", "instrument selection"); pushInitCommand("getblock?path=_inst_select&", "instrument selection");
menuMode = true;
sizeChange(); sizeChange();
} else { } else {
if (message.instrument) { clientTitle = message.instrument + " " + message.device;
window.instrument = message.instrument; console.log('loadBlocks', message);
}
if (message.device) {
window.device = message.device;
}
window.clientTitle = window.instrument + " " + window.device;
// console.log('loadBlocks', message);
loadFirstBlocks(); loadFirstBlocks();
} }
document.title = clientTitle; document.title = "SEA "+clientTitle;
} else { } else {
document.title = clientTitle + " " + message.title; document.title = "SEA "+clientTitle + " " + message.title;
} }
var header = document.getElementById("header"); var header = document.getElementById("header");
header.style.width = 'auto'; header.style.width = 'auto';
@ -99,7 +101,7 @@ function handleUpdateMessage(src, message) {
device.style.width = 'auto' device.style.width = 'auto'
instrument.innerHTML = message.instrument instrument.innerHTML = message.instrument
device.innerHTML = message.device device.innerHTML = message.device
// console.log('ID', initCommands); console.log('ID', initCommands);
nextInitCommand(); nextInitCommand();
break; break;
// console-update-message: Confirms a command. // console-update-message: Confirms a command.
@ -167,7 +169,7 @@ function handleUpdateMessage(src, message) {
if (debugCommunication > 1) { if (debugCommunication > 1) {
console.log(message); console.log(message);
} }
handleUpdate(message, src); updateValues(message, src);
break; break;
} }
} }
@ -194,113 +196,47 @@ function resetTimer(src) {
}, 60000); }, 60000);
} }
function handleUpdate(message, src) { function updateValues(message, src) {
// Handles changes of parameter-values // Handles changes of parameter-values
for (var i = 0; i < message.updates.length; i++) { for (var i = 0; i < message.updates.length; i++) {
let component = message.updates[i]; var component = message.updates[i];
var value = component.value;
// Check for status updates var matches = document.getElementsByName(component.name);
if (component.name.split(":")[1] == 'status') {
updateStatus(component);
}
// Check for target updates in the module block
if (component.name.split(":")[1] == 'target') {
updateTarget(component);
}
updateValue(component); for (var j = 0; j < matches.length; j++) {
} var type = matches[j].__ctype__;
} if (type == "rdonly") {
var text = htmlEscape(value);
function updateTarget(component) { if (text) {
let matches = document.getElementsByName(component.name); matches[j].innerHTML = text;
let elem = matches[0]; // Should be the only match }
// elem.value = component.value; } else if (type == "input") {
let row = elem.closest('div'); var row = matches[j].parentNode.parentNode.parentNode;
row.classList.remove('row-waiting-for-answer'); row.style.backgroundColor = "white";
var mval = matches[j].value;
elem.actualValue = component.value; var oldValue = ('oldValue' in matches[j]) ? matches[j].oldValue : mval;
if(elem.__ctype__ == 'input') { if (value != mval && parseFloat(value) != parseFloat(mval) && value != oldValue) {
resizeTextfield(elem); if (matches[j] == document.activeElement
} || oldValue != mval) {
} row.style.backgroundColor = "orange";
} else {
function updateStatus(component) { matches[j].value = value;
let matches = document.getElementsByName(component.name); }
let status_icon = matches[0]; }
let row = status_icon.closest(".row"); matches[j].actualValue = value;
let right = row.lastChild; resizeTextfield(matches[j]);
let statusCode = component.statuscode; } else if (type == "checkbox") {
var row = matches[j].parentNode.parentNode;
// Update status info, visible when mouse cursor is hovering over status icon row.style.backgroundColor = "white";
let status_info = document.getElementsByName(component.name.split(":")[0] + '-info')[0]; // console.log('CBX', matches[j].name, message, Boolean(value && value != 'false'));
if(status_info) { matches[j].checked = Boolean(value && value != 'false');
status_info.innerHTML = component.formatted; } else if (type == "enum") {
} matches[j].style.display = "block";
var row = matches[j].parentNode.parentNode;
status_icon.classList.remove('icon-status-disabled', 'icon-status-idle', 'icon-status-warn', 'icon-status-busy', 'icon-status-error'); row.style.backgroundColor = "white";
row.classList.remove('row-disabled'); matches[j].value = value;
right.classList.remove = 'col-right-disabled';
switch (statusCode) {
case 0:
status_icon.classList.add('icon-status-disabled');
row.classList.add('row-disabled');
right.classList.add = 'col-right-disabled';
break;
case 1:
status_icon.classList.add('icon-status-idle');
break;
case 2:
status_icon.classList.add('icon-status-warn');
break;
case 3:
status_icon.classList.add('icon-status-busy');
break;
case 4:
status_icon.classList.add('icon-status-error');
break;
}
}
function updateValue(component) {
let matches = document.getElementsByName(component.name);
for (var j = 0; j < matches.length; j++) {
let elem = matches[j];
let type = elem.__ctype__; // -> Show Dom-Properties
if (type == "rdonly") {
let text = htmlEscape(component.formatted);
if (text) {
elem.innerHTML = text;
} }
} else if (type == "input") {
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
elem.actualValue = component.value;
resizeTextfield(elem);
} else if (type == "checkbox") {
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
if (component.value == 'False' || component.value == 'false' || component.value == 0) {
elem.checked = false;
} else {
elem.checked = true;
}
} else if (type == "enum") {
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
// let options = elem.childNodes;
// for (var j = 0; j < options.length; j++) {
// if (options[j].label == component.value) {
// elem.value = j + 1;
// }
// }
} else if (type == "none") {
// pushbutton (e.g. stop command)
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
} }
} }
} }
@ -312,8 +248,8 @@ function reqJSON(s, url, successHandler, errorHandler) {
var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest() var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP'); : new ActiveXObject('Microsoft.XMLHTTP');
if (debugCommunication) { if (debugCommunication) {
console.log("%cto server (reqJSON): %s", console.log("%cto server (reqJSON): " + url,
"color:white;background:darkgreen", url); "color:white;background:lightgreen");
} }
xhr.open('get', url, true); xhr.open('get', url, true);
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
@ -338,8 +274,8 @@ function reqJSONPOST(s, url, parameters, successHandler, errorHandler) {
var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest() var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP'); : new ActiveXObject('Microsoft.XMLHTTP');
if (debugCommunication) { if (debugCommunication) {
console.log("%cto server (reqJSONPOST): %s", console.log("%cto server (reqJSON): " + url,
"color:white;background:lightgreen", url); "color:white;background:lightgreen");
} }
xhr.open('post', url, true); xhr.open('post', url, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
@ -361,8 +297,10 @@ function reqJSONPOST(s, url, parameters, successHandler, errorHandler) {
xhr.send(parameters); xhr.send(parameters);
} }
function successHandler(s, message) { function successHandler(s, message) {
// Handles incoming XMLHttp-messages depending on type of message. // Handles incoming XMLHttp-messages depending on type of message.
// s: slide number or -1 for replacing slide in all slider instances
if (debugCommunication) { if (debugCommunication) {
console.log("%cfrom server (reqJSON): " + message.type, console.log("%cfrom server (reqJSON): " + message.type,
"color:white;background:dimgray", message); "color:white;background:dimgray", message);
@ -370,18 +308,39 @@ function successHandler(s, message) {
switch (message.type) { switch (message.type) {
// Response to a "getblock"-server-request. // Response to a "getblock"-server-request.
case "draw": case "draw":
if (debugCommunication) {
console.log(message);
}
if (message.path == "main") { if (message.path == "main") {
// Happens only initially or at device change. // Happens only initially or at device change.
appendToGridElement(1, message.title, "main", createContent(message)); for (var sLocal = 0; sLocal < 2; sLocal++) { // was up to MAXBLOCK
// appendToGridElement(2, "", "parameters", createContent({components:[]})); insertSlide(sLocal, message.title, "main", createContent(
sLocal, message));
}
insertSlide(2, "", "parameters", createContent(2, {components:[]}));
} else { } else {
// In the module-block a parameter was selected if (s < 0) { // redraw: check for slides in all swiper instances
showParams = true; // not used any more?
// -> write parameter-block to grid-element2 for (var isw = 0; isw < MAXBLOCK; isw ++) {
isl = appendToGridElement(2, message.title, 'parameters', createContent(message)); var isl = findSlide(isw, message.path);
adjustGrid(); if (isl !== null) {
if (nColumns == 1 || nColumns == 2 || nColumns == 3) { var slide = swiper[isw].slides[isl];
document.getElementsByClassName('icon-close-container')[0].innerHTML = '<img class = "icon-main icon-close" src="res/icon_close.png">'; if (slide) {
console.log("redraw", isw, isl);
replaceSlideContent(slide, message.title,
createContent(isw, message));
}
}
}
} else if (message.path == '_overview') {
// remove comment of next line when you want overview _instead_ of Graphics
// isl = insertSlide(s, message.title, "_overview", createContent(sLocal, message));
// swiper[sLocal].slideTo(isl); /* go to found slide */
} else {
// insertSlide(s, message.title, message.path, createContent(s, message));
let sLocal = paramSlider[s];
isl = insertSlide(sLocal, message.title, 'parameters', createContent(sLocal, message));
swiper[sLocal].slideTo(isl); /* go to found slide */
} }
} }
nextInitCommand(); nextInitCommand();
@ -397,8 +356,11 @@ function successHandler(s, message) {
break; break;
// Response to a "console"-server-request. // Response to a "console"-server-request.
case "accept-console": case "accept-console":
// draw console only to the last grid-element // draw console, only on the first and the last swiper
appendToGridElement(3, "console", "console",createContentConsole(3)); insertSlide(0, "console", "console",
createContentConsole(sLocal));
insertSlide(3, "console", "console",
createContentConsole(sLocal));
nextInitCommand(); nextInitCommand();
// send empty command in order to trigger getting history // send empty command in order to trigger getting history
reqJSON(0, "http://" + hostPort + "/sendcommand?command=&id=" + clientID, successHandler, reqJSON(0, "http://" + hostPort + "/sendcommand?command=&id=" + clientID, successHandler,
@ -409,17 +371,15 @@ function successHandler(s, message) {
timeRange = message.time; timeRange = message.time;
/*createGraphics(); /*createGraphics();
// By default mostleft swiper-instance shows graphics. // By default mostleft swiper-instance shows graphics.
swiper[0].slideTo(0);
// Update time-selection. (see also SEAWebClientGraphics.js) // Update time-selection. (see also SEAWebClientGraphics.js)
var select = document.getElementsByClassName("select-time")[0]; var select = document.getElementsByClassName("select-time")[0];
begin = timeRange[0] - timeRange[1]; begin = timeRange[0] - timeRange[1];
select.value = begin; select.value = begin;
// Server-request for variable-list.*/ // Server-request for variable-list.*/
// console.log('TIME', timeRange) reqJSONPOST(0, "http://" + hostPort + "/getvars", "time=" + timeRange[1] + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="
reqJSONPOST(0, "http://" + hostPort + "/getvars", + clientID, successHandler, errorHandler);
"time=" + timeRange[0] + ',' + timeRange[1]
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
+ "&id=" + clientID, successHandler, errorHandler);
break; break;
// Response to a "getvars"-server-request. // Response to a "getvars"-server-request.
case "var_list": case "var_list":
@ -439,7 +399,6 @@ function successHandler(s, message) {
nextInitCommand(); nextInitCommand();
}*/ }*/
// graphs.receivedVars(message.blocks); // graphs.receivedVars(message.blocks);
document.getElementById("device").innerHTML = message.device
graphs.initGraphs(message.blocks); graphs.initGraphs(message.blocks);
nextInitCommand(); nextInitCommand();
break; break;
@ -459,11 +418,6 @@ function successHandler(s, message) {
// Response to a "updategraph"-server-request. // Response to a "updategraph"-server-request.
case "accept-graph": case "accept-graph":
break; break;
case "accept-command":
if (message.result) {
updateValue(message.result);
}
break;
case "error": case "error":
console.log("%cError-Message received!", "color:white;background:red"); console.log("%cError-Message received!", "color:white;background:red");
console.log(message); console.log(message);

View File

@ -48,7 +48,7 @@ function createContentConsole(s) {
histIndex = -1; histIndex = -1;
// Request for command. // Request for command.
reqJSON(s, "http://" + hostPort + "/sendcommand?command=" reqJSON(s, "http://" + hostPort + "/sendcommand?command="
+ encodeURIComponent(commandline.value) + "&id=" + clientID, successHandler, + commandline.value + "&id=" + clientID, successHandler,
errorHandler); errorHandler);
commandline.value = ""; commandline.value = "";
}; };

View File

@ -223,8 +223,7 @@ let globalControls = (function (){
controlBar.id = "control_bar"; controlBar.id = "control_bar";
panel.appendChild(controlBar); panel.appendChild(controlBar);
// let xyControl = new Control("res/x_zoom_white_wide.png", "res/y_zoom_white_wide.png", "Time<->Y zoom (one graph)", graphs.toggleZoomMode, graphs.toggleZoomMode); let xyControl = new Control("res/x_zoom_white_wide.png", "res/y_zoom_white_wide.png", "Time<->Y zoom (one graph)", graphs.toggleZoomMode, graphs.toggleZoomMode);
let xyControl = new Control("res/icon_width.png", "res/icon_height.png", "Time<->Y zoom (one graph)", graphs.toggleZoomMode, graphs.toggleZoomMode);
controlBar.appendChild(xyControl); controlBar.appendChild(xyControl);
@ -341,8 +340,7 @@ function loadGraphicsMenu(panel){
menuGraphicsPopup.addEntry(aboutCurvesSettingsHelpEntry); menuGraphicsPopup.addEntry(aboutCurvesSettingsHelpEntry);
menuGraphicsPopup.addEntry(aboutTopRightHandCornerCrossHelpEntry); menuGraphicsPopup.addEntry(aboutTopRightHandCornerCrossHelpEntry);
// let graphicsMenuControl = new Control("res/menu_white_wide.png", "res/menu_white_wide.png", "Menu", () => { let graphicsMenuControl = new Control("res/menu_white_wide.png", "res/menu_white_wide.png", "Menu", () => {
let graphicsMenuControl = new Control("res/icon_menu_graphics.png", "res/icon_menu_graphics.png", "Menu", () => {
datesPopup.hide(); datesPopup.hide();
exportPopup.hide(); exportPopup.hide();
curvesSettingsPopup.hide(); curvesSettingsPopup.hide();
@ -351,10 +349,9 @@ function loadGraphicsMenu(panel){
panel.appendChild(menuGraphicsPopup); panel.appendChild(menuGraphicsPopup);
menuGraphicsPopup.getContainer().style.top = "28px"; menuGraphicsPopup.getContainer().style.top = "28px";
menuGraphicsPopup.getContainer().style.right = "20px"; menuGraphicsPopup.getContainer().style.right = "20px";
menuGraphicsPopup.style.position = "absolute";
panel.appendChild(graphicsMenuControl); panel.appendChild(graphicsMenuControl);
graphicsMenuControl.style.marginLeft="0px"; graphicsMenuControl.style.marginLeft="6px";
graphicsMenuControl.style.marginRight="8px"; graphicsMenuControl.style.marginRight="22px";
graphicsMenuControl.style.marginTop="2px"; graphicsMenuControl.style.marginTop="2px";
} }
@ -374,9 +371,10 @@ function loadExportPopup(){
*/ */
function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){ function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){
if (binning === null || binning == "None") let binningParam = "None";
binning = ""; if (binning !== null)
let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binning + "&id=" + clientID binningParam = binning
let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binningParam + "&id=" + clientID
let a = document.createElement('a'); let a = document.createElement('a');
a.href = exportURL a.href = exportURL
a.download = true a.download = true
@ -419,7 +417,7 @@ let graphs = (function (){
let minTime, maxTime; // the queried time range let minTime, maxTime; // the queried time range
let lastTime = 0; // time of most recent data point let lastTime = 0; // time of most recent data point
// let resolution = undefined; // current window resolution (ms/pixel) let resolution = undefined; // current window resolution (ms/pixel)
let activateUpdateTimeout = undefined; // timeout for the activateUpdates function let activateUpdateTimeout = undefined; // timeout for the activateUpdates function
let updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback) let updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback)
@ -493,7 +491,7 @@ let graphs = (function (){
if(idx != -1){ //if the clicked block is displayed somewhere, we create a selection window if(idx != -1){ //if the clicked block is displayed somewhere, we create a selection window
createSelection(idx); // We will create a selection at the gindex createSelection(idx); // We will create a selection at the gindex
} }
} }
createGraph(gindex, block); // we create at the current shown selector (gindex), the graph corresponding to the one clicked (block) createGraph(gindex, block); // we create at the current shown selector (gindex), the graph corresponding to the one clicked (block)
}) })
selection.appendChild(bel); selection.appendChild(bel);
@ -513,13 +511,11 @@ let graphs = (function (){
if (liveMode && cursorLinePos === null) if (liveMode && cursorLinePos === null)
// gotoNowElm.innerHTML = ''; // gotoNowElm.innerHTML = '';
// globalControls.getControlsMap()[goToNowKey].changeToAlt(); // globalControls.getControlsMap()[goToNowKey].changeToAlt();
// console.log("Need to change to nothing"); console.log("Need to change to nothing");
;
else else
// gotoNowElm.innerHTML = 'go to now'; // gotoNowElm.innerHTML = 'go to now';
// globalControls.getControlsMap()[goToNowKey].changeToMain(); // globalControls.getControlsMap()[goToNowKey].changeToMain();
// console.log("Need to change to seen"); console.log("Need to change to seen");
;
} }
/** /**
@ -530,7 +526,7 @@ let graphs = (function (){
* @param {{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}} block - The information of the block to create * @param {{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}} block - The information of the block to create
*/ */
function createGraph(gindex, block){ function createGraph(gindex, block){
// console.log("clear for create graph", gindex) console.log("clear for create graph", gindex)
clear(gindex); clear(gindex);
tag_dict[block.tag] = gindex; tag_dict[block.tag] = gindex;
let dict = {} // {string: [name:string, label:string, color:string]} let dict = {} // {string: [name:string, label:string, color:string]}
@ -544,13 +540,12 @@ let graphs = (function (){
varlist = vars_array[gindex]; varlist = vars_array[gindex];
let graph_elm = graph_elm_array[gindex]; let graph_elm = graph_elm_array[gindex];
resolution = getResolution((maxTime - minTime) / 1000) timeDeltaAxis = maxTime - minTime
setResolution(timeDeltaAxis)
AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 + "&variables=" + varlist + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){
+ "&variables=" + varlist
+ "&interval=" + resolution //console.log('Graph', block, data)
+ "&id=" + clientID).getJSON().then(function(data){
// console.log('Graph', block, data);
let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type); let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
graph_array[gindex] = graph; graph_array[gindex] = graph;
@ -562,6 +557,7 @@ let graphs = (function (){
for(let e of data.graph[key]){ for(let e of data.graph[key]){
pdata.push({x: e[0]*1000, y: e[1]}); pdata.push({x: e[0]*1000, y: e[1]});
} }
addDataset(gindex, key, pdata, dict[key]) addDataset(gindex, key, pdata, dict[key])
// if(pdata.length > 0){ // if(pdata.length > 0){
// addDataset(gindex, key, pdata, dict[key]) // addDataset(gindex, key, pdata, dict[key])
@ -779,12 +775,10 @@ let graphs = (function (){
max = max/1000; max = max/1000;
} }
resolution = getResolution((currentMaxTime - currentMinTime) / 1000) timeDelta = currentMaxTime - currentMinTime
setResolution(timeDelta)
AJAX("http://" + hostPort + "/graph?time=" + min + ","+max AJAX("http://" + hostPort + "/graph?time=" + min + ","+max+"&variables=" + variables() + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){
+"&variables=" + variables()
+ "&interval=" + resolution
+ "&id=" + clientID).getJSON().then(function(data){
for(let key in data.graph){ for(let key in data.graph){
let pdata = []; let pdata = [];
for(let e of data.graph[key]){ for(let e of data.graph[key]){
@ -897,10 +891,10 @@ let graphs = (function (){
* Sets the resolution of the viewing window in milliseconds * Sets the resolution of the viewing window in milliseconds
* @param {*} timeDelta - The difference between the maximum time and the minimum time of the window * @param {*} timeDelta - The difference between the maximum time and the minimum time of the window
*/ */
function getResolution(timeDelta){ function setResolution(timeDelta){
return Math.ceil((timeDelta / container.getBoundingClientRect().width)) resolution = Math.ceil((timeDelta / container.getBoundingClientRect().width))
} }
/** /**
* The callback to be called when the user click on the "Jump" button of the date selector * The callback to be called when the user click on the "Jump" button of the date selector
* Gets the vars + device name for the selected date+time, then rebuilds the graphs * Gets the vars + device name for the selected date+time, then rebuilds the graphs
@ -919,10 +913,7 @@ let graphs = (function (){
msRightTimestampGetVars = dateTimestampMs + timeValueMs; msRightTimestampGetVars = dateTimestampMs + timeValueMs;
msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000; msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000;
AJAX("http://" + hostPort + "/getvars").postForm( AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestampGetVars/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){
"time=" + msRightTimestampGetVars/1000
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
+ "&id="+ clientID).then(function(data){
blocks = data.blocks; blocks = data.blocks;
document.getElementById("device").innerHTML = data.device document.getElementById("device").innerHTML = data.device
maxTime = msRightTimestampGetGraph; maxTime = msRightTimestampGetGraph;
@ -996,10 +987,7 @@ let graphs = (function (){
window["wideGraphs"] = false; // will have no effect if hideRightPart is true window["wideGraphs"] = false; // will have no effect if hideRightPart is true
adjustGrid(); adjustGrid();
AJAX("http://" + hostPort + "/getvars").postForm( AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestamp/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){
"time=" + msRightTimestamp/1000 + "&userconfiguration="
+ JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
+ "&id="+ clientID).then(function(data){
currentMaxTime = msRightTimestamp + 60000; currentMaxTime = msRightTimestamp + 60000;
currentMinTime = msLeftTimestamp; currentMinTime = msLeftTimestamp;
@ -1021,7 +1009,18 @@ let graphs = (function (){
function buildGraphicsUI(){ function buildGraphicsUI(){
let f = 0; let f = 0;
appendToGridElement(f, " ", "graphics", container); insertSlide(f, " ", "graphics", container);
let currentSwiper = swiper[f];
function setSlidingMode(mode) {
currentSwiper.params.noSwipingClass = mode ? "allow-swipe" : "swiper-slide-main";
}
currentSwiper.enableSwiping(false);
currentSwiper.on('reachBeginning', function () {
currentSwiper.enableSwiping(false);
})
let graphicsPanel = container.parentNode.querySelector('.panel') let graphicsPanel = container.parentNode.querySelector('.panel')
graphicsPanel.classList.add('graphics'); graphicsPanel.classList.add('graphics');
@ -1062,7 +1061,7 @@ let graphs = (function (){
currentMaxTime = maxTime; currentMaxTime = maxTime;
currentMinTime = minTime; currentMinTime = minTime;
} }
AJAX("http://" + hostPort + "/gettime?time=" + window['timerange'] + "&id="+ clientID).getJSON().then(function(data){ AJAX("http://" + hostPort + "/gettime?time=-1800,0&id="+ clientID).getJSON().then(function(data){
startTime = data.time[1]*1000; startTime = data.time[1]*1000;
maxTime = startTime; maxTime = startTime;
currentMaxTime = maxTime + 60000; currentMaxTime = maxTime + 60000;
@ -1113,7 +1112,7 @@ let graphs = (function (){
"/updategraph?" + "/updategraph?" +
"id=" + clientID).getJSON().then(function(data) { "id=" + clientID).getJSON().then(function(data) {
setLiveMode(data.live); setLiveMode(data.live);
// console.log('LIVE create', liveMode) console.log('LIVE create', liveMode)
}) })
} }
@ -1290,10 +1289,7 @@ let graphs = (function (){
function applySettingsCallback(userConfiguration){ function applySettingsCallback(userConfiguration){
cursorLine(null); cursorLine(null);
AJAX("http://" + hostPort + "/getvars").postForm( AJAX("http://" + hostPort + "/getvars").postForm("time=" + currentMaxTime/1000 + "&userconfiguration=" + JSON.stringify(userConfiguration) + "&id="+ clientID).then(function(data){
"time=" + currentMaxTime/1000
+ "&userconfiguration=" + JSON.stringify(userConfiguration)
+ "&id="+ clientID).then(function(data){
blocks = data.blocks; blocks = data.blocks;
document.getElementById("device").innerHTML = data.device document.getElementById("device").innerHTML = data.device
maxTime = currentMaxTime; maxTime = currentMaxTime;
@ -1671,7 +1667,6 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
legend.style.display = 'none'; legend.style.display = 'none';
let margin = 10; let margin = 10;
let linewidth = 3;
canvas.addEventListener('mouseover', function(e){ canvas.addEventListener('mouseover', function(e){
graphs.bringToFront(legend); graphs.bringToFront(legend);
@ -1699,7 +1694,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
* @param {string} to - The zoom mode to set * @param {string} to - The zoom mode to set
*/ */
function setZoomMode(to){ function setZoomMode(to){
chart.options.plugins.zoom.zoom.mode = to; chart.options.zoom.mode = to;
} }
// Unused // Unused
@ -1724,7 +1719,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
function addDataset(key, data, opts){ function addDataset(key, data, opts){
let dataset_index = chart.data.datasets.length; let dataset_index = chart.data.datasets.length;
chart.data.datasets.push({data: data, label: opts.label, key: key, chart.data.datasets.push({data: data, label: opts.label, key: key,
spanGaps: false, borderJoinStyle: 'bevel', borderWidth: linewidth, stepped: opts.period == 0, spanGaps: false, lineJoin: 'round', borderWidth: 2, steppedLine: opts.period == 0,
borderColor: opts.color,fill: false, pointRadius: 0, tension:0, showLine: true}); borderColor: opts.color,fill: false, pointRadius: 0, tension:0, showLine: true});
let dataset = chart.data.datasets[dataset_index]; let dataset = chart.data.datasets[dataset_index];
@ -1745,7 +1740,6 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
color.appendChild(colorline); color.appendChild(colorline);
colorline.classList.add('colorline'); colorline.classList.add('colorline');
colorline.style.backgroundColor = dataset.borderColor; colorline.style.backgroundColor = dataset.borderColor;
colorline.style.height = linewidth + 'px';
dlabel.innerHTML = dataset.label; dlabel.innerHTML = dataset.label;
//dlabel.addEventListener('click', function(evt){ //dlabel.addEventListener('click', function(evt){
@ -1773,13 +1767,13 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
for (ds of chart.data.datasets) { for (ds of chart.data.datasets) {
ds.borderWidth = 1; ds.borderWidth = 1;
} }
colorline.style.height = linewidth + 'px'; colorline.style.height = '2px';
dataset.borderWidth = linewidth; dataset.borderWidth = 2;
dlabel.style.fontWeight = 700; // bold dlabel.style.fontWeight = 700; // bold
} else { } else {
if (dataset.borderWidth == 1) { if (dataset.borderWidth == 1) {
colorline.style.height = linewidth + 'px'; colorline.style.height = '2px';
dataset.borderWidth = linewidth; dataset.borderWidth = 2;
} else { } else {
colorline.style.height = '1px'; colorline.style.height = '1px';
dataset.borderWidth = 1; dataset.borderWidth = 1;
@ -1789,10 +1783,10 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
} }
if (allDeselected) { if (allDeselected) {
for (ds of chart.data.datasets) { for (ds of chart.data.datasets) {
ds.borderWidth = linewidth; ds.borderWidth = 2;
} }
for (let k in legendlines) { for (let k in legendlines) {
legendlines[k].style.height = linewidth + 'px'; legendlines[k].style.height = '2px';
} }
} }
} }
@ -1931,10 +1925,10 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
linlog.innerHTML = "<strong>&#9746;</strong> log"; linlog.innerHTML = "<strong>&#9746;</strong> log";
} }
chart.options.scales.y.type = type; chart.options.scales.y.type = type;
//chart.options.animation.duration = 800; chart.options.animation.duration = 800;
if (autoScaleFlag) graphs.autoScale(chart); if (autoScaleFlag) graphs.autoScale(chart);
update(); update();
//setTimeout(function(){chart.options.animation.duration = 0;},850) setTimeout(function(){chart.options.animation.duration = 0;},850)
} }
/** /**
@ -1946,15 +1940,10 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
if (x === null) return; if (x === null) return;
for(let i in chart.data.datasets){ for(let i in chart.data.datasets){
let y = null; let y = null;
let metadata = chart.getDatasetMeta(i).data; for(let j = 0; j < chart.getDatasetMeta(i).data.length; j++){
let dataset = chart.data.datasets[i]; let dp = chart.getDatasetMeta(i).data[j];
if (metadata.length != dataset.data.length) { if (dp.x >= x) break; //_model does not exist anymore, properties are defined directly on elements
console.log('length mismatch in dataset.data and metadata') y = chart.data.datasets[i].data[dp.$context.index].y; // $context not mentionned in ChartJS doc, seen with console.log
}
for(let j = 0; j < metadata.length; j++){
let dp = metadata[j];
if (dp.x >= x) break;
y = dataset.data[j].y;
} }
valueElm = legendvalues[chart.data.datasets[i].key]; valueElm = legendvalues[chart.data.datasets[i].key];
if (labelMinWidth == 0) { if (labelMinWidth == 0) {
@ -1963,9 +1952,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
labelMinWidth = valueElm.clientWidth; labelMinWidth = valueElm.clientWidth;
valueElm.style.minWidth = labelMinWidth + 'px'; valueElm.style.minWidth = labelMinWidth + 'px';
} }
if (y == null) { if (y !== null) {
valueElm.innerHTML = '';
} else {
valueElm.innerHTML = strFormat(y, labelDigits); valueElm.innerHTML = strFormat(y, labelDigits);
} }
} }

View File

@ -1,25 +1,37 @@
var writePermission = false; // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
var showParams = false; // % GROUP
var showConsole = false;
var writePermissionTimeout; // Sets writePermission to 'false, restarts by
// user-interaction.
var prompt = false // True while a prompt is opened. var prompt = false // True while a prompt is opened.
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// COMMUNICATION
function getGroup(s, name) { function getGroup(s, name) {
reqJSON(s, "http://" + hostPort + "/getblock?path=" + name var found = false;
+ "&id=" + clientID, successHandler, errorHandler); if (name == "") {
swiper[s].slideTo(defaultSlidePos(s));
return;
}
for (var i = 0; i < swiper[s].slides.length; i++) {
var slideType = swiper[s].slides[i].slideType;
if (slideType == name) {
found = true;
swiper[s].slideTo(i);
}
}
if (!found && name != "console" && name != "graphics") {
// Server-request for group.
reqJSON(s, "http://" + hostPort + "/getblock?path=" + name
+ "&id=" + clientID, successHandler, errorHandler);
}
} }
function sendCommand(s, command) { function sendCommand(s, command) {
reqJSON(s, "http://" + hostPort + "/sendcommand?command=" + encodeURIComponent(command) reqJSON(s, "http://" + hostPort + "/sendcommand?command=" + command
+ "&id=" + clientID, successHandler, errorHandler); + "&id=" + clientID, successHandler, errorHandler);
} }
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function createContent(s, message) {
// GROUP
function createContent(message) {
// Depending on the message received from the server the content of the // Depending on the message received from the server the content of the
// group is created dynamically. Handles draw-message. // group is created dynamically. Handles draw-message.
@ -32,443 +44,267 @@ function createContent(message) {
component.title = component.name; component.title = component.name;
if (!("command" in component)) if (!("command" in component))
component.command = component.name; component.command = component.name;
createFunc = window['create_' + component.type + '_row']
if (message.title == 'modules') { if (createFunc)
let row = createRowForModules(component); content.appendChild(createFunc(s, component))
content.appendChild(row);
} else {
let row = createRowForParameters(component);
content.appendChild(row);
}
} }
return content; return content;
} }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ function gotoGroups(slideNames) {
// ROW slideNames = slideNames.split("%20");
var l = Math.min(MAXBLOCK,slideNames.length);
document.title = "SEA "+ clientTitle + " " + slideNames.join(" ");
for (var s=0; s<l; s++) {
getGroup(s, slideNames[s]);
}
}
function createRowForModules(component) { function create_group_row(s, component) {
let left = createLeftColumnForModules(component); // Creates row-element containing link.
left.id = component.name; var title = component.title;
left.setAttribute('name', 'component.title');
var row = document.createElement('row');
row.id = component.name;
row.name = title;
row.classList.add("interactive", "row", "link");
row.tabIndex = "0";
let right = createRightColumnForModules(component);
let row = appendToContent(left, right);
row.onclick = function () { row.onclick = function () {
var slideNames = getSlideNames();
slideNames[s] = component.name;
document.title = "SEA "+ clientTitle + " " + slideNames.join(" ");
history.pushState({func: "gotoGroups", funarg: slideNames.join("%20")}, document.title, "#" + slideNames.join("%20"));
getGroup(s, component.name);
}
if (title === "console" || title === "device config") {
row.classList.add("interactive", "row", "link", "link-static");
row.innerHTML = "console";
}
row.innerHTML = title;
return row;
}
function create_rdonly_row(s, component) {
// Creates row-element containing link AND read-only-item.
var link = component.link;
if (!link) // simple rdonly
return appendToContent(component, createTitle(component),
createParElement(component));
// with link
var left = document.createElement('a');
left.classList.add("col-left");
left.innerHTML = component.title;
left.id = component.name;
left.name = component.title;
left.classList.add("interactive", "link");
row = appendToContent(component, left, createParElement(component));
row.onclick = function () {
this.style.backgroundColor = "orangered";
left.click();
}
if (link.charAt(0) == ':') {
left.href = "http://" + location.hostname + link + "/";
} else {
left.href = link;
}
row.classList.add("row", "clickable");
return row;
}
function create_rdlink_row(s, component) {
// Creates row-element containing link AND read-only-item.
var name = component.name;
var left = createTitle(component);
left.id = component.name;
left.name = component.title; // or setAttribute('name'.. ?
left.classList.add("interactive", "link");
left.onclick = function () {
getGroup(s, component.title); getGroup(s, component.title);
} }
row.classList.add('row-clickable'); return appendToContent(component, left, createParElement(component));
return row;
} }
function createRowForParameters(component) { function create_pushbutton_row(s, component) {
let left = createLeftColumnForParameters(component); // Creates row-element containing a push button
let right = createRightColumnForParameters(component);
return appendToContent(left, right);
}
function appendToContent(left, right) { var name = component.name;
let row = document.createElement('div'); var command = component.command;
row.classList.add("row"); var left = createTitle(component);
row.appendChild(left);
row.appendChild(right);
return row;
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
// LEFT COLUMN
function createLeftColumnForModules(component) {
var left = document.createElement('span');
left.classList.add('col-left');
if (component.statusname) {
left.appendChild(createStatusIcon(component));
}
let modules_title = document.createElement('span');
modules_title.classList.add('modules-title');
modules_title.innerHTML = component.title;
if (component.type == 'pushbutton') {
modules_title.classList.add('push-button');
if (writePermission == true) {
modules_title.classList.add('push-button-active');
}
modules_title.onclick = function () {
if (writePermission == true) {
let row = button.closest('div');
row.classList.add('row-waiting-for-answer');
sendCommand(s, component.command);
}
}
}
left.appendChild(modules_title);
if (component.statusname) {
let status_info = document.createElement('span');
status_info.classList.add('status-info');
status_info.setAttribute('name', component.title + '-info');
left.appendChild(status_info);
}
if (component.info) {
let icon_info = createInfoIcon(component);
left.appendChild(icon_info);
left.appendChild(createInfoBox(component));
}
return left;
function createStatusIcon(component) {
let icon_status = document.createElement('img');
icon_status.setAttribute('src', 'res/icon_status.png');
icon_status.setAttribute('name', component.title + ':status');
icon_status.classList.add('icon-modules', 'icon-status');
return icon_status;
}
function createInfoIcon(component) {
let icon_info = document.createElement('img');
icon_info.setAttribute('src', 'res/icon_info.png');
icon_info.classList.add('icon-modules', 'icon-info');
if (isTouchDevice) {
icon_info.onclick = function (event) {
event.stopPropagation()
icon_info.nextSibling.classList.toggle("info-box-visible-by-click");
}
}
return icon_info;
}
function createInfoBox(component) {
// Creates info-box, which isn't visible by default but can be displayed.
let info_box = document.createElement('span');
info_box.classList.add("info-box");
info_box.innerHTML = '<b>' + component.title + '</b>: ' + component.info;
return info_box;
}
}
function createLeftColumnForParameters(component) {
let left = document.createElement('span');
left.classList.add('col-left');
if (component.type == 'pushbutton') {
left.appendChild(createPushButton (component));
} else {
left.innerHTML = component.title;
}
return left;
function createPushButton (component) {
let button = document.createElement('span');
button.classList.add('push-button');
if (writePermission == true) {
button.classList.add('push-button-active');
}
button.innerHTML = component.title;
button.onclick = function () {
if (writePermission == true) {
let row = button.closest('div');
row.classList.add('row-waiting-for-answer');
sendCommand(s, component.command);
}
}
return button;
}
}
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
// RIGHT COLUMN
function createRightColumnForModules(component) {
var right = document.createElement('span'); left.id = component.name;
right.classList.add('col-right', 'col-right-modules'); left.name = component.title;
right.appendChild(createValue(component));
if (component.targetname) {
if (component.type == 'input' ||
component.type == 'checkbox' ||
component.type == 'enum'
) {
let input_element = chooseTypeOfInput(component);
let icon_edit = createIconEdit(input_element);
right.appendChild(icon_edit);
right.appendChild(input_element);
}
}
return right;
}
function createRightColumnForParameters(component) { var right = createParElement(component);
right.classList.add("clickable", "push-button");
let right = document.createElement('span');
right.classList.add('col-right-parameters');
right.appendChild(createValue(component));
if (component.type == 'input' ||
component.type == 'checkbox' ||
component.type == 'enum'
) {
let input_element = chooseTypeOfInput(component);
let icon_edit = createIconEdit(input_element);
right.appendChild(icon_edit);
right.appendChild(input_element);
}
return right;
}
function createValue (component) { row = appendToContent(component, left, right);
let value = document.createElement('span'); right.onclick = function () {
value.classList.add('col-right-value'); if (writePermission) {
if (writePermission == true) { var row = left.parentNode;
value.classList.add('col-right-value-with-write-permission'); right.style.backgroundColor = "orangered";
} // Request for command
value.setAttribute('name', component.name); sendCommand(s, command);
if (component.type == 'pushbutton') {
value.__ctype__ = 'none';
} else {
value.__ctype__ = 'rdonly';
}
return value;
}
function createIconEdit (input_element) {
let icon_edit = document.createElement('img');
icon_edit.setAttribute('src', 'res/icon_edit.png');
icon_edit.classList.add('icon-modules', 'icon-edit');
if (writePermission == false) {
icon_edit.classList.add('icon-edit-hidden');
}
icon_edit.onclick = function (event) {
event.stopPropagation()
let is_hidden = input_element.classList.contains('input-element-hidden');
hideInputElements();
if (is_hidden) {
input_element.classList.remove('input-element-hidden');
if (input_element.inputChild) {
// update input value before edit
input_element.inputChild.value = input_element.inputChild.actualValue;
}
icon_edit.setAttribute('src', 'res/icon_edit_close.png');
} else { } else {
icon_edit.setAttribute('src', 'res/icon_edit.png'); prompt = true;
alertify.confirm("", "You are connected with <b>" + clientTitle
+ "</b>. <br>"
+ "Are you sure you want to modify things here?",
function () {
// User decided to proceed.
writePermission = true;
writePermissionTimeout = setTimeout(function () {
writePermission = false;
}, 3600000);
var row = left.parentNode;
row.style.backgroundColor = "orangered";
// Request for command
sendCommand(s, command);
prompt = false;
}, function () {
// User decided to cancel
prompt = false;
});
} }
} }
return icon_edit; row.classList.add("row");
return row;
} }
function chooseTypeOfInput (component) { function create_input_row(s, component) {
let input_element;
switch (component.type) {
case 'enum':
input_element = createEnum(component);
input_element.classList.add('input-element', 'input-element-hidden');
break;
case 'input':
input_element = createInputText(component);
input_element.classList.add('input-element', 'input-element-hidden');
break;
case 'checkbox':
input_element = createCheckbox(component);
input_element.classList.add('input-element', 'input-element-hidden');
break;
}
return input_element;
}
/* ---------------------------------------------------------------------------------- */
// input elements
function createInputText(component) {
// Creates row-element containing input-item. // Creates row-element containing input-item.
var input = createInputElement(component, 'input', 'input-text'); var name = component.name;
var command = component.command;
if (component.info) {
var infoBox = createInfo(component);
}
var left = createTitle(component);
var input = createParElement(component, 'input', 'input-text');
input.type = "text"; input.type = "text";
input.style.width = "100px"; input.style.width = "100px";
input.onclick = function (e) {
e.stopPropagation();
}
// Prevent updates, while user is changing textfield
input.addEventListener("focus", function(evt) { input.addEventListener("focus", function(evt) {
let elm = evt.target; let elm = evt.target;
setTimeout(function(){elm.setSelectionRange(0, elm.value.length);},0); setTimeout(function(){elm.setSelectionRange(0, elm.value.length);},0);
}); });
input.onkeydown = function (e) { input.onkeydown = function (e) {
if (e.key == "Escape") { if (e.which === 27 || e.key == "Escape") {
// User decided to cancel // User decided to cancel
let input = e.target; input.value = intput.oldValue;
input.value = input.oldValue;
resizeTextfield(input); resizeTextfield(input);
var row = input.closest('div'); var row = left.parentNode;
row.classList.remove('row-waiting-for-answer'); row.style.backgroundColor = "white";
hideInputElements();
} }
} }
input.onfocus = function () { input.onfocus = function () {
input.oldValue = input.value; input.oldValue = input.value;
if (isTouchDevice) if (isTouchDevice)
setTimeout(function () { setTimeout(function () {
posTextfield(s, left); posTextfield(s, left);
}, 1); }, 1);
} }
input.onblur = function () {
if (prompt) {
return false;
}
var row = left.parentNode;
var value = input.value;
let oldValue = 'oldValue' in input ? input.oldValue : value;
if (!('actualValue' in input)) input.actualValue = oldValue;
actualValue = input.actualValue;
if (value == actualValue || value == oldValue ||
parseFloat(value) == parseFloat(actualValue) || parseFloat(value) == parseFloat(oldValue)) {
input.value = actualValue;
// nothing to do.
row.style.backgroundColor = "white";
return false;
}
// User changed value and moved focus to other object.
alertify.confirm("", "You changed a field without pressing the return key.<br>"
+ "Hint: press ESC for leaving a field unchanged.<b>"
+ "You are connected with <b>" + clientTitle + "</b>.<br>"
+ "Are you sure you want to change the value of<br><b>"
+ name + "</b> from <b>" + actualValue
+ "</b> to <b>" + value + "</b>?", function () {
// User decided to proceed.
writePermission = true;
writePermissionTimeout = setTimeout(function () {
writePermission = false;
}, 3600000);
row.style.backgroundColor = "orangered";
// Request for command
sendCommand(s, command + " " + value);
resizeTextfield(input);
prompt = false;
}, function () {
// User decided to cancel
input.value = input.actualValue;
resizeTextfield(input);
row.style.backgroundColor = "white";
prompt = false;
});
}
var form = document.createElement('form'); var form = document.createElement('form');
form.onsubmit = function (e) { form.onsubmit = function (e) {
e.preventDefault(); e.preventDefault();
var row = form.closest('div'); if (writePermission) {
row.classList.add('row-waiting-for-answer'); var row = left.parentNode;
// Request for command row.style.backgroundColor = "orangered";
input.actualValue = input.value; // Request for command
if (component.targetname) { sendCommand(s, name + " " + input.value);
sendCommand(s, component.targetname + " " + input.value); input.blur();
} else { } else {
sendCommand(s, component.name + " " + input.value); var value = input.value
prompt = true;
alertify.confirm("", "You are connected with <b>" + clientTitle
+ "</b>. <br>"
+ "Are you sure you want to modify things here?",
function () {
// User decided to proceed.
writePermission = true;
writePermissionTimeout = setTimeout(function () {
writePermission = false;
}, 3600000);
var row = left.parentNode;
row.style.backgroundColor = "orangered";
// Request for command
sendCommand(s, command + " " + value);
resizeTextfield(input);
prompt = false;
}, function () {
// User decided to cancel
input.value = input.oldValue;
resizeTextfield(input);
prompt = false;
});
} }
row.classList.add('row-waiting-for-answer');
input.blur();
hideInputElements();
}; };
form.appendChild(input); form.appendChild(input);
form.appendChild(createSubmitButton()); var right = createParElement(component);
form.inputChild = input; right.appendChild(form);
return form; return appendToContent(component, left, right);
} }
function createCheckbox(component) { function posTextfield(s, left) {
// Creates row-element containing checkbox-item var content = swiper[s].slides[swiper[s].activeIndex].childNodes[1];
let input = createInputElement(component, 'input', 'parameter-checkbox'); var row = left.parentNode;
input.type = "checkbox"; content.scrollTop = row.offsetTop - 30;
input.onclick = function (e) {
e.stopPropagation;
}
let form = document.createElement('form');
form.onsubmit = function (e) {
e.preventDefault();
var row = form.closest('div');
row.classList.add('row-waiting-for-answer');
sendCommand(s, component.command + " " + input.checked);
hideInputElements();
};
form.appendChild(input);
form.appendChild(createSubmitButton());
return form;
}
function createEnum(component) {
// Creates row-element containing dropdown-selection.
var buttons = component.enum_names;
var select = createInputElement(component, 'select', 'select-params');
for (var i = 0; i < buttons.length; i++) {
var option = document.createElement('option');
option.type = "enum";
option.classList.add("option-params");
option.value = buttons[i].value;
option.appendChild(document.createTextNode(buttons[i].title));
select.add(option);
}
select.oninput = function () {
let row = select.closest('div');
row.classList.add('row-waiting-for-answer');
let index = select.value - 1;
console.log('send', buttons[index].title);
sendCommand(s, component.command + " " + select.value);
// hideInputElements();
};
select.onfocus = function () {
// select.oldIndex = select.selectedIndex;
console.log(select.selectedValue);
}
var right = document.createElement('span');
right.appendChild(select);
return right;
}
function createRadio(component) {
console.log(component);
let array_names = component.enum_names;
let form = createInputElement(component, 'form', 'radio-button-group');
form.onsubmit = function (e) {
e.preventDefault();
var row = form.closest('div');
row.classList.add('row-waiting-for-answer');
sendCommand(s, component.command + " " + 'on');
hideInputElements();
};
for (var i = 0; i < array_names.length; i++) {
let label = document.createElement('label');
label.setAttribute('for', array_names[i].title);
label.innerHTML = array_names[i].title;
let radio = document.createElement('input');
radio.setAttribute('type', 'radio');
radio.classList.add("radio");
radio.setAttribute('id', array_names[i].title);
radio.setAttribute('name', component.name);
radio.onclick = function(e) {
e.stopPropagation();
}
form.appendChild(label);
form.appendChild(radio);
}
form.appendChild(createSubmitButton());
return form;
}
function createInputElement(component, tag='span', cls='col-right-modules') {
var input_element = document.createElement(tag);
input_element.classList.add('col-right');
if (cls)
input_element.classList.add(cls);
if (component.targetname) {
input_element.setAttribute('name', component.targetname);
} else {
input_element.setAttribute('name', component.name);
}
// Add DOM-property
input_element.__ctype__ = component.type;
return input_element;
}
function createSubmitButton () {
let submit_btn = document.createElement('input');
submit_btn.setAttribute('type', 'image');
submit_btn.classList.add('icon-modules', 'icon-okay');
submit_btn.setAttribute('src', 'res/icon_okay.png');
submit_btn.onclick = function (e) {
e.stopPropagation();
}
return submit_btn;
}
/* ---------------------------------------------------------------------------------- */
// Hides all input elements (input text, pushbotton, enum, checkbox)
// Changes all iconEditClose (cross) back to iconEdit (pen)
function hideInputElements(){
let input_elements = document.getElementsByClassName('input-element');
for (let i = 0; i < input_elements.length; i++) {
input_elements[i].classList.add('input-element-hidden');
}
let array_icon_edit = document.getElementsByClassName('icon-edit');
for (let i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].setAttribute('src', 'res/icon_edit.png');
}
} }
function resizeTextfield(input) { function resizeTextfield(input) {
@ -483,25 +319,192 @@ function resizeTextfield(input) {
} }
} }
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */ function create_checkbox_row(s, component) {
// CONTENT // Creates row-element containing checkbox-item
var command = component.command;
function appendToGridElement(s, title, type, content) { var left = createTitle(component);
let panel = document.createElement('div');
panel.classList.add("panel");
titlewrapper = document.createElement('span'); var input = createParElement(component, 'input', 'parameter-checkbox');
titlewrapper.innerHTML = title; input.type = "checkbox";
panel.appendChild(titlewrapper);
let gridContainer = document.createElement('div'); input.onkeyup = function (e) {
gridContainer.classList.add("grid-container"); if (e.keyCode === 32) {
// Store type so it can be found easiely later. handleCheckbox();
gridContainer.slideType = type; }
gridContainer.appendChild(panel); }
gridContainer.appendChild(content);
let gridelements = document.getElementsByClassName('grid-element'); var label = document.createElement('label');
gridelements[s].innerHTML = ""; label.for = input;
gridelements[s].appendChild(gridContainer); label.classList.add("parameter-label");
label.onclick = function () {
handleCheckbox();
}
function handleCheckbox() {
if (writePermission) {
var row = left.parentNode;
row.style.backgroundColor = "orangered";
if (input.checked) {
var value = "0";
input.checked = false;
} else {
var value = "1";
input.checked = true;
}
// Request for command
sendCommand(s, command + " " + value);
} else {
alertify.confirm("", "You are connected with <b>" + clientTitle
+ "</b>. <br>"
+ "Are you sure you want to modify things here?",
function () {
// User decided to proceed.
writePermission = true;
writePermissionTimeout = setTimeout(function () {
writePermission = false;
}, 3600000);
var row = left.parentNode;
row.style.backgroundColor = "orangered";
if (input.checked) {
var value = "0";
input.checked = false;
} else {
var value = "1";
input.checked = true;
}
// Request for command
sendCommand(s, command + " " + value);
}, function () {
// User decided to cancel
});
}
};
var right = document.createElement('span');
right.classList.add("col-right");
right.appendChild(input);
right.appendChild(label);
return appendToContent(component, left, right);
}
function create_enum_row(s, component) {
// Creates row-element containing dropdown-selection.
var name = component.name;
var command = component.command;
var buttons = component.enum_names;
var left = createTitle(component);
var select = createParElement(component, 'select', 'select-params');
select.onfocus = function () {
select.oldIndex = select.selectedIndex;
}
select.oninput = function () {
if (writePermission && component.title != "device config") {
var row = left.parentNode;
row.style.backgroundColor = "orangered";
// Request for command
sendCommand(s, command + " " + this.value);
} else {
alertify.confirm("", "You are connected with <b>" + clientTitle
+ "</b>. <br>"
+ "Are you sure you want to modify things here?",
function () {
// User decided to proceed.
writePermission = true;
writePermissionTimeout = setTimeout(function () {
writePermission = false;
}, 3600000);
var row = left.parentNode;
row.style.backgroundColor = "orangered";
// Request for command
sendCommand(s, command + " " + select.value);
}, function () {
// User decided to cancel
select.value = select.options[select.oldIndex].value
});
}
};
for (var i = 0; i < buttons.length; i++) {
var option = document.createElement('option');
option.type = "enum";
option.classList.add("option-params");
option.value = buttons[i].value;
option.appendChild(document.createTextNode(buttons[i].title));
select.add(option);
}
select.style.display = "none";
var right = document.createElement('span');
right.classList.add("col-right");
right.appendChild(select);
return appendToContent(component, left, right);
}
function createTitle(component) {
// Creates left side of row-tag containing title. Title may hold additional
// information, which is shown, when title-tag is clicked.
var left = document.createElement('span');
if (component.info) {
left.classList.add("col-left", "event-toggle-info");
left.onclick = function () {
var infoBox = left.parentNode.childNodes[0];
if (infoBox.style.display == "none") {
infoBox.style.display = "block";
} else {
infoBox.style.display = "none";
}
}
left.innerHTML = component.title + "<sup><b>(i)</b></sup>";
} else {
left.classList.add("col-left");
left.innerHTML = component.title;
}
return left;
}
function createParElement(component, tag='span', cls='col-right') {
var right = document.createElement(tag);
if (cls)
right.classList.add(cls);
// right.name = is not sufficient, getElementsByName would not work
right.setAttribute('name', component.name);
right.__ctype__ = component.type;
return right;
}
function createInfo(component) {
// Creates info-box, which isn't visible by default but can be displayed.
var infoBox = document.createElement('div');
infoBox.classList.add("info-box");
infoBox.onclick = function () {
infoBox.style.display = "none";
}
infoBox.innerHTML = component.info;
return infoBox;
}
function appendToContent(component, left, right) {
// Creates row-tag containing infoBox (not visible by default), left side
// (span) and right side (span).
var row = document.createElement('div');
row.classList.add("row");
if (component.info) {
row.appendChild(createInfo(component));
}
row.appendChild(left);
row.appendChild(right);
return row;
} }

View File

@ -1,5 +1,9 @@
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// % INIT // % INIT
var MAXBLOCK = 4; // max number of blocks
var elements = []; // grid elements
var swiper = []; // This array contains main-swiper-Instances.
var hostPort = ""; // Address and port of static html-file. var hostPort = ""; // Address and port of static html-file.
var clientID = ""; // ID given by server when SSE-connection is established. var clientID = ""; // ID given by server when SSE-connection is established.
var clientTitle = ""; // Contains name of instrument and device. var clientTitle = ""; // Contains name of instrument and device.
@ -7,12 +11,15 @@ var getUpdates = true;
var getUpdatesGraphics = true; var getUpdatesGraphics = true;
var initCommands = []; var initCommands = [];
var loadingShown = true; var loadingShown = true;
var writePermission = false;
var menuMode = false;
var panelOn = true; var panelOn = true;
var firstState = 0;
function Settings() { function Settings() {
// get key/value pairs from search part of the URL and fill into query // get key/value pairs from search part of the URL and fill into query
var qstr = location.search; var qstr = location.search;
// console.log(qstr); console.log(qstr);
if (qstr) { if (qstr) {
var a = (qstr[0] === '?' ? qstr.substr(1) : qstr).split('&'); var a = (qstr[0] === '?' ? qstr.substr(1) : qstr).split('&');
for (var i = 0; i < a.length; i++) { for (var i = 0; i < a.length; i++) {
@ -66,67 +73,50 @@ new Settings()
.treat("debugGraphics", "dg", to_bool, false) .treat("debugGraphics", "dg", to_bool, false)
.treat("hostPort", "hp", 0, location.hostname + ":" + location.port) .treat("hostPort", "hp", 0, location.hostname + ":" + location.port)
.treat("showMain", "sm", to_bool, true) .treat("showMain", "sm", to_bool, true)
.treat("initConsole", "ic", to_bool, true) .treat("showConsole", "sc", to_bool, true)
.treat("showOverview", "so", to_bool, true)
.treat("showGraphics", "sg", to_bool, true) // false) .treat("showGraphics", "sg", to_bool, true) // false)
.treat("hideRightPart", "hr", to_bool, false) //used to completely disable the right part .treat("hideRightPart", "hr", to_bool, true) //used to completely disable the right part
.treat("wideGraphs", "wg", to_bool, false) //used to toggle the size of the graphs part .treat("wideGraphs", "wg", to_bool, false) //used to toggle the size of the graphs part
.treat("showAsync", "sa", to_bool, false) .treat("showAsync", "sa", to_bool, false)
.treat("device", "device", 0, "")
.treat("stream", "stream", 0, "")
.treat("instrument", "instrument", 0, "")
.treat("timerange", "time", 0, "-1800,0")
.treat("lazyPermission", "wr", to_bool, true);
if (window.instrument) {
window.clientTags = "&instrument=" + window.instrument;
} else {
let args = '';
if (window.stream) { args += "&stream=" + window.stream; }
if (window.device) { args += "&device=" + window.device; }
window.clientTags = args;
}
// console.log('TAGS', window.clientTags);
function loadFirstBlocks() { function loadFirstBlocks() {
if (showMain) pushInitCommand("getblock?path=main&", "main") if (showMain) pushInitCommand("getblock?path=main&", "main")
if (initConsole) pushInitCommand("console?", "console") if (showConsole) pushInitCommand("console?", "console")
if (nColumns == 1) { // probably mobile phone} if (nColumns == 1) { // probably mobile phone}
if (showGraphics) pushInitCommand("gettime?time=" + window.timerange + "&", "graphics") if (showGraphics) pushInitCommand("gettime?time=-1800,0&", "graphics")
var goFS = document.getElementById('header'); if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
goFS.addEventListener(
'click',
function () {
document.body.requestFullscreen();
},
false,
);
} else { } else {
if (showGraphics) pushInitCommand("gettime?time=" + window.timerange + "&", "graphics") if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
if (showGraphics) pushInitCommand("gettime?time=-1800,0&", "graphics")
// last is shown first // last is shown first
} }
} }
function nextInitCommand() { function nextInitCommand() {
// do the next init request // do the next init request
if (initCommands.length > 0) { if (initCommands.length > 0) {
next = initCommands.shift(); next = initCommands.shift();
cmd = next[0] cmd = next[0]
text = next[1] text = next[1]
var loadingSpan = document.getElementsByClassName("loading-span")[0]; var loadingSpan = document.getElementsByClassName("loading-span")[0];
loadingSpan.innerHTML = loadingSpan.innerHTML + "<br>loading " + htmlEscape(text) + " ..."; loadingSpan.innerHTML = loadingSpan.innerHTML + "<br>loading " + htmlEscape(text) + " ...";
reqJSON(0, "http://" + hostPort + "/" + cmd + "id=" + clientID, successHandler, errorHandler); reqJSON(0, "http://" + hostPort + "/" + cmd + "id=" + clientID, successHandler, errorHandler);
} else if (loadingShown) { } else if (loadingShown) {
var loadingScreen = document.getElementsByClassName("loading-div")[0]; var loadingScreen = document.getElementsByClassName("loading-div")[0];
loadingScreen.style.display = "none"; loadingScreen.style.display = "none";
loadingShown = false; loadingShown = false;
console.log("loading finished"); if (location.hash) { // there was a #hash part
var slideNames = location.hash.substr(1);
gotoGroups(slideNames);
} }
console.log("loading finished");
}
} }
function pushInitCommand(cmd, text) { function pushInitCommand(cmd, text) {
initCommands.push([cmd, text]); initCommands.push([cmd, text]);
} }
window.onload = function() { window.onload = function() {
@ -142,118 +132,67 @@ window.onload = function() {
// rows 'n' // rows 'n'
adjustGrid(); adjustGrid();
/* ----------------------------------------------------------------------------------------------------- */ let crossElement = document.getElementById("close-cross");
/* MIAN ICONS */
let icon_close_container = document.getElementsByClassName("icon-close-container")[0];
let icon_log_container = document.getElementsByClassName("icon-log-container")[0];
let icon_lock_container = document.getElementsByClassName("icon-lock-container")[0];
if (window.hideRightPart){ if(window["hideRightPart"]){
document.body.removeChild(icon_close_container); document.body.removeChild(crossElement);
} else { }else{
icon_close_container.onclick = function(){ crossElement.onclick = function(){
if (showParams) { if(nColumns == 1){ // if the screen is small, the cross always slides to the next slide
showParams = false; let someSwiper = swiper[0];
// icon_close_container.innerHTML = '<img class = "icon-main icon-close" src="res/icon_sinus.png">'; someSwiper.enableSwiping(true); // needed because someSwiper might be the graphs swiper, and swiping is disable by default
icon_close_container.innerHTML = '<img class = "icon-main icon-close" src="res/icon_close.png">'; someSwiper.slideNext(); // someSwiper can be anything, it will swipe to the next slide
} else { }else{ // else it toggles the graphs window's size and triggers the adjustGrid()
if (window.wideGraphs) { window["wideGraphs"] = !window['wideGraphs'];
window.wideGraphs = false; adjustGrid();
document.getElementsByClassName('graphics')[0].classList.remove('panel-graphics-wide');
// icon_close_container.innerHTML = '<img class = "icon-main icon-close" src="res/icon_sinus.png">';
icon_close_container.innerHTML = '<img class = "icon-main icon-close" src="res/icon_close.png">';
icon_log_container.classList.remove("icon-main-container-hidden");
} else {
window.wideGraphs = true;
document.getElementsByClassName('graphics')[0].classList.add('panel-graphics-wide');
// icon_close_container.innerHTML = '<img class = "icon-main icon-close" src="res/icon_modules.png">';
icon_close_container.innerHTML = '<img class = "icon-main icon-close" src="res/icon_close.png">';
icon_log_container.classList.add("icon-main-container-hidden");
}
} }
adjustGrid();
} }
} }
icon_log_container.onclick = function(){ // Create swiper-instances.
if (showConsole) { for (var s = 0; s < MAXBLOCK; s++) {
showConsole = false; swiper[s] = insertSwiper(s);
} else {
showConsole = true;
}
adjustGrid();
} }
function changeWritePermission(flag) {
let array_icon_edit = document.getElementsByClassName('icon-edit');
let array_pushbutton = document.getElementsByClassName('push-button');
let array_col_right_value = document.getElementsByClassName('col-right-value');
writePermission = flag;
if (writePermission) {
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_open.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.remove('icon-edit-hidden');
}
for(i = 0; i < array_pushbutton.length; i++) {
array_pushbutton[i].classList.add('push-button-active');
}
for (let i = 0; i < array_col_right_value.length; i++) {
array_col_right_value[i].classList.add('col-right-value-with-write-permission');
}
} else {
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_closed.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.add('icon-edit-hidden');
}
for(i = 0; i < array_pushbutton.length; i++) {
array_pushbutton[i].classList.remove('push-button-active');
}
for (let i = 0; i < array_col_right_value.length; i++) {
array_col_right_value[i].classList.remove('col-right-value-with-write-permission');
}
}
}
if (window.lazyPermission) {
changeWritePermission(true);
}
icon_lock_container.onclick = function(){
if (writePermission == false) {
if (window.lazyPermission) {
changeWritePermission(true);
} else {
alertify.prompt( "WRITE PERMISSION", "Current device: <b>"+ window.device + "</b><p>Please confirm the instrument:", ""
, function(evt, value) {
// User decided to proceed
if (window.instrument.toUpperCase() == value.toUpperCase()) {
changeWritePermission(true);
}
}
, function() {
// User decided to cancel
prompt = false;
});
}
} else {
changeWritePermission(false);
}
}
var homeButton = document.getElementById("home-icon"); var homeButton = document.getElementById("home-icon");
homeButton.onclick = function () { // TODO : uncomment this code with the right URL to navigate to when the way to select the instrument will be decided.
window.location = "/select_experiment"; // homeButton.onclick = function () {
}; // window.location = "http://" + location.hostname + ":8800/";
// };
buildUpdateConnection(); buildUpdateConnection();
if (location.hash) {
console.log("hash in url", location.hash);
initSlides = location.hash.substring(1);
} else {
initSlides = "";
}
// Initialisation will be continued, when SSE-connection is established
// and id-message is obtained.
// (see also at SEAWebClientCommunication.js)
addEventListener("popstate", function (e) {
if (e.state) {
if (loadingShown) {
if (initSlides != e.state.funarg) {
console.log("hash mismatch", initSlides, e.state.funarg);
initSlides = e.state.funarg;
}
} else {
console.log("popstate", e.state.func, e.state.funarg);
window[e.state.func](e.state.funarg);
}
} else {
document.title = "SEA "+ clientTitle;
for (var s=0; s<MAXBLOCK; s++) {
swiper[s].slideTo(defaultSlidePos(s));
}
}
})
}; };
function toggleHeader() { function toggleHeader() {
// Show and hide box showing name of the current device ('see also // Show and hide box showing name of the current device ('see also
// SEAWebClient.html') // SEAWebClient.html')
var main_panel = document.getElementById("main-panel"); var main_panel = document.getElementById("main-panel");
panelOn = !panelOn; panelOn = !panelOn;
if (panelOn) { if (panelOn) {

View File

@ -3,27 +3,20 @@
var nColumns = 1; // Viewport is subdivided in nColumns columns. var nColumns = 1; // Viewport is subdivided in nColumns columns.
var nRows = 1; // Viewport is subdivided in nRows rows. var nRows = 1; // Viewport is subdivided in nRows rows.
var gridCountGraphics = 2; // Number of displayed graphics-swipers.
var MINWIDTH = 400; // Minimal width of block. var MINWIDTH = 400; // Minimal width of block.
var MINHEIGHT = 700; // Minimal height of block. var MINHEIGHT = 700; // Minimal height of block.
var MAXBLOCK = 4; // max number of blocks let paramSlider = [0,1,2,3]; // the number of the parameter slider to open
var elements = []; // grid elements let prevActiveSlider = 0;
function createGrid() { function createGrid() {
// Creates grid-elements. // Creates grid-elements. By default only the first one is shown
// 1 - graphics // and
// 2 - modules // takes the whole viewport.
// 3 - parameters
// 4 - log
var elements = []; var elements = [];
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
let element = document.createElement('div'); var element = document.createElement('div');
element.classList.add("grid-element"); element.classList.add("grid-element");
element.classList.add("grid-element-"+i);
let panel_background = document.createElement('div');
panel_background.classList.add("panel");
element.appendChild(panel_background);
document.getElementById("center").appendChild(element); document.getElementById("center").appendChild(element);
elements.push(element); elements.push(element);
} }
@ -52,6 +45,10 @@ function determineViewportSize() {
if (height > MINHEIGHT) { if (height > MINHEIGHT) {
nRows = 2; nRows = 2;
} }
if (menuMode) {
nRows = 1;
nColumns = 1;
}
} }
function sizeChange() { function sizeChange() {
@ -60,121 +57,83 @@ function sizeChange() {
} }
function adjustGrid() { function adjustGrid() {
// Determines size of grid-elements depending on number // Determines size of grid-elements depending on number of columns 'nColumns' and
// of columns 'nColumns' and rows 'nRows' // rows 'nRows'
var width = window.innerWidth || document.documentElement.clientWidth var width = window.innerWidth || document.documentElement.clientWidth
|| document.body.clientWidth; || document.body.clientWidth;
var height = window.innerHeight || document.documentElement.clientHeight var height = window.innerHeight || document.documentElement.clientHeight
|| document.body.clientHeight; || document.body.clientHeight;
paramSlider = [0,1,2,3];
prevActiveSlider = 0;
if (window["hideRightPart"] || window["wideGraphs"]){ if (window["hideRightPart"] || window["wideGraphs"]){
style(0,"100vw","100vh"); style(0,"100vw","100vh");
style(1); // hide style(1); // hide
style(2); // hide style(2); // hide
style(3); // hide style(3); // hide
return; return
} }
switch (nColumns) { switch (nColumns) {
case 1: case 1:
if (showConsole) { if (menuMode) {
if (showParams) { leftWidth = Math.min(100, MINWIDTH / width * 100);
style(0); // hide style(0,leftWidth + "vw","100vh");
style(1); // hide style(1); // hide
style(2,"100vw","50vh"); style(2); // hide
style(3,"100vw","50vh"); style(3); // hide
} else {
style(0); // hide
style(1,"100vw","50vh");
style(2); // hide
style(3,"100vw","50vh");
}
} else { } else {
if (showParams) { // we may want to switch to 90vh on safari ios (workaround)
style(0); // hide style(0,"100vw","100vh");
style(1); // hide style(1); // hide
style(2,"100vw","100vh"); style(2); // hide
style(3); // hide style(3); // hide
} else { }
style(0); // hide
style(1,"100vw","100vh");
style(2); // hide
style(3); // hide
}
}
break; break;
case 2: case 2:
case 3:
rightWidth = Math.min(50, MINWIDTH / width * 100); rightWidth = Math.min(50, MINWIDTH / width * 100);
leftWidth = 100 - rightWidth; leftWidth = 100 - rightWidth;
if (showConsole) { if (nRows == 1) {
if (nRows == 1) { style(0,leftWidth + "vw","100vh");
if (showParams) { style(1,rightWidth + "vw","100vh");
style(0,leftWidth + "vw","100vh"); style(2); // hide
style(1); // hide style(3); // hide
style(2,rightWidth + "vw","50vh");
style(3,rightWidth + "vw","50vh");
} else {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","50vh");
style(2); // hide
style(3,rightWidth + "vw","50vh");
}
} else {
if (showParams) {
style(0,leftWidth + "vw","100vh");
style(1); // hide
style(2,rightWidth + "vw","50vh");
style(3,rightWidth + "vw","50vh");
} else {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","50vh");
style(2); // hide
style(3,rightWidth + "vw","50vh");
}
}
} else { } else {
if (nRows == 1) { style(0,leftWidth + "vw","100vh");
if (showParams) { style(1,rightWidth + "vw","50vh");
style(0,leftWidth + "vw","100vh"); style(2); // hide
style(1); // hide style(3,rightWidth + "vw","50vh");
style(2,rightWidth + "vw","100vh");
style(3); // hide
} else {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","100vh");
style(2); // hide
style(3); // hide
}
} else {
if (showParams) {
style(0,leftWidth + "vw","100vh");
style(1); // hide
style(2,rightWidth + "vw","100vh");
style(3); // hide
} else {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","100vh");
style(2); // hide
style(3); // hide
}
}
} }
break; break;
case 3:
rightWidth = MINWIDTH / width * 100;
leftWidth = 100 - rightWidth;
if (nRows == 1) {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","100vh");
style(2); // hide
style(3); // hide
} else {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","50vh");
style(2); // hide
style(3,rightWidth + "vw","50vh");
}
break;
case 4: case 4:
rightWidth = MINWIDTH / width * 100; rightWidth = MINWIDTH / width * 100;
leftWidth = 100 - 2 * rightWidth; leftWidth = 100 - 2 * rightWidth;
if (showConsole) { if (nRows == 1) {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","100vh");
style(2); // hide
style(3,rightWidth + "vw","100vh");
} else {
style(0,leftWidth + "vw","100vh"); style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","50vh"); style(1,rightWidth + "vw","50vh");
style(2,rightWidth + "vw","50vh"); style(2,rightWidth + "vw","50vh");
style(3,100 - leftWidth + "vw","50vh"); style(3,(2 * rightWidth) + "vw","50vh");
} else {
style(0,leftWidth + "vw","100vh");
style(1,rightWidth + "vw","100vh");
style(2,rightWidth + "vw","100vh");
style(3); // hide
} }
break; break;
default: default:
@ -184,6 +143,8 @@ function adjustGrid() {
function style(s, width, height) { function style(s, width, height) {
if (width) { if (width) {
paramSlider[prevActiveSlider] = s;
prevActiveSlider = s;
elements[s].style.display = "inline-block"; elements[s].style.display = "inline-block";
elements[s].style.width = width; elements[s].style.width = width;
} else { } else {

View File

@ -0,0 +1,184 @@
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// % SWIPER
function insertSwiper(s) {
// Create an empty swiper-instance and append the swiper-container to
// 'grid-element' s.
var container = document.createElement('div');
container.classList.add("swiper", "swiper-container-main");
elements[s].appendChild(container);
var swiperwrapper = document.createElement('div');
swiperwrapper.classList.add("swiper-wrapper", "swiper-wrapper-main");
swiperwrapper.s = s;
container.appendChild(swiperwrapper);
var paginationWrapper = document.createElement('div');
paginationWrapper.classList.add("swiper-pagination");
container.appendChild(paginationWrapper);
var buttonPrev = document.createElement("div");
buttonPrev.classList.add("swiper-button-prev", "swiper-button-black");
var buttonNext = document.createElement("div");
buttonNext.classList.add("swiper-button-next", "swiper-button-black");
var swiper = new Swiper(container, {
direction : 'horizontal',
pagination: {
el: paginationWrapper,
clickable: true,
},
watchOverflow: true,
spaceBetween : 0,
navigation:{
prevEl: buttonPrev,
nextEl: buttonNext
},
noSwiping: true, // this activates the noSwipingClass functionality
});
//console.log(swiper);
// the graphics slide will disable swiping (use hide box instead)
if (isTouchDevice) {
function enableSwiping(allow) {
swiper.params.noSwipingClass = allow ? null : "swiper-slide-main";
}
} else {
function enableSwiping(allow) {
buttonPrev.style.display = allow ? 'block' : 'none';
buttonNext.style.display = allow ? 'block' : 'none';
}
swiper.params.noSwipingClass = "swiper-slide-main";
container.appendChild(buttonPrev);
container.appendChild(buttonNext);
}
swiper.enableSwiping = enableSwiping;
return swiper;
}
function findSlide(s, type) {
var i;
for (i = 0; i < swiper[s].slides.length; i++) {
if (swiper[s].slides[i].slideType === type) {
return i;
}
}
return null;
}
function replaceSlideContent(slide, title, content) {
titlewrapper = slide.childNodes[0].childNodes[0];
titlewrapper.innerHTML = title;
slide.replaceChild(content, slide.childNodes[1])
}
function insertSlide(s, title, type, content) {
// Inserts new group to instance s of Swiper. return inserted position
var isl = findSlide(s, type);
var slide = swiper[s].slides[isl];
if (slide) { // slide already exists
replaceSlideContent(slide, title, content);
return isl;
}
var panel = document.createElement('div');
panel.classList.add("panel");
titlewrapper = document.createElement('span');
titlewrapper.innerHTML = title;
panel.appendChild(titlewrapper);
/*
if (type == "_overview" || type == "main") {
//panel.appendChild(createHomeButton(s));
} else if (type != "graphics" && type != "_inst_select" && type != "console") {
panel.appendChild(createCloseButton(s));
}
*/
/*if (type === "graphics") {
panel.appendChild(createUpdateButton(s));
}*/
slide = document.createElement('div');
slide.classList.add("swiper-slide", "swiper-slide-main");
// Store type so it can be found easiely later.
slide.slideType = type;
slide.appendChild(panel);
slide.appendChild(content);
// Graphics-slide is put at mostleft position.
if (type == "graphics" || type == "_overview") {
swiper[s].prependSlide(slide);
swiper[s].slideTo(0);
return 0;
}
swiper[s].appendSlide(slide);
if (type == "console") {
if (s === 3) {
// Slide mostright swiper-instance to last position (console)
swiper[3].slideNext();
}
return swiper[s].slides.length - 1;
}
let pos = 0;
if (swiper[s].slides.length > 1) {
var consoleslide = swiper[s].slides[swiper[s].slides.length - 2];
if (consoleslide.slideType == "console") {
// shift Console-slide to mostright position.
swiper[s].removeSlide(swiper[s].slides.length - 2);
swiper[s].appendSlide(consoleslide);
// Slide to position of new slide
pos = swiper[s].slides.length - 2;
} else {
pos = swiper[s].slides.length - 1;
}
}
swiper[s].slideTo(pos);
return pos;
}
function createCloseButton(s) {
// Creates 'span'-element containing close-button.
var wrapper = document.createElement('span');
wrapper.onclick = function () {
swiper[s].removeSlide(swiper[s].activeIndex);
swiper[s].slidePrev();
};
var closeButton = '<svg class="interactive icon slide-close-icon" fill="#000000" height="24" viewBox="0 0 24 24" width="24"><path d="M19 6.41L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z"/><path d="M0 0h24v24H0z" fill="none"/></svg>';
wrapper.innerHTML = closeButton;
return wrapper;
}
function createUpdateButton(s){
// Creates 'span'-element containing update-button (Should be removed later!)
var button = document.createElement('span');
button.classList.add("interactive", "toggle-updates-graphics")
button.onclick = function () {
getUpdatesGraphics = ! getUpdatesGraphics;
button.innerHTML = "updates = "+getUpdatesGraphics;
};
button.innerHTML = "updates: "+getUpdatesGraphics;
return button;
}
function defaultSlidePos(s) {
return s < 3 ? 0 : swiper[s].slides.length-1;
}
function getSlideNames() {
var names = []
for (var s=0; s<MAXBLOCK; s++) {
var sw = swiper[s];
var name = "";
if (sw.activeIndex != defaultSlidePos(s) && sw.slides.length > 0) {
name = sw.slides[sw.activeIndex].slideType;
}
names.push();
}
for (var s=MAXBLOCK-1; s>=0; s--) {
if (names[s] != "") break;
names.pop();
}
return names;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
client/res/go_to_now.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

BIN
client/res/menu_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 74 KiB

BIN
client/res/x_zoom_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
client/res/y_zoom_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,9 +0,0 @@
{"name":"",
"short_name":"",
"icons":[
{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},
{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],
"theme_color":"#ffffff",
"background_color":"#ffffff",
"display":"fullscreen"}

View File

@ -18,7 +18,7 @@ def assign_colors_to_curves(blocks):
auto_curves = [] auto_curves = []
for curve in block["curves"]: for curve in block["curves"]:
col = curve.get("color", "").strip() col = curve["color"].strip()
c = ColorMap.to_code(col) c = ColorMap.to_code(col)
if c < 0: if c < 0:
valid = ColorMap.check_hex(col) valid = ColorMap.check_hex(col)

View File

@ -1,8 +1,6 @@
[chart] [chart]
tt=unit:K tt=unit:K
tt.target=unit:K tt.target=unit:K
tt.set_power=unit:W
tt.power=unit:W
cc=- cc=-
hemot.target=- hemot.target=-
mf=unit:T mf=unit:T
@ -11,32 +9,11 @@ ts=unit:K
ts.target=unit:K ts.target=unit:K
treg=- treg=-
tmon=- tmon=-
T_oneK=unit:K,color:yellow T_oneK=unit:K
T_sample=unit:K,color:blue T_sample=unit:K
T_samplehtr=unit:K,color:black T_samplehtr=unit:K
T_mix=unit:K,color:cyan T_mix=unit:K
T_sorb=unit:K,color:dark_violet T_sorb=unit:K
T_sorb.target=- T_sorb.target=-
T_still=unit:K,color:orange
dil=- dil=-
dil.G1=unit:mbar lev=unit:%
dil.G2=unit:mbar
dil.G3=unit:mbar
dil.P1=unit:mbar
dil.P2=unit:mbar
dil.v6pos=unit:%
dil.V12A=unit:%
lev=unit:%,color:brown
lev.n2=unit:%,color:black
hefill=-
ln2fill=-
hepump=-
hemot=-
flow_sensor=-
nv.speed=unit:1
nv.flow=unit:ln/min
nv.flowtarget=unit:ln/min
nv.flowp=unit:ln/min
stickrot=unit:deg
tcoil1=*_coil,unit:K
tcoil2=*_coil,unit:K

View File

@ -1,5 +1,4 @@
[INFLUX] [INFLUX]
url=http://linse-a:8086 url=http://localhost:8086
org=linse org=linse
bucket=curve-test
token=zqDbTcMv9UizfdTj15Fx_6vBetkM5mXN56EE9CiDaFsh7O2FFWZ2X4VwAAmdyqZr3HbpIr5ixRju07-oQmxpXw== token=zqDbTcMv9UizfdTj15Fx_6vBetkM5mXN56EE9CiDaFsh7O2FFWZ2X4VwAAmdyqZr3HbpIr5ixRju07-oQmxpXw==

View File

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python3
import sys
import argparse
import pathlib
sys.path.insert(0, str((pathlib.Path(__file__) / '..').resolve()))
from webserver import server
from base import Client
from dummy import DummyGraph, DummyHistory
from secop import SecopInteractor
def parseArgv(argv):
parser = argparse.ArgumentParser(
description="start webserver with dummy history and SECoP interaction",
)
parser.add_argument("port",
type=str,
default='8888',
nargs='?',
help="port number to serve")
parser.add_argument('-u',
'--uri',
action='store',
help='SECoP uri',
default='localhost:5000')
return parser.parse_args(argv)
args = parseArgv(sys.argv[1:])
server.run(int(args.port), DummyHistory(args.uri), DummyGraph, Client, single_instrument='dummy', secop=SecopInteractor)

181
dummy.py
View File

@ -1,181 +0,0 @@
import time
import math
import io
from colors import assign_colors_to_curves
from secop import SecopClient
from base import get_abs_time, HandlerBase
class DummyGraph(HandlerBase):
def __init__(self, server, instrument, device, tags):
super().__init__() # put methods w_... to handlers
self.handlers['graphpoll'] = self.graphpoll
self.server = server
self.instrument = instrument
self.device = device
self.tags = tags
self.blocks = []
self.phase = {}
self.end_time = 0
phase = 0
for i in range(5):
curves = []
for j in range(5 - i):
name = f'curve{i}{j}'
curves.append({'name': name, 'label': name.title(), 'color': str(j+1)})
self.phase[name] = phase
phase += 15
unit = 'ABCDEFG'[i]
self.blocks.append({'tag': unit, 'unit': unit, 'curves': curves})
def dummy_fun(self, var, t):
return math.sin(math.radians(t % 3600 / 10 - self.phase[var])) + 1.1
def get_curves(self, variables, start, end):
result = {}
step = 5 * max(1, (end - start) // 1000)
for i, var in enumerate(variables):
result[var] = [(t, self.dummy_fun(var, t)) for t in range(start - start % step, end + 1, step)]
time.sleep(0.5)
return result
def w_graph(self, variables, time="-1800,0", interval=None):
"""
Gets the curves given by variables in the time range "time", spaced by "interval" if given (binning/resolution)
Called when the route /graph is reached.
Parameters :
variables (str) : a comma separataed value string of variable names (influx names) to retrieve
time (str) : a commma separated value string (range) of seconds. They are treated as relative from now
if they are lesser than one year.
interval (str) : the interval (resolution) of the values to get (string in milliseconds)
Returns :
{"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionnary with its "graph-draw" type
(so it can be processed by the client), and a "graph" dictionnary with the variable names as key,
and an array of points as a tuple (timestamp, y-value as float)
"""
time = [float(t) for t in time.split(',')]
start, end, now = get_abs_time(time + [0])
start, end, now = int(start), int(end), int(now)
#self.livemode = self.ACTUAL if end+10 >= now else self.HISTORICAL
return dict(type='graph-draw', graph=self.get_curves(variables.split(','), start, end))
def w_gettime(self, time):
"""
Gets the server time for the give time.
Called when the route /gettime is reached.
Parameters :
time (str="-1800,0") : the given point in time represented by a string, which is a comma separated unix
timestamp values list (in seconds). They are treated as relative from now if they are lesser than one year.
Returns :
{"type":"time", "time":(int)} : a dictionnary with its "time" type (so the data can be processed by the
client) and the server unix timestamp in seconds corresponding to the time asked by the client
"""
time = [float(t) for t in time.split(',')]
return dict(type='time', time=get_abs_time(time))
def w_getvars(self, time, userconfiguration = None):
"""
Gets the curve names available at a given point in time, with a possible user configuration on the client side.
Called when the route /getvars is reached.
Parameters :
time (str) : the given point in time represented by a string, which is a unix timestamp in seconds.
It is treated as relative from now if it is lesser than one year.
userconfiguration (str|None) : the JSON string representing the user configuration
Returns :
{"type":"var_list", "device":(str), "blocks":[{"tag":(str),"unit":(str), "curves":
[{"name":(str), "label":(str), "color":(str), "original_color":(str)}]}]}:
a dictionnary with its "var_list" type (so the data can be processed by the client), the device that
was currently set at that time, and the available curves with the name of the internal variable,
the color to display for this curve, its original color in SEA, grouped by their tag (which is a
category or unit if absent) and their unit (in "blocks")
"""
time = [float(t) for t in time.split(',')]
assign_colors_to_curves(self.blocks)
result = dict(type='var_list')
result['blocks'] = self.blocks
result['device'] = 'dummy'
return result
def w_updategraph(self):
"""
Sets the current visualisation mode to LIVE if not in HISTORICAL mode.
Called when the route /updategraph is reached.
Returns :
{"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live"
value telling if the server could change its visualization mode to live
"""
return dict(type='accept-graph', live=True)
def w_export(self, variables, time, nan, interval):
"""
Returns the bytes of a dataframe with the curves given by variables in the time range "time"
Called when the route /export is reached.
Parameters :
variables (str) : a comma separataed value string of variable names (influx names) to retrieve
time (str) : a commma separated value string (range) of seconds.
nan (str) : the representation for NaN values in the TSV
interval (str) : the interval (resolution) of the values to get (string in seconds)
Returns :
io.BytesIO : an BytesIO object containing the dataframe to retrieve
"""
mem = io.BytesIO()
return mem
def graphpoll(self):
"""
Polls the last known values for all the available variables, and returns only those whose polled values
are more recent than the most recent displayed one.
Every plain minute, all the variables are returned with a point having their last known value at the current
timestamp to synchronize all the curves on the GUI.
Returns :
{"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None :
a dictionnary with its "graph-update" type
(so it can be processed by the client), and a "graph" dictionnary with the variable names as key,
and an array of points, which are an array containing the timestamp
as their first value, and the y-value in float as their second one
"""
now, = get_abs_time([0])
if not self.end_time:
self.end_time = now
return None
result = self.get_curves(self.phase, self.end_time, now)
for variable, values in list(result.items()):
# removes points older than the last known point
# (queries are in seconds and might return points already displayed)
while values and values[0][0] < self.end_time:
values.pop(0)
if not values or values[-1][0] > self.end_time:
del result[variable]
self.end_time = now
if len(result) > 0:
return dict(type='graph-update', time=now, graph=result)
return None
class SecopDummyClient(SecopClient):
def poll(self):
messages = super().poll()
msg = self.graphpoll()
if msg:
messages.append(msg)
return messages
class DummyHistory:
def __init__(self, stream):
self.stream = stream
def get_streams(self, instrument=None, **kwds):
return {self.stream: {'device': 'dummy'}}

724
influxdb.py Normal file
View File

@ -0,0 +1,724 @@
from influxdb_client import InfluxDBClient
from configparser import ConfigParser
import ast
from datetime import datetime
from pandas import DataFrame as df, merge_ordered
from numpy import NaN
MEASURMENT_PREFIX = "nicos/se_"
BUCKET_PREFIX = "nicos-cache-"
class InfluxDB:
"""
Class used to handle the connection with the InfluxDB instance
Attributes :
_client (InfluxDBClient) : the InfluxDB client
"""
def __init__(self):
config = ConfigParser()
config.read("./config/influx.ini")
self._client = InfluxDBClient(url=config["INFLUX"]["url"], token=config["INFLUX"]["token"],
org=config["INFLUX"]["org"])
def disconnet(self):
"""
Disconnects from the InfluxDB instance
"""
self._client.close()
def query(self, query_str):
"""
Executes the query on the InfluxDB instance
Parameters :
query_str (string) : the Flux query to execute
Returns :
TableList : an InfluxDB list of the tables returned by the query
"""
return self._client.query_api().query(query_str)
def query_data_frame(self, query_str):
"""
Executes the query on the InfluxDB instance and gets the response as a pandas DataFrame
Parameters :
query_str (string) : the Flux query to execute
Returns :
DataFrame : the query response as a DataFrame
"""
return self._client.query_api().query_data_frame(query_str)
class PrettyFloat(float):
"""saves bandwidth when converting to JSON
a lot of numbers originally have a fixed (low) number of decimal digits
as the binary representation is not exact, it might happen, that a
lot of superfluous digits are transmitted:
str(1/10*3) == '0.30000000000000004'
str(PrettyFloat(1/10*3)) == '0.3'
"""
def __repr__(self):
return '%.15g' % self
class InfluxDataGetter:
"""
Class used to get data from InfluxDB.
Attributes :
_bucket (str) : the name of the InfluxDB bucket to query (used for all queries)
_db (InfluxDB) : the InfluxDB instance of the database to query
"""
def __init__(self, db, instrument_name):
"""
Parameters :
db (InfluxDB) : the InfluxDB instance of the database to query
instrument_name (str) : the name of the instrument from which the data will be got
"""
self._bucket = BUCKET_PREFIX + instrument_name
self._db = db
# ----- PUBLIC METHODS
def get_available_variables_at_time(self, time, chart_configs = None, user_config = None):
"""
Gets the available variables (those that we can have a value for since the device has been installed on the instrument) at the given point in time.
Here, a variable means : SECOP module name + parameter. By default, this method returns the parameters "value" and "target", unless the config files used in chart_configs or user_config indicates other directives.
Parameters :
time (int) : the unix timestamps in seconds of the point in time to get the variables at.
chart_configs ([ChartConfig] | None) : an array of objects, each holding a configuration file for the chart. Configurations are applied in the order of the list.
user_config ({(str):{"cat":(str), "color":(str), "unit":(str)}} | None) : the Python dict representing the user configuration, applied at the end. The key is <secop_module.parameter>.
Returns :
[{"tag":(str), "unit":(str), "curves":[{"name":(str), "label":(str), "color":(str)}]}] : a list of dictionnaries, each one representing
a block of curves with their name, their label and their color to display, grouped by their category if given or unit (in tag).
"""
all_setup_info = self._get_all_setup_info_as_dict(time)
available_variables = self._extract_variables(all_setup_info)
if not chart_configs == None:
for chart_config in chart_configs:
available_variables = self._filter_params_with_config(available_variables, chart_config)
if not user_config == None:
available_variables = self._filter_params_with_user_config(available_variables, user_config)
available_variables = self._remove_variables_params_not_displayed(available_variables)
available_variables = self._remove_variables_params_wihout_param_float_and_split(available_variables, time)
res = self._group_variables_by_cat_unit(available_variables)
return res
def get_curves_in_timerange(self, variables, time, interval = None):
"""
Gets the curves for the given variables within a timerange.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the curves for
time ([int]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int|None) : the interval (resolution) of the values to get (in nanoseconds)
Returns :
{(str):[[(int), (float)]]} : a dictionnary of curves. The key is the name of the influx variable, and the value is an array of pairs (also arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y).
"""
res = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
curve = self._get_curve(variable_name_for_query, parameter, time, interval)
res[variable] = curve
return res
def poll_last_values(self, variables, lastvalues, end_time):
"""
Polls the lastest values for the given variables since their last known point to end_time.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the last known values for
lastvalues ({(str):((float), (float))}) : a dictionnary of tuples, first value being the floating Unix timestamp in seconds (precision = ms) of the last known value for the curve, and the second value being the associated value, indexed by the curve name
end_time (int) : the Unix timestamp in seconds of the last point in time to include the values in
Returns :
{(str):[[(int), (float)]]} : a dictionnary of points. The key is the name of the influx variable, and the value is an array of pairs (also array), the first value being the Unix timestamp in second (x), the seconds being the value (y).
"""
res = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
start_time = int(lastvalues[variable][0]) if variable in lastvalues.keys() else None #if the call to poll_last_values is more recent than getgraph, we trigger only one value, which is the last one available from 0 to end_time
points = []
if start_time == None or start_time < end_time: # start_time might be more recent than end_time if getgraph is called between graphpoll and pol_last_values (influxgraph.py)
points = self._get_last_values(variable_name_for_query,parameter,start_time, end_time)
if len(points) > 0 :
res[variable] = points
return res
def get_device_name(self, time):
"""
Gets the device name available at time with stick and addons
Parameters :
time (int) : the Unix timestamp in seconds of the time we want the device name
Returns :
str : the device name
"""
components = self._get_device_name_components(time)
return "/".join(components)
def get_curves_data_frame(self, variables, times, interval, variables_name_label_map=None):
"""
Gets the curves for the given variables within a timerange times, as a pandas dataframe.
All curves are on a single common time axis.
The first column is called "relative", and consists of floating seconds, relative to the beginning of the query.
The "timestamp" column (absolute floating UNIX timestamps in seconds, precise to the nanosecond) is the last one.
If a curve does not have a point at a given point, the last known value for this curve is used.
If a curve gets expired, it is filled with NaN value for the corresponding expired time windows.
The first value (for each variable) is the last known value before the time interval.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the curves for.
times ([int]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int) : the interval (resolution) of the values to get (in seconds). Allows data binning.
variables_name_label_map ({(str):(str)} | None) : a dictionnary containing curve labels, indexed by INFLUX names. The corresponding label will be used in the TSV header for each variable if found, else the influx name.
Returns :
pandas.DataFrame : the curves in a single pandas DataFrame
"""
variables_info = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
# we need to rename the "_time" column to simply "time" in case we want binning because of the comparison done later in the "binned points with same timestamp" process.
# chr(34) is the double quote char, because we cannot escape them in a f string
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {times[0]}, stop: {times[1] + 1})
|> filter(fn : (r) => r._measurement == "{variable_name_for_query}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
{"|> aggregateWindow(every: duration(v: "+ str(self._seconds_to_nanoseconds(interval))+"), fn: last, createEmpty:false, timeDst:"+chr(34)+"binning_time"+chr(34)+")" if interval != "None" else ""}
|> map(fn: (r) => ({{r with relative: ( float(v: uint(v: {"r.binning_time" if interval != "None" else "r._time"}) - uint(v:{self._seconds_to_nanoseconds(times[0])})) / 1000000000.0 )}}))
|> map(fn: (r) => ({{r with timestamp: float(v: uint(v: {"r.binning_time" if interval != "None" else "r._time"})) / 1000000000.0}}))
{"|> rename(columns: {_time:"+chr(34)+"time"+chr(34)+"})" if interval != "None" else ""}
|> drop(columns:["_start", "_stop", "_field"])
|> pivot(rowKey:["relative", "timestamp", "expired"{", "+chr(34)+"time"+chr(34) if interval != "None" else ""}], columnKey: ["_measurement"], valueColumn: "_value")
"""
data_frame = self._db.query_data_frame(query)
# If there is a binning asked, there can be two points with the same timestamp/relative value, because points with expired=True and expired=False can be found in the same interval, leading to two rows, one with last True, and one with last False.
# For each identified couple, we look at the real timestamp of the points used to feed this interval. We then keep the value which is the more recent in this interval.
if interval != "None" and not data_frame.empty:
# we first identify the expired points
expired_rows = data_frame.loc[data_frame["expired"] == "True"]
# we use itertuples to preserve the pandas dtypes, so comparisons can be done. "tuple" is a Python named tuple
for expired_point_tuple in expired_rows.itertuples():
# Then, we identify if there is a valid point with the same relative time as the current expired point
corresponding_valid_point = data_frame.loc[(data_frame["expired"] == "False") & (data_frame["relative"] == expired_point_tuple.relative)]
# we make sure that there is only one corresponding valid point, even if in theory, there will never be more than one corresponding valid point
if not corresponding_valid_point.empty and len(corresponding_valid_point.index) == 1:
# if we did not rename "_time" to "time" sooner in the query, "_time" would have been renamed to a positionnal name (because it starts with a _, see itertuples() doc), making confusion while reading the code
if corresponding_valid_point.iloc[0]["time"] > expired_point_tuple.time:
data_frame.drop(expired_point_tuple.Index, inplace=True)
else:
data_frame.drop(corresponding_valid_point.index, inplace=True)
# we do not need the "time" column anymore
data_frame.drop(["time"], axis=1, inplace=True)
# Needed for last known value
query_last_known = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {times[0] + 1})
|> filter(fn : (r) => r._measurement == "{variable_name_for_query}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
|> last()
|> map(fn: (r) => ({{r with relative: 0.0}}))
|> map(fn: (r) => ({{r with timestamp: float(v: uint(v: r._time)) / 1000000000.0}}))
|> drop(columns:["_start", "_stop", "_field"])
|> pivot(rowKey:["relative", "timestamp", "expired"], columnKey: ["_measurement"], valueColumn: "_value")
"""
data_frame_last_known = self._db.query_data_frame(query_last_known)
row_to_insert = None
for index, row in data_frame_last_known.iterrows():
try: #needed because row_to_insert == None is not possible
if row_to_insert.empty or row["timestamp"] > row_to_insert["timestamp"]:
row_to_insert = row
except:
row_to_insert = row
try: #row_to_insert might be None
if not row_to_insert.empty :
row_to_insert["timestamp"] = float(times[0])
if data_frame.empty:
data_frame = row_to_insert.to_frame().T
else:
data_frame.loc[-1] = row_to_insert
except:
pass
if data_frame.empty:
continue
variable_df_column_name = variables_name_label_map.get(variable, variable) if not variables_name_label_map == None else variable
data_frame.rename(columns={variable_name_for_query : variable_df_column_name}, inplace=True)
data_frame.drop(["result", "table"], axis=1, inplace=True)
data_frame.sort_values(by=["timestamp"], inplace=True)
data_frame.reset_index()
variables_info[variable_df_column_name] = {}
variables_info[variable_df_column_name]["expired_ranges"] = []
# Identify time windows for which the curve is expired
for index, row in data_frame.iterrows():
if row["expired"] == "True":
data_frame.loc[index, variable_df_column_name] = NaN
variables_info[variable_df_column_name]["expired_ranges"].append([row["timestamp"]])
elif row["expired"] == "False":
if len(variables_info[variable_df_column_name]["expired_ranges"]) > 0 and len(variables_info[variable_df_column_name]["expired_ranges"][-1]) == 1:
variables_info[variable_df_column_name]["expired_ranges"][-1].append(row["timestamp"])
data_frame.reset_index()
data_frame.drop(["expired"], axis=1, inplace=True)
variables_info[variable_df_column_name]["df"] = data_frame
res = None
non_empty_variables = list(variables_info.keys())
# Merge single curve dataframes to a global one
if len(non_empty_variables) == 0:
return df()
elif len(non_empty_variables) == 1:
res = variables_info[non_empty_variables[0]]["df"]
else :
for i in range(0, len(non_empty_variables)):
if i == 1:
res = merge_ordered(variables_info[non_empty_variables[0]]["df"], variables_info[non_empty_variables[1]]["df"], on=["timestamp", "relative"], suffixes=(None, None))
elif i > 1:
res = merge_ordered(res, variables_info[non_empty_variables[i]]["df"], on=["timestamp", "relative"], suffixes=(None, None))
# Forward fill missing values, then set data points to NaN for those which are expired
if len(non_empty_variables) > 1:
res.ffill(inplace=True)
for variable, info in variables_info.items():
for expired_range in info["expired_ranges"]:
res.loc[(res["timestamp"] >= expired_range[0]) & ((res["timestamp"] < expired_range[1]) if len(expired_range) == 2 else True), variable] = NaN
# Change order of columns
cols = res.columns.tolist()
cols = [cols[0]] + cols[2:] + [cols[1]]
res = res[cols]
return res
# ----- PRIVATE METHODS
def _get_all_setup_info_as_dict(self, time):
"""
Gets the value of the field setup_info in the measurements nicos/se_main, nicos/se_stick, nicos/se_addons as an array of Python dicts.
Takes the last setup_info dict (for each measurement) known at time.
Parameters
time (int) : the unix timestamps in seconds of the point in time to get the variables at.
Returns :
[{(str):((str), {...})}]: an array of the parsed "setup_info dict" of each measurements. The key is the secop_module prefixed with "se_", and the value is a tuple with its first value
being the type of Secop device for this module, and the value is too big to give its signature. Some tuple examples can be found under graphs/setup_info_examples.
"""
measurements = ["nicos/se_main", "nicos/se_stick", "nicos/se_addons"]
res = []
for measurement in measurements:
query = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {time + 1})
|> filter(fn: (r) => r._measurement == "{measurement}")
|> filter(fn: (r) => r._field == "setup_info")
|> last()
|> yield(name: "res")
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
res.append(ast.literal_eval(record.get_value()))
return res
def _extract_variables(self, all_setup_info_dict):
"""
Extracts relevant information out of the setup_info dict for each available variable in measurements nicos/se_main, nicos/se_stick, nicos/se_addons
Parameters :
all_setup_info_dict ([{(str):((str), {...})}]) : an array of the parsed "setup_info dict" of each measurements. The key is the secop_module prefixed with "se_", and the value is a tuple with its first value
being the type of Secop device for this module, and the value is too big to give its signature. Some tuple examples can be found under graphs/setup_info_examples.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnares with the category ("*" for value and target, "-" else), the color (empty for the moment)
and the unit ("1" if not available or empty), indexed by the name of the parameter.
"""
available_varirables = []
added_names = []
for setup_info_dict in all_setup_info_dict:
for (_, content) in setup_info_dict.items():
if content[0] != "nicos.devices.secop.devices.SecopDevice":
name = self._transform_secop_module_name_to_influx(content[1]["secop_module"])
if name not in added_names:
value_unit = "1" if (not "unit" in content[1].keys() or content[1]["unit"] == "") else content[1]["unit"]
variable = {
"name":name,
"label":content[1]["secop_module"],
"params":{"value":{"cat":"*", "color":"", "unit":value_unit}} # main value, shown by default
}
for param_name, param_content in content[1]["params_cfg"].items():
param_unit = "1" if (not "unit" in param_content.keys() or param_content["unit"] == "") else param_content["unit"]
variable["params"][param_name] = {
"cat":"*" if param_name == "target" else "-", # target is also shown by default, not the other parameters
"color":"",
"unit":param_unit
}
available_varirables.append(variable)
added_names.append(name)
return available_varirables
def _transform_secop_module_name_to_influx(self, secop_module_name):
"""
Transforms the name of the variable available in the setup_info dict into the Influx name.
Lowers the secop_module_name and adds "nicos/se_" as prefix
Parameters :
secop_module_name (str) : the secop module name of the variable in the setup_info dict.
Returns :
str : the transformed variable name that matches the Influx names reqauirements
"""
return MEASURMENT_PREFIX + secop_module_name.lower()
def _filter_params_with_config(self, available_variables, chart_config):
"""
Updates (cat, color, unit) the parameters of each variable according to the user_config object.
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
chart_config (ChartConfig) : the object holding a configuration file for the chart.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
key = variable["label"] if param_key == "value" else variable["label"]+"."+param_key
param_config = chart_config.get_variable_parameter_config(key)
if param_config != None :
for key, value in param_config.items():
variable["params"][param_key][key] = value
return available_variables
def _filter_params_with_user_config(self, available_variables, user_config):
"""
Updates (cat, color, unit) the parameters of each variable according to the user_config object.
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
user_config ({(str):{"cat":(str), "color":(str), "unit":(str)}}) : the Python dict representing the user configuration. The key is <secop_module.parameter>.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
key = variable["label"] if param_key == "value" else variable["label"]+"."+param_key
param_config = user_config[key] if key in user_config.keys() else None
if param_config != None :
for key, value in param_config.items():
variable["params"][param_key][key] = value
return available_variables
def _remove_variables_params_not_displayed(self, available_variables):
"""
Removes the parameters of each variable if their category is "-".
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
if variable["params"][param_key]["cat"] == "-":
del variable["params"][param_key]
return available_variables
def _remove_variables_params_wihout_param_float_and_split(self, available_variables, time):
"""
For each variable, removes the parameters if the Influx database does not contain <param>.float field, and split the parameters to the corresponding output format.
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnares with the category, the color and the unit, indexed by the name of the parameter.
time (int) : the unix timestamp in seconds of the point in time to get the variables at. Used to have an upper limit in the query.
Returns :
[{"name":(str), "label":(str), "cat":(str), "color":(str), "unit":(str)}] : an array of dictionnaries, each containing the name of the variable[.<param>],
the label to display in the Web GUI, its category, its color and its unit.
"""
res = []
for variable in available_variables:
query = f"""
import "influxdata/influxdb/schema"
schema.measurementFieldKeys(bucket: "{self._bucket}", measurement: "{variable["name"]}", start:0, stop: {time + 1})
|> yield(name: "res")
"""
records = self._db.query(query)[0].records
fields = [record.get_value() for record in records]
for param in variable["params"].keys():
if param+"_float" in fields:
res.append({
"name": variable["name"] if param == "value" else variable["name"]+"."+param,
"label": variable["label"] if param == "value" else variable["label"]+"."+param,
"cat": variable["params"][param]["cat"],
"color": variable["params"][param]["color"],
"unit": variable["params"][param]["unit"]
})
return res
def _group_variables_by_cat_unit(self, available_variables):
"""
Performs a group by cat if specified (different than "*"), or by unit instead for the available variables
Parameters :
available_variables ([{"name":(str), "label":(str), "cat":(str), "color":(str), "unit":(str)}]) : an array of dictionnaries, each containing the name of the variable[.<param>],
the label to display in the Web GUI, its category, its color and its unit.
Returns :
[{"tag":(str), "unit":(str), "curves":[{"name":(str), "label":(str), "color":(str)}]] : a list of dictionnaries, each one representing
a block of curves with their name, their label and their color to display, grouped by their tag, which is the unit or the category if given and not "unit".
"""
groups = {}
for available_variable in available_variables:
key = available_variable["unit"]
if available_variable["cat"] != "*":
key = available_variable["cat"]
if key not in groups.keys():
groups[key] = {"tag":key, "unit":available_variable["unit"], "curves":[]}
groups[key]["curves"].append({
"name":available_variable["name"],
"label":available_variable["label"],
"color":available_variable["color"],
})
return list(groups.values())
def _get_curve(self, variable, parameter, time, interval=None):
"""
Gets the points (curve) within a timerange for the given variable and parameter.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
parameter (str) : the parameter of the variable to get the values from
time ([(int)]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int) : the interval (resolution) of the values to get (in milliseconds)
Returns :
[[(int), (float)]] : an array of pairs (also arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y)
"""
raw = []
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {time[0]}, stop: {time[1] + 1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
{"|> aggregateWindow(every: duration(v:"+str(self._milliseconds_to_nanoseconds(interval))+"), fn: last, createEmpty:false, timeDst:"+chr(34)+"binning_time"+chr(34)+")" if interval else ""}
|> keep(columns: ["_time","_value","expired"{", "+chr(34)+"binning_time"+chr(34) if interval else ""}])
|> yield(name: "res")
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record["binning_time"] if interval else record.get_time()), 3) # t is the real timestamp if no interval is given, or the binned timestamp if interval
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
point = [t, value, record["expired"]]
if interval: # t is the binning time, we need to add the real time of the point that was used in this interval
point.append(record.get_time())
raw.append(point)
if interval:
indexes_to_delete = []
expired_points = {i:point for i,point in enumerate(raw) if point[2] == "True"} #we need to keep track of the indexes of the expired point in the list
for expired_point_index, expired_point in expired_points.items():
for i, point in enumerate(raw):
if point[2] == "False" and expired_point[0] == point[0]: # if the current point is expired and has the same binning time as the current expired point
if point[3] > expired_point[3]: # comparison on the real timestamp used.
indexes_to_delete.insert(0, expired_point_index)
else:
indexes_to_delete.insert(0,i)
sorted(indexes_to_delete, reverse=True) #we have to make sure that the list is sorted in reverse to then delete at the given indexes
for index in indexes_to_delete:
del raw[index]
sorted_raw = sorted(raw, key=lambda pair: pair[0]) #expired=True and expired=False are in two Influx tables, so they need to be synchronized
res = []
for pair in sorted_raw:
if pair[2] == "True":
res.append([pair[0], pair[1]]) # So the user can know precisely when a curve is expired
res.append([pair[0], None]) # So chartJS will cut the curve from this point (which is expired)
else:
res.append([pair[0], pair[1]])
return self._insert_last_known_value(variable, parameter, res, time)
def _insert_last_known_value(self, variable, parameter, curve, time):
"""
Adds the last known value as the first point in the curve if the last known value is outside the viewing window, for the given variable and parameter.
The point is added only if it is not expired.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
parameter (str) : the parameter of the variable to get the values from
curve ([[(int), (float)]]) : an array of pairs (arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y)
time ([(int)]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
Returns :
[[(int), (float)]] : the curve of the parameter, updated with a potential new first point
"""
if len(curve) == 0 or curve[0][0] != time[0]:
query = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {time[0]+1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
|> last()
|> keep(columns: ["_time", "_value", "expired"])
|> yield(name: "res")
"""
tables = self._db.query(query)
pair_to_insert = []
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record.get_time()), 3)
value = None
if record["expired"] == "False":
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
if len(pair_to_insert) == 0 or t >= pair_to_insert[0]:
pair_to_insert = [t, value]
if len(pair_to_insert)==2 and pair_to_insert[1] != None:
curve.insert(0, [time[0], pair_to_insert[1]])
return curve
def _get_last_values(self, variable, parameter, start_time, end_time):
"""
Gets the lastest values for the given variable and parameter that are in [start_time, end_time].
The process is the same as _get_curve.
Parameters :
variable (str) : the name (Influx) of the variable we want the last value of.
parameter (str) : the parameter of the variable to get the values from
start_time (int|None) : the start of time range (Unix timestamp in seconds) to include the values in
end_time (int) : the end of time range (Unix timestamp in seconds) to include the values in
Returns :
[[(int), (float)]] : an array of points (also arrays). The first value is the Unix timestamp in second (x), the seconds is the value (y)
"""
raw = []
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {start_time if start_time != None else 0}, stop: {end_time+1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+ "_float"}")
{"|> last()" if start_time == None else ""}
|> keep(columns: ["_time","_value", "expired"])
|> yield(name: "res")
"""
# this loop might be simplified, but it has to be kept to catch the case when there is unavailable data
tables = self._db.query(query)
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record.get_time()), 3)
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
raw.append([t, value, record["expired"]])
sorted_raw = sorted(raw, key=lambda pair: pair[0])
res = []
for pair in sorted_raw:
if pair[2] == "True":
res.append([pair[0], pair[1]])
res.append([pair[0], None])
else:
res.append([pair[0], pair[1]])
return res
def _get_device_name_components(self, time):
"""
Gets the components of the device name, first in the main name, then stick, then addons.
Parameters :
time (int) : the Unix timestamp in seconds of the time we want the device name
Returns :
[str] : an array of string, each one being a component of the device name
"""
measurements = ["nicos/se_main", "nicos/se_stick", "nicos/se_addons"]
res = []
for measurement in measurements:
query = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {time + 1})
|> filter(fn: (r) => r._measurement == "{measurement}")
|> filter(fn: (r) => r._field == "value")
|> last()
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
name = ast.literal_eval(record.get_value())
if name != None and name != '':
res.append(ast.literal_eval(record.get_value()))
return res
def _seconds_to_nanoseconds(self, seconds):
return seconds * 1000000000
def _milliseconds_to_nanoseconds(self, milliseconds):
return milliseconds * 1000000

View File

@ -1,287 +1,154 @@
from time import time as current_time import time
import logging import logging
from colors import assign_colors_to_curves
import json import json
import io import io
import uuid
# from configparser import ConfigParser
from math import ceil
from sehistory.seinflux import fmtime
from colors import assign_colors_to_curves
from chart_config import ChartConfig from chart_config import ChartConfig
from base import get_abs_time, HandlerBase
class InfluxGraph:
def split_tags(tags): """
return {k: v.split(',') for k, v in tags.items()} Class implementing the logic of the different routes that are called by the client to retrieve graph data with InfluxDB.
class InfluxGraph(HandlerBase):
"""Class implementing the logic of the different routes that are called by the client to retrieve graph data with InfluxDB.
Global constants : Global constants :
HISTORICAL (int) : value that represents the "historical" visualization mode, meaning that the HISTORICAL (int) : value that represents the "historical" visualization mode, meaning that the most recent point is not in the visualisation window (no live data is sent).
most recent point is not in the visualisation window (no live data is sent). ACTUAL (int) : value that represents the "actual" visualization mode, wihch is an intermediate state used before going for live mode (the requested time window includes now)
ACTUAL (int) : value that represents the "actual" visualization mode, wihch is an intermediate LIVE (int) : value that represents the "live" visualization mode, meaning that new points are sent to the client.
state used before going for live mode (the requested time window includes now)
LIVE (int) : value that represents the "live" visualization mode, meaning that new points are
sent to the client.
Attributes : Attributes :
influx_data_getter (InfluxDataGetter) : the InfluxDataGetter instance that allows to get data out of InfluxDB. influx_data_getter (InfluxDataGetter) : the InfluxDataGetter instance that allows to get data out of InfluxDB.
chart_configs ([ChartConfig]) : an array of chart configuration to apply when /getvars is called chart_configs ([ChartConfig]) : an array of chart configuration to apply when /getvars is called
livemode (int) : the type of visualization the user is currently in. Can be HISTORICAL, ACTUAL or LIVE. livemode (int) : the type of visualization the user is currently in. Can be HISTORICAL, ACTUAL or LIVE.
end_query (int) : the unix timestamp in seconds of the most recent requested point in time of the last query end_query (int) : the unix timestamp in seconds of the most recent requested point in time of the last query or update.
or update. lastvalues ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values are tuples, where the first
last_values ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values value is the unix timestamp of the most recent value known for this variable, and the second value its corresponding value
are tuples, where the first value is the unix timestamp of the most recent value known for this variable, variables ({(str):(str)}) : a dictionnary of the current available variables requested by the client. The key is the InfluxDB name of the curve, and the value is its label in the GUI.
and the second value its corresponding value
variables ({(str):(str)}) : a dictionary of the current available variables requested by the client.
The key is the InfluxDB name of the curve, and the value is its label in the GUI.
""" """
HISTORICAL = 0 HISTORICAL = 0
ACTUAL = 1 ACTUAL = 1
LIVE = 2 LIVE = 2
def __init__(self, server, instrument, device_name, tags): def __init__(self, influx_data_getter, instrument):
"""create instance for retrieving history self.influx_data_getter = influx_data_getter
self.chart_configs = [ChartConfig("./config/generic.ini"), ChartConfig(f"./config/{instrument}.ini")]
:param db: a database client (SEInflux instance)
:param instrument: the name of anm instrument or None
:param streams: a stream or comma separated list of streams
:param devices: a device name ar a comma separated list of devices
:param device_name: (comma separated) device name for labelling
typically only one of the 3 last parameters are needed
if more are specified, all of them must be fulfilled
"""
super().__init__() # put methods w_... to handlers
self.handlers['graphpoll'] = self.graphpoll
self.server = server
self.db = server.db
# self.influx_data_getter = influx_data_getter
self.chart_configs = ["./config/generic.ini"]
self.instrument = instrument
self.device_name = device_name
if instrument: # TODO: should it not be better to have inifiles per device?
self.chart_configs.append(f"./config/{instrument}.ini")
self.livemode = self.HISTORICAL self.livemode = self.HISTORICAL
self.last_values = {} # dict <variable> of last known point (<time>, <value>) self.end_query = 0
self.last_time = {} # dict <stream> of last received time self.lastvalues = {}
self.last_minute = 0 self.variables = {} # name:label
self.last_update = 0 # time of last call with a result
self.tags = None
self.init_tags = tags def get_abs_time(self, times):
"""
Gets the absolute times for the given pontential relative times. If the given timestamps are less than one year, then the value is relative
and converted into an asbolute timestamps
Parameters :
times([(float)]) : an array of unix timestamps or relative duration (< 1 year) as floats
Returns :
[(float)] : an array of absolute unix timestamps as floats
"""
now = int(time.time() + 0.999)
oneyear = 365 * 24 * 3600
return [t + now if t < oneyear else t for t in times]
def complete_to_end_and_feed_lastvalues(self, result, endtime):
"""
Completes the data until the last requested point in time by adding the last known y-value at the end point.
Also feeds self.lastvalues.
Parameters :
result ({(str):[[(int),(float)]]}) : a dictionnary with the variable names as key, and an array of points,
which are an array containing the timestamp as their first value, and the y-value in float as their second one.
endtime (int) : the unix timestamp in seconds of the time we want to have data until
"""
for var, c in result.items():
if c:
lastt, lastx = c[-1]
if lastt < endtime:
c.append((endtime, lastx))
self.lastvalues[var] = (lastt, lastx)
def w_graph(self, variables, time="-1800,0", interval=None): def w_graph(self, variables, time="-1800,0", interval=None):
"""Get the curves given by variables in the time range "time" """
Gets the curves given by variables in the time range "time", spaced by "interval" if given (binning/resolution)
spaced by "interval" if given (binning/resolution)
Called when the route /graph is reached. Called when the route /graph is reached.
Parameters : Parameters :
variables (str) : a comma separated string of variable names (influx names) to retrieve variables (str) : a comma separataed value string of variable names (influx names) to retrieve
time (str) : a commma separated value string (range) of seconds. time (str) : a commma separated value string (range) of seconds. They are treated as relative from now if they are lesser than one year.
values < one year are treated as relative from now. interval (str) : the interval (resolution) of the values to get (string in milliseconds)
interval (str) : the interval (resolution) of the values to get (string in seconds)
Returns : Returns :
{"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionary with its "graph-draw" type {"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionnary with its "graph-draw" type (so it can be processed by the client), and a "graph" dictionnary with the variable names as key, and an array of points,
(so it can be processed by the client), and a "graph" dictionary with the variable names as key, which are an array containing the timestamp as their first value, and the y-value in float as their second one.
and an array of points as a tuple (timestamp, y-value as float)
""" """
start, end, now = get_abs_time([float(t) for t in time.split(',')] + [0]) time = [float(t) for t in time.split(',')]
start, end, now = int(start), ceil(end), ceil(now) start, end, now = self.get_abs_time(time + [0])
start, end, now = int(start), int(end), int(now)
queried_time_range = [start, end]
queried_variables = variables.split(',') queried_variables = variables.split(',')
self.livemode = self.ACTUAL if end+10 >= now else self.HISTORICAL self.livemode = self.ACTUAL if end+10 >= now else self.HISTORICAL
logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode) logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode)
if interval: if interval : interval = int(interval)
interval = float(interval)
result = self.db.curves(start, end, queried_variables, merge='_measurement',
interval=interval or None, **self.tags)
self.update_last(result)
self.db.complete(result, self.last_time, 'stream')
self.last_minute = now // 60
return dict(type='graph-draw', graph={k: result[k] for k in queried_variables if k in result})
def update_last(self, curve_dict): result = self.influx_data_getter.get_curves_in_timerange(queried_variables, queried_time_range, interval)
"""update last values per variable and last time per stream""" self.complete_to_end_and_feed_lastvalues(result, min(end, now))
for key, curve in curve_dict.items(): self.end_query = end
stream = curve.tags.get('stream')
tlast, value = curve[-1] return dict(type='graph-draw', graph=result)
self.last_values[key] = curve[-1]
self.last_time[stream] = max(self.last_time.get(stream, 0), tlast)
def w_gettime(self, time): def w_gettime(self, time):
"""Get the server time for the given time(range). """
Gets the server time for the give time.
Called when the route /gettime is reached. Called when the route /gettime is reached.
Parameters : Parameters :
time (str="-1800,0") : the given point in time represented by a string, time (str="-1800,0") : the given point in time represented by a string, which is a comma separated unix timestamp values list (in seconds). They are treated as relative from now if they are lesser than one year.
which is a comma separated unix timestamp values list (in seconds).
values < one year are treated as relative from now.
Returns : Returns :
{"type":"time", "time":(int)} : a dictionary with its "time" type (so the data can be processed by the {"type":"time", "time":(int)} : a dictionnary with its "time" type (so the data can be processed by the client) and the server unix timestamp in seconds corresponding
client) and the server unix timestamp in seconds corresponding to the time asked by the client to the time asked by the client
""" """
return dict(type='time', time=get_abs_time( time = [float(t) for t in time.split(',')]
[float(t) for t in time.split(',')])) return dict(type='time', time= self.get_abs_time(time))
def w_getvars(self, time, userconfiguration=None, **_): def w_getvars(self, time, userconfiguration = None):
"""Get the curve names available at a given point in time """
Gets the curve names available at a given point in time, with a possible user configuration on the client side.
with a possible user configuration on the client side.
Called when the route /getvars is reached. Called when the route /getvars is reached.
Parameters : Parameters :
time (str) : the given point in time represented by a string, which is a unix timestamp in seconds. time (str) : the given point in time represented by a string, which is a unix timestamp in seconds. It is treated as relative from now if it is lesser than one year.
values < one year are treated as relative from now.
Might also be a comma separated time range.
userconfiguration (str|None) : the JSON string representing the user configuration userconfiguration (str|None) : the JSON string representing the user configuration
Returns : Returns :
{"type":"var_list", "device":(str), "blocks":[{"tag":(str),"unit":(str), "curves": {"type":"var_list", "device":(str), "blocks":[{"tag":(str),"unit":(str), "curves":[{"name":(str), "label":(str), "color":(str), "original_color":(str)}]}]} :
[{"name":(str), "label":(str), "color":(str), "original_color":(str)}]}]}: a dictionnary with its "var_list" type (so the data can be processed by the client), the device that was currently set at that time, and the available curves with the name of the internal variable,
a dictionnary with its "var_list" type (so the data can be processed by the client), the device that the color to display for this curve, its original color in SEA, grouped by their tag (which is a category or unit if absent) and their unit (in "blocks")
was currently set at that time, and the available curves with the name of the internal variable,
the color to display for this curve, its original color in SEA, grouped by their tag (which is a
category or unit if absent) and their unit (in "blocks")
""" """
time = get_abs_time([float(t) for t in time.split(',')]) time = [float(t) for t in time.split(',')]
start_time = int(time[0]) end_time = int(self.get_abs_time(time)[-1])
end_time = int(time[-1])
if userconfiguration is not None: if not userconfiguration == None : userconfiguration = json.loads(userconfiguration)
userconfiguration = json.loads(userconfiguration)
blocks = self.influx_data_getter.get_available_variables_at_time(end_time, self.chart_configs, userconfiguration)
device_name = self.influx_data_getter.get_device_name(end_time)
# updates the self.variables attribute to keep track of the available variables
self.variables = {variable["name"]:variable["label"] for block in blocks for variable in block["curves"]}
if self.instrument:
streams, tags, self.device_name = self.server.lookup_streams(self.instrument, **self.init_tags)
self.tags = {**self.init_tags, **tags}
else:
self.tags = self.init_tags
blocks = self.get_available_variables(start_time, end_time, self.chart_configs, userconfiguration)
# initialize self.last_values to keep track of the available variables
self.last_values = {var["name"]: [0, None] for block in blocks for var in block["curves"]}
assign_colors_to_curves(blocks) assign_colors_to_curves(blocks)
# print('DEVICE', device_name, tags) result = dict(type='var_list')
# for block in blocks: result['blocks'] = blocks
# print(block['tag'], [c['name'] for c in block['curves']]) result['device'] = device_name
return {'type': 'var_list', 'blocks': blocks, 'device': self.device_name}
def get_available_variables(self, start_time, end_time, chart_configs=None, user_config=None):
"""Gets the available variables
(those that we can have a value for since the device has been installed
on the instrument) at the given point in time.
Here, a variable means : SECOP module name + parameter.
By default, this method returns the parameters "value" and "target",
unless the config files used in chart_configs or user_config indicates other directives.
Parameters :
start_time, send_time (int) : the unix timestamps in seconds of the point in time to get the variables at.
chart_configs ([ChartConfig] | None) :
an array of objects, each holding a configuration file for the chart.
Configurations are applied in the order of the list.
user_config ({(str):{"cat":(str), "color":(str), "unit":(str)}} | None) :
the Python dict representing the user configuration, applied at the end.
The key is <secop_module.parameter>.
Returns :
[{"tag":(str), "unit":(str), "curves":[{"name":(str), "label":(str), "color":(str)}]}] :
a list of dicts, each one representing
a block of curves with their name, their label and their color to display,
grouped by their category if given or unit (in tag).
"""
if start_time == end_time:
start_time = end_time - 3600
result = self.db.curves(start_time, end_time, _measurement=None,
merge='_measurement', **self.tags)
assert all(c.key_names[0] == '_measurement' for c in result.values())
variables = {k: t.tags.get('unit') for k, t in result.items()}
config = {}
if chart_configs:
for chart_config in chart_configs:
for key, cfg in ChartConfig(chart_config).variables.items():
config.setdefault(key, {}).update(cfg)
if user_config:
for key, cfg in user_config.items():
config.setdefault(key, {}).update(cfg)
groups = {}
def add_to_groups(name, cat=None, unit='1', color='', label=None):
if cat == '-':
return
if name.endswith('.value'):
if not cat:
cat = '*'
if not label:
label = name[:-6]
elif name.endswith('.target'):
if not cat:
cat = '*'
elif not cat:
return
unit = unit or '1'
tag = cat.replace('*', unit)
grp = groups.get(tag)
if grp is None:
crv_dict = {}
groups[tag] = {'tag': cat.replace('*', unit), 'unit': unit, 'curves': crv_dict}
else:
crv_dict = grp['curves']
crv_dict[name] = {'name': name, 'unit': unit, 'label': label or name}
# treat variables in config first (in their order!)
for key, cfg in config.items():
cat = cfg.pop('cat', None)
cfgunit = cfg.pop('unit', '')
if '.' in key:
unit = variables.pop(key, object)
if unit is not object:
add_to_groups(key, cat, cfgunit or unit, **cfg)
else:
var = f'{key}.value'
unit = variables.pop(var, object)
if unit is not object:
label = cfg.pop('label', None) or key
add_to_groups(var, cat, cfgunit or unit, label=label, **cfg)
var = f'{key}.target'
unit = variables.pop(var, object)
if unit is not object:
cfg.pop('color', None)
add_to_groups(var, cat, cfgunit or unit, **cfg)
for var, unit in variables.items():
add_to_groups(var, unit=unit)
# make order a bit more common
result = []
for key in ['K', 'T', 'W', 'ln/min'] + list(groups):
if key in groups:
group = groups.pop(key)
curve_dict = group['curves']
curves = []
# get first '.value' parameters and add targets if available
ordered_keys = [f'{m}.value' for m in ('tt', 'T', 'ts', 'Ts')]
for name in ordered_keys + list(curve_dict):
if name.endswith('.value'):
try:
curves.append(curve_dict.pop(name))
curves.append(curve_dict.pop(f'{name[:-6]}.target'))
except KeyError:
pass # skip not existing or already removed items
# add remaining curves
curves.extend(curve_dict.values())
# print(key, curves)
group['curves'] = curves
result.append(group)
return result return result
def w_updategraph(self): def w_updategraph(self):
"""Set the current visualisation mode to LIVE if not in HISTORICAL mode. """
Sets the current visualisation mode to LIVE if not in HISTORICAL mode.
Called when the route /updategraph is reached. Called when the route /updategraph is reached.
Returns : Returns :
{"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live" {"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live" value telling if the server could change its visualization mode to live
value telling if the server could change its visualization mode to live
""" """
logging.info("UPD GRAPH %d", self.livemode) logging.info("UPD GRAPH %d", self.livemode)
if self.livemode == self.HISTORICAL: if self.livemode == self.HISTORICAL:
@ -290,7 +157,7 @@ class InfluxGraph(HandlerBase):
self.livemode = self.LIVE self.livemode = self.LIVE
return dict(type='accept-graph', live=True) return dict(type='accept-graph', live=True)
def w_export(self, variables, time, nan, interval, timeoffset=None): def w_export(self, variables, time, nan, interval):
""" """
Returns the bytes of a dataframe with the curves given by variables in the time range "time" Returns the bytes of a dataframe with the curves given by variables in the time range "time"
Called when the route /export is reached. Called when the route /export is reached.
@ -305,156 +172,50 @@ class InfluxGraph(HandlerBase):
io.BytesIO : an BytesIO object containing the dataframe to retrieve io.BytesIO : an BytesIO object containing the dataframe to retrieve
""" """
start, end = get_abs_time([float(t) for t in time.split(',')]) time = [float(t) for t in time.split(',')]
start, end = int(start), ceil(end) start, end = self.get_abs_time(time)
start, end = int(start), int(end)
queried_variables = variables.split(',') queried_variables = variables.split(',')
interval = float(interval) if interval else None if interval != "None" : interval = int(interval)
timeoffset = None if timeoffset == 'now' else (timeoffset or 0)
result = self.db.export(start, end, queried_variables, timeoffset=timeoffset, none=nan, interval=interval, df = self.influx_data_getter.get_curves_data_frame(queried_variables, [start, end], interval, self.variables)
**self.tags)
return io.BytesIO(result.encode('utf-8')) mem = io.BytesIO()
df.to_csv(mem, sep="\t", index=False, float_format="%.15g", na_rep=nan)
mem.seek(0)
return mem
def graphpoll(self): def graphpoll(self):
""" """
Polls the last known values for all the available variables, and returns only those whose polled values Polls the last known values for all the available variables, and returns only those whose polled values are more recent than the most recent displayed one.
are more recent than the most recent displayed one. Every plain minute, all the variables are returned with a point having their last known value at the current timestamp to synchronize all the curves on the GUI.
Every plain minute, all the variables are returned with a point having their last known value at the current
timestamp to synchronize all the curves on the GUI.
Returns : Returns :
{"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None : {"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None : a dictionnary with its "graph-update" type
a dictionary with its "graph-update" type (so it can be processed by the client), and a "graph" dictionnary with the variable names as key, and an array of points, which are an array containing the timestamp
(so it can be processed by the client), and a "graph" dictionary with the variable names as key,
and an array of points, which are an array containing the timestamp
as their first value, and the y-value in float as their second one as their first value, and the y-value in float as their second one
""" """
if self.livemode != self.LIVE: if self.livemode != self.LIVE:
return None return None
now = current_time() now, = self.get_abs_time([0])
if now < int(self.last_update) + 1.5:
# the server is only waiting after a None return result = self.influx_data_getter.poll_last_values(list(self.variables.keys()), self.lastvalues, now)
# this avoids to many queries with expected empty result for variable, values in list(result.items()):
return None tlast = self.lastvalues.get(variable, (0,))[0]
last_time = int(min(self.last_time.values(), default=now-3600)) # removes points older than the last known point (queries are in seconds and might return points already displayed)
# if len(self.last_time) > 1: while values and values[0][0] <= tlast:
# print('time_poll_jitter', max(self.last_time.values()) - min(self.last_time.values())) values.pop(0)
prev_minute, self.last_minute = self.last_minute, now // 60 if values and values[-1][0] > tlast:
fullminute = prev_minute != self.last_minute self.lastvalues[variable] = values[-1]
add_prev = 3600 if fullminute else 0
result = self.db.curves(last_time, None, list(self.last_values),
merge='_measurement', add_prev=add_prev, **self.tags)
to_remove = {}
for key, curve in result.items():
tlast = self.last_values.get(key, [0])[0]
# remove points older than the last known point. this might happen for different reasons:
# - queries are rounded to seconds
# - clocks of different streams might not be synched
l = len(curve)
for i, row in enumerate(curve):
if row[0] > tlast:
del curve[:i]
break
else: else:
if not fullminute: del result[variable]
to_remove[key] = l if int(now / 60) != int(self.end_query / 60):
self.update_last(result) # Update unchanged values every plain minute
if fullminute: for var, (_, lastx) in self.lastvalues.items():
self.db.complete(result, self.last_time, 'stream') if var not in result:
for key, length in to_remove.items(): result[var] = [(now, lastx)]
curve = result[key] self.end_query = now
if len(curve) > l:
del curve[:l]
else:
# if fullminute:
# print('R', key)
result.pop(key)
# print('poll', sum(len(c) for c in result.values()), self.last_time)
if len(result) > 0: if len(result) > 0:
self.last_update = now return dict(type='graph-update', time=now, graph=result)
return dict(type='graph-update', time=last_time, graph=result)
return None return None
# class InfluxInstrument(HandlerBase):
#
# def __init__(self, instr_name, inst_config=None):
# super().__init__()
# self.db = InfluxDB()
# # self.influx_data_getter = InfluxDataGetter(self.db, instr_name)
# self.title = instr_name
# self.device = self.influx_data_getter.get_device_name(int(current_time()))
#
# def new_client(self):
# return self.register(InfluxClient(self))
class InfluxParams:
"""Class with dummy routes, in case client side is started with the right part init commands"""
def __init__(self):
self.id = uuid.uuid4().hex[0:15]
self.queue = []
def info(self):
return ["na"]
def w_getblock(self, path):
return dict(type='draw', title="graph", path=path, components=[])
def w_updateblock(self, path):
return dict(type='accept-block')
def w_console(self):
return dict(type='accept-console')
def w_sendcommand(self, command):
return dict(type='accept-command')
# class InfluxClient(InfluxParams, InfluxGraph):
# def __init__(self, instrument):
# InfluxParams.__init__(self)
# InfluxGraph.__init__(self, instrument)
#
# def poll(self):
# messages = self.queue
# self.queue = []
# msg = self.graphpoll()
# if msg:
# messages.append(msg)
# return messages
#
#
# class SecopInfluxClient(SecopClient, InfluxGraph):
# def __init__(self, instrument):
# SecopClient.__init__(self, instrument)
# InfluxGraph.__init__(self, instrument)
#
# def poll(self):
# messages = super().poll()
# msg = self.graphpoll()
# if msg:
# messages.append(msg)
# return messages
#
#
# class SecopInfluxInstrument(SecopInstrument):
#
# def __init__(self, inst_name, instrument_config):
# super().__init__(inst_name, instrument_config)
# config = ConfigParser()
# config.optionxform = str
# config.read("./config/influx.ini")
# section = config["INFLUX"]
# self.db = SEHistory()
# # self.db = InfluxDBWrapper(uri=section["url"], token=section["token"],
# # org=section["org"], bucket=section['bucket'])
# # self.influx_data_getter = InfluxDataGetter(self.db, inst_name)
# # self.device = self.influx_data_getter.get_device_name(int(current_time()))
#
# def get_streams(self, timestamp=None):
# return self.db.get_streams(None, timestamp)
#
# def get_experiments(self, start=None, stop=None):
# return self.db.get_experiments(start, stop)

View File

@ -1,167 +0,0 @@
# Installation
create python virtual env (you might choose another name than 'myenv'):
python3 -m venv myenv
aktivate this venv (to be repeated for each session):
source myenv/bin/activate
clone seweb git repository (token is valid for one year only):
git clone https://<token>@gitlab.psi.ch/samenv/seweb.git
cd seweb
switch to your development branch:
git switch daniel
instal needed packages (goes into myenv)
pip3 install -r requirements.txt
# First Run
start demo frappy server
frappy-server cryo,test -p 5000
this terminal should stay open, now open another terminal and type:
source myenv/activate
cd seweb
start dummy webserver, connects with above frappy server
./dummy-webserver port=8888 instrument=test hostport=localhost:5000
start webclient in browser with http://localhost:8888/
# git
show branches:
git branch
switch to your branch, if not yet there:
git switch daniel
## Aenderungen pushen
make a commit for each batch of coherent code changes:
git add <new files>
git commit -a -m "replace icons by nicer ones"
If the part starting by '-m' is omitted, the default editor opens,
this is helpful for create an extended commit message.
Keep the following format: first line summary, empty 2nd line,
then more lines may follow. Keep witdh within about 72 chars.
git push
## Merge changes from other developers
Assume there are some changes in branch 'master' you want to
include in your branch.
If you have uncommitted chanes you do not want to commit yet,
you may save this changes temporarely on a stack:
git stash
Change to master branch and pull the current version:
git switch master
git pull
Take over these changes in your branch:
git switch daniel
git rebase
If conflicts arise, read carefully the instructions and follow them.
After this, in case you did the git stash command above, you want get back your current modifications now:
git stash pop
# Specifications
## Swiping
Remove swiper completely. Marek and me decied this after the meeting.
The benefit is not worth the effort to solve problems.
Problems:
- swiping is used in graphics for something else
- swiping is used in web pages also for going back in history
## Tile Layout
4 types of blocks/tiles:
- graphics
- moduleblock (list of modules with main values),
(goodie: foldable groups, may need some changes in the server code)
- parblock (editable list of parameters) (goodie: foldable groups)
- logblock (log messages) placed in right bottom quarter
console is no longer used!
(x) means a button in the top right corner
for narrow windows < 2w:
- show moduleblock by default
- (x) on graphics: go to moduleblock
- (x) on modules: go to graphics
- clicking on moduleblock: open parblock
- (x) on parblock: go to moduleblock
- logblock is shown by clicking on right bottom icon
(not available with graphics) and hidden with (x)
for broader windows:
- show graphics + moduleblock by default
- (x) not shown on graphics by default
- (x) on modules: full screen graphics
- click on a module row (or a 'details' icon): add parblock,
do not overwrite moduleblock when window is broad enough (width > 3.5w)
- (x) on parblock (close parblock and reveal moduleblock if hidden)
- logblock is shown by clicking on right bottom icon and hidden with (x)
## moduleblocks
on each row:
- colored indicator depending on status
(yellow: busy, orange: warn, red: error)
- red also when value is in error
- edit icon for changing the target parameter of the module,
if available
- 'details' icon for open the corresponding parblock
(instead or in addittion to link on name)
## parblocks
orange when parameter is in error (update message with error
instead of value: show a little icon with hover revealing error text)
## logging
use logging feature of SECoP - needs some work on the server (Markus)
## resizing window
- hide parameters when size < 3.5w
- hide graphics when size < 2w
- show graphics when size > 2w
- it is not needed to show the parblock again when width is increased
## nicer icons
- replace the (x) in modules by an icon for the graphics (e.g. a sine wave)
- replcae the (x) on graphics by an icon for the modules

View File

@ -1,3 +0,0 @@
gevent
flask
frappy-core

230
seagraph.py Normal file
View File

@ -0,0 +1,230 @@
from datetime import date
import time
import sys
import os
import logging
import json
import numpy as np
class PrettyFloat(float):
"""saves bandwidth when converting to JSON
a lot of numbers originally have a fixed (low) number of decimal digits
as the binary representation is not exact, it might happen, that a
lot of superfluous digits are transmitted:
str(1/10*3) == '0.30000000000000004'
str(PrettyFloat(1/10*3)) == '0.3'
"""
def __repr__(self):
return '%.15g' % self
#encode = "!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
def get_abs_time(times):
now = int(time.time() + 0.999)
oneyear = 365 * 24 * 3600
return [t + now if t < oneyear else t for t in times]
class Scanner(object):
def __init__(self, directory, test_day=None):
self.directory = directory
self.last_time = {}
self.test_day = test_day
def scan(self, variable, timerange, result):
"""return value is true when there are aditionnal points after the time range"""
start, to, now = get_abs_time(timerange + [0])
old = None
t = 0
for di in range(date.fromtimestamp(start).toordinal(), date.fromtimestamp(to).toordinal() + 1):
d = date.fromordinal(di)
year, mon, day = self.test_day if self.test_day else (d.year, d.month, d.day)
path = self.directory + "logger/%d/%s/%.2d-%.2d.log" % \
(year, variable.lower(), mon, day)
try:
# logging.info("logger path %s", path)
with open(path) as f:
t0 = time.mktime((d.year, d.month, d.day, 0, 0, 0, 0, 0, -1))
for line in f:
if line[0] != '#':
t = t0 + (int(line[0:2]) * 60 + int(line[3:5])) * 60 + int(line[6:8])
if line[-1:] == '\n':
value = line[9:-1]
else:
value = line[9:]
if t < start:
old = value
else:
if old is not None:
self.next(start, old, result)
old = None
self.next(t, value, result)
if t > to:
break
except IOError:
# print(f'error reading {path}')
pass
if t < start:
#if t == 0:
# t = start
if old is not None:
self.next(t, old, result)
if t != self.last_time.get(variable,0):
self.last_time[variable] = t
return True
return False
class NumericScanner(Scanner):
def __init__(self, *args, **kwargs):
Scanner.__init__(self, *args, **kwargs)
def next(self, t, value, result):
try:
value = PrettyFloat(value)
except:
value = None
result.append([PrettyFloat(t), value])
#self.value = value
#self.last = t
def get_message(self, variables, timerange, show_empty=True):
self.dirty = False
result = {}
for var in variables:
self.last = 0
curve = []
if self.scan(var, timerange, curve):
self.dirty = True
if show_empty or len(curve) > 1:
result[var] = curve
return result
class ColorMap(object):
'''
ColorMap is using official CSS color names, with the exception of Green, as this
is defined differently with X11 colors than in SEA, and used heavily in config files.
Here Green is an alias to Lime (#00FF00) and MidGreen is #008000, which is called Green in CSS.
The function to_code is case insensitive and accepts also names with underscores.
The order is choosen by M. Zolliker for the SEA client, originally only the first 16 were used.
'''
hex_name = (("#FFFFFF","White"), ("#FF0000","Red"), ("#00FF00","Lime"), ("#0000FF","Blue"), ("#FF00FF","Magenta"),
("#FFFF00","Yellow"), ("#00FFFF","Cyan"), ("#000000","Black"), ("#FFA500","Orange"), ("#006400","DarkGreen"),
("#9400D3","DarkViolet"), ("#A52A2A","Brown"), ("#87CEEB","SkyBlue"), ("#808080","Gray"), ("#FF69B4","HotPink"),
("#FFFFE0","LightYellow"), ("#00FF7F","SpringGreen"), ("#000080","Navy"), ("#1E90FF","DodgerBlue"),
("#9ACD32","YellowGreen"), ("#008B8B","DarkCyan"), ("#808000","Olive"), ("#DEB887","BurlyWood"),
("#7B68EE","MediumSlateBlue"), ("#483D8B","DarkSlateBlue"), ("#98FB98","PaleGreen"), ("#FF1493","DeepPink"),
("#FF6347","Tomato"), ("#32CD32","LimeGreen"), ("#DDA0DD","Plum"), ("#7FFF00","Chartreuse"), ("#800080","Purple"),
("#00CED1","DarkTurquoise"), ("#8FBC8F","DarkSeaGreen"), ("#4682B4","SteelBlue"), ("#800000","Maroon"),
("#3CB371","MediumSeaGreen"), ("#FF4500","OrangeRed"), ("#BA55D3","MediumOrchid"), ("#2F4F4F","DarkSlateGray"),
("#CD853F","Peru"), ("#228B22","ForestGreen"), ("#48D1CC","MediumTurquoise"), ("#DC143C","Crimson"),
("#D3D3D3","LightGray"), ("#ADFF2F","GreenYellow"), ("#7FFFD4","Aquamarine"), ("#BC8F8F","RosyBrown"),
("#20B2AA","LightSeaGreen"), ("#C71585","MediumVioletRed"), ("#F0E68C","Khaki"), ("#6495ED","CornflowerBlue"),
("#556B2F","DarkOliveGreen"), ("#CD5C5C","IndianRed "), ("#2E8B57","SeaGreen"), ("#F08080","LightCoral"),
("#8A2BE2","BlueViolet"), ("#AFEEEE","PaleTurquoise"), ("#4169E1","RoyalBlue"), ("#0000CD","MediumBlue"),
("#B8860B","DarkGoldenRod"), ("#00BFFF","DeepSkyBlue"), ("#FFC0CB","Pink"), ("#4B0082","Indigo "), ("#A0522D","Sienna"),
("#FFD700","Gold"), ("#F4A460","SandyBrown"), ("#DAA520","GoldenRod"), ("#DA70D6","Orchid"), ("#E6E6FA","Lavender"),
("#5F9EA0","CadetBlue"), ("#D2691E","Chocolate"), ("#66CDAA","MediumAquaMarine"), ("#6B8E23","OliveDrab"),
("#A9A9A9","DarkGray"), ("#BDB76B","DarkKhaki"), ("#696969","DimGray"), ("#B0C4DE","LightSteelBlue"),
("#191970","MidnightBlue"), ("#FFE4C4","Bisque"), ("#6A5ACD","SlateBlue"), ("#EE82EE","Violet"),
("#8B4513","SaddleBrown"), ("#FF7F50","Coral"), ("#008000","MidGreen"), ("#DB7093","PaleVioletRed"), ("#C0C0C0","Silver"),
("#E0FFFF","LightCyan"), ("#9370DB","MediumPurple"), ("#FF8C00","DarkOrange"), ("#00FA9A","MediumSpringGreen"),
("#E9967A","DarkSalmon"), ("#778899","LightSlateGray"), ("#9932CC","DarkOrchid"), ("#EEE8AA","PaleGoldenRod"),
("#F8F8FF","GhostWhite"), ("#FFA07A","LightSalmon"), ("#ADD8E6","LightBlue"), ("#D8BFD8","Thistle"),
("#FFE4E1","MistyRose"), ("#FFDEAD","NavajoWhite"), ("#40E0D0","Turquoise"), ("#90EE90","LightGreen"),
("#B22222","FireBrick"), ("#008080","Teal"), ("#F0FFF0","HoneyDew"), ("#FFFACD","LemonChiffon"), ("#FFF5EE","SeaShell"),
("#F5F5DC","Beige"), ("#DCDCDC","Gainsboro"), ("#FA8072","Salmon"), ("#8B008B","DarkMagenta"), ("#FFB6C1","LightPink"),
("#708090","SlateGray"), ("#87CEFA","LightSkyBlue"), ("#FFEFD5","PapayaWhip"), ("#D2B48C","Tan"), ("#FFFFF0","Ivory"),
("#F0FFFF","Azure"), ("#F5DEB3","Wheat"), ("#00008B","DarkBlue"), ("#FFDAB9","PeachPuff"), ("#8B0000","DarkRed"),
("#FAF0E6","Linen"), ("#B0E0E6","PowderBlue"), ("#FFE4B5","Moccasin"), ("#F5F5F5","WhiteSmoke"), ("#FFF8DC","Cornsilk"),
("#FFFAFA","Snow"), ("#FFF0F5","LavenderBlush"), ("#FFEBCD","BlanchedAlmond"), ("#F0F8FF","AliceBlue"),
("#FAEBD7","AntiqueWhite"), ("#FDF5E6","OldLace"), ("#FAFAD2","LightGoldenRodYellow"), ("#F5FFFA","MintCream"),
("#FFFAF0","FloralWhite"), ("#7CFC00","LawnGreen"), ("#663399","RebeccaPurple"))
codes = {}
for i, pair in enumerate(hex_name):
codes[pair[0]] = i
low = pair[1].lower()
codes[low] = i
codes[low.replace("gray","grey")] = i
codes["green"] = 2
codes["fuchsia"] = 4
codes["aqua"] = 6
@staticmethod
def to_code(colortext):
try:
return int(colortext)
except ValueError:
return ColorMap.codes.get(colortext.lower().replace("_",""),-1)
@staticmethod
def check_hex(code):
if not code.startswith("#"):
return None
if len(code) == 4: # convert short code to long code
code = code[0:2] + code[1:3] + code[2:4] + code[3]
if len(code) != 7:
return None
try:
int(code[1:]) # we have a valid hex color code
return code
except ValueError:
return None
@staticmethod
def to_hex(code):
try:
return ColorMap.hex_name[code][0]
except IndexError:
return -1
class VarsScanner(Scanner):
colors = {"red":0}
def __init__(self, directory, test_day=None):
Scanner.__init__(self, directory, test_day=test_day)
logging.info('vars dir %s', directory)
def next(self, t, value, result):
logging.info('vars %s', value)
for var in value.strip().split(" "):
vars = var.split("|")
if len(vars) == 1:
vars.append("")
if len(vars) == 2:
vars.append(vars[0])
if len(vars) == 3:
vars.append("")
name, unit, label, color = vars
if not unit in result:
result[unit] = dict(tag = unit, unit = unit.split("_")[0], curves=[])
result[unit]["curves"].append(dict(name=name, label=label, color=color))
def get_message(self, time):
# get last value only
result = {}
self.scan("vars", [time, time], result)
for unit in result:
color_set = set()
auto_curves = []
for curve in result[unit]["curves"]:
col = curve["color"].strip()
c = ColorMap.to_code(col)
if c < 0:
valid = ColorMap.check_hex(col)
if valid:
curve["original_color"] = col
curve["color"] = valid
else:
auto_curves.append(curve)
curve["original_color"] = col + "?"
else:
color_set.add(c)
curve["original_color"] = col
curve["color"] = ColorMap.to_hex(c)
c = 1 # omit white
for curve in auto_curves:
while c in color_set: c += 1 # find unused color
curve["color"] = ColorMap.to_hex(c)
c += 1
return result

1115
seaweb.py Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
#!/usr/bin/env python
import sys
import argparse
import socket
from webserver import server
from base import Client
from influxgraph import InfluxGraph
from secop import SecopInteractor
from sehistory.seinflux import SEHistory
def parseArgv(argv):
parser = argparse.ArgumentParser(
description="start a webserver for history and interaction",
)
# loggroup = parser.add_mutually_exclusive_group()
# loggroup.add_argument("-v", "--verbose",
# help="Output lots of diagnostic information",
# action='store_true', default=False)
# loggroup.add_argument("-q", "--quiet", help="suppress non-error messages",
# action='store_true', default=False)
parser.add_argument("port",
type=str,
help="port number to serve\n")
# parser.add_argument('-d',
# '--daemonize',
# action='store_true',
# help='Run as daemon',
# default=False)
parser.add_argument('-i',
'--instrument',
action='store',
help="instrument, if running on an instrument computer\n"
"if the value is HOST, take the host name as instrument name",
default=None)
return parser.parse_args(argv)
args = parseArgv(sys.argv[1:])
instrument = socket.gethostname().split('.')[0] if args.instrument == 'HOST' else args.instrument
server.run(int(args.port), SEHistory(), InfluxGraph, Client, single_instrument=instrument, secop=SecopInteractor)

170
secop.py
View File

@ -1,170 +0,0 @@
import logging
from frappy.client import SecopClient
def convert_par(module, name, par):
result = dict(type='input', name=module+":"+name, title=name)
if par.get('readonly', True):
result['type'] = 'rdonly'
else:
result['command'] = 'change %s:%s' % (module, name)
if par['datainfo']['type'] == 'enum':
result['enum_names'] = [dict(title=k, value=v) for k, v in par['datainfo']['members'].items()]
result['type'] = 'enum'
elif par['datainfo']['type'] == 'bool':
result['type'] = 'checkbox'
if par['description']:
result['info'] = par['description']
return result
def convert_cmd(module, name, cmd):
result = dict(type='pushbutton', name=module+":"+name, title=name)
result['command'] = 'do %s:%s' % (module, name)
argument = cmd['datainfo'].get('argument')
if cmd['datainfo'].get('result'):
result['result'] = True
else:
if not argument: # simple command like stop
return result
result['button'] = not argument
# result['type'] = pushbutton will be replaced below
if argument:
if argument['type'] == 'enum':
result['enum_names'] = [dict(title=k, value=v) for k, v in argument['members'].items()]
result['type'] = 'enum'
elif argument['type'] == 'bool':
result['type'] = 'checkbox'
else:
result['type'] = 'input'
else:
result['type'] = 'rdonly'
if cmd['description']:
result['info'] = cmd['description']
return result
class SecopInteractor(SecopClient):
prio_par = ["value", "status", "target"]
hide_par = ["baseclass", "class", "pollinterval"]
skip_par = ["status2"]
def __init__(self, uri, node_map):
super().__init__(uri)
self.module_updates = set()
self.param_updates = set()
self.updates = {}
self.connect()
node_map.update({k: self for k in self.modules})
self.register_callback(None, updateItem=self.updateItem)
def add_main_components(self, components):
for name, desc in self.modules.items():
parameters = desc['parameters']
component = {'type': 'rdonly' if 'value' in parameters else 'none'}
if 'status' in parameters:
component['statusname'] = f'{name}:status'
targetpar = parameters.get('target')
if targetpar:
component.update(convert_par(name, 'target', targetpar))
component['targetname'] = f'{name}:target'
info = desc['properties'].get('description')
if info:
component['info'] = info
component['name'] = f'{name}:value'
component['title'] = name
components.append(component)
self.param_updates.add('value')
self.param_updates.add('status')
self.param_updates.add('target')
def get_components(self, path):
module = self.modules[path]
self.module_updates.add(path) # TODO: remove others?
parameters = dict(module["parameters"])
components = []
for name in SecopInteractor.skip_par:
if name in parameters:
parameters.pop(name)
for name in SecopInteractor.prio_par:
if name in parameters:
components.append(convert_par(path, name, parameters.pop(name)))
components1 = []
for name in SecopInteractor.hide_par:
if name in parameters:
components1.append(convert_par(path, name, parameters.pop(name)))
for name, par in parameters.items():
components.append(convert_par(path, name, par))
components.extend(components1)
for name, cmd in module.get("commands", {}).items():
components.append(convert_cmd(path, name, cmd))
return components
def updateItem(self, module, parameter, entry):
key = module, parameter
# print(key, entry)
if module in self.module_updates or parameter in self.param_updates:
name = f'{module}:{parameter}'
if entry.readerror:
item = {'name': name, 'error': str(entry.readerror)}
elif parameter == 'status':
# statuscode: 0: DISABLED, 1: IDLE, 2: WARN, 3: BUSY, 4: ERROR
statuscode, statustext = entry[0]
formatted = statuscode.name + (f', {statustext}' if statustext else '')
item = {'name': name, 'value': str(entry), 'statuscode': entry[0][0] // 100,
'formatted': formatted}
else:
item = {'name': name, 'value': str(entry), 'formatted': entry.formatted()}
# print(item)
self.updates[key] = item
def update_main(self):
cache = self.cache
for modname in self.modules:
for param in 'value', 'status', 'target':
key = modname, param
if key in cache:
self.updateItem(*key, cache[key])
def update_params(self, path):
cache = self.cache
for param in self.modules[path]['parameters']:
key = path, param
if key in cache:
self.updateItem(*key, cache[key])
def handle_command(self, command):
"""handle command if we can, else return False"""
if not command.strip():
return dict(type='accept-command')
is_param = True
if command.startswith('change '):
command = command[7:]
elif command.startswith('do '):
is_param = False
command = command[3:]
modpar, _, strvalue = command.partition(' ')
module, _, parameter = modpar.partition(':')
if not parameter:
parameter = 'target'
if module not in self.modules:
return None
logging.info('SENDCOMMAND %r', command)
if is_param:
try:
entry = self.setParameterFromString(module, parameter, strvalue)
item = {'name': f'{module}:{parameter}', 'value': str(entry), 'formatted': entry.formatted()}
except Exception as e:
print(f"{e!r} converting {strvalue} to {self.modules[module]['parameters'][parameter]['datatype']}")
self.updates[module, parameter] = item
return True
# called a command
formatted = self.execCommandFromString(module, parameter, strvalue)[0] # ignore qualifiers
return {'name': f'{module}:{parameter}', 'value': str(result), 'formatted': formatted}
def get_updates(self):
updates, self.updates = self.updates, {}
return list(updates.values())
def info(self):
return ["na"]

133
tcp_lineserver.py Normal file
View File

@ -0,0 +1,133 @@
import asyncore
import socket
import errno
import re
import circularlog
import logging
class LineHandler(asyncore.dispatcher_with_send):
def __init__(self, sock):
self.buffer = b""
asyncore.dispatcher_with_send.__init__(self, sock)
self.crlf = 0
def handle_read(self):
data = self.recv(8192)
if data:
parts = data.split(b"\n")
if len(parts) == 1:
self.buffer += data
else:
self.handle_line((self.buffer + parts[0]).decode('ascii'))
for part in parts[1:-1]:
if part[-1] == b"\r":
self.crlf = True
part = part[:-1]
else:
self.crlf = False
self.handle_line(part.decode('ascii'))
self.buffer = parts[-1]
def send_line(self, line):
self.send(line.encode('ascii') + (b"\r\n" if self.crlf else b"\n"))
def handle_line(self, line):
'''
test: simple echo handler
'''
self.send_line("> " + line)
class LineServer(asyncore.dispatcher):
def __init__(self, host, port, lineHandlerClass):
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((host, port))
self.listen(5)
self.lineHandlerClass = lineHandlerClass
def handle_accept(self):
pair = self.accept()
if pair is not None:
sock, addr = pair
print("Incoming connection from %s" % repr(addr))
handler = self.lineHandlerClass(sock)
def loop(self):
asyncore.loop()
class Disconnected(Exception):
pass
class LineClient(object):
def __init__(self, host_port, announcement=None, filter_ascii=False, ridername="r"):
self.host_port = host_port
self.filter_ascii = filter_ascii
self.announcement = announcement
self.circular = circularlog.Rider(ridername)
self.connected = False
def connect(self):
logging.info("connect to %s %s", "%s:%d" % self.host_port, getattr(self, 'name', '?'))
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect(self.host_port)
self.connected = True
self.buffer = [b""]
if self.announcement:
self.send_line('\n'.join(self.announcement))
def get_line(self):
if not self.connected:
logging.info("connect for get_line")
self.connect()
while len(self.buffer) <= 1:
self.socket.setblocking(0)
try:
data = self.socket.recv(1024)
except socket.error as e:
err = e.args[0]
if err == errno.EAGAIN or err == errno.EWOULDBLOCK:
return None
raise e
if data == "":
print(self.buffer, '<')
self.close()
raise Disconnected("disconnected")
self.socket.setblocking(1)
data = data.split(b'\n')
self.buffer[0] += data[0]
for p in data[1:]:
self.buffer.append(p)
line = self.buffer.pop(0).decode('ascii')
if len(line) > 0 and line[-1] == '\r':
line = line[0:-1]
self.circular.put("<", line)
# print '<', line
if self.filter_ascii:
# replace non ascii characters
line = re.sub(r'[^\x00-\x7E]+','?', line)
return line
def send_line(self, line):
if not self.connected:
logging.info("connect for cmd: %s", line)
self.connect()
# print '>', line
self.circular.put(">", line)
self.socket.sendall(line.encode('ascii') + b'\n')
def close(self):
self.socket.close()
self.connected = False
if __name__ == "__main__":
server = LineServer("localhost", 9999, LineHandler)
server.loop()

View File

@ -1,415 +0,0 @@
from gevent import monkey
monkey.patch_all()
import sys
import time
import signal
import socket
import traceback
import logging
import json
import gevent
import gevent.pywsgi
import gevent.queue
import flask
import circularlog
def guess_mimetype(filename):
if filename.endswith('.js'):
mimetype = 'text/javascript'
elif filename.endswith('.css'):
mimetype = 'text/css'
elif filename.endswith('.ico'):
mimetype = 'image/x-icon'
elif filename.endswith(".png"):
mimetype = "image/png"
else:
mimetype = 'text/html'
return mimetype
class MyEncoder(json.JSONEncoder):
def default(self, obj):
try:
return super().default(obj)
except TypeError:
return int(obj) # try to convert SECoP Enum
# SSE 'protocol' is described here: https://bit.ly/UPFyxY
def to_json_sse(msg):
txt = json.dumps(msg, separators=(',', ': '), cls=MyEncoder)
logging.debug('data: %s', txt)
return 'data: %s\n\n' % txt
class Server:
"""singleton: created once in this module"""
interactor_classes = None
client_cls = None
history_cls = None
history = None
single_instrument = None
db = None
def __init__(self):
self.instruments = {}
self.clients = {}
def remove(self, client):
try:
del self.clients[client.id]
except KeyError:
logging.warning('client already removed %s', client.id)
def lookup_streams(self, instrument, stream=None, device=None):
if self.single_instrument:
instrument = self.single_instrument
if stream:
if isinstance(stream, str):
streams = stream.split(',') if stream else []
else:
streams = stream
else:
streams = []
device_names = devices = device.split(',') if device else []
tags = {}
if instrument:
# tags['instrument'] = instrument
stream_dict = self.db.get_streams(instrument, stream=list(streams), device=devices)
streams.extend((s for s in stream_dict if s not in streams))
if not devices:
device_names = list(filter(None, (t.get('device') for t in stream_dict.values())))
if streams:
tags['stream'] = streams[0] if len(streams) == 1 else streams
if devices:
tags['device'] = devices[0] if len(devices) == 1 else devices
return streams, tags, ','.join(device_names)
def register_client(self, instrument=None, stream=None, device=None):
streams, tags, device_name = self.lookup_streams(instrument, stream, device)
client = self.client_cls(self, streams, instrument or '', device_name)
history = self.history_cls(self, instrument, device_name, tags)
# history.db.debug = True
# all relevant methods of the history instance are saved in client.handlers
# so there is no reference needed to history anymore
client.handlers.update(history.handlers)
self.clients[client.id] = client
return client
def run(self, port, db, history_cls, client_cls, single_instrument=None, **interactor_classes):
self.single_instrument = single_instrument
self.db = db
self.history_cls = history_cls
self.client_cls = client_cls
self.interactor_classes = interactor_classes
app.debug = True
logging.basicConfig(filename='webserver.log', filemode='w', level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
# srv = gevent.wsgi.WSGIServer(('', port), app, keyfile='key.key', certfile='key.crt')
srv = gevent.pywsgi.WSGIServer(('', port), app, log=logging.getLogger('server'))
def handle_term(sig, frame):
srv.stop()
srv.close()
signal.signal(signal.SIGTERM, handle_term)
# def handle_pdb(sig, frame):
# import pdb
# print('PDB')
# pdb.Pdb().set_trace(frame)
# signal.signal(signal.SIGUSR1, handle_pdb)
srv.serve_forever()
server = Server()
app = flask.Flask(__name__)
update_rider = circularlog.Rider("upd")
pollinterval = 0.2
@app.route('/update')
def get_update(_=None):
# Client Adress: socket.getfqdn(flask.request.remote_addr)
kwargs = {k: flask.request.values.get(k) for k in ('instrument', 'stream', 'device')}
client = server.register_client(**kwargs)
client.remote_info = circularlog.strtm() + " " + socket.getfqdn(flask.request.remote_addr.split(':')[-1])
@flask.stream_with_context
def generator():
logging.info('UPDATE %s %s', client.id, socket.getfqdn(flask.request.remote_addr.split(':')[-1]))
# msg = dict(type='id', id=client.id, title=instrument.title);
# yield to_json_sse(msg)
msg = dict(type='id', id=client.id, instrument=kwargs.get('instrument') or 'n_a',
device=client.device_name)
yield to_json_sse(msg)
try:
lastmsg = time.time()
while True:
if client.info() == "":
print(time.time()-lastmsg)
messages = client.poll()
for msg in messages:
update_rider.put('-', repr(msg))
yield to_json_sse(msg)
if messages:
lastmsg = time.time()
else:
if time.time() > lastmsg + 30:
if not client.info():
raise GeneratorExit("no activity")
logging.info('HEARTBEAT %s (%s)', client.id, "; ".join(client.info()))
yield to_json_sse(dict(type='heartbeat'))
lastmsg = time.time()
else:
gevent.sleep(pollinterval)
except GeneratorExit as e:
logging.info("except clause %r", repr(e))
logging.info('CLOSED %s', client.id)
print('CLOSE client')
server.remove(client)
except Exception as e:
logging.info('error')
logging.error('%s', traceback.format_exc())
server.remove(client)
# msg = dict(type='error',error=traceback.format_exc())
# yield to_json_sse(msg)
resp = flask.Response(generator(), mimetype='text/event-stream')
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
@app.route('/circular')
def dump_circular():
circularlog.log()
return "log"
@app.route('/clients')
def show_clients():
result = ""
for id in server.clients:
c = server.clients[id]
result += c.remote_info + " " + "; ".join(c.info()) + "<br>"
return result
@app.route('/export')
def export():
args = flask.request.args
kwargs = dict((k, args.get(k)) for k in args)
path = flask.request.path
logging.info('GET %s %s', path, repr(kwargs))
try:
id = kwargs.pop('id')
client = server.clients[id]
bytes = client.handlers['export'](**kwargs)
return flask.send_file(
bytes,
as_attachment=True,
download_name='export.tsv',
mimetype='text/tab-separated-values'
)
except Exception as e:
logging.error('%s', traceback.format_exc())
circularlog.log()
msg = dict(type='error', request=path[1:], error=repr(e))
logging.error('MSG: %r', msg)
resp = flask.Response(json.dumps(msg), mimetype='application/json')
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
@app.route('/getblock')
@app.route('/updateblock')
@app.route('/sendcommand')
@app.route('/console')
@app.route('/graph')
@app.route('/updategraph')
@app.route('/gettime')
@app.route('/getvars', methods=["GET", "POST"])
def reply():
args = flask.request.values
kwargs = dict((k, args.get(k)) for k in args)
path = flask.request.path
logging.info('GET %s %r', path, kwargs)
try:
id = kwargs.pop('id')
client = server.clients[id]
msg = client.handlers[path[1:]](**kwargs)
except Exception as e:
logging.error('%s', traceback.format_exc())
circularlog.log()
msg = dict(type='error', request=path[1:], error=repr(e))
jsonmsg = json.dumps(msg)
if len(jsonmsg) < 120:
logging.info('REPLY %s %s', path, jsonmsg)
else:
logging.info('REPLY %s %s...', path, jsonmsg[:80])
logging.debug('REPLY %s %r', path, jsonmsg)
resp = flask.Response(jsonmsg, mimetype='application/json')
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
@app.route('/test/<file>')
def subdir_test_file(file):
gevent.sleep(2)
resp = flask.send_file("client/test/"+file, mimetype=guess_mimetype(file))
return resp
@app.route('/components/curves_settings_popup/color_selector/<file>')
@app.route('/components/curves_settings_popup/<file>')
@app.route('/components/action_entry/<file>')
@app.route('/components/export_popup/<file>')
@app.route('/components/dates_popup/<file>')
@app.route('/components/menu_popup/<file>')
@app.route('/components/help_popup/<file>')
@app.route('/components/help_entry/<file>')
@app.route('/components/control/<file>')
@app.route('/components/divider/<file>')
@app.route('/components/states_indicator/dates/<file>')
@app.route('/res/<file>')
@app.route('/jsFiles/<file>')
@app.route('/cssFiles/<file>')
@app.route('/externalFiles/<file>')
def subdir_file(file):
subdir = "/".join(flask.request.path.split("/")[1:-1])
resp = flask.send_file("client/" + subdir+"/"+file, mimetype=guess_mimetype(file))
# resp.headers['Content-Security-Policy'] = "sandbox; script-src 'unsafe-inline';"
return resp
@app.route('/externalFiles/maps/<file>.map')
def replace_by_empty(file):
return ""
@app.route('/')
def default():
if not any(flask.request.values.get(k) for k in ('instrument', 'stream', 'device')):
if not server.single_instrument:
return select_experiment()
return general_file('SEAWebClient.html')
@app.route('/select_instrument')
def select_instrument():
out = ['''<html><body><table>
<style>
th {
text-align: left;
}
</style>
<tr><th>instrument</th><th colspan=99>devices</th></tr>''']
result = {}
for stream, tags in server.db.get_streams().items():
ins = tags.get('instrument', '0')
result.setdefault(ins, []).append((stream, tags.get('device')))
bare_streams = result.pop('0', [])
for ins, streams in result.items():
out.append(f'<tr><td><a href="/?ins={ins}">{ins}</a></td>')
out.extend(f'<td>{d or s}</td>' for s, d in streams)
out.append('</tr>')
for stream, device in bare_streams:
out.append(f'<tr><td><a href="/?srv={stream}">{stream}</a></td><td>{device}</td><tr>')
out.extend(['</table></body></html>', ''])
return '\n'.join(out)
@app.route('/select_experiment')
def select_experiment():
out = ['''<html><head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
th {
text-align: left;
background-color: #cccccc;
}
a {
text-decoration: none;
}
</style></head>
<body><table>
''']
showtitle = 0
ONEMONTH = 30 * 24 * 3600
def title(text):
out.append(f'<tr><td colspan=2><b>{text}</b></td></tr>')
# TODO: sort this by (instrument / device) and list dates
# period format: Ymd..Ymd, Ymd (single date), Ymd..now, HM..now
try:
now = time.time()
timerange = flask.request.values.get('time')
if timerange == 'all':
starttime, endtime = None, None
elif timerange:
timerange = timerange.split(',')
starttime, endtime = [None if timerange[i] == '0' else int(timerange[i]) for i in (0, -1)]
else:
starttime, endtime = now - ONEMONTH, now
chunk_list = []
for key, chunk_dict in server.db.get_experiments(starttime, endtime).items():
for (streams, devices), chunks in chunk_dict.items():
chunk_list.extend((r[1], r[0], key, devices) for r in chunks)
chunk_list.sort(reverse=True)
for end, beg, key, devices in chunk_list:
today, begdate, enddate = (time.strftime("%Y-%m-%d", time.localtime(t)) for t in (now, beg, end))
args = ['='.join(key)]
if end > now:
if begdate == today:
daterange = f'since {time.strftime("%H:%M", time.localtime(beg))}'
else:
daterange = f'since {begdate}'
if showtitle == 0:
title('currently running')
showtitle = 1
else:
daterange = begdate if begdate == enddate else f'{begdate}...{enddate}'
if end < now - ONEMONTH:
if showtitle == 1:
title('older than 30 days')
showtitle = 2
out.append(f'<tr><th><a href="/?{"&".join(args)}">{key[1]} / {" ".join(devices)}</a></th>')
out.append(f'<td>{daterange}</td></tr>')
if timerange:
out.append(f'<h3><a href="/select_experiment?time=all">earlier dates</a></h3><br>')
out.extend(['</table></body></html>', ''])
except Exception as e:
logging.error('%s', traceback.format_exc())
circularlog.log()
out = [f"ERROR {e!r}"]
return '\n'.join(out)
@app.route('/<file>')
def general_file(file):
subdir = "client/"
try:
resp = flask.send_file(subdir+file, mimetype=guess_mimetype(file))
except FileNotFoundError:
logging.warning('file %s not found', file)
return 'file not found'
# resp.headers['Content-Security-Policy'] = "sandbox; script-src 'unsafe-inline';"
return resp
def hostport_split(hostport):
h = hostport.split(':')
return (h[0], int(h[1]))