188 Commits

Author SHA1 Message Date
a6ec455563 select_experiment: move links to instruments to top 2025-05-23 13:50:21 +02:00
be404b625b main js: fix clientTags 2025-05-23 13:05:17 +02:00
191f0aed80 logfile name depending on port 2025-05-23 13:04:37 +02:00
b24384f387 improve further select_experiments: add time range in link 2025-05-21 15:37:19 +02:00
e136b66732 no extra headline for central server 2025-05-21 13:14:26 +02:00
822e3ab6a2 big fix: treat history_only=None properly 2025-05-21 13:08:06 +02:00
b1ffe99a5d remove grey background on select_experiments 2025-05-21 12:11:16 +02:00
7d4607e947 work on select_experiment
- on currently running exp. make two links, to instrument
  or history only
- create dummy secop client when hideRightPart is used in order
  to avoid waiting impossible connection
2025-05-21 11:35:25 +02:00
55dd7a3777 fix bug not changing host 2025-05-20 16:59:25 +02:00
fd37ee0dfc cosmetics on home 2025-05-20 16:55:10 +02:00
90e8aa5df0 select_experiment: use links to instruments 2025-05-20 16:51:01 +02:00
6df42b9541 continue with graphics when secop connection fails 2025-05-20 16:51:01 +02:00
2984c051e9 use single instrument name instead of 'n_a' 2025-05-20 16:04:38 +02:00
46fca20f09 add links to servers on intruments 2025-05-20 15:10:19 +02:00
2ed1e3c292 instrument=main: for multiple instruments (on linse-c) 2025-05-20 15:10:19 +02:00
6390d37ab4 add usual locations of sehistory and frappy to sys.path 2025-05-20 10:30:00 +02:00
925bdfc472 improve command handling 2025-05-13 11:11:19 +02:00
3b438d68b2 fix bug in select_experiment page 2025-05-13 11:11:19 +02:00
a94cc98c42 remove obsolete stuff + small adjustments 2025-05-13 11:11:19 +02:00
2cd78ae1e9 rename files with spaces in doc 2025-05-13 11:11:19 +02:00
a4fda418b2 fix chart config parameters
- add SEA dil pressures
- read config each time when it is used
2025-05-13 11:11:19 +02:00
179db4c0a3 remove some console.log for debug 2025-05-13 11:10:35 +02:00
5c70017238 fix simple command (e.g. stop) 2025-05-13 11:10:27 +02:00
268ebc7e93 lazyPermission: allow to configure starting with writePermission=true 2025-05-13 11:10:14 +02:00
f66b071813 Merge commit '62da014d4041e' 2025-05-13 11:09:38 +02:00
d6a69ba05e Prepare radio-button-group 2025-05-13 11:09:04 +02:00
62da014d40 Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-05-05 10:17:59 +02:00
5c18791c13 Prepare radio-button-group 2025-05-05 10:17:35 +02:00
0f16699323 change value of input element only when opening the edit
updates do not need to update them
(not yet done for enum and checkbox)
2025-05-02 11:35:56 +02:00
f883c913ed Merge branch 'master' of gitlab.psi.ch:samenv/seweb 2025-05-02 11:04:07 +02:00
a229626bdf change value of input element only when opening the edit
updates do not need to update them
(not yet done for enum and checkbox)
2025-05-02 11:02:22 +02:00
0a75a0aa37 Merge branch 'daniel' - 2025-05-02 2025-05-02 11:01:47 +02:00
d2ce97b2ab Fixed some layout issues fpr modules and parameters 2025-05-02 11:01:31 +02:00
06bdbbb02e change value of input element only when opening the edit
updates do not need to update them
(not yet done for enum and checkbox)
2025-05-02 10:56:53 +02:00
d6479a7ece pushbutton changed to link in left column 2025-05-02 10:55:53 +02:00
a8e14eb797 okay-button for input 2025-05-02 10:52:32 +02:00
b704440f36 make write permission check case insensitive 2025-05-02 10:45:48 +02:00
10387261c0 back to (modified) old icon theme 2025-05-02 10:45:15 +02:00
9f2bfd668a Changed some icons 2025-05-02 10:45:09 +02:00
70b40646ef modules-Block: prepared different input options, grid-element: panel-background added -> icon always visible 2025-05-02 10:44:46 +02:00
43624b4222 send instrument='n_a' if not available
instead of null

+ remove another debugging print statement
2025-05-02 10:43:34 +02:00
e51942c97f make sure instrument and device are availabel separately
- added comment to the alert to be modified
  ("You changed a field without pressing the return key.")
2025-05-02 10:43:34 +02:00
ae7ed7bdd8 get a faster response from a change
in the previous implementation, the row-waiting-for-answer
class was removed only on the next poll. this should happen
more quickly.

+ remove some print statements
2025-05-02 10:43:34 +02:00
202095d539 change row-waiting-for-answer color to a light yellow
orangered is too aggressive
2025-05-02 10:42:40 +02:00
cf0a979cc9 add nv.speed to generic.ini 2025-05-02 10:42:40 +02:00
8d38dc31f2 Changed some icons, modules block: different input elements (not tested), write permission promt 2025-05-02 10:42:40 +02:00
9bacb41be8 Global write permission | lock button
+ lock icon added: click -> toggle global write permission
2025-05-02 10:42:40 +02:00
8119f221bd input element display:block instead of float -> displayed in the same row 2025-05-02 10:42:40 +02:00
b9a1e7db99 Changed updateValue... 2025-05-02 10:42:40 +02:00
2fda3164e6 use type=rdonly for modules without target, but with a value 2025-05-02 10:42:40 +02:00
0a6ff13ee6 treat different types for module block 2025-05-02 10:42:40 +02:00
9780ab7097 Changed some icons... 2025-05-02 10:42:08 +02:00
9b7261261f fix do command 2025-05-02 10:42:08 +02:00
f6aff481e2 Fixed some display problems for module and parameter block 2025-05-02 10:42:08 +02:00
e6e69c8f5c Edit button for module block 2025-05-02 10:42:08 +02:00
df582a2f23 console not shown at start, infobox for touch device 2025-05-02 10:41:55 +02:00
8f7406c31b fixes for select_instrument 2025-05-02 10:41:55 +02:00
09bf402bbb implement SECoP commands on the server side 2025-05-02 10:41:55 +02:00
6e20ed0f8f Some bugfixes for input elements 2025-05-02 10:39:18 +02:00
960e95c447 Diverse Anpassungen besonders bei SEAWebClientGroup 2025-05-02 10:39:18 +02:00
27f60e1187 report status and target in update_main
+ add SECoP commands [WIP]
+ fix case 4 (error icon) in updateStatus (SEAWebClientCommunication.js)
2025-05-02 10:39:18 +02:00
5c1c94bffc Änderungen für den Modul- und denParameterblock 2025-05-02 10:39:18 +02:00
5d10b6d48d console, modules 2025-05-02 10:39:18 +02:00
d1ea9225dc status-icon with statuscode, use entire row as link to parameters 2025-05-02 10:39:18 +02:00
7898e375b4 Fixed some layout issues fpr modules and parameters 2025-05-01 15:00:16 +02:00
715381c088 pushbutton changed to link in left column 2025-05-01 14:32:31 +02:00
0542f41ec0 Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-05-01 11:19:11 +02:00
023145ca3d okay-button for input 2025-05-01 11:18:58 +02:00
51c9973c0d make write permission check case insensitive 2025-05-01 08:54:26 +02:00
744184eb54 back to (modified) old icon theme 2025-04-30 18:47:56 +02:00
712ea7bbab Changed some icons 2025-04-30 17:23:06 +02:00
82e044020c Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel
# Conflicts:
#	client/jsFiles/SEAWebClientMain.js
2025-04-30 17:04:49 +02:00
8c03da89d9 modules-Block: prepared different input options, grid-element: panel-background added -> icon always visible 2025-04-30 16:43:57 +02:00
d6ad08025b send instrument='n_a' if not available
instead of null

+ remove another debugging print statement
2025-04-30 14:48:01 +02:00
b0761d9be9 make sure instrument and device are availabel separately
- added comment to the alert to be modified
  ("You changed a field without pressing the return key.")
2025-04-30 14:46:22 +02:00
fe80de4d4b get a faster response from a change
in the previous implementation, the row-waiting-for-answer
class was removed only on the next poll. this should happen
more quickly.

+ remove some print statements
2025-04-30 11:18:18 +02:00
a7e23febfe change row-waiting-for-answer color to a light yellow
orangered is too aggressive
2025-04-30 11:16:39 +02:00
b24db856df add nv.speed to generic.ini 2025-04-30 08:14:16 +02:00
2eaced4283 Changed some icons, modules block: different input elements (not tested), write permission promt 2025-04-27 12:39:26 +02:00
a9ca113f2c Global write permission | lock button
+ lock icon added: click -> toggle global write permission
2025-04-27 09:36:03 +02:00
bd9efaa3de input element display:block instead of float -> displayed in the same row 2025-04-25 18:29:00 +02:00
13e8570afd Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-04-25 17:05:47 +02:00
d15b72fa98 Changed updateValue... 2025-04-25 17:05:27 +02:00
45a90145a7 use type=rdonly for modules without target, but with a value 2025-04-25 16:29:52 +02:00
54d77218cd treat different types for module block 2025-04-25 16:14:38 +02:00
b2d6422f9d Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-04-25 14:13:09 +02:00
d6fd8ad6d0 Changed some icons... 2025-04-25 14:12:09 +02:00
273821e191 fix do command 2025-04-25 13:45:11 +02:00
62c981d396 Fixed some display problems for module and parameter block 2025-04-25 13:12:06 +02:00
a4b9ad17cd Edit button for module block 2025-04-25 13:02:49 +02:00
924116627f Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-04-25 09:55:44 +02:00
654d79461b console not shown at start, infobox for touch device 2025-04-25 09:54:55 +02:00
74e1a84253 fixes for select_instrument 2025-04-25 09:00:47 +02:00
ea21d4e138 implement SECoP commands on the server side 2025-04-25 08:59:58 +02:00
ff0c00cabb Some bugfixes for input elements 2025-04-24 18:12:38 +02:00
0d5ffd72a8 Diverse Anpassungen besonders bei SEAWebClientGroup 2025-04-24 14:50:27 +02:00
58ee8130e6 report status and target in update_main
+ add SECoP commands [WIP]
+ fix case 4 (error icon) in updateStatus (SEAWebClientCommunication.js)
2025-04-22 18:03:24 +02:00
4ad37d5c2f Änderungen für den Modul- und denParameterblock 2025-04-22 14:35:19 +02:00
0ec5672068 console, modules 2025-04-16 18:37:32 +02:00
3be94ba3f6 status-icon with statuscode, use entire row as link to parameters 2025-04-16 13:31:24 +02:00
8747b0e7f8 change status update slightly
- formatted is like "BUSY, moving"
- value as with other tuples
- statuscode 0..4
2025-04-15 18:03:12 +02:00
639949f24b change status update slightly
- formatted is like "BUSY, moving"
- value as with other tuples
- statuscode 0..4
2025-04-15 17:52:22 +02:00
555aca9ed0 add statuscode to update for status
- value in a status update now contains the text only
2025-04-15 17:37:29 +02:00
38b2dbcf93 added component info (from SECoP description)
+ add target update for modules block
2025-04-15 17:37:29 +02:00
33c9896bb1 Another icon 2025-04-15 17:37:29 +02:00
da17309e78 some more icons 2025-04-15 17:37:29 +02:00
9b3f89ddaa Some missing icons 2025-04-15 17:37:29 +02:00
3d37b10d61 Some new icons with logic (not completed yet) 2025-04-15 17:37:29 +02:00
8d668f7de6 Some esponsivity bugfixes 2025-04-15 17:37:29 +02:00
85fdaa445f server: remove unnecessary nesting in SecopInteractor.get_components 2025-04-15 17:37:29 +02:00
e132b43263 fix missing initialization of node_map in SecopInteractor 2025-04-15 17:37:28 +02:00
56f85d9a39 small fixes
- variable homeButton was removed in SEAWebClientMain.js
- add webserver.log to .gitignore
2025-04-15 17:37:28 +02:00
bc9c1361e5 followup change: fix dummy webserver
the dummy server was no longer working after server rework
2025-04-15 17:37:28 +02:00
11a475149a Restored some HTML-Debugging-tools 2025-04-15 17:37:28 +02:00
bd7c1f7406 add statuscode to update for status
- value in a status update now contains the text only
2025-04-15 17:36:37 +02:00
c42d7d8bf8 added component info (from SECoP description)
+ add target update for modules block
2025-04-15 17:36:37 +02:00
385a413870 Another icon 2025-04-15 16:39:51 +02:00
1e876a888f some more icons 2025-04-15 15:53:24 +02:00
40dc6dc7d8 Some missing icons 2025-04-15 15:51:49 +02:00
737e29975a Some new icons with logic (not completed yet) 2025-04-15 10:03:39 +02:00
135802c626 Some esponsivity bugfixes 2025-04-13 10:37:45 +02:00
39be5e8353 Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-03-24 10:32:05 +01:00
e815800cb3 server: remove unnecessary nesting in SecopInteractor.get_components 2025-03-24 10:04:48 +01:00
f6b5141e5b Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-03-24 08:38:12 +01:00
dbf98fdf90 fix missing initialization of node_map in SecopInteractor 2025-03-24 08:33:09 +01:00
1c94abb3ce Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-03-24 06:46:19 +01:00
47213d7576 small fixes
- variable homeButton was removed in SEAWebClientMain.js
- add webserver.log to .gitignore
2025-03-19 11:50:06 +01:00
dbadc4ce0a followup change: fix dummy webserver
the dummy server was no longer working after server rework
2025-03-19 11:47:57 +01:00
2da1798d42 Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-03-19 09:51:19 +01:00
f2b12301ee Restored some HTML-Debugging-tools 2025-03-19 09:50:14 +01:00
958e472f3b rework of the server side
main change: the same server may be used for several instruments

- client classes are 'Interactors' dealing with the parameters etc.
- methods from history are added to the clients
+ improvements on the js client side
2025-03-19 08:44:35 +01:00
9f5ae58b9b rework of the server side
main change: the same server may be used for several instruments

- client classes are 'Interactors' dealing with the parameters etc.
- methods from history are added to the clients
+ improvements on the js client side
2025-03-19 08:37:58 +01:00
b8ac8f8bb5 add the status name to the 'rdlink' message 2025-03-18 14:35:14 +01:00
c67c0f7e9d add 'status' to updates for module block 2025-03-18 14:22:10 +01:00
63a9d65626 Merge branch 'master' into daniel 2025-03-18 14:20:43 +01:00
ea780537d9 fix venv activate path 2025-03-18 14:19:41 +01:00
98c3870b81 merge with master branch 2025-03-18 14:18:55 +01:00
0932d07bf7 another push test 2025-03-18 14:16:02 +01:00
5b6684fcef add select_experiment 2025-03-18 14:15:14 +01:00
2c59e37074 major rework
using influxdb structure independed of nicos cache
2025-03-18 14:14:45 +01:00
9fde940c8a Removed obsolete files 2025-03-18 11:42:53 +01:00
8dc8839bf0 Swiper removed
- Swiper removal work in progress
+ Fine debugging for every js-file
2025-03-18 11:42:36 +01:00
7aef895462 Removed obsolete files 2025-03-17 16:17:41 +01:00
12f78582c0 Merge branch 'daniel' of https://gitlab.psi.ch/samenv/seweb into daniel 2025-03-17 16:13:36 +01:00
b036456517 another push test 2025-03-17 13:33:32 +01:00
c87d08d39f push test 2025-03-17 13:23:39 +01:00
4691dbcf0f push test 2025-03-17 13:19:23 +01:00
ca2945ac22 Swiper removed
- Swiper removal work in progress
+ Fine debugging for every js-file
2025-03-17 11:07:50 +01:00
9ec122a146 add select_experiment 2025-02-27 10:41:17 +01:00
65aa822b96 major rework
using influxdb structure independed of nicos cache
2025-02-25 14:29:25 +01:00
680434e5e8 fix activate command 2024-11-05 17:04:39 +01:00
3d2346f632 update spec 2024-10-25 11:17:48 +02:00
5017c5feb6 fix typo 2024-10-25 09:42:04 +02:00
24fa8f8459 Update instructions_specifications.md 2024-10-25 09:22:57 +02:00
a22b70d750 make instructions more nice
- use english consequently (this avoid umlauts...)
- adapt better to markdown syntax
2024-10-25 09:18:41 +02:00
78cadd7bef instructions and specifications for daniels work 2024-10-25 08:53:33 +02:00
279a658e8a update README: *-webserver script, rename to 'seweb' 2024-10-25 08:13:14 +02:00
e339ef711f modify notes about Chart.js migration 2024-10-25 07:50:31 +02:00
66bc1c99db add requirements 2024-10-23 16:36:26 +02:00
7d536c187b add dummy webserver 2024-10-23 16:35:59 +02:00
dc887e68d5 move get_abs_time to base.py
+ initialize Instrument().clients
2024-10-23 16:34:55 +02:00
7736e0f7e3 move classes using influx from secop to influxgraph
+ fix parameter change command in secop
2024-10-23 16:34:12 +02:00
7471a0c171 remove pdb support, remove unused imports
+ remove tcp_lineserver
2024-10-23 16:31:34 +02:00
9cee5ad9bf add secop-webserver binary 2024-10-23 13:53:54 +02:00
b07ca0bd4f restructure code and use frappy.client for SECoP
- separation of SECoP part from webserver
- use frappy client instead of own code for SECoP communication
2024-10-23 11:13:40 +02:00
b708197d27 rework updateValues
+ remove "SEA " from page title
2024-09-27 14:48:08 +02:00
415d4c86f6 rework SECoP: redesign message class, fix command reply 2024-09-27 14:25:11 +02:00
cd306c45ac fix URI encoding and let SECoP module show value
- URI encoding must happen by element
- show values in SECoP module
2024-09-27 13:47:59 +02:00
09c3a5840a add more items to generic.ini 2024-09-27 11:05:07 +02:00
15ecaca93e seaweb.py: JSONencode, SECoP requests, terminate
- make class SecopMsg json encodable
- fix handling of SECoP requests
- terminate properly on SIGTERM
2024-09-27 11:02:15 +02:00
e71cb74391 improve graphics layout
- 'bevel' line style prohibits 'peaks' on locally noisy values
- fix option 'stepped' instead of 'steppedLine'
- set period to 0 for target parameters
- thinker lines, adjustable by a constant
2024-09-27 10:57:45 +02:00
7184d28047 fix wrongly triggered message after a parameter change
after pressing the return key, no message should appear like
'You changed a field without pressing the return key'
2024-09-27 10:53:12 +02:00
2e21f52071 fix in communication: use encodeURI
this is needed e.g. when command socntain blanks
2024-09-27 10:49:57 +02:00
69ea17aec6 allow multiple SecNode connections
+ change layout when console is not to be shown
+ catch file not found error
2024-09-25 16:33:28 +02:00
96bcd67dc4 change icon 2024-09-25 14:50:58 +02:00
6440daaef2 make secop work 2024-09-25 14:50:43 +02:00
664e53d58c fix legend values when value is undefined
+ improve mechanism to find value
2024-09-25 14:16:52 +02:00
6f318f26b7 fix zoom mode
+ update README.md
2024-09-25 14:16:52 +02:00
9ed6f75ace fix setting cursor when clicked
cursor should not be set when dragging (pan)
2024-09-25 14:16:52 +02:00
91c1f7c3bb show time labels only on first chart 2024-09-25 14:16:52 +02:00
6edb926bf8 improve time tick labels more 2024-09-25 14:16:52 +02:00
b919c60000 improve time axis labels
+ change poll interval to 1 sec
2024-09-25 14:16:50 +02:00
48b89a9801 fix mechanism to strip overlapping data for live update
fix bug when when there was no last value yet
2024-09-25 14:15:37 +02:00
66e8d431a9 Doc + working live 2024-09-25 14:15:37 +02:00
a2ad485402 Autoscale considers new ChartJS format, data reloads onzoom and onpan 2024-09-25 14:15:37 +02:00
6c1a13c382 Zoom and pan are synchronized, changed afterBuildTicksSignature 2024-09-25 14:15:34 +02:00
05a77ce2f4 initial test + libraries 2024-09-25 14:14:41 +02:00
9105a5cb41 5 sec update frequency 2024-09-13 17:07:24 +02:00
89 changed files with 3099 additions and 40187 deletions

3
.gitignore vendored
View File

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

View File

@ -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
View 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"]

View File

@ -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

View File

@ -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">&#215;</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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
client/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

20
client/externalFiles/chart.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View 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()}})}));

File diff suppressed because one or more lines are too long

View 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;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
client/externalFiles/luxon.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
client/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

BIN
client/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -10,9 +10,9 @@ var timeoutID; // We need this ID to reset the timer every 30 seconds
function buildUpdateConnection() {
// 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);

View File

@ -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 = "";
};

View File

@ -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>&#9746;</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

View File

@ -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);
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

BIN
client/res/icon_close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
client/res/icon_edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
client/res/icon_height.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
client/res/icon_info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
client/res/icon_log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
client/res/icon_modules.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
client/res/icon_okay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
client/res/icon_sinus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
client/res/icon_status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
client/res/icon_width.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

9
client/site.webmanifest Normal file
View 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"}

View File

@ -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)

View File

@ -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

View File

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

View File

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

32
dummy-webserver Executable file
View 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
View 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'}}

View File

@ -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

View File

@ -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)

View 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
View File

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

View File

@ -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

1115
seaweb.py

File diff suppressed because it is too large Load Diff

44
secop-webserver Executable file
View 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
View 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"]

View File

@ -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
View 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>&nbsp;\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>&nbsp;\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]))