Compare commits
188 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6ec455563 | |||
be404b625b | |||
191f0aed80 | |||
b24384f387 | |||
e136b66732 | |||
822e3ab6a2 | |||
b1ffe99a5d | |||
7d4607e947 | |||
55dd7a3777 | |||
fd37ee0dfc | |||
90e8aa5df0 | |||
6df42b9541 | |||
2984c051e9 | |||
46fca20f09 | |||
2ed1e3c292 | |||
6390d37ab4 | |||
925bdfc472 | |||
3b438d68b2 | |||
a94cc98c42 | |||
2cd78ae1e9 | |||
a4fda418b2 | |||
179db4c0a3 | |||
5c70017238 | |||
268ebc7e93 | |||
f66b071813 | |||
d6a69ba05e | |||
62da014d40 | |||
5c18791c13 | |||
0f16699323 | |||
f883c913ed | |||
a229626bdf | |||
0a75a0aa37 | |||
d2ce97b2ab | |||
06bdbbb02e | |||
d6479a7ece | |||
a8e14eb797 | |||
b704440f36 | |||
10387261c0 | |||
9f2bfd668a | |||
70b40646ef | |||
43624b4222 | |||
e51942c97f | |||
ae7ed7bdd8 | |||
202095d539 | |||
cf0a979cc9 | |||
8d38dc31f2 | |||
9bacb41be8 | |||
8119f221bd | |||
b9a1e7db99 | |||
2fda3164e6 | |||
0a6ff13ee6 | |||
9780ab7097 | |||
9b7261261f | |||
f6aff481e2 | |||
e6e69c8f5c | |||
df582a2f23 | |||
8f7406c31b | |||
09bf402bbb | |||
6e20ed0f8f | |||
960e95c447 | |||
27f60e1187 | |||
5c1c94bffc | |||
5d10b6d48d | |||
d1ea9225dc | |||
7898e375b4 | |||
715381c088 | |||
0542f41ec0 | |||
023145ca3d | |||
51c9973c0d | |||
744184eb54 | |||
712ea7bbab | |||
82e044020c | |||
8c03da89d9 | |||
d6ad08025b | |||
b0761d9be9 | |||
fe80de4d4b | |||
a7e23febfe | |||
b24db856df | |||
2eaced4283 | |||
a9ca113f2c | |||
bd9efaa3de | |||
13e8570afd | |||
d15b72fa98 | |||
45a90145a7 | |||
54d77218cd | |||
b2d6422f9d | |||
d6fd8ad6d0 | |||
273821e191 | |||
62c981d396 | |||
a4b9ad17cd | |||
924116627f | |||
654d79461b | |||
74e1a84253 | |||
ea21d4e138 | |||
ff0c00cabb | |||
0d5ffd72a8 | |||
58ee8130e6 | |||
4ad37d5c2f | |||
0ec5672068 | |||
3be94ba3f6 | |||
8747b0e7f8 | |||
639949f24b | |||
555aca9ed0 | |||
38b2dbcf93 | |||
33c9896bb1 | |||
da17309e78 | |||
9b3f89ddaa | |||
3d37b10d61 | |||
8d668f7de6 | |||
85fdaa445f | |||
e132b43263 | |||
56f85d9a39 | |||
bc9c1361e5 | |||
11a475149a | |||
bd7c1f7406 | |||
c42d7d8bf8 | |||
385a413870 | |||
1e876a888f | |||
40dc6dc7d8 | |||
737e29975a | |||
135802c626 | |||
39be5e8353 | |||
e815800cb3 | |||
f6b5141e5b | |||
dbf98fdf90 | |||
1c94abb3ce | |||
47213d7576 | |||
dbadc4ce0a | |||
2da1798d42 | |||
f2b12301ee | |||
958e472f3b | |||
9f5ae58b9b | |||
b8ac8f8bb5 | |||
c67c0f7e9d | |||
63a9d65626 | |||
ea780537d9 | |||
98c3870b81 | |||
0932d07bf7 | |||
5b6684fcef | |||
2c59e37074 | |||
9fde940c8a | |||
8dc8839bf0 | |||
7aef895462 | |||
12f78582c0 | |||
b036456517 | |||
c87d08d39f | |||
4691dbcf0f | |||
ca2945ac22 | |||
9ec122a146 | |||
65aa822b96 | |||
680434e5e8 | |||
3d2346f632 | |||
5017c5feb6 | |||
24fa8f8459 | |||
a22b70d750 | |||
78cadd7bef | |||
279a658e8a | |||
e339ef711f | |||
66bc1c99db | |||
7d536c187b | |||
dc887e68d5 | |||
7736e0f7e3 | |||
7471a0c171 | |||
9cee5ad9bf | |||
b07ca0bd4f | |||
b708197d27 | |||
415d4c86f6 | |||
cd306c45ac | |||
09c3a5840a | |||
15ecaca93e | |||
e71cb74391 | |||
7184d28047 | |||
2e21f52071 | |||
69ea17aec6 | |||
96bcd67dc4 | |||
6440daaef2 | |||
664e53d58c | |||
6f318f26b7 | |||
9ed6f75ace | |||
91c1f7c3bb | |||
6edb926bf8 | |||
b919c60000 | |||
48b89a9801 | |||
66e8d431a9 | |||
a2ad485402 | |||
6c1a13c382 | |||
05a77ce2f4 | |||
9105a5cb41 |
3
.gitignore
vendored
@ -5,6 +5,7 @@
|
||||
__pycache__
|
||||
.idea
|
||||
log
|
||||
webserver.log
|
||||
client/favicon_package_v0
|
||||
client/favicon.ico
|
||||
client/favicon192.png_old
|
||||
client/favicon192.png_old
|
||||
|
50
README.md
@ -1,9 +1,33 @@
|
||||
# SEAWeb
|
||||
# seweb
|
||||
|
||||
The WEB GUI client of SEA.
|
||||
**The Web GUI client for Sample Environment at SINQ**
|
||||
|
||||
This repository contains the code of the server for the control and graphical parts, plus the client code (HTML, CSS, JS).
|
||||
|
||||
**Migration**
|
||||
|
||||
Remarks for the migration 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).
|
||||
|
||||
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
|
||||
- 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.
|
||||
- Changed all `xAxes` and `yAxes` references to `x` and `y`.
|
||||
- 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
|
||||
- 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 :
|
||||
- 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).
|
||||
|
||||
|
||||
**Summary**
|
||||
|
||||
- [Documentation](#documentation)
|
||||
@ -77,7 +101,7 @@ seagraph.py <-- Its content is used if the server is run
|
||||
|
||||
1. Clone this repository on the `~` folder on your machine
|
||||
2. If not done yet, activate a Python environnment
|
||||
3. Run the command `cd seaweb`
|
||||
3. Run the command `cd seweb`
|
||||
|
||||
### Configuring the application
|
||||
|
||||
@ -94,29 +118,33 @@ For the `generic.ini` and `<instrument>.ini` files, go to `./doc/variables_confi
|
||||
|
||||
### Starting the application
|
||||
|
||||
1. Run the command `cd ~/seaweb`
|
||||
1. Run the command `cd ~/seweb`
|
||||
|
||||
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.
|
||||
|
||||
4.
|
||||
To start the server without the right part and the history from InfluxDB (NICOS cache), run the command :
|
||||
To start the server the history from InfluxDB (NICOS cache), run the command :
|
||||
|
||||
`python ./seaweb.py type=influx port=<port> instrument=<instrument>`
|
||||
`./secop-webserver port=<port> instrument=<instrument> hostport=<host:port>`
|
||||
|
||||
With the right part :
|
||||
Dummy server (with dummy graphics):
|
||||
|
||||
`python ./seaweb.py type=influxsea port=<port> sea=<sea_address> instrument=<instrument>`
|
||||
`./dummy-webserver port=<port> instrument=<instrument> hostport=<host:port>`
|
||||
|
||||
Where :
|
||||
- `<port>` : the port of the machine to start the server with (for example : 8841)
|
||||
- `<sea_address>` : the address of the SEA server (host:port) in order to have a running right part (for example : samenv:8664)
|
||||
- `<host:port>` : the address of the SECoP server
|
||||
- `<instrument>` : the name of the instrument (for example : lab4)
|
||||
|
||||
### Stopping the application
|
||||
|
||||
1. Run the command `ps ax | grep seaweb`
|
||||
If the server is started diretly in a Terminal: press ctrl-C
|
||||
|
||||
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
|
||||
3. Run the command `kill <previously_identified_PID>`
|
||||
|
||||
@ -141,4 +169,4 @@ For the `generic.ini` and `<instrument>.ini` files, go to `./doc/variables_confi
|
||||
- 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.
|
||||
- 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
Normal file
@ -0,0 +1,101 @@
|
||||
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"]
|
@ -1,4 +1,7 @@
|
||||
from configparser import ConfigParser
|
||||
import logging
|
||||
|
||||
|
||||
class ChartConfig:
|
||||
"""
|
||||
Class that holds the chart section of a configuration file (for an instrument).
|
||||
@ -6,46 +9,38 @@ class ChartConfig:
|
||||
Attributes :
|
||||
chart_config (Section) : the Section corresponding to the "chart" section in the given configuration file
|
||||
"""
|
||||
|
||||
KEYS = ["cat", "color", "unit", "label"]
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
Parameters :
|
||||
path (str) : the path to the configuration file
|
||||
"""
|
||||
self.variables = {}
|
||||
cfgp = ConfigParser(interpolation=None)
|
||||
cfgp.optionxform = str
|
||||
cfgp.read(path)
|
||||
self.chart_config = cfgp["chart"]
|
||||
|
||||
def get_variable_parameter_config(self, key):
|
||||
"""
|
||||
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]
|
||||
|
||||
try:
|
||||
section = cfgp["chart"]
|
||||
except KeyError:
|
||||
return
|
||||
for key, raw_value in section.items():
|
||||
arguments = raw_value.split(",")
|
||||
keyword_mode = False
|
||||
config = {'cat': '*'}
|
||||
for i, argument in enumerate(arguments):
|
||||
pieces = argument.split(":")
|
||||
if len(pieces) == 2:
|
||||
argname, _, argvalue = argument.rpartition(':')
|
||||
if argname:
|
||||
keyword_mode = True
|
||||
if pieces[1] != "":
|
||||
config[pieces[0]] = pieces[1]
|
||||
config[argname] = argvalue
|
||||
else:
|
||||
if not keyword_mode: #everything is going well
|
||||
if pieces[0] != "":
|
||||
config[positionnal[i]] = pieces[0]
|
||||
else: #we cannot have a positionnal argument after a keyword argument
|
||||
return None
|
||||
return config
|
||||
else:
|
||||
return None
|
||||
if keyword_mode:
|
||||
logging.error('positional arg after keywd arg: %s=%r', key, raw_value)
|
||||
else:
|
||||
try:
|
||||
if argvalue:
|
||||
config[self.KEYS[i]] = argvalue
|
||||
except Exception as e:
|
||||
logging.error('%r in %s=%r', e, key, raw_value)
|
||||
self.variables[key] = config
|
||||
|
@ -2,8 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="icon" type="image/png" href="/favicon192.png" sizes=192x192>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<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"
|
||||
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" />
|
||||
@ -30,8 +34,6 @@
|
||||
|
||||
<!-- CSS-Files -->
|
||||
<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/SEAWebClientConsole.css">
|
||||
<link rel="stylesheet" href="cssFiles/SEAWebClientGraphics.css">
|
||||
@ -39,14 +41,16 @@
|
||||
<!-- JS-Files -->
|
||||
<script src="externalFiles/alertify.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/luxon.min.js"></script>
|
||||
<script src="externalFiles/chartjs-adapter-luxon.umd.min.js"></script>
|
||||
|
||||
<script src="externalFiles/hammer.js"></script>
|
||||
<script src="externalFiles/chartjs-zoom.js"></script>
|
||||
<script src="externalFiles/chartjs-plugin-zoom.min.js"></script>
|
||||
<!-- <script src="externalFiles/chartjs-zoom.js"></script> -->
|
||||
<script src="jsFiles/SEAWebClientLocalStorage.js"></script>
|
||||
<script src="jsFiles/SEAWebClientResponsivity.js"></script>
|
||||
<script src="jsFiles/SEAWebClientSwiper.js"></script>
|
||||
<script src="jsFiles/SEAWebClientGroup.js"></script>
|
||||
<script src="jsFiles/SEAWebClientConsole.js"></script>
|
||||
<script src="jsFiles/SEAWebClientGraphics.js"></script>
|
||||
@ -72,7 +76,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="center"></div>
|
||||
<div id="close-cross">×</div>
|
||||
<div class = "icon-close-container icon-main-container">
|
||||
<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>
|
||||
|
||||
</html>
|
||||
|
@ -1,96 +0,0 @@
|
||||
<!--- 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>
|
BIN
client/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
client/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
client/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
@ -8,6 +8,7 @@
|
||||
width: 100%;
|
||||
padding: 26px 0px 0px 0px;
|
||||
background-color: darkgray;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.commandline {
|
||||
@ -26,13 +27,12 @@
|
||||
}
|
||||
|
||||
.history {
|
||||
position: absolute;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
padding: 80px 8px 50px 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
color: black;
|
||||
background-color: lightgray;
|
||||
color: #303030;
|
||||
}
|
@ -37,7 +37,7 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
margin-top: 30px;
|
||||
/* margin-top: 30px; */
|
||||
}
|
||||
|
||||
.graph{
|
||||
|
@ -18,17 +18,11 @@
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
transition: 0.4s;
|
||||
cursor: pointer;
|
||||
color: steelblue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* panel */
|
||||
|
||||
.link:focus {
|
||||
color: orangered;
|
||||
outline: none;
|
||||
}
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* row */
|
||||
|
||||
.row {
|
||||
padding: 4px 0px 4px 0px;
|
||||
@ -36,46 +30,207 @@
|
||||
min-height: 24px;
|
||||
display: block;
|
||||
border-bottom: dotted darkgray 2px;
|
||||
overflow: hidden;
|
||||
transition: 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
.row-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row-clickable:hover {
|
||||
background-color:lightgray;
|
||||
}
|
||||
|
||||
.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 {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
background-color: #303030;
|
||||
color: white;
|
||||
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;
|
||||
}
|
||||
|
||||
.link-static {
|
||||
padding-left: 4px;
|
||||
.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;
|
||||
color: white;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
margin: 4px 0px 4px 0px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
background-color: darkslategray;
|
||||
color: white;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 2px;
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
display: none;
|
||||
width: 100%;
|
||||
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;
|
||||
text-align: center;
|
||||
/* border: 1px solid #303030; */
|
||||
color: #303030;
|
||||
background: #dddddd;
|
||||
|
||||
/* box-shadow: 2px 4px 4px lightgray; */
|
||||
}
|
||||
|
||||
.col-left {
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
float: left;
|
||||
.push-button-active:hover {
|
||||
background: whitesmoke;
|
||||
/* box-shadow: 1px 2px 2px dimgray; */
|
||||
}
|
||||
|
||||
.event-toggle-info {
|
||||
color: darkslategray;
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* right */
|
||||
|
||||
.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;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.col-right {
|
||||
float: right;
|
||||
.icon-edit:hover {
|
||||
transform: scale(.8);
|
||||
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;
|
||||
}
|
||||
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
@ -87,6 +242,9 @@
|
||||
border: solid 2px dimgray;
|
||||
color: black;
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
::-ms-clear { /* remove the x in the input box on IE */
|
||||
@ -95,22 +253,21 @@
|
||||
}
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* CHECKBOX */
|
||||
.parameter-checkbox {
|
||||
/* .parameter-checkbox {
|
||||
opacity: 0;
|
||||
float: left;
|
||||
}
|
||||
} */
|
||||
|
||||
.parameter-checkbox + .parameter-label {
|
||||
/* .parameter-checkbox + .parameter-label {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
}
|
||||
} */
|
||||
|
||||
.parameter-checkbox:focus+.parameter-label {
|
||||
/* .parameter-checkbox:focus+.parameter-label {
|
||||
opacity: 0.8;
|
||||
}
|
||||
} */
|
||||
|
||||
.parameter-checkbox+.parameter-label::before {
|
||||
/* .parameter-checkbox+.parameter-label::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
@ -120,9 +277,9 @@
|
||||
display: block;
|
||||
background: lightgray;
|
||||
border: 2px solid dimgray;
|
||||
}
|
||||
} */
|
||||
|
||||
.parameter-checkbox+.parameter-label::after {
|
||||
/* .parameter-checkbox+.parameter-label::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
left: -19px;
|
||||
@ -139,14 +296,14 @@
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
} */
|
||||
|
||||
.parameter-checkbox:checked+.parameter-label::after {
|
||||
/* .parameter-checkbox:checked+.parameter-label::after {
|
||||
-ms-transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
} */
|
||||
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* RADIO */
|
||||
@ -170,9 +327,25 @@ option {
|
||||
}
|
||||
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* PUSH-BUTTON */
|
||||
|
||||
.push-button {
|
||||
border: 2px solid dimgray;
|
||||
border-radius: 4px;
|
||||
/* PANEL <- moved here from SEAWebClientSwiper.css */
|
||||
.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;
|
||||
}
|
||||
|
||||
.toggle-updates-graphics {
|
||||
float: right;
|
||||
}
|
||||
|
@ -112,6 +112,27 @@ meta, body {
|
||||
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 {
|
||||
@ -128,14 +149,90 @@ meta, body {
|
||||
border: solid 4px dimgray;
|
||||
}
|
||||
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* CLOSE CROSS */
|
||||
.grid-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#close-cross{
|
||||
z-index: 50;
|
||||
top: 9px;
|
||||
right: 12px;
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.panel-graphics-wide {
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
/* MAIN ICONS */
|
||||
|
||||
.icon-main-container {
|
||||
z-index: 1001;
|
||||
position: fixed;
|
||||
color: white;
|
||||
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;
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
@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;
|
||||
}
|
1
client/externalFiles/chart.umd.js.map
Normal file
20
client/externalFiles/chart.umd.min.js
vendored
Normal file
7
client/externalFiles/chartjs-adapter-luxon.umd.min.js
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* chartjs-adapter-luxon v1.3.1
|
||||
* https://www.chartjs.org
|
||||
* (c) 2023 chartjs-adapter-luxon Contributors
|
||||
* Released under the MIT license
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));
|
7
client/externalFiles/chartjs-plugin-zoom.min.js
vendored
Normal file
@ -1,678 +0,0 @@
|
||||
//'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;
|
8
client/externalFiles/d3.min.js
vendored
1
client/externalFiles/luxon.min.js
vendored
Normal file
15
client/externalFiles/oldswiper.min.css
vendored
18
client/externalFiles/oldswiper.min.js
vendored
13
client/externalFiles/swiper-bundle.min.css
vendored
14
client/externalFiles/swiper-bundle.min.js
vendored
@ -1,576 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
12
client/externalFiles/swiper.min.css
vendored
13
client/externalFiles/swiper.min.js
vendored
BIN
client/favicon-16x16.png
Normal file
After Width: | Height: | Size: 659 B |
BIN
client/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.6 KiB |
@ -10,9 +10,9 @@ var timeoutID; // We need this ID to reset the timer every 30 seconds
|
||||
function buildUpdateConnection() {
|
||||
// Establishes server-sent-event-connection, which is used for all sorts of
|
||||
// updates and exists as long as the client is running.
|
||||
// Executed at programstart (see also SEAWebClientMain.js).
|
||||
// Executed at program start (see also SEAWebClientMain.js).
|
||||
|
||||
var path = "http://" + hostPort + "/update";
|
||||
var path = "http://" + hostPort + "/update?" + window.clientTags;
|
||||
if (debugCommunication) {
|
||||
console.log("%cto server (SSE): " + path,
|
||||
"color:white;background:lightblue");
|
||||
@ -22,8 +22,7 @@ function buildUpdateConnection() {
|
||||
var src = new EventSource(path);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
alertify.prompt(
|
||||
"NETWORK ERROR",
|
||||
alertify.prompt("NETWORK ERROR",
|
||||
"Failed to establish connection to data-server at the given address!"
|
||||
+ "Try to enter HOST and PORT of the data-server manually!",
|
||||
hostPort, function(evt, value) {
|
||||
@ -43,9 +42,7 @@ function buildUpdateConnection() {
|
||||
src.onerror = function(e) {
|
||||
console.log(e);
|
||||
console.log('EVTSRC error')
|
||||
alertify
|
||||
.prompt(
|
||||
"NETWORK ERROR",
|
||||
alertify.prompt("NETWORK ERROR",
|
||||
"Failed to establish connection to data-server at the given address!"
|
||||
+ "Try to enter HOST and PORT of the data-server manually!",
|
||||
hostPort, function(evt, value) {
|
||||
@ -59,7 +56,6 @@ function buildUpdateConnection() {
|
||||
|
||||
function handleUpdateMessage(src, message) {
|
||||
// Handles incoming SSE-messages depending on type of message.
|
||||
|
||||
if (debugCommunication > 1) {
|
||||
console.log("%cfrom server (SSE): " + message.type,
|
||||
"color:white;background:lightgray", message);
|
||||
@ -73,25 +69,27 @@ function handleUpdateMessage(src, message) {
|
||||
// id-message: Confirms establishment of SSE-connection and determines
|
||||
// specific ID of the client
|
||||
case "id":
|
||||
for (var i = 0; i < swiper.length; i++) {
|
||||
swiper[i].removeAllSlides();
|
||||
}
|
||||
clientID = message.id;
|
||||
if ("device" in message) {
|
||||
if (message.device == "_inst_select") {
|
||||
clientTitle = "select instrument";
|
||||
window.clientTitle = "select instrument";
|
||||
console.log('IDselect')
|
||||
pushInitCommand("getblock?path=_inst_select&", "instrument selection");
|
||||
menuMode = true;
|
||||
sizeChange();
|
||||
} else {
|
||||
clientTitle = message.instrument + " " + message.device;
|
||||
console.log('loadBlocks', message);
|
||||
if (message.instrument) {
|
||||
window.instrument = message.instrument;
|
||||
}
|
||||
if (message.device) {
|
||||
window.device = message.device;
|
||||
}
|
||||
window.clientTitle = window.instrument + " " + window.device;
|
||||
// console.log('loadBlocks', message);
|
||||
loadFirstBlocks();
|
||||
}
|
||||
document.title = "SEA "+clientTitle;
|
||||
document.title = clientTitle;
|
||||
} else {
|
||||
document.title = "SEA "+clientTitle + " " + message.title;
|
||||
document.title = clientTitle + " " + message.title;
|
||||
}
|
||||
var header = document.getElementById("header");
|
||||
header.style.width = 'auto';
|
||||
@ -101,7 +99,7 @@ function handleUpdateMessage(src, message) {
|
||||
device.style.width = 'auto'
|
||||
instrument.innerHTML = message.instrument
|
||||
device.innerHTML = message.device
|
||||
console.log('ID', initCommands);
|
||||
// console.log('ID', initCommands);
|
||||
nextInitCommand();
|
||||
break;
|
||||
// console-update-message: Confirms a command.
|
||||
@ -169,7 +167,7 @@ function handleUpdateMessage(src, message) {
|
||||
if (debugCommunication > 1) {
|
||||
console.log(message);
|
||||
}
|
||||
updateValues(message, src);
|
||||
handleUpdate(message, src);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -196,47 +194,113 @@ function resetTimer(src) {
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
function updateValues(message, src) {
|
||||
function handleUpdate(message, src) {
|
||||
// Handles changes of parameter-values
|
||||
|
||||
for (var i = 0; i < message.updates.length; i++) {
|
||||
var component = message.updates[i];
|
||||
var value = component.value;
|
||||
var matches = document.getElementsByName(component.name);
|
||||
let component = message.updates[i];
|
||||
|
||||
// Check for status updates
|
||||
if (component.name.split(":")[1] == 'status') {
|
||||
updateStatus(component);
|
||||
}
|
||||
// Check for target updates in the module block
|
||||
if (component.name.split(":")[1] == 'target') {
|
||||
updateTarget(component);
|
||||
}
|
||||
|
||||
for (var j = 0; j < matches.length; j++) {
|
||||
var type = matches[j].__ctype__;
|
||||
if (type == "rdonly") {
|
||||
var text = htmlEscape(value);
|
||||
if (text) {
|
||||
matches[j].innerHTML = text;
|
||||
}
|
||||
} else if (type == "input") {
|
||||
var row = matches[j].parentNode.parentNode.parentNode;
|
||||
row.style.backgroundColor = "white";
|
||||
var mval = matches[j].value;
|
||||
var oldValue = ('oldValue' in matches[j]) ? matches[j].oldValue : mval;
|
||||
if (value != mval && parseFloat(value) != parseFloat(mval) && value != oldValue) {
|
||||
if (matches[j] == document.activeElement
|
||||
|| oldValue != mval) {
|
||||
row.style.backgroundColor = "orange";
|
||||
} else {
|
||||
matches[j].value = value;
|
||||
}
|
||||
}
|
||||
matches[j].actualValue = value;
|
||||
resizeTextfield(matches[j]);
|
||||
} else if (type == "checkbox") {
|
||||
var row = matches[j].parentNode.parentNode;
|
||||
row.style.backgroundColor = "white";
|
||||
// console.log('CBX', matches[j].name, message, Boolean(value && value != 'false'));
|
||||
matches[j].checked = Boolean(value && value != 'false');
|
||||
} else if (type == "enum") {
|
||||
matches[j].style.display = "block";
|
||||
var row = matches[j].parentNode.parentNode;
|
||||
row.style.backgroundColor = "white";
|
||||
matches[j].value = value;
|
||||
updateValue(component);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTarget(component) {
|
||||
let matches = document.getElementsByName(component.name);
|
||||
let elem = matches[0]; // Should be the only match
|
||||
// elem.value = component.value;
|
||||
let row = elem.closest('div');
|
||||
row.classList.remove('row-waiting-for-answer');
|
||||
|
||||
elem.actualValue = component.value;
|
||||
if(elem.__ctype__ == 'input') {
|
||||
resizeTextfield(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(component) {
|
||||
let matches = document.getElementsByName(component.name);
|
||||
let status_icon = matches[0];
|
||||
let row = status_icon.closest(".row");
|
||||
let right = row.lastChild;
|
||||
let statusCode = component.statuscode;
|
||||
|
||||
// Update status info, visible when mouse cursor is hovering over status icon
|
||||
let status_info = document.getElementsByName(component.name.split(":")[0] + '-info')[0];
|
||||
if(status_info) {
|
||||
status_info.innerHTML = component.formatted;
|
||||
}
|
||||
|
||||
status_icon.classList.remove('icon-status-disabled', 'icon-status-idle', 'icon-status-warn', 'icon-status-busy', 'icon-status-error');
|
||||
row.classList.remove('row-disabled');
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -248,8 +312,8 @@ function reqJSON(s, url, successHandler, errorHandler) {
|
||||
var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP');
|
||||
if (debugCommunication) {
|
||||
console.log("%cto server (reqJSON): " + url,
|
||||
"color:white;background:lightgreen");
|
||||
console.log("%cto server (reqJSON): %s",
|
||||
"color:white;background:darkgreen", url);
|
||||
}
|
||||
xhr.open('get', url, true);
|
||||
xhr.onreadystatechange = function() {
|
||||
@ -274,8 +338,8 @@ function reqJSONPOST(s, url, parameters, successHandler, errorHandler) {
|
||||
var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP');
|
||||
if (debugCommunication) {
|
||||
console.log("%cto server (reqJSON): " + url,
|
||||
"color:white;background:lightgreen");
|
||||
console.log("%cto server (reqJSONPOST): %s",
|
||||
"color:white;background:lightgreen", url);
|
||||
}
|
||||
xhr.open('post', url, true);
|
||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
@ -297,10 +361,8 @@ function reqJSONPOST(s, url, parameters, successHandler, errorHandler) {
|
||||
xhr.send(parameters);
|
||||
}
|
||||
|
||||
|
||||
function successHandler(s, message) {
|
||||
// Handles incoming XMLHttp-messages depending on type of message.
|
||||
// s: slide number or -1 for replacing slide in all slider instances
|
||||
if (debugCommunication) {
|
||||
console.log("%cfrom server (reqJSON): " + message.type,
|
||||
"color:white;background:dimgray", message);
|
||||
@ -308,39 +370,18 @@ function successHandler(s, message) {
|
||||
switch (message.type) {
|
||||
// Response to a "getblock"-server-request.
|
||||
case "draw":
|
||||
if (debugCommunication) {
|
||||
console.log(message);
|
||||
}
|
||||
if (message.path == "main") {
|
||||
// Happens only initially or at device change.
|
||||
for (var sLocal = 0; sLocal < 2; sLocal++) { // was up to MAXBLOCK
|
||||
insertSlide(sLocal, message.title, "main", createContent(
|
||||
sLocal, message));
|
||||
}
|
||||
insertSlide(2, "", "parameters", createContent(2, {components:[]}));
|
||||
appendToGridElement(1, message.title, "main", createContent(message));
|
||||
// appendToGridElement(2, "", "parameters", createContent({components:[]}));
|
||||
} else {
|
||||
if (s < 0) { // redraw: check for slides in all swiper instances
|
||||
// not used any more?
|
||||
for (var isw = 0; isw < MAXBLOCK; isw ++) {
|
||||
var isl = findSlide(isw, message.path);
|
||||
if (isl !== null) {
|
||||
var slide = swiper[isw].slides[isl];
|
||||
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 */
|
||||
// In the module-block a parameter was selected
|
||||
showParams = true;
|
||||
// -> write parameter-block to grid-element2
|
||||
isl = appendToGridElement(2, message.title, 'parameters', createContent(message));
|
||||
adjustGrid();
|
||||
if (nColumns == 1 || nColumns == 2 || nColumns == 3) {
|
||||
document.getElementsByClassName('icon-close-container')[0].innerHTML = '<img class = "icon-main icon-close" src="res/icon_close.png">';
|
||||
}
|
||||
}
|
||||
nextInitCommand();
|
||||
@ -356,11 +397,8 @@ function successHandler(s, message) {
|
||||
break;
|
||||
// Response to a "console"-server-request.
|
||||
case "accept-console":
|
||||
// draw console, only on the first and the last swiper
|
||||
insertSlide(0, "console", "console",
|
||||
createContentConsole(sLocal));
|
||||
insertSlide(3, "console", "console",
|
||||
createContentConsole(sLocal));
|
||||
// draw console only to the last grid-element
|
||||
appendToGridElement(3, "console", "console",createContentConsole(3));
|
||||
nextInitCommand();
|
||||
// send empty command in order to trigger getting history
|
||||
reqJSON(0, "http://" + hostPort + "/sendcommand?command=&id=" + clientID, successHandler,
|
||||
@ -371,15 +409,17 @@ function successHandler(s, message) {
|
||||
timeRange = message.time;
|
||||
/*createGraphics();
|
||||
// By default mostleft swiper-instance shows graphics.
|
||||
swiper[0].slideTo(0);
|
||||
|
||||
// Update time-selection. (see also SEAWebClientGraphics.js)
|
||||
var select = document.getElementsByClassName("select-time")[0];
|
||||
begin = timeRange[0] - timeRange[1];
|
||||
select.value = begin;
|
||||
// Server-request for variable-list.*/
|
||||
reqJSONPOST(0, "http://" + hostPort + "/getvars", "time=" + timeRange[1] + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="
|
||||
+ clientID, successHandler, errorHandler);
|
||||
// console.log('TIME', timeRange)
|
||||
reqJSONPOST(0, "http://" + hostPort + "/getvars",
|
||||
"time=" + timeRange[0] + ',' + timeRange[1]
|
||||
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
||||
+ "&id=" + clientID, successHandler, errorHandler);
|
||||
break;
|
||||
// Response to a "getvars"-server-request.
|
||||
case "var_list":
|
||||
@ -399,6 +439,7 @@ function successHandler(s, message) {
|
||||
nextInitCommand();
|
||||
}*/
|
||||
// graphs.receivedVars(message.blocks);
|
||||
document.getElementById("device").innerHTML = message.device
|
||||
graphs.initGraphs(message.blocks);
|
||||
nextInitCommand();
|
||||
break;
|
||||
@ -418,6 +459,11 @@ function successHandler(s, message) {
|
||||
// Response to a "updategraph"-server-request.
|
||||
case "accept-graph":
|
||||
break;
|
||||
case "accept-command":
|
||||
if (message.result) {
|
||||
updateValue(message.result);
|
||||
}
|
||||
break;
|
||||
case "error":
|
||||
console.log("%cError-Message received!", "color:white;background:red");
|
||||
console.log(message);
|
||||
|
@ -48,7 +48,7 @@ function createContentConsole(s) {
|
||||
histIndex = -1;
|
||||
// Request for command.
|
||||
reqJSON(s, "http://" + hostPort + "/sendcommand?command="
|
||||
+ commandline.value + "&id=" + clientID, successHandler,
|
||||
+ encodeURIComponent(commandline.value) + "&id=" + clientID, successHandler,
|
||||
errorHandler);
|
||||
commandline.value = "";
|
||||
};
|
||||
|
@ -223,7 +223,8 @@ let globalControls = (function (){
|
||||
controlBar.id = "control_bar";
|
||||
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);
|
||||
|
||||
@ -340,7 +341,8 @@ function loadGraphicsMenu(panel){
|
||||
menuGraphicsPopup.addEntry(aboutCurvesSettingsHelpEntry);
|
||||
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();
|
||||
exportPopup.hide();
|
||||
curvesSettingsPopup.hide();
|
||||
@ -349,9 +351,10 @@ function loadGraphicsMenu(panel){
|
||||
panel.appendChild(menuGraphicsPopup);
|
||||
menuGraphicsPopup.getContainer().style.top = "28px";
|
||||
menuGraphicsPopup.getContainer().style.right = "20px";
|
||||
menuGraphicsPopup.style.position = "absolute";
|
||||
panel.appendChild(graphicsMenuControl);
|
||||
graphicsMenuControl.style.marginLeft="6px";
|
||||
graphicsMenuControl.style.marginRight="22px";
|
||||
graphicsMenuControl.style.marginLeft="0px";
|
||||
graphicsMenuControl.style.marginRight="8px";
|
||||
graphicsMenuControl.style.marginTop="2px";
|
||||
}
|
||||
|
||||
@ -371,10 +374,9 @@ function loadExportPopup(){
|
||||
*/
|
||||
function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){
|
||||
|
||||
let binningParam = "None";
|
||||
if (binning !== null)
|
||||
binningParam = binning
|
||||
let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binningParam + "&id=" + clientID
|
||||
if (binning === null || binning == "None")
|
||||
binning = "";
|
||||
let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binning + "&id=" + clientID
|
||||
let a = document.createElement('a');
|
||||
a.href = exportURL
|
||||
a.download = true
|
||||
@ -395,9 +397,11 @@ function loadCurvesSettingsPopup(){
|
||||
let graphs = (function (){
|
||||
let dataset_to_graph_map = {}; // a dictionnary mapping a variable name to a two values array, containing its graph index and its position inside the graph
|
||||
let blocks, liveMode=true, top_vars=[], bottom_vars=[];
|
||||
let legendFlag = false, currentZoomMode = isTouchDevice ? 'xy' : 'x';
|
||||
let legendFlag = false;
|
||||
let currentZoomMode = isTouchDevice ? 'xy' : 'x';
|
||||
let prevTime = null, prevMin = null, prevMax = null, prevGraph = null; // zoom speed limitation
|
||||
let cursorLinePos = null; // the position of the cursor line (given by its x value)
|
||||
let clickMode = 0; // 1: mouse is down, 2: pan is active, 0: after mouse down
|
||||
|
||||
let type = 'linear'; // type of graphs axis to display
|
||||
|
||||
@ -415,7 +419,7 @@ let graphs = (function (){
|
||||
let minTime, maxTime; // the queried time range
|
||||
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 updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback)
|
||||
@ -489,7 +493,7 @@ let graphs = (function (){
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
createGraph(gindex, block); // we create at the current shown selector (gindex), the graph corresponding to the one clicked (block)
|
||||
})
|
||||
selection.appendChild(bel);
|
||||
@ -509,11 +513,13 @@ let graphs = (function (){
|
||||
if (liveMode && cursorLinePos === null)
|
||||
// gotoNowElm.innerHTML = '';
|
||||
// globalControls.getControlsMap()[goToNowKey].changeToAlt();
|
||||
console.log("Need to change to nothing");
|
||||
// console.log("Need to change to nothing");
|
||||
;
|
||||
else
|
||||
// gotoNowElm.innerHTML = 'go to now';
|
||||
// globalControls.getControlsMap()[goToNowKey].changeToMain();
|
||||
console.log("Need to change to seen");
|
||||
// console.log("Need to change to seen");
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -524,7 +530,7 @@ let graphs = (function (){
|
||||
* @param {{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}} block - The information of the block to create
|
||||
*/
|
||||
function createGraph(gindex, block){
|
||||
console.log("clear for create graph", gindex)
|
||||
// console.log("clear for create graph", gindex)
|
||||
clear(gindex);
|
||||
tag_dict[block.tag] = gindex;
|
||||
let dict = {} // {string: [name:string, label:string, color:string]}
|
||||
@ -538,12 +544,13 @@ let graphs = (function (){
|
||||
varlist = vars_array[gindex];
|
||||
let graph_elm = graph_elm_array[gindex];
|
||||
|
||||
timeDeltaAxis = maxTime - minTime
|
||||
setResolution(timeDeltaAxis)
|
||||
resolution = getResolution((maxTime - minTime) / 1000)
|
||||
|
||||
AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 + "&variables=" + varlist + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){
|
||||
|
||||
//console.log('Graph', block, data)
|
||||
AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000
|
||||
+ "&variables=" + varlist
|
||||
+ "&interval=" + resolution
|
||||
+ "&id=" + clientID).getJSON().then(function(data){
|
||||
// console.log('Graph', block, data);
|
||||
let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
|
||||
graph_array[gindex] = graph;
|
||||
|
||||
@ -555,7 +562,6 @@ let graphs = (function (){
|
||||
for(let e of data.graph[key]){
|
||||
pdata.push({x: e[0]*1000, y: e[1]});
|
||||
}
|
||||
|
||||
addDataset(gindex, key, pdata, dict[key])
|
||||
// if(pdata.length > 0){
|
||||
// addDataset(gindex, key, pdata, dict[key])
|
||||
@ -601,8 +607,8 @@ let graphs = (function (){
|
||||
* @returns If the minimun y-value of all the curves of the charts is greater than the maximum y-value (same)
|
||||
*/
|
||||
function autoScale(chart) {
|
||||
axis = chart.options.scales.yAxes[0];
|
||||
tax = chart.options.scales.xAxes[0].ticks;
|
||||
ay = chart.options.scales.y;
|
||||
ax = chart.options.scales.x;
|
||||
datasets = chart.data.datasets;
|
||||
let max = -1e99;
|
||||
let min = 1e99;
|
||||
@ -613,8 +619,8 @@ let graphs = (function (){
|
||||
for (let i = 0; i < datasets.length; i++){
|
||||
ds = datasets[i];
|
||||
if (ds.borderWidth == 1) continue;
|
||||
let lmax = maxAr(ds.data, tax.min, tax.max);
|
||||
let lmin = minAr(ds.data, tax.min, tax.max);
|
||||
let lmax = maxAr(ds.data, ax.min, ax.max);
|
||||
let lmin = minAr(ds.data, ax.min, ax.max);
|
||||
if(lmax > max)
|
||||
max = lmax;
|
||||
if(lmin < min)
|
||||
@ -648,8 +654,8 @@ let graphs = (function (){
|
||||
}
|
||||
extraMin = Math.min(min - ystep * 0.5, extraMin);
|
||||
extraMax = Math.max(max + ystep * 0.5, extraMax);
|
||||
if (min >= axis.ticks.min && axis.ticks.min >= extraMin &&
|
||||
max <= axis.ticks.max && axis.ticks.max <= extraMax) {
|
||||
if (min >= ay.min && ay.min >= extraMin &&
|
||||
max <= ay.max && ay.max <= extraMax) {
|
||||
//console.log('NOCHANGE', max, axis.ticks.max, extraMax)
|
||||
return; // do not yet change
|
||||
}
|
||||
@ -658,8 +664,8 @@ let graphs = (function (){
|
||||
min = extraMin;
|
||||
max = extraMax;
|
||||
}
|
||||
axis.min = axis.ticks.min = min;
|
||||
axis.max = axis.ticks.max = max;
|
||||
ay.min = min;
|
||||
ay.max = max;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -701,9 +707,13 @@ let graphs = (function (){
|
||||
legendFlag = true;
|
||||
let trect = evt.target.getBoundingClientRect();
|
||||
let X = evt.clientX - trect.x, Y = evt.clientY - trect.y;
|
||||
menuGraphicsPopup.hide();
|
||||
showLegends(true, false);
|
||||
cursorLine(X);
|
||||
if (X == cursorLinePos) {
|
||||
cursorLine(null);
|
||||
} else {
|
||||
menuGraphicsPopup.hide();
|
||||
showLegends(true, false);
|
||||
cursorLine(X);
|
||||
}
|
||||
setLiveMode();
|
||||
update();
|
||||
for (let gr of graph_array.slice(0, ngraphs)) {
|
||||
@ -714,7 +724,18 @@ let graphs = (function (){
|
||||
}
|
||||
}
|
||||
}
|
||||
container.addEventListener('click', clickHandler)
|
||||
function mouseDown(evt) {
|
||||
clickMode = 1;
|
||||
}
|
||||
function mouseUp(evt) {
|
||||
if (clickMode == 1) { // mouse was down, but no pan happend
|
||||
clickHandler(evt);
|
||||
}
|
||||
clickMode = 0;
|
||||
}
|
||||
// container.addEventListener('click', clickHandler)
|
||||
container.addEventListener('mousedown', mouseDown);
|
||||
container.addEventListener('mouseup', mouseUp);
|
||||
|
||||
/**
|
||||
* Sets (overwrite) the data (curve) of the given variable
|
||||
@ -758,10 +779,12 @@ let graphs = (function (){
|
||||
max = max/1000;
|
||||
}
|
||||
|
||||
timeDelta = currentMaxTime - currentMinTime
|
||||
setResolution(timeDelta)
|
||||
resolution = getResolution((currentMaxTime - currentMinTime) / 1000)
|
||||
|
||||
AJAX("http://" + hostPort + "/graph?time=" + min + ","+max+"&variables=" + variables() + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){
|
||||
AJAX("http://" + hostPort + "/graph?time=" + min + ","+max
|
||||
+"&variables=" + variables()
|
||||
+ "&interval=" + resolution
|
||||
+ "&id=" + clientID).getJSON().then(function(data){
|
||||
for(let key in data.graph){
|
||||
let pdata = [];
|
||||
for(let e of data.graph[key]){
|
||||
@ -788,8 +811,8 @@ let graphs = (function (){
|
||||
* @returns When data is received (no need to autoScale and update as it is done in reloadData)
|
||||
*/
|
||||
function checkReload(graph){
|
||||
let tk = graph.chart.options.scales.xAxes[0].ticks;
|
||||
let xmin = tk.min, xmax = tk.max;
|
||||
let ax = graph.chart.options.scales.x;
|
||||
let xmin = ax.min, xmax = ax.max;
|
||||
/*
|
||||
if (xmax < now()-100000) { // was 100000 = 100sec
|
||||
if (liveMode) console.log('UPDATES OFF?')
|
||||
@ -828,14 +851,14 @@ let graphs = (function (){
|
||||
* @param {*} graph - The graph Object on which the zoom callback has to be called
|
||||
*/
|
||||
function zoomCallback(graph){
|
||||
let tk, min, max;
|
||||
let a, min, max;
|
||||
if (currentZoomMode == 'y') {
|
||||
tk = graph.chart.options.scales.yAxes[0].ticks;
|
||||
a = graph.chart.options.scales.y;
|
||||
} else {
|
||||
tk = graph.chart.options.scales.xAxes[0].ticks;
|
||||
a = graph.chart.options.scales.x;
|
||||
}
|
||||
min = tk.min;
|
||||
max = tk.max;
|
||||
min = a.min;
|
||||
max = a.max;
|
||||
if (!isTouchDevice) {
|
||||
/*
|
||||
if (prevGraph != graph) {
|
||||
@ -859,8 +882,8 @@ let graphs = (function (){
|
||||
*/
|
||||
}
|
||||
if (currentZoomMode == 'y') {
|
||||
tk.min = min;
|
||||
tk.max = max;
|
||||
a.min = min;
|
||||
a.max = max;
|
||||
graph.setAutoScale(false);
|
||||
} else {
|
||||
if (liveMode && max < lastTime) setLiveMode(false);
|
||||
@ -874,10 +897,10 @@ let graphs = (function (){
|
||||
* Sets the resolution of the viewing window in milliseconds
|
||||
* @param {*} timeDelta - The difference between the maximum time and the minimum time of the window
|
||||
*/
|
||||
function setResolution(timeDelta){
|
||||
resolution = Math.ceil((timeDelta / container.getBoundingClientRect().width))
|
||||
function getResolution(timeDelta){
|
||||
return Math.ceil((timeDelta / container.getBoundingClientRect().width))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -896,7 +919,10 @@ let graphs = (function (){
|
||||
msRightTimestampGetVars = dateTimestampMs + timeValueMs;
|
||||
msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000;
|
||||
|
||||
AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestampGetVars/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){
|
||||
AJAX("http://" + hostPort + "/getvars").postForm(
|
||||
"time=" + msRightTimestampGetVars/1000
|
||||
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
||||
+ "&id="+ clientID).then(function(data){
|
||||
blocks = data.blocks;
|
||||
document.getElementById("device").innerHTML = data.device
|
||||
maxTime = msRightTimestampGetGraph;
|
||||
@ -924,8 +950,9 @@ let graphs = (function (){
|
||||
* @param {*} graph - The graph for which the function has to be called
|
||||
*/
|
||||
function panCallback(graph){
|
||||
let tk = graph.chart.options.scales.xAxes[0].ticks;
|
||||
let xmin = tk.min, xmax = tk.max;
|
||||
let ax = graph.chart.options.scales.x;
|
||||
let xmin = ax.min, xmax = ax.max;
|
||||
clickMode = 2; // mouse pan mode
|
||||
if (liveMode && xmax < lastTime) setLiveMode(false);
|
||||
setMinMax(xmin,xmax);
|
||||
update();
|
||||
@ -969,7 +996,10 @@ let graphs = (function (){
|
||||
window["wideGraphs"] = false; // will have no effect if hideRightPart is true
|
||||
adjustGrid();
|
||||
|
||||
AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestamp/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){
|
||||
AJAX("http://" + hostPort + "/getvars").postForm(
|
||||
"time=" + msRightTimestamp/1000 + "&userconfiguration="
|
||||
+ JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
||||
+ "&id="+ clientID).then(function(data){
|
||||
currentMaxTime = msRightTimestamp + 60000;
|
||||
currentMinTime = msLeftTimestamp;
|
||||
|
||||
@ -991,18 +1021,7 @@ let graphs = (function (){
|
||||
function buildGraphicsUI(){
|
||||
|
||||
let f = 0;
|
||||
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);
|
||||
})
|
||||
appendToGridElement(f, " ", "graphics", container);
|
||||
|
||||
let graphicsPanel = container.parentNode.querySelector('.panel')
|
||||
graphicsPanel.classList.add('graphics');
|
||||
@ -1043,7 +1062,7 @@ let graphs = (function (){
|
||||
currentMaxTime = maxTime;
|
||||
currentMinTime = minTime;
|
||||
}
|
||||
AJAX("http://" + hostPort + "/gettime?time=-1800,0&id="+ clientID).getJSON().then(function(data){
|
||||
AJAX("http://" + hostPort + "/gettime?time=" + window['timerange'] + "&id="+ clientID).getJSON().then(function(data){
|
||||
startTime = data.time[1]*1000;
|
||||
maxTime = startTime;
|
||||
currentMaxTime = maxTime + 60000;
|
||||
@ -1094,7 +1113,7 @@ let graphs = (function (){
|
||||
"/updategraph?" +
|
||||
"id=" + clientID).getJSON().then(function(data) {
|
||||
setLiveMode(data.live);
|
||||
console.log('LIVE create', liveMode)
|
||||
// console.log('LIVE create', liveMode)
|
||||
})
|
||||
|
||||
}
|
||||
@ -1271,7 +1290,10 @@ let graphs = (function (){
|
||||
function applySettingsCallback(userConfiguration){
|
||||
cursorLine(null);
|
||||
|
||||
AJAX("http://" + hostPort + "/getvars").postForm("time=" + currentMaxTime/1000 + "&userconfiguration=" + JSON.stringify(userConfiguration) + "&id="+ clientID).then(function(data){
|
||||
AJAX("http://" + hostPort + "/getvars").postForm(
|
||||
"time=" + currentMaxTime/1000
|
||||
+ "&userconfiguration=" + JSON.stringify(userConfiguration)
|
||||
+ "&id="+ clientID).then(function(data){
|
||||
blocks = data.blocks;
|
||||
document.getElementById("device").innerHTML = data.device
|
||||
maxTime = currentMaxTime;
|
||||
@ -1363,119 +1385,138 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
parent.appendChild(canvas);
|
||||
let ctx = canvas.getContext("2d");
|
||||
let self = this;
|
||||
chart = new Chart(ctx, {
|
||||
type: 'scatter',
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation:{duration:0},
|
||||
scales: {
|
||||
yAxes: [{ticks: {
|
||||
beginAtZero: false,
|
||||
mirror: true,
|
||||
padding: -10,
|
||||
//workaround for proper number format
|
||||
callback: function(label, index, labels) {
|
||||
if(index == 0 || index == labels.length-1)
|
||||
return "";
|
||||
return strFormat(label);
|
||||
}
|
||||
},
|
||||
gridLines:{drawTicks:false},
|
||||
scaleLabel: false, // {display: true, labelString: y_label},
|
||||
type: scaleType,
|
||||
position: 'right',
|
||||
afterBuildTicks: function(axis, ticks) {
|
||||
if (scaleType == "logarithmic" && ticks.length <= 4) {
|
||||
y1 = ticks[0];
|
||||
y0 = ticks.slice(-1)[0];
|
||||
span = y1 - y0;
|
||||
step = Math.abs(span * 0.3).toExponential(0);
|
||||
if (step[0] > '5') {
|
||||
step = '5' + step.substr(1);
|
||||
} else if (step[0] > '2') {
|
||||
step = '2' + step.substr(1);
|
||||
}
|
||||
step = Number.parseFloat(step);
|
||||
ticks = [y1];
|
||||
for (let yt = Math.ceil(y1 / step) * step; yt > y0; yt -= step) {
|
||||
ticks.push(yt);
|
||||
}
|
||||
ticks.push(y0);
|
||||
}
|
||||
return ticks
|
||||
},
|
||||
}],
|
||||
xAxes: [{
|
||||
scaleLabel: false,//{display: true, labelString: x_label},
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: {'millisecond': 'HH:mm:ss.SSS', 'second': 'HH:mm:ss', 'minute': 'HH:mm','hour': 'dd HH:mm', 'day': 'dd MMM DD', 'week': 'MMM DD', 'month': 'MMM DD'},
|
||||
},
|
||||
ticks: { padding: -20,
|
||||
callback: function(label, index, labels) {
|
||||
let l = labels.length - 1;
|
||||
if (index == 0 || index == l) return "";
|
||||
if (index == 1 || index == l - 1) {
|
||||
// skip first and / or last label, if too close to the end
|
||||
let minstep = 0.05 * (labels[l].value - labels[0].value);
|
||||
if (index == 1) {
|
||||
if (labels[1].value - labels[0].value < minstep) return "";
|
||||
} else {
|
||||
if (labels[l].value - labels[l-1].value < minstep) return "";
|
||||
}
|
||||
}
|
||||
hourofday = /\S+ (\d+:00)/.exec(label);
|
||||
if (hourofday && hourofday[1] != '00:00') {
|
||||
return hourofday[1];
|
||||
}
|
||||
return label;
|
||||
|
||||
let chart_options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation:{duration:0},
|
||||
scales: {
|
||||
y:{
|
||||
beginAtZero: false,
|
||||
ticks:{
|
||||
mirror: true,
|
||||
padding: -10,
|
||||
callback: function(label, index, labels) {
|
||||
if(index == 0 || index == labels.length-1)
|
||||
return "";
|
||||
return strFormat(label);
|
||||
}
|
||||
},
|
||||
grid:{drawTicks:false},
|
||||
title: false, //Former scaleLabel
|
||||
type: scaleType,
|
||||
position: 'right',
|
||||
afterBuildTicks: function(axis) {
|
||||
let ticks = axis.ticks
|
||||
if (scaleType == "logarithmic" && ticks.length <= 4) {
|
||||
y1 = ticks[0];
|
||||
y0 = ticks.slice(-1)[0];
|
||||
span = y1 - y0;
|
||||
step = Math.abs(span * 0.3).toExponential(0);
|
||||
if (step[0] > '5') {
|
||||
step = '5' + step.substr(1);
|
||||
} else if (step[0] > '2') {
|
||||
step = '2' + step.substr(1);
|
||||
}
|
||||
},
|
||||
afterBuildTicks: function(axis, ticks) {
|
||||
if (!ticks || ticks.length <= 2) return ticks;
|
||||
first = ticks[0].value;
|
||||
step = ticks[1].value - first;
|
||||
offset = (first - axis._adapter.startOf(first, 'day')) % step;
|
||||
let start = 0;
|
||||
if (ticks[0].value - offset < axis.min) start = 1;
|
||||
let v = axis.min;
|
||||
result = [{value: v, major: false}];
|
||||
for (tick of ticks.slice(start)) {
|
||||
v = tick.value - offset;
|
||||
result.push({value: v, major: false});
|
||||
}
|
||||
v += step;
|
||||
if (v < axis.max) result.push({value:v, major: false});
|
||||
result.push({value: axis.max, major: false});
|
||||
return result;
|
||||
},
|
||||
gridLines:{drawTicks:false},
|
||||
}],
|
||||
step = Number.parseFloat(step);
|
||||
ticks = [y1];
|
||||
for (let yt = Math.ceil(y1 / step) * step; yt > y0; yt -= step) {
|
||||
ticks.push(yt);
|
||||
}
|
||||
ticks.push(y0);
|
||||
}
|
||||
return ticks
|
||||
},
|
||||
},
|
||||
tooltips: false,
|
||||
legend: false,
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: 'xy',
|
||||
speed: 10,
|
||||
threshold: 10,
|
||||
onPan: function({chart}) { graphs.panCallback(chart.graph);},
|
||||
//onPanComplete: function({chart}){graphs.checkReload(chart.graph);redraw()},
|
||||
onPanComplete: function({chart}){graphs.updateAuto();},
|
||||
},
|
||||
zoom: {
|
||||
enabled: true,
|
||||
drag: false,
|
||||
mode: isTouchDevice ? 'xy': 'x',
|
||||
speed: 0.1,
|
||||
sensitivity: 1,
|
||||
onZoom: function({chart}) { graphs.zoomCallback(chart.graph);},
|
||||
//onZoomComplete: function({chart}){graphs.checkReload(chart.graph);redraw()},
|
||||
onZoomComplete: function({chart}){graphs.onZoomCompleteCallback()},
|
||||
x:{
|
||||
title: false, // former scaleLabel
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: {'millisecond': 'HH:mm:ss.SSS', 'second': 'HH:mm:ss', 'minute': 'HH:mm','hour': 'EEE d. HH:mm', 'day': 'EE d.', 'week': 'd. MMM yy', 'month': 'MMM yy'},
|
||||
},
|
||||
ticks: {
|
||||
padding: -20,
|
||||
// stepSize: 180000,
|
||||
autoSkip: true,
|
||||
maxRotation: 0,
|
||||
// callback not used, this is better done in afterBuildTicks
|
||||
},
|
||||
afterBuildTicks: function(axis) {
|
||||
let ticks = axis.ticks
|
||||
|
||||
if (!ticks || ticks.length <= 2) return ticks;
|
||||
first = ticks[0].value;
|
||||
step = ticks[1].value - first;
|
||||
offset = (first - axis._adapter.startOf(first, 'day')) % step;
|
||||
let result = [];
|
||||
let v = axis.min;
|
||||
for (tick of ticks) {
|
||||
v = tick.value - offset;
|
||||
if (v > axis.min + step / 2) {
|
||||
result.push({value: v, major: false});
|
||||
}
|
||||
}
|
||||
v += step;
|
||||
if (v < axis.max) result.push({value:v, major: false});
|
||||
axis.ticks = result;
|
||||
// return result;
|
||||
},
|
||||
beforeFit: function(axis) { // called after ticks are autoskipped
|
||||
prevday = '';
|
||||
for (tick of axis.ticks) {
|
||||
s = tick.label.split(' ');
|
||||
if (s.length == 3) { // format with day
|
||||
// show date only on first label of a day
|
||||
day = s.slice(0, 2).join(' ');
|
||||
if (day != prevday) {
|
||||
prevday = day;
|
||||
} else {
|
||||
tick.label = s[2]; // time
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
grid:{drawTicks:false},
|
||||
}
|
||||
},
|
||||
plugins:{
|
||||
tooltip: false,
|
||||
legend: false,
|
||||
zoom:{
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: 'xy',
|
||||
speed: 10,
|
||||
threshold: 10,
|
||||
onPan: function({chart}) { graphs.panCallback(chart.graph);},
|
||||
onPanComplete: function({chart}){graphs.updateAuto();},
|
||||
},
|
||||
zoom: {
|
||||
wheel:{
|
||||
enabled: true
|
||||
},
|
||||
pinch:{
|
||||
enabled: true
|
||||
},
|
||||
mode: isTouchDevice ? 'xy': 'x',
|
||||
speed: 0.1,
|
||||
sensitivity: 1,
|
||||
onZoom: function({chart}) { graphs.zoomCallback(chart.graph);},
|
||||
onZoomComplete: function({chart}){graphs.onZoomCompleteCallback()},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (gindex != 0) {
|
||||
// show time labels only on first chart
|
||||
chart_options.scales.x.ticks.callback = function () { return ' '; }
|
||||
}
|
||||
|
||||
chart = new Chart(ctx, {type: 'scatter', options: chart_options})
|
||||
|
||||
//console.log('create legend')
|
||||
let legend = document.createElement('div');
|
||||
@ -1630,6 +1671,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
legend.style.display = 'none';
|
||||
|
||||
let margin = 10;
|
||||
let linewidth = 3;
|
||||
|
||||
canvas.addEventListener('mouseover', function(e){
|
||||
graphs.bringToFront(legend);
|
||||
@ -1657,7 +1699,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
* @param {string} to - The zoom mode to set
|
||||
*/
|
||||
function setZoomMode(to){
|
||||
chart.options.zoom.mode = to;
|
||||
chart.options.plugins.zoom.zoom.mode = to;
|
||||
}
|
||||
|
||||
// Unused
|
||||
@ -1682,7 +1724,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
function addDataset(key, data, opts){
|
||||
let dataset_index = chart.data.datasets.length;
|
||||
chart.data.datasets.push({data: data, label: opts.label, key: key,
|
||||
spanGaps: false, lineJoin: 'round', borderWidth: 2, steppedLine: opts.period == 0,
|
||||
spanGaps: false, borderJoinStyle: 'bevel', borderWidth: linewidth, stepped: opts.period == 0,
|
||||
borderColor: opts.color,fill: false, pointRadius: 0, tension:0, showLine: true});
|
||||
|
||||
let dataset = chart.data.datasets[dataset_index];
|
||||
@ -1703,6 +1745,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
color.appendChild(colorline);
|
||||
colorline.classList.add('colorline');
|
||||
colorline.style.backgroundColor = dataset.borderColor;
|
||||
colorline.style.height = linewidth + 'px';
|
||||
|
||||
dlabel.innerHTML = dataset.label;
|
||||
//dlabel.addEventListener('click', function(evt){
|
||||
@ -1730,13 +1773,13 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
for (ds of chart.data.datasets) {
|
||||
ds.borderWidth = 1;
|
||||
}
|
||||
colorline.style.height = '2px';
|
||||
dataset.borderWidth = 2;
|
||||
colorline.style.height = linewidth + 'px';
|
||||
dataset.borderWidth = linewidth;
|
||||
dlabel.style.fontWeight = 700; // bold
|
||||
} else {
|
||||
if (dataset.borderWidth == 1) {
|
||||
colorline.style.height = '2px';
|
||||
dataset.borderWidth = 2;
|
||||
colorline.style.height = linewidth + 'px';
|
||||
dataset.borderWidth = linewidth;
|
||||
} else {
|
||||
colorline.style.height = '1px';
|
||||
dataset.borderWidth = 1;
|
||||
@ -1746,10 +1789,10 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
}
|
||||
if (allDeselected) {
|
||||
for (ds of chart.data.datasets) {
|
||||
ds.borderWidth = 2;
|
||||
ds.borderWidth = linewidth;
|
||||
}
|
||||
for (let k in legendlines) {
|
||||
legendlines[k].style.height = '2px';
|
||||
legendlines[k].style.height = linewidth + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1802,8 +1845,8 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
* @param {number} max - The maximum timestamp in milliseconds of the viewing window
|
||||
*/
|
||||
function setMinMax(min, max){
|
||||
let ax = chart.options.scales.xAxes[0];
|
||||
let ay = chart.options.scales.yAxes[0];
|
||||
let ax = chart.options.scales.x;
|
||||
let ay = chart.options.scales.y;
|
||||
// clamp X-span
|
||||
let span = max - min;
|
||||
let half = 0;
|
||||
@ -1818,13 +1861,13 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
mid = (chart.lastXmin + chart.lastXmax) * 0.5;
|
||||
min = mid - half;
|
||||
max = mid + half;
|
||||
ay.ticks.min = chart.lastYmin;
|
||||
ay.ticks.max = chart.lastYmax;
|
||||
ay.min = chart.lastYmin;
|
||||
ay.max = chart.lastYmax;
|
||||
} else {
|
||||
chart.lastXmin = min;
|
||||
chart.lastXmax = max;
|
||||
chart.lastYmin = ay.ticks.min;
|
||||
chart.lastYmax = ay.ticks.max;
|
||||
chart.lastYmin = ay.min;
|
||||
chart.lastYmax = ay.max;
|
||||
}
|
||||
// custom algorithm for tick step
|
||||
mainstep = 1000;
|
||||
@ -1844,13 +1887,13 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
}
|
||||
mainstep *= info[1];
|
||||
}
|
||||
ax.time.unit = info[0];
|
||||
ax.time.stepSize = Math.round(step / mainstep);
|
||||
//ax.ticks.unit = ax.time.unit;
|
||||
//ax.ticks.stepSize =ax.time.stepSize;
|
||||
//console.log('INFO', step, mainstep, info, ax, ax.time);
|
||||
ax.ticks.max = max;
|
||||
ax.ticks.min = min;
|
||||
unit = info[0];
|
||||
rstep = Math.round(step / mainstep);
|
||||
ax.time.unit = unit;
|
||||
ax.ticks.stepSize = rstep;
|
||||
|
||||
ax.max = max;
|
||||
ax.min = min;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1873,7 +1916,7 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
* Called when log button in the legend is clicked
|
||||
*/
|
||||
function toggleAxesType(){
|
||||
setAxesType((chart.options.scales.yAxes[0].type=== 'linear') ? 'logarithmic' : 'linear');
|
||||
setAxesType((chart.options.scales.y.type=== 'linear') ? 'logarithmic' : 'linear');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1887,11 +1930,11 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
}else{
|
||||
linlog.innerHTML = "<strong>☒</strong> log";
|
||||
}
|
||||
chart.options.scales.yAxes[0].type = type;
|
||||
chart.options.animation.duration = 800;
|
||||
chart.options.scales.y.type = type;
|
||||
//chart.options.animation.duration = 800;
|
||||
if (autoScaleFlag) graphs.autoScale(chart);
|
||||
update();
|
||||
setTimeout(function(){chart.options.animation.duration = 0;},850)
|
||||
//setTimeout(function(){chart.options.animation.duration = 0;},850)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1903,10 +1946,15 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
if (x === null) return;
|
||||
for(let i in chart.data.datasets){
|
||||
let y = null;
|
||||
for(let j = 0; j < chart.getDatasetMeta(i).data.length; j++){
|
||||
let dp = chart.getDatasetMeta(i).data[j];
|
||||
if (dp._model.x >= x) break;
|
||||
y = chart.data.datasets[i].data[dp._index].y;
|
||||
let metadata = chart.getDatasetMeta(i).data;
|
||||
let dataset = chart.data.datasets[i];
|
||||
if (metadata.length != dataset.data.length) {
|
||||
console.log('length mismatch in dataset.data and metadata')
|
||||
}
|
||||
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];
|
||||
if (labelMinWidth == 0) {
|
||||
@ -1915,7 +1963,9 @@ function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
||||
labelMinWidth = valueElm.clientWidth;
|
||||
valueElm.style.minWidth = labelMinWidth + 'px';
|
||||
}
|
||||
if (y !== null) {
|
||||
if (y == null) {
|
||||
valueElm.innerHTML = '';
|
||||
} else {
|
||||
valueElm.innerHTML = strFormat(y, labelDigits);
|
||||
}
|
||||
}
|
||||
@ -1996,6 +2046,7 @@ function updateCharts2(graph){
|
||||
console.log('graphs.doUpdates skipped');
|
||||
return;
|
||||
}
|
||||
//console.log('G', graph);
|
||||
for(let key in graph){
|
||||
if (graph[key][0] != null) {
|
||||
// there is at least ONE valid datapoint
|
||||
|
@ -1,37 +1,25 @@
|
||||
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
// % GROUP
|
||||
|
||||
var writePermissionTimeout; // Sets writePermission to 'false, restarts by
|
||||
// user-interaction.
|
||||
|
||||
var writePermission = false;
|
||||
var showParams = false;
|
||||
var showConsole = false;
|
||||
var prompt = false // True while a prompt is opened.
|
||||
|
||||
function getGroup(s, name) {
|
||||
var found = false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
// COMMUNICATION
|
||||
|
||||
function sendCommand(s, command) {
|
||||
reqJSON(s, "http://" + hostPort + "/sendcommand?command=" + command
|
||||
function getGroup(s, name) {
|
||||
reqJSON(s, "http://" + hostPort + "/getblock?path=" + name
|
||||
+ "&id=" + clientID, successHandler, errorHandler);
|
||||
}
|
||||
|
||||
function createContent(s, message) {
|
||||
function sendCommand(s, command) {
|
||||
reqJSON(s, "http://" + hostPort + "/sendcommand?command=" + encodeURIComponent(command)
|
||||
+ "&id=" + clientID, successHandler, errorHandler);
|
||||
}
|
||||
|
||||
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
// GROUP
|
||||
|
||||
function createContent(message) {
|
||||
// Depending on the message received from the server the content of the
|
||||
// group is created dynamically. Handles draw-message.
|
||||
|
||||
@ -44,267 +32,443 @@ function createContent(s, message) {
|
||||
component.title = component.name;
|
||||
if (!("command" in component))
|
||||
component.command = component.name;
|
||||
createFunc = window['create_' + component.type + '_row']
|
||||
if (createFunc)
|
||||
content.appendChild(createFunc(s, component))
|
||||
|
||||
if (message.title == 'modules') {
|
||||
let row = createRowForModules(component);
|
||||
content.appendChild(row);
|
||||
} else {
|
||||
let row = createRowForParameters(component);
|
||||
content.appendChild(row);
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function gotoGroups(slideNames) {
|
||||
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 create_group_row(s, component) {
|
||||
// Creates row-element containing link.
|
||||
var title = component.title;
|
||||
|
||||
var row = document.createElement('row');
|
||||
row.id = component.name;
|
||||
row.name = title;
|
||||
row.classList.add("interactive", "row", "link");
|
||||
row.tabIndex = "0";
|
||||
|
||||
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;
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
// ROW
|
||||
|
||||
function createRowForModules(component) {
|
||||
let left = createLeftColumnForModules(component);
|
||||
left.id = component.name;
|
||||
left.name = component.title;
|
||||
left.classList.add("interactive", "link");
|
||||
left.setAttribute('name', 'component.title');
|
||||
|
||||
row = appendToContent(component, left, createParElement(component));
|
||||
let right = createRightColumnForModules(component);
|
||||
let row = appendToContent(left, right);
|
||||
|
||||
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);
|
||||
}
|
||||
return appendToContent(component, left, createParElement(component));
|
||||
row.classList.add('row-clickable');
|
||||
return row;
|
||||
}
|
||||
|
||||
function create_pushbutton_row(s, component) {
|
||||
// Creates row-element containing a push button
|
||||
function createRowForParameters(component) {
|
||||
let left = createLeftColumnForParameters(component);
|
||||
let right = createRightColumnForParameters(component);
|
||||
return appendToContent(left, right);
|
||||
}
|
||||
|
||||
var name = component.name;
|
||||
var command = component.command;
|
||||
var left = createTitle(component);
|
||||
function appendToContent(left, right) {
|
||||
let row = document.createElement('div');
|
||||
row.classList.add("row");
|
||||
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) {
|
||||
|
||||
left.id = component.name;
|
||||
left.name = component.title;
|
||||
var right = document.createElement('span');
|
||||
right.classList.add('col-right', 'col-right-modules');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var right = createParElement(component);
|
||||
right.classList.add("clickable", "push-button");
|
||||
function createRightColumnForParameters(component) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
row = appendToContent(component, left, right);
|
||||
right.onclick = function () {
|
||||
if (writePermission) {
|
||||
var row = left.parentNode;
|
||||
right.style.backgroundColor = "orangered";
|
||||
// Request for command
|
||||
sendCommand(s, command);
|
||||
function createValue (component) {
|
||||
let value = document.createElement('span');
|
||||
value.classList.add('col-right-value');
|
||||
if (writePermission == true) {
|
||||
value.classList.add('col-right-value-with-write-permission');
|
||||
}
|
||||
value.setAttribute('name', component.name);
|
||||
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 {
|
||||
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;
|
||||
});
|
||||
icon_edit.setAttribute('src', 'res/icon_edit.png');
|
||||
}
|
||||
}
|
||||
|
||||
row.classList.add("row");
|
||||
return row;
|
||||
return icon_edit;
|
||||
}
|
||||
|
||||
function create_input_row(s, component) {
|
||||
// Creates row-element containing input-item.
|
||||
|
||||
var name = component.name;
|
||||
var command = component.command;
|
||||
|
||||
if (component.info) {
|
||||
var infoBox = createInfo(component);
|
||||
function chooseTypeOfInput (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;
|
||||
}
|
||||
var left = createTitle(component);
|
||||
return input_element;
|
||||
}
|
||||
|
||||
var input = createParElement(component, 'input', 'input-text');
|
||||
/* ---------------------------------------------------------------------------------- */
|
||||
// input elements
|
||||
|
||||
|
||||
function createInputText(component) {
|
||||
// Creates row-element containing input-item.
|
||||
|
||||
var input = createInputElement(component, 'input', 'input-text');
|
||||
input.type = "text";
|
||||
input.style.width = "100px";
|
||||
input.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
// Prevent updates, while user is changing textfield
|
||||
input.addEventListener("focus", function(evt) {
|
||||
let elm = evt.target;
|
||||
setTimeout(function(){elm.setSelectionRange(0, elm.value.length);},0);
|
||||
});
|
||||
|
||||
input.onkeydown = function (e) {
|
||||
if (e.which === 27 || e.key == "Escape") {
|
||||
if (e.key == "Escape") {
|
||||
// User decided to cancel
|
||||
input.value = intput.oldValue;
|
||||
let input = e.target;
|
||||
input.value = input.oldValue;
|
||||
resizeTextfield(input);
|
||||
var row = left.parentNode;
|
||||
row.style.backgroundColor = "white";
|
||||
var row = input.closest('div');
|
||||
row.classList.remove('row-waiting-for-answer');
|
||||
hideInputElements();
|
||||
}
|
||||
}
|
||||
|
||||
input.onfocus = function () {
|
||||
input.oldValue = input.value;
|
||||
|
||||
if (isTouchDevice)
|
||||
setTimeout(function () {
|
||||
posTextfield(s, left);
|
||||
}, 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');
|
||||
form.onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
if (writePermission) {
|
||||
var row = left.parentNode;
|
||||
row.style.backgroundColor = "orangered";
|
||||
// Request for command
|
||||
sendCommand(s, name + " " + input.value);
|
||||
input.blur();
|
||||
var row = form.closest('div');
|
||||
row.classList.add('row-waiting-for-answer');
|
||||
// Request for command
|
||||
input.actualValue = input.value;
|
||||
if (component.targetname) {
|
||||
sendCommand(s, component.targetname + " " + input.value);
|
||||
} else {
|
||||
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;
|
||||
});
|
||||
sendCommand(s, component.name + " " + input.value);
|
||||
}
|
||||
row.classList.add('row-waiting-for-answer');
|
||||
input.blur();
|
||||
hideInputElements();
|
||||
};
|
||||
form.appendChild(input);
|
||||
var right = createParElement(component);
|
||||
right.appendChild(form);
|
||||
return appendToContent(component, left, right);
|
||||
form.appendChild(createSubmitButton());
|
||||
form.inputChild = input;
|
||||
return form;
|
||||
}
|
||||
|
||||
function posTextfield(s, left) {
|
||||
var content = swiper[s].slides[swiper[s].activeIndex].childNodes[1];
|
||||
var row = left.parentNode;
|
||||
content.scrollTop = row.offsetTop - 30;
|
||||
function createCheckbox(component) {
|
||||
// Creates row-element containing checkbox-item
|
||||
let input = createInputElement(component, 'input', 'parameter-checkbox');
|
||||
input.type = "checkbox";
|
||||
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) {
|
||||
@ -319,192 +483,25 @@ function resizeTextfield(input) {
|
||||
}
|
||||
}
|
||||
|
||||
function create_checkbox_row(s, component) {
|
||||
// Creates row-element containing checkbox-item
|
||||
var command = component.command;
|
||||
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% */
|
||||
// CONTENT
|
||||
|
||||
var left = createTitle(component);
|
||||
function appendToGridElement(s, title, type, content) {
|
||||
let panel = document.createElement('div');
|
||||
panel.classList.add("panel");
|
||||
|
||||
var input = createParElement(component, 'input', 'parameter-checkbox');
|
||||
input.type = "checkbox";
|
||||
titlewrapper = document.createElement('span');
|
||||
titlewrapper.innerHTML = title;
|
||||
panel.appendChild(titlewrapper);
|
||||
|
||||
input.onkeyup = function (e) {
|
||||
if (e.keyCode === 32) {
|
||||
handleCheckbox();
|
||||
}
|
||||
}
|
||||
let gridContainer = document.createElement('div');
|
||||
gridContainer.classList.add("grid-container");
|
||||
// Store type so it can be found easiely later.
|
||||
gridContainer.slideType = type;
|
||||
gridContainer.appendChild(panel);
|
||||
gridContainer.appendChild(content);
|
||||
|
||||
var label = document.createElement('label');
|
||||
label.for = input;
|
||||
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;
|
||||
let gridelements = document.getElementsByClassName('grid-element');
|
||||
gridelements[s].innerHTML = "";
|
||||
gridelements[s].appendChild(gridContainer);
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
// % 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 clientID = ""; // ID given by server when SSE-connection is established.
|
||||
var clientTitle = ""; // Contains name of instrument and device.
|
||||
@ -11,15 +7,12 @@ var getUpdates = true;
|
||||
var getUpdatesGraphics = true;
|
||||
var initCommands = [];
|
||||
var loadingShown = true;
|
||||
var writePermission = false;
|
||||
var menuMode = false;
|
||||
var panelOn = true;
|
||||
var firstState = 0;
|
||||
|
||||
function Settings() {
|
||||
// get key/value pairs from search part of the URL and fill into query
|
||||
var qstr = location.search;
|
||||
console.log(qstr);
|
||||
// console.log(qstr);
|
||||
if (qstr) {
|
||||
var a = (qstr[0] === '?' ? qstr.substr(1) : qstr).split('&');
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
@ -73,50 +66,68 @@ new Settings()
|
||||
.treat("debugGraphics", "dg", to_bool, false)
|
||||
.treat("hostPort", "hp", 0, location.hostname + ":" + location.port)
|
||||
.treat("showMain", "sm", to_bool, true)
|
||||
.treat("showConsole", "sc", to_bool, true)
|
||||
.treat("showOverview", "so", to_bool, true)
|
||||
.treat("initConsole", "ic", to_bool, true)
|
||||
.treat("showGraphics", "sg", to_bool, true) // false)
|
||||
.treat("hideRightPart", "hr", to_bool, false) //used to completely disable the right part
|
||||
.treat("wideGraphs", "wg", to_bool, false) //used to toggle the size of the graphs part
|
||||
.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);
|
||||
|
||||
|
||||
let args = '';
|
||||
if (window.instrument) {
|
||||
args += "&instrument=" + window.instrument;
|
||||
} else {
|
||||
if (window.stream) { args += "&stream=" + window.stream; }
|
||||
if (window.device) { args += "&device=" + window.device; }
|
||||
}
|
||||
if (window.hideRightPart) { args += "&history_only=1"; }
|
||||
window.clientTags = args;
|
||||
|
||||
// console.log('TAGS', window.clientTags);
|
||||
|
||||
function loadFirstBlocks() {
|
||||
|
||||
if (showMain) pushInitCommand("getblock?path=main&", "main")
|
||||
if (showConsole) pushInitCommand("console?", "console")
|
||||
if (initConsole) pushInitCommand("console?", "console")
|
||||
if (nColumns == 1) { // probably mobile phone}
|
||||
if (showGraphics) pushInitCommand("gettime?time=-1800,0&", "graphics")
|
||||
if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
|
||||
if (showGraphics) pushInitCommand("gettime?time=" + window.timerange + "&", "graphics")
|
||||
var goFS = document.getElementById('header');
|
||||
goFS.addEventListener(
|
||||
'click',
|
||||
function () {
|
||||
document.body.requestFullscreen();
|
||||
},
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
|
||||
if (showGraphics) pushInitCommand("gettime?time=-1800,0&", "graphics")
|
||||
if (showGraphics) pushInitCommand("gettime?time=" + window.timerange + "&", "graphics")
|
||||
// last is shown first
|
||||
}
|
||||
}
|
||||
|
||||
function nextInitCommand() {
|
||||
// do the next init request
|
||||
if (initCommands.length > 0) {
|
||||
next = initCommands.shift();
|
||||
cmd = next[0]
|
||||
text = next[1]
|
||||
var loadingSpan = document.getElementsByClassName("loading-span")[0];
|
||||
loadingSpan.innerHTML = loadingSpan.innerHTML + "<br>loading " + htmlEscape(text) + " ...";
|
||||
reqJSON(0, "http://" + hostPort + "/" + cmd + "id=" + clientID, successHandler, errorHandler);
|
||||
} else if (loadingShown) {
|
||||
var loadingScreen = document.getElementsByClassName("loading-div")[0];
|
||||
loadingScreen.style.display = "none";
|
||||
loadingShown = false;
|
||||
if (location.hash) { // there was a #hash part
|
||||
var slideNames = location.hash.substr(1);
|
||||
gotoGroups(slideNames);
|
||||
// do the next init request
|
||||
if (initCommands.length > 0) {
|
||||
next = initCommands.shift();
|
||||
cmd = next[0]
|
||||
text = next[1]
|
||||
var loadingSpan = document.getElementsByClassName("loading-span")[0];
|
||||
loadingSpan.innerHTML = loadingSpan.innerHTML + "<br>loading " + htmlEscape(text) + " ...";
|
||||
reqJSON(0, "http://" + hostPort + "/" + cmd + "id=" + clientID, successHandler, errorHandler);
|
||||
} else if (loadingShown) {
|
||||
var loadingScreen = document.getElementsByClassName("loading-div")[0];
|
||||
loadingScreen.style.display = "none";
|
||||
loadingShown = false;
|
||||
console.log("loading finished");
|
||||
}
|
||||
console.log("loading finished");
|
||||
}
|
||||
}
|
||||
|
||||
function pushInitCommand(cmd, text) {
|
||||
initCommands.push([cmd, text]);
|
||||
initCommands.push([cmd, text]);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
@ -132,67 +143,118 @@ window.onload = function() {
|
||||
// rows 'n'
|
||||
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"]){
|
||||
document.body.removeChild(crossElement);
|
||||
}else{
|
||||
crossElement.onclick = function(){
|
||||
if(nColumns == 1){ // if the screen is small, the cross always slides to the next slide
|
||||
let someSwiper = swiper[0];
|
||||
someSwiper.enableSwiping(true); // needed because someSwiper might be the graphs swiper, and swiping is disable by default
|
||||
someSwiper.slideNext(); // someSwiper can be anything, it will swipe to the next slide
|
||||
}else{ // else it toggles the graphs window's size and triggers the adjustGrid()
|
||||
window["wideGraphs"] = !window['wideGraphs'];
|
||||
adjustGrid();
|
||||
if (window.hideRightPart){
|
||||
document.body.removeChild(icon_close_container);
|
||||
} else {
|
||||
icon_close_container.onclick = function(){
|
||||
if (showParams) {
|
||||
showParams = false;
|
||||
// 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">';
|
||||
} else {
|
||||
if (window.wideGraphs) {
|
||||
window.wideGraphs = false;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Create swiper-instances.
|
||||
for (var s = 0; s < MAXBLOCK; s++) {
|
||||
swiper[s] = insertSwiper(s);
|
||||
icon_log_container.onclick = function(){
|
||||
if (showConsole) {
|
||||
showConsole = false;
|
||||
} 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");
|
||||
|
||||
// TODO : uncomment this code with the right URL to navigate to when the way to select the instrument will be decided.
|
||||
// homeButton.onclick = function () {
|
||||
// window.location = "http://" + location.hostname + ":8800/";
|
||||
// };
|
||||
homeButton.onclick = function () {
|
||||
window.location = "/select_experiment";
|
||||
};
|
||||
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() {
|
||||
// Show and hide box showing name of the current device ('see also
|
||||
// SEAWebClient.html')
|
||||
|
||||
var main_panel = document.getElementById("main-panel");
|
||||
panelOn = !panelOn;
|
||||
if (panelOn) {
|
||||
|
@ -3,20 +3,27 @@
|
||||
|
||||
var nColumns = 1; // Viewport is subdivided in nColumns columns.
|
||||
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 MINHEIGHT = 700; // Minimal height of block.
|
||||
let paramSlider = [0,1,2,3]; // the number of the parameter slider to open
|
||||
let prevActiveSlider = 0;
|
||||
var MAXBLOCK = 4; // max number of blocks
|
||||
var elements = []; // grid elements
|
||||
|
||||
function createGrid() {
|
||||
// Creates grid-elements. By default only the first one is shown
|
||||
// and
|
||||
// takes the whole viewport.
|
||||
// Creates grid-elements.
|
||||
// 1 - graphics
|
||||
// 2 - modules
|
||||
// 3 - parameters
|
||||
// 4 - log
|
||||
var elements = [];
|
||||
for (var i = 0; i < 4; i++) {
|
||||
var element = document.createElement('div');
|
||||
let element = document.createElement('div');
|
||||
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);
|
||||
elements.push(element);
|
||||
}
|
||||
@ -45,10 +52,6 @@ function determineViewportSize() {
|
||||
if (height > MINHEIGHT) {
|
||||
nRows = 2;
|
||||
}
|
||||
if (menuMode) {
|
||||
nRows = 1;
|
||||
nColumns = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function sizeChange() {
|
||||
@ -57,83 +60,121 @@ function sizeChange() {
|
||||
}
|
||||
|
||||
function adjustGrid() {
|
||||
// Determines size of grid-elements depending on number of columns 'nColumns' and
|
||||
// rows 'nRows'
|
||||
// Determines size of grid-elements depending on number
|
||||
// of columns 'nColumns' and rows 'nRows'
|
||||
|
||||
var width = window.innerWidth || document.documentElement.clientWidth
|
||||
|| document.body.clientWidth;
|
||||
var height = window.innerHeight || document.documentElement.clientHeight
|
||||
|| document.body.clientHeight;
|
||||
paramSlider = [0,1,2,3];
|
||||
prevActiveSlider = 0;
|
||||
|
||||
if (window["hideRightPart"] || window["wideGraphs"]){
|
||||
style(0,"100vw","100vh");
|
||||
style(1); // hide
|
||||
style(2); // hide
|
||||
style(3); // hide
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
switch (nColumns) {
|
||||
case 1:
|
||||
if (menuMode) {
|
||||
leftWidth = Math.min(100, MINWIDTH / width * 100);
|
||||
style(0,leftWidth + "vw","100vh");
|
||||
style(1); // hide
|
||||
style(2); // hide
|
||||
style(3); // hide
|
||||
if (showConsole) {
|
||||
if (showParams) {
|
||||
style(0); // hide
|
||||
style(1); // hide
|
||||
style(2,"100vw","50vh");
|
||||
style(3,"100vw","50vh");
|
||||
} else {
|
||||
style(0); // hide
|
||||
style(1,"100vw","50vh");
|
||||
style(2); // hide
|
||||
style(3,"100vw","50vh");
|
||||
}
|
||||
} else {
|
||||
// we may want to switch to 90vh on safari ios (workaround)
|
||||
style(0,"100vw","100vh");
|
||||
style(1); // hide
|
||||
style(2); // hide
|
||||
style(3); // hide
|
||||
}
|
||||
if (showParams) {
|
||||
style(0); // hide
|
||||
style(1); // hide
|
||||
style(2,"100vw","100vh");
|
||||
style(3); // hide
|
||||
} else {
|
||||
style(0); // hide
|
||||
style(1,"100vw","100vh");
|
||||
style(2); // hide
|
||||
style(3); // hide
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
rightWidth = Math.min(50, 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
|
||||
if (showConsole) {
|
||||
if (nRows == 1) {
|
||||
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 {
|
||||
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 {
|
||||
style(0,leftWidth + "vw","100vh");
|
||||
style(1,rightWidth + "vw","50vh");
|
||||
style(2); // hide
|
||||
style(3,rightWidth + "vw","50vh");
|
||||
if (nRows == 1) {
|
||||
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
|
||||
}
|
||||
} 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;
|
||||
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:
|
||||
rightWidth = MINWIDTH / width * 100;
|
||||
leftWidth = 100 - 2 * rightWidth;
|
||||
if (nRows == 1) {
|
||||
style(0,leftWidth + "vw","100vh");
|
||||
style(1,rightWidth + "vw","100vh");
|
||||
style(2); // hide
|
||||
style(3,rightWidth + "vw","100vh");
|
||||
} else {
|
||||
if (showConsole) {
|
||||
style(0,leftWidth + "vw","100vh");
|
||||
style(1,rightWidth + "vw","50vh");
|
||||
style(2,rightWidth + "vw","50vh");
|
||||
style(3,(2 * rightWidth) + "vw","50vh");
|
||||
style(3,100 - leftWidth + "vw","50vh");
|
||||
} else {
|
||||
style(0,leftWidth + "vw","100vh");
|
||||
style(1,rightWidth + "vw","100vh");
|
||||
style(2,rightWidth + "vw","100vh");
|
||||
style(3); // hide
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -143,8 +184,6 @@ function adjustGrid() {
|
||||
|
||||
function style(s, width, height) {
|
||||
if (width) {
|
||||
paramSlider[prevActiveSlider] = s;
|
||||
prevActiveSlider = s;
|
||||
elements[s].style.display = "inline-block";
|
||||
elements[s].style.width = width;
|
||||
} else {
|
||||
|
@ -1,184 +0,0 @@
|
||||
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
// % 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;
|
||||
}
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 47 KiB |
BIN
client/res/icon_close.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
client/res/icon_edit.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
client/res/icon_edit_close.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
client/res/icon_height.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
client/res/icon_info.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
client/res/icon_lock_closed.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
client/res/icon_lock_open.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
client/res/icon_log.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
client/res/icon_menu_graphics.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
client/res/icon_modules.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
client/res/icon_okay.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
client/res/icon_sinus.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
client/res/icon_status.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
client/res/icon_width.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 35 KiB |
9
client/site.webmanifest
Normal file
@ -0,0 +1,9 @@
|
||||
{"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"}
|
||||
|
@ -18,7 +18,7 @@ def assign_colors_to_curves(blocks):
|
||||
auto_curves = []
|
||||
for curve in block["curves"]:
|
||||
|
||||
col = curve["color"].strip()
|
||||
col = curve.get("color", "").strip()
|
||||
c = ColorMap.to_code(col)
|
||||
if c < 0:
|
||||
valid = ColorMap.check_hex(col)
|
||||
|
@ -1,6 +1,8 @@
|
||||
[chart]
|
||||
tt=unit:K
|
||||
tt.target=unit:K
|
||||
tt.set_power=unit:W
|
||||
tt.power=unit:W
|
||||
cc=-
|
||||
hemot.target=-
|
||||
mf=unit:T
|
||||
@ -9,11 +11,32 @@ ts=unit:K
|
||||
ts.target=unit:K
|
||||
treg=-
|
||||
tmon=-
|
||||
T_oneK=unit:K
|
||||
T_sample=unit:K
|
||||
T_samplehtr=unit:K
|
||||
T_mix=unit:K
|
||||
T_sorb=unit:K
|
||||
T_oneK=unit:K,color:yellow
|
||||
T_sample=unit:K,color:blue
|
||||
T_samplehtr=unit:K,color:black
|
||||
T_mix=unit:K,color:cyan
|
||||
T_sorb=unit:K,color:dark_violet
|
||||
T_sorb.target=-
|
||||
T_still=unit:K,color:orange
|
||||
dil=-
|
||||
lev=unit:%
|
||||
dil.G1=unit:mbar
|
||||
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
|
||||
|
@ -1,4 +1,5 @@
|
||||
[INFLUX]
|
||||
url=http://localhost:8086
|
||||
url=http://linse-a:8086
|
||||
org=linse
|
||||
bucket=curve-test
|
||||
token=zqDbTcMv9UizfdTj15Fx_6vBetkM5mXN56EE9CiDaFsh7O2FFWZ2X4VwAAmdyqZr3HbpIr5ixRju07-oQmxpXw==
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
32
dummy-webserver
Executable file
@ -0,0 +1,32 @@
|
||||
#!/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
Normal file
@ -0,0 +1,181 @@
|
||||
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
@ -1,724 +0,0 @@
|
||||
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
|
513
influxgraph.py
@ -1,154 +1,287 @@
|
||||
import time
|
||||
from time import time as current_time
|
||||
import logging
|
||||
from colors import assign_colors_to_curves
|
||||
import json
|
||||
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 base import get_abs_time, HandlerBase
|
||||
|
||||
class InfluxGraph:
|
||||
"""
|
||||
Class implementing the logic of the different routes that are called by the client to retrieve graph data with InfluxDB.
|
||||
|
||||
def split_tags(tags):
|
||||
return {k: v.split(',') for k, v in tags.items()}
|
||||
|
||||
|
||||
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 :
|
||||
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).
|
||||
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)
|
||||
LIVE (int) : value that represents the "live" visualization mode, meaning that new points are sent to the client.
|
||||
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).
|
||||
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)
|
||||
LIVE (int) : value that represents the "live" visualization mode, meaning that new points are
|
||||
sent to the client.
|
||||
|
||||
Attributes :
|
||||
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
|
||||
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 or update.
|
||||
lastvalues ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values are tuples, where the first
|
||||
value is the unix timestamp of the most recent value known for this variable, and the second value its corresponding value
|
||||
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.
|
||||
end_query (int) : the unix timestamp in seconds of the most recent requested point in time of the last query
|
||||
or update.
|
||||
last_values ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values
|
||||
are tuples, where the first value is the unix timestamp of the most recent value known for this variable,
|
||||
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
|
||||
ACTUAL = 1
|
||||
LIVE = 2
|
||||
|
||||
def __init__(self, influx_data_getter, instrument):
|
||||
self.influx_data_getter = influx_data_getter
|
||||
self.chart_configs = [ChartConfig("./config/generic.ini"), ChartConfig(f"./config/{instrument}.ini")]
|
||||
def __init__(self, server, instrument, device_name, tags):
|
||||
"""create instance for retrieving history
|
||||
|
||||
: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.end_query = 0
|
||||
self.lastvalues = {}
|
||||
self.variables = {} # name:label
|
||||
|
||||
|
||||
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)
|
||||
self.last_values = {} # dict <variable> of last known point (<time>, <value>)
|
||||
self.last_time = {} # dict <stream> of last received time
|
||||
self.last_minute = 0
|
||||
self.last_update = 0 # time of last call with a result
|
||||
self.tags = None
|
||||
self.init_tags = tags
|
||||
|
||||
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)
|
||||
"""Get 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)
|
||||
variables (str) : a comma separated string of variable names (influx names) to retrieve
|
||||
time (str) : a commma separated value string (range) of seconds.
|
||||
values < one year are treated as relative from now.
|
||||
interval (str) : the interval (resolution) of the values to get (string in seconds)
|
||||
|
||||
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,
|
||||
which are an array containing the timestamp as their first value, and the y-value in float as their second one.
|
||||
{"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionary with its "graph-draw" type
|
||||
(so it can be processed by the client), and a "graph" dictionary 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 = self.get_abs_time(time + [0])
|
||||
start, end, now = int(start), int(end), int(now)
|
||||
queried_time_range = [start, end]
|
||||
start, end, now = get_abs_time([float(t) for t in time.split(',')] + [0])
|
||||
start, end, now = int(start), ceil(end), ceil(now)
|
||||
queried_variables = variables.split(',')
|
||||
self.livemode = self.ACTUAL if end+10 >= now else self.HISTORICAL
|
||||
logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode)
|
||||
if interval : interval = int(interval)
|
||||
if 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})
|
||||
|
||||
result = self.influx_data_getter.get_curves_in_timerange(queried_variables, queried_time_range, interval)
|
||||
self.complete_to_end_and_feed_lastvalues(result, min(end, now))
|
||||
self.end_query = end
|
||||
|
||||
return dict(type='graph-draw', graph=result)
|
||||
def update_last(self, curve_dict):
|
||||
"""update last values per variable and last time per stream"""
|
||||
for key, curve in curve_dict.items():
|
||||
stream = curve.tags.get('stream')
|
||||
tlast, value = curve[-1]
|
||||
self.last_values[key] = curve[-1]
|
||||
self.last_time[stream] = max(self.last_time.get(stream, 0), tlast)
|
||||
|
||||
def w_gettime(self, time):
|
||||
"""
|
||||
Gets the server time for the give time.
|
||||
"""Get the server time for the given time(range).
|
||||
|
||||
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.
|
||||
time (str="-1800,0") : the given point in time represented by a string,
|
||||
which is a comma separated unix timestamp values list (in seconds).
|
||||
values < one year are treated as relative from now.
|
||||
|
||||
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
|
||||
{"type":"time", "time":(int)} : a dictionary 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= self.get_abs_time(time))
|
||||
return dict(type='time', time=get_abs_time(
|
||||
[float(t) for t in time.split(',')]))
|
||||
|
||||
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.
|
||||
def w_getvars(self, time, userconfiguration=None, **_):
|
||||
"""Get 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.
|
||||
time (str) : the given point in time represented by a string, which is a unix timestamp in seconds.
|
||||
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
|
||||
|
||||
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")
|
||||
{"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(',')]
|
||||
end_time = int(self.get_abs_time(time)[-1])
|
||||
|
||||
if not userconfiguration == None : 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"]}
|
||||
time = get_abs_time([float(t) for t in time.split(',')])
|
||||
start_time = int(time[0])
|
||||
end_time = int(time[-1])
|
||||
if userconfiguration is not None:
|
||||
userconfiguration = json.loads(userconfiguration)
|
||||
|
||||
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)
|
||||
result = dict(type='var_list')
|
||||
result['blocks'] = blocks
|
||||
result['device'] = device_name
|
||||
# print('DEVICE', device_name, tags)
|
||||
# for block in blocks:
|
||||
# print(block['tag'], [c['name'] for c in block['curves']])
|
||||
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
|
||||
|
||||
def w_updategraph(self):
|
||||
"""
|
||||
Sets the current visualisation mode to LIVE if not in HISTORICAL mode.
|
||||
"""Set 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
|
||||
{"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
|
||||
"""
|
||||
logging.info("UPD GRAPH %d", self.livemode)
|
||||
if self.livemode == self.HISTORICAL:
|
||||
@ -157,7 +290,7 @@ class InfluxGraph:
|
||||
self.livemode = self.LIVE
|
||||
return dict(type='accept-graph', live=True)
|
||||
|
||||
def w_export(self, variables, time, nan, interval):
|
||||
def w_export(self, variables, time, nan, interval, timeoffset=None):
|
||||
"""
|
||||
Returns the bytes of a dataframe with the curves given by variables in the time range "time"
|
||||
Called when the route /export is reached.
|
||||
@ -172,56 +305,156 @@ class InfluxGraph:
|
||||
io.BytesIO : an BytesIO object containing the dataframe to retrieve
|
||||
"""
|
||||
|
||||
time = [float(t) for t in time.split(',')]
|
||||
start, end = self.get_abs_time(time)
|
||||
start, end = int(start), int(end)
|
||||
start, end = get_abs_time([float(t) for t in time.split(',')])
|
||||
start, end = int(start), ceil(end)
|
||||
|
||||
queried_variables = variables.split(',')
|
||||
if interval != "None" : interval = int(interval)
|
||||
|
||||
df = self.influx_data_getter.get_curves_data_frame(queried_variables, [start, end], interval, self.variables)
|
||||
|
||||
mem = io.BytesIO()
|
||||
df.to_csv(mem, sep="\t", index=False, float_format="%.15g", na_rep=nan)
|
||||
mem.seek(0)
|
||||
return mem
|
||||
interval = float(interval) if interval else None
|
||||
timeoffset = None if timeoffset == 'now' else (timeoffset or 0)
|
||||
result = self.db.export(start, end, queried_variables, timeoffset=timeoffset, none=nan, interval=interval,
|
||||
**self.tags)
|
||||
return io.BytesIO(result.encode('utf-8'))
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
{"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None :
|
||||
a dictionary with its "graph-update" type
|
||||
(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
|
||||
"""
|
||||
if self.livemode != self.LIVE:
|
||||
return None
|
||||
now, = self.get_abs_time([0])
|
||||
|
||||
result = self.influx_data_getter.poll_last_values(list(self.variables.keys()), self.lastvalues, now)
|
||||
for variable in self.lastvalues.keys():
|
||||
if variable in result.keys():
|
||||
|
||||
# removes points older than the last known point (queries are in seconds and might return points already displayed)
|
||||
while len(result[variable]) > 0:
|
||||
if result[variable][0][0] <= self.lastvalues[variable][0]:
|
||||
result[variable].pop(0)
|
||||
else:
|
||||
break
|
||||
|
||||
if len(result[variable]) > 0 and result[variable][-1][0] > self.lastvalues[variable][0]:
|
||||
self.lastvalues[variable] = (result[variable][-1][0], result[variable][-1][1])
|
||||
else:
|
||||
del result[variable]
|
||||
|
||||
if int(now / 60) != int(self.end_query / 60):
|
||||
# Update unchanged values every plain minute
|
||||
for var, (_, lastx) in self.lastvalues.items():
|
||||
if var not in result:
|
||||
result[var] = [(now, lastx)]
|
||||
self.end_query = now
|
||||
now = current_time()
|
||||
if now < int(self.last_update) + 1.5:
|
||||
# the server is only waiting after a None return
|
||||
# this avoids to many queries with expected empty result
|
||||
return None
|
||||
last_time = int(min(self.last_time.values(), default=now-3600))
|
||||
# if len(self.last_time) > 1:
|
||||
# print('time_poll_jitter', max(self.last_time.values()) - min(self.last_time.values()))
|
||||
prev_minute, self.last_minute = self.last_minute, now // 60
|
||||
fullminute = prev_minute != self.last_minute
|
||||
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:
|
||||
if not fullminute:
|
||||
to_remove[key] = l
|
||||
self.update_last(result)
|
||||
if fullminute:
|
||||
self.db.complete(result, self.last_time, 'stream')
|
||||
for key, length in to_remove.items():
|
||||
curve = result[key]
|
||||
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:
|
||||
return dict(type='graph-update', time=now, graph=result)
|
||||
return None
|
||||
self.last_update = now
|
||||
return dict(type='graph-update', time=last_time, graph=result)
|
||||
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)
|
||||
|
||||
|
167
instructions_specifications.md
Normal file
@ -0,0 +1,167 @@
|
||||
# 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
|
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
gevent
|
||||
flask
|
||||
frappy-core
|
230
seagraph.py
@ -1,230 +0,0 @@
|
||||
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
|
||||
|
44
secop-webserver
Executable file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
from os.path import expanduser
|
||||
# look for sehistory and frappy at usual locations in home directory
|
||||
sys.path.extend([expanduser('~'), expanduser('~/frappy')])
|
||||
import argparse
|
||||
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 = None if args.instrument=='main' else args.instrument
|
||||
server.run(int(args.port), SEHistory(), InfluxGraph, Client, single_instrument=instrument, secop=SecopInteractor)
|
173
secop.py
Normal file
@ -0,0 +1,173 @@
|
||||
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 = {}
|
||||
try:
|
||||
self.connect()
|
||||
node_map.update({k: self for k in self.modules})
|
||||
self.register_callback(None, updateItem=self.updateItem)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
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"]
|
@ -1,133 +0,0 @@
|
||||
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()
|
466
webserver.py
Executable file
@ -0,0 +1,466 @@
|
||||
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
|
||||
|
||||
|
||||
instruments = {ins: 8642 for ins in
|
||||
['amor', 'boa', 'camea', 'dmc', 'eiger', 'focus', 'hrpt', 'sans', 'tasp', 'zebra']
|
||||
}
|
||||
|
||||
|
||||
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, history_only=None):
|
||||
streams, tags, device_name = self.lookup_streams(instrument, stream, device)
|
||||
if (history_only or '0') != '0':
|
||||
# create dummy client
|
||||
client = self.client_cls(self, [], '', '')
|
||||
else:
|
||||
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=f'logfile{port}.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', 'history_only')}
|
||||
|
||||
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 server.single_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.append('</table>')
|
||||
# out.append('<h3>servers on the instruments:</h3>')
|
||||
# out.extend([f"<a href='http://{i.lower()}.psi.ch:8642/'>{i}</a> \n" for i in instlist])
|
||||
# out.extend(['</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;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style></head>
|
||||
<body><table>
|
||||
''']
|
||||
ONEMONTH = 30 * 24 * 3600
|
||||
|
||||
out.append('<br><i>direct link to instruments:</i><br>')
|
||||
out.extend([f'<a href="http://{ins}.psi.ch:{port}/">{ins.upper()}</a> \n'
|
||||
for ins, port in instruments.items()])
|
||||
if server.db.has_local:
|
||||
out.append('<h3><a href="http://linse-c.psi.ch:8888/">linse-c (central)</a></h3>')
|
||||
|
||||
class prev: # just a namesapce
|
||||
title = None
|
||||
legend = None
|
||||
|
||||
def change_title(text):
|
||||
if text == prev.title:
|
||||
return False
|
||||
if prev.legend:
|
||||
out.append(f'<tr>{prev.legend}</tr>')
|
||||
prev.legend = None
|
||||
prev.title = text
|
||||
out.append(f'<tr><td colspan=3><br><i>{text}:</i></td></tr>')
|
||||
return True
|
||||
|
||||
# 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))
|
||||
port = None
|
||||
if key[0] == 'instrument':
|
||||
ins = key[1]
|
||||
port = instruments.get(ins)
|
||||
left = ins.upper()
|
||||
else:
|
||||
left = key[1] # shown in left column
|
||||
args = ['='.join(key)]
|
||||
remote = None if port is None else f'http://{ins}.psi.ch:{port}'
|
||||
history_only = bool(remote)
|
||||
if end > now:
|
||||
if begdate == today:
|
||||
daterange = f'since {time.strftime("%H:%M", time.localtime(beg))}'
|
||||
else:
|
||||
daterange = f'since {begdate}'
|
||||
change_title('currently running')
|
||||
else:
|
||||
args.append(f'time={beg},{end}')
|
||||
history_only = True
|
||||
remote = None
|
||||
daterange = begdate if begdate == enddate else f'{begdate}...{enddate}'
|
||||
if end > now - ONEMONTH:
|
||||
change_title('recently running (history graphics only)')
|
||||
else:
|
||||
change_title('older than 30 days')
|
||||
if history_only:
|
||||
args.append('hr=1')
|
||||
|
||||
def link(label):
|
||||
return f'<a href="/?{"&".join(args)}">{label}</a>'
|
||||
|
||||
label = " ".join(devices)
|
||||
if remote:
|
||||
prev.legend = '<td></td><td></td><td colspan=2>linse-c*: <i>history graphics only</i></td>'
|
||||
out.append(f'<tr><td><a href="{remote}">{ins.upper()}</a></td>'
|
||||
f'<td>{label}</td><td>{link("linse-c*")}</td>')
|
||||
else:
|
||||
out.append(f'<tr><td>{link(left)}</td><td colspan=2>{label}</td>')
|
||||
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.append('</table>')
|
||||
|
||||
out.extend(['</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]))
|